package cli import ( "fmt" "net/url" "os" "path/filepath" "strings" "github.com/bytedance/sonic" "github.com/spf13/cobra" "github.com/lain/larc/internal/core" "github.com/lain/larc/internal/protocol" "github.com/lain/larc/internal/repo" "github.com/lain/larc/internal/status" ) /* Remote commands: clone, pull, push */ // CloneCmd creates the clone command func CloneCmd() *cobra.Command { cmd := &cobra.Command{ Use: "clone [directory]", Short: "Clone a repository from server", Args: cobra.RangeArgs(1, 2), RunE: func(cmd *cobra.Command, args []string) error { repoURL := args[0] /* parse URL to get repo name */ parsed, err := url.Parse(repoURL) if err != nil { return fmt.Errorf("invalid URL: %w", err) } /* extract repo name from path */ repoName := strings.TrimPrefix(parsed.Path, "/") repoName = strings.TrimSuffix(repoName, "/") /* determine target directory */ targetDir := repoName if len(args) > 1 { targetDir = args[1] } /* get base URL (without repo path) */ baseURL := fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host) fmt.Printf("Cloning into '%s'...\n", targetDir) /* create client */ client := protocol.NewClient(baseURL) /* check for auth in URL */ if parsed.User != nil { password, _ := parsed.User.Password() client.SetAuth(parsed.User.Username(), password) } /* get repo info */ info, err := client.GetInfo(repoName) if err != nil { return fmt.Errorf("failed to get repo info: %w", err) } fmt.Printf("Remote: %s at r%d\n", info.Name, info.LatestRev) /* create local directory */ if err := os.MkdirAll(targetDir, 0755); err != nil { return fmt.Errorf("create directory: %w", err) } /* init local repo */ r, err := repo.Init(targetDir) if err != nil { return fmt.Errorf("init local repo: %w", err) } defer r.Close() /* save remote URL */ remotePath := filepath.Join(r.Dir, repo.RemoteFile) if err := os.WriteFile(remotePath, []byte(repoURL), 0644); err != nil { return fmt.Errorf("save remote: %w", err) } if info.LatestRev == 0 { fmt.Println("Warning: empty repository") return nil } /* pull all revisions */ fmt.Printf("Receiving objects...\n") for revNum := int64(1); revNum <= info.LatestRev; revNum++ { rev, err := client.GetRevision(repoName, revNum) if err != nil { return fmt.Errorf("get revision %d: %w", revNum, err) } /* get tree */ tree, err := client.GetTree(repoName, revNum) if err != nil { return fmt.Errorf("get tree for r%d: %w", revNum, err) } /* download blobs */ for _, entry := range tree.Entries { if !r.Blobs.Exists(entry.BlobHash) { data, err := client.GetBlob(repoName, entry.BlobHash) if err != nil { return fmt.Errorf("get blob %s: %w", entry.BlobHash, err) } if _, err := r.Blobs.Write(data); err != nil { return fmt.Errorf("write blob: %w", err) } } } /* store tree */ treeData, _ := encodeTree(tree) if err := r.Meta.StoreTree(tree.Hash, treeData); err != nil { return fmt.Errorf("store tree: %w", err) } /* create revision */ if err := r.Meta.CreateRevision(rev); err != nil { return fmt.Errorf("create revision: %w", err) } /* update branch */ branch, _ := r.Meta.GetBranch(rev.Branch) if branch == nil { branch = &core.Branch{ Name: rev.Branch, HeadRev: revNum, CreatedAt: rev.Timestamp, CreatedFrom: rev.Parent, } r.Meta.CreateBranch(branch) } else { r.Meta.UpdateBranchHead(rev.Branch, revNum) } fmt.Printf("\r r%d/%d", revNum, info.LatestRev) } fmt.Println() /* checkout latest revision */ latestRev, _ := client.GetRevision(repoName, info.LatestRev) tree, _ := client.GetTree(repoName, info.LatestRev) fmt.Printf("Checking out r%d on branch '%s'...\n", info.LatestRev, latestRev.Branch) for _, entry := range tree.Entries { filePath := filepath.Join(targetDir, entry.Path) /* create parent dirs */ if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { continue } /* write file */ data, err := r.Blobs.Read(entry.BlobHash) if err != nil { continue } if err := os.WriteFile(filePath, data, os.FileMode(entry.Mode)); err != nil { continue } } /* update current state */ r.SetCurrentBranch(latestRev.Branch) r.SetCurrentRevision(info.LatestRev) /* update index to track checked out files */ scanner := status.NewScanner(r) defer scanner.Close() for _, entry := range tree.Entries { scanner.Stage(entry.Path, entry.BlobHash, entry.Size) } scanner.SaveIndex() scanner.ClearStaging() fmt.Println("Done.") return nil }, } return cmd } // PullCmd creates the pull command func PullCmd() *cobra.Command { cmd := &cobra.Command{ Use: "pull", Short: "Pull changes from remote", RunE: func(cmd *cobra.Command, args []string) error { wd, _ := os.Getwd() repoRoot, err := repo.FindRepoRoot(wd) if err != nil { return fmt.Errorf("not a larc repository") } r, err := repo.Open(repoRoot) if err != nil { return err } defer r.Close() /* read remote URL */ remotePath := filepath.Join(r.Dir, repo.RemoteFile) remoteData, err := os.ReadFile(remotePath) if err != nil { return fmt.Errorf("no remote configured (use 'larc remote add ')") } repoURL := string(remoteData) parsed, err := url.Parse(repoURL) if err != nil { return fmt.Errorf("invalid remote URL: %w", err) } repoName := strings.TrimPrefix(parsed.Path, "/") repoName = strings.TrimSuffix(repoName, "/") baseURL := fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host) client := protocol.NewClient(baseURL) if parsed.User != nil { password, _ := parsed.User.Password() client.SetAuth(parsed.User.Username(), password) } /* get current and remote revision */ localRev, _ := r.CurrentRevision() remoteRev, err := client.GetLatestRevision(repoName) if err != nil { return fmt.Errorf("get remote revision: %w", err) } if remoteRev <= localRev { fmt.Println("Already up to date.") return nil } fmt.Printf("Pulling r%d -> r%d...\n", localRev, remoteRev) /* pull new revisions */ for revNum := localRev + 1; revNum <= remoteRev; revNum++ { rev, err := client.GetRevision(repoName, revNum) if err != nil { return fmt.Errorf("get revision %d: %w", revNum, err) } tree, err := client.GetTree(repoName, revNum) if err != nil { return fmt.Errorf("get tree: %w", err) } /* download new blobs */ for _, entry := range tree.Entries { if !r.Blobs.Exists(entry.BlobHash) { data, err := client.GetBlob(repoName, entry.BlobHash) if err != nil { return fmt.Errorf("get blob: %w", err) } r.Blobs.Write(data) } } /* store tree and revision */ treeData, _ := encodeTree(tree) r.Meta.StoreTree(tree.Hash, treeData) r.Meta.CreateRevision(rev) r.Meta.UpdateBranchHead(rev.Branch, revNum) fmt.Printf(" r%d: %s\n", revNum, rev.Message) } /* update working directory */ latestRev, _ := client.GetRevision(repoName, remoteRev) tree, _ := client.GetTree(repoName, remoteRev) for _, entry := range tree.Entries { filePath := filepath.Join(repoRoot, entry.Path) os.MkdirAll(filepath.Dir(filePath), 0755) data, _ := r.Blobs.Read(entry.BlobHash) os.WriteFile(filePath, data, os.FileMode(entry.Mode)) } r.SetCurrentBranch(latestRev.Branch) r.SetCurrentRevision(remoteRev) /* update index to track checked out files */ scanner := status.NewScanner(r) defer scanner.Close() for _, entry := range tree.Entries { scanner.Stage(entry.Path, entry.BlobHash, entry.Size) } scanner.SaveIndex() scanner.ClearStaging() fmt.Printf("Updated to r%d\n", remoteRev) return nil }, } return cmd } // PushCmd creates the push command func PushCmd() *cobra.Command { cmd := &cobra.Command{ Use: "push", Short: "Push changes to remote", RunE: func(cmd *cobra.Command, args []string) error { wd, _ := os.Getwd() repoRoot, err := repo.FindRepoRoot(wd) if err != nil { return fmt.Errorf("not a larc repository") } r, err := repo.Open(repoRoot) if err != nil { return err } defer r.Close() /* read remote URL */ remotePath := filepath.Join(r.Dir, repo.RemoteFile) remoteData, err := os.ReadFile(remotePath) if err != nil { return fmt.Errorf("no remote configured") } repoURL := string(remoteData) parsed, err := url.Parse(repoURL) if err != nil { return fmt.Errorf("invalid remote URL: %w", err) } repoName := strings.TrimPrefix(parsed.Path, "/") repoName = strings.TrimSuffix(repoName, "/") baseURL := fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host) client := protocol.NewClient(baseURL) if parsed.User != nil { password, _ := parsed.User.Password() client.SetAuth(parsed.User.Username(), password) } /* get current and remote revision */ localRev, _ := r.CurrentRevision() remoteRev, err := client.GetLatestRevision(repoName) if err != nil { return fmt.Errorf("get remote revision: %w", err) } if localRev <= remoteRev { fmt.Println("Nothing to push.") return nil } fmt.Printf("Pushing r%d -> r%d...\n", remoteRev+1, localRev) /* push new revisions */ for revNum := remoteRev + 1; revNum <= localRev; revNum++ { rev, err := r.Meta.GetRevision(revNum) if err != nil { return fmt.Errorf("get local revision %d: %w", revNum, err) } tree, err := r.GetTree(rev.TreeHash) if err != nil { return fmt.Errorf("get tree: %w", err) } /* upload blobs */ for _, entry := range tree.Entries { data, err := r.Blobs.Read(entry.BlobHash) if err != nil { continue } client.UploadBlob(repoName, data) } /* create commit on server */ commitReq := &protocol.CommitRequest{ Branch: rev.Branch, Message: rev.Message, Author: rev.Author, Entries: tree.Entries, } _, err = client.Commit(repoName, commitReq) if err != nil { return fmt.Errorf("push revision %d: %w", revNum, err) } fmt.Printf(" r%d -> remote\n", revNum) } fmt.Println("Done.") return nil }, } return cmd } // RemoteCmd creates the remote command func RemoteCmd() *cobra.Command { cmd := &cobra.Command{ Use: "remote [url]", Short: "Show or set remote URL", RunE: func(cmd *cobra.Command, args []string) error { wd, _ := os.Getwd() repoRoot, err := repo.FindRepoRoot(wd) if err != nil { return fmt.Errorf("not a larc repository") } r, err := repo.Open(repoRoot) if err != nil { return err } defer r.Close() remotePath := filepath.Join(r.Dir, repo.RemoteFile) if len(args) == 0 { /* show current remote */ data, err := os.ReadFile(remotePath) if err != nil { fmt.Println("No remote configured") return nil } fmt.Println(string(data)) return nil } /* set remote */ if err := os.WriteFile(remotePath, []byte(args[0]), 0644); err != nil { return fmt.Errorf("save remote: %w", err) } fmt.Printf("Remote set to: %s\n", args[0]) return nil }, } return cmd } func encodeTree(tree *core.Tree) ([]byte, error) { return sonic.Marshal(tree) }