arche / internal/wc/hooks.go

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