You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(retarget): walk stack chain to find nearest non-merged ancestor
When `arb retarget` is called without a target and the configured base
is merged, the command now walks the workspace config chain to find
the nearest non-merged ancestor instead of falling back to the default
branch. For main←a←b←c with b merged, workspace c retargets onto a.
The chain walk runs at the command layer before per-repo classification,
using dependency-injected helpers for testability. Manual fetch before
chain walking ensures fresh refs for merge detection; non-stacked
workspaces preserve phased render via the normal runPlanFlow path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When workspace C stacks on workspace B which stacks on workspace A (`main <- a <- b <- c`), and B gets merged into `main`, running `arb retarget` in C falls back to the repo's default branch (`main`). In a deep stack, the correct target is A — the next non-merged ancestor in the stack chain. Without chain walking, the user must manually specify `arb retarget feat/a`, which requires knowing the stack topology.
8
+
9
+
The "local branches as base" feature (DR-0098) added `sourceWorkspace` to the status model, identifying which workspace has the base branch checked out. This enables reading the base workspace's config to discover the chain.
10
+
11
+
## Options
12
+
13
+
### A: Per-repo chain walking in the classifier
14
+
15
+
Each repo independently resolves the chain-walked target during `assessRetargetRepo`. The classifier reads workspace configs and runs merge detection.
16
+
17
+
-**Pros:** Self-contained per repo; no shared state.
18
+
-**Cons:** Workspace configs are a workspace-level concern, not per-repo. The classifier has no access to `arbRootDir` or workspace listing. Duplicates merge detection across repos. Breaks the classifier's design contract (classifiers operate on `RepoStatus`, not workspace configs).
19
+
20
+
### B: Command-layer chain walking before per-repo classification
21
+
22
+
The command layer resolves the chain-walked target once, then passes it to the classifier as a pre-resolved `targetBranch`. Uses a single representative repo for merge detection.
23
+
24
+
-**Pros:** Clean separation — workspace-level concern handled at workspace level. Single merge detection pass. Classifier's existing `targetBranch` parameter handles the result naturally.
25
+
-**Cons:** Uses one representative repo for merge detection; if repos have fundamentally different remote structures, the representative may not reflect all repos. Per-repo classifier catches this via `retarget-target-not-found` skip (non-blocking).
26
+
27
+
### C: Chain walking inside `postAssess` after initial assessment
28
+
29
+
After the first round of assessment reveals all repos targeting default (merged base), intercept with chain walking and re-assess with the new target.
30
+
31
+
-**Pros:** No changes to fetch timing; assessment already has fresh refs.
Option B: command-layer chain walking before per-repo classification. Fetch manually before chain walking (`shouldFetch: false` to `runPlanFlow`), ensuring fresh refs for merge detection.
37
+
38
+
## Reasoning
39
+
40
+
The chain walk is a workspace-level concern — it reads workspace configs, which are workspace-scoped. Placing it in the command layer matches the existing pattern where workspace-level decisions (config updates, target resolution) live in the command, while per-repo decisions live in the classifier.
41
+
42
+
Manual fetch before chain walking ensures accurate merge detection. This trades phased render (the optimization showing a pre-fetch plan that updates post-fetch) for correctness. Retarget is an infrequent operation, making the trade-off acceptable.
43
+
44
+
The dependency-injected `walkRetargetChain` function isolates the algorithm from git and filesystem dependencies, enabling thorough unit testing without spawning processes.
45
+
46
+
## Consequences
47
+
48
+
-`arb retarget` without arguments automatically follows the stack chain when the base is merged, finding the nearest non-merged ancestor. Users no longer need to know the exact stack topology.
49
+
- Chain walking requires workspace configs to exist for intermediate branches. Deleted workspaces break the chain, falling back to default branch resolution (same as current behavior).
50
+
- Layered squash merges (where each level was independently squash-merged) may not be detected by the current merge detection (cumulative patch-id doesn't match). Regular merges and single-level squash merges work correctly. This is an existing merge detection limitation, not introduced by chain walking.
51
+
- Retarget loses phased render when chain walking is possible (`targetBranch === null && configBase` path). Non-stacked workspaces and explicit targets are unaffected.
@@ -56,7 +60,7 @@ export function registerRetargetCommand(program: Command): void {
56
60
)
57
61
.summary("Change the base branch and rebase onto it")
58
62
.description(
59
-
"Examples:\n\n arb retarget feature1 Retarget onto feature1\n arb retarget Retarget onto the default branch\n arb retarget feature1 --verbose Show commits in the plan\n\nChanges the workspace's base branch and rebases all repos onto the new base. This is the \"I want to change what my workspace is based on\" command.\n\nWith a branch argument, retargets onto that branch — useful for stacking onto a feature branch, switching between base branches, or retargeting after the base branch has been merged. Without a branch argument, retargets onto each repo's default branch (e.g. main) and removes the configured base.\n\nWhen the old base was merged (squash or regular), uses 'git rebase --onto' to replay only your commits. When the old base was not merged, uses the same mechanism to graft your commits onto the new base.\n\nRequires a configured base branch or an explicit branch argument. If no base is configured and no branch is given, this is an error — use 'arb retarget <branch>' to set a base.\n\nAll-or-nothing: if any repo is blocked (dirty, wrong branch, etc.), the entire retarget is refused so the workspace config stays consistent. Use --autostash to stash uncommitted changes before rebasing.\n\nAlways operates on all repos in the workspace — retarget is a structural change to the workspace, not a per-repo operation. To change the base config without rebasing, or to rebase selectively, use 'arb branch base <branch>' then 'arb rebase [repos...]'.\n\nUse --verbose to show the incoming commits for each repo in the plan. Use --graph to show a branch divergence diagram. See 'arb help stacked' for stacked workspace workflows.",
63
+
"Examples:\n\n arb retarget feature1 Retarget onto feature1\n arb retarget Retarget onto the default branch\n arb retarget feature1 --verbose Show commits in the plan\n\nChanges the workspace's base branch and rebases all repos onto the new base. This is the \"I want to change what my workspace is based on\" command.\n\nWith a branch argument, retargets onto that branch — useful for stacking onto a feature branch, switching between base branches, or retargeting after the base branch has been merged. Without a branch argument, walks the stack chain to find the nearest non-merged ancestor (e.g. if B is merged in A←B←C, retargets C onto A). If no non-merged ancestor exists, retargets onto each repo's default branch (e.g. main) and removes the configured base.\n\nWhen the old base was merged (squash or regular), uses 'git rebase --onto' to replay only your commits. When the old base was not merged, uses the same mechanism to graft your commits onto the new base.\n\nRequires a configured base branch or an explicit branch argument. If no base is configured and no branch is given, this is an error — use 'arb retarget <branch>' to set a base.\n\nAll-or-nothing: if any repo is blocked (dirty, wrong branch, etc.), the entire retarget is refused so the workspace config stays consistent. Use --autostash to stash uncommitted changes before rebasing.\n\nAlways operates on all repos in the workspace — retarget is a structural change to the workspace, not a per-repo operation. To change the base config without rebasing, or to rebase selectively, use 'arb branch base <branch>' then 'arb rebase [repos...]'.\n\nUse --verbose to show the incoming commits for each repo in the plan. Use --graph to show a branch divergence diagram. See 'arb help stacked' for stacked workspace workflows.",
0 commit comments