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.
core/git/git.go +72−7
| @@ -1139,18 +1139,83 @@ func (r *Repo) Diff(rangeOrFrom, to string, maxBytes int) (string, bool, error) | |||
| 1139 | 1139 | return string(out), len(out) == maxBytes, nil | |
| 1140 | 1140 | } | |
| 1141 | 1141 | ||
| 1142 | // ShowPatch is the patch of one commit against its parent (or the empty tree for | ||
| 1143 | // a root commit). `git show` gets that case right, which is why it is used here | ||
| 1144 | // instead of assembling a range by hand. | ||
| 1145 | func (r *Repo) ShowPatch(rev string, maxBytes int) (string, bool, error) { | ||
| 1146 | out, errs, err := r.runHead(maxBytes, | ||
| 1147 | "show", "--format=", "--patch", "--no-color", "--no-ext-diff", "--find-renames", "--unified=3", rev) | ||
| 1142 | // ShowPatchPath is a commit's patch against its parent (or the empty tree for a | ||
| 1143 | // root commit), limited to the paths given. `git show` gets the root case right, | ||
| 1144 | // which is why it is used here instead of assembling a range by hand. | ||
| 1145 | // | ||
| 1146 | // It is what a per-file commit view reads, so opening a broad commit costs the | ||
| 1147 | // hunks of one file rather than the whole diff. A rename needs both of its paths | ||
| 1148 | // named: filtered to the destination alone, git reports what is left as a new | ||
| 1149 | // file, and the view would lose the rename. | ||
| 1150 | func (r *Repo) ShowPatchPath(rev string, paths []string, maxBytes int) (string, bool, error) { | ||
| 1151 | args := []string{"show", "--format=", "--patch", "--no-color", "--no-ext-diff", "--find-renames", "--unified=3", rev, "--"} | ||
| 1152 | args = append(args, paths...) | ||
| 1153 | out, errs, err := r.runHead(maxBytes, args...) | ||
| 1148 | 1154 | if err != nil { | |
| 1149 | return "", false, fmt.Errorf("show %s: %w: %s", rev, err, errs) | ||
| 1155 | return "", false, fmt.Errorf("show %s (%s): %w: %s", rev, strings.Join(paths, " "), err, errs) | ||
| 1150 | 1156 | } | |
| 1151 | 1157 | return string(out), len(out) == maxBytes, nil | |
| 1152 | 1158 | } | |
| 1153 | 1159 | ||
| 1160 | // PathChange is one path a commit touched, as git's name-status reports it. Old | ||
| 1161 | // is the path before the commit and New is the path after it, so Old is empty for | ||
| 1162 | // a file the commit added and New is empty for a deletion. They differ only for a | ||
| 1163 | // rename or a copy, and the patch for such a path is complete only when git is | ||
| 1164 | // told both. | ||
| 1165 | // | ||
| 1166 | // Status is git's single letter: A added, M modified, D deleted, R renamed, | ||
| 1167 | // C copied, T type changed. | ||
| 1168 | type PathChange struct { | ||
| 1169 | Status string | ||
| 1170 | Old string | ||
| 1171 | New string | ||
| 1172 | } | ||
| 1173 | |||
| 1174 | // ShowNameStatus lists the paths one commit touched without generating its | ||
| 1175 | // patch. It is the cheap index a per-file viewer renders first, so the cost of | ||
| 1176 | // opening a commit grows with the number of files it touched, not with the size | ||
| 1177 | // of their diffs. | ||
| 1178 | // | ||
| 1179 | // The output is read NUL-separated because git otherwise quotes a path holding a | ||
| 1180 | // quote, a tab or a non-ASCII byte, and these paths are what the view builds | ||
| 1181 | // links and pathspecs from. | ||
| 1182 | func (r *Repo) ShowNameStatus(rev string) ([]PathChange, error) { | ||
| 1183 | out, errs, err := r.run(nil, "show", "--format=", "--name-status", "--no-color", "--find-renames", "-z", rev) | ||
| 1184 | if err != nil { | ||
| 1185 | return nil, fmt.Errorf("show --name-status %s: %w: %s", rev, err, errs) | ||
| 1186 | } | ||
| 1187 | fields := strings.Split(string(out), "\x00") | ||
| 1188 | changes := make([]PathChange, 0, len(fields)/2) | ||
| 1189 | for i := 0; i < len(fields); { | ||
| 1190 | status := strings.TrimSpace(fields[i]) | ||
| 1191 | i++ | ||
| 1192 | if status == "" { | ||
| 1193 | continue | ||
| 1194 | } | ||
| 1195 | if i >= len(fields) { | ||
| 1196 | break | ||
| 1197 | } | ||
| 1198 | first := fields[i] | ||
| 1199 | i++ | ||
| 1200 | c := PathChange{Status: status[:1], Old: first, New: first} | ||
| 1201 | if c.Status == "R" || c.Status == "C" { | ||
| 1202 | if i >= len(fields) { | ||
| 1203 | break | ||
| 1204 | } | ||
| 1205 | c.New = fields[i] | ||
| 1206 | i++ | ||
| 1207 | } | ||
| 1208 | switch c.Status { | ||
| 1209 | case "A": | ||
| 1210 | c.Old = "" | ||
| 1211 | case "D": | ||
| 1212 | c.New = "" | ||
| 1213 | } | ||
| 1214 | changes = append(changes, c) | ||
| 1215 | } | ||
| 1216 | return changes, nil | ||
| 1217 | } | ||
| 1218 | |||
| 1154 | 1219 | // CommitBody is a commit's full message, for the commit page. | |
| 1155 | 1220 | func (r *Repo) CommitBody(rev string) (string, error) { | |
| 1156 | 1221 | out, errs, err := r.run(nil, "show", "-s", "--format=%B", rev) | |