Skip to content

Commit 744fc7f

Browse files
committed
fix: sort candidates by trunk distance and document --no-detect flags
- Sort `autoDetectAndAdopt` candidates by merge-base distance from trunk (ascending) so parents are processed before children in untracked chains, preventing mis-adoption when a child branch alphabetically sorts before its untracked parent - Document `--no-detect` flag in README for `restack` and `sync` commands (was already implemented but undocumented)
1 parent 14114a7 commit 744fc7f

2 files changed

Lines changed: 43 additions & 5 deletions

File tree

README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -302,11 +302,12 @@ If a rebase conflict occurs, resolve it and run `gh stack continue`.
302302

303303
#### restack Flags
304304

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 |
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 |
310311

311312
### continue
312313

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

337339
### undo
338340

cmd/detect_helpers.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package cmd
22

33
import (
4+
"cmp"
45
"fmt"
6+
"slices"
57

68
"github.com/boneskull/gh-stack/internal/config"
79
"github.com/boneskull/gh-stack/internal/detect"
@@ -33,6 +35,12 @@ func autoDetectAndAdopt(cfg *config.Config, g *git.Git, gh *github.Client, s *st
3335
return nil
3436
}
3537

38+
// Sort candidates by merge-base distance from trunk (ascending) so that
39+
// branches closer to trunk (parents) are processed before their children.
40+
// Without this, alphabetical ordering can cause a child to be adopted with
41+
// the wrong parent when both it and its true parent are untracked.
42+
sortCandidatesByTrunkDistance(candidates, trunk, g)
43+
3644
// Loop until no progress: untracked chains (e.g., C based on untracked B)
3745
// may require multiple passes since a branch can only be detected once its
3846
// parent has been adopted into the tracked set.
@@ -127,6 +135,34 @@ func autoDetectAndAdopt(cfg *config.Config, g *git.Git, gh *github.Client, s *st
127135
return nil
128136
}
129137

138+
// sortCandidatesByTrunkDistance sorts branches by their merge-base distance
139+
// from trunk (ascending). Branches closer to trunk are processed first, which
140+
// ensures parents are adopted before their children in untracked chains.
141+
// Branches whose distance can't be computed are sorted to the end.
142+
func sortCandidatesByTrunkDistance(candidates []string, trunk string, g *git.Git) {
143+
const maxDist = 1<<31 - 1
144+
dist := make(map[string]int, len(candidates))
145+
for _, b := range candidates {
146+
mb, err := g.GetMergeBase(b, trunk)
147+
if err != nil {
148+
dist[b] = maxDist
149+
continue
150+
}
151+
n, err := g.RevListCount(mb, b)
152+
if err != nil {
153+
dist[b] = maxDist
154+
continue
155+
}
156+
dist[b] = n
157+
}
158+
slices.SortFunc(candidates, func(a, b string) int {
159+
if d := cmp.Compare(dist[a], dist[b]); d != 0 {
160+
return d
161+
}
162+
return cmp.Compare(a, b)
163+
})
164+
}
165+
130166
// wouldCycle returns true if setting branch's parent to parent would create a
131167
// cycle in the config-based parent chain. It walks cfg.GetParent from parent
132168
// upward; if it ever reaches branch, adopting would create a loop.

0 commit comments

Comments
 (0)