2 Commits

Author SHA1 Message Date
benvin 836fcc0aa5 Merge pull request 'Add --base to agentpr pr edit' (#26) from benvin/agentpr-edit-base into main
ci/woodpecker/tag/release Pipeline was successful
Reviewed-on: #26
2026-10-17 21:54:02 +11:00
unkin-agent 4ef8f458e2 Add --base to agentpr pr edit to retarget a PR
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
2026-10-17 12:52:36 +11:00
5 changed files with 45 additions and 11 deletions
+2 -1
View File
@@ -51,9 +51,10 @@ agentpr pr create --repo unkin/argocd-apps \
# Comment on a PR
agentpr pr comment --repo unkin/argocd-apps --pr 42 --body "Rebased, CI green."
# Edit a PR's title and/or body; an omitted flag is left unchanged
# Edit a PR's title, body and/or base branch; an omitted flag is left unchanged
agentpr pr edit --repo unkin/argocd-apps --pr 42 --body "Adds the ServiceAccount ..."
agentpr pr edit --repo unkin/argocd-apps --pr 42 --title "Add woodpecker SA"
agentpr pr edit --repo unkin/argocd-apps --pr 42 --base benvin/parent-change # retarget a stacked PR
# prints: #<number> <html_url>
# File an issue (--body optional)
+14 -4
View File
@@ -5,7 +5,7 @@
//
// agentpr pr create --repo owner/repo --base main --head feature --title T --body B
// agentpr pr comment --repo owner/repo --pr 12 --body "..."
// agentpr pr edit --repo owner/repo --pr 12 --title T --body B
// agentpr pr edit --repo owner/repo --pr 12 --title T --body B --base B
// agentpr issue create --repo owner/repo --title T --body B
// agentpr issue comment --repo owner/repo --issue 12 --body "..."
// agentpr issue edit --repo owner/repo --issue 12 --title T --body B
@@ -169,11 +169,11 @@ func newCommentCmd(numFlag, noun, short string) *cobra.Command {
}
func newPREditCmd() *cobra.Command {
var repo, title, body string
var repo, title, body, base string
var pr int
cmd := &cobra.Command{
Use: "edit",
Short: "Edit a pull request's title and/or body",
Short: "Edit a pull request's title, body and/or base branch",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
owner, name, err := agent.ParseRepo(repo)
@@ -187,6 +187,12 @@ func newPREditCmd() *cobra.Command {
if err != nil {
return err
}
if cmd.Flags().Changed("base") {
if base == "" {
return fmt.Errorf("--base cannot be empty")
}
opts.Base = &base
}
c, err := client()
if err != nil {
return err
@@ -204,6 +210,7 @@ func newPREditCmd() *cobra.Command {
f.IntVar(&pr, "pr", 0, "PR number (required)")
f.StringVar(&title, "title", "", "New PR title (unchanged when omitted)")
f.StringVar(&body, "body", "", "New PR body (unchanged when omitted)")
f.StringVar(&base, "base", "", "New base branch (unchanged when omitted)")
_ = cmd.MarkFlagRequired("repo")
_ = cmd.MarkFlagRequired("pr")
return cmd
@@ -225,7 +232,10 @@ func editOptions(cmd *cobra.Command, title, body string) (agent.EditOptions, err
if cmd.Flags().Changed("body") {
opts.Body = &body
}
if opts.Title == nil && opts.Body == nil {
if opts.Title == nil && opts.Body == nil && !cmd.Flags().Changed("base") {
if cmd.Flags().Lookup("base") != nil {
return opts, fmt.Errorf("at least one of --title, --body or --base is required")
}
return opts, fmt.Errorf("at least one of --title or --body is required")
}
return opts, nil
+22 -3
View File
@@ -20,7 +20,7 @@ func TestExecuteBadRepoErrors(t *testing.T) {
}
}
// `pr edit` with neither --title nor --body has nothing to send; it must fail
// `pr edit` with none of --title, --body or --base has nothing to send; it must fail
// with a usage error before any Vault/Gitea call, so this stays hermetic.
func TestPREditRequiresTitleOrBody(t *testing.T) {
cmd := newRootCmd()
@@ -29,13 +29,32 @@ func TestPREditRequiresTitleOrBody(t *testing.T) {
cmd.SetErr(io.Discard)
err := cmd.Execute()
if err == nil {
t.Fatal("Execute() = nil, want an error when neither --title nor --body is given")
t.Fatal("Execute() = nil, want an error when no edit flag is given")
}
if !strings.Contains(err.Error(), "--title or --body") {
if !strings.Contains(err.Error(), "--title, --body or --base") {
t.Errorf("error = %q, want it to name the missing flags", err)
}
}
// An empty --base names no branch to retarget to; it must fail before any
// Vault/Gitea call rather than be sent.
func TestPREditRejectsEmptyBase(t *testing.T) {
err := execute("pr", "edit", "--repo", "unkin/repo", "--pr", "7", "--base", "")
if err == nil {
t.Fatal("Execute() = nil, want an error for an empty --base")
}
if !strings.Contains(err.Error(), "--base cannot be empty") {
t.Errorf("error = %q, want it to reject the empty base", err)
}
}
// issue edit has no --base, so its usage error must not suggest one.
func TestIssueEditHasNoBaseFlag(t *testing.T) {
if err := execute("issue", "edit", "--repo", "unkin/repo", "--issue", "12", "--base", "x"); err == nil {
t.Fatal("Execute() = nil, want an unknown-flag error for issue edit --base")
}
}
// Gitea silently ignores an empty title, so `pr edit --title ""` would report
// success while changing nothing; it must fail before any Vault/Gitea call.
func TestPREditRejectsEmptyTitle(t *testing.T) {
+3 -1
View File
@@ -102,7 +102,7 @@ func TestCreatePRRequestBody(t *testing.T) {
// An edit must send only the fields it was given: Gitea overwrites whatever
// key it receives, so an omitted --title arriving as "" would blank the title.
func TestEditPRSendsOnlySuppliedFields(t *testing.T) {
title, body, empty := "new title", "new body", ""
title, body, base, empty := "new title", "new body", "benvin/parent", ""
tests := []struct {
name string
opts EditOptions
@@ -112,6 +112,8 @@ func TestEditPRSendsOnlySuppliedFields(t *testing.T) {
{"title only", EditOptions{Title: &title}, map[string]any{"title": "new title"}},
{"both", EditOptions{Title: &title, Body: &body}, map[string]any{"title": "new title", "body": "new body"}},
{"explicit empty body is sent", EditOptions{Body: &empty}, map[string]any{"body": ""}},
{"base only", EditOptions{Base: &base}, map[string]any{"base": "benvin/parent"}},
{"base with title", EditOptions{Title: &title, Base: &base}, map[string]any{"title": "new title", "base": "benvin/parent"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+4 -2
View File
@@ -283,13 +283,15 @@ func (c *GiteaClient) CreatePR(repoPath string, opts CreatePROptions) (PullReque
// entirely, leaving that field as it is. The two fields are not symmetric:
// Gitea only applies a title when it is non-empty, so Title can be set but
// never cleared and a "" title is a silent no-op, while a pointer to "" Body
// really does blank the body.
// really does blank the body. Base retargets a pull request and is never set
// for an issue.
type EditOptions struct {
Title *string `json:"title,omitempty"`
Body *string `json:"body,omitempty"`
Base *string `json:"base,omitempty"`
}
// EditPR updates a pull request's title and/or body
// EditPR updates a pull request's title, body and/or base branch
// (PATCH /api/v1/repos/{owner}/{repo}/pulls/{index}).
func (c *GiteaClient) EditPR(repoPath string, number int, opts EditOptions) (PullRequest, error) {
var pr PullRequest