Skip to content

Commit eba030a

Browse files
henrikjeclaude
andcommitted
fix(retarget): exempt retarget-base-not-found from blocking the operation
Repos skipped because the old base branch is missing (retarget-base-not-found) no longer block other repos from being retargeted. The ahead count that triggered this flag was measured against the fallback default branch, not the missing base, so it incorrectly blocked repos where the old base never existed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent eaed986 commit eba030a

4 files changed

Lines changed: 48 additions & 7 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Exempt retarget-base-not-found from blocking retarget
2+
3+
Date: 2026-04-10
4+
5+
## Context
6+
7+
`arb retarget` applies an all-or-nothing gate: if any skipped repo has a non-exempt skip flag, the entire operation is blocked. The `retarget-base-not-found` flag fires when the old base branch is missing (neither remote nor local) and `status.base.ahead > 0`. The original intent was to block retarget when a repo has stacked work whose rebase boundary cannot be determined.
8+
9+
However, `ahead` is measured against the fallback default branch (e.g., `main`), not the missing old base — because when the configured base is gone, base resolution falls back to the default branch. This means any feature branch with commits ahead of `main` triggers the flag, even when the old base branch was never present on that repo. In multi-repo workspaces where only some repos have the old base branch, this blocks the entire retarget unnecessarily.
10+
11+
## Options
12+
13+
### Exempt retarget-base-not-found
14+
Add the flag to `RETARGET_EXEMPT_SKIPS` alongside `no-base-branch` and `retarget-target-not-found`.
15+
- **Pros:** Simple one-line change. Follows the existing flat-set exemption pattern. Repos with this flag are already skipped (not operated on), so the exemption only controls whether they block other repos.
16+
- **Cons:** Does not distinguish "old base was never here" from "old base existed but was deleted." Both cases become non-blocking.
17+
18+
### Split into two distinct flags
19+
Introduce `retarget-base-never-present` (exempt) and `retarget-base-deleted` (blocking) to preserve the safety distinction.
20+
- **Pros:** More precise safety model in theory.
21+
- **Cons:** The distinction cannot be reliably determined — when a branch is gone from both remote and local, there is no evidence of whether it was ever present. The `ahead` count is unreliable for this purpose (measured against fallback, not the missing base). Disproportionate complexity for no practical safety gain.
22+
23+
## Decision
24+
25+
Exempt `retarget-base-not-found` by adding it to `RETARGET_EXEMPT_SKIPS`.
26+
27+
## Reasoning
28+
29+
The exemption is safe because repos with this flag are already being skipped — they will not be rebased. The change only determines whether the skip also prevents other repos from being retargeted. After the retarget completes, the workspace config is updated to the new base, and the next `arb rebase` evaluates each repo freshly against the new base.
30+
31+
The split-flag approach is not implementable without maintaining per-repo history of which branches have existed, and even if it were, the "deleted" case is equally safe to exempt for the same reason: the repo is skipped, not operated on.
32+
33+
## Consequences
34+
35+
- Multi-repo workspaces where only some repos have the old base branch can now be retargeted without manual intervention.
36+
- The `retarget-base-not-found` skip is still surfaced in the plan table (with attention styling via `BENIGN_SKIPS`), so the user sees which repos were skipped and why.
37+
- If a future scenario arises where a repo's stacked work truly depends on the deleted old base, the repo is still skipped — no data loss occurs. The user would need to manually resolve the branch state before rebasing that repo.

src/lib/status/skip-flags.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ describe("RETARGET_EXEMPT_SKIPS", () => {
2222
test("contains retarget exemptions", () => {
2323
expect(RETARGET_EXEMPT_SKIPS.has("no-base-branch")).toBe(true);
2424
expect(RETARGET_EXEMPT_SKIPS.has("retarget-target-not-found")).toBe(true);
25+
expect(RETARGET_EXEMPT_SKIPS.has("retarget-base-not-found")).toBe(true);
2526
});
2627

2728
test("does not contain blocking flags", () => {
2829
expect(RETARGET_EXEMPT_SKIPS.has("dirty")).toBe(false);
2930
expect(RETARGET_EXEMPT_SKIPS.has("wrong-branch")).toBe(false);
30-
expect(RETARGET_EXEMPT_SKIPS.has("retarget-base-not-found")).toBe(false);
3131
});
3232
});

src/lib/status/skip-flags.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ export const BENIGN_SKIPS: ReadonlySet<SkipFlag> = new Set([
3434
"no-base-branch",
3535
]);
3636

37-
export const RETARGET_EXEMPT_SKIPS: ReadonlySet<SkipFlag> = new Set(["no-base-branch", "retarget-target-not-found"]);
37+
export const RETARGET_EXEMPT_SKIPS: ReadonlySet<SkipFlag> = new Set([
38+
"no-base-branch",
39+
"retarget-target-not-found",
40+
"retarget-base-not-found",
41+
]);
3842

3943
export const EXTRACT_EXEMPT_SKIPS: ReadonlySet<SkipFlag> = new Set(["no-base-branch"]);

test/integration/retarget.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ describe("explicit retarget to non-default branch", () => {
463463
expect(result.output).not.toContain("may not be merged");
464464
}));
465465

466-
test("arb retarget blocks when old base ref is missing in truly stacked repo", () =>
466+
test("arb retarget proceeds when old base ref is missing in stacked repo", () =>
467467
withEnv(async (env) => {
468468
const repoA = join(env.projectDir, ".arb/repos/repo-a");
469469
await git(repoA, ["checkout", "-b", "feat/auth"]);
@@ -499,14 +499,14 @@ describe("explicit retarget to non-default branch", () => {
499499
}
500500

501501
// repo-a is truly stacked (base exists), repo-b's base is gone (both remote and local)
502-
// Retarget can't compute the --onto boundary for repo-b → blocks all-or-nothing check
502+
// repo-b is skipped (retarget-base-not-found) but does not block the operation
503503
const result = await arb(env, ["retarget", "main", "--yes"], {
504504
cwd: join(env.projectDir, "stacked"),
505505
});
506-
expect(result.exitCode).not.toBe(0);
507-
expect(result.output).toContain("Cannot retarget");
506+
expect(result.exitCode).toBe(0);
507+
expect(result.output).toContain("Retargeted");
508508
expect(result.output).toContain("repo-b");
509-
expect(result.output).toContain("not found");
509+
expect(result.output).toContain("skipped");
510510
}));
511511

512512
test("arb retarget rejects retargeting to the current feature branch", () =>

0 commit comments

Comments
 (0)