Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,15 +358,15 @@ When creating new PRs, you will be prompted to enter a title for each one. Press
| Flag | Description |
|------|-------------|
| `--auto` | Use auto-generated PR titles without prompting |
| `--draft` | Create new PRs as drafts |
| `--open` | Mark new and existing PRs as ready for review |
| `--remote <name>` | Remote to push to (defaults to auto-detected remote) |

**Examples:**

```sh
gh stack submit
gh stack submit --auto
gh stack submit --draft
gh stack submit --open
```

### `gh stack link`
Expand All @@ -386,7 +386,7 @@ If the PRs are not yet in a stack, a new stack is created. If some of the PRs ar
| Flag | Description |
|------|-------------|
| `--base <branch>` | Base branch for the bottom of the stack (default: `main`) |
| `--draft` | Create new PRs as drafts |
| `--open` | Mark new and existing PRs as ready for review |
| `--remote <name>` | Remote to push to (defaults to auto-detected remote) |

**Examples:**
Expand All @@ -401,8 +401,8 @@ gh stack link 10 20 30
# Add branches to an existing stack of PRs
gh stack link 42 43 feature-auth feature-ui

# Use a different base branch and create PRs as drafts
gh stack link --base develop --draft feat-a feat-b feat-c
# Use a different base branch and mark PRs as ready for review
gh stack link --base develop --open feat-a feat-b feat-c
```

### `gh stack view`
Expand Down
17 changes: 14 additions & 3 deletions cmd/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (

type linkOptions struct {
base string
draft bool
open bool
remote string
}

Expand Down Expand Up @@ -51,7 +51,7 @@ the new PRs (existing PRs are never removed).`,
}

cmd.Flags().StringVar(&opts.base, "base", "main", "Base branch for the bottom of the stack")
cmd.Flags().BoolVar(&opts.draft, "draft", false, "Create new PRs as drafts")
cmd.Flags().BoolVar(&opts.open, "open", false, "Mark new and existing PRs as ready for review")
cmd.Flags().StringVar(&opts.remote, "remote", "", "Remote to push to (defaults to auto-detected remote)")
Comment thread
skarim marked this conversation as resolved.

return cmd
Expand Down Expand Up @@ -321,7 +321,7 @@ func createMissingPRs(cfg *config.Config, client github.ClientOps, opts *linkOpt
title := humanize(arg)
body := generatePRBody("")

newPR, err := client.CreatePR(baseBranch, arg, title, body, opts.draft)
newPR, err := client.CreatePR(baseBranch, arg, title, body, !opts.open)
if err != nil {
cfg.Errorf("failed to create PR for branch %s: %v", arg, err)
return nil, ErrAPIFailure
Expand Down Expand Up @@ -375,6 +375,17 @@ func fixBaseBranches(cfg *config.Config, client github.ClientOps, opts *linkOpti
cfg.PRLink(r.prNumber, r.prURL), expectedBase)
}
}

// Convert draft PR to ready for review when --open is set.
if opts.open && pr.IsDraft {
if err := client.MarkPRReadyForReview(pr.ID); err != nil {
cfg.Warningf("failed to mark PR %s as ready for review: %v",
cfg.PRLink(r.prNumber, r.prURL), err)
} else {
cfg.Successf("Marked PR %s as ready for review",
cfg.PRLink(r.prNumber, r.prURL))
}
}
}
}

Expand Down
113 changes: 110 additions & 3 deletions cmd/link_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ func TestLink_BranchNames_AllNeedPRs(t *testing.T) {
assert.Contains(t, output, "Created stack with 3 PRs")
}

