Skip to content

residue v0.0.5

Choose a tag to compare

@github-actions github-actions released this 07 Nov 00:38
· 101 commits to main since this release
v0.0.5
9205512

Deletes what you saw, or nothing, and makes a scheduled sweep reversible. Until 0.0.4, remove re-ran the checks right before acting, but it compared the verdict, HEAD, branch, lock and the set of ignored entries, not what they held. Measured with scripts/race-check.sh against the 0.0.4 release binary: a file added inside node_modules/ while the prompt waited was deleted with the worktree (exit 0). And a scheduled sweep that took a paused agent's worktree took it for good.

  • Decision, the plan is a record: for each worktree it now keeps HEAD, the branch, the lock, the staged, modified and untracked files, the file count, size and newest modification time of every ignored entry, and the modification times of the worktree's index and logs/HEAD. Right before acting, residue compares a fresh record with it and skips on any difference, with a reason that names the difference: node_modules/ changed since you confirmed (1,204 -> 1,388 files), HEAD changed since you confirmed (a82c15ee7687 -> 55c48105f23a), changed since you confirmed: 1 untracked. Under --yes the reason says since the plan. logs/HEAD is fine here although --idle must not use it: a git gc between plan and removal only makes residue skip.
  • Decision, move aside before deleting: after the re-check, git worktree move renames the worktree to a hidden sibling, .NAME.residue-removing-PID, and residue locks it with the reason residue: removing. git worktree move is one rename by Git that updates only this worktree's record, and it refuses another filesystem instead of copying; residue skips a worktree that is a mount point. Not mv plus git worktree repair: repair rewrites the records of every worktree of the repository. At the new path residue checks again: Git lists it there with the planned HEAD, branch and residue's lock, a new process snapshot sees nobody inside it, and the record still matches. Then it unlocks it and git worktree remove, without --force, deletes it. On any difference or failure it moves the worktree back and unlocks it (...; it is back in place). This narrows the window between the last check and the deletion; it does not close it. From that check until Git finishes, a process that opens the hidden directory by its new name, or one the probe cannot see, can still write into it, and the write is deleted with it. The README says so.
  • A run killed partway leaves the worktree under its hidden name, still registered. scan lists it under interrupted removals with the one command that puts it back (git -C MAIN worktree unlock HIDDEN && git -C MAIN worktree move HIDDEN ORIGINAL, or git worktree remove HIDDEN when the directory is gone), and says when the PID that moved it still runs. Neither a sweep nor remove PATH touches such a worktree. Scan JSON: each repository gains interrupted, and summary gains interrupted_removals.
  • remove --trash moves each worktree into a trash instead of deleting it, and --idle turns it on, so a scheduled sweep is reversible; --no-trash deletes at once. Decision, where: residue-trash/ in the repository's Git common directory (.git/residue-trash/NAME-STAMP/). A move there is one rename on the repository's filesystem, where linked worktrees usually live; every worktree of the repository finds it; git status never shows it; and it goes away with the repository. A worktree on another filesystem is refused, never copied. Each entry holds the worktree and a record, info, with its original path, branch, HEAD, time and size. The worktree stays registered and locked (residue: in trash), so restore and purge are Git commands; its branch stays checked out there until then.
  • residue restore lists the trash of the repositories at or under the current directory; residue restore ENTRY (the entry name, the worktree's name or branch, or a path) moves it back with git worktree move and drops the entry. JSON schema residue.restore/v1.
  • Every remove run purges the trash entries older than --keep-trash DURATION (default 7d) in the repositories it covers, as part of the plan you confirm: each entry is checked again, without --idle (the trash's clock is --keep-trash), and deleted with git worktree remove without --force. The purge is what frees the disk, and the README says so. Remove JSON gains trash (enabled, keep, keep_seconds), purged, and trash_entry on each result; the result trashed is new. Scan JSON gains each repository's trash and summary.trash_entries. A sweep fails (exit 3) when an expired entry it meant to purge was skipped or failed, or a trash record could not be read; an entry kept because a check failed does not fail it.
  • README: "The trash", and "Run it on a schedule" with --keep-trash 7d in the command, the launchd plist and the systemd service.
  • JSON: each worktree's rule_sources is now its full effective list: --regenerable (command_line), the residue config file (residue_config), then its repository's Git config rules in the order Git reads them. Rules add up, so a duplicate applies once and its first entry is the source that wins. The top-level rule_sources stays the run-wide subset, now in the same order (command_line first). Before, a worktree listed only its Git config rules, so an agent judging one worktree had to merge two lists, and with Git config rules only, the top-level [] read as "no rules". The schemas stay at v1.
  • scripts/race-check.sh drives the release binary's prompt under a pseudo-terminal, changes the worktree while the prompt waits (an untracked file, a modified tracked file, a file in an ignored regenerable directory, a new commit, with and without --trash), answers yes, and checks for exit 3, a changed since you confirmed reason, and the worktree still in place; two control cases must be removed or trashed. CI runs it on macOS and both Linux runners.
  • Fix: a stale worktree in a sweep's kept list now says why it is stale.
  • residue watch ROOT...: stays running and removes a worktree when its work landed (its upstream branch was deleted and it has been unchanged for --grace, default 1h) or when the disk is low (below --min-free, default 10%: worktrees unchanged for --idle, default 2d, largest first, until free space is 5 points above). No schedule: it wakes on changes under refs/remotes, packed-refs and reftable (inotify on Linux, kqueue on macOS and the BSDs), on the moment a waiting worktree passes its wait, and on a filesystem dropping below the threshold. Decision: watch deletes nothing itself. Every removal is a residue remove --yes --idle --no-trash child process, so it makes only removals remove would make. Decision, no trash: --idle turns the trash on, and watch turns it off, because freeing the disk is its job, a trashed worktree keeps every byte until a later run purges it, and nothing would purge the trash of a repository where watch removes nothing more. Each of its runs purges that repository's trash entries older than 7 days, and the log names each one. --dry-run logs instead, --once acts once. One watch per user (a lock in $XDG_STATE_HOME/residue).
  • residue service install|status|uninstall: runs residue watch from login as a launchd agent on macOS or a systemd user service on Linux. Under Homebrew it starts the stable bin/residue link, so it survives brew upgrade.
  • FreeBSD: an activity probe, from the base system's procstat -f. Before, FreeBSD had no probe, so nothing was ever a candidate. A packaged lsof is not used: on FreeBSD 14 it listed processes without resolving their working directories, so a worktree in use came back as a candidate. The procstat parser fails closed: a line it cannot read makes activity unknown.
  • residue builds on illumos and Solaris, which have no tm_gmtoff. NetBSD, OpenBSD, DragonFly and illumos build but still have no probe, so nothing there is a candidate.
  • CI runs .github/smoke.sh on macOS, Linux x86_64 and arm64, and FreeBSD 14 as root and as an ordinary user: a clean candidate, a kept .env, a worktree a process is using, and a removal that keeps the branch.
  • Signed dnf and pacman repositories next to the apt one, at https://whiteheap.github.io/residue/rpm and https://whiteheap.github.io/residue/arch: dnf install residue on Fedora and RHEL, pacman -S residue on Arch and Omarchy, and dnf upgrade and pacman -Syu keep residue current. Every release updates all three. The .rpm files in the dnf repository are signed, and one key signs everything (packaging/signing-key.asc).