internal/control/wiki.go

3c0c20da787d91bcad19aec3b5e1b5c4ac5a4745
gitbay/internal/control/wiki.go history · blame · raw

169 lines · 4717 bytes

  1package control
  2
  3import (
  4	"encoding/base64"
  5	"fmt"
  6	"io"
  7	"path"
  8	"strings"
  9
 10	"gitbay.org/gitbay/internal/gitutil"
 11	"gitbay.org/gitbay/internal/policy"
 12	"gitbay.org/gitbay/internal/protocol"
 13	"gitbay.org/gitbay/internal/store"
 14)
 15
 16func init() {
 17	register(Command{
 18		Path:     []string{"wiki", "list"},
 19		Summary:  "list a repository's wiki pages",
 20		Usage:    "wiki list <owner/name>",
 21		ReadOnly: true,
 22		Run:      runWikiList,
 23	})
 24	register(Command{
 25		Path:     []string{"wiki", "show"},
 26		Summary:  "print a wiki page",
 27		Usage:    "wiki show <owner/name> [<page>]",
 28		ReadOnly: true,
 29		Run:      runWikiShow,
 30	})
 31}
 32
 33// Wiki pages are files under .gitbay/wiki on the repository's default
 34// branch, alongside ci.yml and CODEOWNERS. They have no store row of
 35// their own — access derives from the parent, exactly as it does for any
 36// other path in the repository.
 37//
 38// Writing is a push, or repo commit-file, like any other file in the
 39// repository. There is no wiki-specific write command.
 40
 41// wikiExts are the page formats the web renders, in resolution order.
 42var wikiExts = []string{".md", ".org", ".markdown"}
 43
 44// wikiTreePath is where wiki pages live in a repository's tree.
 45const wikiTreePath = ".gitbay/wiki"
 46
 47// wikiDir resolves the parent, checks read access, and returns its
 48// directory and default branch. A parent you cannot read has no wiki you
 49// can read.
 50func wikiDir(c *Ctx, spec string) (repo store.Repo, dir, branch string, code int) {
 51	parent, code := resolveRepo(c, spec, policy.CanRead)
 52	if code >= 0 {
 53		return store.Repo{}, "", "", code
 54	}
 55	return parent, RepoDir(c.Cfg.Server.Root, parent.OwnerName, parent.Name), parent.DefaultBranch, -1
 56}
 57
 58// wikiPages lists the page names under .gitbay/wiki, without extensions.
 59func wikiPages(dir, branch string) []string {
 60	entries, err := gitutil.ListTree(dir, branch, wikiTreePath)
 61	if err != nil {
 62		return nil // no .gitbay/wiki tree on this branch
 63	}
 64	var pages []string
 65	for _, e := range entries {
 66		if e.Type != "blob" {
 67			continue
 68		}
 69		ext := strings.ToLower(path.Ext(e.Name))
 70		for _, want := range wikiExts {
 71			if ext == want {
 72				pages = append(pages, strings.TrimSuffix(e.Name, path.Ext(e.Name)))
 73				break
 74			}
 75		}
 76	}
 77	return pages
 78}
 79
 80func runWikiList(c *Ctx, args []string) int {
 81	if len(args) != 1 {
 82		return c.fail(protocol.ExitUsage, "usage: wiki list <owner/name>")
 83	}
 84	repo, dir, branch, code := wikiDir(c, args[0])
 85	if code >= 0 {
 86		return code
 87	}
 88	pages := wikiPages(dir, branch)
 89	type out struct {
 90		Path  string   `json:"path"`
 91		Home  string   `json:"home,omitempty"`
 92		Pages []string `json:"pages"`
 93	}
 94	d := out{Path: repo.Path(), Home: wikiHome(pages), Pages: pages}
 95	if d.Pages == nil {
 96		d.Pages = []string{}
 97	}
 98	return c.emit(d, func(w io.Writer) {
 99		for _, p := range d.Pages {
100			fmt.Fprintln(w, p)
101		}
102	})
103}
104
105// wikiHome picks the landing page the way the web does: a conventional
106// name if one exists, otherwise the first page.
107func wikiHome(pages []string) string {
108	for _, home := range []string{"Home", "home", "README", "index"} {
109		for _, p := range pages {
110			if p == home {
111				return home
112			}
113		}
114	}
115	if len(pages) > 0 {
116		return pages[0]
117	}
118	return ""
119}
120
121func runWikiShow(c *Ctx, args []string) int {
122	if len(args) < 1 || len(args) > 2 {
123		return c.fail(protocol.ExitUsage, "usage: wiki show <owner/name> [<page>]")
124	}
125	repo, dir, branch, code := wikiDir(c, args[0])
126	if code >= 0 {
127		return code
128	}
129	pages := wikiPages(dir, branch)
130	page := wikiHome(pages)
131	if len(args) == 2 {
132		page = strings.TrimSuffix(args[1], path.Ext(args[1]))
133	}
134	if page == "" {
135		return c.fail(protocol.ExitNotFound, "%s has no wiki pages", repo.Path())
136	}
137	// A page name is a file name, so it must not climb out of the repo.
138	if cleaned, ok := cleanRepoPath(page); !ok || cleaned != page {
139		return c.fail(protocol.ExitUsage, "page must be a name inside the wiki")
140	}
141
142	for _, ext := range wikiExts {
143		raw, err := gitutil.ReadBlob(dir, branch, wikiTreePath+"/"+page+ext, c.Cfg.Limits.MaxBlobBytes)
144		if err != nil {
145			continue
146		}
147		binary := gitutil.IsBinary(raw)
148		type out struct {
149			Path    string `json:"path"`
150			Page    string `json:"page"`
151			File    string `json:"file"`
152			Size    int    `json:"size"`
153			Binary  bool   `json:"binary,omitempty"`
154			Content string `json:"content,omitempty"`
155			Base64  string `json:"base64,omitempty"`
156		}
157		d := out{Path: repo.Path(), Page: page, File: page + ext,
158			Size: len(raw), Binary: binary}
159		if binary {
160			d.Base64 = base64.StdEncoding.EncodeToString(raw)
161		} else {
162			d.Content = string(raw)
163		}
164		return c.emit(d, func(w io.Writer) {
165			fmt.Fprint(w, d.Content)
166		})
167	}
168	return c.fail(protocol.ExitNotFound, "no wiki page %q in %s", page, repo.Path())
169}