Skip to content

Commit d07c2be

Browse files
committed
support adopting branches with PRs
1 parent 2821962 commit d07c2be

File tree

2 files changed

+111
-21
lines changed

2 files changed

+111
-21
lines changed

cmd/init.go

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -174,25 +174,6 @@ func runInit(cfg *config.Config, opts *initOptions) error {
174174
}
175175
}
176176
branches = opts.branches
177-
178-
// Check if any adopted branches already have PRs on GitHub.
179-
// If offline or unable to create client, skip silently.
180-
if client, clientErr := cfg.GitHubClient(); clientErr == nil {
181-
for _, b := range branches {
182-
pr, err := client.FindAnyPRForBranch(b)
183-
if err != nil {
184-
continue
185-
}
186-
if pr != nil {
187-
state := "open"
188-
if pr.Merged {
189-
state = "merged"
190-
}
191-
cfg.Errorf("branch %q already has a %s PR (#%d: %s)", b, state, pr.Number, pr.URL)
192-
return ErrInvalidArgs
193-
}
194-
}
195-
}
196177
} else if len(opts.branches) > 0 {
197178
// Explicit branch names provided — apply prefix and create them
198179
prefixed := make([]string, 0, len(opts.branches))
@@ -323,8 +304,28 @@ func runInit(cfg *config.Config, opts *initOptions) error {
323304

324305
sf.AddStack(newStack)
325306

326-
// Sync PR state for adopted branches
327-
syncStackPRs(cfg, &sf.Stacks[len(sf.Stacks)-1])
307+
// Discover existing PRs for the new stack's branches.
308+
// For adopt, only record open/draft PRs (ignore closed/merged).
309+
// For non-adopt, use the standard sync which also detects merges.
310+
newStack_ := &sf.Stacks[len(sf.Stacks)-1]
311+
if opts.adopt {
312+
if client, clientErr := cfg.GitHubClient(); clientErr == nil {
313+
for i := range newStack_.Branches {
314+
b := &newStack_.Branches[i]
315+
pr, err := client.FindPRForBranch(b.Branch)
316+
if err != nil || pr == nil {
317+
continue
318+
}
319+
b.PullRequest = &stack.PullRequestRef{
320+
Number: pr.Number,
321+
ID: pr.ID,
322+
URL: pr.URL,
323+
}
324+
}
325+
}
326+
} else {
327+
syncStackPRs(cfg, newStack_)
328+
}
328329

329330
if err := stack.Save(gitDir, sf); err != nil {
330331
return handleSaveError(cfg, err)

cmd/init_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/github/gh-stack/internal/config"
1010
"github.com/github/gh-stack/internal/git"
11+
"github.com/github/gh-stack/internal/github"
1112
"github.com/github/gh-stack/internal/stack"
1213
"github.com/stretchr/testify/assert"
1314
"github.com/stretchr/testify/require"
@@ -290,3 +291,91 @@ func TestInit_MultipleBranches_CreatesAll(t *testing.T) {
290291
names := sf.Stacks[0].BranchNames()
291292
assert.Equal(t, []string{"b1", "b2", "b3"}, names)
292293
}
294+
295+
func TestInit_AdoptWithExistingOpenPR(t *testing.T) {
296+
gitDir := t.TempDir()
297+
restore := git.SetOps(&git.MockOps{
298+
GitDirFn: func() (string, error) { return gitDir, nil },
299+
DefaultBranchFn: func() (string, error) { return "main", nil },
300+
CurrentBranchFn: func() (string, error) { return "main", nil },
301+
BranchExistsFn: func(string) bool { return true },
302+
})
303+
defer restore()
304+
305+
cfg, outR, errR := config.NewTestConfig()
306+
cfg.GitHubClientOverride = &github.MockClient{
307+
FindPRForBranchFn: func(branch string) (*github.PullRequest, error) {
308+
if branch == "b1" {
309+
return &github.PullRequest{
310+
Number: 42,
311+
ID: "PR_42",
312+
URL: "https://github.com/owner/repo/pull/42",
313+
State: "OPEN",
314+
HeadRefName: "b1",
315+
}, nil
316+
}
317+
return nil, nil
318+
},
319+
}
320+
321+
err := runInit(cfg, &initOptions{
322+
branches: []string{"b1", "b2"},
323+
adopt: true,
324+
})
325+
output := collectOutput(cfg, outR, errR)
326+
327+
require.NoError(t, err, "adopt should succeed even when branch has an open PR")
328+
require.NotContains(t, output, "\u2717", "unexpected error in output")
329+
330+
sf, err := stack.Load(gitDir)
331+
require.NoError(t, err, "loading stack")
332+
require.Len(t, sf.Stacks, 1)
333+
334+
// b1 should have the open PR recorded
335+
b1 := sf.Stacks[0].Branches[0]
336+
require.NotNil(t, b1.PullRequest, "open PR should be recorded")
337+
assert.Equal(t, 42, b1.PullRequest.Number)
338+
assert.Equal(t, "https://github.com/owner/repo/pull/42", b1.PullRequest.URL)
339+
340+
// b2 should have no PR
341+
b2 := sf.Stacks[0].Branches[1]
342+
assert.Nil(t, b2.PullRequest, "branch without PR should have nil PullRequest")
343+
}
344+
345+
func TestInit_AdoptIgnoresClosedAndMergedPRs(t *testing.T) {
346+
gitDir := t.TempDir()
347+
restore := git.SetOps(&git.MockOps{
348+
GitDirFn: func() (string, error) { return gitDir, nil },
349+
DefaultBranchFn: func() (string, error) { return "main", nil },
350+
CurrentBranchFn: func() (string, error) { return "main", nil },
351+
BranchExistsFn: func(string) bool { return true },
352+
})
353+
defer restore()
354+
355+
cfg, outR, errR := config.NewTestConfig()
356+
// FindPRForBranch only returns OPEN PRs — closed/merged PRs won't be
357+
// returned by the API, so the mock returns nil for all branches.
358+
cfg.GitHubClientOverride = &github.MockClient{
359+
FindPRForBranchFn: func(branch string) (*github.PullRequest, error) {
360+
return nil, nil
361+
},
362+
}
363+
364+
err := runInit(cfg, &initOptions{
365+
branches: []string{"b1", "b2"},
366+
adopt: true,
367+
})
368+
output := collectOutput(cfg, outR, errR)
369+
370+
require.NoError(t, err, "adopt should succeed when branches have closed/merged PRs")
371+
require.NotContains(t, output, "\u2717", "unexpected error in output")
372+
373+
sf, err := stack.Load(gitDir)
374+
require.NoError(t, err, "loading stack")
375+
require.Len(t, sf.Stacks, 1)
376+
377+
// Neither branch should have a PR recorded (closed/merged are filtered out)
378+
for _, b := range sf.Stacks[0].Branches {
379+
assert.Nil(t, b.PullRequest, "closed/merged PRs should not be recorded for branch %s", b.Branch)
380+
}
381+
}

0 commit comments

Comments
 (0)