internal/control/explore.go

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

109 lines · 3474 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	f, err := parseFlags(args, flagSpec{Values: []string{"--ref"}, MaxPos: -1, Usage: usage})
 84	if err != nil {
 85		return c.fail(protocol.ExitUsage, "%v", err)
 86	}
 87	rest, ref := f.Pos, f.Value("--ref")
 88	if len(rest) != 1 {
 89		return c.fail(protocol.ExitUsage, "usage: %s", usage)
 90	}
 91	repo, code := resolveRepo(c, rest[0], policy.CanRead)
 92	if code >= 0 {
 93		return code
 94	}
 95	dir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name)
 96	if ref == "" {
 97		ref = repo.DefaultBranch
 98	}
 99	if _, err := gitutil.ResolveRef(dir, ref); err != nil {
100		return c.fail(protocol.ExitNotFound, "no ref %q in %s", ref, repo.Path())
101	}
102	// The prefix git puts on every path inside the archive, so unpacking
103	// lands in a named directory rather than the current one.
104	prefix := repo.Name + "-" + ref
105	if err := gitutil.Archive(dir, ref, prefix, c.Stdout); err != nil {
106		return c.fail(protocol.ExitFailure, "%v", err)
107	}
108	return protocol.ExitOK
109}