docs/specs/2026-09-19-profile-about-repo-design.md
223 lines · 8433 bytes
1# Profile about text in a repository
2
3Closes #236.
4
5The `about` text on a user or org profile is a column on `users`/`orgs`,
6set through `profile set --about` and rendered by `aboutHTML`. It is the
7only long-form, version-worthy prose on the instance that is not a file
8in a repository. This moves it into one.
9
10Links, description and website stay where they are. The issue lists
11links as a "consider"; moving them buys nothing the DB columns do not
12already give, and a second file or a front-matter parser is cost without
13a return.
14
15## Where it lives
16
17`profile/README.md` or `profile/README.org` on the default branch of a
18repository named `.gitbay` under the owner's namespace:
19
20```
21cmc/.gitbay
22└── profile/
23 └── README.org
24```
25
26Resolution order is the wiki's `wikiExts`: `.md`, `.org`, `.markdown`;
27the first that exists wins. There are no store rows for the file —
28access derives from the parent repository, exactly as
29`internal/control/wiki.go` states for wiki pages.
30
31A dot-repo rather than the GitHub-style `cmc/cmc` because `.gitbay` is
32not single-purpose: it is the place later per-owner configuration
33(issue templates, org defaults) goes, and a leading dot is the signal
34that it is infrastructure rather than a project.
35
36### Reading
37
38A helper in `internal/control/profile.go`:
39
40```go
41// ownerAbout returns the about text and its format for an owner, read
42// from profile/README.* on the default branch of <owner>/.gitbay.
43// Everything missing — the repo, the branch, the file — is an empty
44// about, as is a repo the caller cannot read.
45func ownerAbout(c *Ctx, owner string) (text, format string)
46```
47
48It resolves `<owner>/.gitbay` through the same access check every other
49read takes, so a repository the caller cannot read yields no about. The
50blob read is capped at `maxCommitFileBytes` (1MB).
51
52`ProfileOut.About` and `ProfileOut.AboutFormat` keep their JSON names
53and meanings; only the source changes. `AboutFormat` is `org` for a
54`.org` file and `md` otherwise. The API contract and the iOS client are
55untouched.
56
57One field is added: `about_path`, the repository-relative path the text
58was read from, empty when there is no about. The web needs it to link to
59the file rather than guess its extension, and every other client gets
60the same pointer.
61
62`aboutHTML` in `internal/httpd/web.go` becomes a direct
63`renderReadme(name, raw)` call — there is a filename to dispatch on now,
64so the stored-format indirection goes away.
65
66## Naming
67
68`policy.namePat` requires a leading alphanumeric, so `.gitbay` is an
69invalid repository name today:
70
71```go
72var namePat = regexp.MustCompile(`^\.?[a-z0-9][a-z0-9._-]{0,61}$`)
73```
74
75One optional leading dot, same 63-character ceiling. `ValidateName`
76keeps refusing `.`, `..` and a `.git` suffix, and gains an exact-`.git`
77refusal — the suffix rule only fires for names longer than four
78characters.
79
80Relaxing the pattern rather than whitelisting the one name `.gitbay` is
81the smaller change, and it gives owners `.dotfiles` and the like for
82free.
83
84## A first commit into an empty repository
85
86`gitutil.CommitFileChange` resolves the target branch and fails when it
87does not exist, so committing the first file into a freshly created
88`.gitbay` is impossible today. Both the web's create button and the
89backfill need it to work.
90
91An unresolvable branch becomes a root commit **only when the repository
92has no refs at all**. Anywhere else it stays the error it is now — a
93typo'd branch name in a repository with history must not silently start
94an orphan branch.
95
96This also makes `repo commit-file` work on a repository created but
97never pushed to, which is the same gap seen from the CLI.
98
99## Writing
100
101There is no about-specific write command, for the reason the wiki has
102none: the content is a file, and the file is written the way files are
103written.
104
105- `profile set` loses `--about`, `--about-format` and `--file`, and
106 loses `ReadsStdin`.
107- `org profile` loses the same three flags.
108- Authoring is a push, or
109 `repo commit-file cmc/.gitbay profile/README.org --ref main --file -`.
110
111### The web
112
113The About textarea on the account page is replaced by a line naming the
114file and linking to it, with a button that creates `<owner>/.gitbay`
115and commits a starter `profile/README.md` when the repository does not
116exist yet. Editing then happens in the repository file editor that
117already exists.
118
119The alternative — keeping the textarea and dispatching
120`repo commit-file` from it — needs an auto-create path and has a stale
121file problem: moving the format picker from md to org leaves a
122`README.md` that keeps winning resolution, and `commit-file` cannot
123delete it, so the handler needs a second dispatch to `file remove`. The
124pointer is smaller and matches how a wiki page is edited.
125
126The account form's `profile` case keeps `--description`, `--website`
127and `--link`, and stops sending `--about-format` and stdin. The Preview
128button on that form goes with the textarea; the repository file editor
129has its own.
130
131## Access and visibility
132
133The about renders to whoever can read `<owner>/.gitbay`. A private
134`.gitbay` means the about is visible to the owner and admins only. That
135is the parent-derived rule already in force for wiki pages, not a new
136one.
137
138The backfill creates the repository **public**, so no about that was
139world-readable becomes hidden by the move.
140
141Repositories whose name starts with `.` are filtered out of:
142
143- the profile page's repository list (`ProfileOut.Repos`), and
144- `explore`.
145
146They stay in `repo list`, which is the owner's own inventory, and stay
147reachable at their URL. Hiding the repository is what a dot-repo buys
148over `cmc/cmc`; without the filter the move trades one visible
149single-purpose repository for another.
150
151## Migration
152
153A SQL migration cannot write git objects, so the move is two pieces
154that ship together. `gitbayd` runs `MigrateUp` at startup, so a backfill
155that reads the columns must not run after a migration that drops them —
156hence the holding table.
157
158**Migration 0058** copies every owner with a non-empty about into a
159holding table, then drops the columns:
160
161```sql
162CREATE TABLE profile_about_backfill (
163 owner_kind TEXT NOT NULL,
164 owner_id INTEGER NOT NULL,
165 about TEXT NOT NULL,
166 about_format TEXT NOT NULL,
167 PRIMARY KEY (owner_kind, owner_id)
168);
169INSERT INTO profile_about_backfill ... -- users, then orgs
170ALTER TABLE users DROP COLUMN about; -- and about_format
171ALTER TABLE orgs DROP COLUMN about; -- and about_format
172```
173
174The down migration re-adds the columns empty and drops the table.
175
176**`gitbayd admin migrate-profile-about`**, in the shape of
177`adminMigrateCommitRefsCmd` in `cmd/gitbayd/adminusers.go`, drains the
178table. Per row: create `<owner>/.gitbay` public if it does not exist,
179`gitutil.CommitFileChange` the README at the recorded format, delete
180the row. Idempotent — an owner who already has the file is skipped and
181their row deleted.
182
183Commit identity is the owner's primary verified email when they have
184one, otherwise `<name>@users.noreply.<host>`. Orgs have no email and
185always take the fallback.
186
187A later release drops the emptied holding table.
188
189## Testing
190
191Unit:
192
193- `policy`: `.gitbay` and `.dotfiles` accepted; `.`, `..`, `.git` and
194 `x.git` still refused; the 63-character ceiling holds with the dot.
195- `ownerAbout`: `.md` wins over `.org`; a missing repo, a missing
196 branch and a missing file each give an empty about; a repo the caller
197 cannot read gives an empty about.
198- `gitutil.CommitFileChange`: the first commit into an empty repository
199 succeeds and reads back; the second takes the parented path; an
200 unknown branch in a repository with history is still an error.
201
202e2e, new `e2e/profileabout_test.go`:
203
204- a committed `profile/README.md` shows on `profile show` and on the
205 web profile page;
206- a private `.gitbay` hides the about from an outsider while the owner
207 still sees it;
208- dot-repos do not appear in `explore` or in a profile's repository
209 list, and do appear in `repo list`;
210- `profile set --about` is refused as an unknown flag.
211
212e2e for the backfill in the shape of `e2e/commentmigrate_test.go`:
213a row in the holding table becomes a repository with the file, and a
214second run is a no-op.
215
216`TestStdinCommandsReadStdin` already polices `profile set` dropping
217`ReadsStdin`.
218
219## Documentation
220
221- `.gitbay/wiki/Parity` — the profile row.
222- `.gitbay/wiki/Users` — the profile section: where the about lives and
223 how to write it.