add agentws prune
Agents leave their managed worktrees behind, and `agentws rm` takes one path at a time with no idea whether a branch's work is safely upstream, so clearing an accumulation by hand risks destroying unmerged commits. - classify every managed worktree: dirty, PR open, upstream, or unproven - remove only what is safe; delete the local branch only when work is upstream - prove "upstream" with merge-base and git cherry, so squash merges count - match a PR by head.label, which survives the branch deletion a merge does - dry run by default; --yes applies, --keep-branches spares every branch - read the Gitea path from origin's URL rather than assuming the owner
This commit is contained in:
@@ -2,6 +2,7 @@ package agent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -67,6 +68,14 @@ func GitFetch(repoDir, remote string, globalArgs ...string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// GitFetchPrune runs `git fetch --prune <remote>` in repoDir so remote-tracking
|
||||
// refs for branches deleted on the remote (e.g. after a merge) disappear.
|
||||
func GitFetchPrune(repoDir, remote string, globalArgs ...string) error {
|
||||
args := append(append([]string{}, globalArgs...), "fetch", "--prune", remote)
|
||||
_, err := runGit(repoDir, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
// GitRemoteDefaultBranch returns the short name of remote's default branch
|
||||
// (e.g. "main") by resolving refs/remotes/<remote>/HEAD.
|
||||
func GitRemoteDefaultBranch(repoDir, remote string) (string, error) {
|
||||
@@ -83,6 +92,64 @@ func GitBranchExists(repoDir, branch string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// GitRemoteURL returns the configured URL for a remote.
|
||||
func GitRemoteURL(repoDir, remote string) (string, error) {
|
||||
return runGit(repoDir, "remote", "get-url", remote)
|
||||
}
|
||||
|
||||
// GitRemoteBranchExists reports whether a remote-tracking ref for branch exists
|
||||
// (accurate only after a pruning fetch).
|
||||
func GitRemoteBranchExists(repoDir, remote, branch string) bool {
|
||||
_, err := runGit(repoDir, "show-ref", "--verify", "--quiet", "refs/remotes/"+remote+"/"+branch)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// GitIsDirty reports whether the checkout at dir has uncommitted or untracked
|
||||
// changes.
|
||||
func GitIsDirty(dir string) (bool, error) {
|
||||
out, err := runGit(dir, "status", "--porcelain")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return strings.TrimSpace(out) != "", 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)
|
||||
cmd.Dir = repoDir
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
// Exit 1 is the documented "not an ancestor" answer; anything else is a
|
||||
// real failure (bad ref, not a repo).
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("git merge-base --is-ancestor %s %s: %w: %s",
|
||||
ancestor, descendant, err, strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// GitUnmergedCommits counts commits on head whose patch has no equivalent on
|
||||
// upstream, using `git cherry` so squash- and rebase-merged work is recognised
|
||||
// despite its rewritten SHAs.
|
||||
func GitUnmergedCommits(repoDir, upstream, head string) (int, error) {
|
||||
out, err := runGit(repoDir, "cherry", upstream, head)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n := 0
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "+") {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// GitWorktreeAdd adds a worktree at path checked out to branch. When the branch
|
||||
// already exists it is reused; otherwise it is created from startPoint.
|
||||
func GitWorktreeAdd(repoDir, path, branch, startPoint string) error {
|
||||
|
||||
Reference in New Issue
Block a user