func TestLink_BranchNames_DraftFlag(t *testing.T) {
func TestLink_BranchNames_DefaultDraft(t *testing.T) {
restore := git.SetOps(newLinkGitMock("feat-a", "feat-b"))
defer restore()

Expand Down Expand Up @@ -512,13 +512,120 @@ func TestLink_BranchNames_DraftFlag(t *testing.T) {
}

cmd := LinkCmd(cfg)
cmd.SetArgs([]string{"--draft", "feat-a", "feat-b"})
cmd.SetArgs([]string{"feat-a", "feat-b"})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
err := cmd.Execute()

assert.NoError(t, err)
assert.True(t, createdDraft, "PRs should be created as drafts by default")
}

func TestLink_BranchNames_OpenFlag(t *testing.T) {
restore := git.SetOps(newLinkGitMock("feat-a", "feat-b"))
defer restore()

var createdDraft bool
prCounter := 0

cfg, _, _ := config.NewTestConfig()
cfg.GitHubClientOverride = &github.MockClient{
FindPRForBranchFn: func(branch string) (*github.PullRequest, error) {
return nil, nil
},
FindPRByNumberFn: func(n int) (*github.PullRequest, error) {
heads := map[int]string{1: "feat-a", 2: "feat-b"}
bases := map[int]string{1: "main", 2: "feat-a"}
if h, ok := heads[n]; ok {
return &github.PullRequest{Number: n, HeadRefName: h, BaseRefName: bases[n]}, nil
}
return nil, nil
},
CreatePRFn: func(base, head, title, body string, draft bool) (*github.PullRequest, error) {
createdDraft = draft
prCounter++
return &github.PullRequest{
Number: prCounter, HeadRefName: head, BaseRefName: base,
URL: fmt.Sprintf("https://github.com/o/r/pull/%d", prCounter),
}, nil
},
ListStacksFn: func() ([]github.RemoteStack, error) {
return []github.RemoteStack{}, nil
},
CreateStackFn: func([]int) (int, error) { return 1, nil },
}

cmd := LinkCmd(cfg)
cmd.SetArgs([]string{"--open", "feat-a", "feat-b"})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
err := cmd.Execute()

assert.NoError(t, err)
assert.False(t, createdDraft, "PRs should not be created as drafts when --open is set")
}

func TestLink_OpenFlag_ConvertsDraftPRs(t *testing.T) {
restore := git.SetOps(newLinkGitMock("feat-a", "feat-b"))
defer restore()

var markedReady []string

cfg, _, errR := config.NewTestConfig()
cfg.GitHubClientOverride = &github.MockClient{
FindPRForBranchFn: func(branch string) (*github.PullRequest, error) {
switch branch {
case "feat-a":
return &github.PullRequest{
Number: 1, ID: "PR_1", HeadRefName: "feat-a", BaseRefName: "main",
IsDraft: true, URL: "https://github.com/o/r/pull/1",
}, nil
case "feat-b":
return &github.PullRequest{
Number: 2, ID: "PR_2", HeadRefName: "feat-b", BaseRefName: "feat-a",
IsDraft: true, URL: "https://github.com/o/r/pull/2",
}, nil
}
return nil, nil
},
FindPRByNumberFn: func(n int) (*github.PullRequest, error) {
switch n {
case 1:
return &github.PullRequest{
Number: 1, ID: "PR_1", HeadRefName: "feat-a", BaseRefName: "main",
IsDraft: true, URL: "https://github.com/o/r/pull/1",
}, nil
case 2:
return &github.PullRequest{
Number: 2, ID: "PR_2", HeadRefName: "feat-b", BaseRefName: "feat-a",
IsDraft: true, URL: "https://github.com/o/r/pull/2",
}, nil
}
return nil, nil
},
MarkPRReadyForReviewFn: func(prID string) error {
markedReady = append(markedReady, prID)
return nil
},
ListStacksFn: func() ([]github.RemoteStack, error) {
return []github.RemoteStack{}, nil
},
CreateStackFn: func([]int) (int, error) { return 1, nil },
}

cmd := LinkCmd(cfg)
cmd.SetArgs([]string{"--open", "feat-a", "feat-b"})
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
err := cmd.Execute()

cfg.Err.Close()
errOut, _ := io.ReadAll(errR)
output := string(errOut)

assert.NoError(t, err)
assert.True(t, createdDraft, "PRs should be created as drafts when --draft is set")
assert.Equal(t, []string{"PR_1", "PR_2"}, markedReady, "both draft PRs should be marked ready")
assert.Contains(t, output, "Marked PR")
}

func TestLink_MixedArgs_PRNumberAndBranch(t *testing.T) {
Expand Down
17 changes: 14 additions & 3 deletions cmd/submit.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (

type submitOptions struct {
auto bool
draft bool
open bool
remote string
}

Expand All @@ -34,7 +34,7 @@ func SubmitCmd(cfg *config.Config) *cobra.Command {
}

cmd.Flags().BoolVar(&opts.auto, "auto", false, "Use auto-generated PR titles without prompting")
cmd.Flags().BoolVar(&opts.draft, "draft", false, "Create PRs as drafts")
cmd.Flags().BoolVar(&opts.open, "open", false, "Mark new and existing PRs as ready for review")
cmd.Flags().StringVar(&opts.remote, "remote", "", "Remote to push to (defaults to auto-detected remote)")
Comment thread
skarim marked this conversation as resolved.

return cmd
Expand Down Expand Up @@ -235,6 +235,17 @@ func ensurePR(cfg *config.Config, client github.ClientOps, s *stack.Stack, i int
cfg.Printf("PR %s for %s is up to date", cfg.PRLink(pr.Number, pr.URL), b.Branch)
}

// Convert draft PR to ready for review when --open is set.
if opts.open && pr.IsDraft {
if err := client.MarkPRReadyForReview(pr.ID); err != nil {
cfg.Warningf("failed to mark PR %s as ready for review: %v",
cfg.PRLink(pr.Number, pr.URL), err)
} else {
cfg.Successf("Marked PR %s as ready for review",
cfg.PRLink(pr.Number, pr.URL))
}
}

return nil
}

Expand Down Expand Up @@ -263,7 +274,7 @@ func createPR(cfg *config.Config, client github.ClientOps, s *stack.Stack, i int
}
body := generatePRBody(prBody)

newPR, createErr := client.CreatePR(baseBranch, b.Branch, title, body, opts.draft)
newPR, createErr := client.CreatePR(baseBranch, b.Branch, title, body, !opts.open)
if createErr != nil {
cfg.Warningf("failed to create PR for %s: %v", b.Branch, createErr)
return nil
Expand Down
Loading
Loading