Skip to content

Commit f436da4

Browse files
committed
fix: revert "feat: auto-detect parent branches for untracked local branches (#53)"
This reverts commit 0f62bdf.
1 parent 65dc9a5 commit f436da4

File tree

15 files changed

+36
-1292
lines changed

15 files changed

+36
-1292
lines changed

README.md

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -168,16 +168,12 @@ By default, `init` auto-detects the trunk branch (`main` or `master`). If neithe
168168

169169
Display the branch tree showing the stack hierarchy, current branch, and associated PR numbers.
170170

171-
By default, `log` also shows untracked local branches in the tree if it can detect their likely parent via merge-base analysis. These "detected" branches are annotated but not persisted. Use `--no-detect` to disable this.
172-
173-
Detection is automatically skipped in `--porcelain` mode since the porcelain format has no column to distinguish detected branches from tracked ones.
174-
175171
#### log Flags
176172

177-
| Flag | Description |
178-
| --------------- | ---------------------------------------------- |
179-
| `--porcelain` | Machine-readable tab-separated output |
180-
| `--no-detect` | Skip auto-detection of untracked branches |
173+
| Flag | Description |
174+
| ------------- | ------------------------------------- |
175+
| `--all` | Show all branches |
176+
| `--porcelain` | Machine-readable tab-separated output |
181177

182178
#### Porcelain Format
183179

@@ -212,12 +208,10 @@ Start tracking an existing branch by setting its parent.
212208

213209
By default, adopts the current branch. The parent must be either the trunk or another tracked branch.
214210

215-
When no parent is specified, `adopt` auto-detects the parent using PR base branch data (if available) and local merge-base analysis. If the result is ambiguous in interactive mode, you'll be prompted to choose; in non-interactive mode an error is returned.
216-
217211
#### adopt Usage
218212

219213
```bash
220-
gh stack adopt [parent]
214+
gh stack adopt <parent>
221215
```
222216

223217
#### adopt Flags
@@ -302,12 +296,11 @@ If a rebase conflict occurs, resolve it and run `gh stack continue`.
302296

303297
#### restack Flags
304298

305-
| Flag | Description |
306-
| --------------- | -------------------------------------------------------- |
307-
| `--only` | Only restack current branch, not descendants |
308-
| `--dry-run` | Show what would be done |
309-
| `--worktrees` | Rebase branches checked out in linked worktrees in-place |
310-
| `--no-detect` | Skip auto-detection and adoption of untracked branches |
299+
| Flag | Description |
300+
| ------------- | -------------------------------------------------------- |
301+
| `--only` | Only restack current branch, not descendants |
302+
| `--dry-run` | Show what would be done |
303+
| `--worktrees` | Rebase branches checked out in linked worktrees in-place |
311304

312305
### continue
313306

