docs/specs/2026-09-04-wiki-in-repo-design.md
156 lines · 7243 bytes
1# Wikis in the repository
2
3Ref #169, #170. Milestone v1.14.0. Two merge requests: path filters first,
4because without them the second one makes the wiki worse to use than it is
5today.
6
7## Problem
8
9A wiki is a companion bare repo at `<owner>/<name>.wiki.git`, created on first
10push (`internal/sshd/sshd.go:359-362, 473-480`). It has no row in the store;
11access derives from the parent.
12
13That buys one thing — prose edits stay out of the code repository's history,
14its protected branches and its builds — and costs four:
15
16- **Invisible to the store.** Backup verification cannot distinguish a wiki
17 from a leaked directory and prints so (`cmd/gitbayd/backup.go:270`). Quotas,
18 `repo list`, search and the activity feed do not see it either.
19- **Push is the only write path.** There is no `wiki edit` command and the web
20 renders wikis read-only. It is the one capability that does not follow "the
21 capability lands as a control command, then the surfaces render it".
22- **`.wiki` is a reserved name suffix**, permanently
23 (`internal/policy/names.go:62`).
24- **A second clone URL** that cannot be discovered without knowing the
25 convention.
26
27## Approach
28
29Pages move to `.gitbay/wiki/` on the default branch, beside `ci.yml` and
30`CODEOWNERS`, which is already where repository-scoped gitbay metadata lives.
31The companion path is removed rather than kept alongside: one wiki exists across
3270 repositories on this instance, so a compatibility path would be permanent
33cost for a single migration.
34
35Rejected: an orphan ref (`refs/wiki/main`) in the main repository. It keeps
36prose off the code DAG and out of normal clones, but it is invisible to plain
37git tooling, needs a custom refspec to fetch, and would need its own write
38commands to be usable at all. The gain over a directory is that prose stays out
39of `git log`; the cost is a wiki nobody can edit without forge-specific
40instructions.
41
42## What this does and does not buy
43
44**Does:** one clone, one backup, one permission model, one history. Wiki edits
45become reviewable through merge requests, approvals and CODEOWNERS for projects
46that want that.
47
48**Does not:** web editing on every repository. `repo commit-file` is the command
49behind the web editor, and it refuses repositories that require verified
50signatures, because the server authors those commits unsigned and will not write
51a commit the repository's own policy would reject
52(`internal/control/commitfile.go:28-35`). `krz/gitbay` requires signed commits,
53so its wiki stays push-only. That is not a regression — it is push-only today —
54but the parity gain is conditional and should not be claimed otherwise.
55
56## Design
57
58### Phase 1 — path filters (#169)
59
60`ci.Job` gains `Paths` and `PathsIgnore`, each a list of globs matched against
61the changed-file list from `gitutil.DiffFiles(dir, old, new)`, which already
62exists (`internal/gitutil/merge.go:276`).
63
64**Matching.** `path.Match` alone is not enough: Go's `*` does not cross `/`, so
65`.gitbay/wiki/**` matches `.gitbay/wiki/Home.md` but not
66`.gitbay/wiki/sub/Page.md` — a filter that appears to work and quietly misses
67nested files. Define it explicitly: a pattern ending in `/**` matches that
68directory and everything beneath it at any depth, implemented as a prefix
69check; every other pattern goes to `path.Match` against the full path. A test
70must cover the nested case, since that is the one a reader will assume works.
71
72**Selection.** A job runs when `Paths` is empty or at least one changed file
73matches one of its patterns. It is then skipped only when every changed file
74matches at least one `PathsIgnore` pattern. A push touching one ignored file
75and one other file runs the job.
76
77**Fail open.** A job runs whenever the filter cannot be evaluated: a new branch
78with no diff base (`old` is empty or all zeros), a `DiffFiles` error, or a job
79declaring neither key. A filter that silently skips CI when it cannot tell is
80worse than no filter, because the failure is invisible.
81
82`QueueBranchBuilds` is shared with the merge path, which moves a ref without
83reaching a hook (`internal/hookd/hookd.go:272-278`), so the old sha must reach
84both callers. `u.Old` is already available at the hook call site
85(`hookd.go:212`).
86
87Tag jobs are unaffected: a tag build has no meaningful diff base.
88
89### Phase 2 — the move (#170)
90
91**Storage.** `.gitbay/wiki/*.{md,org,markdown}` on `repo.DefaultBranch`.
92`wikiExts` is unchanged.
93
94**Resolution.** `wikiPages` and `wikiHome` keep their logic; they read a tree at
95`.gitbay/wiki` on the default branch instead of the root of the companion's
96`main`. `wiki list` and `wiki show` keep their argv, their JSON fields and their
97exit codes — only resolution moves, so no surface changes shape.
98
99`HasWiki` (`internal/httpd/web.go:336`) becomes "the default branch holds a
100non-empty `.gitbay/wiki/` tree". The web route `/{owner}/{repo}/wiki` is
101externally identical.
102
103**Writing.** A push, like any other file. `repo commit-file <owner/name>
104.gitbay/wiki/Page.md --ref <branch> --file -` is the existing command and the
105existing web editor path; no `wiki edit` is added, because it would duplicate
106one.
107
108**Removal.** The `.wiki` suffix branch and `runWikiGit` in
109`internal/sshd/sshd.go`; `wikiDir` in `internal/control/wiki.go` and
110`internal/httpd/wiki.go`; the reservation in `internal/policy/names.go:62` and
111the test asserting it; companion rename and delete in
112`internal/control/repo.go:396,452`; the special-case wording in
113`cmd/gitbayd/backup.go:270`.
114
115**Migration.** One repository, by hand, not a shipped command:
116
117```
118git bundle create gitbay-wiki-$(date +%F).bundle --all # in a clone of the companion
119git subtree add --prefix=.gitbay/wiki <wiki-url> main
120```
121
122`git subtree add` preserves the wiki's history inside the repository's DAG
123rather than flattening it into one import commit. Verify pages render, keep the
124bundle, then remove the bare repo from the server.
125
126Add `paths-ignore: [".gitbay/wiki/**"]` to this repository's own heavy jobs in
127the same change, so the migration does not immediately demonstrate the problem
128phase 1 exists to prevent.
129
130## Tests
131
132Phase 1:
133- A job with `paths` matching a changed file runs; one matching nothing does not.
134- `paths-ignore` covering every changed file skips the job; covering some of
135 them does not.
136- A new branch runs every job.
137- A `DiffFiles` failure runs every job.
138- A job with neither key runs, unchanged from today.
139- Tag builds are unaffected.
140
141Phase 2:
142- `wiki list` and `wiki show` return the same JSON for a repository whose pages
143 are in `.gitbay/wiki/` as the old commands returned for a companion.
144- The web wiki tab renders, and reports no wiki when the directory is absent.
145- A repository with no `.gitbay/wiki/` reports no wiki rather than erroring.
146- Pushing to `<name>.wiki.git` is refused, since the route is gone.
147- A repository may now be named `something.wiki`.
148- `repo commit-file` writes a page on a repository that permits it, and is
149 refused on one requiring verified signatures.
150
151## Documentation
152
153CLAUDE.md's "the repo's own documentation lives in the wiki" stops being true of
154the storage and needs rewording. The wiki's Parity rows for wiki capabilities
155change, and the "SSH only, by design" list does not mention wikis, so it needs
156no edit.