1package swarm
2
3import (
4 "errors"
5 "fmt"
6 "os"
7 "os/exec"
8 "path/filepath"
9 "strings"
10)
11
12// gitWorktree creates per-agent working directories via `git worktree
13// add`. If the repo path isn't a git repository it falls back to a
14// plain mkdir under root/<id>, which is still useful for tests and
15// for running agents in non-git directories.
16type gitWorktree struct {
17 root string // <swarm-root>/worktrees
18 repo string // user CWD; git operations resolve from here
19}
20
21// Create makes a new worktree on branch off of HEAD. The branch is
22// created if it doesn't already exist. Returns the absolute path.
23func (g *gitWorktree) Create(id, branch, base string) (string, error) {
24 dir := filepath.Join(g.root, id)
25 if err := os.MkdirAll(g.root, 0o755); err != nil {
26 return "", err
27 }
28 if !isGitRepo(g.repo) {
29 // Non-git fallback: just a fresh directory. The agent works
30 // here; nothing is staged for merge.
31 if err := os.MkdirAll(dir, 0o755); err != nil {
32 return "", err
33 }
34 return dir, nil
35 }
36 cmd := exec.Command("git", "worktree", "add", "-b", branch, dir)
37 cmd.Dir = g.repo
38 out, err := cmd.CombinedOutput()
39 if err != nil {
40 // If the branch already exists (e.g. a leftover from a
41 // previous run), retry without -b so the user doesn't need
42 // to clean up by hand.
43 if strings.Contains(string(out), "already exists") {
44 cmd = exec.Command("git", "worktree", "add", dir, branch)
45 cmd.Dir = g.repo
46 out, err = cmd.CombinedOutput()
47 }
48 }
49 if err != nil {
50 return "", fmt.Errorf("git worktree add: %s: %w", strings.TrimSpace(string(out)), err)
51 }
52 return dir, nil
53}
54
55// Remove deletes the worktree. Uses `git worktree remove --force` so
56// dirty trees don't block cleanup; the user is the one running this
57// command explicitly.
58func (g *gitWorktree) Remove(id, dir string) error {
59 if dir == "" {
60 return errors.New("empty worktree dir")
61 }
62 if !isGitRepo(g.repo) {
63 return os.RemoveAll(dir)
64 }
65 cmd := exec.Command("git", "worktree", "remove", "--force", dir)
66 cmd.Dir = g.repo
67 out, err := cmd.CombinedOutput()
68 if err != nil {
69 // `git worktree remove` refuses to operate on an unknown path;
70 // in that case fall back to a plain rmdir so the user can
71 // always clean up.
72 if strings.Contains(string(out), "not a working tree") {
73 return os.RemoveAll(dir)
74 }
75 return fmt.Errorf("git worktree remove: %s: %w", strings.TrimSpace(string(out)), err)
76 }
77 return nil
78}
79
80func isGitRepo(dir string) bool {
81 if dir == "" {
82 return false
83 }
84 cmd := exec.Command("git", "-C", dir, "rev-parse", "--git-dir")
85 return cmd.Run() == nil
86}
87
88// memWorktree is a WorktreeManager used by tests. It does no git
89// operations; it just makes a fresh subdirectory under root.
90type memWorktree struct{ root string }
91
92// MemWorktree returns a WorktreeManager that creates plain
93// subdirectories. Exposed for tests and for callers running zot
94// outside a git repository.
95func MemWorktree(root string) WorktreeManager { return &memWorktree{root: root} }
96
97func (m *memWorktree) Create(id, branch, base string) (string, error) {
98 dir := filepath.Join(m.root, id)
99 if err := os.MkdirAll(dir, 0o755); err != nil {
100 return "", err
101 }
102 return dir, nil
103}
104
105func (m *memWorktree) Remove(id, dir string) error {
106 if dir == "" {
107 return errors.New("empty worktree dir")
108 }
109 return os.RemoveAll(dir)
110}