internal/control/explore.go

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