Commit 070b443e46
070b443e4636836d3ed067923d31203af51935a7
parent: 7149e0bd2e
Verified · cmc
cmc <hello@cleberg.net> · 2026-09-12 05:22 UTC
web: the editor answers 404 for a missing branch and names a new file
A ref that does not exist was rendered as a new-file form whose submit
could only fail. A path that does not exist on a real branch stays a
new-file form, since commit-file creates it, and the page now says so.
Ref #182
docs/plans/2026-09-11-web-ux-sweep.md
+1 −1
| @@ -533,7 +533,7 @@ In `editForm`, after `repo` is resolved and before reading the blob, compute: |
| 533 | 533 | } |
| 534 | 534 | ``` |
| 535 | 535 | |
| 536 | | Pass `Blocked: blocked` in the page struct. Still read the blob so a missing file is a 404 either way. Add `"slices"` to the imports. |
| 536 | Pass `Blocked: blocked` in the page struct. Still read the blob; a missing path on a real branch is a new-file form. Add `"slices"` to the imports. |
| 537 | 537 | |
| 538 | 538 | - [ ] **Step 4: The template** |
| 539 | 539 | |
docs/specs/2026-09-11-web-ux-sweep-design.md
+3
| @@ -32,6 +32,9 @@ in, and the rules chosen to fix them. |
| 32 | 32 | does not parse as a version sorts after the ones that do, by name. |
| 33 | 33 | - **Editor.** When the repository requires signed commits, or the ref |
| 34 | 34 | refuses direct pushes, the edit page explains that and shows no form. |
| 35 | A branch that does not exist is a 404; a path that does not exist on |
| 36 | a real branch is a new-file form that says so, since `commit-file` |
| 37 | creates it. |
| 35 | 38 | - **Repository settings.** Every Save names its field. |
| 36 | 39 | - **Empty states.** Sidebars use "none yet" for things and "nobody yet" |
| 37 | 40 | for people; lists keep their sentence and, where a command creates the |
e2e/accounts_test.go
+10
| @@ -129,6 +129,16 @@ func TestWebAccounts(t *testing.T) { |
| 129 | 129 | t.Fatalf("edit submit: %d", status) |
| 130 | 130 | } |
| 131 | 131 | |
| 132 | // A branch that does not exist is a 404; a path that does not exist |
| 133 | // on a real branch is a new-file form that says so. |
| 134 | if status, _ := browserGet(t, browser, inst.base()+"/alice/site/edit/nope/notes.txt"); status != 404 { |
| 135 | t.Fatalf("edit form on a missing branch: %d", status) |
| 136 | } |
| 137 | status, body = browserGet(t, browser, inst.base()+"/alice/site/edit/main/new.txt") |
| 138 | if status != 200 || !strings.Contains(body, "does not exist on main; committing creates it") || !strings.Contains(body, "<textarea") { |
| 139 | t.Fatalf("edit form for a new file: %d\n%s", status, body) |
| 140 | } |
| 141 | |
| 132 | 142 | // The edit is a real commit: authored with the verified email, and it |
| 133 | 143 | // displays as unsigned — the honest outcome for a server-side commit. |
| 134 | 144 | logOut, _, code := inst.ssh(t, aliceKey, "", "repo", "log", "alice/site", "--limit", "1", "--json") |
internal/httpd/accounts.go
+12 −3
| @@ -480,6 +480,8 @@ type editPage struct { |
| 480 | 480 | Content string |
| 481 | 481 | Error string |
| 482 | 482 | Blocked string |
| 483 | // Creating marks a path the branch does not have yet. |
| 484 | Creating bool |
| 483 | 485 | } |
| 484 | 486 | |
| 485 | 487 | func (s *Server) editForm(w http.ResponseWriter, r *http.Request, u store.User) { |
| @@ -499,9 +501,16 @@ func (s *Server) editForm(w http.ResponseWriter, r *http.Request, u store.User) |
| 499 | 501 | } |
| 500 | 502 | |
| 501 | 503 | dir := control.RepoDir(s.cfg.Server.Root, repo.OwnerName, repo.Name) |
| 504 | // A branch that does not exist has nothing to edit. A path that does |
| 505 | // not exist on a real branch is a new file: commit-file creates it. |
| 506 | if _, err := gitutil.ResolveRef(dir, "refs/heads/"+ref); err != nil { |
| 507 | s.notFound(w, r) |
| 508 | return |
| 509 | } |
| 502 | 510 | content, err := gitutil.ReadBlob(dir, "refs/heads/"+ref, filePath, maxRenderBytes) |
| 503 | | if err != nil { |
| 504 | | content = nil // new file |
| 511 | creating := err != nil |
| 512 | if creating { |
| 513 | content = nil |
| 505 | 514 | } |
| 506 | 515 | if gitutil.IsBinary(content) { |
| 507 | 516 | http.Error(w, "binary files cannot be edited in the browser", http.StatusBadRequest) |
| @@ -509,7 +518,7 @@ func (s *Server) editForm(w http.ResponseWriter, r *http.Request, u store.User) |
| 509 | 518 | } |
| 510 | 519 | s.render(w, "edit.html", editPage{ |
| 511 | 520 | basePage: s.baseFor(u), Repo: repo, |
| 512 | | Ref: ref, Path: filePath, Content: string(content), Blocked: blocked, |
| 521 | Ref: ref, Path: filePath, Content: string(content), Blocked: blocked, Creating: creating, |
| 513 | 522 | }) |
| 514 | 523 | } |
| 515 | 524 | |
internal/web/templates/edit.html
+1
| @@ -3,6 +3,7 @@ |
| 3 | 3 | <h1>edit {{.Repo.OwnerName}}/{{.Repo.Name}} : {{.Path}} @ {{.Ref}}</h1> |
| 4 | 4 | {{if .Error}}<p class="error" role="alert">{{.Error}}</p>{{end}} |
| 5 | 5 | {{if .Blocked}}<p class="empty-note">{{.Blocked}}</p>{{else}} |
| 6 | {{if .Creating}}<p class="meta"><code>{{.Path}}</code> does not exist on {{.Ref}}; committing creates it.</p>{{end}} |
| 6 | 7 | <form method="post" action="/{{.Repo.OwnerName}}/{{.Repo.Name}}/edit/{{.Ref}}/{{.Path}}" class="editform"> |
| 7 | 8 | <p><textarea name="content" aria-label="File contents" rows="24" spellcheck="false">{{.Content}}</textarea></p> |
| 8 | 9 | <p><input name="message" aria-label="Commit message" placeholder="commit message"> |