v0.51.0 a9d674e

worktree.go RawHistoryFolder

1package tools
2
3import (
4	"context"
5	"encoding/json"
6	"errors"
7	"fmt"
8	"strings"
9
10	"github.com/bnema/zut/packages/core"
11)
12
13// WorktreeTool exposes persistent worktree creation and lifecycle management
14// through one public tool while keeping the implementations focused internally.
15type WorktreeTool struct {
16	Create *CreateWorktreeTool
17	Manage *ManageWorktreesTool
18}
19
20const worktreeSchema = `{
21  "type":"object",
22  "properties":{
23    "action":{"type":"string","enum":["create","list","cleanup"],"description":"Create a branch worktree, list repository worktrees, or remove explicitly selected safe worktrees."},
24    "branch":{"type":"string","description":"New branch name. Required for create."},
25    "base":{"type":"string","description":"create only: branch, tag, or commit the new branch starts from. Defaults to the current HEAD. For a sub-worktree of a feature branch (for example feat/x-api from feat/x), set base to that feature branch."},
26    "paths":{"type":"array","items":{"type":"string"},"description":"Exact registered worktree paths. Required for cleanup."},
27    "into":{"type":"string","description":"cleanup/list: branch, tag, or commit the worktree branches must be merged into to be removable. Defaults to the current HEAD. After merging sub-branches into a feature branch, set into to that feature branch."},
28    "delete_branch":{"type":"boolean","description":"cleanup only: also delete each removed worktree's local branch, only if it is still at the inspected commit. Never deletes the merge target or a branch checked out elsewhere."}
29  },
30  "required":["action"]
31}`
32
33func (t *WorktreeTool) Name() string { return "worktree" }
34func (t *WorktreeTool) Description() string {
35	return "Create, list, and safely clean up Git worktrees. Supports sub-worktrees based on a feature branch and cleanup of branches merged into it."
36}
37func (t *WorktreeTool) Schema() json.RawMessage { return json.RawMessage(worktreeSchema) }
38
39func (t *WorktreeTool) Preview(ctx context.Context, raw json.RawMessage) (core.ToolResult, error) {
40	action, delegated, err := parseWorktreeAction(raw)
41	if err != nil {
42		return core.ToolResult{}, err
43	}
44	switch action {
45	case "create":
46		if t.Create == nil {
47			return core.ToolResult{}, errors.New("worktree: create action is unavailable")
48		}
49		result, err := t.Create.Preview(ctx, delegated)
50		return result, worktreeError(err)
51	case "list", "cleanup":
52		if t.Manage == nil {
53			return core.ToolResult{}, errors.New("worktree: management actions are unavailable")
54		}
55		result, err := t.Manage.Preview(ctx, delegated)
56		return result, worktreeError(err)
57	default:
58		return core.ToolResult{}, errors.New("worktree: unknown action")
59	}
60}
61
62func (t *WorktreeTool) Execute(ctx context.Context, raw json.RawMessage, progress func(string)) (core.ToolResult, error) {
63	action, delegated, err := parseWorktreeAction(raw)
64	if err != nil {
65		return core.ToolResult{}, err
66	}
67	switch action {
68	case "create":
69		if t.Create == nil {
70			return core.ToolResult{}, errors.New("worktree: create action is unavailable")
71		}
72		result, err := t.Create.Execute(ctx, delegated, progress)
73		return result, worktreeError(err)
74	case "list", "cleanup":
75		if t.Manage == nil {
76			return core.ToolResult{}, errors.New("worktree: management actions are unavailable")
77		}
78		result, err := t.Manage.Execute(ctx, delegated, progress)
79		return result, worktreeError(err)
80	default:
81		return core.ToolResult{}, errors.New("worktree: unknown action")
82	}
83}
84
85func parseWorktreeAction(raw json.RawMessage) (string, json.RawMessage, error) {
86	var values map[string]json.RawMessage
87	if err := json.Unmarshal(raw, &values); err != nil {
88		return "", nil, err
89	}
90	var action string
91	if err := json.Unmarshal(values["action"], &action); err != nil || action == "" {
92		return "", nil, errors.New("worktree: action is required")
93	}
94	delete(values, "action")
95	var fields map[string]any
96	switch action {
97	case "list":
98		fields = map[string]any{"action": action, "into": rawField(values, "into")}
99	case "cleanup":
100		fields = map[string]any{
101			"action":        action,
102			"paths":         rawField(values, "paths"),
103			"into":          rawField(values, "into"),
104			"delete_branch": rawField(values, "delete_branch"),
105		}
106	default:
107		fields = map[string]any{"branch": rawField(values, "branch"), "base": rawField(values, "base")}
108	}
109	encoded, err := json.Marshal(fields)
110	return action, encoded, err
111}
112
113func worktreeError(err error) error {
114	if err == nil {
115		return nil
116	}
117	message := err.Error()
118	for _, prefix := range []string{"create_worktree: ", "manage_worktrees: "} {
119		if strings.HasPrefix(message, prefix) {
120			message = strings.TrimPrefix(message, prefix)
121			break
122		}
123	}
124	return fmt.Errorf("worktree: %s", message)
125}
126
127func rawField(values map[string]json.RawMessage, key string) any {
128	value, ok := values[key]
129	if !ok {
130		return nil
131	}
132	var decoded any
133	if json.Unmarshal(value, &decoded) != nil {
134		return nil
135	}
136	return decoded
137}

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