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.
Read a change's file list without its patch, and never cut a diff
f64c77e4950c1759f336d3bc1849ead86e8b8ebf · Keenan Nemetz · 8d ago · parent 11720250
Read a change's file list without its patch, and never cut a diff The review tree, the file count and the landing summary were all parsed out of one `git diff` capped at 512 KiB. When a change carries a large file the cut lands inside it, and every file named after the cut is missing from the tree: reviewing #143, a 546 KB deletion hid the 296 files that came after it while the tab still said "6 changed". The cap existed to stop a page rendering every hunk, but the pages it guarded already render one file at a time. The inventory and the patch are separate reads now. DiffInventory runs `git diff --raw --numstat -z`, which names every file with its status and line counts in one walk of the trees and generates no hunks; the changed-file tree, the counts and the landing summary read that. The Files tab then asks for the one file it shows with DiffPaths, and the commit page keeps its per-file ShowPatchPath. Nothing is capped by bytes: a page is as large as the file it shows, and the inventory is the whole change however large it is. Compare still renders every file of a range at once, and is uncapped too, so a large comparison is one large page. Making that per-file is the next step if it turns out to matter. TestDiffInventoryCountsEveryFile covers the statuses, counts, renames and binary files the inventory has to report; TestRequestFilesListsEveryChangedFile is the regression for the tree that lost its files, and the commit page test now asserts the end of a large patch instead of its cut.
server/web/request_files.go +30−8
| @@ -15,6 +15,7 @@ import ( | |||
| 15 | 15 | "strconv" | |
| 16 | 16 | "strings" | |
| 17 | 17 | ||
| 18 | "github.com/rodent-software/vole/core/git" | ||
| 18 | 19 | "github.com/rodent-software/vole/forge/repo" | |
| 19 | 20 | "github.com/rodent-software/vole/metadata" | |
| 20 | 21 | ) | |
| @@ -87,6 +88,25 @@ func buildReviewTree(files []diffFile, active string) []*treeNode { | |||
| 87 | 88 | return root.Children | |
| 88 | 89 | } | |
| 89 | 90 | ||
| 91 | // reviewFileDiff reads the patch of the one file the pane shows, and only that | ||
| 92 | // file's. Reading every file's hunks to display one is what made a broad change | ||
| 93 | // expensive, and cutting the result to fit is what hid the files that came after | ||
| 94 | // the cut. | ||
| 95 | func reviewFileDiff(g *git.Repo, change reviewChange, f diffFile) ([]diffFile, error) { | ||
| 96 | patch, err := g.DiffPaths(change.From, change.To, f.pathspec()) | ||
| 97 | if err != nil { | ||
| 98 | return nil, err | ||
| 99 | } | ||
| 100 | for _, p := range parseDiff(patch) { | ||
| 101 | if diffFilePath(p) == diffFilePath(f) { | ||
| 102 | return []diffFile{p}, nil | ||
| 103 | } | ||
| 104 | } | ||
| 105 | // A file git prints no patch for (a mode change, for instance) still belongs | ||
| 106 | // in the pane: the tree lists it, so the pane cannot pretend it is not there. | ||
| 107 | return []diffFile{f}, nil | ||
| 108 | } | ||
| 109 | |||
| 90 | 110 | // reviewMainView is what the Files pane renders, both as the full page's main | |
| 91 | 111 | // column and as the HTMX fragment a tree click swaps in. | |
| 92 | 112 | type reviewMainView struct { | |
| @@ -96,7 +116,6 @@ type reviewMainView struct { | |||
| 96 | 116 | Files []diffFile | |
| 97 | 117 | Added int | |
| 98 | 118 | Removed int | |
| 99 | Truncated bool | ||
| 100 | 119 | AlreadyMerged bool | |
| 101 | 120 | LineCommentURL string | |
| 102 | 121 | } | |
| @@ -122,9 +141,9 @@ func (w *Web) RequestFiles(rw http.ResponseWriter, r *http.Request) { | |||
| 122 | 141 | w.notFound(rw, r) | |
| 123 | 142 | return | |
| 124 | 143 | } | |
| 125 | files, truncated, alreadyMerged := w.requestDiff(m, req) | ||
| 144 | change := w.reviewDiff(m, req) | ||
| 145 | files := change.Files | ||
| 126 | 146 | comments := w.requestComments(m, req) | |
| 127 | attachRequestComments(files, comments) | ||
| 128 | 147 | ||
| 129 | 148 | selected := r.PathValue("path") | |
| 130 | 149 | if selected == "" && len(files) > 0 { | |
| @@ -150,14 +169,17 @@ func (w *Web) RequestFiles(rw http.ResponseWriter, r *http.Request) { | |||
| 150 | 169 | Name: m.Name, | |
| 151 | 170 | Request: req, | |
| 152 | 171 | Path: active, | |
| 153 | Truncated: truncated, | ||
| 154 | AlreadyMerged: alreadyMerged, | ||
| 172 | AlreadyMerged: change.Merged, | ||
| 155 | 173 | LineCommentURL: "/r/" + m.Name + "/requests/" + req.ID, | |
| 156 | 174 | } | |
| 157 | 175 | if chosen != nil { | |
| 158 | main.Files = []diffFile{*chosen} | ||
| 159 | main.Added = chosen.Added | ||
| 160 | main.Removed = chosen.Removed | ||
| 176 | main.Files, err = reviewFileDiff(m.Git(), change, *chosen) | ||
| 177 | if err != nil { | ||
| 178 | w.serverError(rw, r, "cannot read that change: "+err.Error()) | ||
| 179 | return | ||
| 180 | } | ||
| 181 | attachRequestComments(main.Files, comments) | ||
| 182 | main.Added, main.Removed = diffSummary(main.Files) | ||
| 161 | 183 | } | |
| 162 | 184 | if htmxRequest(r) { | |
| 163 | 185 | w.render(rw, "review_main", main, 0) | |