arche / internal/wc/hunk_snap.go

commit 154431fd
  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}