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/issues_changes_test.go +49
| @@ -700,6 +700,55 @@ func TestIssueAndChangeListsReturnFragmentsForHtmx(t *testing.T) { | |||
| 700 | 700 | } | |
| 701 | 701 | } | |
| 702 | 702 | ||
| 703 | // TestRequestFilesListsEveryChangedFile: the changed-file tree and the count come | ||
| 704 | // from the change's inventory, so a file larger than a page can render does not | ||
| 705 | // hide the files named after it. The patch the tab shows is still one file's. | ||
| 706 | func TestRequestFilesListsEveryChangedFile(t *testing.T) { | ||
| 707 | t.Parallel() | ||
| 708 | f := newFixture(t) | ||
| 709 | f.publish(f.repo) | ||
| 710 | base := workRepo(t, map[string]string{"readme.md": "v1\n"}) | ||
| 711 | if _, errs, err := f.git(base, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil { | ||
| 712 | t.Fatalf("main push: %s", errs) | ||
| 713 | } | ||
| 714 | // a-big.txt sorts first and is larger than the 512 KiB that used to bound the | ||
| 715 | // patch the tree was parsed from, so the files after it were never listed. | ||
| 716 | files := map[string]string{"a-big.txt": strings.Repeat("x", 600<<10) + "\n"} | ||
| 717 | for _, name := range []string{"b.txt", "docs/c.txt", "z.txt"} { | ||
| 718 | files[name] = name + "\n" | ||
| 719 | } | ||
| 720 | feature, _ := f.cloneWithChange(t, files, "a big change") | ||
| 721 | if _, errs, err := f.git(feature, "push", f.sshURL(f.repo), "HEAD:refs/heads/feature"); err != nil { | ||
| 722 | t.Fatalf("feature push: %s", errs) | ||
| 723 | } | ||
| 724 | sess, cookie := f.browserSession(t, f.ownerID) | ||
| 725 | resp, body := f.browserPost(t, "/r/"+f.repo+"/requests", cookie, url.Values{ | ||
| 726 | "csrf": {sess.CSRF}, "title": {"Every file"}, "body": {"body\n"}, | ||
| 727 | "base": {"main"}, "head": {"feature"}, | ||
| 728 | }) | ||
| 729 | if resp.StatusCode != http.StatusSeeOther { | ||
| 730 | t.Fatalf("create change: %d\n%s", resp.StatusCode, body) | ||
| 731 | } | ||
| 732 | id := strings.TrimPrefix(resp.Header.Get("Location"), "/r/"+f.repo+"/requests/") | ||
| 733 | |||
| 734 | filesURL := "/r/" + f.repo + "/requests/" + id + "/files" | ||
| 735 | _, page := f.browserGet(t, filesURL, cookie) | ||
| 736 | if !strings.Contains(page, "4 changed") { | ||
| 737 | t.Errorf("the tree does not count every file:\n%.2000s", page) | ||
| 738 | } | ||
| 739 | for _, name := range []string{"a-big.txt", "b.txt", "docs/c.txt", "z.txt"} { | ||
| 740 | if !strings.Contains(page, filesURL+"/"+name) { | ||
| 741 | t.Errorf("the tree is missing %s:\n%.2000s", name, page) | ||
| 742 | } | ||
| 743 | } | ||
| 744 | // The open file is one file's patch, and it is whole. | ||
| 745 | _, page = f.browserGet(t, filesURL+"/z.txt", cookie) | ||
| 746 | if !strings.Contains(page, `<span class="path">z.txt</span>`) || | ||
| 747 | !strings.Contains(page, `data-highlight-side="new">z.txt<`) { | ||
| 748 | t.Fatalf("the selected file has no patch:\n%.2000s", page) | ||
| 749 | } | ||
| 750 | } | ||
| 751 | |||
| 703 | 752 | func TestLineAnchoredReviewCommentFromTheWeb(t *testing.T) { | |
| 704 | 753 | t.Parallel() | |
| 705 | 754 | f := newFixture(t) | |