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.

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)