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