Commit 34a78ef570

34a78ef570fbffbfc01e61a70c85f049c100fc5f

parent: 5671f4a322

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-05 05:28 UTC

wiki: pages move into .gitbay/wiki on the default branch

The companion bare repo (<owner>/<name>.wiki.git) is gone: its SSH
route, the wikiDir helpers that resolved it, the reserved .wiki name
suffix, and its rename/delete handling. Pages now live at
.gitbay/wiki/*.{md,org,markdown} on the repository's default branch,
resolved and access-checked through the parent exactly as before.
wiki list and wiki show keep their argv, JSON fields and exit codes,
except that a repository with no .gitbay/wiki now reports an empty
list instead of erroring, since that is the common case for every
repository until the one production wiki is migrated by hand.

Writing is unchanged in kind: a push, or repo commit-file, like any
other file. No wiki-specific write command is added.

InitBare's empty-hooksPath branch was reachable only from the removed
companion init call; removed along with the comment describing it.

Closes #170

Layout: unified · split

cmd/gitbayd/backup.go +1 −1
@@ -267,7 +267,7 @@ func verifyBackup(path string) error {
267267 return fmt.Errorf("%s: %d repositories the database names are not in the archive: %s", path, len(missing), strings.Join(missing, ", "))
268268 }
269269 if extra > 0 {
270 fmt.Printf("%d repositories in the archive that the database does not name (deleted after the snapshot, or a wiki)\n", extra)
270 fmt.Printf("%d repositories in the archive that the database does not name (deleted after the snapshot)\n", extra)
271271 }
272272 return nil
273273}
e2e/readonly_test.go +1 −1
@@ -152,7 +152,7 @@ func TestReadOnlyCommandsWriteNothing(t *testing.T) {
152152 "wiki show": {"alice/app"},
153153 }
154154 // Reads whose subject legitimately does not exist in this fixture.
155 notFoundOK := map[string]bool{"wiki list": true, "wiki show": true, "repo deps status": true}
155 notFoundOK := map[string]bool{"wiki show": true, "repo deps status": true}
156156
157157 dbPath := filepath.Join(inst.root, "gitbay.db")
158158 before := dbFingerprint(t, dbPath)
e2e/wiki_test.go +55 −28
@@ -11,7 +11,8 @@ func TestWikis(t *testing.T) {
1111 inst := startInstance(t)
1212 aliceKey := inst.newKey(t, "alice")
1313 bobKey := inst.newKey(t, "bob")
14 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
14 inst.admin(t, "admin", "user", "create", "alice",
15 "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
1516 inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
1617 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
1718 t.Fatal("repo create failed")
@@ -20,8 +21,8 @@ func TestWikis(t *testing.T) {
2021 t.Fatal("private repo create failed")
2122 }
2223
23 // No wiki yet: the tab is absent, the page shows the push hint, and
24 // cloning the companion says so.
24 // No wiki yet: the tab is absent, the page shows the missing hint, and
25 // listing reports no wiki rather than erroring.
2526 _, body := inst.get(t, "/alice/app")
2627 if strings.Contains(body, ">Wiki<") {
2728 t.Fatal("wiki tab shown with no wiki")
@@ -30,26 +31,29 @@ func TestWikis(t *testing.T) {
3031 if !strings.Contains(body, "no wiki yet") {
3132 t.Fatal("missing-wiki hint absent")
3233 }
33 env := inst.gitEnv(aliceKey)
34 if out, code := gitRun(t, t.TempDir(), env, "clone", inst.sshURL("alice/app.wiki"), "w"); code == 0 || !strings.Contains(out, "no wiki yet") {
35 t.Fatalf("clone of absent wiki: %d\n%s", code, out)
34 if out, errOut, code := inst.ssh(t, aliceKey, "", "wiki", "list", "alice/app", "--json"); code != 0 {
35 t.Fatalf("wiki list on a repo without one: %s", errOut)
36 } else if !strings.Contains(out, `"pages":[]`) {
37 t.Errorf("wiki list on a repo without one returned pages: %s", out)
3638 }
3739
38 // First push creates the wiki. A reader without write cannot push it.
40 // Pages are ordinary files: pushing them creates the wiki.
41 env := inst.gitEnv(aliceKey)
3942 work := t.TempDir()
4043 mustGit(t, work, env, "init", "-q", "-b", "main", "w")
4144 dir := filepath.Join(work, "w")
42 os.WriteFile(filepath.Join(dir, "Home.md"), []byte(
45 os.MkdirAll(filepath.Join(dir, ".gitbay", "wiki"), 0o755)
46 os.WriteFile(filepath.Join(dir, ".gitbay", "wiki", "Home.md"), []byte(
4347 "# welcome\n\nsee [Setup](Setup.md) and ![shot](shot.png)\n"), 0o644)
44 os.WriteFile(filepath.Join(dir, "Setup.org"), []byte("* setup\n\nsteps here\n"), 0o644)
45 os.WriteFile(filepath.Join(dir, "shot.png"), []byte{0x89, 0x50, 0x4e, 0x47}, 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)
4650 mustGit(t, dir, env, "add", ".")
4751 mustGit(t, dir, env, "commit", "-q", "-m", "wiki start")
48 mustGit(t, dir, env, "push", "-q", inst.sshURL("alice/app.wiki"), "main")
52 mustGit(t, dir, env, "push", "-q", inst.sshURL("alice/app"), "main")
4953
5054 benv := inst.gitEnv(bobKey)
51 if out, code := gitRun(t, dir, benv, "push", inst.sshURL("alice/app.wiki"), "main"); code == 0 && !strings.Contains(out, "denied") {
52 t.Fatalf("reader pushed the wiki: %d\n%s", code, out)
55 if out, code := gitRun(t, dir, benv, "push", inst.sshURL("alice/app"), "main"); code == 0 && !strings.Contains(out, "denied") {
56 t.Fatalf("reader pushed the repository: %d\n%s", code, out)
5357 }
5458
5559 // Rendering: home resolves, tab appears, links rewrite to wiki pages
@@ -115,10 +119,12 @@ func TestWikis(t *testing.T) {
115119 if _, _, code := inst.ssh(t, aliceKey, "", "wiki", "show", "alice/app", "../../etc/passwd"); code == 0 {
116120 t.Error("wiki show escaped the repository")
117121 }
118 // A repository with no wiki says so rather than failing oddly.
119 if _, errOut, code := inst.ssh(t, aliceKey, "", "wiki", "list", "alice/secretive"); code == 0 ||
120 !strings.Contains(errOut, "no wiki") {
121 t.Errorf("wiki list on a repo without one: %d %s", code, errOut)
122 // A repository with no wiki says so rather than failing oddly, even
123 // for its owner.
124 if out, errOut, code := inst.ssh(t, aliceKey, "", "wiki", "list", "alice/secretive", "--json"); code != 0 {
125 t.Fatalf("wiki list on a repo without one: %s", errOut)
126 } else if !strings.Contains(out, `"pages":[]`) {
127 t.Errorf("wiki list on a repo without one returned pages: %s", out)
122128 }
123129 // Wiki access derives from the parent: a stranger gets nothing.
124130 if _, _, code := inst.ssh(t, bobKey, "", "wiki", "list", "alice/secretive"); code == 0 {
@@ -126,24 +132,45 @@ func TestWikis(t *testing.T) {
126132 }
127133
128134 // 404-parity: a private repo's wiki is invisible, over web and git.
129 mustGit(t, dir, env, "push", "-q", inst.sshURL("alice/secretive.wiki"), "main")
130135 if status, _ := inst.get(t, "/alice/secretive/wiki"); status != 404 {
131136 t.Fatalf("private wiki page: %d", status)
132137 }
133 if out, code := gitRun(t, t.TempDir(), benv, "clone", inst.sshURL("alice/secretive.wiki"), "x"); code == 0 || !strings.Contains(out, "not found") {
134 t.Fatalf("private wiki clone by outsider: %d\n%s", code, out)
138
139 // Pushing to <name>.wiki.git is refused now that the companion route
140 // is gone; there is no such repository.
141 if out, code := gitRun(t, t.TempDir(), env, "clone", inst.sshURL("alice/app.wiki"), "x"); code == 0 {
142 t.Fatalf("cloned a nonexistent companion: %s", out)
143 } else if !strings.Contains(out, "not found") {
144 t.Fatalf("clone of alice/app.wiki: %s", out)
135145 }
136146
137 // Repo names ending .wiki are refused (companion namespace).
138 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/notes.wiki"); code != 2 || !strings.Contains(errOut, "reserved") {
139 t.Fatalf(".wiki name allowed: %d %s", code, errOut)
147 // A repository may now be named something.wiki: the suffix is no
148 // longer reserved.
149 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/notes.wiki"); code != 0 {
150 t.Fatalf("something.wiki repo name refused: %s", errOut)
140151 }
141152
142 // Deleting the repo removes the wiki companion.
143 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "delete", "alice/secretive", "--yes"); code != 0 {
144 t.Fatal("repo delete failed")
153 // repo commit-file writes a page on a repository that permits
154 // server-authored commits: it is the command behind the web editor,
155 // and there is no wiki-specific write command.
156 if _, errOut, code := inst.ssh(t, aliceKey, "written by commit-file\n",
157 "repo", "commit-file", "alice/app", ".gitbay/wiki/Extra.md",
158 "--ref", "main", "--message", "'add a page'", "--file", "-"); code != 0 {
159 t.Fatalf("repo commit-file: %s", errOut)
160 }
161 out, _, code = inst.ssh(t, aliceKey, "", "wiki", "show", "alice/app", "Extra", "--json")
162 if code != 0 || !strings.Contains(out, "written by commit-file") {
163 t.Errorf("wiki show Extra: %s", out)
145164 }
146 if _, err := os.Stat(filepath.Join(inst.root, "repos", "alice", "secretive.wiki.git")); err == nil {
147 t.Fatal("wiki survived repo delete")
165
166 // A repository requiring verified signatures refuses repo commit-file,
167 // since the server cannot sign on the user's behalf.
168 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "settings", "require-signed", "alice/app", "on"); code != 0 {
169 t.Fatal("require-signed failed")
170 }
171 if _, errOut, code := inst.ssh(t, aliceKey, "blocked\n",
172 "repo", "commit-file", "alice/app", ".gitbay/wiki/Blocked.md",
173 "--ref", "main", "--file", "-"); code == 0 || !strings.Contains(errOut, "requires signed commits") {
174 t.Errorf("repo commit-file not refused on a signed-commits repo: %d %s", code, errOut)
148175 }
149176}
internal/control/repo.go +1 −7
@@ -392,11 +392,6 @@ func runRepoTransfer(c *Ctx, args []string) int {
392392 }
393393 return c.fail(protocol.ExitFailure, "moving repository: %v", err)
394394 }
395 // The wiki companion follows its repo.
396 oldWiki := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name+".wiki")
397 if _, err := os.Stat(oldWiki); err == nil {
398 os.Rename(oldWiki, RepoDir(c.Cfg.Server.Root, newOwner, repo.Name+".wiki"))
399 }
400395 newPath := newOwner + "/" + repo.Name
401396 return c.emit(map[string]string{"repo": newPath, "was": repo.Path()}, func(w io.Writer) {
402397 fmt.Fprintf(w, "transferred %s to %s — clone URLs now use %s\n", repo.Path(), newPath, newPath)
@@ -429,7 +424,7 @@ func runRepoDelete(c *Ctx, args []string) int {
429424}
430425
431426// deleteRepo removes a repository the caller has already been cleared to
432// delete: the database row, then the directory and its wiki companion.
427// delete: the database row, then the directory.
433428//
434429// There is deliberately no repo.deleted event. events.repo_id and
435430// webhooks.repo_id both cascade from repos, so recording one would delete
@@ -449,7 +444,6 @@ func deleteRepo(c *Ctx, repo store.Repo) int {
449444 if err := os.RemoveAll(RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name)); err != nil {
450445 return c.fail(protocol.ExitFailure, "database row removed but disk cleanup failed: %v", err)
451446 }
452 os.RemoveAll(RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name+".wiki"))
453447 return c.emit(map[string]string{"deleted": repo.Path()}, func(w io.Writer) {
454448 fmt.Fprintf(w, "deleted %s\n", repo.Path())
455449 })
internal/control/wiki.go +24 −28
@@ -4,7 +4,6 @@ import (
44 "encoding/base64"
55 "fmt"
66 "io"
7 "os"
87 "path"
98 "strings"
109
@@ -31,39 +30,36 @@ func init() {
3130 })
3231}
3332
34// A wiki lives in a companion bare repo beside its parent, so prose
35// edits stay out of the code repository's history, its protected
36// branches and its builds. The companion has no store row of its own —
37// access derives from the parent, exactly as it does for git over SSH —
38// which is why it needs commands rather than being addressable as a
39// repository.
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.
4037//
41// Editing stays a push to <repo>.wiki.git. That is the whole write
42// interface, on every surface, and there is nothing for a command to
43// add.
38// Writing is a push, or repo commit-file, like any other file in the
39// repository. There is no wiki-specific write command.
4440
4541// wikiExts are the page formats the web renders, in resolution order.
4642var wikiExts = []string{".md", ".org", ".markdown"}
4743
48// wikiDir resolves the parent, checks read access, and returns the
49// companion's path. A parent you cannot read has no wiki you can read.
50func wikiDir(c *Ctx, spec string) (repo store.Repo, dir string, code int) {
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) {
5151 parent, code := resolveRepo(c, spec, policy.CanRead)
5252 if code >= 0 {
53 return store.Repo{}, "", code
54 }
55 d := RepoDir(c.Cfg.Server.Root, parent.OwnerName, parent.Name+".wiki")
56 if _, err := os.Stat(d); err != nil {
57 return store.Repo{}, "", c.fail(protocol.ExitNotFound, "%s has no wiki", parent.Path())
53 return store.Repo{}, "", "", code
5854 }
59 return parent, d, -1
55 return parent, RepoDir(c.Cfg.Server.Root, parent.OwnerName, parent.Name), parent.DefaultBranch, -1
6056}
6157
62// wikiPages lists the page names in the companion, without extensions.
63func wikiPages(dir string) []string {
64 entries, err := gitutil.ListTree(dir, "main", "")
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)
6561 if err != nil {
66 return nil // the companion exists but has no commits yet
62 return nil // no .gitbay/wiki tree on this branch
6763 }
6864 var pages []string
6965 for _, e := range entries {
@@ -85,11 +81,11 @@ func runWikiList(c *Ctx, args []string) int {
8581 if len(args) != 1 {
8682 return c.fail(protocol.ExitUsage, "usage: wiki list <owner/name>")
8783 }
88 repo, dir, code := wikiDir(c, args[0])
84 repo, dir, branch, code := wikiDir(c, args[0])
8985 if code >= 0 {
9086 return code
9187 }
92 pages := wikiPages(dir)
88 pages := wikiPages(dir, branch)
9389 type out struct {
9490 Path string `json:"path"`
9591 Home string `json:"home,omitempty"`
@@ -126,11 +122,11 @@ func runWikiShow(c *Ctx, args []string) int {
126122 if len(args) < 1 || len(args) > 2 {
127123 return c.fail(protocol.ExitUsage, "usage: wiki show <owner/name> [<page>]")
128124 }
129 repo, dir, code := wikiDir(c, args[0])
125 repo, dir, branch, code := wikiDir(c, args[0])
130126 if code >= 0 {
131127 return code
132128 }
133 pages := wikiPages(dir)
129 pages := wikiPages(dir, branch)
134130 page := wikiHome(pages)
135131 if len(args) == 2 {
136132 page = strings.TrimSuffix(args[1], path.Ext(args[1]))
@@ -144,7 +140,7 @@ func runWikiShow(c *Ctx, args []string) int {
144140 }
145141
146142 for _, ext := range wikiExts {
147 raw, err := gitutil.ReadBlob(dir, "main", page+ext, c.Cfg.Limits.MaxBlobBytes)
143 raw, err := gitutil.ReadBlob(dir, branch, wikiTreePath+"/"+page+ext, c.Cfg.Limits.MaxBlobBytes)
148144 if err != nil {
149145 continue
150146 }
internal/gitutil/gitutil.go +3 −7
@@ -25,13 +25,9 @@ func InitBare(path, defaultBranch, hooksPath string) error {
2525 if out, err := cmd.CombinedOutput(); err != nil {
2626 return fmt.Errorf("git init: %v\n%s", err, out)
2727 }
28 // An empty hooksPath leaves the bare repo with no hooks — used for
29 // companion repos (wikis) that carry no ref policy.
30 if hooksPath != "" {
31 cmd = exec.Command(toolpath.Look("git"), "-C", path, "config", "core.hooksPath", hooksPath)
32 if out, err := cmd.CombinedOutput(); err != nil {
33 return fmt.Errorf("git config core.hooksPath: %v\n%s", err, out)
34 }
28 cmd = exec.Command(toolpath.Look("git"), "-C", path, "config", "core.hooksPath", hooksPath)
29 if out, err := cmd.CombinedOutput(); err != nil {
30 return fmt.Errorf("git config core.hooksPath: %v\n%s", err, out)
3531 }
3632 return nil
3733}
internal/httpd/web.go +1 −1
@@ -333,7 +333,7 @@ func (s *Server) repoFor(w http.ResponseWriter, r *http.Request, ref string) (re
333333 Mirrors: mirrors,
334334 Pinned: pinned,
335335 Watch: watch,
336 HasWiki: s.wikiDir(repo.OwnerName, repo.Name) != "",
336 HasWiki: s.hasWiki(repo),
337337 Host: s.cfg.SiteHost(),
338338 Desc: gitutil.ReadDescription(control.RepoDir(s.cfg.Server.Root, repo.OwnerName, repo.Name)),
339339 Repo: repo,
internal/httpd/wiki.go +18 −26
@@ -3,7 +3,6 @@ package httpd
33import (
44 "html/template"
55 "net/http"
6 "os"
76 "path"
87 "strings"
98
@@ -15,16 +14,24 @@ import (
1514 "gitbay.org/gitbay/internal/store"
1615)
1716
18// wikiDir returns the companion repo path, or "" when the repo has none.
19func (s *Server) wikiDir(owner, name string) string {
20 dir := control.RepoDir(s.cfg.Server.Root, owner, name+".wiki")
21 if _, err := os.Stat(dir); err != nil {
22 return ""
23 }
24 return dir
17// wikiTreePath is where wiki pages live in a repository's tree.
18const wikiTreePath = ".gitbay/wiki"
19
20// wikiDir returns repo's directory and default branch, where wiki pages
21// are resolved from .gitbay/wiki.
22func (s *Server) wikiDir(repo store.Repo) (dir, branch string) {
23 return control.RepoDir(s.cfg.Server.Root, repo.OwnerName, repo.Name), repo.DefaultBranch
24}
25
26// hasWiki reports whether repo's default branch holds a non-empty
27// .gitbay/wiki tree.
28func (s *Server) hasWiki(repo store.Repo) bool {
29 dir, branch := s.wikiDir(repo)
30 entries, err := gitutil.ListTree(dir, branch, wikiTreePath)
31 return err == nil && len(entries) > 0
2532}
2633
27// wiki renders a page from the repo's wiki companion. The home page is
34// wiki renders a page from the repo's .gitbay/wiki tree. The home page is
2835// Home.<ext> (or README.<ext>); /wiki/<name> resolves <name> with .md and
2936// .org fallbacks. Rendering reuses the same sanitized pipeline as READMEs.
3037func (s *Server) wiki(w http.ResponseWriter, r *http.Request) {
@@ -33,17 +40,6 @@ func (s *Server) wiki(w http.ResponseWriter, r *http.Request) {
3340 return
3441 }
3542 p.Tab = "wiki"
36 dir := s.wikiDir(p.Repo.OwnerName, p.Repo.Name)
37 if dir == "" {
38 s.render(w, "wiki.html", struct {
39 repoPage
40 Page string
41 PageHTML template.HTML
42 Pages []string
43 Missing bool
44 }{repoPage: p, Missing: true})
45 return
46 }
4743 var listing struct {
4844 Home string `json:"home"`
4945 Pages []string `json:"pages"`
@@ -97,12 +93,8 @@ func (s *Server) wikiRaw(w http.ResponseWriter, r *http.Request) {
9793 if !ok {
9894 return
9995 }
100 dir := s.wikiDir(p.Repo.OwnerName, p.Repo.Name)
101 if dir == "" {
102 s.notFound(w, r)
103 return
104 }
105 data, err := gitutil.ReadBlob(dir, "main", strings.Trim(r.PathValue("path"), "/"), s.cfg.Limits.MaxBlobBytes)
96 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)
10698 if err != nil {
10799 s.notFound(w, r)
108100 return
internal/policy/names.go −3
@@ -59,9 +59,6 @@ func ValidateName(name string) error {
5959 if len(name) > 4 && name[len(name)-4:] == ".git" {
6060 return fmt.Errorf("invalid name %q: must not end in .git", name)
6161 }
62 if len(name) > 5 && name[len(name)-5:] == ".wiki" {
63 return fmt.Errorf("invalid name %q: .wiki names are reserved for wiki companion repositories", name)
64 }
6562 return nil
6663}
6764
internal/sshd/sshd.go −60
@@ -16,7 +16,6 @@ import (
1616 "os"
1717 "path/filepath"
1818 "strconv"
19 "strings"
2019 "sync"
2120 "sync/atomic"
2221 "time"
@@ -356,12 +355,6 @@ func runGit(cfg config.Config, st *store.Store, user store.User, scope string, a
356355 }
357356 write := service == "git-receive-pack"
358357
359 // Wiki companion repos: ssh://.../owner/name.wiki.git. Access derives
360 // from the parent repo; the bare repo is created on first push.
361 if base, ok := strings.CutSuffix(strings.TrimSuffix(argv[1], ".git"), ".wiki"); ok {
362 return runWikiGit(cfg, st, user, scope, service, base, write, stdin, stdout, stderr)
363 }
364
365358 repo, err := st.RepoByPath(argv[1])
366359 if err != nil {
367360 fmt.Fprintln(stderr, "repository not found")
@@ -433,56 +426,3 @@ func runGit(cfg config.Config, st *store.Store, user store.User, scope string, a
433426 }
434427 return protocol.ExitOK
435428}
436
437// runWikiGit serves a repo's wiki companion. The wiki carries no ref
438// policy (no hooks, no merge requests); access is exactly the parent
439// repo's, and the bare repo is created on the first push. Deploy keys —
440// bound to the repo's own git data for CI — cannot touch the wiki.
441func runWikiGit(cfg config.Config, st *store.Store, user store.User, scope, service, basePath string,
442 write bool, stdin io.Reader, stdout, stderr io.Writer) int {
443 repo, err := st.RepoByPath(basePath)
444 if err != nil {
445 fmt.Fprintln(stderr, "repository not found")
446 return protocol.ExitNotFound
447 }
448 if policy.IsDeployScope(scope) {
449 fmt.Fprintln(stderr, "repository not found")
450 return protocol.ExitNotFound
451 }
452 grant, err := st.AccessRole(repo.ID, user.ID)
453 if err != nil {
454 fmt.Fprintln(stderr, "internal error")
455 return protocol.ExitFailure
456 }
457 if !policy.CanRead(user, repo, grant) {
458 fmt.Fprintln(stderr, "repository not found")
459 return protocol.ExitNotFound
460 }
461 if !policy.ScopeAllowsGit(scope, repo.Path(), write) {
462 fmt.Fprintf(stderr, "this key's scope (%s) does not allow %s on the wiki\n", scope, service)
463 return protocol.ExitDenied
464 }
465 if write && !policy.CanWrite(user, repo, grant) {
466 fmt.Fprintf(stderr, "write access to %s wiki denied\n", repo.Path())
467 return protocol.ExitDenied
468 }
469 if write && repo.Settings.Archived {
470 fmt.Fprintf(stderr, "%s is archived and read-only\n", repo.Path())
471 return protocol.ExitDenied
472 }
473 dir := control.RepoDir(cfg.Server.Root, repo.OwnerName, repo.Name+".wiki")
474 if _, err := os.Stat(dir); err != nil {
475 if !write {
476 fmt.Fprintln(stderr, "this repository has no wiki yet")
477 return protocol.ExitNotFound
478 }
479 if err := gitutil.InitBare(dir, "main", ""); err != nil {
480 fmt.Fprintln(stderr, "initializing wiki failed")
481 return protocol.ExitFailure
482 }
483 }
484 if err := gitutil.Transport(service, dir, stdin, stdout, stderr, nil, cfg.Limits.MaxPackBytes); err != nil {
485 return protocol.ExitFailure
486 }
487 return protocol.ExitOK
488}
internal/web/templates/wiki.html +3 −3
@@ -1,8 +1,8 @@
11{{define "title"}}wiki{{if .Page}}: {{.Page}}{{end}} · {{.Repo.OwnerName}}/{{.Repo.Name}}{{end}}
22{{define "content"}}
33<h1 class="vh">Wiki{{if .Page}}: {{.Page}}{{end}}</h1>
4{{if .Missing}}<p class="empty-note">no wiki yet — create one by pushing pages:<br>
5<code>git clone ssh://git@{{.Host}}/{{.Repo.OwnerName}}/{{.Repo.Name}}.wiki.git</code> then add <code>Home.md</code> (or .org) and push.</p>
4{{if .Missing}}<p class="empty-note">no wiki yet — add pages under <code>.gitbay/wiki/</code> on the default branch:<br>
5push <code>.gitbay/wiki/Home.md</code> (or .org), or use the file editor.</p>
66{{else}}
77<div class="wikilayout">
88<div class="wikipage">
@@ -14,7 +14,7 @@
1414<nav class="wikinav">
1515<p class="meta">pages</p>
1616<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 by push:<br><code>{{.Repo.Path}}.wiki.git</code></p>
17<p class="meta">edit under <code>.gitbay/wiki/</code> — push, or the file editor.</p>
1818</nav>
1919</div>
2020{{end}}