1package wc
2
3import (
4 "fmt"
5 "os"
6 "os/exec"
7 "path/filepath"
8 "sort"
9 "strings"
10 "syscall"
11 "time"
12
13 "arche/internal/object"
14 "arche/internal/repo"
15 "arche/internal/store"
16)
17
18func RunHooksSequential(repoRoot, label string, hooks []string) error {
19 sh := os.Getenv("SHELL")
20 if sh == "" {
21 sh = "/bin/sh"
22 }
23 for _, cmd := range hooks {
24 fmt.Fprintf(os.Stderr, "Running %s: %s\n", label, cmd)
25 c := exec.Command(sh, "-c", cmd)
26 c.Dir = repoRoot
27 c.Stdout = os.Stdout
28 c.Stderr = os.Stderr
29 if err := c.Run(); err != nil {
30 return fmt.Errorf("hook %q: %w", cmd, err)
31 }
32 }
33 return nil
34}
35
36func (wc *WC) snapshotRestrictedPathsIntoTx(
37 tx *store.Tx,
38 headCommit *object.Commit,
39 headBlobs map[string][32]byte,
40 headModes map[string]object.EntryMode,
41 diffPaths map[string]bool,
42 message string,
43 now time.Time,
44) (*object.Commit, [32]byte, error) {
45 r := wc.Repo
46 var entries []fileEntry
47
48 for path, blobID := range headBlobs {
49 if diffPaths[path] {
50 continue
51 }
52 mode := headModes[path]
53 absPath := filepath.Join(r.Root, filepath.FromSlash(path))
54 if info, statErr := os.Lstat(absPath); statErr == nil {
55 if hookChangedFile(r.Store, path, info, blobID) {
56 fmt.Fprintf(os.Stderr,
57 "arche snap: hook modified %s (outside snap diff) - leaving as working-copy change\n", path)
58 }
59 }
60 entries = append(entries, fileEntry{path: path, blobID: blobID, mode: mode})
61 }
62
63 var reSnapNames []string
64 for path := range diffPaths {
65 if _, err := os.Lstat(filepath.Join(r.Root, filepath.FromSlash(path))); err == nil {
66 reSnapNames = append(reSnapNames, path)
67 }
68 }
69 if len(reSnapNames) > 0 {
70 sort.Strings(reSnapNames)
71 fmt.Fprintf(os.Stderr, "Re-snapping %d modified file(s) (%s)\n",
72 len(reSnapNames), strings.Join(reSnapNames, ", "))
73 }
74
75 for path := range diffPaths {
76 absPath := filepath.Join(r.Root, filepath.FromSlash(path))
77 info, err := os.Lstat(absPath)
78 if os.IsNotExist(err) {
79 continue
80 }
81 if err != nil {
82 return nil, object.ZeroID, fmt.Errorf("stat %s: %w", path, err)
83 }
84 content, err := readFileContent(absPath, info)
85 if err != nil {
86 return nil, object.ZeroID, fmt.Errorf("read %s: %w", path, err)
87 }
88 blobID, err := repo.WriteBlobTx(r.Store, tx, &object.Blob{Content: content})
89 if err != nil {
90 return nil, object.ZeroID, err
91 }
92 mode := fileMode(info)
93 entries = append(entries, fileEntry{path: path, blobID: blobID, mode: mode})
94 if st, ok := info.Sys().(*syscall.Stat_t); ok {
95 _ = r.Store.SetWCacheEntry(tx, store.WCacheEntry{
96 Path: path,
97 Inode: st.Ino,
98 MtimeNs: info.ModTime().UnixNano(),
99 Size: info.Size(),
100 BlobID: blobID,
101 })
102 }
103 }
104
105 treeID, err := buildTree(r, tx, entries)
106 if err != nil {
107 return nil, object.ZeroID, fmt.Errorf("build tree: %w", err)
108 }
109
110 sig := object.Signature{Name: r.Cfg.User.Name, Email: r.Cfg.User.Email, Timestamp: now}
111 c := &object.Commit{
112 TreeID: treeID,
113 Parents: headCommit.Parents,
114 ChangeID: headCommit.ChangeID,
115 Author: headCommit.Author,
116 Committer: sig,
117 Message: message,
118 Phase: headCommit.Phase,
119 }
120 if headCommit.Author.Timestamp.IsZero() {
121 c.Author = sig
122 }
123
124 if err := wc.maybeSign(c); err != nil {
125 return nil, object.ZeroID, err
126 }
127
128 commitID, err := repo.WriteCommitTx(r.Store, tx, c)
129 if err != nil {
130 return nil, object.ZeroID, err
131 }
132 if err := r.Store.SetChangeCommit(tx, c.ChangeID, commitID); err != nil {
133 return nil, object.ZeroID, err
134 }
135 return c, commitID, nil
136}
137
138func (wc *WC) SnapshotRestrictedPaths(message string, diffPaths map[string]bool) (*object.Commit, [32]byte, error) {
139 r := wc.Repo
140 now := time.Now()
141
142 headCommit, _, err := r.HeadCommit()
143 if err != nil {
144 return nil, object.ZeroID, err
145 }
146
147 headBlobs := make(map[string][32]byte)
148 headModes := make(map[string]object.EntryMode)
149 if err := flattenTree(r, headCommit.TreeID, "", headBlobs); err != nil {
150 return nil, object.ZeroID, fmt.Errorf("flatten HEAD tree: %w", err)
151 }
152 if err := flattenTreeModes(r, headCommit.TreeID, "", headModes); err != nil {
153 return nil, object.ZeroID, fmt.Errorf("flatten HEAD modes: %w", err)
154 }
155
156 tx, err := r.Store.Begin()
157 if err != nil {
158 return nil, object.ZeroID, err
159 }
160
161 c, commitID, err := wc.snapshotRestrictedPathsIntoTx(tx, headCommit, headBlobs, headModes, diffPaths, message, now)
162 if err != nil {
163 r.Store.Rollback(tx)
164 return nil, object.ZeroID, err
165 }
166 if err := r.Store.Commit(tx); err != nil {
167 return nil, object.ZeroID, err
168 }
169 return c, commitID, nil
170}
171
172func hookChangedFile(s store.Store, path string, info os.FileInfo, expectedBlobID [32]byte) bool {
173 if cached, err := s.GetWCacheEntry(path); err == nil && cached != nil {
174 if st, ok := info.Sys().(*syscall.Stat_t); ok {
175 if cached.Inode == st.Ino &&
176 cached.MtimeNs == info.ModTime().UnixNano() &&
177 cached.Size == info.Size() {
178 return cached.BlobID != expectedBlobID
179 }
180 }
181 }
182
183 return true
184}