1package wc
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "sort"
8 "syscall"
9 "time"
10
11 "arche/internal/diff"
12 "arche/internal/object"
13 "arche/internal/repo"
14 "arche/internal/store"
15)
16
17func (wc *WC) ComputeWorkingDiffs() ([]diff.FileHunkDiff, error) {
18 r := wc.Repo
19
20 head, _, err := r.HeadCommit()
21 if err != nil {
22 return nil, err
23 }
24
25 var parentTreeID [32]byte
26 if len(head.Parents) > 0 {
27 parent, err := r.ReadCommit(head.Parents[0])
28 if err != nil {
29 return nil, fmt.Errorf("read parent commit: %w", err)
30 }
31 parentTreeID = parent.TreeID
32 }
33
34 parentBlobs := make(map[string][32]byte)
35 parentModes := make(map[string]object.EntryMode)
36 if err := flattenTree(r, parentTreeID, "", parentBlobs); err != nil {
37 return nil, err
38 }
39 if err := flattenTreeModes(r, parentTreeID, "", parentModes); err != nil {
40 return nil, err
41 }
42
43 wcPaths, err := wc.trackedPaths()
44 if err != nil {
45 return nil, err
46 }
47 wcBlobMap := make(map[string]bool, len(wcPaths))
48 for _, p := range wcPaths {
49 wcBlobMap[p] = true
50 }
51
52 allPaths := make(map[string]bool)
53 for p := range parentBlobs {
54 allPaths[p] = true
55 }
56 for _, p := range wcPaths {
57 allPaths[p] = true
58 }
59
60 var out []diff.FileHunkDiff
61 for path := range allPaths {
62 inParent := func() bool { _, ok := parentBlobs[path]; return ok }()
63 inWC := wcBlobMap[path]
64
65 oldContent := ""
66 if inParent {
67 data, err2 := r.ReadBlob(parentBlobs[path])
68 if err2 == nil {
69 oldContent = string(data)
70 }
71 }
72
73 newContent := ""
74 if inWC {
75 abs := filepath.Join(r.Root, filepath.FromSlash(path))
76 data, err2 := os.ReadFile(abs)
77 if err2 == nil {
78 newContent = string(data)
79 }
80 }
81
82 if oldContent == newContent {
83 continue
84 }
85
86 var status rune
87 switch {
88 case !inParent && inWC:
89 status = 'A'
90 case inParent && !inWC:
91 status = 'D'
92 default:
93 status = 'M'
94 }
95
96 fhd := diff.ComputeFileHunks(path, oldContent, newContent, status)
97 out = append(out, fhd)
98 }
99
100 sort.Slice(out, func(i, j int) bool { return out[i].Path < out[j].Path })
101 return out, nil
102}
103
104func (wc *WC) SnapSelectedHunks(
105 message string,
106 diffs []diff.FileHunkDiff,
107 perFile map[string][]bool,
108) (*object.Commit, [32]byte, error) {
109 r := wc.Repo
110 now := time.Now()
111
112 before, _ := r.CaptureRefState()
113
114 head, _, err := r.HeadCommit()
115 if err != nil {
116 return nil, object.ZeroID, err
117 }
118
119 var parentTreeID [32]byte
120 if len(head.Parents) > 0 {
121 parent, err := r.ReadCommit(head.Parents[0])
122 if err != nil {
123 return nil, object.ZeroID, err
124 }
125 parentTreeID = parent.TreeID
126 }
127
128 parentBlobs := make(map[string][32]byte)
129 parentModes := make(map[string]object.EntryMode)
130 if err := flattenTree(r, parentTreeID, "", parentBlobs); err != nil {
131 return nil, object.ZeroID, err
132 }
133 if err := flattenTreeModes(r, parentTreeID, "", parentModes); err != nil {
134 return nil, object.ZeroID, err
135 }
136
137 diffMap := make(map[string]diff.FileHunkDiff, len(diffs))
138 for _, fhd := range diffs {
139 diffMap[fhd.Path] = fhd
140 }
141
142 tx, err := r.Store.Begin()
143 if err != nil {
144 return nil, object.ZeroID, err
145 }
146
147 fileSet := make(map[string]fileEntry, len(parentBlobs))
148 for path, blobID := range parentBlobs {
149 fileSet[path] = fileEntry{path: path, blobID: blobID, mode: parentModes[path]}
150 }
151
152 for path, selected := range perFile {
153 fhd, ok := diffMap[path]
154 if !ok {
155 continue
156 }
157
158 anySelected := false
159 for _, s := range selected {
160 if s {
161 anySelected = true
162 break
163 }
164 }
165
166 switch {
167 case !anySelected:
168 if fhd.Status == 'A' {
169 delete(fileSet, path)
170 }
171
172 case fhd.Status == 'D' && anySelected:
173 delete(fileSet, path)
174
175 case fhd.Status == 'A' && anySelected:
176 content := fhd.NewContent
177 blobID, err := repo.WriteBlobTx(r.Store, tx, &object.Blob{Content: []byte(content)})
178 if err != nil {
179 r.Store.Rollback(tx)
180 return nil, object.ZeroID, err
181 }
182 mode := workingCopyMode(r.Root, path)
183 fileSet[path] = fileEntry{path: path, blobID: blobID, mode: mode}
184
185 default:
186 partialContent := diff.ApplySelectedHunks(fhd, selected)
187 blobID, err := repo.WriteBlobTx(r.Store, tx, &object.Blob{Content: []byte(partialContent)})
188 if err != nil {
189 r.Store.Rollback(tx)
190 return nil, object.ZeroID, err
191 }
192 existingMode := parentModes[path]
193 if existingMode == 0 {
194 existingMode = workingCopyMode(r.Root, path)
195 }
196 fileSet[path] = fileEntry{path: path, blobID: blobID, mode: existingMode}
197 }
198 }
199
200 entries := make([]fileEntry, 0, len(fileSet))
201 for _, e := range fileSet {
202 entries = append(entries, e)
203 }
204 sort.Slice(entries, func(i, j int) bool { return entries[i].path < entries[j].path })
205
206 treeID, err := buildTree(r, tx, entries)
207 if err != nil {
208 r.Store.Rollback(tx)
209 return nil, object.ZeroID, err
210 }
211
212 sig := object.Signature{Name: r.Cfg.User.Name, Email: r.Cfg.User.Email, Timestamp: now}
213 c := &object.Commit{
214 TreeID: treeID,
215 Parents: head.Parents,
216 ChangeID: head.ChangeID,
217 Author: head.Author,
218 Committer: sig,
219 Message: message,
220 Phase: head.Phase,
221 }
222 if head.Author.Timestamp.IsZero() {
223 c.Author = sig
224 }
225
226 if err := wc.maybeSign(c); err != nil {
227 r.Store.Rollback(tx)
228 return nil, object.ZeroID, err
229 }
230
231 commitID, err := repo.WriteCommitTx(r.Store, tx, c)
232 if err != nil {
233 r.Store.Rollback(tx)
234 return nil, object.ZeroID, err
235 }
236 if err := r.Store.SetChangeCommit(tx, c.ChangeID, commitID); err != nil {
237 r.Store.Rollback(tx)
238 return nil, object.ZeroID, err
239 }
240 if err := r.Store.Commit(tx); err != nil {
241 return nil, object.ZeroID, err
242 }
243
244 tx2, err := r.Store.Begin()
245 if err != nil {
246 return c, commitID, err
247 }
248
249 newChangeID, err := r.Store.AllocChangeID(tx2)
250 if err != nil {
251 r.Store.Rollback(tx2)
252 return c, commitID, err
253 }
254
255 newDraft := &object.Commit{
256 TreeID: treeID,
257 Parents: [][32]byte{commitID},
258 ChangeID: newChangeID,
259 Author: sig,
260 Committer: sig,
261 Message: "",
262 Phase: object.PhaseDraft,
263 }
264 newDraftID, err := repo.WriteCommitTx(r.Store, tx2, newDraft)
265 if err != nil {
266 r.Store.Rollback(tx2)
267 return c, commitID, err
268 }
269 if err := r.Store.SetChangeCommit(tx2, newChangeID, newDraftID); err != nil {
270 r.Store.Rollback(tx2)
271 return c, commitID, err
272 }
273
274 after := buildRefState(commitID, object.FormatChangeID(newChangeID))
275 op := store.Operation{
276 Kind: "snap",
277 Timestamp: now.Unix(),
278 Before: before,
279 After: after,
280 Metadata: "'" + firstLine(message) + "'",
281 }
282 if _, err := r.Store.InsertOperation(tx2, op); err != nil {
283 r.Store.Rollback(tx2)
284 return c, commitID, err
285 }
286 if err := r.Store.Commit(tx2); err != nil {
287 return c, commitID, err
288 }
289
290 if err := r.WriteHead(object.FormatChangeID(newChangeID)); err != nil {
291 return c, commitID, err
292 }
293
294 return c, commitID, nil
295}
296
297func (wc *WC) SnapFirstOfSplit(
298 message string,
299 diffs []diff.FileHunkDiff,
300 perFile map[string][]bool,
301) (*object.Commit, [32]byte, error) {
302 r := wc.Repo
303 now := time.Now()
304
305 head, _, err := r.HeadCommit()
306 if err != nil {
307 return nil, object.ZeroID, err
308 }
309
310 var parentTreeID [32]byte
311 if len(head.Parents) > 0 {
312 parent, err := r.ReadCommit(head.Parents[0])
313 if err != nil {
314 return nil, object.ZeroID, err
315 }
316 parentTreeID = parent.TreeID
317 }
318
319 parentBlobs := make(map[string][32]byte)
320 parentModes := make(map[string]object.EntryMode)
321 if err := flattenTree(r, parentTreeID, "", parentBlobs); err != nil {
322 return nil, object.ZeroID, err
323 }
324 if err := flattenTreeModes(r, parentTreeID, "", parentModes); err != nil {
325 return nil, object.ZeroID, err
326 }
327
328 diffMap := make(map[string]diff.FileHunkDiff, len(diffs))
329 for _, fhd := range diffs {
330 diffMap[fhd.Path] = fhd
331 }
332
333 tx, err := r.Store.Begin()
334 if err != nil {
335 return nil, object.ZeroID, err
336 }
337
338 fileSet := make(map[string]fileEntry, len(parentBlobs))
339 for path, blobID := range parentBlobs {
340 fileSet[path] = fileEntry{path: path, blobID: blobID, mode: parentModes[path]}
341 }
342
343 for path, selected := range perFile {
344 fhd, ok := diffMap[path]
345 if !ok {
346 continue
347 }
348 anySelected := false
349 for _, s := range selected {
350 if s {
351 anySelected = true
352 break
353 }
354 }
355 switch {
356 case !anySelected:
357 if fhd.Status == 'A' {
358 delete(fileSet, path)
359 }
360 case fhd.Status == 'D' && anySelected:
361 delete(fileSet, path)
362 case fhd.Status == 'A' && anySelected:
363 blobID, err := repo.WriteBlobTx(r.Store, tx, &object.Blob{Content: []byte(fhd.NewContent)})
364 if err != nil {
365 r.Store.Rollback(tx)
366 return nil, object.ZeroID, err
367 }
368 fileSet[path] = fileEntry{path: path, blobID: blobID, mode: workingCopyMode(r.Root, path)}
369 default:
370 partialContent := diff.ApplySelectedHunks(fhd, selected)
371 blobID, err := repo.WriteBlobTx(r.Store, tx, &object.Blob{Content: []byte(partialContent)})
372 if err != nil {
373 r.Store.Rollback(tx)
374 return nil, object.ZeroID, err
375 }
376 mode := parentModes[path]
377 if mode == 0 {
378 mode = workingCopyMode(r.Root, path)
379 }
380 fileSet[path] = fileEntry{path: path, blobID: blobID, mode: mode}
381 }
382 }
383
384 entries := make([]fileEntry, 0, len(fileSet))
385 for _, e := range fileSet {
386 entries = append(entries, e)
387 }
388 sort.Slice(entries, func(i, j int) bool { return entries[i].path < entries[j].path })
389
390 treeID, err := buildTree(r, tx, entries)
391 if err != nil {
392 r.Store.Rollback(tx)
393 return nil, object.ZeroID, err
394 }
395
396 newChangeID, err := r.Store.AllocChangeID(tx)
397 if err != nil {
398 r.Store.Rollback(tx)
399 return nil, object.ZeroID, err
400 }
401
402 sig := object.Signature{Name: r.Cfg.User.Name, Email: r.Cfg.User.Email, Timestamp: now}
403 c := &object.Commit{
404 TreeID: treeID,
405 Parents: head.Parents,
406 ChangeID: newChangeID,
407 Author: sig,
408 Committer: sig,
409 Message: message,
410 Phase: head.Phase,
411 }
412
413 if err := wc.maybeSign(c); err != nil {
414 r.Store.Rollback(tx)
415 return nil, object.ZeroID, err
416 }
417
418 commitID, err := repo.WriteCommitTx(r.Store, tx, c)
419 if err != nil {
420 r.Store.Rollback(tx)
421 return nil, object.ZeroID, err
422 }
423 if err := r.Store.SetChangeCommit(tx, newChangeID, commitID); err != nil {
424 r.Store.Rollback(tx)
425 return nil, object.ZeroID, err
426 }
427 if err := r.Store.Commit(tx); err != nil {
428 return nil, object.ZeroID, err
429 }
430 return c, commitID, nil
431}
432
433func (wc *WC) SnapRemaining(msg string, parentID [32]byte) (*object.Commit, [32]byte, error) {
434 r := wc.Repo
435 now := time.Now()
436
437 wcPaths, err := wc.trackedPaths()
438 if err != nil {
439 return nil, object.ZeroID, err
440 }
441
442 tx, err := r.Store.Begin()
443 if err != nil {
444 return nil, object.ZeroID, err
445 }
446
447 if err := r.Store.ClearWCache(tx); err != nil {
448 r.Store.Rollback(tx)
449 return nil, object.ZeroID, fmt.Errorf("clear wcache: %w", err)
450 }
451
452 var entries []fileEntry
453 for _, rel := range wcPaths {
454 abs := filepath.Join(r.Root, rel)
455 info, err := os.Lstat(abs)
456 if err != nil {
457 continue
458 }
459 data, err := readFileContent(abs, info)
460 if err != nil {
461 r.Store.Rollback(tx)
462 return nil, object.ZeroID, err
463 }
464 blobID, err := repo.WriteBlobTx(r.Store, tx, &object.Blob{Content: data})
465 if err != nil {
466 r.Store.Rollback(tx)
467 return nil, object.ZeroID, err
468 }
469 if st, ok := info.Sys().(*syscall.Stat_t); ok {
470 _ = r.Store.SetWCacheEntry(tx, store.WCacheEntry{
471 Path: rel,
472 Inode: st.Ino,
473 MtimeNs: info.ModTime().UnixNano(),
474 Size: info.Size(),
475 BlobID: blobID,
476 })
477 }
478 entries = append(entries, fileEntry{path: rel, blobID: blobID, mode: fileMode(info)})
479 }
480
481 treeID, err := buildTree(r, tx, entries)
482 if err != nil {
483 r.Store.Rollback(tx)
484 return nil, object.ZeroID, err
485 }
486
487 newChangeID, err := r.Store.AllocChangeID(tx)
488 if err != nil {
489 r.Store.Rollback(tx)
490 return nil, object.ZeroID, err
491 }
492
493 sig := object.Signature{Name: r.Cfg.User.Name, Email: r.Cfg.User.Email, Timestamp: now}
494 c := &object.Commit{
495 TreeID: treeID,
496 Parents: [][32]byte{parentID},
497 ChangeID: newChangeID,
498 Author: sig,
499 Committer: sig,
500 Message: msg,
501 Phase: object.PhaseDraft,
502 }
503
504 commitID, err := repo.WriteCommitTx(r.Store, tx, c)
505 if err != nil {
506 r.Store.Rollback(tx)
507 return nil, object.ZeroID, err
508 }
509 if err := r.Store.SetChangeCommit(tx, newChangeID, commitID); err != nil {
510 r.Store.Rollback(tx)
511 return nil, object.ZeroID, err
512 }
513 if err := r.Store.Commit(tx); err != nil {
514 return nil, object.ZeroID, err
515 }
516
517 if err := r.WriteHead(object.FormatChangeID(newChangeID)); err != nil {
518 return nil, object.ZeroID, err
519 }
520
521 return c, commitID, nil
522}
523
524func workingCopyMode(root, path string) object.EntryMode {
525 abs := filepath.Join(root, filepath.FromSlash(path))
526 info, err := os.Lstat(abs)
527 if err != nil {
528 return object.ModeFile
529 }
530 return fileMode(info)
531}