internal/control/wiki.go
197 lines · 5369 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 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.emit(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 })
108}
109
110// wikiHome picks the landing page the way the web does: a conventional
111// name if one exists, otherwise the first page.
112func wikiHome(pages []string) string {
113 for _, home := range []string{"Home", "home", "README", "index"} {
114 for _, p := range pages {
115 if p == home {
116 return home
117 }
118 }
119 }
120 if len(pages) > 0 {
121 return pages[0]
122 }
123 return ""
124}
125
126func runWikiShow(c *Ctx, args []string) int {
127 if len(args) < 1 || len(args) > 2 {
128 return c.usage()
129 }
130 repo, dir, branch, code := wikiDir(c, args[0])
131 if code >= 0 {
132 return code
133 }
134 pages := wikiPages(dir, branch)
135 page := wikiHome(pages)
136 if len(args) == 2 {
137 page = strings.TrimSuffix(args[1], path.Ext(args[1]))
138 }
139 if page == "" {
140 return c.fail(protocol.ExitNotFound, "%s has no wiki pages", repo.Path())
141 }
142 // A page name is a file name, so it must not climb out of the repo.
143 if cleaned, ok := cleanRepoPath(page); !ok || cleaned != page {
144 return c.fail(protocol.ExitUsage, "page must be a name inside the wiki")
145 }
146
147 for _, ext := range wikiExts {
148 raw, err := gitutil.ReadBlob(dir, branch, wikiTreePath+"/"+page+ext, c.Cfg.Limits.MaxBlobBytes)
149 if err != nil {
150 continue
151 }
152 binary := gitutil.IsBinary(raw)
153 type out struct {
154 Path string `json:"path"`
155 Page string `json:"page"`
156 File string `json:"file"`
157 Size int `json:"size"`
158 Binary bool `json:"binary,omitempty"`
159 Content string `json:"content,omitempty"`
160 Base64 string `json:"base64,omitempty"`
161 }
162 d := out{Path: repo.Path(), Page: page, File: page + ext,
163 Size: len(raw), Binary: binary}
164 if binary {
165 d.Base64 = base64.StdEncoding.EncodeToString(raw)
166 } else {
167 d.Content = string(raw)
168 }
169 format := "md"
170 if ext == ".org" {
171 format = "org"
172 }
173 return c.emit(d, func(w io.Writer) {
174 // Plain: the page source verbatim, same as any other piped
175 // file read. The title/fields/body layout is terminal-only.
176 if c.Term.Cols == 0 {
177 fmt.Fprint(w, d.Content)
178 return
179 }
180 v := c.view(w)
181 v.title(repo.Path(), page, "")
182 binaryNote := ""
183 if binary {
184 binaryNote = fmt.Sprintf("%d bytes, binary", d.Size)
185 }
186 v.fields(
187 "file", d.File,
188 "binary", binaryNote,
189 "url", c.siteURL(repo.Path(), "wiki", page),
190 )
191 if !binary {
192 v.body(d.Content, format)
193 }
194 })
195 }
196 return c.fail(protocol.ExitNotFound, "no wiki page %q in %s", page, repo.Path())
197}