arche / internal/repo/repo.go

commit 154431fd
  1package repo
  2
  3import (
  4	"bytes"
  5	"encoding/json"
  6	"errors"
  7	"fmt"
  8	"os"
  9	"path/filepath"
 10	"strings"
 11	"time"
 12
 13	"arche/internal/object"
 14	"arche/internal/store"
 15)
 16
 17const (
 18	archeDirName = ".arche"
 19	storeFile    = "store.db"
 20	packsDir     = "packs"
 21	headFile     = "HEAD"
 22	configFile   = "config.toml"
 23	worktreeFile = ".arche-wt"
 24)
 25
 26var ErrNotARepo = errors.New("not an Arche repository (no .arche directory found)")
 27
 28type Repo struct {
 29	Root  string
 30	Store store.Store
 31	Cfg   *Config
 32
 33	archeDir     string
 34	worktreeName string
 35}
 36
 37func (r *Repo) ArcheDir() string { return r.archeDir }
 38
 39func (r *Repo) headPath() string {
 40	if r.worktreeName != "" {
 41		return filepath.Join(r.archeDir, "worktrees", r.worktreeName, headFile)
 42	}
 43	return filepath.Join(r.archeDir, headFile)
 44}
 45
 46type repoLocation struct {
 47	mainRoot     string
 48	root         string
 49	worktreeName string
 50}
 51
 52func findLocation(start string) (*repoLocation, error) {
 53	dir, err := filepath.Abs(start)
 54	if err != nil {
 55		return nil, err
 56	}
 57	for {
 58		if fi, err := os.Stat(filepath.Join(dir, archeDirName)); err == nil && fi.IsDir() {
 59			return &repoLocation{mainRoot: dir, root: dir}, nil
 60		}
 61		if data, err := os.ReadFile(filepath.Join(dir, worktreeFile)); err == nil {
 62			parts := strings.SplitN(strings.TrimSpace(string(data)), "\n", 2)
 63			if len(parts) == 2 && parts[0] != "" && parts[1] != "" {
 64				return &repoLocation{
 65					mainRoot:     parts[0],
 66					root:         dir,
 67					worktreeName: parts[1],
 68				}, nil
 69			}
 70		}
 71		parent := filepath.Dir(dir)
 72		if parent == dir {
 73			return nil, ErrNotARepo
 74		}
 75		dir = parent
 76	}
 77}
 78
 79func FindRoot(start string) (string, error) {
 80	loc, err := findLocation(start)
 81	if err != nil {
 82		return "", err
 83	}
 84	return loc.mainRoot, nil
 85}
 86
 87func Open(dir string) (*Repo, error) {
 88	loc, err := findLocation(dir)
 89	if err != nil {
 90		return nil, err
 91	}
 92	r, err := openAt(loc.mainRoot)
 93	if err != nil {
 94		return nil, err
 95	}
 96	if loc.worktreeName != "" {
 97		r.Root = loc.root
 98		r.worktreeName = loc.worktreeName
 99	}
100	return r, nil
101}
102
103func openAt(root string) (*Repo, error) {
104	archeDir := filepath.Join(root, archeDirName)
105	dbPath := filepath.Join(archeDir, storeFile)
106	packDir := filepath.Join(archeDir, packsDir)
107
108	cfg, err := loadConfig(filepath.Join(archeDir, configFile))
109	if err != nil {
110		return nil, fmt.Errorf("load config: %w", err)
111	}
112
113	s, err := store.OpenSQLiteStore(dbPath, packDir, cfg.Storage.PackThreshold, cfg.Storage.PackSealSize, cfg.Storage.Compression)
114	if err != nil {
115		return nil, fmt.Errorf("open store: %w", err)
116	}
117
118	return &Repo{Root: root, archeDir: archeDir, Store: s, Cfg: cfg}, nil
119}
120
121func Init(path string) (*Repo, error) {
122	absPath, err := filepath.Abs(path)
123	if err != nil {
124		return nil, err
125	}
126
127	archeDir := filepath.Join(absPath, archeDirName)
128	if _, err := os.Stat(archeDir); err == nil {
129		return nil, fmt.Errorf("already an Arche repository at %s", absPath)
130	}
131
132	if err := os.MkdirAll(absPath, 0o755); err != nil {
133		return nil, fmt.Errorf("create repo dir: %w", err)
134	}
135	if err := os.MkdirAll(archeDir, 0o755); err != nil {
136		return nil, fmt.Errorf("create .arche dir: %w", err)
137	}
138
139	cfg := DefaultConfig()
140	if err := writeConfig(filepath.Join(archeDir, configFile), cfg); err != nil {
141		return nil, fmt.Errorf("write config: %w", err)
142	}
143
144	r, err := openAt(absPath)
145	if err != nil {
146		return nil, err
147	}
148
149	if err := r.bootstrap(); err != nil {
150		r.Close()
151		return nil, fmt.Errorf("bootstrap: %w", err)
152	}
153
154	return r, nil
155}
156
157func (r *Repo) bootstrap() error {
158	now := time.Now()
159	sig := r.authorSig(now)
160
161	emptyTree := &object.Tree{Entries: nil}
162	treeID := object.HashTree(emptyTree)
163
164	tx, err := r.Store.Begin()
165	if err != nil {
166		return err
167	}
168
169	changeID, err := r.Store.AllocChangeID(tx)
170	if err != nil {
171		r.Store.Rollback(tx)
172		return err
173	}
174
175	c := &object.Commit{
176		TreeID:    treeID,
177		Parents:   nil,
178		ChangeID:  changeID,
179		Author:    sig,
180		Committer: sig,
181		Message:   "",
182		Phase:     object.PhaseDraft,
183	}
184	commitID := object.HashCommit(c)
185
186	if _, err := WriteTreeTx(r.Store, tx, emptyTree); err != nil {
187		r.Store.Rollback(tx)
188		return err
189	}
190	if _, err := WriteCommitTx(r.Store, tx, c); err != nil {
191		r.Store.Rollback(tx)
192		return err
193	}
194	if err := r.Store.SetChangeCommit(tx, changeID, commitID); err != nil {
195		r.Store.Rollback(tx)
196		return err
197	}
198
199	op := store.Operation{
200		Kind:      "init",
201		Timestamp: now.Unix(),
202		Before:    "{}",
203		After:     refSnapshot(changeID, commitID),
204	}
205	if _, err := r.Store.InsertOperation(tx, op); err != nil {
206		r.Store.Rollback(tx)
207		return err
208	}
209
210	if err := r.Store.Commit(tx); err != nil {
211		return err
212	}
213
214	return r.WriteHead(object.FormatChangeID(changeID))
215}
216
217func (r *Repo) Head() (string, error) {
218	data, err := os.ReadFile(r.headPath())
219	if err != nil {
220		return "", fmt.Errorf("read HEAD: %w", err)
221	}
222	return strings.TrimSpace(string(data)), nil
223}
224
225func (r *Repo) WriteHead(changeID string) error {
226	p := r.headPath()
227	tmp := p + ".tmp"
228	if err := os.WriteFile(tmp, []byte(changeID+"\n"), 0o644); err != nil {
229		return err
230	}
231	return os.Rename(tmp, p)
232}
233
234func (r *Repo) HeadChangeID() (string, error) {
235	raw, err := r.Head()
236	if err != nil {
237		return "", err
238	}
239	return object.StripChangeIDPrefix(raw), nil
240}
241
242func (r *Repo) HeadCommit() (*object.Commit, [32]byte, error) {
243	cid, err := r.HeadChangeID()
244	if err != nil {
245		return nil, object.ZeroID, err
246	}
247	commitID, err := r.Store.GetChangeCommit(cid)
248	if err != nil {
249		return nil, object.ZeroID, fmt.Errorf("resolve HEAD change %q: %w", cid, err)
250	}
251	c, err := r.ReadCommit(commitID)
252	if err != nil {
253		return nil, commitID, err
254	}
255	return c, commitID, nil
256}
257
258func (r *Repo) ReadCommit(id [32]byte) (*object.Commit, error) {
259	_, raw, err := r.Store.ReadObject(id)
260	if err != nil {
261		return nil, err
262	}
263	return object.DecodeCommit(raw)
264}
265
266func (r *Repo) ReadTree(id [32]byte) (*object.Tree, error) {
267	_, raw, err := r.Store.ReadObject(id)
268	if err != nil {
269		return nil, err
270	}
271	return object.DecodeTree(raw)
272}
273
274func (r *Repo) ReadBlob(id [32]byte) ([]byte, error) {
275	_, raw, err := r.Store.ReadObject(id)
276	if err != nil {
277		return nil, err
278	}
279	b, err := object.DecodeBlob(raw)
280	if err != nil {
281		return nil, err
282	}
283	return b.Content, nil
284}
285
286func (r *Repo) Close() error {
287	return r.Store.Close()
288}
289
290func (r *Repo) SaveConfig() error {
291	return writeConfig(filepath.Join(r.ArcheDir(), configFile), r.Cfg)
292}
293
294func (r *Repo) authorSig(t time.Time) object.Signature {
295	return object.Signature{
296		Name:      r.Cfg.User.Name,
297		Email:     r.Cfg.User.Email,
298		Timestamp: t,
299	}
300}
301
302func WriteBlobTx(s store.Store, tx *store.Tx, b *object.Blob) ([32]byte, error) {
303	id := object.HashBlob(b)
304	var buf bytes.Buffer
305	object.EncodeBlob(&buf, b)
306	return id, s.WriteObject(tx, id, string(object.KindBlob), buf.Bytes())
307}
308
309func WriteConflictTx(s store.Store, tx *store.Tx, c *object.Conflict) ([32]byte, error) {
310	id := object.HashConflict(c)
311	var buf bytes.Buffer
312	object.EncodeConflict(&buf, c)
313	return id, s.WriteObject(tx, id, string(object.KindConflict), buf.Bytes())
314}
315
316func (r *Repo) ReadConflict(id [32]byte) (*object.Conflict, error) {
317	_, raw, err := r.Store.ReadObject(id)
318	if err != nil {
319		return nil, err
320	}
321	return object.DecodeConflict(raw)
322}
323
324func WriteTreeTx(s store.Store, tx *store.Tx, t *object.Tree) ([32]byte, error) {
325	id := object.HashTree(t)
326	var buf bytes.Buffer
327	object.EncodeTree(&buf, t)
328	return id, s.WriteObject(tx, id, string(object.KindTree), buf.Bytes())
329}
330
331func WriteCommitTx(s store.Store, tx *store.Tx, c *object.Commit) ([32]byte, error) {
332	id := object.HashCommit(c)
333	var buf bytes.Buffer
334	object.EncodeCommit(&buf, c)
335	return id, s.WriteObject(tx, id, string(object.KindCommit), buf.Bytes())
336}
337
338func WriteObsoleteTx(s store.Store, tx *store.Tx, o *object.ObsoleteMarker) ([32]byte, error) {
339	id := object.HashObsolete(o)
340	var buf bytes.Buffer
341	object.EncodeObsolete(&buf, o)
342	return id, s.WriteObject(tx, id, string(object.KindObsolete), buf.Bytes())
343}
344
345func refSnapshot(changeID string, commitID [32]byte) string {
346	m := map[string]string{
347		"head": object.FormatChangeID(changeID),
348		"tip":  fmt.Sprintf("%x", commitID),
349	}
350	b, _ := json.Marshal(m)
351	return string(b)
352}
353
354type RefState struct {
355	Head      string            `json:"head"`
356	Tip       string            `json:"tip"`
357	Bookmarks map[string]string `json:"bookmarks,omitempty"`
358}
359
360func (r *Repo) CaptureRefState() (string, error) {
361	changeID, err := r.Head()
362	if err != nil {
363		return "{}", nil
364	}
365	bare := object.StripChangeIDPrefix(changeID)
366	commitID, err := r.Store.GetChangeCommit(bare)
367	if err != nil {
368		return "{}", nil
369	}
370	bms, _ := r.Store.ListBookmarks()
371	bmMap := make(map[string]string, len(bms))
372	for _, bm := range bms {
373		bmMap[bm.Name] = fmt.Sprintf("%x", bm.CommitID)
374	}
375	s := RefState{
376		Head:      changeID,
377		Tip:       fmt.Sprintf("%x", commitID),
378		Bookmarks: bmMap,
379	}
380	b, _ := json.Marshal(s)
381	return string(b), nil
382}