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