1package repo
2
3import (
4 "bytes"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "os"
9 "path/filepath"
10 "strings"
11 "time"
12
13 "arche/internal/object"
14 "arche/internal/store"
15)
16
17const (
18 archeDirName = ".arche"
19 storeFile = "store.db"
20 packsDir = "packs"
21 headFile = "HEAD"
22 configFile = "config.toml"
23 worktreeFile = ".arche-wt"
24)
25
26var ErrNotARepo = errors.New("not an Arche repository (no .arche directory found)")
27
28type Repo struct {
29 Root string
30 Store store.Store
31 Cfg *Config
32
33 archeDir string
34 worktreeName string
35}
36
37func (r *Repo) ArcheDir() string { return r.archeDir }
38
39func (r *Repo) headPath() string {
40 if r.worktreeName != "" {
41 return filepath.Join(r.archeDir, "worktrees", r.worktreeName, headFile)
42 }
43 return filepath.Join(r.archeDir, headFile)
44}
45
46type repoLocation struct {
47 mainRoot string
48 root string
49 worktreeName string
50}
51
52func findLocation(start string) (*repoLocation, error) {
53 dir, err := filepath.Abs(start)
54 if err != nil {
55 return nil, err
56 }
57 for {
58 if fi, err := os.Stat(filepath.Join(dir, archeDirName)); err == nil && fi.IsDir() {
59 return &repoLocation{mainRoot: dir, root: dir}, nil
60 }
61 if data, err := os.ReadFile(filepath.Join(dir, worktreeFile)); err == nil {
62 parts := strings.SplitN(strings.TrimSpace(string(data)), "\n", 2)
63 if len(parts) == 2 && parts[0] != "" && parts[1] != "" {
64 return &repoLocation{
65 mainRoot: parts[0],
66 root: dir,
67 worktreeName: parts[1],
68 }, nil
69 }
70 }
71 parent := filepath.Dir(dir)
72 if parent == dir {
73 return nil, ErrNotARepo
74 }
75 dir = parent
76 }
77}
78
79func FindRoot(start string) (string, error) {
80 loc, err := findLocation(start)
81 if err != nil {
82 return "", err
83 }
84 return loc.mainRoot, nil
85}
86
87func Open(dir string) (*Repo, error) {
88 loc, err := findLocation(dir)
89 if err != nil {
90 return nil, err
91 }
92 r, err := openAt(loc.mainRoot)
93 if err != nil {
94 return nil, err
95 }
96 if loc.worktreeName != "" {
97 r.Root = loc.root
98 r.worktreeName = loc.worktreeName
99 }
100 return r, nil
101}
102
103func openAt(root string) (*Repo, error) {
104 archeDir := filepath.Join(root, archeDirName)
105 dbPath := filepath.Join(archeDir, storeFile)
106 packDir := filepath.Join(archeDir, packsDir)
107
108 cfg, err := loadConfig(filepath.Join(archeDir, configFile))
109 if err != nil {
110 return nil, fmt.Errorf("load config: %w", err)
111 }
112
113 s, err := store.OpenSQLiteStore(dbPath, packDir, cfg.Storage.PackThreshold, cfg.Storage.PackSealSize, cfg.Storage.Compression)
114 if err != nil {
115 return nil, fmt.Errorf("open store: %w", err)
116 }
117
118 return &Repo{Root: root, archeDir: archeDir, Store: s, Cfg: cfg}, nil
119}
120
121func Init(path string) (*Repo, error) {
122 absPath, err := filepath.Abs(path)
123 if err != nil {
124 return nil, err
125 }
126
127 archeDir := filepath.Join(absPath, archeDirName)
128 if _, err := os.Stat(archeDir); err == nil {
129 return nil, fmt.Errorf("already an Arche repository at %s", absPath)
130 }
131
132 if err := os.MkdirAll(absPath, 0o755); err != nil {
133 return nil, fmt.Errorf("create repo dir: %w", err)
134 }
135 if err := os.MkdirAll(archeDir, 0o755); err != nil {
136 return nil, fmt.Errorf("create .arche dir: %w", err)
137 }
138
139 cfg := DefaultConfig()
140 if err := writeConfig(filepath.Join(archeDir, configFile), cfg); err != nil {
141 return nil, fmt.Errorf("write config: %w", err)
142 }
143
144 r, err := openAt(absPath)
145 if err != nil {
146 return nil, err
147 }
148
149 if err := r.bootstrap(); err != nil {
150 r.Close()
151 return nil, fmt.Errorf("bootstrap: %w", err)
152 }
153
154 return r, nil
155}
156
157func (r *Repo) bootstrap() error {
158 now := time.Now()
159 sig := r.authorSig(now)
160
161 emptyTree := &object.Tree{Entries: nil}
162 treeID := object.HashTree(emptyTree)
163
164 tx, err := r.Store.Begin()
165 if err != nil {
166 return err
167 }
168
169 changeID, err := r.Store.AllocChangeID(tx)
170 if err != nil {
171 r.Store.Rollback(tx)
172 return err
173 }
174
175 c := &object.Commit{
176 TreeID: treeID,
177 Parents: nil,
178 ChangeID: changeID,
179 Author: sig,
180 Committer: sig,
181 Message: "",
182 Phase: object.PhaseDraft,
183 }
184 commitID := object.HashCommit(c)
185
186 if _, err := WriteTreeTx(r.Store, tx, emptyTree); err != nil {
187 r.Store.Rollback(tx)
188 return err
189 }
190 if _, err := WriteCommitTx(r.Store, tx, c); err != nil {
191 r.Store.Rollback(tx)
192 return err
193 }
194 if err := r.Store.SetChangeCommit(tx, changeID, commitID); err != nil {
195 r.Store.Rollback(tx)
196 return err
197 }
198
199 op := store.Operation{
200 Kind: "init",
201 Timestamp: now.Unix(),
202 Before: "{}",
203 After: refSnapshot(changeID, commitID),
204 }
205 if _, err := r.Store.InsertOperation(tx, op); err != nil {
206 r.Store.Rollback(tx)
207 return err
208 }
209
210 if err := r.Store.Commit(tx); err != nil {
211 return err
212 }
213
214 return r.WriteHead(object.FormatChangeID(changeID))
215}
216
217func (r *Repo) Head() (string, error) {
218 data, err := os.ReadFile(r.headPath())
219 if err != nil {
220 return "", fmt.Errorf("read HEAD: %w", err)
221 }
222 return strings.TrimSpace(string(data)), nil
223}
224
225func (r *Repo) WriteHead(changeID string) error {
226 p := r.headPath()
227 tmp := p + ".tmp"
228 if err := os.WriteFile(tmp, []byte(changeID+"\n"), 0o644); err != nil {
229 return err
230 }
231 return os.Rename(tmp, p)
232}
233
234func (r *Repo) HeadChangeID() (string, error) {
235 raw, err := r.Head()
236 if err != nil {
237 return "", err
238 }
239 return object.StripChangeIDPrefix(raw), nil
240}
241
242func (r *Repo) HeadCommit() (*object.Commit, [32]byte, error) {
243 cid, err := r.HeadChangeID()
244 if err != nil {
245 return nil, object.ZeroID, err
246 }
247 commitID, err := r.Store.GetChangeCommit(cid)
248 if err != nil {
249 return nil, object.ZeroID, fmt.Errorf("resolve HEAD change %q: %w", cid, err)
250 }
251 c, err := r.ReadCommit(commitID)
252 if err != nil {
253 return nil, commitID, err
254 }
255 return c, commitID, nil
256}
257
258func (r *Repo) ReadCommit(id [32]byte) (*object.Commit, error) {
259 _, raw, err := r.Store.ReadObject(id)
260 if err != nil {
261 return nil, err
262 }
263 return object.DecodeCommit(raw)
264}
265
266func (r *Repo) ReadTree(id [32]byte) (*object.Tree, error) {
267 _, raw, err := r.Store.ReadObject(id)
268 if err != nil {
269 return nil, err
270 }
271 return object.DecodeTree(raw)
272}
273
274func (r *Repo) ReadBlob(id [32]byte) ([]byte, error) {
275 _, raw, err := r.Store.ReadObject(id)
276 if err != nil {
277 return nil, err
278 }
279 b, err := object.DecodeBlob(raw)
280 if err != nil {
281 return nil, err
282 }
283 return b.Content, nil
284}
285
286func (r *Repo) Close() error {
287 return r.Store.Close()
288}
289
290func (r *Repo) SaveConfig() error {
291 return writeConfig(filepath.Join(r.ArcheDir(), configFile), r.Cfg)
292}
293
294func (r *Repo) authorSig(t time.Time) object.Signature {
295 return object.Signature{
296 Name: r.Cfg.User.Name,
297 Email: r.Cfg.User.Email,
298 Timestamp: t,
299 }
300}
301
302func WriteBlobTx(s store.Store, tx *store.Tx, b *object.Blob) ([32]byte, error) {
303 id := object.HashBlob(b)
304 var buf bytes.Buffer
305 object.EncodeBlob(&buf, b)
306 return id, s.WriteObject(tx, id, string(object.KindBlob), buf.Bytes())
307}
308
309func WriteConflictTx(s store.Store, tx *store.Tx, c *object.Conflict) ([32]byte, error) {
310 id := object.HashConflict(c)
311 var buf bytes.Buffer
312 object.EncodeConflict(&buf, c)
313 return id, s.WriteObject(tx, id, string(object.KindConflict), buf.Bytes())
314}
315
316func (r *Repo) ReadConflict(id [32]byte) (*object.Conflict, error) {
317 _, raw, err := r.Store.ReadObject(id)
318 if err != nil {
319 return nil, err
320 }
321 return object.DecodeConflict(raw)
322}
323
324func WriteTreeTx(s store.Store, tx *store.Tx, t *object.Tree) ([32]byte, error) {
325 id := object.HashTree(t)
326 var buf bytes.Buffer
327 object.EncodeTree(&buf, t)
328 return id, s.WriteObject(tx, id, string(object.KindTree), buf.Bytes())
329}
330
331func WriteCommitTx(s store.Store, tx *store.Tx, c *object.Commit) ([32]byte, error) {
332 id := object.HashCommit(c)
333 var buf bytes.Buffer
334 object.EncodeCommit(&buf, c)
335 return id, s.WriteObject(tx, id, string(object.KindCommit), buf.Bytes())
336}
337
338func WriteObsoleteTx(s store.Store, tx *store.Tx, o *object.ObsoleteMarker) ([32]byte, error) {
339 id := object.HashObsolete(o)
340 var buf bytes.Buffer
341 object.EncodeObsolete(&buf, o)
342 return id, s.WriteObject(tx, id, string(object.KindObsolete), buf.Bytes())
343}
344
345func refSnapshot(changeID string, commitID [32]byte) string {
346 m := map[string]string{
347 "head": object.FormatChangeID(changeID),
348 "tip": fmt.Sprintf("%x", commitID),
349 }
350 b, _ := json.Marshal(m)
351 return string(b)
352}
353
354type RefState struct {
355 Head string `json:"head"`
356 Tip string `json:"tip"`
357 Bookmarks map[string]string `json:"bookmarks,omitempty"`
358}
359
360func (r *Repo) CaptureRefState() (string, error) {
361 changeID, err := r.Head()
362 if err != nil {
363 return "{}", nil
364 }
365 bare := object.StripChangeIDPrefix(changeID)
366 commitID, err := r.Store.GetChangeCommit(bare)
367 if err != nil {
368 return "{}", nil
369 }
370 bms, _ := r.Store.ListBookmarks()
371 bmMap := make(map[string]string, len(bms))
372 for _, bm := range bms {
373 bmMap[bm.Name] = fmt.Sprintf("%x", bm.CommitID)
374 }
375 s := RefState{
376 Head: changeID,
377 Tip: fmt.Sprintf("%x", commitID),
378 Bookmarks: bmMap,
379 }
380 b, _ := json.Marshal(s)
381 return string(b), nil
382}