Add commit fields to web file editor

This commit is contained in:
2026-06-08 15:29:39 -05:00
parent c025fbe946
commit f2d8743b6b
4 changed files with 61 additions and 11 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ CLI auth config defaults to:
## Web UI ## Web UI
The web UI is intentionally minimal and server-rendered. It supports repository search, user profile pages with public repositories, login/register/logout, repository creation, branch selection, a branches page for branch creation/deletion, selected-branch latest commit and commit count status, commit history pages, single commit pages with limited diff rendering, repository file browsing, Markdown rendering, image rendering for common image files, plain-text file viewing, simple textarea-based file editing for users with write access, pull request list/create/view/comment/close/merge workflows, and forking public repositories. Private repositories cannot be forked. Branch renaming is intentionally unsupported in both the API and web UI. The web UI is intentionally minimal and server-rendered. It supports repository search, user profile pages with public repositories, login/register/logout, repository creation, branch selection, a branches page for branch creation/deletion, selected-branch latest commit and commit count status, commit history pages, single commit pages with limited diff rendering, repository file browsing, Markdown rendering, image rendering for common image files, plain-text file viewing, simple textarea-based file editing with commit messages for users with write access, pull request list/create/view/comment/close/merge workflows, and forking public repositories. Private repositories cannot be forked. Branch renaming is intentionally unsupported in both the API and web UI.
## Development ## Development
+44 -8
View File
@@ -75,10 +75,12 @@ type webBlobData struct {
} }
type webEditData struct { type webEditData struct {
Repo Repository Repo Repository
Ref string Ref string
Path string Path string
Content string Content string
CommitMessage string
CommitDescription string
} }
type webTreeEntry struct { type webTreeEntry struct {
@@ -337,6 +339,9 @@ var webTemplates = template.Must(template.New("web").Funcs(template.FuncMap{
<p><label>Branch<br><input name="ref" value="{{$d.Ref}}"></label></p> <p><label>Branch<br><input name="ref" value="{{$d.Ref}}"></label></p>
<p><label>Path<br><input name="path" value="{{$d.Path}}" size="80"></label></p> <p><label>Path<br><input name="path" value="{{$d.Path}}" size="80"></label></p>
<p><label>Content<br><textarea name="content" rows="24" cols="100">{{$d.Content}}</textarea></label></p> <p><label>Content<br><textarea name="content" rows="24" cols="100">{{$d.Content}}</textarea></label></p>
<h2>Commit</h2>
<p><label>Commit message<br><input name="commit_message" value="{{$d.CommitMessage}}" size="80"></label></p>
<p><label>Extended description<br><textarea name="commit_description" rows="4" cols="80">{{$d.CommitDescription}}</textarea></label></p>
<p><button type="submit">commit changes</button></p> <p><button type="submit">commit changes</button></p>
</form> </form>
{{end}} {{end}}
@@ -964,7 +969,7 @@ func (s *Server) webRepoEdit(w http.ResponseWriter, r *http.Request, owner, name
content = c content = c
} }
} }
s.renderWeb(w, r, "Edit File", webEditData{Repo: repo, Ref: ref, Path: p, Content: content}, errMsg) s.renderWeb(w, r, "Edit File", webEditData{Repo: repo, Ref: ref, Path: p, Content: content, CommitMessage: defaultEditCommitMessage(p)}, errMsg)
} }
func (s *Server) webRepoEditPost(w http.ResponseWriter, r *http.Request, owner, name string) { func (s *Server) webRepoEditPost(w http.ResponseWriter, r *http.Request, owner, name string) {
@@ -993,7 +998,12 @@ func (s *Server) webRepoEditPost(w http.ResponseWriter, r *http.Request, owner,
s.webRepoEdit(w, r, owner, name, err.Error()) s.webRepoEdit(w, r, owner, name, err.Error())
return return
} }
if err := s.commitEditedFile(repo, ref, p, r.FormValue("content"), user); err != nil { message, description, err := editCommitFields(p, r.FormValue("commit_message"), r.FormValue("commit_description"))
if err != nil {
s.webRepoEdit(w, r, owner, name, err.Error())
return
}
if err := s.commitEditedFile(repo, ref, p, r.FormValue("content"), message, description, user); err != nil {
s.webRepoEdit(w, r, owner, name, err.Error()) s.webRepoEdit(w, r, owner, name, err.Error())
return return
} }
@@ -1664,7 +1674,29 @@ func renderMarkdown(s string) (template.HTML, error) {
return template.HTML(buf.String()), nil return template.HTML(buf.String()), nil
} }
func (s *Server) commitEditedFile(repo Repository, ref, p, content string, user User) error { func defaultEditCommitMessage(p string) string {
if p == "" {
return "Create file"
}
return "Edit " + p
}
func editCommitFields(p, message, description string) (string, string, error) {
message = strings.TrimSpace(message)
description = strings.TrimSpace(description)
if message == "" {
message = defaultEditCommitMessage(p)
}
if strings.ContainsAny(message, "\x00\r\n") {
return "", "", errors.New("commit message must be a single line")
}
if strings.Contains(description, "\x00") {
return "", "", errors.New("commit description is invalid")
}
return message, description, nil
}
func (s *Server) commitEditedFile(repo Repository, ref, p, content, message, description string, user User) error {
if !branchRE.MatchString(ref) { if !branchRE.MatchString(ref) {
return errors.New("invalid branch") return errors.New("invalid branch")
} }
@@ -1702,7 +1734,11 @@ func (s *Server) commitEditedFile(repo Repository, ref, p, content string, user
if err := gitRunOutput(work, "add", filepath.FromSlash(p)); err != nil { if err := gitRunOutput(work, "add", filepath.FromSlash(p)); err != nil {
return err return err
} }
if err := gitRunOutput(work, "commit", "-m", "Edit "+p); err != nil { args := []string{"commit", "-m", message}
if description != "" {
args = append(args, "-m", description)
}
if err := gitRunOutput(work, args...); err != nil {
if strings.Contains(err.Error(), "nothing to commit") { if strings.Contains(err.Error(), "nothing to commit") {
return nil return nil
} }
+15 -1
View File
@@ -197,7 +197,17 @@ func TestWebRepoPagesCreateEditAndForkRules(t *testing.T) {
expectLoadRepo(mock, "alice", "demo", repo) expectLoadRepo(mock, "alice", "demo", repo)
expectBearerUser(mock, "tok", user) expectBearerUser(mock, "tok", user)
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPost, "/alice/demo/edit", strings.NewReader("_csrf="+csrf.Value+"&ref=main&path=web.txt&content=from+web")) req = httptest.NewRequest(http.MethodGet, "/alice/demo/edit?ref=main&path=README.md", nil)
req.AddCookie(&http.Cookie{Name: webAuthCookie, Value: "tok"})
s.ServeHTTP(rr, req)
if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "Commit message") || !strings.Contains(rr.Body.String(), "Extended description") || !strings.Contains(rr.Body.String(), "Edit README.md") {
t.Fatalf("edit page status=%d body=%s", rr.Code, rr.Body.String())
}
expectLoadRepo(mock, "alice", "demo", repo)
expectBearerUser(mock, "tok", user)
rr = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPost, "/alice/demo/edit", strings.NewReader("_csrf="+csrf.Value+"&ref=main&path=web.txt&content=from+web&commit_message=Custom+web+commit&commit_description=Detailed+body"))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(csrf) req.AddCookie(csrf)
req.AddCookie(&http.Cookie{Name: webAuthCookie, Value: "tok"}) req.AddCookie(&http.Cookie{Name: webAuthCookie, Value: "tok"})
@@ -209,6 +219,10 @@ func TestWebRepoPagesCreateEditAndForkRules(t *testing.T) {
if err != nil || !strings.Contains(content, "from web") { if err != nil || !strings.Contains(content, "from web") {
t.Fatalf("edited file content=%q err=%v", content, err) t.Fatalf("edited file content=%q err=%v", content, err)
} }
commits, err := gitCommitHistory(bare, "main", 1)
if err != nil || len(commits) != 1 || commits[0].Subject != "Custom web commit" || !strings.Contains(commits[0].Body, "Detailed body") {
t.Fatalf("edited commit history=%#v err=%v", commits, err)
}
privateRepo := repo privateRepo := repo
privateRepo.Visibility = "private" privateRepo.Visibility = "private"
+1 -1
View File
@@ -15,7 +15,7 @@ go test -cover ./...
Current default coverage snapshot after Phase 4 implementation: Current default coverage snapshot after Phase 4 implementation:
```text ```text
internal/app 60.2% internal/app 60.8%
internal/backup 42.3% internal/backup 42.3%
internal/config 79.5% internal/config 79.5%
internal/dbutil 8.6% internal/dbutil 8.6%