Commit 63974ed532

63974ed532d875902b8a22eea18153a45500592c

parent: c7a2a1a6ab

Verified · cmc

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

Design: wikis in the repository

Ref #169, #170

Layout: unified · split

docs/specs/2026-09-04-wiki-in-repo-design.md added +146
@@ -0,0 +1,146 @@
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 with
61`path.Match` against the changed-file list from `gitutil.DiffFiles(dir, old,
62new)`, which already exists (`internal/gitutil/merge.go:276`).
63
64A job runs when its `Paths` is empty or at least one changed file matches it,
65and no `PathsIgnore` pattern matches every changed file.
66
67**Fail open.** A job runs whenever the filter cannot be evaluated: a new branch
68with no diff base (`old` is empty or all zeros), a `DiffFiles` error, or a job
69declaring neither key. A filter that silently skips CI when it cannot tell is
70worse than no filter, because the failure is invisible.
71
72`QueueBranchBuilds` is shared with the merge path, which moves a ref without
73reaching a hook (`internal/hookd/hookd.go:272-278`), so the old sha must reach
74both callers. `u.Old` is already available at the hook call site
75(`hookd.go:212`).
76
77Tag jobs are unaffected: a tag build has no meaningful diff base.
78
79### Phase 2 — the move (#170)
80
81**Storage.** `.gitbay/wiki/*.{md,org,markdown}` on `repo.DefaultBranch`.
82`wikiExts` is unchanged.
83
84**Resolution.** `wikiPages` and `wikiHome` keep their logic; they read a tree at
85`.gitbay/wiki` on the default branch instead of the root of the companion's
86`main`. `wiki list` and `wiki show` keep their argv, their JSON fields and their
87exit codes — only resolution moves, so no surface changes shape.
88
89`HasWiki` (`internal/httpd/web.go:336`) becomes "the default branch holds a
90non-empty `.gitbay/wiki/` tree". The web route `/{owner}/{repo}/wiki` is
91externally identical.
92
93**Writing.** A push, like any other file. `repo commit-file <owner/name>
94.gitbay/wiki/Page.md --ref <branch> --file -` is the existing command and the
95existing web editor path; no `wiki edit` is added, because it would duplicate
96one.
97
98**Removal.** The `.wiki` suffix branch and `runWikiGit` in
99`internal/sshd/sshd.go`; `wikiDir` in `internal/control/wiki.go` and
100`internal/httpd/wiki.go`; the reservation in `internal/policy/names.go:62` and
101the test asserting it; companion rename and delete in
102`internal/control/repo.go:396,452`; the special-case wording in
103`cmd/gitbayd/backup.go:270`.
104
105**Migration.** One repository, by hand, not a shipped command:
106
107```
108git bundle create gitbay-wiki-$(date +%F).bundle --all # in a clone of the companion
109git subtree add --prefix=.gitbay/wiki <wiki-url> main
110```
111
112`git subtree add` preserves the wiki's history inside the repository's DAG
113rather than flattening it into one import commit. Verify pages render, keep the
114bundle, then remove the bare repo from the server.
115
116Add `paths-ignore: [".gitbay/wiki/**"]` to this repository's own heavy jobs in
117the same change, so the migration does not immediately demonstrate the problem
118phase 1 exists to prevent.
119
120## Tests
121
122Phase 1:
123- A job with `paths` matching a changed file runs; one matching nothing does not.
124- `paths-ignore` covering every changed file skips the job; covering some of
125 them does not.
126- A new branch runs every job.
127- A `DiffFiles` failure runs every job.
128- A job with neither key runs, unchanged from today.
129- Tag builds are unaffected.
130
131Phase 2:
132- `wiki list` and `wiki show` return the same JSON for a repository whose pages
133 are in `.gitbay/wiki/` as the old commands returned for a companion.
134- The web wiki tab renders, and reports no wiki when the directory is absent.
135- A repository with no `.gitbay/wiki/` reports no wiki rather than erroring.
136- Pushing to `<name>.wiki.git` is refused, since the route is gone.
137- A repository may now be named `something.wiki`.
138- `repo commit-file` writes a page on a repository that permits it, and is
139 refused on one requiring verified signatures.
140
141## Documentation
142
143CLAUDE.md's "the repo's own documentation lives in the wiki" stops being true of
144the storage and needs rewording. The wiki's Parity rows for wiki capabilities
145change, and the "SSH only, by design" list does not mention wikis, so it needs
146no edit.