internal/control/wiki.go
196 lines · 5290 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.
61func wikiPages(dir, branch string) []string {
62 entries, err := gitutil.ListTree(dir, branch, wikiTreePath)
63 if err != nil {
64 return nil // no .gitbay/wiki tree on this branch
65 }
66 var pages []string
67 for _, e := range entries {
68 if e.Type != "blob" {
69 continue
70 }
71 ext := strings.ToLower(path.Ext(e.Name))
72 for _, want := range wikiExts {
73 if ext == want {
74 pages = append(pages, strings.TrimSuffix(e.Name, path.Ext(e.Name)))
75 break
76 }
77 }
78 }
79 return pages
80}
81
82func runWikiList(c *Ctx, args []string) int {
83 if len(args) != 1 {
84 return c.usage()
85 }
86 repo, dir, branch, code := wikiDir(c, args[0])
87 if code >= 0 {
88 return code
89 }
90 pages := wikiPages(dir, branch)
91 type out struct {
92 Path string `json:"path"`
93 Home string `json:"home,omitempty"`
94 Pages []string `json:"pages"`
95 }
96 d := out{Path: repo.Path(), Home: wikiHome(pages), Pages: pages}
97 if d.Pages == nil {
98 d.Pages = []string{}
99 }
100 return c.emit(d, func(w io.Writer) {
101 tb := c.table(w, "PAGE")
102 for _, p := range d.Pages {
103 tb.row(cRef(p))
104 }
105 tb.flush()
106 })
107}
108
109// wikiHome picks the landing page the way the web does: a conventional
110// name if one exists, otherwise the first page.
111func wikiHome(pages []string) string {
112 for _, home := range []string{"Home", "home", "README", "index"} {
113 for _, p := range pages {
114 if p == home {
115 return home
116 }
117 }
118 }
119 if len(pages) > 0 {
120 return pages[0]
121 }
122 return ""
123}
124
125func runWikiShow(c *Ctx, args []string) int {
126 if len(args) < 1 || len(args) > 2 {
127 return c.usage()
128 }
129 repo, dir, branch, code := wikiDir(c, args[0])
130 if code >= 0 {
131 return code
132 }
133 pages := wikiPages(dir, branch)
134 page := wikiHome(pages)
135 if len(args) == 2 {
136 page = strings.TrimSuffix(args[1], path.Ext(args[1]))
137 }
138 if page == "" {
139 return c.fail(protocol.ExitNotFound, "%s has no wiki pages", repo.Path())
140 }
141 // A page name is a file name, so it must not climb out of the repo.
142 if cleaned, ok := cleanRepoPath(page); !ok || cleaned != page {
143 return c.fail(protocol.ExitUsage, "page must be a name inside the wiki")
144 }
145
146 for _, ext := range wikiExts {
147 raw, err := gitutil.ReadBlob(dir, branch, wikiTreePath+"/"+page+ext, c.Cfg.Limits.MaxBlobBytes)
148 if err != nil {
149 continue
150 }
151 binary := gitutil.IsBinary(raw)
152 type out struct {
153 Path string `json:"path"`
154 Page string `json:"page"`
155 File string `json:"file"`
156 Size int `json:"size"`
157 Binary bool `json:"binary,omitempty"`
158 Content string `json:"content,omitempty"`
159 Base64 string `json:"base64,omitempty"`
160 }
161 d := out{Path: repo.Path(), Page: page, File: page + ext,
162 Size: len(raw), Binary: binary}
163 if binary {
164 d.Base64 = base64.StdEncoding.EncodeToString(raw)
165 } else {
166 d.Content = string(raw)
167 }
168 format := "md"
169 if ext == ".org" {
170 format = "org"
171 }
172 return c.emit(d, func(w io.Writer) {
173 // Plain: the page source verbatim, same as any other piped
174 // file read. The title/fields/body layout is terminal-only.
175 if c.Term.Cols == 0 {
176 fmt.Fprint(w, d.Content)
177 return
178 }
179 v := c.view(w)
180 v.title(repo.Path(), page, "")
181 binaryNote := ""
182 if binary {
183 binaryNote = fmt.Sprintf("%d bytes, binary", d.Size)
184 }
185 v.fields(
186 "file", d.File,
187 "binary", binaryNote,
188 "url", c.siteURL(repo.Path(), "wiki", page),
189 )
190 if !binary {
191 v.body(d.Content, format)
192 }
193 })
194 }
195 return c.fail(protocol.ExitNotFound, "no wiki page %q in %s", page, repo.Path())
196}