Commit ddab521653

ddab52165310177c117b89a07d5b155e56fb56dc

parent: 0a4bb99d63

Verified · cmc ci/build: success ci/test: success ci/vuln: success

cmc <hello@cleberg.net> · 2026-09-04 01:39 UTC

web: a diff cut at 4 MiB says so, and the commit list says how many it hides

Diff and ShowPatch cut the patch at the byte limit mid-line and
reported nothing, so the last hunk rendered wrong and the diffstat
under-reported. Both now cut on a line boundary and return whether
they did; the commit and merge request pages show a notice and the
diffstat says its counts are partial; mr diff and repo commit say so
on stderr. The merge request's commit list is capped at 100 and now
says "first 100 of N".

Closes #117

Layout: unified · split

internal/control/mr.go +4 −1
@@ -548,11 +548,14 @@ func runMRDiff(c *Ctx, args []string) int {
548548 }
549549 base = b
550550 }
551 patch, err := gitutil.Diff(dir, base, head, 4<<20)
551 patch, truncated, err := gitutil.Diff(dir, base, head, 4<<20)
552552 if err != nil {
553553 return c.fail(protocol.ExitFailure, "%v", err)
554554 }
555555 fmt.Fprint(c.Stdout, patch)
556 if truncated {
557 fmt.Fprintln(c.Stderr, "diff truncated at 4 MiB; fetch the branch for the rest")
558 }
556559 return protocol.ExitOK
557560}
558561
internal/control/sig.go +4 −1
@@ -246,10 +246,13 @@ func runRepoCommit(c *Ctx, args []string) int {
246246 if err != nil {
247247 return c.fail(protocol.ExitFailure, "verifying %s: %v", full, err)
248248 }
249 patch, err := gitutil.ShowPatch(dir, full, 4<<20)
249 patch, truncated, err := gitutil.ShowPatch(dir, full, 4<<20)
250250 if err != nil {
251251 return c.fail(protocol.ExitFailure, "%v", err)
252252 }
253 if truncated {
254 fmt.Fprintln(c.Stderr, "patch truncated at 4 MiB; clone the repository for the rest")
255 }
253256 statuses, err := c.Store.ListCommitStatuses(repo.ID, full)
254257 if err != nil {
255258 return c.fail(protocol.ExitFailure, "%v", err)
internal/gitutil/endofoptions_test.go +1 −1
@@ -35,7 +35,7 @@ func TestRefsAreNotOptions(t *testing.T) {
3535 "Archive": func() error { return Archive(dir, ref, "x", &sink) },
3636 "Grep": func() error { _, err := Grep(dir, ref, "hi", 10); return err },
3737 "MergeBase": func() error { _, err := MergeBase(dir, ref, "main"); return err },
38 "Diff": func() error { _, err := Diff(dir, ref, "main", 1<<20); return err },
38 "Diff": func() error { _, _, err := Diff(dir, ref, "main", 1<<20); return err },
3939 "DiffFiles": func() error { _, err := DiffFiles(dir, ref, "main"); return err },
4040 "RevListRange": func() error { _, err := RevListRange(dir, "main", ref); return err },
4141 "Blame": func() error { _, err := Blame(dir, ref, "f.txt", 1, 1); return err },
internal/gitutil/merge.go +20 −6
@@ -1,6 +1,7 @@
11package gitutil
22
33import (
4 "bytes"
45 "fmt"
56 "os"
67 "os/exec"
@@ -93,17 +94,30 @@ func CommitTree(dir, tree string, parents []string, name, email, message string)
9394}
9495
9596// Diff returns the patch for old..new (three-dot semantics are the caller's
96// job: pass the merge base as old).
97func Diff(dir, old, new string, limit int64) (string, error) {
97// job: pass the merge base as old), cut at limit bytes on a line boundary;
98// truncated says whether it was cut, so the caller can say so instead of
99// rendering a hunk that ends mid-line (#117).
100func Diff(dir, old, new string, limit int64) (patch string, truncated bool, err error) {
98101 cmd := exec.Command("git", "-C", dir, "diff", "--stat", "--patch", "--end-of-options", old, new)
99102 out, err := cmd.Output()
100103 if err != nil {
101 return "", fmt.Errorf("diff: %w", err)
104 return "", false, fmt.Errorf("diff: %w", err)
102105 }
103 if int64(len(out)) > limit {
104 out = out[:limit]
106 out, truncated = cutAtLine(out, limit)
107 return string(out), truncated, nil
108}
109
110// cutAtLine keeps at most limit bytes, ending on the last newline before
111// the limit.
112func cutAtLine(out []byte, limit int64) ([]byte, bool) {
113 if int64(len(out)) <= limit {
114 return out, false
115 }
116 cut := out[:limit]
117 if i := bytes.LastIndexByte(cut, '\n'); i >= 0 {
118 cut = cut[:i+1]
105119 }
106 return string(out), nil
120 return cut, true
107121}
108122
109123// MergeBase returns the best common ancestor, or an error if none exists.
internal/gitutil/read.go +9 −7
@@ -151,21 +151,23 @@ func (c *cappedWriter) Write(p []byte) (int, error) {
151151}
152152
153153// ShowPatch returns the stat+patch text for one commit.
154func ShowPatch(dir, sha string, limit int64) (string, error) {
155 cmd := exec.Command("git", "-C", dir, "show", "--stat", "--patch", "--format=", sha)
154func ShowPatch(dir, sha string, limit int64) (patch string, truncated bool, err error) {
155 cmd := exec.Command("git", "-C", dir, "show", "--stat", "--patch", "--format=", "--end-of-options", sha)
156156 stdout, err := cmd.StdoutPipe()
157157 if err != nil {
158 return "", err
158 return "", false, err
159159 }
160160 if err := cmd.Start(); err != nil {
161 return "", err
161 return "", false, err
162162 }
163 data, _ := io.ReadAll(io.LimitReader(stdout, limit))
163 // One byte past the limit says whether there was more.
164 data, _ := io.ReadAll(io.LimitReader(stdout, limit+1))
164165 io.Copy(io.Discard, stdout)
165166 if err := cmd.Wait(); err != nil {
166 return "", fmt.Errorf("show %s: %w", sha, err)
167 return "", false, fmt.Errorf("show %s: %w", sha, err)
167168 }
168 return string(data), nil
169 data, truncated = cutAtLine(data, limit)
170 return string(data), truncated, nil
169171}
170172
171173// IsBinary reports whether data looks like binary content.
internal/httpd/web.go +11 −5
@@ -1448,7 +1448,7 @@ func (s *Server) commit(w http.ResponseWriter, r *http.Request) {
14481448 s.notFound(w, r)
14491449 return
14501450 }
1451 patch, _ := gitutil.ShowPatch(p.Dir, full, 4<<20)
1451 patch, truncated, _ := gitutil.ShowPatch(p.Dir, full, 4<<20)
14521452 files := parseDiff(patch)
14531453 committerEmail := ""
14541454 if parsed.CommitterEmail != parsed.AuthorEmail {
@@ -1468,9 +1468,10 @@ func (s *Server) commit(w http.ResponseWriter, r *http.Request) {
14681468 Sig sigView
14691469 Checks []store.CommitStatus
14701470 DiffFiles []diffFile
1471 DiffTruncated bool
14711472 }{p, full, full[:10], commitNames.name(parsed.AuthorEmail, parsed.AuthorName), parsed.AuthorEmail, commitUser, committerEmail,
14721473 time.Unix(parsed.AuthorUnix, 0).UTC().Format(time.RFC3339), msg,
1473 gitutil.Parents(p.Dir, full), v, checks, files})
1474 gitutil.Parents(p.Dir, full), v, checks, files, truncated})
14741475}
14751476
14761477// labelPalette provides default label chip colors: mid-tone hues that stay
@@ -1661,9 +1662,10 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) {
16611662 base = b
16621663 }
16631664 }
1665 var diffTruncated bool
16641666 if base != "" {
1665 if patch, err := gitutil.Diff(p.Dir, base, headRef, 4<<20); err == nil {
1666 files = parseDiff(patch)
1667 if patch, truncated, err := gitutil.Diff(p.Dir, base, headRef, 4<<20); err == nil {
1668 files, diffTruncated = parseDiff(patch), truncated
16671669 }
16681670 }
16691671 md := s.ugcFor(r, p.Repo)
@@ -1682,9 +1684,11 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) {
16821684 }
16831685 mrNames := s.authorNames()
16841686 var commits []commitRow
1687 commitsTotal := 0
16851688 if base != "" {
16861689 const maxMRCommits = 100
16871690 shas, _ := gitutil.RevListRange(p.Dir, base, headRef)
1691 commitsTotal = len(shas)
16881692 if len(shas) > maxMRCommits {
16891693 shas = shas[:maxMRCommits]
16901694 }
@@ -1730,8 +1734,10 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) {
17301734 Comments []renderedComment
17311735 Reviews []store.MRReview
17321736 DiffFiles []diffFile
1737 DiffTruncated bool
17331738 Stat diffStat
17341739 Commits []commitRow
1740 CommitsTotal int
17351741 Branches []gitutil.Ref
17361742 CanEdit bool
17371743 CanWrite bool
@@ -1741,7 +1747,7 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) {
17411747 StackedOn *store.MR
17421748 Stacked []store.MR
17431749 }{p, m, view, md(m.Body, m.BodyFormat), checks, combined, renderComments(comments, md),
1744 reviews, files, stat, commits, branches, s.canEditItem(r, p.Repo, m.Author),
1750 reviews, files, diffTruncated, stat, commits, commitsTotal, branches, s.canEditItem(r, p.Repo, m.Author),
17451751 canWrite, unresolved, r.URL.Query().Get("e"), detachedThreads, stackedOn, stacked})
17461752}
17471753
internal/web/templates/commit.html +1
@@ -9,5 +9,6 @@
99 {{if .CommitterEmail}}<br>committer: &lt;{{.CommitterEmail}}&gt;{{end}}</p>
1010</div>
1111<pre class="message">{{.Message}}</pre>
12{{if .DiffTruncated}}<p class="error" role="alert">This patch is larger than 4 MiB and is cut off below. Clone the repository to see all of it.</p>{{end}}
1213{{template "difffiles" dict "Files" .DiffFiles "Base" "" "Viewer" ""}}
1314{{end}}
internal/web/templates/mr.html +3 −1
@@ -57,6 +57,7 @@
5757{{end}}
5858
5959{{else if eq .View "commits"}}
60{{if gt .CommitsTotal (len .Commits)}}<p class="meta">first {{len .Commits}} of {{.CommitsTotal}} commits; the rest are in the branch</p>{{end}}
6061<ul class="loglist">
6162{{range .Commits}}<li>
6263 <div class="commitmain">
@@ -73,7 +74,8 @@
7374</ul>
7475
7576{{else}}
76<p class="diffstat">{{.Stat.Files}} file{{if ne .Stat.Files 1}}s{{end}} changed, <span class="add">+{{.Stat.Adds}}</span> <span class="del">−{{.Stat.Dels}}</span></p>
77<p class="diffstat">{{.Stat.Files}} file{{if ne .Stat.Files 1}}s{{end}} changed, <span class="add">+{{.Stat.Adds}}</span> <span class="del">−{{.Stat.Dels}}</span>{{if .DiffTruncated}} · shown up to 4 MiB; the counts and the last file are partial{{end}}</p>
78{{if .DiffTruncated}}<p class="error" role="alert">This diff is larger than 4 MiB and is cut off below. Fetch the branch to see all of it.</p>{{end}}
7779{{template "difffiles" dict "Files" .DiffFiles "Base" $base "Viewer" .Viewer}}
7880{{end}}
7981