Add commit fields to web file editor
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
+40
-4
@@ -79,6 +79,8 @@ type webEditData struct {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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%
|
||||||
|
|||||||
Reference in New Issue
Block a user