internal/control/wiki.go

204 lines · 5743 bytes

8 symbols in this file
  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		Examples: []string{"wiki list krz/gitbay"},
 22		ReadOnly: true,
 23		Run:      runWikiList,
 24	})
 25	register(Command{
 26		Path:     []string{"wiki", "show"},
 27		Summary:  "print a wiki page",
 28		Usage:    "wiki show <owner/name> [<page>]",
 29		Examples: []string{"wiki show krz/gitbay Quickstart"},
 30		ReadOnly: true,
 31		Run:      runWikiShow,
 32	})
 33}
 34
 35// Wiki pages are files under .gitbay/wiki on the repository's default
 36// branch, alongside ci.yml and CODEOWNERS. They have no store row of
 37// their own — access derives from the parent, exactly as it does for any
 38// other path in the repository.
 39//
 40// Writing is a push, or repo commit-file, like any other file in the
 41// repository. There is no wiki-specific write command.
 42
 43// wikiExts are the page formats the web renders, in resolution order.
 44var wikiExts = []string{".md", ".org", ".markdown"}
 45
 46// wikiTreePath is where wiki pages live in a repository's tree.
 47const wikiTreePath = ".gitbay/wiki"
 48
 49// wikiDir resolves the parent, checks read access, and returns its
 50// directory and default branch. A parent you cannot read has no wiki you
 51// can read.
 52func wikiDir(c *Ctx, spec string) (repo store.Repo, dir, branch string, code int) {
 53	parent, code := resolveRepo(c, spec, policy.CanRead)
 54	if code >= 0 {
 55		return store.Repo{}, "", "", code
 56	}
 57	return parent, RepoDir(c.Cfg.Server.Root, parent.OwnerName, parent.Name), parent.DefaultBranch, -1
 58}
 59
 60// wikiPages lists the page names under .gitbay/wiki, without extensions.
 61// A page in a subfolder is named by its path: Architecture/Identity.
 62func wikiPages(dir, branch string) []string {
 63	entries, err := gitutil.ListTreeRecursive(dir, branch, wikiTreePath)
 64	if err != nil {
 65		return nil // no .gitbay/wiki tree on this branch
 66	}
 67	var pages []string
 68	for _, e := range entries {
 69		if e.Type != "blob" {
 70			continue
 71		}
 72		ext := strings.ToLower(path.Ext(e.Name))
 73		for _, want := range wikiExts {
 74			if ext == want {
 75				pages = append(pages, strings.TrimSuffix(e.Name, path.Ext(e.Name)))
 76				break
 77			}
 78		}
 79	}
 80	return pages
 81}
 82
 83func runWikiList(c *Ctx, args []string) int {
 84	if len(args) != 1 {
 85		return c.usage()
 86	}
 87	repo, dir, branch, code := wikiDir(c, args[0])
 88	if code >= 0 {
 89		return code
 90	}
 91	pages := wikiPages(dir, branch)
 92	type out struct {
 93		Path  string   `json:"path"`
 94		Home  string   `json:"home,omitempty"`
 95		Pages []string `json:"pages"`
 96	}
 97	d := out{Path: repo.Path(), Home: wikiHome(pages), Pages: pages}
 98	if d.Pages == nil {
 99		d.Pages = []string{}
100	}
101	return c.emitView(d, func(w io.Writer) {
102		tb := c.table(w, "PAGE")
103		for _, p := range d.Pages {
104			tb.row(cRef(p))
105		}
106		tb.flush()
107	}, func() screen {
108		rows := make([]row, len(d.Pages))
109		for i, pg := range d.Pages {
110			home := ""
111			if pg == d.Home {
112				home = "home"
113			}
114			rows[i] = rowOf(cLink(pg, c.siteURL(repo.Path(), "wiki", pg)), cMeta(home))
115		}
116		s := listScreen("Wiki", rows)
117		if d.Home != "" {
118			s.actions = []action{{"Read", []string{"wiki", "show", repo.Path(), d.Home}}}
119		}
120		return s
121	})
122}
123
124// wikiHome picks the landing page the way the web does: a conventional
125// name if one exists, otherwise the first page.
126func wikiHome(pages []string) string {
127	for _, home := range []string{"Home", "home", "README", "index"} {
128		for _, p := range pages {
129			if p == home {
130				return home
131			}
132		}
133	}
134	if len(pages) > 0 {
135		return pages[0]
136	}
137	return ""
138}
139
140func runWikiShow(c *Ctx, args []string) int {
141	if len(args) < 1 || len(args) > 2 {
142		return c.usage()
143	}
144	repo, dir, branch, code := wikiDir(c, args[0])
145	if code >= 0 {
146		return code
147	}
148	pages := wikiPages(dir, branch)
149	page := wikiHome(pages)
150	if len(args) == 2 {
151		page = strings.TrimSuffix(args[1], path.Ext(args[1]))
152	}
153	if page == "" {
154		return c.fail(protocol.ExitNotFound, "%s has no wiki pages", repo.Path())
155	}
156	// A page name is a file name, so it must not climb out of the repo.
157	if cleaned, ok := cleanRepoPath(page); !ok || cleaned != page {
158		return c.fail(protocol.ExitUsage, "page must be a name inside the wiki")
159	}
160
161	for _, ext := range wikiExts {
162		raw, err := gitutil.ReadBlob(dir, branch, wikiTreePath+"/"+page+ext, c.Cfg.Limits.MaxBlobBytes)
163		if err != nil {
164			continue
165		}
166		binary := gitutil.IsBinary(raw)
167		type out struct {
168			Path    string `json:"path"`
169			Page    string `json:"page"`
170			File    string `json:"file"`
171			Size    int    `json:"size"`
172			Binary  bool   `json:"binary,omitempty"`
173			Content string `json:"content,omitempty"`
174			Base64  string `json:"base64,omitempty"`
175		}
176		d := out{Path: repo.Path(), Page: page, File: page + ext,
177			Size: len(raw), Binary: binary}
178		if binary {
179			d.Base64 = base64.StdEncoding.EncodeToString(raw)
180		} else {
181			d.Content = string(raw)
182		}
183		format := "md"
184		if ext == ".org" {
185			format = "org"
186		}
187		return c.emitView(d, func(w io.Writer) {
188			// Plain: the page source verbatim, same as any other piped
189			// file read.
190			fmt.Fprint(w, d.Content)
191		}, func() screen {
192			s := screen{fields: []field{
193				{"Page", []cell{cLink(page, c.siteURL(repo.Path(), "wiki", page)), cMeta(repo.Path(), d.File)}},
194			}, actions: []action{{"Read", []string{"wiki", "list", repo.Path()}}}}
195			if binary {
196				s.fields = append(s.fields, field{"Binary", []cell{cSize(int64(d.Size))}})
197			} else {
198				s.body, s.format = d.Content, format
199			}
200			return s
201		})
202	}
203	return c.fail(protocol.ExitNotFound, "no wiki page %q in %s", page, repo.Path())
204}