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