-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstate.go
More file actions
212 lines (194 loc) · 5.52 KB
/
Copy pathstate.go
File metadata and controls
212 lines (194 loc) · 5.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
package main
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
// Worker represents a single Claude Code worker tied to a git worktree and tmux session.
type Worker struct {
ID int `json:"id"`
Branch string `json:"branch"`
Worktree string `json:"worktree"`
Session string `json:"session"`
Status string `json:"status"`
GraftStatus string `json:"graft_status,omitempty"` // "", "active", "failed"
CreatedAt string `json:"created_at"`
DeletedAt string `json:"deleted_at,omitempty"`
SessionStarted bool `json:"session_started"`
PRNumber int `json:"pr_number,omitempty"`
PRState string `json:"pr_state,omitempty"` // "OPEN", "DRAFT", "MERGED", "CLOSED"
PRURL string `json:"pr_url,omitempty"`
}
// State is the persistent state for garrison in a given repo.
type State struct {
Repo string `json:"repo"`
NextID int `json:"next_id"`
Workers []Worker `json:"workers"`
DeletedWorkers []Worker `json:"deleted_workers,omitempty"`
}
const defaultGraftCommand = "yarn install && yarn run watch"
// Config holds per-repo tulip configuration, separate from ephemeral state.
type Config struct {
GraftCommand string `json:"graft_command,omitempty"`
}
// GraftCmd returns the configured graft command, falling back to the default.
func (c *Config) GraftCmd() string {
if c.GraftCommand != "" {
return c.GraftCommand
}
return defaultGraftCommand
}
// configPath returns the path to the tulip config file for a given repo root.
func configPath(repoRoot string) string {
return filepath.Join(repoRoot, ".tulip", "config.json")
}
// loadConfig loads config from disk, returning an empty Config if the file doesn't exist.
func loadConfig(repoRoot string) (*Config, error) {
data, err := os.ReadFile(configPath(repoRoot))
if err != nil {
if os.IsNotExist(err) {
return &Config{}, nil
}
return nil, err
}
var c Config
if err := json.Unmarshal(data, &c); err != nil {
return nil, err
}
return &c, nil
}
// saveConfig writes config to disk, creating directories as needed.
func saveConfig(repoRoot string, c *Config) error {
path := configPath(repoRoot)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
data, err := json.MarshalIndent(c, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0o644)
}
// findRepoRoot walks up from cwd until it finds a directory containing .git.
func findRepoRoot() (string, error) {
dir, err := os.Getwd()
if err != nil {
return "", err
}
for {
if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil {
return dir, nil
}
parent := filepath.Dir(dir)
if parent == dir {
return "", errors.New("not inside a git repository")
}
dir = parent
}
}
// statePath returns the path to the tulip state file for a given repo root.
func statePath(repoRoot string) string {
return filepath.Join(repoRoot, ".tulip", "state.json")
}
// loadState loads state from disk, returning an empty State if the file doesn't exist.
func loadState(repoRoot string) (*State, error) {
path := statePath(repoRoot)
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return &State{
Repo: repoRoot,
NextID: 1,
}, nil
}
return nil, err
}
var s State
if err := json.Unmarshal(data, &s); err != nil {
return nil, err
}
return &s, nil
}
// saveState writes state to disk, creating directories as needed.
func saveState(s *State) error {
path := statePath(s.Repo)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
data, err := json.MarshalIndent(s, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0o644)
}
// makeSessionName converts a branch name to a tmux-safe session name.
func makeSessionName(branch string) string {
name := strings.ReplaceAll(branch, "/", "-")
name = strings.ReplaceAll(name, ".", "-")
name = strings.ReplaceAll(name, ":", "-")
return "tulip-" + name
}
// addWorker creates a new Worker and appends it to the state, returning a pointer to it.
func addWorker(s *State, branch, worktree string) *Worker {
w := Worker{
ID: s.NextID,
Branch: branch,
Worktree: worktree,
Session: makeSessionName(branch),
Status: "waiting",
CreatedAt: time.Now().Format("Jan 02 15:04"),
SessionStarted: false,
}
s.NextID++
s.Workers = append(s.Workers, w)
return &s.Workers[len(s.Workers)-1]
}
// findWorker finds a worker by branch name or numeric ID string, returning nil if not found.
func findWorker(s *State, nameOrID string) *Worker {
var id int
if n, _ := fmt.Sscanf(nameOrID, "%d", &id); n == 1 {
for i := range s.Workers {
if s.Workers[i].ID == id {
return &s.Workers[i]
}
}
}
for i := range s.Workers {
if s.Workers[i].Branch == nameOrID {
return &s.Workers[i]
}
}
return nil
}
// removeWorker removes a worker from the state by ID.
func removeWorker(s *State, id int) {
filtered := s.Workers[:0]
for _, w := range s.Workers {
if w.ID != id {
filtered = append(filtered, w)
}
}
s.Workers = filtered
}
// archiveWorker moves a worker to DeletedWorkers, recording the deletion time.
func archiveWorker(s *State, id int) {
var kept []Worker
for _, w := range s.Workers {
if w.ID == id {
w.DeletedAt = time.Now().Format("Jan 02 15:04")
w.Worktree = ""
w.Session = ""
w.Status = ""
w.GraftStatus = ""
w.SessionStarted = false
s.DeletedWorkers = append([]Worker{w}, s.DeletedWorkers...)
} else {
kept = append(kept, w)
}
}
s.Workers = kept
}