Repository navigation
Skip worktrees with a live process inside them #66
Description
Problem
git sync decides a worktree is safe to remove from git state alone: is the branch merged, is the
tree dirty (Git::worktree_dirty, src/git.rs:829), does it have unmerged commits
(branch_has_unmerged_commits, src/git.rs:845), is it locked or too young
(worktree_guard, src/cleaner.rs:990).
None of that says whether someone is working in it right now. A worktree can be perfectly merged
and perfectly clean while a coding agent, a dev server, a test watcher or a build is running inside
it with uncommitted work still in memory. git sync -y removes it, and that work is gone.
This is increasingly common: agent harnesses create a worktree per session, commit and push, and
keep running. The branch reads merged, the tree reads clean, and we delete the ground from under
the session.
Proposal
Add a "busy" guard: a worktree whose path is the current working directory of a live process is
never removed, regardless of merge state, and regardless of --yes.
Whether to consult the process table at all is the user's decision, so this ships as a flag
plus a git-config setting rather than as unconditional behaviour:
--check-busy/--no-check-busy, mutuallyoverrides_witheach other — the same shape as the
existing--worktrunk/--no-worktrunkpair (src/cli.rs).sync.checkbusy(bool) in git-config.- Enabled by default. Precedence follows
resolve_worktrunk/resolve_effort
(src/main.rs:424-449): CLI flag > git config > default (true). Add aresolve_check_busy
next to them, exposesync.checkbusythroughgit sync config setand the config listing, and
offer it in the setup wizard only if it proves worth a question — otherwise leave it to config. - With
--no-check-busythe scan is skipped entirely, so users who do not want the process-table
read pay nothing and get exactly today's behaviour.
Detection:
- One process-table scan per run, not one per worktree, and only when the check is enabled.
- Linux: walk
/proc/*/cwdsymlinks. - macOS/BSD:
lsof -a -d cwd -Fnfallback. - Neither available: the guard simply never fires; classification carries on as today, without
an error (same degradation contract as the git >= 2.38merge_adds_nothingstrategy).
- Linux: walk
- Exclude interactive shells (
bash,zsh,fish,sh,nu, ...) — a terminal tab parked in a
finished worktree must not pin it forever. - Exclude
git-sync's own process ancestry, otherwise runninggit syncfrom inside a linked
worktree pins that worktree. - Add
WorktreeGuard::Busynext toLocked/TooYoung(src/cleaner.rs:963) with a skip
message naming the offending process (pid, comm) so the user can act. - Report it in JSON: a new
ItemStatus/ skip reason insrc/report.rs. --forcemay override it, consistent with the dirty/unmerged path — but plain--yesmust not.
Acceptance
- Busy worktree is skipped by
git sync -y, with a message identifying the process. -
--no-check-busyskips the scan entirely and restores today's behaviour. -
sync.checkbusy = falsehas the same effect;--check-busyoverrides it back on. - Absent flag and absent config → check is enabled.
- An idle
zshsitting in the worktree does not mark it busy. - Running
git syncfrom inside the worktree does not mark it busy. - Platform without
/procand withoutlsof: no error, no behaviour change. - Cost is one scan per invocation; measured on a repo with 20+ worktrees.
- Unit tests (spawn a
sleepwith cwd in a fixture worktree) + integration test + README
(usage flags table, configuration option table,git sync configdocs, man page).
Activity
noirbizarre commentedon Aug 16, 2026
Follow-up from #71:
git sync statusshipped without thebusystatus, which was deferred here.When this lands,
git sync statusneeds:busyfield onstatus::Flagsand abusytoken instatus::status_tokens(src/status.rs), which renders into the combinedSTATUScolumn alongsideorphan,locked,merged,gone,unmerged,dirtyandclean. Suggested position: right afterlocked, since both are "hands off" signals;StatusFlag::Busyvariant insrc/report.rs— an added enum variant in an array-valued field, soReport::VERSIONstays1;--no-check-busy/sync.checkbusy = false.statusis meant to be cheap and safe to run from a shell prompt, so the flag and config must be honoured there too.statusnever removes anything, sobusyis purely informational there — but it is arguably the single most useful column for the "what is lying around" question the command answers.Also update the status vocabulary table in the README
### Statussection.feat(status): add a read-only inventory subcommand (#71)