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