Skip to content

feat(cli): --dry-run on every mutating command#108

Merged
dubscode merged 5 commits into
mainfrom
feature/dub-70--dry-run-flag-on-every-mutating-command
May 25, 2026
Merged

feat(cli): --dry-run on every mutating command#108
dubscode merged 5 commits into
mainfrom
feature/dub-70--dry-run-flag-on-every-mutating-command

Conversation

@dubscode

Copy link
Copy Markdown
Contributor

Summary

Closes DUB-70.

Extend --dry-run from the four commands that already had it
(submit, post-merge, merge-next, prune) to every other mutating
command 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-run prints a structured plan and exits without touching
    .git/dubstack/state.json, the undo log, or any git ref.
  • --dry-run --json emits the plan as { schemaVersion, ...plan } via
    withSchemaVersion, so MCP / scripted callers can consume it.
  • AI-mode commands (absorb --ai, split --ai, create --ai, squash --ai)
    skip the provider call in dry-run so previewing never bills the user.
  • Each command's Result now carries a dryRun: boolean so callers can
    branch on it without re-parsing flags.
  • --help for each command mentions --dry-run with at least one example.

Implementation notes

  • A new emitDryRunPlan(plan) helper in packages/cli/src/index.ts wraps
    the plan with withSchemaVersion and activates JSON-mode error handling.
  • Library helpers (lib/freeze.ts, lib/track.ts, lib/untrack.ts,
    lib/delete.ts, lib/sync/types.ts) thread dryRun through their
    options and bail before writeState, saveUndoEntry, pushBranch,
    checkoutBranch, fetchBranches, etc.
  • sync --dry-run is scope-only on purpose: it reports the roots and
    branches 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.
  • New freeze --dry-run unit test asserts plan is reported, frozen
    flags are not written, and the undo entry from a prior create is
    left untouched.
  • Existing track / untrack / delete / move / continue mocks
    updated to reflect the new dryRun: boolean field on their result
    types so the suite still type-checks.
  • Adversarial review caught two regressions which are fixed in this
    branch:
    - absorb --ai --dry-run was firing the AI call before bailing.
    Hoisted the dry-run return above aiPickTargets(...).
    - pop --dry-run was running assertBranchesNotCheckedOutElsewhere
    unconditionally. Now wrapped in if (!dryRun).

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
Copilot AI review requested due to automatic review settings May 25, 2026 20:58
@vercel

vercel Bot commented May 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
dubstack Skipped Skipped May 25, 2026 9:54pm

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 dryRun through command/library option types and return shapes (adds dryRun: boolean to 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.

Comment thread packages/cli/src/lib/track.ts Outdated
Comment thread packages/cli/src/commands/create.ts
Comment thread packages/cli/src/commands/unlink.ts
Comment thread packages/cli/src/commands/move.ts
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
@dubscode

Copy link
Copy Markdown
Contributor Author

@copilot please re-review — addressed all 4 inline comments in 0bb76d2:

  • track.ts: dry-run now falls back to in-memory empty state
  • create.ts: dry-run now runs read-only hasStagedChanges/isWorkingTreeClean so the plan matches a real run
  • unlink.ts: newStackId is now a deterministic <would-create-new-stack> placeholder
  • move.ts: retargeted is empty in dry-run; new retargetCandidates field carries the unverified upper bound

Tests added for each behavior change (packages/cli/test/commands/dry-run-coverage.test.ts).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 33 changed files in this pull request and generated 6 comments.

Comment thread packages/cli/src/commands/split.ts
Comment thread packages/cli/src/index.ts
Comment thread packages/cli/src/index.ts
Comment thread packages/cli/src/commands/create.ts Outdated
Comment thread packages/cli/src/lib/track.ts
Comment thread packages/cli/src/commands/revert.ts

Copilot AI commented May 25, 2026

Copy link
Copy Markdown

@copilot please re-review — addressed all 4 inline comments in 0bb76d2:

  • track.ts: dry-run now falls back to in-memory empty state...

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 test/commands/dry-run-coverage.test.ts successfully with those cases covered, and I don’t see further follow-up changes needed on this thread.

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:

  • fonts.googleapis.com
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node node /home/REDACTED/work/dubstack/dubstack/apps/docs/node_modules/.bin/../../../../node_modules/.pnpm/next@16.2.6_@opentelemetry&#43;api@1.9.0_react-dom@19.2.4_react@19.2.4__react@19.2.4/node_modules/next/dist/bin/next build (dns block)
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node node /home/REDACTED/work/dubstack/dubstack/apps/docs/node_modules/.bin/../../../../node_modules/.pnpm/next@16.2.6_@opentelemetry&#43;api@1.9.0_react-dom@19.2.4_react@19.2.4__react@19.2.4/node_modules/next/dist/bin/next build git rev-�� --verify refs/heads/feat/b de/node/bin/git anches/feat/a 960c3e2d tnet/tools/git git upda�� refs/dubstack/branches/feat/a a64085fda499e8e8c0febbe8ec1f070d57d8f205 sh --show-toplevel git it git (dns block)

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
@dubscode dubscode enabled auto-merge (squash) May 25, 2026 21:55
@dubscode dubscode merged commit 9011c25 into main May 25, 2026
12 checks passed
@dubscode dubscode deleted the feature/dub-70--dry-run-flag-on-every-mutating-command branch May 25, 2026 21:56
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.10.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants