rickub/clipublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/rickub/cli.git
git clone ssh://git@rickub.com/rickub/cli.git
repo.go · 519 lines · 13.4 KBGo Blame HistoryRaw
Initial import of the rickub CLI as a standalone public project 1a1d430 Olivier Girardot 9h ago1package cmd
2
3import (
4 "encoding/base64"
5 "fmt"
6 "io"
7 "os/exec"
8 "strings"
9
10 "rickub.com/rickub/cli/internal/api"
11 "rickub.com/rickub/cli/internal/config"
12
13 "github.com/spf13/cobra"
14)
15
16var (
17 repoListUser string
18 repoListOrg string
19 repoCreateOrg string
20 repoCreatePriv bool
21 repoCreatePub bool
22 repoCreateDesc string
23 repoEditVis string
24 repoEditDesc string
25 repoEditBranch string
26 repoDeleteYes bool
27 repoContentsRef string
28 pageFlag int
29 perPageFlag int
30)
31
32func init() {
33 repoCmd := &cobra.Command{
34 Use: "repo",
35 Aliases: []string{"repos"},
36 Short: "Manage repositories",
37 }
38
39 // create
40 createCmd := &cobra.Command{
41 Use: "create <name>",
42 Short: "Create a repository",
43 Long: `Create a repository owned by you, or by an org via --org.
44
45By default repositories are private; pass --public to create a public one.`,
46 Args: cobra.ExactArgs(1),
47 RunE: runRepoCreate,
48 }
49 createCmd.Flags().StringVar(&repoCreateOrg, "org", "", "create under this org (default: your account)")
50 createCmd.Flags().BoolVar(&repoCreatePub, "public", false, "make the repository public")
51 createCmd.Flags().BoolVar(&repoCreatePriv, "private", false, "make the repository private (default)")
52 createCmd.Flags().StringVarP(&repoCreateDesc, "description", "d", "", "repository description")
53
54 // list
55 listCmd := &cobra.Command{
56 Use: "list",
57 Short: "List repositories for a user or org",
58 Args: cobra.NoArgs,
59 RunE: runRepoList,
60 }
61 listCmd.Flags().StringVar(&repoListUser, "user", "", "list this user's repositories")
62 listCmd.Flags().StringVar(&repoListOrg, "org", "", "list this org's repositories")
63 addPaging(listCmd)
64
65 // view
66 viewCmd := &cobra.Command{
67 Use: "view <owner/repo>",
68 Short: "Show a repository",
69 Args: cobra.ExactArgs(1),
70 RunE: runRepoView,
71 }
72
73 // edit
74 editCmd := &cobra.Command{
75 Use: "edit <owner/repo>",
76 Short: "Edit visibility, description, or default branch",
77 Args: cobra.ExactArgs(1),
78 RunE: runRepoEdit,
79 }
80 editCmd.Flags().StringVar(&repoEditVis, "visibility", "", "public | private")
81 editCmd.Flags().StringVarP(&repoEditDesc, "description", "d", "", "new description")
82 editCmd.Flags().StringVar(&repoEditBranch, "default-branch", "", "new default branch")
83
84 // delete
85 deleteCmd := &cobra.Command{
86 Use: "delete <owner/repo>",
87 Short: "Delete a repository and all of its contents",
88 Args: cobra.ExactArgs(1),
89 RunE: runRepoDelete,
90 }
91 deleteCmd.Flags().BoolVar(&repoDeleteYes, "yes", false, "skip the confirmation prompt")
92
93 // clone
94 cloneCmd := &cobra.Command{
95 Use: "clone <owner/repo> [dir] [-- git-args…]",
96 Short: "Clone a repository with git",
97 Long: `Clone a repository by shelling out to git.
98
99The clone URL is derived from the configured host as <host>/<owner>/<repo>.git.
100Extra arguments after -- are passed through to git clone.`,
101 Args: cobra.MinimumNArgs(1),
102 RunE: runRepoClone,
103 }
104
105 // files (list dir)
106 filesCmd := &cobra.Command{
107 Use: "files <owner/repo> [path]",
108 Short: "List a directory in a repository",
109 Args: cobra.RangeArgs(1, 2),
110 RunE: runRepoFiles,
111 }
112 filesCmd.Flags().StringVar(&repoContentsRef, "ref", "", "branch, tag, or SHA (default: default branch)")
113
114 // cat (file content)
115 catCmd := &cobra.Command{
116 Use: "cat <owner/repo> <path>",
117 Short: "Print a file's contents",
118 Args: cobra.ExactArgs(2),
119 RunE: runRepoCat,
120 }
121 catCmd.Flags().StringVar(&repoContentsRef, "ref", "", "branch, tag, or SHA (default: default branch)")
122
123 // commits
124 commitsCmd := &cobra.Command{
125 Use: "commits <owner/repo> [ref]",
126 Short: "List commit history reachable from a ref",
127 Args: cobra.RangeArgs(1, 2),
128 RunE: runRepoCommits,
129 }
130 addPaging(commitsCmd)
131
132 // compare
133 compareCmd := &cobra.Command{
134 Use: "compare <owner/repo> <base...head>",
135 Short: "Compare two refs (base...head)",
136 Args: cobra.ExactArgs(2),
137 RunE: runRepoCompare,
138 }
139
140 repoCmd.AddCommand(createCmd, listCmd, viewCmd, editCmd, deleteCmd, cloneCmd, filesCmd, catCmd, commitsCmd, compareCmd, collaboratorCmd())
141 rootCmd.AddCommand(repoCmd)
142}
143
144func addPaging(c *cobra.Command) {
145 c.Flags().IntVar(&pageFlag, "page", 0, "page number (1-based)")
146 c.Flags().IntVar(&perPageFlag, "per-page", 0, "results per page (max 100)")
147}
148
149func runRepoCreate(cmd *cobra.Command, args []string) error {
150 client, err := newClient()
151 if err != nil {
152 return err
153 }
154 if repoCreatePub && repoCreatePriv {
155 return fmt.Errorf("--public and --private are mutually exclusive")
156 }
157 vis := ""
158 if repoCreatePub {
159 vis = "public"
160 } else if repoCreatePriv {
161 vis = "private"
162 }
163 r, err := client.CreateRepo(cmd.Context(), api.RepoCreate{
164 Owner: repoCreateOrg,
165 Name: args[0],
166 Visibility: vis,
167 Description: repoCreateDesc,
168 })
169 if err != nil {
170 return err
171 }
172 if flagJSON {
173 return printJSON(cmd.OutOrStdout(), r)
174 }
175 fmt.Fprintf(cmd.OutOrStdout(), "Created %s (%s)\n", r.FullName, r.Visibility)
176 return nil
177}
178
179func runRepoList(cmd *cobra.Command, _ []string) error {
180 client, err := newClient()
181 if err != nil {
182 return err
183 }
184 if repoListUser != "" && repoListOrg != "" {
185 return fmt.Errorf("--user and --org are mutually exclusive")
186 }
187 var page *api.RepoPage
188 switch {
189 case repoListOrg != "":
190 page, err = client.ListOrgRepos(cmd.Context(), repoListOrg, pageFlag, perPageFlag)
191 case repoListUser != "":
192 page, err = client.ListUserRepos(cmd.Context(), repoListUser, pageFlag, perPageFlag)
193 default:
194 // Default to the authenticated user's repos.
195 u, uerr := client.GetUser(cmd.Context())
196 if uerr != nil {
197 return uerr
198 }
199 page, err = client.ListUserRepos(cmd.Context(), u.Handle, pageFlag, perPageFlag)
200 }
201 if err != nil {
202 return err
203 }
204 if flagJSON {
205 return printJSON(cmd.OutOrStdout(), page)
206 }
207 if len(page.Items) == 0 {
208 fmt.Fprintln(cmd.OutOrStdout(), "No repositories found.")
209 return nil
210 }
211 tw := newTabw(cmd.OutOrStdout())
212 fmt.Fprintln(tw, "NAME\tVISIBILITY\tDESCRIPTION")
213 for _, r := range page.Items {
214 fmt.Fprintf(tw, "%s\t%s\t%s\n", r.FullName, r.Visibility, dash(r.Description))
215 }
216 tw.Flush()
217 printPageFooter(cmd, page.Page)
218 return nil
219}
220
221func runRepoView(cmd *cobra.Command, args []string) error {
222 client, err := newClient()
223 if err != nil {
224 return err
225 }
226 owner, repo, err := parseOwnerRepo(args[0])
227 if err != nil {
228 return err
229 }
230 r, err := client.GetRepo(cmd.Context(), owner, repo)
231 if err != nil {
232 return err
233 }
234 if flagJSON {
235 return printJSON(cmd.OutOrStdout(), r)
236 }
237 out := cmd.OutOrStdout()
238 fmt.Fprintf(out, "%s\n", r.FullName)
239 fmt.Fprintf(out, "Visibility: %s\n", r.Visibility)
240 fmt.Fprintf(out, "Default branch: %s\n", dash(r.DefaultBranch))
241 fmt.Fprintf(out, "Description: %s\n", dash(r.Description))
242 if r.Fork {
243 fmt.Fprintf(out, "Fork of: %s/%s\n", r.ForkedFromOwner, r.ForkedFromName)
244 }
245 fmt.Fprintf(out, "Created: %s\n", humanTime(r.CreatedAt))
246 return nil
247}
248
249func runRepoEdit(cmd *cobra.Command, args []string) error {
250 client, err := newClient()
251 if err != nil {
252 return err
253 }
254 owner, repo, err := parseOwnerRepo(args[0])
255 if err != nil {
256 return err
257 }
258 var in api.RepoUpdate
259 if cmd.Flags().Changed("visibility") {
260 in.Visibility = &repoEditVis
261 }
262 if cmd.Flags().Changed("description") {
263 in.Description = &repoEditDesc
264 }
265 if cmd.Flags().Changed("default-branch") {
266 in.DefaultBranch = &repoEditBranch
267 }
268 if in.Visibility == nil && in.Description == nil && in.DefaultBranch == nil {
269 return fmt.Errorf("nothing to edit: pass --visibility, --description, or --default-branch")
270 }
271 r, err := client.UpdateRepo(cmd.Context(), owner, repo, in)
272 if err != nil {
273 return err
274 }
275 if flagJSON {
276 return printJSON(cmd.OutOrStdout(), r)
277 }
278 fmt.Fprintf(cmd.OutOrStdout(), "Updated %s\n", r.FullName)
279 return nil
280}
281
282func runRepoDelete(cmd *cobra.Command, args []string) error {
283 client, err := newClient()
284 if err != nil {
285 return err
286 }
287 owner, repo, err := parseOwnerRepo(args[0])
288 if err != nil {
289 return err
290 }
291 if !repoDeleteYes {
292 ans := prompt(cmd, fmt.Sprintf("Delete %s/%s and its storage? This cannot be undone. Type the repo name to confirm: ", owner, repo))
293 if ans != repo {
294 return fmt.Errorf("confirmation did not match; aborted")
295 }
296 }
297 if err := client.DeleteRepo(cmd.Context(), owner, repo); err != nil {
298 return err
299 }
300 fmt.Fprintf(cmd.OutOrStdout(), "Deleted %s/%s\n", owner, repo)
301 return nil
302}
303
304func runRepoClone(cmd *cobra.Command, args []string) error {
305 cfg, err := loadConfig()
306 if err != nil {
307 return err
308 }
309 owner, repo, err := parseOwnerRepo(args[0])
310 if err != nil {
311 return err
312 }
313 host := hostFor(cfg)
314 cloneURL := fmt.Sprintf("%s/%s/%s.git", host, owner, repo)
315 gitArgs := []string{"clone", cloneURL}
316 gitArgs = append(gitArgs, args[1:]...)
317 fmt.Fprintf(cmd.OutOrStdout(), "Cloning %s…\n", cloneURL)
318 g := exec.Command("git", gitArgs...)
319 g.Stdout = cmd.OutOrStdout()
320 g.Stderr = cmd.ErrOrStderr()
321 g.Stdin = cmd.InOrStdin()
322 return g.Run()
323}
324
325func runRepoFiles(cmd *cobra.Command, args []string) error {
326 client, err := newClient()
327 if err != nil {
328 return err
329 }
330 owner, repo, err := parseOwnerRepo(args[0])
331 if err != nil {
332 return err
333 }
334 path := ""
335 if len(args) == 2 {
336 path = args[1]
337 }
338 ref := repoContentsRef
339 if ref == "" {
340 ref, err = defaultBranch(cmd, client, owner, repo)
341 if err != nil {
342 return err
343 }
344 }
345 c, err := client.GetContents(cmd.Context(), owner, repo, ref, path)
346 if err != nil {
347 return err
348 }
349 if flagJSON {
350 return printJSON(cmd.OutOrStdout(), c)
351 }
352 if c.Type == "file" {
353 fmt.Fprintf(cmd.OutOrStdout(), "%s is a file (%d bytes); use `rickub repo cat`.\n", c.Path, c.File.Size)
354 return nil
355 }
356 if len(c.Entries) == 0 {
357 fmt.Fprintln(cmd.OutOrStdout(), "(empty)")
358 return nil
359 }
360 tw := newTabw(cmd.OutOrStdout())
361 fmt.Fprintln(tw, "TYPE\tNAME\tSIZE")
362 for _, e := range c.Entries {
363 kind := "dir"
364 size := "-"
365 if e.Type == "blob" {
366 kind = "file"
367 size = fmt.Sprintf("%d", e.Size)
368 }
369 fmt.Fprintf(tw, "%s\t%s\t%s\n", kind, e.Name, size)
370 }
371 tw.Flush()
372 return nil
373}
374
375func runRepoCat(cmd *cobra.Command, args []string) error {
376 client, err := newClient()
377 if err != nil {
378 return err
379 }
380 owner, repo, err := parseOwnerRepo(args[0])
381 if err != nil {
382 return err
383 }
384 ref := repoContentsRef
385 if ref == "" {
386 ref, err = defaultBranch(cmd, client, owner, repo)
387 if err != nil {
388 return err
389 }
390 }
391 c, err := client.GetContents(cmd.Context(), owner, repo, ref, args[1])
392 if err != nil {
393 return err
394 }
395 if c.Type != "file" || c.File == nil {
396 return fmt.Errorf("%s is not a file", args[1])
397 }
398 if flagJSON {
399 return printJSON(cmd.OutOrStdout(), c)
400 }
401 out := cmd.OutOrStdout()
402 if c.File.IsBinary {
403 raw, derr := base64.StdEncoding.DecodeString(c.File.Content)
404 if derr != nil {
405 return fmt.Errorf("decode binary content: %w", derr)
406 }
407 _, err = out.Write(raw)
408 return err
409 }
410 _, err = io.WriteString(out, c.File.Content)
411 if err == nil && !strings.HasSuffix(c.File.Content, "\n") {
412 fmt.Fprintln(out)
413 }
414 if c.File.Truncated {
415 fmt.Fprintln(cmd.ErrOrStderr(), "(file truncated by the server)")
416 }
417 return err
418}
419
420func runRepoCommits(cmd *cobra.Command, args []string) error {
421 client, err := newClient()
422 if err != nil {
423 return err
424 }
425 owner, repo, err := parseOwnerRepo(args[0])
426 if err != nil {
427 return err
428 }
429 ref := ""
430 if len(args) == 2 {
431 ref = args[1]
432 }
433 if ref == "" {
434 ref, err = defaultBranch(cmd, client, owner, repo)
435 if err != nil {
436 return err
437 }
438 }
439 page, err := client.GetCommits(cmd.Context(), owner, repo, ref, pageFlag, perPageFlag)
440 if err != nil {
441 return err
442 }
443 if flagJSON {
444 return printJSON(cmd.OutOrStdout(), page)
445 }
446 if len(page.Items) == 0 {
447 fmt.Fprintln(cmd.OutOrStdout(), "No commits.")
448 return nil
449 }
450 tw := newTabw(cmd.OutOrStdout())
451 fmt.Fprintln(tw, "SHA\tAUTHOR\tDATE\tSUBJECT")
452 for _, c := range page.Items {
453 fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", c.Short, dash(c.Author), dash(c.Date), c.Subject)
454 }
455 tw.Flush()
456 printPageFooter(cmd, page.Page)
457 return nil
458}
459
460func runRepoCompare(cmd *cobra.Command, args []string) error {
461 client, err := newClient()
462 if err != nil {
463 return err
464 }
465 owner, repo, err := parseOwnerRepo(args[0])
466 if err != nil {
467 return err
468 }
469 cmp, err := client.Compare(cmd.Context(), owner, repo, args[1])
470 if err != nil {
471 return err
472 }
473 if flagJSON {
474 return printJSON(cmd.OutOrStdout(), cmp)
475 }
476 out := cmd.OutOrStdout()
477 fmt.Fprintf(out, "merge-base: %s\n", cmp.MergeBase)
478 fmt.Fprintf(out, "ahead by %d, behind by %d\n\n", cmp.AheadBy, cmp.BehindBy)
479 if len(cmp.Commits) > 0 {
480 tw := newTabw(out)
481 fmt.Fprintln(tw, "SHA\tAUTHOR\tSUBJECT")
482 for _, c := range cmp.Commits {
483 fmt.Fprintf(tw, "%s\t%s\t%s\n", c.Short, dash(c.Author), c.Subject)
484 }
485 tw.Flush()
486 }
487 if len(cmp.Files) > 0 {
488 fmt.Fprintf(out, "\n%d file(s) changed:\n", len(cmp.Files))
489 for _, f := range cmp.Files {
490 fmt.Fprintf(out, " %s +%d -%d (%s)\n", f.Path, f.Additions, f.Deletions, f.Status)
491 }
492 }
493 return nil
494}
495
496// defaultBranch resolves the repo's default branch via the refs endpoint,
497// falling back to the repo record.
498func defaultBranch(cmd *cobra.Command, client *api.Client, owner, repo string) (string, error) {
499 refs, err := client.GetRefs(cmd.Context(), owner, repo)
500 if err == nil && refs.DefaultBranch != "" {
501 return refs.DefaultBranch, nil
502 }
503 r, rerr := client.GetRepo(cmd.Context(), owner, repo)
504 if rerr != nil {
505 if err != nil {
506 return "", err
507 }
508 return "", rerr
509 }
510 if r.DefaultBranch == "" {
511 return "", fmt.Errorf("could not determine default branch; pass --ref")
512 }
513 return r.DefaultBranch, nil
514}
515
516// hostFor returns the effective host from flags/env/config.
517func hostFor(cfg *config.Config) string {
518 return config.ResolveHost(flagHost, cfg)
519}