Skip to content

fix(cleanup): date worktree cleanup from git activity, not Pebble metadata edits - #125

Merged
TsekaLuk merged 1 commit into
mainfrom
port/worktree-git-activity
Aug 10, 2026
Merged

fix(cleanup): date worktree cleanup from git activity, not Pebble metadata edits#125
TsekaLuk merged 1 commit into
mainfrom
port/worktree-git-activity

Conversation

@TsekaLuk

Copy link
Copy Markdown
Contributor

Closes part of #81 (third acceptance criterion: cleanup should use reflog
timestamps rather than a signal git maintenance can forge).

Problem

Worktree.LastActivityAt has exactly two writers, both in
applyWorktreeMetadataUpdate: renaming a worktree, and editing its comment.
Nothing stamps it on creation, on session activity, or on any git operation —
so for practically every worktree it is 0.

workspaceCleanupReasons then does:

idle := time.Duration(scannedAt-worktree.LastActivityAt) * time.Millisecond
if idle >= workspaceCleanupIdle { // 30 days
    reasons = append(reasons, "idle-clean")
}

scannedAt - 0 is about 58 years. Every clean, non-main, non-folder worktree
clears the 30-day threshold, so the cleanup scan offers up worktrees the user
was working in an hour ago. cleanupEligibleWorktrees has no other gate — the
git evidence read that follows only adds blockers for dirty trees, so a clean
worktree in daily use is presented as an idle deletion candidate.

Change

Take the activity signal from git rather than from Pebble's own metadata edits.

worktreeGitActivityAt reads the timestamp recorded inside the newest
reflog entry of the worktree's git directory — resolving the gitdir: pointer
that git worktree add leaves in place of a .git directory — and raises
LastActivityAt to it when it is newer than the stored value.

Deliberately not used as signals:

  • the reflog file's mtime. git maintenance and a bare git status
    rewrite logs/HEAD in linked worktrees, which reports an untouched worktree
    as freshly active. This is the specific false signal upstream #12131 removed;
    reading the entry contents is immune to it.
  • index and gitdir mtimes, restamped by the same background work.

When the reflog has been expired to an empty file there is no entry to read, so
HEAD, COMMIT_EDITMSG and ORIG_HEAD mtimes stand in — metadata git only
writes when something real happened.

The probe runs for local projects only. An SSH worktree's path does not
exist on this machine; stat-ing it here would read an unrelated local directory
or nothing at all. Remote projects keep going through
disconnectedWorkspaceCleanupCandidate unchanged.

It reads files directly rather than shelling out to git, so a scan across
many worktrees costs no subprocesses, and only a bounded 8 KB tail of the
reflog is read.

Fixture change

createCleanupGitFixture now runs git with GIT_AUTHOR_DATE /
GIT_COMMITTER_DATE backdated. TestWorkspaceCleanupEmitsIncrementalProgress
declares LastActivityAt: -45 days but built its worktree with
git worktree add moments earlier, so git rightly reported it as seconds old
and it stopped being a candidate — the test blocked forever on its progress
channel. A fixture standing in for an abandoned worktree has to look abandoned
to git too. That hang is itself the proof the new signal reaches the classifier.

Tests

git_worktree_activity_test.go pins the reflog contract: newest entry wins; a
restamped logs/HEAD mtime does not leak in; index / gitdir are not
activity; an expired reflog degrades to commit metadata; the bounded tail read
still finds the newest entry in a 600-entry reflog; linked worktrees and plain
repositories both resolve; malformed and dangling layouts report no signal; and
committer names containing spaces (or a >) still parse.

Two classifier tests cover the actual behaviour change: a worktree git says is
active is no longer offered for cleanup, a worktree git says is stale still is,
and a stored stamp newer than the git signal is never lowered.

Five mutations were checked and each fails at least one test: trusting the
reflog mtime, counting index as activity, taking the oldest entry in the
tail, letting the git signal lower a newer stored stamp, and dropping linked
worktree resolution.

Verification

  • go test ./internal/runtimecore/ -count=1 green (17s).
  • go test ./internal/runtimecore/ -race -count=1 green (20s).
  • go vet clean; gofmt clean on all touched files.
  • node config/scripts/verify-tauri-mainline.mjs passes, with two new rules
    pinning the excluded-file list and the local-only guard.

verify does not run Go tests (.github/workflows/pr.yml has no setup-go
and no go test), so all of the above was run locally.

Not in scope

The other three acceptance criteria on #81 — Windows setup shell, non-blocking
delete, and avoiding a redundant fetch on delete — are untouched here and the
issue stays open for them.

…adata edits

Worktree.LastActivityAt is only ever written by applyWorktreeMetadataUpdate,
i.e. when the user renames a worktree or edits its comment. It is never
stamped on creation, on session activity, or on any git operation, so it
stays 0 for practically every worktree.

workspaceCleanupReasons computes idle = scannedAt - LastActivityAt against a
30-day threshold. With LastActivityAt at 0 that difference is ~58 years, so
every clean non-main worktree is classified "idle-clean" and offered up as a
cleanup candidate no matter how recently it was used.

Read git's own record instead: the timestamp inside the newest reflog entry
of the worktree's git directory, falling back to HEAD / COMMIT_EDITMSG /
ORIG_HEAD mtimes when the reflog has been expired away. The reflog file's own
mtime is deliberately not used, and neither are `index` or `gitdir` — git
maintenance and a bare `git status` restamp all three, which is the false
activity signal upstream #12131 removed.

Applied to local projects only: an SSH worktree's path does not exist on this
machine, so probing it here would stat an unrelated local directory.

The cleanup fixture now runs git with GIT_COMMITTER_DATE backdated, because a
worktree standing in for an abandoned one has to look abandoned to git too.
@TsekaLuk
TsekaLuk force-pushed the port/worktree-git-activity branch from f99ef68 to b2ff97e Compare August 10, 2026 01:11
@TsekaLuk
TsekaLuk merged commit 1a208d2 into main Aug 10, 2026
3 of 5 checks passed
@TsekaLuk
TsekaLuk deleted the port/worktree-git-activity branch August 10, 2026 01:33
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