v0.1.80 63e28ad

worktree.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close