Commit 7c015c39b3

7c015c39b36312e245114fb3a257a3537a088092

parent: 2dd9add6d0

Verified · cmc

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

web, store: final-review wording, header and range-diff fixes

- privacy.html: tokens now also mint on the settings page.
- account.html's stock-OpenSSH example quoted token create, which the
  block above no longer offers; use web sessions list, which resolves
  (templatecmds_test).
- registered.html's next-steps links now match the settings page's
  actual section headings, API tokens and Email addresses.
- store.go: a foreign_key_check failure formatted the whole
  sql.NullInt64 struct instead of the row id.
- mrrangediff.go: a bad ?from=/?to= now renders the command's refusal
  on the page; only an unknown merge request 404s, since that is the
  only case checked before the command runs.
- CHANGELOG.org: fold the four #261 bullets into one, drop the watch
  three-state clause from the #271 bullet (already in the #261 one),
  even out blank-line spacing between bullets, and add bullets for
  the fork-owner MR link, the new-issue draft fixes, and the
  remaining empty-state cleanup.

Ref #270, Ref #271

Layout: unified · split

CHANGELOG.org +28 −18
@@ -14,18 +14,29 @@ anything beyond "replace the binary and restart" is needed.
14- A wiki link to an existing non-page file (an .svg, .txt, .pdf) now 14- A wiki link to an existing non-page file (an .svg, .txt, .pdf) now
15 resolves to the raw route instead of 404ing against the page route 15 resolves to the raw route instead of 404ing against the page route
16 (#283). 16 (#283).
17
18- The new-issue form takes labels, milestone and assignee in one step 17- The new-issue form takes labels, milestone and assignee in one step
19 for writers; the watch button names watching, muted and default; a 18 for writers; a Discussion heading sits before comment threads; the
20 Discussion heading sits before comment threads; the build page's 19 build page's live note says the page updates itself; and the rail
21 live note says the page updates itself; and the rail and the phone 20 and the phone More menu render from one list (#271).
22 More menu render from one list (#271). 21- The new-issue form keeps milestone and assignee through preview and
23 22 a refused create, the way it already kept title, body and labels;
23 a refused create re-renders the form with the draft and the
24 refusal instead of an error page (#271).
25- The merge request range-diff page renders a bad =from=/=to== query
26 parameter inline instead of 404ing; only an unknown merge request
27 404s (#271).
24- Empty states on the web state the fact instead of a CLI command, and 28- Empty states on the web state the fact instead of a CLI command, and
25 drop "yet" on a finished item; the merge request list offers a New 29 drop "yet" on a finished item; the merge request list offers a New
26 merge request link, a fork link, or a sign-in prompt depending on 30 merge request link, a fork link, or a sign-in prompt depending on
27 what the visitor can do; and the search page's scope caption is 31 what the visitor can do; and the search page's scope caption is
28 always visible, not only before a first search (#270). 32 always visible, not only before a first search (#270).
33- Issues, milestones, org milestones and releases drop the CLI command
34 from their empty states too, matching the rest of the register: a
35 link to the web form that does the thing when one exists, otherwise
36 just the fact (#270).
37- The merge request list and compare page offer New merge request to a
38 reader who owns a writable fork of the repository, not only to a
39 writer (#270).
29 40
30Credentials and sessions: revocation, delegation, expiry and an idle 41Credentials and sessions: revocation, delegation, expiry and an idle
31timeout (#256, #257, #276, #277). 42timeout (#256, #257, #276, #277).
@@ -148,18 +159,17 @@ missing, =gitbayd admin backup --verify <archive>= names it, and
148- A =-- foreign_keys: off= migration's =foreign_key_check= now runs 159- A =-- foreign_keys: off= migration's =foreign_key_check= now runs
149 inside the migration's own transaction, before commit, so a 160 inside the migration's own transaction, before commit, so a
150 violation rolls the migration back instead of leaving the bad 161 violation rolls the migration back instead of leaving the bad
151 schema and =user_version= already persisted (#261). 162 schema and =user_version= already persisted; the web pin and watch
152- The web pin and watch buttons dispatch through =repo pin=/=unpin= 163 buttons dispatch through =repo pin=/=unpin= and =repo
153 and =repo watch=/=mute=/=unwatch= instead of writing the store 164 watch=/=mute=/=unwatch= instead of writing the store directly, so a
154 directly, so a refusal reaches the viewer as a message instead of 165 refusal reaches the viewer as a message instead of being dropped,
155 being dropped. The watch button now cycles three states — default, 166 and the watch button now cycles three states — default, watching,
156 watching, muted — instead of two (#261). 167 muted — instead of two; the response that consumes a login link's
157- The response that consumes a login link's =?token== sends 168 =?token== sends =Cache-Control: no-store=, so no intermediary keeps
158 =Cache-Control: no-store=, so no intermediary keeps a copy of the 169 a copy of the single-use URL; and wiki documentation fixes:
159 single-use URL (#261). 170 API.org clarifies token commands work on the API, Parity.org
160- Wiki documentation fixes: API.org clarifies token commands work on the 171 documents batched review and web watch/pin dispatch, Threat-Model.org
161 API, Parity.org documents batched review and web watch/pin dispatch, 172 documents the login-link URL exception (#261).
162 Threat-Model.org documents the login-link URL exception (#261).
163- The account settings page quotes the CLI and SSH command forms that 173- The account settings page quotes the CLI and SSH command forms that
164 actually resolve; a test runs every command a web page quotes against 174 actually resolve; a test runs every command a web page quotes against
165 the CLI and control registries so a renamed command fails CI instead 175 the CLI and control registries so a renamed command fails CI instead
internal/httpd/accounts_test.go +2 −2
@@ -21,7 +21,7 @@ func TestRegisteredPageNumberedStepsAndTokenMention(t *testing.T) {
21 if !strings.Contains(out, "<ol>") { 21 if !strings.Contains(out, "<ol>") {
22 t.Error("next steps are not a numbered list") 22 t.Error("next steps are not a numbered list")
23 } 23 }
24 if !strings.Contains(out, "Settings → Tokens") { 24 if !strings.Contains(out, "Settings → API tokens") {
25 t.Error("no mention of Settings → Tokens for the iOS app") 25 t.Error("no mention of Settings → API tokens for the iOS app")
26 } 26 }
27} 27}
internal/httpd/mrrangediff.go +4 −4
@@ -37,11 +37,11 @@ func (s *Server) mrRangeDiff(w http.ResponseWriter, r *http.Request) {
37 if to := r.URL.Query().Get("to"); to != "" { 37 if to := r.URL.Query().Get("to"); to != "" {
38 argv = append(argv, "--to", to) 38 argv = append(argv, "--to", to)
39 } 39 }
40 // Only an unknown MR 404s (checked above). A bad --from/--to also
41 // resolves to nothing in git, which range-diff reports as
42 // ExitNotFound too, so that result renders on the page instead of
43 // turning a bad query parameter into a 404 (#271).
40 out, msg, code := s.runControlCode(viewer, argv) 44 out, msg, code := s.runControlCode(viewer, argv)
41 if code == protocol.ExitNotFound {
42 s.notFound(w, r)
43 return
44 }
45 errMsg := "" 45 errMsg := ""
46 if code != protocol.ExitOK { 46 if code != protocol.ExitOK {
47 errMsg = msg 47 errMsg = msg
internal/httpd/mrrangediff_test.go +12 −5
@@ -228,8 +228,8 @@ func TestMRRangeDiffPagePrivateRepo(t *testing.T) {
228} 228}
229 229
230// --from/--to reach the control command as real argv: an unknown 230// --from/--to reach the control command as real argv: an unknown
231// revision is refused with not-found, and two real revisions render the 231// revision renders the command's refusal on the page, and two real
232// range-diff between exactly those two. 232// revisions render the range-diff between exactly those two.
233func TestMRRangeDiffPageFromToQuery(t *testing.T) { 233func TestMRRangeDiffPageFromToQuery(t *testing.T) {
234 st, cfg, alice, _, repo, n, v1, v2, _ := rangeDiffFixture(t) 234 st, cfg, alice, _, repo, n, v1, v2, _ := rangeDiffFixture(t)
235 s := New(cfg, st, nil) 235 s := New(cfg, st, nil)
@@ -244,11 +244,18 @@ func TestMRRangeDiffPageFromToQuery(t *testing.T) {
244 return httptest.NewRecorder(), req 244 return httptest.NewRecorder(), req
245 } 245 }
246 246
247 t.Run("unknown revision is 404", func(t *testing.T) { 247 // A bad --from/--to is a query parameter, not an unknown merge
248 // request: it renders the command's refusal inline rather than
249 // 404ing the page, which is reserved for an MR that does not exist
250 // (#271).
251 t.Run("unknown revision renders the refusal inline", func(t *testing.T) {
248 rr, req := newReq("?from=0000000000000000000000000000000000000000") 252 rr, req := newReq("?from=0000000000000000000000000000000000000000")
249 s.mrRangeDiff(rr, req) 253 s.mrRangeDiff(rr, req)
250 if rr.Code != 404 { 254 if rr.Code != 200 {
251 t.Fatalf("status %d, want 404, body %s", rr.Code, rr.Body.String()) 255 t.Fatalf("status %d, want 200 (the refusal renders on the page), body %s", rr.Code, rr.Body.String())
256 }
257 if !strings.Contains(rr.Body.String(), "is not a revision of") {
258 t.Errorf("page does not show the refusal:\n%s", rr.Body.String())
252 } 259 }
253 }) 260 })
254 261
internal/store/store.go +1 −1
@@ -284,7 +284,7 @@ func (s *Store) migrateStep(sqlText string, newVersion int, fkOff bool) (retErr
284 return err 284 return err
285 } 285 }
286 rows.Close() 286 rows.Close()
287 return fmt.Errorf("foreign_key_check failed after migration: %s row %v", table, rowid) 287 return fmt.Errorf("foreign_key_check failed after migration: %s row %d", table, rowid.Int64)
288 } 288 }
289 if err := rows.Err(); err != nil { 289 if err := rows.Err(); err != nil {
290 rows.Close() 290 rows.Close()
internal/web/templates/account.html +1 −1
@@ -232,7 +232,7 @@ narrowest scope and shortest lifetime the job needs.</p>
232gitbay admin ... # instance administration</pre> 232gitbay admin ... # instance administration</pre>
233<p class="meta">All of it works from stock OpenSSH too, with the CLI's 233<p class="meta">All of it works from stock OpenSSH too, with the CLI's
234grouping words dropped: <code>ssh git@{{.Host}} whoami</code>, 234grouping words dropped: <code>ssh git@{{.Host}} whoami</code>,
235<code>ssh git@{{.Host}} token create --name laptop</code>.</p> 235<code>ssh git@{{.Host}} web sessions list</code>.</p>
236</section> 236</section>
237</div> 237</div>
238</div> 238</div>
internal/web/templates/privacy.html +1 −1
@@ -30,7 +30,7 @@ is active is kept in app preferences; nothing else is retained on the
30device. The app embeds no analytics, no crash reporting, and no 30device. The app embeds no analytics, no crash reporting, and no
31third-party SDK, so there is nothing to opt out of. Removing the account 31third-party SDK, so there is nothing to opt out of. Removing the account
32deletes the token from the Keychain.</p> 32deletes the token from the Keychain.</p>
33<p>Tokens are minted over SSH and can be revoked at any time with 33<p>Tokens are minted over SSH or on the settings page and can be revoked at any time with
34<code>gitbay auth token revoke</code>, which ends the app's access 34<code>gitbay auth token revoke</code>, which ends the app's access
35immediately.</p> 35immediately.</p>
36 36
internal/web/templates/registered.html +2 −2
@@ -8,10 +8,10 @@
8<ol> 8<ol>
9<li>Copy the verification code from the mail you were just sent.</li> 9<li>Copy the verification code from the mail you were just sent.</li>
10<li><a href="/login">Sign in</a> with an emailed link.</li> 10<li><a href="/login">Sign in</a> with an emailed link.</li>
11<li>Paste the code in <a href="/settings#emails">Settings → Email</a>.</li> 11<li>Paste the code in <a href="/settings#emails">Settings → Email addresses</a>.</li>
12</ol> 12</ol>
13<p>Then + creates your first repository. Using the iOS app? Create a 13<p>Then + creates your first repository. Using the iOS app? Create a
14token in <a href="/settings#tokens">Settings → Tokens</a>.</p> 14token in <a href="/settings#tokens">Settings → API tokens</a>.</p>
15<h2>From the terminal</h2> 15<h2>From the terminal</h2>
16<pre class="quickstart" tabindex="0">ssh git@{{.Host}} whoami 16<pre class="quickstart" tabindex="0">ssh git@{{.Host}} whoami
17ssh git@{{.Host}} repo create {{.Username}}/hello 17ssh git@{{.Host}} repo create {{.Username}}/hello