Commit c36365c522
Verified · cmc ci/build: success ci/sonar: success ci/test: success ci/vuln: success
Layout: unified · split
.gitbay/ci.yml +6
| @@ -15,6 +15,8 @@ jobs: | |||
| 15 | steps: | 15 | steps: |
| 16 | - missing=""; for t in git git-lfs gpg; do command -v "$t" >/dev/null || missing="$missing $t"; done; test -x /usr/sbin/sshd || missing="$missing sshd"; test -z "$missing" || { echo "runner is missing:$missing"; exit 1; } | 16 | - missing=""; for t in git git-lfs gpg; do command -v "$t" >/dev/null || missing="$missing $t"; done; test -x /usr/sbin/sshd || missing="$missing sshd"; test -z "$missing" || { echo "runner is missing:$missing"; exit 1; } |
| 17 | - go test ./... -count=1 -timeout 20m | 17 | - go test ./... -count=1 -timeout 20m |
| 18 | paths-ignore: | ||
| 19 | - .gitbay/wiki/** | ||
| 18 | # Vulnerability scanning, separate from test so a newly published advisory | 20 | # Vulnerability scanning, separate from test so a newly published advisory |
| 19 | # against unchanged code does not mask a real test failure. It fails the | 21 | # against unchanged code does not mask a real test failure. It fails the |
| 20 | # build on purpose: an advisory that only lands in a report nobody reads is | 22 | # build on purpose: an advisory that only lands in a report nobody reads is |
| @@ -23,6 +25,8 @@ jobs: | |||
| 23 | vuln: | 25 | vuln: |
| 24 | steps: | 26 | steps: |
| 25 | - go run golang.org/x/vuln/cmd/govulncheck@latest ./... | 27 | - go run golang.org/x/vuln/cmd/govulncheck@latest ./... |
| 28 | paths-ignore: | ||
| 29 | - .gitbay/wiki/** | ||
| 26 | # SonarCloud static analysis. Report-only: unlike vuln this does not | 30 | # SonarCloud static analysis. Report-only: unlike vuln this does not |
| 27 | # gate, so a first scan of an existing codebase does not turn every | 31 | # gate, so a first scan of an existing codebase does not turn every |
| 28 | # build red before anyone has read what it says. Flip the trailing | 32 | # build red before anyone has read what it says. Flip the trailing |
| @@ -66,3 +70,5 @@ jobs: | |||
| 66 | fi | 70 | fi |
| 67 | SONAR_HOST_URL=https://sonarcloud.io \ | 71 | SONAR_HOST_URL=https://sonarcloud.io \ |
| 68 | "$SCANNER/bin/sonar-scanner" -Dsonar.scm.revision="${GITBAY_SHA:-}" $BRANCH_ARG || true | 72 | "$SCANNER/bin/sonar-scanner" -Dsonar.scm.revision="${GITBAY_SHA:-}" $BRANCH_ARG || true |
| 73 | paths-ignore: | ||
| 74 | - .gitbay/wiki/** | ||
README.org +1 −2
| @@ -104,8 +104,7 @@ via =gitbay man=, completions via =gitbay completion <shell>=. | |||
| 104 | 104 | ||
| 105 | * Documentation | 105 | * Documentation |
| 106 | 106 | ||
| 107 | Docs live in [[https://gitbay.org/krz/gitbay/wiki][the wiki]] — itself a git repository | 107 | Docs live in [[https://gitbay.org/krz/gitbay/wiki][the wiki]], dogfooding the |
| 108 | (=git clone ssh://git@gitbay.org/krz/gitbay.wiki.git=), dogfooding the | ||
| 109 | wiki feature: | 108 | wiki feature: |
| 110 | 109 | ||
| 111 | - [[https://gitbay.org/krz/gitbay/wiki/Users][user guide]] — accounts, keys, verified commits, repos, issues, MRs, scripting | 110 | - [[https://gitbay.org/krz/gitbay/wiki/Users][user guide]] — accounts, keys, verified commits, repos, issues, MRs, scripting |
e2e/wiki_test.go +8
| @@ -47,6 +47,7 @@ func TestWikis(t *testing.T) { | |||
| 47 | "# welcome\n\nsee [Setup](Setup.md) and \n"), 0o644) | 47 | "# welcome\n\nsee [Setup](Setup.md) and \n"), 0o644) |
| 48 | os.WriteFile(filepath.Join(dir, ".gitbay", "wiki", "Setup.org"), []byte("* setup\n\nsteps here\n"), 0o644) | 48 | os.WriteFile(filepath.Join(dir, ".gitbay", "wiki", "Setup.org"), []byte("* setup\n\nsteps here\n"), 0o644) |
| 49 | os.WriteFile(filepath.Join(dir, ".gitbay", "wiki", "shot.png"), []byte{0x89, 0x50, 0x4e, 0x47}, 0o644) | 49 | os.WriteFile(filepath.Join(dir, ".gitbay", "wiki", "shot.png"), []byte{0x89, 0x50, 0x4e, 0x47}, 0o644) |
| 50 | os.WriteFile(filepath.Join(dir, "top.txt"), []byte("not part of the wiki\n"), 0o644) | ||
| 50 | mustGit(t, dir, env, "add", ".") | 51 | mustGit(t, dir, env, "add", ".") |
| 51 | mustGit(t, dir, env, "commit", "-q", "-m", "wiki start") | 52 | mustGit(t, dir, env, "commit", "-q", "-m", "wiki start") |
| 52 | mustGit(t, dir, env, "push", "-q", inst.sshURL("alice/app"), "main") | 53 | mustGit(t, dir, env, "push", "-q", inst.sshURL("alice/app"), "main") |
| @@ -80,6 +81,13 @@ func TestWikis(t *testing.T) { | |||
| 80 | if status != 200 || !strings.HasPrefix(raw, "\x89PNG") { | 81 | if status != 200 || !strings.HasPrefix(raw, "\x89PNG") { |
| 81 | t.Fatalf("wiki raw: %d", status) | 82 | t.Fatalf("wiki raw: %d", status) |
| 82 | } | 83 | } |
| 84 | // The raw route cannot climb out of .gitbay/wiki. A literal ".." is | ||
| 85 | // caught by the mux's own path cleaning, which would make this pass | ||
| 86 | // vacuously; percent-encoding it reaches the handler with real ".." | ||
| 87 | // segments in PathValue, which is what the guard has to refuse. | ||
| 88 | if status, body := inst.get(t, "/alice/app/wiki/_raw/%2e%2e/%2e%2e/top.txt"); status == 200 { | ||
| 89 | t.Fatalf("wiki raw escaped .gitbay/wiki: %d\n%s", status, body) | ||
| 90 | } | ||
| 83 | 91 | ||
| 84 | // A wiki is readable from every surface, not just a browser: the | 92 | // A wiki is readable from every surface, not just a browser: the |
| 85 | // commands are what the web dispatches, and what the CLI and the | 93 | // commands are what the web dispatches, and what the CLI and the |
internal/httpd/wiki.go +30 −4
| @@ -17,18 +17,39 @@ import ( | |||
| 17 | // wikiTreePath is where wiki pages live in a repository's tree. | 17 | // wikiTreePath is where wiki pages live in a repository's tree. |
| 18 | const wikiTreePath = ".gitbay/wiki" | 18 | const wikiTreePath = ".gitbay/wiki" |
| 19 | 19 | ||
| 20 | // wikiExts are the page formats that count as wiki content. | ||
| 21 | var wikiExts = []string{".md", ".org", ".markdown"} | ||
| 22 | |||
| 20 | // wikiDir returns repo's directory and default branch, where wiki pages | 23 | // wikiDir returns repo's directory and default branch, where wiki pages |
| 21 | // are resolved from .gitbay/wiki. | 24 | // are resolved from .gitbay/wiki. |
| 22 | func (s *Server) wikiDir(repo store.Repo) (dir, branch string) { | 25 | func (s *Server) wikiDir(repo store.Repo) (dir, branch string) { |
| 23 | return control.RepoDir(s.cfg.Server.Root, repo.OwnerName, repo.Name), repo.DefaultBranch | 26 | return control.RepoDir(s.cfg.Server.Root, repo.OwnerName, repo.Name), repo.DefaultBranch |
| 24 | } | 27 | } |
| 25 | 28 | ||
| 26 | // hasWiki reports whether repo's default branch holds a non-empty | 29 | // hasWiki reports whether repo's default branch holds a wiki page. |
| 27 | // .gitbay/wiki tree. | ||
| 28 | func (s *Server) hasWiki(repo store.Repo) bool { | 30 | func (s *Server) hasWiki(repo store.Repo) bool { |
| 29 | dir, branch := s.wikiDir(repo) | 31 | dir, branch := s.wikiDir(repo) |
| 30 | entries, err := gitutil.ListTree(dir, branch, wikiTreePath) | 32 | entries, err := gitutil.ListTree(dir, branch, wikiTreePath) |
| 31 | return err == nil && len(entries) > 0 | 33 | if err != nil { |
| 34 | return false | ||
| 35 | } | ||
| 36 | for _, e := range entries { | ||
| 37 | if e.Type == "blob" && wikiExtMatch(e.Name) { | ||
| 38 | return true | ||
| 39 | } | ||
| 40 | } | ||
| 41 | return false | ||
| 42 | } | ||
| 43 | |||
| 44 | // wikiExtMatch reports whether name has one of the wiki page extensions. | ||
| 45 | func wikiExtMatch(name string) bool { | ||
| 46 | ext := strings.ToLower(path.Ext(name)) | ||
| 47 | for _, want := range wikiExts { | ||
| 48 | if ext == want { | ||
| 49 | return true | ||
| 50 | } | ||
| 51 | } | ||
| 52 | return false | ||
| 32 | } | 53 | } |
| 33 | 54 | ||
| 34 | // wiki renders a page from the repo's .gitbay/wiki tree. The home page is | 55 | // wiki renders a page from the repo's .gitbay/wiki tree. The home page is |
| @@ -94,7 +115,12 @@ func (s *Server) wikiRaw(w http.ResponseWriter, r *http.Request) { | |||
| 94 | return | 115 | return |
| 95 | } | 116 | } |
| 96 | dir, branch := s.wikiDir(p.Repo) | 117 | dir, branch := s.wikiDir(p.Repo) |
| 97 | data, err := gitutil.ReadBlob(dir, branch, path.Join(wikiTreePath, strings.Trim(r.PathValue("path"), "/")), s.cfg.Limits.MaxBlobBytes) | 118 | rel := path.Join(wikiTreePath, strings.Trim(r.PathValue("path"), "/")) |
| 119 | if !strings.HasPrefix(rel, wikiTreePath+"/") { | ||
| 120 | s.notFound(w, r) | ||
| 121 | return | ||
| 122 | } | ||
| 123 | data, err := gitutil.ReadBlob(dir, branch, rel, s.cfg.Limits.MaxBlobBytes) | ||
| 98 | if err != nil { | 124 | if err != nil { |
| 99 | s.notFound(w, r) | 125 | s.notFound(w, r) |
| 100 | return | 126 | return |
internal/web/templates/wiki.html +2 −2
| @@ -2,7 +2,7 @@ | |||
| 2 | {{define "content"}} | 2 | {{define "content"}} |
| 3 | <h1 class="vh">Wiki{{if .Page}}: {{.Page}}{{end}}</h1> | 3 | <h1 class="vh">Wiki{{if .Page}}: {{.Page}}{{end}}</h1> |
| 4 | {{if .Missing}}<p class="empty-note">no wiki yet — add pages under <code>.gitbay/wiki/</code> on the default branch:<br> | 4 | {{if .Missing}}<p class="empty-note">no wiki yet — add pages under <code>.gitbay/wiki/</code> on the default branch:<br> |
| 5 | push <code>.gitbay/wiki/Home.md</code> (or .org), or use the file editor.</p> | 5 | push <code>.gitbay/wiki/Home.md</code> (or .org).</p> |
| 6 | {{else}} | 6 | {{else}} |
| 7 | <div class="wikilayout"> | 7 | <div class="wikilayout"> |
| 8 | <div class="wikipage"> | 8 | <div class="wikipage"> |
| @@ -14,7 +14,7 @@ push <code>.gitbay/wiki/Home.md</code> (or .org), or use the file editor.</p> | |||
| 14 | <nav class="wikinav"> | 14 | <nav class="wikinav"> |
| 15 | <p class="meta">pages</p> | 15 | <p class="meta">pages</p> |
| 16 | <ul>{{range .Pages}}<li><a {{if eq . $.Page}}class="active" {{end}}href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/wiki/{{.}}">{{.}}</a></li>{{end}}</ul> | 16 | <ul>{{range .Pages}}<li><a {{if eq . $.Page}}class="active" {{end}}href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/wiki/{{.}}">{{.}}</a></li>{{end}}</ul> |
| 17 | <p class="meta">edit under <code>.gitbay/wiki/</code> — push, or the file editor.</p> | 17 | <p class="meta">edit under <code>.gitbay/wiki/</code> by push.</p> |
| 18 | </nav> | 18 | </nav> |
| 19 | </div> | 19 | </div> |
| 20 | {{end}} | 20 | {{end}} |