Add --base to agentpr pr edit to retarget a PR
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user