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:
533533 }
534534```
535535
536Pass `Blocked: blocked` in the page struct. Still read the blob so a missing file is a 404 either way. Add `"slices"` to the imports.
536Pass `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.
537537
538538- [ ] **Step 4: The template**
539539
docs/specs/2026-09-11-web-ux-sweep-design.md +3
@@ -32,6 +32,9 @@ in, and the rules chosen to fix them.
3232 does not parse as a version sorts after the ones that do, by name.
3333- **Editor.** When the repository requires signed commits, or the ref
3434 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.
3538- **Repository settings.** Every Save names its field.
3639- **Empty states.** Sidebars use "none yet" for things and "nobody yet"
3740 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) {
129129 t.Fatalf("edit submit: %d", status)
130130 }
131131
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
132142 // The edit is a real commit: authored with the verified email, and it
133143 // displays as unsigned — the honest outcome for a server-side commit.
134144 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 {
480480 Content string
481481 Error string
482482 Blocked string
483 // Creating marks a path the branch does not have yet.
484 Creating bool
483485}
484486
485487func (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)
499501 }
500502
501503 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 }
502510 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
505514 }
506515 if gitutil.IsBinary(content) {
507516 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)
509518 }
510519 s.render(w, "edit.html", editPage{
511520 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,
513522 })
514523}
515524
internal/web/templates/edit.html +1
@@ -3,6 +3,7 @@
33<h1>edit {{.Repo.OwnerName}}/{{.Repo.Name}} : {{.Path}} @ {{.Ref}}</h1>
44{{if .Error}}<p class="error" role="alert">{{.Error}}</p>{{end}}
55{{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}}
67<form method="post" action="/{{.Repo.OwnerName}}/{{.Repo.Name}}/edit/{{.Ref}}/{{.Path}}" class="editform">
78<p><textarea name="content" aria-label="File contents" rows="24" spellcheck="false">{{.Content}}</textarea></p>
89<p><input name="message" aria-label="Commit message" placeholder="commit message">