larc r2

439 lines ยท 10.7 KB Raw
1 package cli
2
3 import (
4 "fmt"
5 "net/url"
6 "os"
7 "path/filepath"
8 "strings"
9
10 "github.com/bytedance/sonic"
11 "github.com/spf13/cobra"
12
13 "github.com/lain/larc/internal/core"
14 "github.com/lain/larc/internal/protocol"
15 "github.com/lain/larc/internal/repo"
16 )
17
18 /* Remote commands: clone, pull, push */
19
20 // CloneCmd creates the clone command
21 func CloneCmd() *cobra.Command {
22 cmd := &cobra.Command{
23 Use: "clone <url> [directory]",
24 Short: "Clone a repository from server",
25 Args: cobra.RangeArgs(1, 2),
26 RunE: func(cmd *cobra.Command, args []string) error {
27 repoURL := args[0]
28
29 /* parse URL to get repo name */
30 parsed, err := url.Parse(repoURL)
31 if err != nil {
32 return fmt.Errorf("invalid URL: %w", err)
33 }
34
35 /* extract repo name from path */
36 repoName := strings.TrimPrefix(parsed.Path, "/")
37 repoName = strings.TrimSuffix(repoName, "/")
38
39 /* determine target directory */
40 targetDir := repoName
41 if len(args) > 1 {
42 targetDir = args[1]
43 }
44
45 /* get base URL (without repo path) */
46 baseURL := fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host)
47
48 fmt.Printf("Cloning into '%s'...\n", targetDir)
49
50 /* create client */
51 client := protocol.NewClient(baseURL)
52
53 /* check for auth in URL */
54 if parsed.User != nil {
55 password, _ := parsed.User.Password()
56 client.SetAuth(parsed.User.Username(), password)
57 }
58
59 /* get repo info */
60 info, err := client.GetInfo(repoName)
61 if err != nil {
62 return fmt.Errorf("failed to get repo info: %w", err)
63 }
64
65 fmt.Printf("Remote: %s at r%d\n", info.Name, info.LatestRev)
66
67 /* create local directory */
68 if err := os.MkdirAll(targetDir, 0755); err != nil {
69 return fmt.Errorf("create directory: %w", err)
70 }
71
72 /* init local repo */
73 r, err := repo.Init(targetDir)
74 if err != nil {
75 return fmt.Errorf("init local repo: %w", err)
76 }
77 defer r.Close()
78
79 /* save remote URL */
80 remotePath := filepath.Join(r.Dir, repo.RemoteFile)
81 if err := os.WriteFile(remotePath, []byte(repoURL), 0644); err != nil {
82 return fmt.Errorf("save remote: %w", err)
83 }
84
85 if info.LatestRev == 0 {
86 fmt.Println("Warning: empty repository")
87 return nil
88 }
89
90 /* pull all revisions */
91 fmt.Printf("Receiving objects...\n")
92
93 for revNum := int64(1); revNum <= info.LatestRev; revNum++ {
94 rev, err := client.GetRevision(repoName, revNum)
95 if err != nil {
96 return fmt.Errorf("get revision %d: %w", revNum, err)
97 }
98
99 /* get tree */
100 tree, err := client.GetTree(repoName, revNum)
101 if err != nil {
102 return fmt.Errorf("get tree for r%d: %w", revNum, err)
103 }
104
105 /* download blobs */
106 for _, entry := range tree.Entries {
107 if !r.Blobs.Exists(entry.BlobHash) {
108 data, err := client.GetBlob(repoName, entry.BlobHash)
109 if err != nil {
110 return fmt.Errorf("get blob %s: %w", entry.BlobHash, err)
111 }
112 if _, err := r.Blobs.Write(data); err != nil {
113 return fmt.Errorf("write blob: %w", err)
114 }
115 }
116 }
117
118 /* store tree */
119 treeData, _ := encodeTree(tree)
120 if err := r.Meta.StoreTree(tree.Hash, treeData); err != nil {
121 return fmt.Errorf("store tree: %w", err)
122 }
123
124 /* create revision */
125 if err := r.Meta.CreateRevision(rev); err != nil {
126 return fmt.Errorf("create revision: %w", err)
127 }
128
129 /* update branch */
130 branch, _ := r.Meta.GetBranch(rev.Branch)
131 if branch == nil {
132 branch = &core.Branch{
133 Name: rev.Branch,
134 HeadRev: revNum,
135 CreatedAt: rev.Timestamp,
136 CreatedFrom: rev.Parent,
137 }
138 r.Meta.CreateBranch(branch)
139 } else {
140 r.Meta.UpdateBranchHead(rev.Branch, revNum)
141 }
142
143 fmt.Printf("\r r%d/%d", revNum, info.LatestRev)
144 }
145 fmt.Println()
146
147 /* checkout latest revision */
148 latestRev, _ := client.GetRevision(repoName, info.LatestRev)
149 tree, _ := client.GetTree(repoName, info.LatestRev)
150
151 fmt.Printf("Checking out r%d on branch '%s'...\n", info.LatestRev, latestRev.Branch)
152
153 for _, entry := range tree.Entries {
154 filePath := filepath.Join(targetDir, entry.Path)
155
156 /* create parent dirs */
157 if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
158 continue
159 }
160
161 /* write file */
162 data, err := r.Blobs.Read(entry.BlobHash)
163 if err != nil {
164 continue
165 }
166
167 if err := os.WriteFile(filePath, data, os.FileMode(entry.Mode)); err != nil {
168 continue
169 }
170 }
171
172 /* update current state */
173 r.SetCurrentBranch(latestRev.Branch)
174 r.SetCurrentRevision(info.LatestRev)
175
176 fmt.Println("Done.")
177 return nil
178 },
179 }
180
181 return cmd
182 }
183
184 // PullCmd creates the pull command
185 func PullCmd() *cobra.Command {
186 cmd := &cobra.Command{
187 Use: "pull",
188 Short: "Pull changes from remote",
189 RunE: func(cmd *cobra.Command, args []string) error {
190 wd, _ := os.Getwd()
191 repoRoot, err := repo.FindRepoRoot(wd)
192 if err != nil {
193 return fmt.Errorf("not a larc repository")
194 }
195
196 r, err := repo.Open(repoRoot)
197 if err != nil {
198 return err
199 }
200 defer r.Close()
201
202 /* read remote URL */
203 remotePath := filepath.Join(r.Dir, repo.RemoteFile)
204 remoteData, err := os.ReadFile(remotePath)
205 if err != nil {
206 return fmt.Errorf("no remote configured (use 'larc remote add <url>')")
207 }
208
209 repoURL := string(remoteData)
210 parsed, err := url.Parse(repoURL)
211 if err != nil {
212 return fmt.Errorf("invalid remote URL: %w", err)
213 }
214
215 repoName := strings.TrimPrefix(parsed.Path, "/")
216 repoName = strings.TrimSuffix(repoName, "/")
217 baseURL := fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host)
218
219 client := protocol.NewClient(baseURL)
220 if parsed.User != nil {
221 password, _ := parsed.User.Password()
222 client.SetAuth(parsed.User.Username(), password)
223 }
224
225 /* get current and remote revision */
226 localRev, _ := r.CurrentRevision()
227 remoteRev, err := client.GetLatestRevision(repoName)
228 if err != nil {
229 return fmt.Errorf("get remote revision: %w", err)
230 }
231
232 if remoteRev <= localRev {
233 fmt.Println("Already up to date.")
234 return nil
235 }
236
237 fmt.Printf("Pulling r%d -> r%d...\n", localRev, remoteRev)
238
239 /* pull new revisions */
240 for revNum := localRev + 1; revNum <= remoteRev; revNum++ {
241 rev, err := client.GetRevision(repoName, revNum)
242 if err != nil {
243 return fmt.Errorf("get revision %d: %w", revNum, err)
244 }
245
246 tree, err := client.GetTree(repoName, revNum)
247 if err != nil {
248 return fmt.Errorf("get tree: %w", err)
249 }
250
251 /* download new blobs */
252 for _, entry := range tree.Entries {
253 if !r.Blobs.Exists(entry.BlobHash) {
254 data, err := client.GetBlob(repoName, entry.BlobHash)
255 if err != nil {
256 return fmt.Errorf("get blob: %w", err)
257 }
258 r.Blobs.Write(data)
259 }
260 }
261
262 /* store tree and revision */
263 treeData, _ := encodeTree(tree)
264 r.Meta.StoreTree(tree.Hash, treeData)
265 r.Meta.CreateRevision(rev)
266 r.Meta.UpdateBranchHead(rev.Branch, revNum)
267
268 fmt.Printf(" r%d: %s\n", revNum, rev.Message)
269 }
270
271 /* update working directory */
272 latestRev, _ := client.GetRevision(repoName, remoteRev)
273 tree, _ := client.GetTree(repoName, remoteRev)
274
275 for _, entry := range tree.Entries {
276 filePath := filepath.Join(repoRoot, entry.Path)
277 os.MkdirAll(filepath.Dir(filePath), 0755)
278 data, _ := r.Blobs.Read(entry.BlobHash)
279 os.WriteFile(filePath, data, os.FileMode(entry.Mode))
280 }
281
282 r.SetCurrentBranch(latestRev.Branch)
283 r.SetCurrentRevision(remoteRev)
284
285 fmt.Printf("Updated to r%d\n", remoteRev)
286 return nil
287 },
288 }
289
290 return cmd
291 }
292
293 // PushCmd creates the push command
294 func PushCmd() *cobra.Command {
295 cmd := &cobra.Command{
296 Use: "push",
297 Short: "Push changes to remote",
298 RunE: func(cmd *cobra.Command, args []string) error {
299 wd, _ := os.Getwd()
300 repoRoot, err := repo.FindRepoRoot(wd)
301 if err != nil {
302 return fmt.Errorf("not a larc repository")
303 }
304
305 r, err := repo.Open(repoRoot)
306 if err != nil {
307 return err
308 }
309 defer r.Close()
310
311 /* read remote URL */
312 remotePath := filepath.Join(r.Dir, repo.RemoteFile)
313 remoteData, err := os.ReadFile(remotePath)
314 if err != nil {
315 return fmt.Errorf("no remote configured")
316 }
317
318 repoURL := string(remoteData)
319 parsed, err := url.Parse(repoURL)
320 if err != nil {
321 return fmt.Errorf("invalid remote URL: %w", err)
322 }
323
324 repoName := strings.TrimPrefix(parsed.Path, "/")
325 repoName = strings.TrimSuffix(repoName, "/")
326 baseURL := fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host)
327
328 client := protocol.NewClient(baseURL)
329 if parsed.User != nil {
330 password, _ := parsed.User.Password()
331 client.SetAuth(parsed.User.Username(), password)
332 }
333
334 /* get current and remote revision */
335 localRev, _ := r.CurrentRevision()
336 remoteRev, err := client.GetLatestRevision(repoName)
337 if err != nil {
338 return fmt.Errorf("get remote revision: %w", err)
339 }
340
341 if localRev <= remoteRev {
342 fmt.Println("Nothing to push.")
343 return nil
344 }
345
346 fmt.Printf("Pushing r%d -> r%d...\n", remoteRev+1, localRev)
347
348 /* push new revisions */
349 for revNum := remoteRev + 1; revNum <= localRev; revNum++ {
350 rev, err := r.Meta.GetRevision(revNum)
351 if err != nil {
352 return fmt.Errorf("get local revision %d: %w", revNum, err)
353 }
354
355 tree, err := r.GetTree(rev.TreeHash)
356 if err != nil {
357 return fmt.Errorf("get tree: %w", err)
358 }
359
360 /* upload blobs */
361 for _, entry := range tree.Entries {
362 data, err := r.Blobs.Read(entry.BlobHash)
363 if err != nil {
364 continue
365 }
366 client.UploadBlob(repoName, data)
367 }
368
369 /* create commit on server */
370 commitReq := &protocol.CommitRequest{
371 Branch: rev.Branch,
372 Message: rev.Message,
373 Author: rev.Author,
374 Entries: tree.Entries,
375 }
376
377 _, err = client.Commit(repoName, commitReq)
378 if err != nil {
379 return fmt.Errorf("push revision %d: %w", revNum, err)
380 }
381
382 fmt.Printf(" r%d -> remote\n", revNum)
383 }
384
385 fmt.Println("Done.")
386 return nil
387 },
388 }
389
390 return cmd
391 }
392
393 // RemoteCmd creates the remote command
394 func RemoteCmd() *cobra.Command {
395 cmd := &cobra.Command{
396 Use: "remote [url]",
397 Short: "Show or set remote URL",
398 RunE: func(cmd *cobra.Command, args []string) error {
399 wd, _ := os.Getwd()
400 repoRoot, err := repo.FindRepoRoot(wd)
401 if err != nil {
402 return fmt.Errorf("not a larc repository")
403 }
404
405 r, err := repo.Open(repoRoot)
406 if err != nil {
407 return err
408 }
409 defer r.Close()
410
411 remotePath := filepath.Join(r.Dir, repo.RemoteFile)
412
413 if len(args) == 0 {
414 /* show current remote */
415 data, err := os.ReadFile(remotePath)
416 if err != nil {
417 fmt.Println("No remote configured")
418 return nil
419 }
420 fmt.Println(string(data))
421 return nil
422 }
423
424 /* set remote */
425 if err := os.WriteFile(remotePath, []byte(args[0]), 0644); err != nil {
426 return fmt.Errorf("save remote: %w", err)
427 }
428 fmt.Printf("Remote set to: %s\n", args[0])
429 return nil
430 },
431 }
432
433 return cmd
434 }
435
436 func encodeTree(tree *core.Tree) ([]byte, error) {
437 return sonic.Marshal(tree)
438 }
439