Skip to content

Commit 8919052

Browse files
henrikjeclaude
andcommitted
refactor: extract withReflogAction helper and fix stale doc references
Extract the repeated GIT_REFLOG_ACTION try/finally pattern into a withReflogAction() helper in core/operation.ts, replacing 10 manual instances across 8 files. Fix ARCHITECTURE.md reference to nonexistent sync/abort-flow.ts (now sync/undo/) and update stale sync/ directory listing in CLAUDE.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2417d65 commit 8919052

12 files changed

Lines changed: 53 additions & 64 deletions

File tree

ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ Workspace config (`.arbws/config.json`) and project config (`.arb/config.json`)
133133

134134
Commands that can fail partway through sequential multi-repo execution write an operation record to `.arbws/operation.json`, enabling `--continue` (resume after conflicts), `--abort` (cancel and restore), and `arb undo` (reverse a completed operation). Running a bare command during an in-progress operation is blocked with guidance — the user must explicitly choose `--continue` or `--abort`.
135135

136-
The shared infrastructure lives in `core/operation.ts` (schema, I/O, gate, reconciliation), `sync/continue-flow.ts` (shared continue orchestration), and `sync/abort-flow.ts` (shared abort execution). Adding a new command type requires a thin handler and an undo switch case. See `decisions/0095-operation-record-and-recovery-model.md` for the design rationale, option analysis, and the relationship between `--abort` and `arb undo`.
136+
The shared infrastructure lives in `core/operation.ts` (schema, I/O, gate, reconciliation), `sync/continue-flow.ts` (shared continue orchestration), and `sync/undo/` (assessment, planning, execution for both `--abort` and `arb undo`). Adding a new command type requires a thin handler and an undo switch case. See `decisions/0095-operation-record-and-recovery-model.md` for the design rationale, option analysis, and the relationship between `--abort` and `arb undo`.
137137

138138
### Git worktree directory layout
139139

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Organized into semantic subdirectories. Each directory has a barrel `index.ts` r
4040
- **`status/`** — Canonical status model: `status.ts` (RepoStatus, RepoFlags, gathering, filtering — see ARCHITECTURE.md), `skip-flags.ts`, `pr-detection.ts`, `ticket-detection.ts`, `test-helpers.ts` (makeRepo fixtures)
4141
- **`render/`** — Declarative render model: `model.ts` (Cell, Span, Attention, OutputNode types, cell helpers — zero lib imports), `analysis.ts` (analyze* functions, buildStatusCountsCell, formatStatusCounts, flagLabels), `render.ts` (OutputNode[] → ANSI string), `status-view.ts`, `status-verbose.ts`, `conflict-report.ts`, `repo-header.ts`, `plan-format.ts`, `integrate-graph.ts`, `integrate-cells.ts`, `phased-render.ts`, `height-fit.ts` (vertical truncation for watch mode)
4242
- **`workspace/`** — Workspace management: `arb-root.ts` (.arb/ marker detection), `repos.ts` (repo listing, selection), `worktrees.ts`, `branch.ts` (workspace branch detection), `context.ts` (requireWorkspace/requireBranch guards), `clean.ts`, `templates.ts`
43-
- **`sync/`** — Synchronization: `integrate.ts` (shared rebase/merge logic), `parallel-fetch.ts` (concurrent fetch with timeout), `mutation-flow.ts` (confirmation prompts, phased render integration)
43+
- **`sync/`** — Synchronization: `integrate.ts` (shared rebase/merge logic), `continue-flow.ts` (shared --continue orchestration), `undo/` (assessment, planning, execution for --abort and arb undo), `parallel-fetch.ts` (concurrent fetch with timeout), `mutation-flow.ts` (confirmation prompts, phased render integration), `assess-with-cache.ts` (shared status+classify factory), `classify-integrate.ts` / `classify-retarget.ts` (per-repo classifiers), `network-errors.ts`, `constants.ts`, `types.ts`
4444
- **`json/`** — JSON output: `json-types.ts` (Zod schemas), `json-schema.ts`
4545
- **`help/`** — Help topics
4646

src/commands/branch-rename.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
finalizeOperationRecord,
1111
readInProgressOperation,
1212
readWorkspaceConfig,
13+
withReflogAction,
1314
writeOperationRecord,
1415
writeWorkspaceConfig,
1516
} from "../lib/core";
@@ -439,8 +440,7 @@ async function runRename(
439440
let renameOk = 0;
440441
const failures: string[] = [];
441442

442-
try {
443-
process.env.GIT_REFLOG_ACTION = "arb-branch-rename";
443+
await withReflogAction("arb-branch-rename", async () => {
444444
for (const a of willRename) {
445445
inlineStart(a.repo, "renaming");
446446
const result = await renameBranch(a.repoDir, oldBranch, newBranch);
@@ -472,10 +472,7 @@ async function runRename(
472472
failures.push(a.repo);
473473
}
474474
}
475-
} finally {
476-
// biome-ignore lint/performance/noDelete: must truly unset env var, not coerce to string
477-
delete process.env.GIT_REFLOG_ACTION;
478-
}
475+
});
479476

480477
if (failures.length > 0) {
481478
process.stderr.write("\n");

src/commands/pull.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
assertNoInProgressOperation,
1111
readInProgressOperation,
1212
readWorkspaceConfig,
13+
withReflogAction,
1314
writeOperationRecord,
1415
} from "../lib/core";
1516
import {
@@ -276,8 +277,7 @@ export async function runPull(
276277
writeOperationRecord(wsDir, record);
277278
};
278279

279-
try {
280-
process.env.GIT_REFLOG_ACTION = "arb-pull";
280+
await withReflogAction("arb-pull", async () => {
281281
for (const a of willPull) {
282282
const strategy = a.pullStrategy ?? (a.pullMode === "rebase" ? "rebase-pull" : "merge-pull");
283283
inlineStart(a.repo, `pulling (${pullStrategyLabel(strategy)})`);
@@ -383,10 +383,7 @@ export async function runPull(
383383
}
384384
}
385385
}
386-
} finally {
387-
// biome-ignore lint/performance/noDelete: must truly unset env var, not coerce to string
388-
delete process.env.GIT_REFLOG_ACTION;
389-
}
386+
});
390387

391388
// Consolidated conflict report
392389
const conflictNodes = buildConflictReport(

src/commands/rename.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
captureRepoState,
1111
readInProgressOperation,
1212
readWorkspaceConfig,
13+
withReflogAction,
1314
writeOperationRecord,
1415
writeWorkspaceConfig,
1516
} from "../lib/core";
@@ -277,8 +278,7 @@ async function runWorkspaceRename(
277278
// Operation record tracks in-progress state (no config mutation until completion)
278279

279280
const failures: string[] = [];
280-
try {
281-
process.env.GIT_REFLOG_ACTION = "arb-rename";
281+
await withReflogAction("arb-rename", async () => {
282282
for (const a of willRename) {
283283
inlineStart(a.repo, "renaming");
284284
const result = await renameBranch(a.repoDir, oldBranch, newBranch);
@@ -304,10 +304,7 @@ async function runWorkspaceRename(
304304
failures.push(a.repo);
305305
}
306306
}
307-
} finally {
308-
// biome-ignore lint/performance/noDelete: must truly unset env var, not coerce to string
309-
delete process.env.GIT_REFLOG_ACTION;
310-
}
307+
});
311308

312309
if (failures.length > 0) {
313310
process.stderr.write("\n");

src/commands/reset.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
assertNoInProgressOperation,
99
captureRepoState,
1010
readWorkspaceConfig,
11+
withReflogAction,
1112
writeOperationRecord,
1213
} from "../lib/core";
1314
import { getCommitsBetweenFull, getShortHead, gitLocal } from "../lib/git";
@@ -501,8 +502,7 @@ export function registerResetCommand(program: Command): void {
501502
let resetOk = 0;
502503
const failed: { assessment: ResetAssessment; stderr: string }[] = [];
503504

504-
try {
505-
process.env.GIT_REFLOG_ACTION = "arb-reset";
505+
await withReflogAction("arb-reset", async () => {
506506
for (const a of willReset) {
507507
inlineStart(a.repo, `resetting to ${a.target}`);
508508
const result = await gitLocal(a.repoDir, "reset", `--${resetMode}`, a.target);
@@ -526,10 +526,7 @@ export function registerResetCommand(program: Command): void {
526526
failed.push({ assessment: a, stderr: result.stderr });
527527
}
528528
}
529-
} finally {
530-
// biome-ignore lint/performance/noDelete: must truly unset env var, not coerce to string
531-
delete process.env.GIT_REFLOG_ACTION;
532-
}
529+
});
533530

534531
// Failure report
535532
if (failed.length > 0) {

src/commands/retarget.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
assertNoInProgressOperation,
1111
readInProgressOperation,
1212
readWorkspaceConfig,
13+
withReflogAction,
1314
writeOperationRecord,
1415
writeWorkspaceConfig,
1516
} from "../lib/core";
@@ -305,8 +306,7 @@ export function registerRetargetCommand(program: Command): void {
305306
let succeeded = 0;
306307
const conflicted: { assessment: RetargetAssessment; stdout: string; stderr: string }[] = [];
307308

308-
try {
309-
process.env.GIT_REFLOG_ACTION = "arb-retarget";
309+
await withReflogAction("arb-retarget", async () => {
310310
for (const a of willRetarget) {
311311
const targetRef = `${a.baseRemote}/${a.targetBranch}`;
312312

@@ -352,10 +352,7 @@ export function registerRetargetCommand(program: Command): void {
352352
conflicted.push({ assessment: a, stdout: result.stdout, stderr: result.stderr });
353353
}
354354
}
355-
} finally {
356-
// biome-ignore lint/performance/noDelete: must truly unset env var, not coerce to string
357-
delete process.env.GIT_REFLOG_ACTION;
358-
}
355+
});
359356

360357
// Phase 7: conflict report
361358
const conflictNodes = buildConflictReport(

src/lib/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export {
3131
deleteOperationRecord,
3232
finalizeOperationRecord,
3333
readInProgressOperation,
34+
withReflogAction,
3435
readOperationRecord,
3536
writeOperationRecord,
3637
} from "./operation";

src/lib/core/operation.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,23 @@ export function finalizeOperationRecord(wsDir: string, outcome: OperationOutcome
158158
}
159159
}
160160

161+
// ── Reflog action ──
162+
163+
/**
164+
* Run an async function with `GIT_REFLOG_ACTION` set so git reflog entries
165+
* are tagged (e.g. `arb-rebase`, `arb-undo`). The env var is always cleaned
166+
* up, even if the function throws.
167+
*/
168+
export async function withReflogAction<T>(action: string, fn: () => Promise<T>): Promise<T> {
169+
process.env.GIT_REFLOG_ACTION = action;
170+
try {
171+
return await fn();
172+
} finally {
173+
// biome-ignore lint/performance/noDelete: must truly unset env var, not coerce to string
174+
delete process.env.GIT_REFLOG_ACTION;
175+
}
176+
}
177+
161178
// ── Gate ──
162179

163180
export async function assertNoInProgressOperation(wsDir: string): Promise<void> {

src/lib/sync/continue-flow.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { basename } from "node:path";
22
import { ArbError } from "../core/errors";
33
import type { OperationRecord } from "../core/operation";
4-
import { classifyContinueRepo, writeOperationRecord } from "../core/operation";
4+
import { classifyContinueRepo, withReflogAction, writeOperationRecord } from "../core/operation";
55
import { gitLocal } from "../git/git";
66
import { buildConflictReport } from "../render/conflict-report";
77
import type { Cell, OutputNode } from "../render/model";
@@ -172,8 +172,7 @@ export async function runContinueFlow(params: ContinueFlowParams): Promise<void>
172172
let succeeded = 0;
173173
const newConflicts: { repo: string; stdout: string; stderr: string }[] = [];
174174

175-
try {
176-
process.env.GIT_REFLOG_ACTION = `arb-${mode}-continue`;
175+
await withReflogAction(`arb-${mode}-continue`, async () => {
177176
for (const c of willContinue) {
178177
inlineStart(c.repo, `continuing ${mode}`);
179178
const cmd = typeof gitContinueCmd === "string" ? gitContinueCmd : await gitContinueCmd(c.repoDir);
@@ -197,10 +196,7 @@ export async function runContinueFlow(params: ContinueFlowParams): Promise<void>
197196
newConflicts.push({ repo: c.repo, stdout: result.stdout, stderr: result.stderr });
198197
}
199198
}
200-
} finally {
201-
// biome-ignore lint/performance/noDelete: must truly unset env var, not coerce to string
202-
delete process.env.GIT_REFLOG_ACTION;
203-
}
199+
});
204200

205201
// Step 9: Show conflict details for failures (B2 fix)
206202
if (newConflicts.length > 0) {

0 commit comments

Comments
 (0)