diff --git a/bin/.local/bin/herdr-prune-worktrees b/bin/.local/bin/herdr-prune-worktrees new file mode 100755 index 0000000..4f8ffef --- /dev/null +++ b/bin/.local/bin/herdr-prune-worktrees @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# +# Removes herdr git worktrees (~/.herdr/worktrees//) that have had no +# activity for the given number of days. Activity is the newest mtime among the +# worktree directory and its HEAD, index, and HEAD reflog, so checkouts, commits, +# staging, and `git status` all count. +# +# Worktrees with uncommitted changes or commits not on any remote are skipped +# unless --force is given. Branches are left in place. + +set -u + +usage() { + cat >&2 <&2 '%s: --days requires a non-negative integer\n' "${0##*/}" + exit 2 + fi + days=$2 + shift + ;; + -h|--help) usage; exit 0 ;; + *) usage; exit 2 ;; + esac + shift +done + +root=${HERDR_WORKTREES:-$HOME/.herdr/worktrees} +if [[ ! -d $root ]]; then + printf >&2 '%s: %s does not exist\n' "${0##*/}" "$root" + exit 1 +fi + +cutoff=$(( $(date +%s) - days * 86400 )) +removed=0 +skipped=0 +failed=0 + +for wt in "$root"/*/*/; do + wt=${wt%/} + name=${wt#"$root"/} + [[ -f $wt/.git ]] || continue + + gitdir=$(sed -n 's/^gitdir: //p' "$wt/.git") + if [[ ! -d $gitdir ]]; then + printf 'skip %s (orphaned: %s is missing; run `git worktree prune` in the parent repo)\n' "$name" "$gitdir" + (( skipped++ )) + continue + fi + + last_active=$(stat -c %Y "$wt" "$gitdir"/{HEAD,index,logs/HEAD} 2>/dev/null | sort -n | tail -1) + (( last_active < cutoff )) || continue + last_active_date=$(date -d "@$last_active" +%F) + + if (( !force )); then + if [[ -n $(git -C "$wt" status --porcelain 2>/dev/null) ]]; then + printf 'skip %s (last active %s; uncommitted changes)\n' "$name" "$last_active_date" + (( skipped++ )) + continue + fi + if [[ -n $(git -C "$wt" log -1 --format=%h HEAD --not --remotes 2>/dev/null) ]]; then + printf 'skip %s (last active %s; unpushed commits)\n' "$name" "$last_active_date" + (( skipped++ )) + continue + fi + fi + + if (( dry_run )); then + printf 'would remove %s (last active %s)\n' "$name" "$last_active_date" + (( removed++ )) + continue + fi + + main_wt=$(git -C "$wt" worktree list --porcelain | sed -n '1s/^worktree //p') + force_args=() + (( force )) && force_args=(--force) + if git -C "$main_wt" worktree remove "${force_args[@]}" "$wt"; then + printf 'removed %s (last active %s)\n' "$name" "$last_active_date" + (( removed++ )) + else + printf >&2 'failed %s\n' "$name" + (( failed++ )) + fi +done + +if (( !dry_run )); then + find "$root" -mindepth 1 -maxdepth 1 -type d -empty -delete +fi + +verb=Removed +(( dry_run )) && verb='Would remove' +printf '\n%s %d worktree(s); skipped %d; failed %d (inactive for %d+ days)\n' \ + "$verb" "$removed" "$skipped" "$failed" "$days" +(( failed == 0 ))