larc r2

281 lines ยท 6.8 KB Raw
1 package repo
2
3 import (
4 "errors"
5 "fmt"
6 "os"
7 "path/filepath"
8 "time"
9
10 "github.com/bytedance/sonic"
11
12 "github.com/lain/larc/internal/core"
13 "github.com/lain/larc/internal/hash"
14 "github.com/lain/larc/internal/storage"
15 )
16
17 /* Repository is the main interface for larc operations.
18 * Combines metadata storage (SQLite) with blob storage (files + zstd).
19 * Handles init, commit, checkout, and branch operations. */
20
21 const (
22 LarcDir = ".larc"
23 ConfigFile = "config.yaml"
24 DBFile = "larc.db"
25 ObjectsDir = "objects"
26 TreesDir = "trees"
27 BranchFile = "branch"
28 RevisionFile = "revision"
29 RemoteFile = "remote"
30 )
31
32 var (
33 ErrNotARepo = errors.New("not a larc repository")
34 ErrAlreadyExists = errors.New("repository already exists")
35 ErrNoChanges = errors.New("no changes to commit")
36 )
37
38 // Config is the repository configuration stored in config.yaml
39 type Config struct {
40 Name string `yaml:"name" json:"name"`
41 DefaultBranch string `yaml:"default_branch" json:"default_branch"`
42 CompressionLevel int `yaml:"compression" json:"compression"`
43 }
44
45 // Repository represents a larc repository
46 type Repository struct {
47 Root string // working directory root
48 Dir string // .larc directory path
49 Config *Config // repository configuration
50 Meta *storage.MetaStore // SQLite metadata
51 Blobs *storage.BlobStore // blob storage
52 }
53
54 // Open opens an existing repository
55 func Open(path string) (*Repository, error) {
56 larcDir := filepath.Join(path, LarcDir)
57
58 if _, err := os.Stat(larcDir); os.IsNotExist(err) {
59 return nil, ErrNotARepo
60 }
61
62 meta, err := storage.NewMetaStore(filepath.Join(larcDir, DBFile))
63 if err != nil {
64 return nil, fmt.Errorf("open meta store: %w", err)
65 }
66
67 blobs, err := storage.NewBlobStore(filepath.Join(larcDir, ObjectsDir))
68 if err != nil {
69 meta.Close()
70 return nil, fmt.Errorf("open blob store: %w", err)
71 }
72
73 repo := &Repository{
74 Root: path,
75 Dir: larcDir,
76 Meta: meta,
77 Blobs: blobs,
78 Config: &Config{DefaultBranch: core.DefaultBranchName},
79 }
80
81 return repo, nil
82 }
83
84 // Init creates a new repository
85 func Init(path string) (*Repository, error) {
86 larcDir := filepath.Join(path, LarcDir)
87
88 if _, err := os.Stat(larcDir); err == nil {
89 return nil, ErrAlreadyExists
90 }
91
92 /* create directory structure */
93 dirs := []string{
94 larcDir,
95 filepath.Join(larcDir, ObjectsDir),
96 filepath.Join(larcDir, TreesDir),
97 }
98
99 for _, dir := range dirs {
100 if err := os.MkdirAll(dir, 0755); err != nil {
101 return nil, fmt.Errorf("create dir %s: %w", dir, err)
102 }
103 }
104
105 /* init meta store */
106 meta, err := storage.NewMetaStore(filepath.Join(larcDir, DBFile))
107 if err != nil {
108 return nil, fmt.Errorf("init meta store: %w", err)
109 }
110
111 if err := meta.InitRepo(); err != nil {
112 meta.Close()
113 return nil, fmt.Errorf("init repo: %w", err)
114 }
115
116 /* init blob store */
117 blobs, err := storage.NewBlobStore(filepath.Join(larcDir, ObjectsDir))
118 if err != nil {
119 meta.Close()
120 return nil, fmt.Errorf("init blob store: %w", err)
121 }
122
123 /* write current branch */
124 if err := os.WriteFile(filepath.Join(larcDir, BranchFile), []byte(core.DefaultBranchName), 0644); err != nil {
125 meta.Close()
126 return nil, fmt.Errorf("write branch file: %w", err)
127 }
128
129 /* write current revision (0 = no commits yet) */
130 if err := os.WriteFile(filepath.Join(larcDir, RevisionFile), []byte("0"), 0644); err != nil {
131 meta.Close()
132 return nil, fmt.Errorf("write revision file: %w", err)
133 }
134
135 repo := &Repository{
136 Root: path,
137 Dir: larcDir,
138 Meta: meta,
139 Blobs: blobs,
140 Config: &Config{DefaultBranch: core.DefaultBranchName, CompressionLevel: storage.DefaultCompressionLevel},
141 }
142
143 return repo, nil
144 }
145
146 // Close closes the repository
147 func (r *Repository) Close() error {
148 return r.Meta.Close()
149 }
150
151 // CurrentBranch returns the current branch name
152 func (r *Repository) CurrentBranch() (string, error) {
153 data, err := os.ReadFile(filepath.Join(r.Dir, BranchFile))
154 if err != nil {
155 return "", fmt.Errorf("read branch file: %w", err)
156 }
157 return string(data), nil
158 }
159
160 // SetCurrentBranch sets the current branch
161 func (r *Repository) SetCurrentBranch(name string) error {
162 return os.WriteFile(filepath.Join(r.Dir, BranchFile), []byte(name), 0644)
163 }
164
165 // CurrentRevision returns the current revision number
166 func (r *Repository) CurrentRevision() (int64, error) {
167 data, err := os.ReadFile(filepath.Join(r.Dir, RevisionFile))
168 if err != nil {
169 return 0, fmt.Errorf("read revision file: %w", err)
170 }
171
172 var rev int64
173 fmt.Sscanf(string(data), "%d", &rev)
174 return rev, nil
175 }
176
177 // SetCurrentRevision sets the current revision
178 func (r *Repository) SetCurrentRevision(rev int64) error {
179 return os.WriteFile(filepath.Join(r.Dir, RevisionFile), []byte(fmt.Sprintf("%d", rev)), 0644)
180 }
181
182 // Commit creates a new revision from the given entries
183 func (r *Repository) Commit(author, message string, entries []core.TreeEntry) (*core.Revision, error) {
184 if len(entries) == 0 {
185 return nil, ErrNoChanges
186 }
187
188 branch, err := r.CurrentBranch()
189 if err != nil {
190 return nil, err
191 }
192
193 parent, err := r.CurrentRevision()
194 if err != nil {
195 return nil, err
196 }
197
198 /* create tree */
199 tree := &core.Tree{Entries: entries}
200 treeData, err := sonic.Marshal(tree)
201 if err != nil {
202 return nil, fmt.Errorf("marshal tree: %w", err)
203 }
204 tree.Hash = hash.Bytes(treeData)
205
206 /* store tree */
207 if err := r.Meta.StoreTree(tree.Hash, treeData); err != nil {
208 return nil, fmt.Errorf("store tree: %w", err)
209 }
210
211 /* get next revision number */
212 revNum, err := r.Meta.NextRevision()
213 if err != nil {
214 return nil, fmt.Errorf("get next revision: %w", err)
215 }
216
217 /* create revision */
218 rev := &core.Revision{
219 Number: revNum,
220 Timestamp: time.Now().Unix(),
221 Author: author,
222 Message: message,
223 Branch: branch,
224 Parent: parent,
225 TreeHash: tree.Hash,
226 }
227
228 if err := r.Meta.CreateRevision(rev); err != nil {
229 return nil, fmt.Errorf("create revision: %w", err)
230 }
231
232 /* update branch head */
233 if err := r.Meta.UpdateBranchHead(branch, revNum); err != nil {
234 return nil, fmt.Errorf("update branch head: %w", err)
235 }
236
237 /* update current revision */
238 if err := r.SetCurrentRevision(revNum); err != nil {
239 return nil, fmt.Errorf("set current revision: %w", err)
240 }
241
242 return rev, nil
243 }
244
245 // GetTree retrieves and parses a tree by hash
246 func (r *Repository) GetTree(h string) (*core.Tree, error) {
247 data, err := r.Meta.GetTree(h)
248 if err != nil {
249 return nil, err
250 }
251
252 var tree core.Tree
253 if err := sonic.Unmarshal(data, &tree); err != nil {
254 return nil, fmt.Errorf("unmarshal tree: %w", err)
255 }
256 tree.Hash = h
257
258 return &tree, nil
259 }
260
261 // FindRepoRoot walks up from path to find repository root
262 func FindRepoRoot(path string) (string, error) {
263 absPath, err := filepath.Abs(path)
264 if err != nil {
265 return "", err
266 }
267
268 for {
269 larcDir := filepath.Join(absPath, LarcDir)
270 if _, err := os.Stat(larcDir); err == nil {
271 return absPath, nil
272 }
273
274 parent := filepath.Dir(absPath)
275 if parent == absPath {
276 return "", ErrNotARepo
277 }
278 absPath = parent
279 }
280 }
281