internal/control/wiki.go
204 lines · 5743 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.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}