internal/control/wiki.go
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}