Skip to content

Add sync subcommand - #58

Open
leogdion wants to merge 1 commit into
release-1.0.3from
50-sync
Open

Add sync subcommand#58
leogdion wants to merge 1 commit into
release-1.0.3from
50-sync

Conversation

@leogdion

@leogdion leogdion commented Aug 21, 2026

Copy link
Copy Markdown
Member

Implements git trees sync [worktree] [--pull] [--ff-only|--rebase].

Closes #50

Behavior

With no positional argument it covers every worktree; passing one names a single worktree, by branch name or by path.

The default is fetch only. It runs one git fetch --prune origin and stops — nothing in any working tree is touched, so there is no --apply gate and the command acts immediately. --pull additionally updates the working trees from the refs that fetch just brought in.

The branch name of each successfully updated worktree goes to stdout, one per line; every notice goes to stderr. The loop always runs to completion, accumulating failed, so a nonzero exit means partial success rather than a stop.

Why --ff-only is the default

A fast-forward is the only update that can neither discard work nor stop half-finished. A rebase can conflict, and a conflicted rebase leaves the worktree mid-rebase — an unacceptable state for a command whose whole point is touching every worktree in the container unattended. --rebase is available when you want it, but you have to ask for it.

Passing a strategy without --pull is an error rather than a silent no-op: sync --rebase that quietly only fetched would look like it had rebased.

One fetch for the whole container

sync runs exactly one git fetch --prune origin up front, not one per worktree. Every worktree in the layout shares a single object store, so a per-worktree fetch transfers nothing after the first and costs only network round-trips — on a container with a dozen worktrees that is a dozen round-trips for one fetch's worth of data.

Unlike clean's fetch, this one is not silenced. With no flags the fetch is the command's entire job, and a silent success would be indistinguishable from a no-op. If the fetch fails, sync returns 1 without pulling anything.

A positional target is resolved before the fetch, so a typo'd name does not fire a network operation.

Why it does not use git pull

git pull re-fetches on every invocation, which would undo the single-fetch design above — one fetch up front plus N more inside the loop. Instead the loop runs git merge --ff-only @{upstream} or git rebase @{upstream}, both of which work against the already-current remote-tracking refs, need no fetch of their own, and are idempotent.

Skip conditions under --pull

Situation Behavior
Detached HEAD Reported on stderr, not a failure — detaching is deliberate, and failing would make sync --pull permanently nonzero for anyone keeping such a worktree
No upstream Reported, git trees track named as the remedy, counted as a failure
Uncommitted changes Reported and skipped, counted as a failure
Diverged under --ff-only Reported, --rebase named as the remedy, counted as a failure
Rebase conflict Reported, worktree left mid-rebase with git rebase --abort named — deliberately not auto-aborted, which would discard the user's chance to resolve it

Dirtiness includes untracked files, matching the dirty column in git trees list and git worktree remove's own refusal. Documented in the README, since a stray .DS_Store is then enough to skip a pull. The check runs only under --pull — fetch-only never touches a work tree.

Notes for review

  • Two new helpers sit after _ref_info: _is_dirty and _worktree_paths. The latter filters the bare stanza that git worktree list --porcelain emits for the container's bare store, which has no work tree at all (git -C <bare> status exits 128), so any iteration over worktrees must drop it.
  • _sync_target is local to the sync section rather than factored out of cmd_rm. cmd_rm's resolution hard-codes its own messages and carries a worktree-registration gate that exists specifically to keep TREES_RM_CMD="rm -rf" away from the container root — a concern sync does not have. Extracting it would turn a purely additive change into one that rewrites the most safety-critical function in the file.
  • No new TREES_* env var; the env surface is unchanged at five.

Testing

57 new assertions in a sync section placed between rm and clean (clean stays last, since its fixtures mutate the shared $ORIGIN). The sync fixtures also mutate $ORIGIN but commit on feature-x only, never on main — stated in a comment. Every fixture step is asserted, so a silently failed setup cannot leave a branch already up to date and make later assertions pass against nothing.

Covered: fetch-only advances origin/feature-x while leaving the worktree HEAD and files untouched; --pull fast-forwards and names the branch on stdout; dirty worktree skipped with the uncommitted file preserved and the upstream change not applied; no-upstream skip; detached HEAD reported but not a failure; divergence under --ff-only reported with the local commit preserved; --rebase gets past the divergence keeping both commits; single target by branch name and by path; and the full argument-validation matrix.

All network-free, on file:// fixtures under mktemp -d.

bash -n git-trees && bash -n install.sh && bash -n tests/smoke.sh   # ok
shellcheck -s bash git-trees install.sh tests/smoke.sh             # clean
tests/smoke.sh ./git-trees                                          # 255 ok, 0 failures, exit 0

🤖 Generated with Claude Code


Release coordination (v1.0.3)

One of five PRs into release-1.0.3 (#56 prune, #58 sync, #60 completions, #59 curl install, #57 Homebrew). All five are green on CI (smoke on Linux + macOS).

Suggested merge order: #56#58#60#59#57. Only one pair conflicts: #58 vs #56.

Needs a rebase on release-1.0.3 after #56 merges. Both PRs append to the same two spots in tests/smoke.sh — the help lists … assertion block and the new-section insertion point before # --- clean ---. Two trivial adjacent-append conflicts, no semantic overlap; git-trees and README.md merge cleanly (the two commands were given deliberately opposite insertion anchors there).

Note: CodeRabbit skipped all five — "reviews are disabled for this base branch." These have not had automated review; that would come when release-1.0.3 merges to main.

The CHANGELOG is deliberately excluded from every PR — it needs the merged PR URLs, so it lands as one commit on release-1.0.3 before tagging v1.0.3.

`git trees sync [worktree] [--pull] [--ff-only|--rebase]` brings the
container up to date with origin. The default is fetch only, so it is
non-destructive and needs no --apply gate; --pull additionally updates
the working trees.

One `git fetch --prune origin` runs for the whole container rather than
one per worktree: they share a single object store, so a per-worktree
fetch transfers nothing after the first and costs only round-trips. That
is also why the pull loop uses `git merge --ff-only @{upstream}` and
`git rebase @{upstream}` instead of `git pull`, which would re-fetch
every time.

--ff-only is the default strategy — the only update that can neither
discard work nor stop half-finished. A strategy without --pull is an
error rather than a silent no-op.

Under --pull a worktree is skipped when it has a detached HEAD (reported
but not a failure, since detaching is deliberate), no upstream,
uncommitted changes, or has diverged under --ff-only. A rebase conflict
leaves the worktree mid-rebase rather than auto-aborting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 78ba2057-1740-4cbd-bef0-811fedacfd02

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant