feat(cli): --dry-run on every mutating command#108
Conversation
Extend --dry-run beyond submit/post-merge/merge-next/prune to: create, modify, restack, sync, delete, untrack, track, pop, split, absorb, squash, fold, rename, move, freeze, unfreeze, reorder, unlink, revert, stash. Each command gathers its plan, returns a result with dryRun: true, and short-circuits before any writeState, saveUndoEntry, git ref mutation, or remote call. --dry-run --json emits the plan via withSchemaVersion. AI-mode absorb/split skip the model call in dry-run so previews never bill the provider. Completes DUB-70
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
There was a problem hiding this comment.
Pull request overview
This PR extends --dry-run support across the CLI’s mutating commands so users (and scripted/MCP callers via --dry-run --json) can preview a structured plan without writing DubStack state, touching the undo log, or mutating git refs.
Changes:
- Thread
dryRunthrough command/library option types and return shapes (addsdryRun: booleanto results for downstream branching). - Add
emitDryRunPlan()to consistently emit{ schemaVersion, ...plan }JSON output and activate JSON error handling. - Implement dry-run short-circuits across commands (skip state writes, undo entries, worktree guards, fetches, and AI provider calls where applicable) and update unit tests/mocks accordingly.
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/cli/src/index.ts | Adds emitDryRunPlan() and wires --dry-run / --json handling into many command handlers. |
| packages/cli/src/lib/track.ts | Threads dryRun into tracking logic and skips state writes during previews. |
| packages/cli/src/lib/untrack.ts | Adds dryRun option/result and avoids writing state on dry-run. |
| packages/cli/src/lib/delete.ts | Adds dryRun option/result and avoids checkout/delete/writeState on dry-run. |
| packages/cli/src/lib/freeze.ts | Adds dryRun option/result and returns a plan without undo/state mutation. |
| packages/cli/src/lib/sync/types.ts | Extends sync option/result types to include dryRun and planned scope output. |
| packages/cli/src/commands/create.ts | Adds dry-run planning (including AI no-bill behavior) and threads dryRun through create flow. |
| packages/cli/src/commands/modify.ts | Adds a structured ModifyPlan return type for --dry-run and returns it instead of mutating. |
| packages/cli/src/commands/move.ts | Adds dry-run planning and bypasses worktree/GH mutations when previewing. |
| packages/cli/src/commands/restack.ts | Adds dry-run planning (plannedRebases/plannedSkips) without executing rebases or writing state. |
| packages/cli/src/commands/sync.ts | Adds dry-run to compute/return planned scope without fetch/mutations. |
| packages/cli/src/commands/split.ts | Adds dry-run plan output for non-AI modes; AI dry-run still produces a proposal without applying. |
| packages/cli/src/commands/squash.ts | Adds dry-run planning (including placeholder AI message) and skips mutations/guards accordingly. |
| packages/cli/src/commands/reorder.ts | Adds --dry-run mode to list reorderable commits without launching the picker. |
| packages/cli/src/commands/pop.ts | Adds dry-run planning and avoids worktree guard/reset during preview. |
| packages/cli/src/commands/revert.ts | Adds dry-run planning without creating branches/commits. |
| packages/cli/src/commands/rename.ts | Adds dry-run planning and avoids worktree guard/mutations during preview. |
| packages/cli/src/commands/unlink.ts | Adds dry-run planning and bypasses worktree guard/GH retarget work when previewing. |
| packages/cli/src/commands/track.ts | Threads dry-run into undo behavior (skip undo entry on dry-run). |
| packages/cli/src/commands/untrack.ts | Threads dry-run into undo behavior (skip undo entry on dry-run). |
| packages/cli/src/commands/delete.ts | Threads dry-run into confirmation + undo behavior (skip confirm/undo on dry-run). |
| packages/cli/src/commands/freeze.ts | Threads dry-run into freeze command wrapper. |
| packages/cli/src/commands/unfreeze.ts | Threads dry-run into unfreeze command wrapper. |
| packages/cli/src/commands/fold.ts | Adds dry-run planning path using fold preview. |
| packages/cli/src/commands/stash.ts | Adds dry-run planning for stash push/pop without invoking git stash/checkout or writing logs/state. |
| packages/cli/src/commands/absorb.ts | Adds dryRun to result types and ensures AI provider call is skipped during dry-run. |
| packages/cli/src/commands/*.test.ts | Updates mocks/expectations to include the new dryRun field and adds a new freeze --dry-run test. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Adds a single integration suite that, for each of the 20 commands extended in this PR, asserts the dry-run path: (1) returns a result with dryRun: true, (2) leaves .git/dubstack/state.json byte-identical, (3) appends no undo entry, and (4) when refs would normally move (pop, squash, fold, restack, etc.), leaves the branch tip untouched. This satisfies the DUB-70 acceptance criterion "No mutations occur in dry-run (verified by test)". Refs DUB-70
- track: fall back to an in-memory empty state when state.json is missing so `dub track --dry-run` works on a fresh repo (matches create/revert). - create: run read-only `hasStagedChanges` (or `isWorkingTreeClean` when an aggregate flag is set) in dry-run too, so the plan surfaces the same "no staged changes" / "no changes to commit" error a real run would. - unlink: return a stable `'<would-create-new-stack>'` placeholder instead of `crypto.randomUUID()` so dry-run JSON plans are deterministic and don't promise a stack id that the actual run won't honor. - move: keep `retargeted` empty in dry-run and surface candidates under a new `retargetCandidates` field — dry-run doesn't call `gh pr view` to verify each PR is OPEN with a stale base, so reporting them as "retargeted" was misleading. CLI handler prints the candidates with that caveat. Adds tests covering each behavior change. Refs DUB-70
|
@copilot please re-review — addressed all 4 inline comments in 0bb76d2:
Tests added for each behavior change ( |
Re-reviewed and confirmed the four fixes in 0bb76d2 are present and behaving as intended (track fallback state, create dry-run validations, deterministic unlink placeholder, move dry-run retarget fields). I also re-ran Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
…un-flag-on-every-mutating-command # Conflicts: # packages/cli/src/index.ts
- split --ai: bail BEFORE invoking the AI provider so previews never bill. Existing test for the old behavior is updated to assert generateText is never called and the result omits aiProposal. - restack --continue + --dry-run: refuse the combination with a clear DubError. --continue resumes an in-flight rebase, which has no non-mutating preview, so silently ignoring one of the flags would risk unwanted mutation. - emitDryRunPlan + a Commander preAction hook activate JSON mode whenever --dry-run --json is on the action, before validation runs. DubErrors thrown during pre-mutation checks now serialize through jsonErrorEnvelope instead of leaking human-formatted red text, keeping the documented contract for scripted callers. - create / track / revert: replace the broad `readState(cwd).catch(() => emptyState)` with the new readStateForDryRun helper. The helper only swallows the "DubStack is not initialized" DubError and re-throws everything else (corruption, IO failures), so dry-run can't promise a plan the real run won't execute. Adds new tests for split AI bail and restack dry-run sanity. Refs DUB-70
|
🎉 This PR is included in version 1.10.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Summary
Closes DUB-70.
Extend
--dry-runfrom the four commands that already had it(
submit,post-merge,merge-next,prune) to every other mutatingcommand listed in the ticket:
create,modify,restack,sync,delete,untrack,track,pop,split,absorb,squash,fold,rename,move,freeze,unfreeze,reorder,unlink,revert,stash.Behaviour contract for every command:
--dry-runprints a structured plan and exits without touching.git/dubstack/state.json, the undo log, or any git ref.--dry-run --jsonemits the plan as{ schemaVersion, ...plan }viawithSchemaVersion, so MCP / scripted callers can consume it.absorb --ai,split --ai,create --ai,squash --ai)skip the provider call in dry-run so previewing never bills the user.
Resultnow carries adryRun: booleanso callers canbranch on it without re-parsing flags.
--helpfor each command mentions--dry-runwith at least one example.Implementation notes
emitDryRunPlan(plan)helper inpackages/cli/src/index.tswrapsthe plan with
withSchemaVersionand activates JSON-mode error handling.lib/freeze.ts,lib/track.ts,lib/untrack.ts,lib/delete.ts,lib/sync/types.ts) threaddryRunthrough theiroptions and bail before
writeState,saveUndoEntry,pushBranch,checkoutBranch,fetchBranches, etc.sync --dry-runis scope-only on purpose: it reports the roots andbranches that would be synced but never fetches from origin. The
detailed per-branch reconcile decision still requires a real run.
Test plan
pnpm typecheck— clean.pnpm exec biome check .— clean.pnpm test— 1446 / 1446 passing across 129 test files.freeze --dry-rununit test asserts plan is reported, frozenflags are not written, and the undo entry from a prior
createisleft untouched.
track/untrack/delete/move/continuemocksupdated to reflect the new
dryRun: booleanfield on their resulttypes so the suite still type-checks.
branch:
-
absorb --ai --dry-runwas firing the AI call before bailing.Hoisted the dry-run return above
aiPickTargets(...).-
pop --dry-runwas runningassertBranchesNotCheckedOutElsewhereunconditionally. Now wrapped in
if (!dryRun).