Skip to content

Skip worktrees with a live process inside them #66

Open

Description

@noirbizarre

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, mutually overrides_with each other — the same shape as the
    existing --worktrunk / --no-worktrunk pair (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 a resolve_check_busy
    next to them, expose sync.checkbusy through git sync config set and 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-busy the 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/*/cwd symlinks.
    • macOS/BSD: lsof -a -d cwd -Fn fallback.
    • Neither available: the guard simply never fires; classification carries on as today, without
      an error (same degradation contract as the git >= 2.38 merge_adds_nothing strategy).
  • 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 running git sync from inside a linked
    worktree pins that worktree.
  • Add WorktreeGuard::Busy next to Locked / 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 in src/report.rs.
  • --force may override it, consistent with the dirty/unmerged path — but plain --yes must not.

Acceptance

  • Busy worktree is skipped by git sync -y, with a message identifying the process.
  • --no-check-busy skips the scan entirely and restores today's behaviour.
  • sync.checkbusy = false has the same effect; --check-busy overrides it back on.
  • Absent flag and absent config → check is enabled.
  • An idle zsh sitting in the worktree does not mark it busy.
  • Running git sync from inside the worktree does not mark it busy.
  • Platform without /proc and without lsof: no error, no behaviour change.
  • Cost is one scan per invocation; measured on a repo with 20+ worktrees.
  • Unit tests (spawn a sleep with cwd in a fixture worktree) + integration test + README
    (usage flags table, configuration option table, git sync config docs, man page).

Activity

  1. noirbizarre commented on Aug 16, 2026

    @noirbizarre
    OwnerAuthor

    Follow-up from #71: git sync status shipped without the busy status, which was deferred here.

    When this lands, git sync status needs:

    • a busy field on status::Flags and a busy token in status::status_tokens (src/status.rs), which renders into the combined STATUS column alongside orphan, locked, merged, gone, unmerged, dirty and clean. Suggested position: right after locked, since both are "hands off" signals;
    • a StatusFlag::Busy variant in src/report.rs — an added enum variant in an array-valued field, so Report::VERSION stays 1;
    • the one process-table scan per run described above, reused for every row, and skipped entirely under --no-check-busy / sync.checkbusy = false. status is meant to be cheap and safe to run from a shell prompt, so the flag and config must be honoured there too.

    status never removes anything, so busy is 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 ### Status section.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions