internal/control/explore.go

ff759b53942049b2043e132f5482b442a7265b98
gitbay/internal/control/explore.go history · blame · raw

124 lines · 3871 bytes

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