internal/control/explore.go
158 lines · 5012 bytes
3 symbols in this file
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.emitPageView(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 }, func() screen {
95 rows := make([]row, len(ds))
96 for i, d := range ds {
97 archived := ""
98 if d.Archived {
99 archived = "archived"
100 }
101 rows[i] = rowOf(cLink(d.Path, c.siteURL(d.Path)), cFlex(d.Description), cMeta(strings.Join(d.Topics, ", "), archived))
102 }
103 s := listScreen("Explore", rows)
104 if len(ds) > 0 {
105 s.actions = []action{{"Read", []string{"repo", "show", ds[0].Path}}}
106 }
107 return s
108 })
109}
110
111// runRepoDownload writes a gzipped tarball of a ref to stdout, the way
112// release asset get writes an asset. The web's /archive route is the
113// same bytes with a Content-Disposition on them.
114func runRepoDownload(c *Ctx, args []string) int {
115 f, err := c.parseArgs(args, flagSpec{Values: []string{"--ref"}, MaxPos: -1, Usage: c.Cmd.Usage})
116 if err != nil {
117 return c.fail(protocol.ExitUsage, "%v", err)
118 }
119 rest, ref := f.Pos, f.Value("--ref")
120 if len(rest) != 1 {
121 return c.usage()
122 }
123 repo, code := resolveRepo(c, rest[0], policy.CanRead)
124 if code >= 0 {
125 return code
126 }
127 dir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name)
128 if ref == "" {
129 ref = repo.DefaultBranch
130 }
131 if _, err := gitutil.ResolveRef(dir, ref); err != nil {
132 return c.fail(protocol.ExitNotFound, "no ref %q in %s", ref, repo.Path())
133 }
134 // The prefix git puts on every path inside the archive, so unpacking
135 // lands in a named directory rather than the current one.
136 prefix := repo.Name + "-" + ref
137 // git archive draws on the same budget as clones and web archives,
138 // counted against the account the way upload-pack is.
139 principal := "user:" + strconv.FormatInt(c.User.ID, 10)
140 release, err := c.Packs.Acquire(c.Done, principal)
141 if err != nil {
142 transport := "ssh"
143 if c.ViaAPI {
144 transport = "api"
145 }
146 c.Packs.Refused(transport, principal, err)
147 c.Busy = true
148 if errors.Is(err, packlimit.ErrBusy) {
149 return c.fail(protocol.ExitFailure, "the server is busy: it is at its limit of concurrent clones and fetches; try again in a minute")
150 }
151 return c.fail(protocol.ExitFailure, "the server is restarting; try again in a minute")
152 }
153 defer release()
154 if err := gitutil.Archive(dir, ref, prefix, c.Stdout); err != nil {
155 return c.fail(protocol.ExitFailure, "%v", err)
156 }
157 return protocol.ExitOK
158}