internal/control/explore.go

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

158 lines · 5012 bytes

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