@@ -334,7 +327,6 @@ This is the command to run when upstream changes have occurred (e.g., a PR in yo
334327
| `--no-restack` | Skip restacking branches |
335328
| `--dry-run` | Show what would be done |
336329
| `--worktrees` | Rebase branches checked out in linked worktrees in-place |
337-
| `--no-detect` | Skip auto-detection and adoption of untracked branches |
338330

339331
### undo
340332

cmd/adopt.go

Lines changed: 20 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,19 @@ import (
77
"os"
88

99
"github.com/boneskull/gh-stack/internal/config"
10-
"github.com/boneskull/gh-stack/internal/detect"
1110
"github.com/boneskull/gh-stack/internal/git"
12-
"github.com/boneskull/gh-stack/internal/github"
13-
"github.com/boneskull/gh-stack/internal/prompt"
1411
"github.com/boneskull/gh-stack/internal/style"
12+
"github.com/boneskull/gh-stack/internal/tree"
1513
"github.com/spf13/cobra"
1614
)
1715

1816
var adoptCmd = &cobra.Command{
19-
Use: "adopt [parent]",
17+
Use: "adopt <parent>",
2018
Short: "Start tracking an existing branch",
2119
Long: `Start tracking an existing branch by setting its parent.
2220
23-
When no parent is specified, the parent is auto-detected using PR base branch
24-
and merge-base analysis. If the result is ambiguous, you will be prompted to
25-
choose (interactive) or an error is returned (non-interactive).
26-
2721
By default, adopts the current branch. Use --branch to specify a different branch.`,
28-
Args: cobra.MaximumNArgs(1),
22+
Args: cobra.ExactArgs(1),
2923
RunE: runAdopt,
3024
}
3125

@@ -48,7 +42,9 @@ func runAdopt(cmd *cobra.Command, args []string) error {
4842
}
4943

5044
g := git.New(cwd)
51-
s := style.New()
45+
46+
// Parent is the required positional argument
47+
parent := args[0]
5248

5349
// Determine branch to adopt (from flag or current branch)
5450
var branchName string
@@ -77,62 +73,25 @@ func runAdopt(cmd *cobra.Command, args []string) error {
7773
return err
7874
}
7975

80-
var parent string
81-
var detectedPRNumber int
82-
83-
if len(args) > 0 {
84-
// Explicit parent provided
85-
parent = args[0]
86-
} else {
87-
// Auto-detect parent
88-
tracked, listErr := cfg.ListTrackedBranches()
89-
if listErr != nil {
90-
return fmt.Errorf("list tracked branches: %w", listErr)
91-
}
92-
93-
// Try to get GitHub client (may fail if no auth -- that's ok)
94-
gh, _ := github.NewClient() //nolint:errcheck // nil client is fine for local-only detection
95-
96-
result, detectErr := detect.DetectParent(branchName, tracked, trunk, g, gh)
97-
if detectErr != nil {
98-
return fmt.Errorf("auto-detect parent: %w", detectErr)
99-
}
100-
101-
switch result.Confidence {
102-
case detect.High, detect.Medium:
103-
parent = result.Parent
104-
fmt.Printf("%s Detected parent %s %s\n",
105-
s.SuccessIcon(), s.Branch(parent), s.Muted("("+result.Confidence.String()+" confidence)"))
106-
case detect.Ambiguous:
107-
if len(result.Candidates) == 0 {
108-
return fmt.Errorf("could not detect parent for %s; specify one explicitly", s.Branch(branchName))
109-
}
110-
if !prompt.IsInteractive() {
111-
return fmt.Errorf("ambiguous parent for %s (candidates: %v); specify one explicitly",
112-
s.Branch(branchName), result.Candidates)
113-
}
114-
idx, promptErr := prompt.Select(
115-
fmt.Sprintf("Multiple parent candidates for %s:", branchName),
116-
result.Candidates, 0)
117-
if promptErr != nil {
118-
return fmt.Errorf("prompt: %w", promptErr)
119-
}
120-
parent = result.Candidates[idx]
121-
}
122-
123-
detectedPRNumber = result.PRNumber
124-
}
125-
12676
if parent != trunk {
12777
if _, parentErr := cfg.GetParent(parent); parentErr != nil {
12878
return fmt.Errorf("parent %q is not tracked", parent)
12979
}
13080
}
13181

132-
// Check for cycles via config parent chain walk (catches cases the tree
133-
// model misses when nodes with broken parent links are omitted).
134-
if wouldCycle(cfg, branchName, parent) {
135-
return errors.New("cannot adopt: would create a cycle")
82+
// Check for cycles (branch can't be ancestor of parent)
83+
root, err := tree.Build(cfg)
84+
if err != nil {
85+
return err
86+
}
87+
88+
parentNode := tree.FindNode(root, parent)
89+
if parentNode != nil {
90+
for _, ancestor := range tree.GetAncestors(parentNode) {
91+
if ancestor.Name == branchName {
92+
return errors.New("cannot adopt: would create a cycle")
93+
}
94+
}
13695
}
13796

13897
// Set parent
@@ -146,11 +105,7 @@ func runAdopt(cmd *cobra.Command, args []string) error {
146105
_ = cfg.SetForkPoint(branchName, forkPoint) //nolint:errcheck // best effort
147106
}
148107

149-
// Store PR number if detected
150-
if detectedPRNumber > 0 {
151-
_ = cfg.SetPR(branchName, detectedPRNumber) //nolint:errcheck // best effort
152-
}
153-
108+
s := style.New()
154109
fmt.Printf("%s Adopted branch %s with parent %s\n", s.SuccessIcon(), s.Branch(branchName), s.Branch(parent))
155110
return nil
156111
}

cmd/adopt_test.go

Lines changed: 0 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,13 @@
22
package cmd_test
33

44
import (
5-
"os"
6-
"os/exec"
7-
"path/filepath"
85
"testing"
96

107
"github.com/boneskull/gh-stack/internal/config"
11-
"github.com/boneskull/gh-stack/internal/detect"
128
"github.com/boneskull/gh-stack/internal/git"
139
"github.com/boneskull/gh-stack/internal/tree"
1410
)
1511

16-
// addCommit creates a file with the given content and commits it.
17-
func addCommit(t *testing.T, dir, filename, content string) {
18-
t.Helper()
19-
if err := os.WriteFile(filepath.Join(dir, filename), []byte(content), 0644); err != nil {
20-
t.Fatalf("write %s: %v", filename, err)
21-
}
22-
cmd := exec.Command("git", "-C", dir, "add", ".")
23-
if err := cmd.Run(); err != nil {
24-
t.Fatalf("git add: %v", err)
25-
}
26-
cmd = exec.Command("git", "-C", dir, "commit", "-m", "add "+filename)
27-
if err := cmd.Run(); err != nil {
28-
t.Fatalf("git commit: %v", err)
29-
}
30-
}
31-
3212
func TestAdoptBranch(t *testing.T) {
3313
dir := setupTestRepo(t)
3414

@@ -168,114 +148,3 @@ func TestAdoptStoresForkPoint(t *testing.T) {
168148
t.Errorf("fork point = %s, want %s", storedFP, trunkTip)
169149
}
170150
}
171-
172-
// TestAdoptAutoDetect exercises the full detection-to-adoption pipeline:
173-
// detect parent, validate, set parent, store fork point.
174-
func TestAdoptAutoDetect(t *testing.T) {
175-
dir := setupTestRepo(t)
176-
g := git.New(dir)
177-
178-
cfg, err := config.Load(dir)
179-
if err != nil {
180-
t.Fatalf("Load config: %v", err)
181-
}
182-
183-
trunk, err := g.CurrentBranch()
184-
if err != nil {
185-
t.Fatalf("CurrentBranch: %v", err)
186-
}
187-
188-
err = cfg.SetTrunk(trunk)
189-
if err != nil {
190-
t.Fatalf("SetTrunk: %v", err)
191-
}
192-
193-
// Create tracked branch A
194-
err = g.CreateAndCheckout("feature-a")
195-
if err != nil {
196-
t.Fatalf("CreateAndCheckout feature-a: %v", err)
197-
}
198-
addCommit(t, dir, "a.txt", "a")
199-
200-
err = cfg.SetParent("feature-a", trunk)
201-
if err != nil {
202-
t.Fatalf("SetParent feature-a: %v", err)
203-
}
204-
205-
// Create untracked branch B off A
206-
err = g.CreateAndCheckout("feature-b")
207-
if err != nil {
208-
t.Fatalf("CreateAndCheckout feature-b: %v", err)
209-
}
210-
addCommit(t, dir, "b.txt", "b")
211-
212-
// feature-b should not be tracked yet
213-
_, getErr := cfg.GetParent("feature-b")
214-
if getErr == nil {
215-
t.Fatal("feature-b should not be tracked yet")
216-
}
217-
218-
// Simulate what runAdopt does when no parent arg is given:
219-
// 1. Detect parent
220-
tracked, _ := cfg.ListTrackedBranches()
221-
result, detectErr := detect.DetectParent("feature-b", tracked, trunk, g, nil)
222-
if detectErr != nil {
223-
t.Fatalf("detection failed: %v", detectErr)
224-
}
225-
if result.Confidence == detect.Ambiguous {
226-
t.Fatal("expected non-ambiguous detection")
227-
}
228-
if result.Parent != "feature-a" {
229-
t.Errorf("expected detected parent 'feature-a', got %q", result.Parent)
230-
}
231-
232-
// 2. Validate parent is tracked (same check as runAdopt)
233-
if result.Parent != trunk {
234-
if _, parentErr := cfg.GetParent(result.Parent); parentErr != nil {
235-
t.Fatalf("detected parent %q is not tracked: %v", result.Parent, parentErr)
236-
}
237-
}
238-
239-
// 3. Set parent (same as runAdopt)
240-
err = cfg.SetParent("feature-b", result.Parent)
241-
if err != nil {
242-
t.Fatalf("SetParent failed: %v", err)
243-
}
244-
245-
// 4. Store fork point (same as runAdopt)
246-
forkPoint, fpErr := g.GetMergeBase("feature-b", result.Parent)
247-
if fpErr != nil {
248-
t.Fatalf("GetMergeBase failed: %v", fpErr)
249-
}
250-
_ = cfg.SetForkPoint("feature-b", forkPoint)
251-
252-
// Verify the full adoption persisted correctly
253-
parent, err := cfg.GetParent("feature-b")
254-
if err != nil {
255-
t.Fatalf("feature-b should be tracked now: %v", err)
256-
}
257-
if parent != "feature-a" {
258-
t.Errorf("expected parent 'feature-a', got %q", parent)
259-
}
260-
261-
storedFP, fpGetErr := cfg.GetForkPoint("feature-b")
262-
if fpGetErr != nil {
263-
t.Fatalf("GetForkPoint failed: %v", fpGetErr)
264-
}
265-
if storedFP != forkPoint {
266-
t.Errorf("fork point mismatch: stored=%s, expected=%s", storedFP, forkPoint)
267-
}
268-
269-
// Verify tree now includes feature-b
270-
root, buildErr := tree.Build(cfg)
271-
if buildErr != nil {
272-
t.Fatalf("Build failed: %v", buildErr)
273-
}
274-
nodeB := tree.FindNode(root, "feature-b")
275-
if nodeB == nil {
276-
t.Fatal("feature-b should appear in tree after adoption")
277-
}
278-
if nodeB.Parent.Name != "feature-a" {
279-
t.Errorf("expected parent node 'feature-a', got %q", nodeB.Parent.Name)
280-
}
281-
}

cmd/cascade.go

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

99
"github.com/boneskull/gh-stack/internal/config"
1010
"github.com/boneskull/gh-stack/internal/git"
11-
"github.com/boneskull/gh-stack/internal/github"
1211
"github.com/boneskull/gh-stack/internal/state"
1312
"github.com/boneskull/gh-stack/internal/style"
1413
"github.com/boneskull/gh-stack/internal/tree"
@@ -31,14 +30,12 @@ var (
3130
cascadeOnlyFlag bool
3231
cascadeDryRunFlag bool
3332
cascadeWorktreesFlag bool
34-
cascadeNoDetectFlag bool
3533
)
3634

3735
func init() {
3836
cascadeCmd.Flags().BoolVar(&cascadeOnlyFlag, "only", false, "only restack current branch, not descendants")
3937
cascadeCmd.Flags().BoolVar(&cascadeDryRunFlag, "dry-run", false, "show what would be done")
4038
cascadeCmd.Flags().BoolVar(&cascadeWorktreesFlag, "worktrees", false, "rebase branches checked out in linked worktrees in-place")
41-
cascadeCmd.Flags().BoolVar(&cascadeNoDetectFlag, "no-detect", false, "skip auto-detection of untracked branches")
4239
rootCmd.AddCommand(cascadeCmd)
4340
}
4441

@@ -67,14 +64,6 @@ func runCascade(cmd *cobra.Command, args []string) error {
6764
return err
6865
}
6966

70-
// Auto-detect and adopt untracked branches
71-
if !cascadeNoDetectFlag {
72-
gh, _ := github.NewClient() //nolint:errcheck // nil is fine, skip PR detection
73-
if adoptErr := autoDetectAndAdopt(cfg, g, gh, s); adoptErr != nil {
74-
fmt.Printf("%s auto-detection: %v\n", s.WarningIcon(), adoptErr)
75-
}
76-
}
77-
7867
// Build tree
7968
root, err := tree.Build(cfg)
8069
if err != nil {

0 commit comments

Comments
 (0)