arche / internal/repo/worktree.go

commit 154431fd
  1package repo
  2
  3import (
  4	"fmt"
  5	"os"
  6	"path/filepath"
  7	"strings"
  8
  9	"arche/internal/object"
 10)
 11
 12const worktreesDir = "worktrees"
 13
 14type WorktreeInfo struct {
 15	Name string
 16	Path string
 17}
 18
 19func (r *Repo) AddWorktree(path, bookmark string) error {
 20	absPath, err := filepath.Abs(path)
 21	if err != nil {
 22		return fmt.Errorf("worktree add: resolve path: %w", err)
 23	}
 24
 25	var commitID [32]byte
 26	var changeID string
 27	if bookmark != "" {
 28		bms, err := r.Store.ListBookmarks()
 29		if err != nil {
 30			return err
 31		}
 32		found := false
 33		for _, bm := range bms {
 34			if bm.Name == bookmark {
 35				commitID = bm.CommitID
 36				found = true
 37				break
 38			}
 39		}
 40		if !found {
 41			return fmt.Errorf("worktree add: bookmark %q not found", bookmark)
 42		}
 43		c, err := r.ReadCommit(commitID)
 44		if err != nil {
 45			return fmt.Errorf("worktree add: read commit: %w", err)
 46		}
 47		changeID = object.FormatChangeID(c.ChangeID)
 48	} else {
 49		c, id, err := r.HeadCommit()
 50		if err != nil {
 51			return fmt.Errorf("worktree add: read HEAD commit: %w", err)
 52		}
 53		commitID = id
 54		changeID = object.FormatChangeID(c.ChangeID)
 55	}
 56
 57	name := filepath.Base(absPath)
 58	if name == "" || name == "." {
 59		name = "wt"
 60	}
 61	wtBaseDir := filepath.Join(r.archeDir, worktreesDir)
 62	wtDir := filepath.Join(wtBaseDir, name)
 63	if _, err := os.Stat(wtDir); err == nil {
 64		for i := 2; ; i++ {
 65			cand := fmt.Sprintf("%s%d", name, i)
 66			if _, err := os.Stat(filepath.Join(wtBaseDir, cand)); os.IsNotExist(err) {
 67				name = cand
 68				wtDir = filepath.Join(wtBaseDir, cand)
 69				break
 70			}
 71		}
 72	}
 73
 74	if err := os.MkdirAll(wtDir, 0o755); err != nil {
 75		return fmt.Errorf("worktree add: create meta dir: %w", err)
 76	}
 77
 78	headFilePath := filepath.Join(wtDir, headFile)
 79	if err := os.WriteFile(headFilePath, []byte(changeID+"\n"), 0o644); err != nil {
 80		os.RemoveAll(wtDir) //nolint:errcheck
 81		return fmt.Errorf("worktree add: write HEAD: %w", err)
 82	}
 83
 84	if err := os.WriteFile(filepath.Join(wtDir, "path"), []byte(absPath+"\n"), 0o644); err != nil {
 85		os.RemoveAll(wtDir) //nolint:errcheck
 86		return fmt.Errorf("worktree add: write path: %w", err)
 87	}
 88
 89	if err := os.MkdirAll(absPath, 0o755); err != nil {
 90		os.RemoveAll(wtDir) //nolint:errcheck
 91		return fmt.Errorf("worktree add: create directory: %w", err)
 92	}
 93
 94	mainRoot := filepath.Dir(r.archeDir)
 95	sentinel := mainRoot + "\n" + name + "\n"
 96	sentinelPath := filepath.Join(absPath, worktreeFile)
 97	if err := os.WriteFile(sentinelPath, []byte(sentinel), 0o644); err != nil {
 98		os.RemoveAll(wtDir) //nolint:errcheck
 99		return fmt.Errorf("worktree add: write sentinel: %w", err)
100	}
101
102	if err := r.materializeInto(absPath, commitID); err != nil {
103		os.RemoveAll(wtDir)     //nolint:errcheck
104		os.Remove(sentinelPath) //nolint:errcheck
105		return fmt.Errorf("worktree add: materialize: %w", err)
106	}
107
108	return nil
109}
110
111func (r *Repo) materializeInto(targetDir string, commitID [32]byte) error {
112	c, err := r.ReadCommit(commitID)
113	if err != nil {
114		return err
115	}
116	return r.writeTree(targetDir, c.TreeID)
117}
118
119func (r *Repo) writeTree(dir string, treeID [32]byte) error {
120	if treeID == object.ZeroID {
121		return nil
122	}
123	_, raw, err := r.Store.ReadObject(treeID)
124	if err != nil {
125		return err
126	}
127	tree, err := object.DecodeTree(raw)
128	if err != nil {
129		return err
130	}
131	for _, entry := range tree.Entries {
132		targetPath := filepath.Join(dir, entry.Name)
133		switch entry.Mode {
134		case object.ModeDir:
135			if err := os.MkdirAll(targetPath, 0o755); err != nil {
136				return err
137			}
138			if err := r.writeTree(targetPath, entry.ObjectID); err != nil {
139				return err
140			}
141		case object.ModeExec:
142			if err := r.writeBlob(targetPath, entry.ObjectID, 0o755); err != nil {
143				return err
144			}
145		case object.ModeSymlink:
146			content, err := r.readBlobContent(entry.ObjectID)
147			if err != nil {
148				return err
149			}
150			os.Remove(targetPath) //nolint:errcheck
151			if err := os.Symlink(string(content), targetPath); err != nil {
152				return err
153			}
154		default:
155			if err := r.writeBlob(targetPath, entry.ObjectID, 0o644); err != nil {
156				return err
157			}
158		}
159	}
160	return nil
161}
162
163func (r *Repo) writeBlob(path string, blobID [32]byte, perm os.FileMode) error {
164	content, err := r.readBlobContent(blobID)
165	if err != nil {
166		return err
167	}
168	tmp := path + ".arche-tmp"
169	if err := os.WriteFile(tmp, content, perm); err != nil {
170		return err
171	}
172	return os.Rename(tmp, path)
173}
174
175func (r *Repo) readBlobContent(blobID [32]byte) ([]byte, error) {
176	kind, raw, err := r.Store.ReadObject(blobID)
177	if err != nil {
178		return nil, err
179	}
180	if kind != string(object.KindBlob) {
181		return nil, fmt.Errorf("expected blob, got %s", kind)
182	}
183	b, err := object.DecodeBlob(raw)
184	if err != nil {
185		return nil, err
186	}
187	return b.Content, nil
188}
189
190func (r *Repo) ListWorktrees() ([]WorktreeInfo, error) {
191	wtBaseDir := filepath.Join(r.archeDir, worktreesDir)
192	entries, err := os.ReadDir(wtBaseDir)
193	if os.IsNotExist(err) {
194		return nil, nil
195	}
196	if err != nil {
197		return nil, err
198	}
199	var out []WorktreeInfo
200	for _, e := range entries {
201		if !e.IsDir() {
202			continue
203		}
204		pathBytes, err := os.ReadFile(filepath.Join(wtBaseDir, e.Name(), "path"))
205		if err != nil {
206			continue
207		}
208		out = append(out, WorktreeInfo{
209			Name: e.Name(),
210			Path: strings.TrimSpace(string(pathBytes)),
211		})
212	}
213	return out, nil
214}
215
216func (r *Repo) RemoveWorktree(name string) error {
217	wtDir := filepath.Join(r.archeDir, worktreesDir, name)
218	if _, err := os.Stat(wtDir); os.IsNotExist(err) {
219		return fmt.Errorf("worktree %q not found", name)
220	}
221
222	pathBytes, err := os.ReadFile(filepath.Join(wtDir, "path"))
223	if err == nil {
224		wtPath := strings.TrimSpace(string(pathBytes))
225		os.Remove(filepath.Join(wtPath, worktreeFile)) //nolint:errcheck
226	}
227
228	return os.RemoveAll(wtDir)
229}