package cmd import ( "encoding/base64" "fmt" "io" "os/exec" "strings" "rickub.com/rickub/cli/internal/api" "rickub.com/rickub/cli/internal/config" "github.com/spf13/cobra" ) var ( repoListUser string repoListOrg string repoCreateOrg string repoCreatePriv bool repoCreatePub bool repoCreateDesc string repoEditVis string repoEditDesc string repoEditBranch string repoDeleteYes bool repoContentsRef string pageFlag int perPageFlag int ) func init() { repoCmd := &cobra.Command{ Use: "repo", Aliases: []string{"repos"}, Short: "Manage repositories", } // create createCmd := &cobra.Command{ Use: "create ", Short: "Create a repository", Long: `Create a repository owned by you, or by an org via --org. By default repositories are private; pass --public to create a public one.`, Args: cobra.ExactArgs(1), RunE: runRepoCreate, } createCmd.Flags().StringVar(&repoCreateOrg, "org", "", "create under this org (default: your account)") createCmd.Flags().BoolVar(&repoCreatePub, "public", false, "make the repository public") createCmd.Flags().BoolVar(&repoCreatePriv, "private", false, "make the repository private (default)") createCmd.Flags().StringVarP(&repoCreateDesc, "description", "d", "", "repository description") // list listCmd := &cobra.Command{ Use: "list", Short: "List repositories for a user or org", Args: cobra.NoArgs, RunE: runRepoList, } listCmd.Flags().StringVar(&repoListUser, "user", "", "list this user's repositories") listCmd.Flags().StringVar(&repoListOrg, "org", "", "list this org's repositories") addPaging(listCmd) // view viewCmd := &cobra.Command{ Use: "view ", Short: "Show a repository", Args: cobra.ExactArgs(1), RunE: runRepoView, } // edit editCmd := &cobra.Command{ Use: "edit ", Short: "Edit visibility, description, or default branch", Args: cobra.ExactArgs(1), RunE: runRepoEdit, } editCmd.Flags().StringVar(&repoEditVis, "visibility", "", "public | private") editCmd.Flags().StringVarP(&repoEditDesc, "description", "d", "", "new description") editCmd.Flags().StringVar(&repoEditBranch, "default-branch", "", "new default branch") // delete deleteCmd := &cobra.Command{ Use: "delete ", Short: "Delete a repository and all of its contents", Args: cobra.ExactArgs(1), RunE: runRepoDelete, } deleteCmd.Flags().BoolVar(&repoDeleteYes, "yes", false, "skip the confirmation prompt") // clone cloneCmd := &cobra.Command{ Use: "clone [dir] [-- git-args…]", Short: "Clone a repository with git", Long: `Clone a repository by shelling out to git. The clone URL is derived from the configured host as //.git. Extra arguments after -- are passed through to git clone.`, Args: cobra.MinimumNArgs(1), RunE: runRepoClone, } // files (list dir) filesCmd := &cobra.Command{ Use: "files [path]", Short: "List a directory in a repository", Args: cobra.RangeArgs(1, 2), RunE: runRepoFiles, } filesCmd.Flags().StringVar(&repoContentsRef, "ref", "", "branch, tag, or SHA (default: default branch)") // cat (file content) catCmd := &cobra.Command{ Use: "cat ", Short: "Print a file's contents", Args: cobra.ExactArgs(2), RunE: runRepoCat, } catCmd.Flags().StringVar(&repoContentsRef, "ref", "", "branch, tag, or SHA (default: default branch)") // commits commitsCmd := &cobra.Command{ Use: "commits [ref]", Short: "List commit history reachable from a ref", Args: cobra.RangeArgs(1, 2), RunE: runRepoCommits, } addPaging(commitsCmd) // compare compareCmd := &cobra.Command{ Use: "compare ", Short: "Compare two refs (base...head)", Args: cobra.ExactArgs(2), RunE: runRepoCompare, } repoCmd.AddCommand(createCmd, listCmd, viewCmd, editCmd, deleteCmd, cloneCmd, filesCmd, catCmd, commitsCmd, compareCmd, collaboratorCmd()) rootCmd.AddCommand(repoCmd) } func addPaging(c *cobra.Command) { c.Flags().IntVar(&pageFlag, "page", 0, "page number (1-based)") c.Flags().IntVar(&perPageFlag, "per-page", 0, "results per page (max 100)") } func runRepoCreate(cmd *cobra.Command, args []string) error { client, err := newClient() if err != nil { return err } if repoCreatePub && repoCreatePriv { return fmt.Errorf("--public and --private are mutually exclusive") } vis := "" if repoCreatePub { vis = "public" } else if repoCreatePriv { vis = "private" } r, err := client.CreateRepo(cmd.Context(), api.RepoCreate{ Owner: repoCreateOrg, Name: args[0], Visibility: vis, Description: repoCreateDesc, }) if err != nil { return err } if flagJSON { return printJSON(cmd.OutOrStdout(), r) } fmt.Fprintf(cmd.OutOrStdout(), "Created %s (%s)\n", r.FullName, r.Visibility) return nil } func runRepoList(cmd *cobra.Command, _ []string) error { client, err := newClient() if err != nil { return err } if repoListUser != "" && repoListOrg != "" { return fmt.Errorf("--user and --org are mutually exclusive") } var page *api.RepoPage switch { case repoListOrg != "": page, err = client.ListOrgRepos(cmd.Context(), repoListOrg, pageFlag, perPageFlag) case repoListUser != "": page, err = client.ListUserRepos(cmd.Context(), repoListUser, pageFlag, perPageFlag) default: // Default to the authenticated user's repos. u, uerr := client.GetUser(cmd.Context()) if uerr != nil { return uerr } page, err = client.ListUserRepos(cmd.Context(), u.Handle, pageFlag, perPageFlag) } if err != nil { return err } if flagJSON { return printJSON(cmd.OutOrStdout(), page) } if len(page.Items) == 0 { fmt.Fprintln(cmd.OutOrStdout(), "No repositories found.") return nil } tw := newTabw(cmd.OutOrStdout()) fmt.Fprintln(tw, "NAME\tVISIBILITY\tDESCRIPTION") for _, r := range page.Items { fmt.Fprintf(tw, "%s\t%s\t%s\n", r.FullName, r.Visibility, dash(r.Description)) } tw.Flush() printPageFooter(cmd, page.Page) return nil } func runRepoView(cmd *cobra.Command, args []string) error { client, err := newClient() if err != nil { return err } owner, repo, err := parseOwnerRepo(args[0]) if err != nil { return err } r, err := client.GetRepo(cmd.Context(), owner, repo) if err != nil { return err } if flagJSON { return printJSON(cmd.OutOrStdout(), r) } out := cmd.OutOrStdout() fmt.Fprintf(out, "%s\n", r.FullName) fmt.Fprintf(out, "Visibility: %s\n", r.Visibility) fmt.Fprintf(out, "Default branch: %s\n", dash(r.DefaultBranch)) fmt.Fprintf(out, "Description: %s\n", dash(r.Description)) if r.Fork { fmt.Fprintf(out, "Fork of: %s/%s\n", r.ForkedFromOwner, r.ForkedFromName) } fmt.Fprintf(out, "Created: %s\n", humanTime(r.CreatedAt)) return nil } func runRepoEdit(cmd *cobra.Command, args []string) error { client, err := newClient() if err != nil { return err } owner, repo, err := parseOwnerRepo(args[0]) if err != nil { return err } var in api.RepoUpdate if cmd.Flags().Changed("visibility") { in.Visibility = &repoEditVis } if cmd.Flags().Changed("description") { in.Description = &repoEditDesc } if cmd.Flags().Changed("default-branch") { in.DefaultBranch = &repoEditBranch } if in.Visibility == nil && in.Description == nil && in.DefaultBranch == nil { return fmt.Errorf("nothing to edit: pass --visibility, --description, or --default-branch") } r, err := client.UpdateRepo(cmd.Context(), owner, repo, in) if err != nil { return err } if flagJSON { return printJSON(cmd.OutOrStdout(), r) } fmt.Fprintf(cmd.OutOrStdout(), "Updated %s\n", r.FullName) return nil } func runRepoDelete(cmd *cobra.Command, args []string) error { client, err := newClient() if err != nil { return err } owner, repo, err := parseOwnerRepo(args[0]) if err != nil { return err } if !repoDeleteYes { ans := prompt(cmd, fmt.Sprintf("Delete %s/%s and its storage? This cannot be undone. Type the repo name to confirm: ", owner, repo)) if ans != repo { return fmt.Errorf("confirmation did not match; aborted") } } if err := client.DeleteRepo(cmd.Context(), owner, repo); err != nil { return err } fmt.Fprintf(cmd.OutOrStdout(), "Deleted %s/%s\n", owner, repo) return nil } func runRepoClone(cmd *cobra.Command, args []string) error { cfg, err := loadConfig() if err != nil { return err } owner, repo, err := parseOwnerRepo(args[0]) if err != nil { return err } host := hostFor(cfg) cloneURL := fmt.Sprintf("%s/%s/%s.git", host, owner, repo) gitArgs := []string{"clone", cloneURL} gitArgs = append(gitArgs, args[1:]...) fmt.Fprintf(cmd.OutOrStdout(), "Cloning %s…\n", cloneURL) g := exec.Command("git", gitArgs...) g.Stdout = cmd.OutOrStdout() g.Stderr = cmd.ErrOrStderr() g.Stdin = cmd.InOrStdin() return g.Run() } func runRepoFiles(cmd *cobra.Command, args []string) error { client, err := newClient() if err != nil { return err } owner, repo, err := parseOwnerRepo(args[0]) if err != nil { return err } path := "" if len(args) == 2 { path = args[1] } ref := repoContentsRef if ref == "" { ref, err = defaultBranch(cmd, client, owner, repo) if err != nil { return err } } c, err := client.GetContents(cmd.Context(), owner, repo, ref, path) if err != nil { return err } if flagJSON { return printJSON(cmd.OutOrStdout(), c) } if c.Type == "file" { fmt.Fprintf(cmd.OutOrStdout(), "%s is a file (%d bytes); use `rickub repo cat`.\n", c.Path, c.File.Size) return nil } if len(c.Entries) == 0 { fmt.Fprintln(cmd.OutOrStdout(), "(empty)") return nil } tw := newTabw(cmd.OutOrStdout()) fmt.Fprintln(tw, "TYPE\tNAME\tSIZE") for _, e := range c.Entries { kind := "dir" size := "-" if e.Type == "blob" { kind = "file" size = fmt.Sprintf("%d", e.Size) } fmt.Fprintf(tw, "%s\t%s\t%s\n", kind, e.Name, size) } tw.Flush() return nil } func runRepoCat(cmd *cobra.Command, args []string) error { client, err := newClient() if err != nil { return err } owner, repo, err := parseOwnerRepo(args[0]) if err != nil { return err } ref := repoContentsRef if ref == "" { ref, err = defaultBranch(cmd, client, owner, repo) if err != nil { return err } } c, err := client.GetContents(cmd.Context(), owner, repo, ref, args[1]) if err != nil { return err } if c.Type != "file" || c.File == nil { return fmt.Errorf("%s is not a file", args[1]) } if flagJSON { return printJSON(cmd.OutOrStdout(), c) } out := cmd.OutOrStdout() if c.File.IsBinary { raw, derr := base64.StdEncoding.DecodeString(c.File.Content) if derr != nil { return fmt.Errorf("decode binary content: %w", derr) } _, err = out.Write(raw) return err } _, err = io.WriteString(out, c.File.Content) if err == nil && !strings.HasSuffix(c.File.Content, "\n") { fmt.Fprintln(out) } if c.File.Truncated { fmt.Fprintln(cmd.ErrOrStderr(), "(file truncated by the server)") } return err } func runRepoCommits(cmd *cobra.Command, args []string) error { client, err := newClient() if err != nil { return err } owner, repo, err := parseOwnerRepo(args[0]) if err != nil { return err } ref := "" if len(args) == 2 { ref = args[1] } if ref == "" { ref, err = defaultBranch(cmd, client, owner, repo) if err != nil { return err } } page, err := client.GetCommits(cmd.Context(), owner, repo, ref, pageFlag, perPageFlag) if err != nil { return err } if flagJSON { return printJSON(cmd.OutOrStdout(), page) } if len(page.Items) == 0 { fmt.Fprintln(cmd.OutOrStdout(), "No commits.") return nil } tw := newTabw(cmd.OutOrStdout()) fmt.Fprintln(tw, "SHA\tAUTHOR\tDATE\tSUBJECT") for _, c := range page.Items { fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", c.Short, dash(c.Author), dash(c.Date), c.Subject) } tw.Flush() printPageFooter(cmd, page.Page) return nil } func runRepoCompare(cmd *cobra.Command, args []string) error { client, err := newClient() if err != nil { return err } owner, repo, err := parseOwnerRepo(args[0]) if err != nil { return err } cmp, err := client.Compare(cmd.Context(), owner, repo, args[1]) if err != nil { return err } if flagJSON { return printJSON(cmd.OutOrStdout(), cmp) } out := cmd.OutOrStdout() fmt.Fprintf(out, "merge-base: %s\n", cmp.MergeBase) fmt.Fprintf(out, "ahead by %d, behind by %d\n\n", cmp.AheadBy, cmp.BehindBy) if len(cmp.Commits) > 0 { tw := newTabw(out) fmt.Fprintln(tw, "SHA\tAUTHOR\tSUBJECT") for _, c := range cmp.Commits { fmt.Fprintf(tw, "%s\t%s\t%s\n", c.Short, dash(c.Author), c.Subject) } tw.Flush() } if len(cmp.Files) > 0 { fmt.Fprintf(out, "\n%d file(s) changed:\n", len(cmp.Files)) for _, f := range cmp.Files { fmt.Fprintf(out, " %s +%d -%d (%s)\n", f.Path, f.Additions, f.Deletions, f.Status) } } return nil } // defaultBranch resolves the repo's default branch via the refs endpoint, // falling back to the repo record. func defaultBranch(cmd *cobra.Command, client *api.Client, owner, repo string) (string, error) { refs, err := client.GetRefs(cmd.Context(), owner, repo) if err == nil && refs.DefaultBranch != "" { return refs.DefaultBranch, nil } r, rerr := client.GetRepo(cmd.Context(), owner, repo) if rerr != nil { if err != nil { return "", err } return "", rerr } if r.DefaultBranch == "" { return "", fmt.Errorf("could not determine default branch; pass --ref") } return r.DefaultBranch, nil } // hostFor returns the effective host from flags/env/config. func hostFor(cfg *config.Config) string { return config.ResolveHost(flagHost, cfg) }