internal/control/explore.go

144 lines · 4597 bytes

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