internal/control/explore.go

6821a6f76082b1e10ff899ff51021b11695c4ad6
gitbay/internal/control/explore.go history · blame · raw

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}