internal/control/explore.go
124 lines · 3871 bytes
1package control
2
3import (
4 "io"
5 "strings"
6
7 "gitbay.org/gitbay/internal/gitutil"
8 "gitbay.org/gitbay/internal/policy"
9 "gitbay.org/gitbay/internal/protocol"
10)
11
12func init() {
13 register(Command{
14 Path: []string{"explore"},
15 Summary: "list public repositories",
16 Usage: "explore [--limit <n>] [--cursor <c>]",
17 Flags: []Flag{
18 {"--limit", "<n>", "rows per page", ""},
19 {"--cursor", "<c>", "continue from the previous page", ""},
20 },
21 Examples: []string{"explore --limit 20"},
22 ReadOnly: true,
23 Run: runExplore,
24 })
25 register(Command{
26 Path: []string{"repo", "download"},
27 // Not "repo archive": that name is taken by the read-only flag,
28 // and renaming it would break every script that sets it.
29 Summary: "write a tar.gz of a ref to stdout",
30 Usage: "repo download <owner/name> [--ref <r>] > repo.tar.gz",
31 Flags: []Flag{
32 {"--ref", "<r>", "branch, tag or commit to archive", ""},
33 },
34 Examples: []string{"repo download krz/gitbay --ref main > gitbay.tar.gz"},
35 ReadOnly: true,
36 Run: runRepoDownload,
37 })
38}
39
40// runExplore is the public listing the web serves at /explore. Without a
41// command there was no way to browse what an instance hosts without
42// already knowing a name to search for.
43func runExplore(c *Ctx, args []string) int {
44 rest, p, code := parsePageFlags(c, args, "explore", false)
45 if code >= 0 {
46 return code
47 }
48 if len(rest) != 0 {
49 return c.usage()
50 }
51 repos, err := c.Store.ListPublicRepos()
52 if err != nil {
53 return c.fail(protocol.ExitFailure, "%v", err)
54 }
55 // Public means public, but an archived repo is still worth marking,
56 // and the cursor is the path since the listing is ordered by it.
57 type out struct {
58 Path string `json:"path"`
59 Description string `json:"description,omitempty"`
60 Archived bool `json:"archived,omitempty"`
61 Topics []string `json:"topics,omitempty"`
62 }
63 var ds []out
64 for _, repo := range repos {
65 // A dot-repo is infrastructure, not a project: .gitbay holds an
66 // owner's profile content and has nothing to explore.
67 if strings.HasPrefix(repo.Name, ".") {
68 continue
69 }
70 if p.key != "" && repo.Path() <= p.key {
71 continue
72 }
73 topics, _ := c.Store.ListTopics(repo.ID)
74 ds = append(ds, out{
75 Path: repo.Path(),
76 Description: gitutil.ReadDescription(RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name)),
77 Archived: repo.Settings.Archived,
78 Topics: topics,
79 })
80 if p.limit > 0 && len(ds) > p.limit {
81 break
82 }
83 }
84 ds, next := trimPage(p, ds, "explore", func(o out) string { return o.Path })
85 return c.emitPage(p, ds, next, func(w io.Writer) {
86 tb := c.table(w, "PATH", "DESCRIPTION")
87 for _, d := range ds {
88 tb.row(cRef(d.Path), cFlex(d.Description))
89 }
90 tb.flush()
91 })
92}
93
94// runRepoDownload writes a gzipped tarball of a ref to stdout, the way
95// release asset get writes an asset. The web's /archive route is the
96// same bytes with a Content-Disposition on them.
97func runRepoDownload(c *Ctx, args []string) int {
98 f, err := c.parseArgs(args, flagSpec{Values: []string{"--ref"}, MaxPos: -1, Usage: c.Cmd.Usage})
99 if err != nil {
100 return c.fail(protocol.ExitUsage, "%v", err)
101 }
102 rest, ref := f.Pos, f.Value("--ref")
103 if len(rest) != 1 {
104 return c.usage()
105 }
106 repo, code := resolveRepo(c, rest[0], policy.CanRead)
107 if code >= 0 {
108 return code
109 }
110 dir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name)
111 if ref == "" {
112 ref = repo.DefaultBranch
113 }
114 if _, err := gitutil.ResolveRef(dir, ref); err != nil {
115 return c.fail(protocol.ExitNotFound, "no ref %q in %s", ref, repo.Path())
116 }
117 // The prefix git puts on every path inside the archive, so unpacking
118 // lands in a named directory rather than the current one.
119 prefix := repo.Name + "-" + ref
120 if err := gitutil.Archive(dir, ref, prefix, c.Stdout); err != nil {
121 return c.fail(protocol.ExitFailure, "%v", err)
122 }
123 return protocol.ExitOK
124}