1package repo
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strings"
8
9 "arche/internal/object"
10)
11
12const worktreesDir = "worktrees"
13
14type WorktreeInfo struct {
15 Name string
16 Path string
17}
18
19func (r *Repo) AddWorktree(path, bookmark string) error {
20 absPath, err := filepath.Abs(path)
21 if err != nil {
22 return fmt.Errorf("worktree add: resolve path: %w", err)
23 }
24
25 var commitID [32]byte
26 var changeID string
27 if bookmark != "" {
28 bms, err := r.Store.ListBookmarks()
29 if err != nil {
30 return err
31 }
32 found := false
33 for _, bm := range bms {
34 if bm.Name == bookmark {
35 commitID = bm.CommitID
36 found = true
37 break
38 }
39 }
40 if !found {
41 return fmt.Errorf("worktree add: bookmark %q not found", bookmark)
42 }
43 c, err := r.ReadCommit(commitID)
44 if err != nil {
45 return fmt.Errorf("worktree add: read commit: %w", err)
46 }
47 changeID = object.FormatChangeID(c.ChangeID)
48 } else {
49 c, id, err := r.HeadCommit()
50 if err != nil {
51 return fmt.Errorf("worktree add: read HEAD commit: %w", err)
52 }
53 commitID = id
54 changeID = object.FormatChangeID(c.ChangeID)
55 }
56
57 name := filepath.Base(absPath)
58 if name == "" || name == "." {
59 name = "wt"
60 }
61 wtBaseDir := filepath.Join(r.archeDir, worktreesDir)
62 wtDir := filepath.Join(wtBaseDir, name)
63 if _, err := os.Stat(wtDir); err == nil {
64 for i := 2; ; i++ {
65 cand := fmt.Sprintf("%s%d", name, i)
66 if _, err := os.Stat(filepath.Join(wtBaseDir, cand)); os.IsNotExist(err) {
67 name = cand
68 wtDir = filepath.Join(wtBaseDir, cand)
69 break
70 }
71 }
72 }
73
74 if err := os.MkdirAll(wtDir, 0o755); err != nil {
75 return fmt.Errorf("worktree add: create meta dir: %w", err)
76 }
77
78 headFilePath := filepath.Join(wtDir, headFile)
79 if err := os.WriteFile(headFilePath, []byte(changeID+"\n"), 0o644); err != nil {
80 os.RemoveAll(wtDir) //nolint:errcheck
81 return fmt.Errorf("worktree add: write HEAD: %w", err)
82 }
83
84 if err := os.WriteFile(filepath.Join(wtDir, "path"), []byte(absPath+"\n"), 0o644); err != nil {
85 os.RemoveAll(wtDir) //nolint:errcheck
86 return fmt.Errorf("worktree add: write path: %w", err)
87 }
88
89 if err := os.MkdirAll(absPath, 0o755); err != nil {
90 os.RemoveAll(wtDir) //nolint:errcheck
91 return fmt.Errorf("worktree add: create directory: %w", err)
92 }
93
94 mainRoot := filepath.Dir(r.archeDir)
95 sentinel := mainRoot + "\n" + name + "\n"
96 sentinelPath := filepath.Join(absPath, worktreeFile)
97 if err := os.WriteFile(sentinelPath, []byte(sentinel), 0o644); err != nil {
98 os.RemoveAll(wtDir) //nolint:errcheck
99 return fmt.Errorf("worktree add: write sentinel: %w", err)
100 }
101
102 if err := r.materializeInto(absPath, commitID); err != nil {
103 os.RemoveAll(wtDir) //nolint:errcheck
104 os.Remove(sentinelPath) //nolint:errcheck
105 return fmt.Errorf("worktree add: materialize: %w", err)
106 }
107
108 return nil
109}
110
111func (r *Repo) materializeInto(targetDir string, commitID [32]byte) error {
112 c, err := r.ReadCommit(commitID)
113 if err != nil {
114 return err
115 }
116 return r.writeTree(targetDir, c.TreeID)
117}
118
119func (r *Repo) writeTree(dir string, treeID [32]byte) error {
120 if treeID == object.ZeroID {
121 return nil
122 }
123 _, raw, err := r.Store.ReadObject(treeID)
124 if err != nil {
125 return err
126 }
127 tree, err := object.DecodeTree(raw)
128 if err != nil {
129 return err
130 }
131 for _, entry := range tree.Entries {
132 targetPath := filepath.Join(dir, entry.Name)
133 switch entry.Mode {
134 case object.ModeDir:
135 if err := os.MkdirAll(targetPath, 0o755); err != nil {
136 return err
137 }
138 if err := r.writeTree(targetPath, entry.ObjectID); err != nil {
139 return err
140 }
141 case object.ModeExec:
142 if err := r.writeBlob(targetPath, entry.ObjectID, 0o755); err != nil {
143 return err
144 }
145 case object.ModeSymlink:
146 content, err := r.readBlobContent(entry.ObjectID)
147 if err != nil {
148 return err
149 }
150 os.Remove(targetPath) //nolint:errcheck
151 if err := os.Symlink(string(content), targetPath); err != nil {
152 return err
153 }
154 default:
155 if err := r.writeBlob(targetPath, entry.ObjectID, 0o644); err != nil {
156 return err
157 }
158 }
159 }
160 return nil
161}
162
163func (r *Repo) writeBlob(path string, blobID [32]byte, perm os.FileMode) error {
164 content, err := r.readBlobContent(blobID)
165 if err != nil {
166 return err
167 }
168 tmp := path + ".arche-tmp"
169 if err := os.WriteFile(tmp, content, perm); err != nil {
170 return err
171 }
172 return os.Rename(tmp, path)
173}
174
175func (r *Repo) readBlobContent(blobID [32]byte) ([]byte, error) {
176 kind, raw, err := r.Store.ReadObject(blobID)
177 if err != nil {
178 return nil, err
179 }
180 if kind != string(object.KindBlob) {
181 return nil, fmt.Errorf("expected blob, got %s", kind)
182 }
183 b, err := object.DecodeBlob(raw)
184 if err != nil {
185 return nil, err
186 }
187 return b.Content, nil
188}
189
190func (r *Repo) ListWorktrees() ([]WorktreeInfo, error) {
191 wtBaseDir := filepath.Join(r.archeDir, worktreesDir)
192 entries, err := os.ReadDir(wtBaseDir)
193 if os.IsNotExist(err) {
194 return nil, nil
195 }
196 if err != nil {
197 return nil, err
198 }
199 var out []WorktreeInfo
200 for _, e := range entries {
201 if !e.IsDir() {
202 continue
203 }
204 pathBytes, err := os.ReadFile(filepath.Join(wtBaseDir, e.Name(), "path"))
205 if err != nil {
206 continue
207 }
208 out = append(out, WorktreeInfo{
209 Name: e.Name(),
210 Path: strings.TrimSpace(string(pathBytes)),
211 })
212 }
213 return out, nil
214}
215
216func (r *Repo) RemoveWorktree(name string) error {
217 wtDir := filepath.Join(r.archeDir, worktreesDir, name)
218 if _, err := os.Stat(wtDir); os.IsNotExist(err) {
219 return fmt.Errorf("worktree %q not found", name)
220 }
221
222 pathBytes, err := os.ReadFile(filepath.Join(wtDir, "path"))
223 if err == nil {
224 wtPath := strings.TrimSpace(string(pathBytes))
225 os.Remove(filepath.Join(wtPath, worktreeFile)) //nolint:errcheck
226 }
227
228 return os.RemoveAll(wtDir)
229}