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