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
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
+44 -8
View File
@@ -75,10 +75,12 @@ type webBlobData struct {
}
type webEditData struct {
Repo Repository
Ref string
Path string
Content string
Repo Repository
Ref string
Path string
Content string
CommitMessage string
CommitDescription string
}
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>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>
<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>
</form>
{{end}}
@@ -964,7 +969,7 @@ func (s *Server) webRepoEdit(w http.ResponseWriter, r *http.Request, owner, name
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) {
@@ -993,7 +998,12 @@ func (s *Server) webRepoEditPost(w http.ResponseWriter, r *http.Request, owner,
s.webRepoEdit(w, r, owner, name, err.Error())
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())
return
}
@@ -1664,7 +1674,29 @@ func renderMarkdown(s string) (template.HTML, error) {
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) {
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 {
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") {
return nil
}
+15 -1
View File
@@ -197,7 +197,17 @@ func TestWebRepoPagesCreateEditAndForkRules(t *testing.T) {
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"))
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.AddCookie(csrf)
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") {
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.Visibility = "private"
+1 -1
View File
@@ -15,7 +15,7 @@ go test -cover ./...
Current default coverage snapshot after Phase 4 implementation:
```text
internal/app 60.2%
internal/app 60.8%
internal/backup 42.3%
internal/config 79.5%
internal/dbutil 8.6%