Find every stale worktree, not just the managed ones
- discover worktrees from `git worktree list` on each source checkout, not only the worktree root, so hand-made ones, stale registrations and orphaned directories are classified too - normalise each candidate to its main checkout, so a linked worktree in the source root cannot offer up the repo's real checkout - keep locked, mid-rebase and detached-with-unique-commits worktrees, whose removal would destroy state nothing else holds - name the retained branch in every unproven verdict - add --no-fetch, --json, --include-unmanaged and --include-keep
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -25,6 +26,10 @@ type Worktree struct {
|
||||
Branch string // short branch name ("" when detached or bare)
|
||||
Bare bool
|
||||
Detached bool
|
||||
Locked bool
|
||||
// Prunable is git's own reason a registration is stale (e.g. "gitdir file
|
||||
// points to non-existent location"); empty when the worktree is intact.
|
||||
Prunable string
|
||||
}
|
||||
|
||||
// runGit runs git with args, using dir as the working directory (empty = the
|
||||
@@ -114,6 +119,54 @@ func GitIsDirty(dir string) (bool, error) {
|
||||
return strings.TrimSpace(out) != "", nil
|
||||
}
|
||||
|
||||
// GitDir returns the absolute path to the git directory backing the checkout at
|
||||
// dir (per-worktree, unlike GitCommonDir).
|
||||
func GitDir(dir string) (string, error) {
|
||||
return runGit(dir, "rev-parse", "--path-format=absolute", "--git-dir")
|
||||
}
|
||||
|
||||
// inProgressMarkers maps a sentinel inside the git dir to the operation it means
|
||||
// is half-finished. Such a checkout holds state that lives nowhere else.
|
||||
var inProgressMarkers = []struct{ path, op string }{
|
||||
{"rebase-merge", "rebase"},
|
||||
{"rebase-apply", "rebase"},
|
||||
{"MERGE_HEAD", "merge"},
|
||||
{"CHERRY_PICK_HEAD", "cherry-pick"},
|
||||
{"REVERT_HEAD", "revert"},
|
||||
{"BISECT_LOG", "bisect"},
|
||||
}
|
||||
|
||||
// GitInProgressOp names the sequencer operation underway in the checkout at dir,
|
||||
// or "" when none is.
|
||||
func GitInProgressOp(dir string) (string, error) {
|
||||
gitDir, err := GitDir(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, m := range inProgressMarkers {
|
||||
if _, err := os.Stat(filepath.Join(gitDir, m.path)); err == nil {
|
||||
return m.op, nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// GitCommitsNotOnRemotes counts commits reachable from HEAD that no
|
||||
// remote-tracking ref holds, i.e. work that exists only in this checkout.
|
||||
func GitCommitsNotOnRemotes(dir string) (int, error) {
|
||||
out, err := runGit(dir, "rev-list", "--count", "HEAD", "--not", "--remotes")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, err := strconv.Atoi(strings.TrimSpace(out))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parse rev-list count %q: %w", out, err)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// GitIsAncestor reports whether ancestor is reachable from descendant.
|
||||
func GitIsAncestor(repoDir, ancestor, descendant string) (bool, error) {
|
||||
cmd := exec.Command("git", "merge-base", "--is-ancestor", ancestor, descendant)
|
||||
@@ -277,6 +330,18 @@ func ParseWorktreeList(out string) []Worktree {
|
||||
if cur != nil {
|
||||
cur.Detached = true
|
||||
}
|
||||
case "locked":
|
||||
if cur != nil {
|
||||
cur.Locked = true
|
||||
}
|
||||
case "prunable":
|
||||
if cur != nil {
|
||||
// git omits the reason when it has none, so record the flag itself.
|
||||
cur.Prunable = val
|
||||
if cur.Prunable == "" {
|
||||
cur.Prunable = "prunable"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
flush()
|
||||
|
||||
Reference in New Issue
Block a user