1package wc
2
3import (
4 "encoding/json"
5 "fmt"
6 "io/fs"
7 "os"
8 "path/filepath"
9 "sort"
10 "strings"
11 "syscall"
12 "time"
13
14 "arche/internal/merge"
15 "arche/internal/object"
16 "arche/internal/repo"
17 "arche/internal/store"
18 "arche/internal/watcher"
19)
20
21func dirtySet(r *repo.Repo) (map[string]bool, error) {
22 if !watcher.IsActive(r.ArcheDir(), r.Store) {
23 return nil, nil
24 }
25 entries, err := r.Store.ListDirtyWCacheEntries()
26 if err != nil {
27 return nil, err
28 }
29 m := make(map[string]bool, len(entries))
30 for _, e := range entries {
31 m[e.Path] = true
32 }
33 return m, nil
34}
35
36type FileStatus struct {
37 Path string
38 Status rune
39}
40
41type WC struct {
42 Repo *repo.Repo
43 SignKey string
44 NoAutoAdvance bool
45 AuthorOverride *object.Signature
46}
47
48func New(r *repo.Repo) *WC { return &WC{Repo: r} }
49
50func (wc *WC) maybeSign(c *object.Commit) error {
51 if wc.SignKey == "" {
52 return nil
53 }
54 body := object.CommitBodyForSigning(c)
55 sig, _, err := object.SignCommitBody(body, wc.SignKey)
56 if err != nil {
57 return fmt.Errorf("commit signing: %w", err)
58 }
59 c.CommitSig = sig
60 return nil
61}
62
63func (wc *WC) snapshotIntoTx(tx *store.Tx, headCommit *object.Commit, paths []string, cacheMap map[string]store.WCacheEntry, dirty map[string]bool, message string, now time.Time) (*object.Commit, [32]byte, error) {
64 r := wc.Repo
65
66 var entries []fileEntry
67
68 if err := r.Store.ClearWCache(tx); err != nil {
69 return nil, object.ZeroID, fmt.Errorf("clear wcache: %w", err)
70 }
71
72 for _, rel := range paths {
73 if dirty != nil && !dirty[rel] {
74 if cached, ok := cacheMap[rel]; ok {
75 entries = append(entries, fileEntry{
76 path: rel,
77 blobID: cached.BlobID,
78 mode: object.EntryMode(cached.Mode),
79 })
80 if err := r.Store.SetWCacheEntry(tx, cached); err != nil {
81 return nil, object.ZeroID, fmt.Errorf("set wcache: %w", err)
82 }
83 continue
84 }
85 }
86
87 abs := filepath.Join(r.Root, rel)
88 info, err := os.Lstat(abs)
89 if err != nil {
90 continue
91 }
92
93 var blobID [32]byte
94 mode := fileMode(info)
95
96 if cached, ok := cacheMap[rel]; ok {
97 st := info.Sys().(*syscall.Stat_t)
98 inode := st.Ino
99 mtime := info.ModTime().UnixNano()
100 size := info.Size()
101 if cached.Inode == inode && cached.MtimeNs == mtime && cached.Size == size {
102 blobID = cached.BlobID
103 }
104 }
105
106 if blobID == object.ZeroID {
107 content, err := readFileContent(abs, info)
108 if err != nil {
109 return nil, object.ZeroID, err
110 }
111 id, err := repo.WriteBlobTx(r.Store, tx, &object.Blob{Content: content})
112 if err != nil {
113 return nil, object.ZeroID, err
114 }
115 blobID = id
116 }
117
118 st := info.Sys().(*syscall.Stat_t)
119 if err := r.Store.SetWCacheEntry(tx, store.WCacheEntry{
120 Path: rel,
121 Inode: st.Ino,
122 MtimeNs: info.ModTime().UnixNano(),
123 Size: info.Size(),
124 BlobID: blobID,
125 Mode: uint8(mode),
126 }); err != nil {
127 return nil, object.ZeroID, fmt.Errorf("set wcache: %w", err)
128 }
129
130 entries = append(entries, fileEntry{path: rel, blobID: blobID, mode: mode})
131 }
132
133 tree, err := buildTree(r, tx, entries)
134 if err != nil {
135 return nil, object.ZeroID, err
136 }
137
138 sig := object.Signature{
139 Name: r.Cfg.User.Name,
140 Email: r.Cfg.User.Email,
141 Timestamp: now,
142 }
143
144 c := &object.Commit{
145 TreeID: tree,
146 Parents: headCommit.Parents,
147 ChangeID: headCommit.ChangeID,
148 Author: headCommit.Author,
149 Committer: sig,
150 Message: message,
151 Phase: headCommit.Phase,
152 }
153 if headCommit.Author.Timestamp.IsZero() {
154 c.Author = sig
155 }
156
157 if err := wc.maybeSign(c); err != nil {
158 return nil, object.ZeroID, err
159 }
160
161 commitID, err := repo.WriteCommitTx(r.Store, tx, c)
162 if err != nil {
163 return nil, object.ZeroID, err
164 }
165 if err := r.Store.SetChangeCommit(tx, c.ChangeID, commitID); err != nil {
166 return nil, object.ZeroID, err
167 }
168
169 return c, commitID, nil
170}
171
172func (wc *WC) snapshotInput() (paths []string, cacheMap map[string]store.WCacheEntry, dirty map[string]bool, err error) {
173 r := wc.Repo
174
175 cacheEntries, err := r.Store.ListWCacheEntries()
176 if err != nil {
177 return nil, nil, nil, err
178 }
179 cacheMap = make(map[string]store.WCacheEntry, len(cacheEntries))
180 for _, e := range cacheEntries {
181 cacheMap[e.Path] = e
182 }
183
184 dirty, _ = dirtySet(r)
185
186 if dirty != nil {
187 seen := make(map[string]bool, len(cacheMap)+len(dirty))
188 for p := range cacheMap {
189 seen[p] = true
190 paths = append(paths, p)
191 }
192 for p := range dirty {
193 if !seen[p] {
194 paths = append(paths, p)
195 }
196 }
197 } else {
198 paths, err = wc.trackedPaths()
199 if err != nil {
200 return nil, nil, nil, err
201 }
202 }
203
204 return paths, cacheMap, dirty, nil
205}
206
207func (wc *WC) Snapshot(message string) (*object.Commit, [32]byte, error) {
208 r := wc.Repo
209 now := time.Now()
210
211 head, _, err := r.HeadCommit()
212 if err != nil {
213 return nil, object.ZeroID, err
214 }
215
216 paths, cacheMap, dirty, err := wc.snapshotInput()
217 if err != nil {
218 return nil, object.ZeroID, err
219 }
220
221 tx, err := r.Store.Begin()
222 if err != nil {
223 return nil, object.ZeroID, err
224 }
225
226 c, commitID, err := wc.snapshotIntoTx(tx, head, paths, cacheMap, dirty, message, now)
227 if err != nil {
228 r.Store.Rollback(tx)
229 return nil, object.ZeroID, err
230 }
231 if err := r.Store.Commit(tx); err != nil {
232 return nil, object.ZeroID, err
233 }
234 return c, commitID, nil
235}
236
237func (wc *WC) Snap(message string) (*object.Commit, [32]byte, error) {
238 r := wc.Repo
239 now := time.Now()
240
241 before, err := r.CaptureRefState()
242 if err != nil {
243 return nil, object.ZeroID, err
244 }
245
246 statusBefore, err := wc.Status()
247 if err != nil {
248 return nil, object.ZeroID, err
249 }
250 diffPaths := make(map[string]bool, len(statusBefore))
251 for _, fsEntry := range statusBefore {
252 diffPaths[fsEntry.Path] = true
253 }
254
255 useRestrictedPaths := len(r.Cfg.Hooks.PreSnap) > 0
256 if useRestrictedPaths {
257 if err := RunHooksSequential(r.Root, "pre-snap", r.Cfg.Hooks.PreSnap); err != nil {
258 return nil, object.ZeroID, fmt.Errorf("pre-snap hook failed: %w", err)
259 }
260 }
261
262 head, oldHeadID, err := r.HeadCommit()
263 if err != nil {
264 return nil, object.ZeroID, err
265 }
266
267 type snapshotFn func(tx *store.Tx) (*object.Commit, [32]byte, error)
268 var doSnapshot snapshotFn
269
270 if useRestrictedPaths {
271 headBlobs := make(map[string][32]byte)
272 headModes := make(map[string]object.EntryMode)
273 if err := flattenTree(r, head.TreeID, "", headBlobs); err != nil {
274 return nil, object.ZeroID, err
275 }
276 if err := flattenTreeModes(r, head.TreeID, "", headModes); err != nil {
277 return nil, object.ZeroID, err
278 }
279 doSnapshot = func(tx *store.Tx) (*object.Commit, [32]byte, error) {
280 return wc.snapshotRestrictedPathsIntoTx(tx, head, headBlobs, headModes, diffPaths, message, now)
281 }
282 } else {
283 paths, cacheMap, dirty, err := wc.snapshotInput()
284 if err != nil {
285 return nil, object.ZeroID, err
286 }
287 doSnapshot = func(tx *store.Tx) (*object.Commit, [32]byte, error) {
288 return wc.snapshotIntoTx(tx, head, paths, cacheMap, dirty, message, now)
289 }
290 }
291
292 existingBookmarks, _ := r.Store.ListBookmarks()
293
294 tx, err := r.Store.Begin()
295 if err != nil {
296 return nil, object.ZeroID, err
297 }
298
299 snapped, snappedID, err := doSnapshot(tx)
300 if err != nil {
301 r.Store.Rollback(tx)
302 return nil, object.ZeroID, err
303 }
304
305 if snappedID != oldHeadID {
306 for _, bm := range existingBookmarks {
307 if bm.CommitID == oldHeadID {
308 _ = r.Store.SetBookmark(tx, store.Bookmark{
309 Name: bm.Name,
310 CommitID: snappedID,
311 Remote: bm.Remote,
312 })
313 }
314 }
315 }
316
317 newChangeID, err := r.Store.AllocChangeID(tx)
318 if err != nil {
319 r.Store.Rollback(tx)
320 return nil, object.ZeroID, err
321 }
322
323 sig := object.Signature{Name: r.Cfg.User.Name, Email: r.Cfg.User.Email, Timestamp: now}
324 newDraft := &object.Commit{
325 TreeID: snapped.TreeID,
326 Parents: [][32]byte{snappedID},
327 ChangeID: newChangeID,
328 Author: sig,
329 Committer: sig,
330 Message: "",
331 Phase: object.PhaseDraft,
332 }
333
334 newDraftID, err := repo.WriteCommitTx(r.Store, tx, newDraft)
335 if err != nil {
336 r.Store.Rollback(tx)
337 return nil, object.ZeroID, err
338 }
339
340 if err := r.Store.SetChangeCommit(tx, newChangeID, newDraftID); err != nil {
341 r.Store.Rollback(tx)
342 return nil, object.ZeroID, err
343 }
344
345 after := buildRefState(snappedID, object.FormatChangeID(newChangeID))
346 op := store.Operation{
347 Kind: "snap",
348 Timestamp: now.Unix(),
349 Before: before,
350 After: after,
351 Metadata: "'" + firstLine(snapped.Message) + "'",
352 }
353 if _, err := r.Store.InsertOperation(tx, op); err != nil {
354 r.Store.Rollback(tx)
355 return nil, object.ZeroID, err
356 }
357
358 if err := r.Store.Commit(tx); err != nil {
359 return nil, object.ZeroID, err
360 }
361
362 if err := r.WriteHead(object.FormatChangeID(newChangeID)); err != nil {
363 return nil, object.ZeroID, err
364 }
365
366 if len(r.Cfg.Hooks.PostSnap) > 0 {
367 if err := RunHooksSequential(r.Root, "post-snap", r.Cfg.Hooks.PostSnap); err != nil {
368 fmt.Fprintf(os.Stderr, "arche snap: post-snap hook: %v\n", err)
369 }
370 }
371
372 return snapped, snappedID, nil
373}
374
375func (wc *WC) Status() ([]FileStatus, error) {
376 r := wc.Repo
377 head, _, err := r.HeadCommit()
378 if err != nil {
379 return nil, err
380 }
381
382 headFiles := make(map[string][32]byte)
383 if err := flattenTree(r, head.TreeID, "", headFiles); err != nil {
384 return nil, err
385 }
386
387 wcPaths, err := wc.trackedPaths()
388 if err != nil {
389 return nil, err
390 }
391 wcSet := make(map[string]bool, len(wcPaths))
392 for _, p := range wcPaths {
393 wcSet[p] = true
394 }
395
396 cacheEntries, _ := r.Store.ListWCacheEntries()
397 cacheMap := make(map[string]store.WCacheEntry, len(cacheEntries))
398 for _, e := range cacheEntries {
399 cacheMap[e.Path] = e
400 }
401 dirty, _ := dirtySet(r)
402
403 var out []FileStatus
404
405 for path, headBlobID := range headFiles {
406 if !wcSet[path] {
407 out = append(out, FileStatus{Path: path, Status: 'D'})
408 continue
409 }
410
411 if dirty != nil && !dirty[path] {
412 if cached, ok := cacheMap[path]; ok {
413 if cached.BlobID != headBlobID {
414 out = append(out, FileStatus{Path: path, Status: 'M'})
415 }
416 continue
417 }
418 }
419
420 curBlobID, err := wc.blobIDForPath(path)
421 if err != nil {
422 continue
423 }
424 if curBlobID != headBlobID {
425 out = append(out, FileStatus{Path: path, Status: 'M'})
426 }
427 }
428
429 ignore, _ := loadIgnore(r.Root)
430 for _, path := range wcPaths {
431 if _, inHead := headFiles[path]; !inHead {
432 if ignore.Match(path) {
433 continue
434 }
435 out = append(out, FileStatus{Path: path, Status: 'A'})
436 }
437 }
438
439 sort.Slice(out, func(i, j int) bool { return out[i].Path < out[j].Path })
440 return out, nil
441}
442
443func (wc *WC) materializeDisk(treeID [32]byte) (map[string][32]byte, map[string]object.EntryMode, error) {
444 r := wc.Repo
445
446 wantFiles := make(map[string][32]byte)
447 wantMode := make(map[string]object.EntryMode)
448 if err := flattenTree(r, treeID, "", wantFiles); err != nil {
449 return nil, nil, err
450 }
451
452 if err := flattenTreeModes(r, treeID, "", wantMode); err != nil {
453 return nil, nil, err
454 }
455
456 ignore, _ := loadIgnore(r.Root)
457 err := filepath.WalkDir(r.Root, func(path string, d fs.DirEntry, err error) error {
458 if err != nil {
459 return nil
460 }
461 rel, _ := filepath.Rel(r.Root, path)
462 if rel == "." {
463 return nil
464 }
465 if d.IsDir() {
466 if rel == archeDirName || strings.HasPrefix(rel, archeDirName+string(os.PathSeparator)) {
467 return filepath.SkipDir
468 }
469 if ignore.MatchDir(rel) {
470 return filepath.SkipDir
471 }
472 return nil
473 }
474 if ignore.Match(rel) {
475 return nil
476 }
477 if _, ok := wantFiles[rel]; !ok {
478 return os.Remove(path)
479 }
480 return nil
481 })
482 if err != nil {
483 return nil, nil, err
484 }
485
486 var conflictPaths []string
487 for relPath, blobID := range wantFiles {
488 abs := filepath.Join(r.Root, relPath)
489 if err := os.MkdirAll(filepath.Dir(abs), 0o755); err != nil {
490 return nil, nil, err
491 }
492 content, err := r.ReadBlob(blobID)
493 if err != nil {
494 if conf, cErr := r.ReadConflict(blobID); cErr == nil {
495 content = renderConflictMarkers(r, conf)
496 conflictPaths = append(conflictPaths, relPath)
497 err = nil
498 }
499 }
500 if err != nil {
501 return nil, nil, err
502 }
503 perm := fs.FileMode(0o644)
504 if wantMode[relPath] == object.ModeExec {
505 perm = 0o755
506 }
507 if err := os.WriteFile(abs, content, perm); err != nil {
508 return nil, nil, err
509 }
510 }
511
512 for _, p := range conflictPaths {
513 delete(wantFiles, p)
514 }
515
516 return wantFiles, wantMode, nil
517}
518
519func renderConflictMarkers(r *repo.Repo, conf *object.Conflict) []byte {
520 readStr := func(id [32]byte) string {
521 if id == object.ZeroID {
522 return ""
523 }
524 b, _ := r.ReadBlob(id)
525 return string(b)
526 }
527 nl := func(s string) string {
528 if len(s) > 0 && s[len(s)-1] != '\n' {
529 return s + "\n"
530 }
531 return s
532 }
533 if conf.Ours.BlobID == object.ZeroID {
534 return []byte(fmt.Sprintf("<<<<<<< ours\n(deleted)\n=======\n%s>>>>>>> theirs\n", nl(readStr(conf.Theirs.BlobID))))
535 }
536 if conf.Theirs.BlobID == object.ZeroID {
537 return []byte(fmt.Sprintf("<<<<<<< ours\n%s=======\n(deleted)\n>>>>>>> theirs\n", nl(readStr(conf.Ours.BlobID))))
538 }
539 return []byte(fmt.Sprintf("<<<<<<< ours\n%s=======\n%s>>>>>>> theirs\n",
540 nl(readStr(conf.Ours.BlobID)),
541 nl(readStr(conf.Theirs.BlobID))))
542}
543
544func (wc *WC) populateWCacheInTx(tx *store.Tx, wantFiles map[string][32]byte) error {
545 r := wc.Repo
546 if err := r.Store.ClearWCache(tx); err != nil {
547 return err
548 }
549 for relPath, blobID := range wantFiles {
550 abs := filepath.Join(r.Root, relPath)
551 info, err := os.Lstat(abs)
552 if err != nil {
553 continue
554 }
555 st, ok := info.Sys().(*syscall.Stat_t)
556 if !ok {
557 continue
558 }
559 _ = r.Store.SetWCacheEntry(tx, store.WCacheEntry{
560 Path: relPath,
561 Inode: st.Ino,
562 MtimeNs: info.ModTime().UnixNano(),
563 Size: info.Size(),
564 BlobID: blobID,
565 Mode: uint8(fileMode(info)),
566 })
567 }
568 return nil
569}
570
571func (wc *WC) MaterializeQuiet(treeID [32]byte) error {
572 r := wc.Repo
573
574 wantFiles, _, err := wc.materializeDisk(treeID)
575 if err != nil {
576 return err
577 }
578
579 tx, err := r.Store.Begin()
580 if err != nil {
581 return err
582 }
583 if err := wc.populateWCacheInTx(tx, wantFiles); err != nil {
584 r.Store.Rollback(tx)
585 return err
586 }
587 return r.Store.Commit(tx)
588}
589
590func (wc *WC) Materialize(treeID [32]byte, newChangeID string) error {
591 r := wc.Repo
592
593 before, _ := r.CaptureRefState()
594 now := time.Now()
595
596 wantFiles, _, err := wc.materializeDisk(treeID)
597 if err != nil {
598 return err
599 }
600
601 bare := object.StripChangeIDPrefix(newChangeID)
602 commitID, _ := r.Store.GetChangeCommit(bare)
603 after := buildRefState(commitID, newChangeID)
604
605 tx, err := r.Store.Begin()
606 if err != nil {
607 return err
608 }
609 if err := wc.populateWCacheInTx(tx, wantFiles); err != nil {
610 r.Store.Rollback(tx)
611 return err
612 }
613
614 op := store.Operation{
615 Kind: "co",
616 Timestamp: now.Unix(),
617 Before: before,
618 After: after,
619 Metadata: "checked out " + newChangeID,
620 }
621 if _, err := r.Store.InsertOperation(tx, op); err != nil {
622 r.Store.Rollback(tx)
623 return err
624 }
625
626 return r.Store.Commit(tx)
627}
628
629const archeDirName = ".arche"
630
631func (wc *WC) trackedPaths() ([]string, error) {
632 r := wc.Repo
633 ignore, _ := loadIgnore(r.Root)
634
635 var paths []string
636 err := filepath.WalkDir(r.Root, func(path string, d fs.DirEntry, err error) error {
637 if err != nil {
638 return nil
639 }
640 rel, _ := filepath.Rel(r.Root, path)
641 if rel == "." {
642 return nil
643 }
644 if d.IsDir() {
645 if rel == archeDirName || strings.HasPrefix(rel, archeDirName+string(os.PathSeparator)) {
646 return filepath.SkipDir
647 }
648 if ignore.MatchDir(rel) {
649 return filepath.SkipDir
650 }
651 return nil
652 }
653 if ignore.Match(rel) {
654 return nil
655 }
656 paths = append(paths, filepath.ToSlash(rel))
657 return nil
658 })
659 return paths, err
660}
661
662func (wc *WC) TrackedPaths() ([]string, error) { return wc.trackedPaths() }
663
664func (wc *WC) blobIDForPath(rel string) ([32]byte, error) {
665 r := wc.Repo
666 abs := filepath.Join(r.Root, rel)
667 info, err := os.Lstat(abs)
668 if err != nil {
669 return object.ZeroID, err
670 }
671 st := info.Sys().(*syscall.Stat_t)
672
673 if cached, _ := r.Store.GetWCacheEntry(rel); cached != nil {
674 if cached.Inode == st.Ino &&
675 cached.MtimeNs == info.ModTime().UnixNano() &&
676 cached.Size == info.Size() {
677 return cached.BlobID, nil
678 }
679 }
680
681 content, err := readFileContent(abs, info)
682 if err != nil {
683 return object.ZeroID, err
684 }
685 b := &object.Blob{Content: content}
686 return object.HashBlob(b), nil
687}
688
689func flattenTree(r *repo.Repo, treeID [32]byte, prefix string, out map[string][32]byte) error {
690 if treeID == object.ZeroID {
691 return nil
692 }
693 t, err := r.ReadTree(treeID)
694 if err != nil {
695 return err
696 }
697 for _, e := range t.Entries {
698 rel := join(prefix, e.Name)
699 switch e.Mode {
700 case object.ModeDir:
701 if err := flattenTree(r, e.ObjectID, rel, out); err != nil {
702 return err
703 }
704 default:
705 out[rel] = e.ObjectID
706 }
707 }
708 return nil
709}
710
711func flattenTreeModes(r *repo.Repo, treeID [32]byte, prefix string, out map[string]object.EntryMode) error {
712 if treeID == object.ZeroID {
713 return nil
714 }
715 t, err := r.ReadTree(treeID)
716 if err != nil {
717 return err
718 }
719 for _, e := range t.Entries {
720 rel := join(prefix, e.Name)
721 switch e.Mode {
722 case object.ModeDir:
723 if err := flattenTreeModes(r, e.ObjectID, rel, out); err != nil {
724 return err
725 }
726 default:
727 out[rel] = e.Mode
728 }
729 }
730 return nil
731}
732
733type fileEntry struct {
734 path string
735 blobID [32]byte
736 mode object.EntryMode
737}
738
739func buildTree(r *repo.Repo, tx *store.Tx, entries []fileEntry) ([32]byte, error) {
740 type node struct {
741 isFile bool
742 blobID [32]byte
743 mode object.EntryMode
744 children map[string]*node
745 }
746 root := &node{children: make(map[string]*node)}
747
748 for _, e := range entries {
749 parts := strings.Split(e.path, "/")
750 cur := root
751 for i, part := range parts {
752 if i == len(parts)-1 {
753 cur.children[part] = &node{isFile: true, blobID: e.blobID, mode: e.mode}
754 } else {
755 if _, ok := cur.children[part]; !ok {
756 cur.children[part] = &node{children: make(map[string]*node)}
757 }
758 cur = cur.children[part]
759 }
760 }
761 }
762
763 var writeNode func(n *node) ([32]byte, error)
764 writeNode = func(n *node) ([32]byte, error) {
765 var treeEntries []object.TreeEntry
766 for name, child := range n.children {
767 if child.isFile {
768 treeEntries = append(treeEntries, object.TreeEntry{
769 Name: name,
770 Mode: child.mode,
771 ObjectID: child.blobID,
772 })
773 } else {
774 subID, err := writeNode(child)
775 if err != nil {
776 return object.ZeroID, err
777 }
778 treeEntries = append(treeEntries, object.TreeEntry{
779 Name: name,
780 Mode: object.ModeDir,
781 ObjectID: subID,
782 })
783 }
784 }
785 sort.Slice(treeEntries, func(i, j int) bool { return treeEntries[i].Name < treeEntries[j].Name })
786 t := &object.Tree{Entries: treeEntries}
787 id, err := repo.WriteTreeTx(r.Store, tx, t)
788 return id, err
789 }
790
791 return writeNode(root)
792}
793
794func fileMode(info os.FileInfo) object.EntryMode {
795 if info.Mode()&0o111 != 0 {
796 return object.ModeExec
797 }
798 if info.Mode()&os.ModeSymlink != 0 {
799 return object.ModeSymlink
800 }
801 return object.ModeFile
802}
803
804func readFileContent(abs string, info os.FileInfo) ([]byte, error) {
805 if info.Mode()&os.ModeSymlink != 0 {
806 target, err := os.Readlink(abs)
807 if err != nil {
808 return nil, err
809 }
810 return []byte(target), nil
811 }
812 return os.ReadFile(abs)
813}
814
815func join(prefix, name string) string {
816 if prefix == "" {
817 return name
818 }
819 return prefix + "/" + name
820}
821
822func buildRefState(commitID [32]byte, changeID string) string {
823 m := map[string]string{
824 "head": changeID,
825 "tip": fmt.Sprintf("%x", commitID),
826 }
827 b, _ := json.Marshal(m)
828 return string(b)
829}
830
831func firstLine(s string) string {
832 if i := strings.IndexByte(s, '\n'); i >= 0 {
833 return s[:i]
834 }
835 return s
836}
837
838func (wc *WC) SnapshotTree() ([32]byte, error) {
839 r := wc.Repo
840
841 paths, cacheMap, dirty, err := wc.snapshotInput()
842 if err != nil {
843 return object.ZeroID, err
844 }
845
846 tx, err := r.Store.Begin()
847 if err != nil {
848 return object.ZeroID, err
849 }
850
851 var entries []fileEntry
852 for _, rel := range paths {
853 if dirty != nil && !dirty[rel] {
854 if cached, ok := cacheMap[rel]; ok {
855 entries = append(entries, fileEntry{
856 path: rel,
857 blobID: cached.BlobID,
858 mode: object.EntryMode(cached.Mode),
859 })
860 continue
861 }
862 }
863
864 abs := filepath.Join(r.Root, rel)
865 info, err := os.Lstat(abs)
866 if err != nil {
867 continue
868 }
869
870 var blobID [32]byte
871 mode := fileMode(info)
872
873 if cached, ok := cacheMap[rel]; ok {
874 st := info.Sys().(*syscall.Stat_t)
875 if cached.Inode == st.Ino && cached.MtimeNs == info.ModTime().UnixNano() && cached.Size == info.Size() {
876 blobID = cached.BlobID
877 }
878 }
879
880 if blobID == object.ZeroID {
881 content, err := readFileContent(abs, info)
882 if err != nil {
883 r.Store.Rollback(tx) //nolint:errcheck
884 return object.ZeroID, err
885 }
886 id, err := repo.WriteBlobTx(r.Store, tx, &object.Blob{Content: content})
887 if err != nil {
888 r.Store.Rollback(tx) //nolint:errcheck
889 return object.ZeroID, err
890 }
891 blobID = id
892 }
893
894 entries = append(entries, fileEntry{path: rel, blobID: blobID, mode: mode})
895 }
896
897 treeID, err := buildTree(r, tx, entries)
898 if err != nil {
899 r.Store.Rollback(tx) //nolint:errcheck
900 return object.ZeroID, err
901 }
902 if err := r.Store.Commit(tx); err != nil {
903 return object.ZeroID, err
904 }
905 return treeID, nil
906}
907
908func (wc *WC) Amend(message string) (*object.Commit, [32]byte, error) {
909 r := wc.Repo
910 now := time.Now()
911
912 head, oldHeadID, err := r.HeadCommit()
913 if err != nil {
914 return nil, object.ZeroID, err
915 }
916 if head.Phase == object.PhasePublic {
917 return nil, object.ZeroID, fmt.Errorf("cannot amend a public commit; use --force-rewrite if you are sure")
918 }
919
920 before, err := r.CaptureRefState()
921 if err != nil {
922 return nil, object.ZeroID, err
923 }
924
925 if message == "" {
926 message = head.Message
927 }
928
929 paths, cacheMap, dirty, err := wc.snapshotInput()
930 if err != nil {
931 return nil, object.ZeroID, err
932 }
933
934 tx, err := r.Store.Begin()
935 if err != nil {
936 return nil, object.ZeroID, err
937 }
938
939 amended, amendedID, err := wc.snapshotIntoTx(tx, head, paths, cacheMap, dirty, message, now)
940 if err != nil {
941 r.Store.Rollback(tx)
942 return nil, object.ZeroID, err
943 }
944
945 if oldHeadID != amendedID {
946 obs := &object.ObsoleteMarker{
947 Predecessor: oldHeadID,
948 Successors: [][32]byte{amendedID},
949 Reason: "amend",
950 Timestamp: now.Unix(),
951 }
952 if _, err := repo.WriteObsoleteTx(r.Store, tx, obs); err != nil {
953 r.Store.Rollback(tx)
954 return nil, object.ZeroID, err
955 }
956 }
957
958 after := buildRefState(amendedID, object.FormatChangeID(amended.ChangeID))
959 op := store.Operation{
960 Kind: "amend",
961 Timestamp: now.Unix(),
962 Before: before,
963 After: after,
964 Metadata: "'" + firstLine(amended.Message) + "'",
965 }
966 if _, err := r.Store.InsertOperation(tx, op); err != nil {
967 r.Store.Rollback(tx)
968 return nil, object.ZeroID, err
969 }
970
971 if err := r.Store.Commit(tx); err != nil {
972 return nil, object.ZeroID, err
973 }
974
975 if oldHeadID != amendedID {
976 if err := wc.autoRebaseDownstream(oldHeadID, amendedID, head.ChangeID, now); err != nil {
977 fmt.Fprintf(os.Stderr, "arche: warning: downstream rebase failed: %v\n", err)
978 }
979 }
980
981 return amended, amendedID, nil
982}
983
984func (wc *WC) autoRebaseDownstream(oldParentID, newParentID [32]byte, headChangeID string, now time.Time) error {
985 r := wc.Repo
986
987 allChanges, err := r.Store.ListChanges()
988 if err != nil {
989 return err
990 }
991
992 type draftEntry struct {
993 id [32]byte
994 changeID string
995 commit *object.Commit
996 }
997
998 children := make(map[[32]byte][]draftEntry)
999 for _, ch := range allChanges {
1000 if ch.CommitID == object.ZeroID {
1001 continue
1002 }
1003 c, err := r.ReadCommit(ch.CommitID)
1004 if err != nil || c == nil {
1005 continue
1006 }
1007 if c.Phase != object.PhaseDraft {
1008 continue
1009 }
1010 if c.ChangeID == headChangeID {
1011 continue
1012 }
1013 if len(c.Parents) == 0 {
1014 continue
1015 }
1016 d := draftEntry{id: ch.CommitID, changeID: ch.Name, commit: c}
1017 children[c.Parents[0]] = append(children[c.Parents[0]], d)
1018 }
1019
1020 type rebaseTask struct {
1021 entry draftEntry
1022 newParent [32]byte
1023 }
1024 var tasks []rebaseTask
1025 queue := []struct {
1026 oldID [32]byte
1027 newID [32]byte
1028 }{{oldParentID, newParentID}}
1029
1030 for len(queue) > 0 {
1031 cur := queue[0]
1032 queue = queue[1:]
1033 for _, child := range children[cur.oldID] {
1034 tasks = append(tasks, rebaseTask{entry: child, newParent: cur.newID})
1035 queue = append(queue, struct{ oldID, newID [32]byte }{child.id, child.id})
1036 }
1037 }
1038
1039 remapped := map[[32]byte][32]byte{oldParentID: newParentID}
1040
1041 for _, task := range tasks {
1042 oldFirst := task.entry.commit.Parents[0]
1043 newParent, ok := remapped[oldFirst]
1044 if !ok {
1045 newParent = oldFirst
1046 }
1047
1048 var baseTreeID [32]byte
1049 if pc, err2 := r.ReadCommit(oldFirst); err2 == nil {
1050 baseTreeID = pc.TreeID
1051 }
1052 newParentCommit, err := r.ReadCommit(newParent)
1053 if err != nil {
1054 return fmt.Errorf("read new parent for %s: %w", object.FormatChangeID(task.entry.changeID), err)
1055 }
1056
1057 result, err := merge.Trees(r, baseTreeID, task.entry.commit.TreeID, newParentCommit.TreeID)
1058 if err != nil {
1059 return fmt.Errorf("merge for %s: %w", object.FormatChangeID(task.entry.changeID), err)
1060 }
1061
1062 newCommit := &object.Commit{
1063 TreeID: result.TreeID,
1064 Parents: [][32]byte{newParent},
1065 ChangeID: task.entry.changeID,
1066 Author: task.entry.commit.Author,
1067 Committer: object.Signature{Name: r.Cfg.User.Name, Email: r.Cfg.User.Email, Timestamp: now},
1068 Message: task.entry.commit.Message,
1069 Phase: task.entry.commit.Phase,
1070 }
1071
1072 tx, err := r.Store.Begin()
1073 if err != nil {
1074 return err
1075 }
1076 newCommitID, err := repo.WriteCommitTx(r.Store, tx, newCommit)
1077 if err != nil {
1078 r.Store.Rollback(tx)
1079 return err
1080 }
1081 if err := r.Store.SetChangeCommit(tx, task.entry.changeID, newCommitID); err != nil {
1082 r.Store.Rollback(tx)
1083 return err
1084 }
1085 obs := &object.ObsoleteMarker{
1086 Predecessor: task.entry.id,
1087 Successors: [][32]byte{newCommitID},
1088 Reason: "amend",
1089 Timestamp: now.Unix(),
1090 }
1091 if _, err := repo.WriteObsoleteTx(r.Store, tx, obs); err != nil {
1092 r.Store.Rollback(tx)
1093 return err
1094 }
1095 if err := r.Store.Commit(tx); err != nil {
1096 return err
1097 }
1098
1099 remapped[task.entry.id] = newCommitID
1100 conflictNote := ""
1101 if len(result.Conflicts) > 0 {
1102 conflictNote = fmt.Sprintf(" (%d conflict(s))", len(result.Conflicts))
1103 }
1104 fmt.Printf(" auto-rebased %s%s\n", object.FormatChangeID(task.entry.changeID), conflictNote)
1105 }
1106 return nil
1107}