larc r18

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