Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Open one file at a time on the commit page

628d45239eb09af4e46c9dc779d30144e96c7e6f · Keenan Nemetz · 11d ago · parent 816cc543

Open one file at a time on the commit page

A commit page rendered every patch it contained, so a broad commit turned one
page load into every blob it touched. It now opens the file tree beside the diff
of a single file, the same shape a change request's Files tab uses: the tree is
built from `git show --name-status`, only the selected path's patch is read, and
a tree click swaps the pane over HTMX. `/commit/{sha}/{path}` deep-links a file
and a 512 KiB cap still bounds one file's patch.

The file tree became one widget for three surfaces rather than two, so its
context gained a Commit field and its href/target/status rules moved onto the
view (Branch, Diff, Href).

The pane's header is now one row: the file's name on the left and its line
counts on the right, with the separate "1 file changed" summary line dropped
when a single file fills the pane. The total-line remains for the multi-file
diff a compare renders.

server/web/commit.go +118−31

@@ -1,8 +1,10 @@
1 1 package web
2 2
3 // Commit pages: metadata plus the patch the commit introduced. This is the view
4 // a reviewer needs before approving anything, and it is read-only like the rest
5 // of the web — the review itself is a git artifact, not a web session.
3 // Commit pages: a commit's metadata above a tree of the files it touched, and
4 // the diff of the one file that is open. A broad commit is reviewed file by
5 // file, because rendering every patch at once turns one page load into every
6 // blob the commit touched. This is read-only like the rest of the web — the
7 // review itself is a git artifact, not a web session.
6 8
7 9 import (
8 10 "net/http"
@@ -10,11 +12,40 @@ import (
10 12 "github.com/rodent-software/vole/core/git"
11 13 )
12 14
13 // diffCap bounds what a single view renders. A patch is parsed in memory and
15 // diffCap bounds what one file's view renders. A patch is parsed in memory and
14 16 // shown in full; a generated 50 MiB diff is not a review, so it is cut and
15 17 // labelled as cut.
16 18 const diffCap = 512 << 10
17 19
20 // commitView is the commit page: the metadata, the changed-file tree, and the
21 // pane the selected file's diff renders in.
22 type commitView struct {
23 Page
24 Name string
25 Commit git.Commit
26 Body string
27 Parents []string
28 FileCount int
29 FilesTruncated bool
30 Tree []*treeNode
31 Main commitMainView
32 }
33
34 // commitMainView is the diff pane, both as the page's main column and as the
35 // HTMX fragment a tree click swaps in.
36 type commitMainView struct {
37 Path string
38 Files []diffFile
39 Added int
40 Removed int
41 Truncated bool
42 // A commit page is read-only, so a diff line has nowhere to attach a review
43 // comment; the diff component reads this and draws no comment control.
44 LineCommentURL string
45 }
46
47 // CommitView renders one commit, opening the first file it touched. A path
48 // deep-links one file, and tree clicks fetch only the diff pane over HTMX.
18 49 func (w *Web) CommitView(rw http.ResponseWriter, r *http.Request) {
19 50 m, ok := w.open(rw, r)
20 51 if !ok {
@@ -31,39 +62,95 @@ func (w *Web) CommitView(rw http.ResponseWriter, r *http.Request) {
31 62 w.notFound(rw, r)
32 63 return
33 64 }
34 patch, truncated, err := g.ShowPatch(sha, diffCap)
65 // The tree is built from the names alone: reading every file's hunks to find
66 // out which files the commit touched is what makes a commit page expensive.
67 changes, err := g.ShowNameStatus(sha)
68 if err != nil {
69 w.serverError(rw, r, "cannot read that commit: "+err.Error())
70 return
71 }
72 files := changedFiles(changes)
73 truncated := false
74 if len(files) > maxTreeFiles {
75 files = files[:maxTreeFiles]
76 truncated = true
77 }
78
79 selected := r.PathValue("path")
80 if selected == "" && len(files) > 0 {
81 selected = diffFilePath(files[0])
82 }
83 chosen := -1
84 for i := range files {
85 if diffFilePath(files[i]) == selected {
86 chosen = i
87 break
88 }
89 }
90 if selected != "" && chosen < 0 {
91 w.notFound(rw, r)
92 return
93 }
94 main, err := commitDiff(g, sha, selected, files, chosen)
35 95 if err != nil {
36 96 w.serverError(rw, r, "cannot read that commit: "+err.Error())
37 97 return
38 98 }
99 if htmxRequest(r) {
100 w.render(rw, "commit_main", main, 0)
101 return
102 }
39 103 body, _ := g.CommitBody(sha)
40 files := parseDiff(patch)
41 added, removed := diffSummary(files)
42 104 parents, _ := g.CommitParents(sha)
43
44 w.render(rw, "commit", struct {
45 Page
46 Name string
47 Commit git.Commit
48 Body string
49 Files []diffFile
50 Added int
51 Removed int
52 Truncated bool
53 Parents []string
54 LineCommentURL string
55 }{
56 Page: w.page(r, m.Name+" · "+log[0].Subject),
57 Name: m.Name,
58 Commit: log[0],
59 Body: body,
60 Files: files,
61 Added: added,
62 Removed: removed,
63 // A cut patch and a patch with nothing in it look the same to a parser;
64 // the flag is what tells the reader which one they have.
65 Truncated: truncated,
105 w.render(rw, "commit", commitView{
106 Page: w.page(r, m.Name+" · "+log[0].Subject),
107 Name: m.Name,
108 Commit: log[0],
109 Body: body,
66 110 Parents: parents,
67 LineCommentURL: "",
111 FileCount: len(files),
112 FilesTruncated: truncated,
113 Tree: buildReviewTree(files, selected),
114 Main: main,
68 115 }, 0)
69 116 }
117
118 // commitDiff reads the patch of the one file being shown, and only that file's.
119 func commitDiff(g *git.Repo, sha, selected string, files []diffFile, chosen int) (commitMainView, error) {
120 main := commitMainView{Path: selected}
121 if chosen < 0 {
122 return main, nil
123 }
124 f := files[chosen]
125 patch, truncated, err := g.ShowPatchPath(sha, f.pathspec(), diffCap)
126 if err != nil {
127 return main, err
128 }
129 main.Truncated = truncated
130 for _, p := range parseDiff(patch) {
131 if diffFilePath(p) == selected {
132 main.Files = []diffFile{p}
133 break
134 }
135 }
136 if main.Files == nil {
137 // A patch git printed nothing for (a mode change, or a cut that stopped
138 // before this file) still belongs in the pane: the tree lists it, so the
139 // pane can never pretend it is not there.
140 f.Language = prismLanguage(f.Label())
141 main.Files = []diffFile{f}
142 }
143 main.Added, main.Removed = diffSummary(main.Files)
144 return main, nil
145 }
146
147 // changedFiles turns a commit's name-status lines into the rows the tree and the
148 // pane need: the same shape a parsed diff produces, so the status mark and the
149 // file's label are derived the same way whichever read built them.
150 func changedFiles(changes []git.PathChange) []diffFile {
151 files := make([]diffFile, 0, len(changes))
152 for _, c := range changes {
153 files = append(files, diffFile{OldPath: c.Old, NewPath: c.New})
154 }
155 return files
156 }