Commit ddab521653
Verified · cmc ci/build: success ci/test: success ci/vuln: success
Layout: unified · split
internal/control/mr.go +4 −1
| @@ -548,11 +548,14 @@ func runMRDiff(c *Ctx, args []string) int { | |||
| 548 | } | 548 | } |
| 549 | base = b | 549 | base = b |
| 550 | } | 550 | } |
| 551 | patch, err := gitutil.Diff(dir, base, head, 4<<20) | 551 | patch, truncated, err := gitutil.Diff(dir, base, head, 4<<20) |
| 552 | if err != nil { | 552 | if err != nil { |
| 553 | return c.fail(protocol.ExitFailure, "%v", err) | 553 | return c.fail(protocol.ExitFailure, "%v", err) |
| 554 | } | 554 | } |
| 555 | fmt.Fprint(c.Stdout, patch) | 555 | 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 | } | ||
| 556 | return protocol.ExitOK | 559 | return protocol.ExitOK |
| 557 | } | 560 | } |
| 558 | 561 | ||
internal/control/sig.go +4 −1
| @@ -246,10 +246,13 @@ func runRepoCommit(c *Ctx, args []string) int { | |||
| 246 | if err != nil { | 246 | if err != nil { |
| 247 | return c.fail(protocol.ExitFailure, "verifying %s: %v", full, err) | 247 | return c.fail(protocol.ExitFailure, "verifying %s: %v", full, err) |
| 248 | } | 248 | } |
| 249 | patch, err := gitutil.ShowPatch(dir, full, 4<<20) | 249 | patch, truncated, err := gitutil.ShowPatch(dir, full, 4<<20) |
| 250 | if err != nil { | 250 | if err != nil { |
| 251 | return c.fail(protocol.ExitFailure, "%v", err) | 251 | return c.fail(protocol.ExitFailure, "%v", err) |
| 252 | } | 252 | } |
| 253 | if truncated { | ||
| 254 | fmt.Fprintln(c.Stderr, "patch truncated at 4 MiB; clone the repository for the rest") | ||
| 255 | } | ||
| 253 | statuses, err := c.Store.ListCommitStatuses(repo.ID, full) | 256 | statuses, err := c.Store.ListCommitStatuses(repo.ID, full) |
| 254 | if err != nil { | 257 | if err != nil { |
| 255 | return c.fail(protocol.ExitFailure, "%v", err) | 258 | return c.fail(protocol.ExitFailure, "%v", err) |
internal/gitutil/endofoptions_test.go +1 −1
| @@ -35,7 +35,7 @@ func TestRefsAreNotOptions(t *testing.T) { | |||
| 35 | "Archive": func() error { return Archive(dir, ref, "x", &sink) }, | 35 | "Archive": func() error { return Archive(dir, ref, "x", &sink) }, |
| 36 | "Grep": func() error { _, err := Grep(dir, ref, "hi", 10); return err }, | 36 | "Grep": func() error { _, err := Grep(dir, ref, "hi", 10); return err }, |
| 37 | "MergeBase": func() error { _, err := MergeBase(dir, ref, "main"); return err }, | 37 | "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 }, |
| 39 | "DiffFiles": func() error { _, err := DiffFiles(dir, ref, "main"); return err }, | 39 | "DiffFiles": func() error { _, err := DiffFiles(dir, ref, "main"); return err }, |
| 40 | "RevListRange": func() error { _, err := RevListRange(dir, "main", ref); return err }, | 40 | "RevListRange": func() error { _, err := RevListRange(dir, "main", ref); return err }, |
| 41 | "Blame": func() error { _, err := Blame(dir, ref, "f.txt", 1, 1); return err }, | 41 | "Blame": func() error { _, err := Blame(dir, ref, "f.txt", 1, 1); return err }, |
internal/gitutil/merge.go +20 −6
| @@ -1,6 +1,7 @@ | |||
| 1 | package gitutil | 1 | package gitutil |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "bytes" | ||
| 4 | "fmt" | 5 | "fmt" |
| 5 | "os" | 6 | "os" |
| 6 | "os/exec" | 7 | "os/exec" |
| @@ -93,17 +94,30 @@ func CommitTree(dir, tree string, parents []string, name, email, message string) | |||
| 93 | } | 94 | } |
| 94 | 95 | ||
| 95 | // Diff returns the patch for old..new (three-dot semantics are the caller's | 96 | // Diff returns the patch for old..new (three-dot semantics are the caller's |
| 96 | // job: pass the merge base as old). | 97 | // job: pass the merge base as old), cut at limit bytes on a line boundary; |
| 97 | func Diff(dir, old, new string, limit int64) (string, error) { | 98 | // truncated says whether it was cut, so the caller can say so instead of |
| 99 | // rendering a hunk that ends mid-line (#117). | ||
| 100 | func Diff(dir, old, new string, limit int64) (patch string, truncated bool, err error) { | ||
| 98 | cmd := exec.Command("git", "-C", dir, "diff", "--stat", "--patch", "--end-of-options", old, new) | 101 | cmd := exec.Command("git", "-C", dir, "diff", "--stat", "--patch", "--end-of-options", old, new) |
| 99 | out, err := cmd.Output() | 102 | out, err := cmd.Output() |
| 100 | if err != nil { | 103 | if err != nil { |
| 101 | return "", fmt.Errorf("diff: %w", err) | 104 | return "", false, fmt.Errorf("diff: %w", err) |
| 102 | } | 105 | } |
| 103 | if int64(len(out)) > limit { | 106 | out, truncated = cutAtLine(out, limit) |
| 104 | out = 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. | ||
| 112 | func 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] | ||
| 105 | } | 119 | } |
| 106 | return string(out), nil | 120 | return cut, true |
| 107 | } | 121 | } |
| 108 | 122 | ||
| 109 | // MergeBase returns the best common ancestor, or an error if none exists. | 123 | // 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) { | |||
| 151 | } | 151 | } |
| 152 | 152 | ||
| 153 | // ShowPatch returns the stat+patch text for one commit. | 153 | // ShowPatch returns the stat+patch text for one commit. |
| 154 | func ShowPatch(dir, sha string, limit int64) (string, error) { | 154 | func ShowPatch(dir, sha string, limit int64) (patch string, truncated bool, err error) { |
| 155 | cmd := exec.Command("git", "-C", dir, "show", "--stat", "--patch", "--format=", sha) | 155 | cmd := exec.Command("git", "-C", dir, "show", "--stat", "--patch", "--format=", "--end-of-options", sha) |
| 156 | stdout, err := cmd.StdoutPipe() | 156 | stdout, err := cmd.StdoutPipe() |
| 157 | if err != nil { | 157 | if err != nil { |
| 158 | return "", err | 158 | return "", false, err |
| 159 | } | 159 | } |
| 160 | if err := cmd.Start(); err != nil { | 160 | if err := cmd.Start(); err != nil { |
| 161 | return "", err | 161 | return "", false, err |
| 162 | } | 162 | } |
| 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)) | ||
| 164 | io.Copy(io.Discard, stdout) | 165 | io.Copy(io.Discard, stdout) |
| 165 | if err := cmd.Wait(); err != nil { | 166 | 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) |
| 167 | } | 168 | } |
| 168 | return string(data), nil | 169 | data, truncated = cutAtLine(data, limit) |
| 170 | return string(data), truncated, nil | ||
| 169 | } | 171 | } |
| 170 | 172 | ||
| 171 | // IsBinary reports whether data looks like binary content. | 173 | // 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) { | |||
| 1448 | s.notFound(w, r) | 1448 | s.notFound(w, r) |
| 1449 | return | 1449 | return |
| 1450 | } | 1450 | } |
| 1451 | patch, _ := gitutil.ShowPatch(p.Dir, full, 4<<20) | 1451 | patch, truncated, _ := gitutil.ShowPatch(p.Dir, full, 4<<20) |
| 1452 | files := parseDiff(patch) | 1452 | files := parseDiff(patch) |
| 1453 | committerEmail := "" | 1453 | committerEmail := "" |
| 1454 | if parsed.CommitterEmail != parsed.AuthorEmail { | 1454 | if parsed.CommitterEmail != parsed.AuthorEmail { |
| @@ -1468,9 +1468,10 @@ func (s *Server) commit(w http.ResponseWriter, r *http.Request) { | |||
| 1468 | Sig sigView | 1468 | Sig sigView |
| 1469 | Checks []store.CommitStatus | 1469 | Checks []store.CommitStatus |
| 1470 | DiffFiles []diffFile | 1470 | DiffFiles []diffFile |
| 1471 | DiffTruncated bool | ||
| 1471 | }{p, full, full[:10], commitNames.name(parsed.AuthorEmail, parsed.AuthorName), parsed.AuthorEmail, commitUser, committerEmail, | 1472 | }{p, full, full[:10], commitNames.name(parsed.AuthorEmail, parsed.AuthorName), parsed.AuthorEmail, commitUser, committerEmail, |
| 1472 | time.Unix(parsed.AuthorUnix, 0).UTC().Format(time.RFC3339), msg, | 1473 | 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}) |
| 1474 | } | 1475 | } |
| 1475 | 1476 | ||
| 1476 | // labelPalette provides default label chip colors: mid-tone hues that stay | 1477 | // 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) { | |||
| 1661 | base = b | 1662 | base = b |
| 1662 | } | 1663 | } |
| 1663 | } | 1664 | } |
| 1665 | var diffTruncated bool | ||
| 1664 | if base != "" { | 1666 | if base != "" { |
| 1665 | if patch, err := gitutil.Diff(p.Dir, base, headRef, 4<<20); err == nil { | 1667 | if patch, truncated, err := gitutil.Diff(p.Dir, base, headRef, 4<<20); err == nil { |
| 1666 | files = parseDiff(patch) | 1668 | files, diffTruncated = parseDiff(patch), truncated |
| 1667 | } | 1669 | } |
| 1668 | } | 1670 | } |
| 1669 | md := s.ugcFor(r, p.Repo) | 1671 | md := s.ugcFor(r, p.Repo) |
| @@ -1682,9 +1684,11 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) { | |||
| 1682 | } | 1684 | } |
| 1683 | mrNames := s.authorNames() | 1685 | mrNames := s.authorNames() |
| 1684 | var commits []commitRow | 1686 | var commits []commitRow |
| 1687 | commitsTotal := 0 | ||
| 1685 | if base != "" { | 1688 | if base != "" { |
| 1686 | const maxMRCommits = 100 | 1689 | const maxMRCommits = 100 |
| 1687 | shas, _ := gitutil.RevListRange(p.Dir, base, headRef) | 1690 | shas, _ := gitutil.RevListRange(p.Dir, base, headRef) |
| 1691 | commitsTotal = len(shas) | ||
| 1688 | if len(shas) > maxMRCommits { | 1692 | if len(shas) > maxMRCommits { |
| 1689 | shas = shas[:maxMRCommits] | 1693 | shas = shas[:maxMRCommits] |
| 1690 | } | 1694 | } |
| @@ -1730,8 +1734,10 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) { | |||
| 1730 | Comments []renderedComment | 1734 | Comments []renderedComment |
| 1731 | Reviews []store.MRReview | 1735 | Reviews []store.MRReview |
| 1732 | DiffFiles []diffFile | 1736 | DiffFiles []diffFile |
| 1737 | DiffTruncated bool | ||
| 1733 | Stat diffStat | 1738 | Stat diffStat |
| 1734 | Commits []commitRow | 1739 | Commits []commitRow |
| 1740 | CommitsTotal int | ||
| 1735 | Branches []gitutil.Ref | 1741 | Branches []gitutil.Ref |
| 1736 | CanEdit bool | 1742 | CanEdit bool |
| 1737 | CanWrite bool | 1743 | CanWrite bool |
| @@ -1741,7 +1747,7 @@ func (s *Server) mr(w http.ResponseWriter, r *http.Request) { | |||
| 1741 | StackedOn *store.MR | 1747 | StackedOn *store.MR |
| 1742 | Stacked []store.MR | 1748 | Stacked []store.MR |
| 1743 | }{p, m, view, md(m.Body, m.BodyFormat), checks, combined, renderComments(comments, md), | 1749 | }{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), |
| 1745 | canWrite, unresolved, r.URL.Query().Get("e"), detachedThreads, stackedOn, stacked}) | 1751 | canWrite, unresolved, r.URL.Query().Get("e"), detachedThreads, stackedOn, stacked}) |
| 1746 | } | 1752 | } |
| 1747 | 1753 | ||
internal/web/templates/commit.html +1
| @@ -9,5 +9,6 @@ | |||
| 9 | {{if .CommitterEmail}}<br>committer: <{{.CommitterEmail}}>{{end}}</p> | 9 | {{if .CommitterEmail}}<br>committer: <{{.CommitterEmail}}>{{end}}</p> |
| 10 | </div> | 10 | </div> |
| 11 | <pre class="message">{{.Message}}</pre> | 11 | <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}} | ||
| 12 | {{template "difffiles" dict "Files" .DiffFiles "Base" "" "Viewer" ""}} | 13 | {{template "difffiles" dict "Files" .DiffFiles "Base" "" "Viewer" ""}} |
| 13 | {{end}} | 14 | {{end}} |
internal/web/templates/mr.html +3 −1
| @@ -57,6 +57,7 @@ | |||
| 57 | {{end}} | 57 | {{end}} |
| 58 | 58 | ||
| 59 | {{else if eq .View "commits"}} | 59 | {{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}} | ||
| 60 | <ul class="loglist"> | 61 | <ul class="loglist"> |
| 61 | {{range .Commits}}<li> | 62 | {{range .Commits}}<li> |
| 62 | <div class="commitmain"> | 63 | <div class="commitmain"> |
| @@ -73,7 +74,8 @@ | |||
| 73 | </ul> | 74 | </ul> |
| 74 | 75 | ||
| 75 | {{else}} | 76 | {{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}} | ||
| 77 | {{template "difffiles" dict "Files" .DiffFiles "Base" $base "Viewer" .Viewer}} | 79 | {{template "difffiles" dict "Files" .DiffFiles "Base" $base "Viewer" .Viewer}} |
| 78 | {{end}} | 80 | {{end}} |
| 79 | 81 | ||