Improve web diff viewer

This commit is contained in:
2026-06-08 15:34:03 -05:00
parent f2d8743b6b
commit af5eac897f
4 changed files with 108 additions and 6 deletions
+105 -3
View File
@@ -104,9 +104,18 @@ type webCommit struct {
AuthorEmail string
Date time.Time
Diff string
DiffLines []webDiffLine
DiffTruncated bool
}
type webDiffLine struct {
Index int
Kind string
OldLine string
NewLine string
Text string
}
type webCommitHistoryData struct {
Repo Repository
Ref string
@@ -148,7 +157,23 @@ var webTemplates = template.Must(template.New("web").Funcs(template.FuncMap{
"shortHash": shortHash,
"truncate": truncateString,
}).Parse(`{{define "layout"}}<!doctype html>
<html><head><meta charset="utf-8"><title>{{.Title}} - gitocean</title></head>
<html><head><meta charset="utf-8"><title>{{.Title}} - gitocean</title>
<style>
.diff-toolbar { margin: 1rem 0 .5rem; color: #444; }
.diff-shell { border: 1px solid #d0d7de; border-radius: 6px; overflow: auto; background: #fff; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; line-height: 1.45; }
.diff-line { display: grid; grid-template-columns: 4.5rem 4.5rem max-content; min-width: 100%; }
.diff-num { padding: 0 .7rem; text-align: right; color: #57606a; background: #f6f8fa; border-right: 1px solid #d0d7de; user-select: none; text-decoration: none; }
.diff-code { margin: 0; padding: 0 .8rem; white-space: pre; tab-size: 4; font: inherit; }
.diff-line:hover .diff-num, .diff-line:hover .diff-code { background: #fff8c5; }
.diff-add .diff-code { background: #e6ffec; }
.diff-add .diff-num { background: #ccffd8; }
.diff-del .diff-code { background: #ffebe9; }
.diff-del .diff-num { background: #ffd7d5; }
.diff-hunk .diff-code, .diff-hunk .diff-num { background: #ddf4ff; color: #0550ae; }
.diff-file { position: sticky; top: 0; z-index: 1; }
.diff-file .diff-code, .diff-file .diff-num { background: #f6f8fa; font-weight: 700; }
.diff-meta .diff-code, .diff-meta .diff-num { background: #f6f8fa; color: #57606a; }
</style></head>
<body>
<header style="margin-bottom: 1.5rem">
<strong><a href="/repos">gitocean</a></strong>
@@ -314,8 +339,20 @@ var webTemplates = template.Must(template.New("web").Funcs(template.FuncMap{
<p>{{$d.Commit.AuthorName}} &lt;{{$d.Commit.AuthorEmail}}&gt; authored {{$d.Commit.Date}}</p>
{{if $d.Commit.Body}}<pre style="white-space: pre-wrap">{{$d.Commit.Body}}</pre>{{end}}
<h2>Diff</h2>
{{if $d.Commit.DiffTruncated}}<p>Diff truncated.</p>{{end}}
<pre style="white-space: pre-wrap">{{$d.Commit.Diff}}</pre>
<div class="diff-toolbar">
{{if $d.Commit.DiffTruncated}}Diff truncated. Showing the first diff chunk only.{{else}}Review changes with line numbers.{{end}}
</div>
{{if $d.Commit.DiffLines}}
<div class="diff-shell" role="table" aria-label="Commit diff">
{{range $d.Commit.DiffLines}}
<div class="diff-line diff-{{.Kind}}" id="D{{.Index}}" role="row">
<a class="diff-num" href="#D{{.Index}}" aria-label="old line {{.OldLine}}">{{.OldLine}}</a>
<a class="diff-num" href="#D{{.Index}}" aria-label="new line {{.NewLine}}">{{.NewLine}}</a>
<pre class="diff-code">{{.Text}}</pre>
</div>
{{end}}
</div>
{{else}}<p>No diff for this commit.</p>{{end}}
{{end}}
{{define "File"}}
@@ -1546,6 +1583,7 @@ func gitCommitDetail(repoPath, ref, id string) (webCommit, error) {
return webCommit{}, err
}
commit.Diff = diff
commit.DiffLines = parseDiffLines(diff)
commit.DiffTruncated = truncated
return commit, nil
}
@@ -1617,6 +1655,70 @@ func gitCommitDiff(repoPath, full string) (string, bool, error) {
return string(out), truncated, nil
}
func parseDiffLines(diff string) []webDiffLine {
if diff == "" {
return nil
}
lines := strings.Split(diff, "\n")
if len(lines) > 0 && lines[len(lines)-1] == "" {
lines = lines[:len(lines)-1]
}
oldLine, newLine := 0, 0
out := make([]webDiffLine, 0, len(lines))
for i, line := range lines {
entry := webDiffLine{Index: i + 1, Kind: "context", Text: line}
switch {
case strings.HasPrefix(line, "diff --git"):
oldLine, newLine = 0, 0
entry.Kind = "file"
case strings.HasPrefix(line, "@@"):
oldLine, newLine = parseDiffHunkHeader(line)
entry.Kind = "hunk"
case strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++"):
entry.Kind = "add"
entry.NewLine = strconv.Itoa(newLine)
newLine++
case strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---"):
entry.Kind = "del"
entry.OldLine = strconv.Itoa(oldLine)
oldLine++
case strings.HasPrefix(line, "index ") || strings.HasPrefix(line, "---") || strings.HasPrefix(line, "+++") || strings.HasPrefix(line, "\\"):
entry.Kind = "meta"
case oldLine > 0 || newLine > 0:
entry.Kind = "context"
entry.OldLine = strconv.Itoa(oldLine)
entry.NewLine = strconv.Itoa(newLine)
oldLine++
newLine++
default:
entry.Kind = "meta"
}
out = append(out, entry)
}
return out
}
func parseDiffHunkHeader(line string) (int, int) {
oldStart, newStart := 0, 0
for _, field := range strings.Fields(line) {
if strings.HasPrefix(field, "-") {
oldStart = parseDiffRangeStart(strings.TrimPrefix(field, "-"))
}
if strings.HasPrefix(field, "+") {
newStart = parseDiffRangeStart(strings.TrimPrefix(field, "+"))
}
}
return oldStart, newStart
}
func parseDiffRangeStart(s string) int {
if before, _, ok := strings.Cut(s, ","); ok {
s = before
}
n, _ := strconv.Atoi(s)
return n
}
func gitReadBlob(repoPath, ref, p string) (string, error) {
out, err := gitReadBlobBytes(repoPath, ref, p)
if err != nil {
+1 -1
View File
@@ -150,7 +150,7 @@ func TestWebRepoPagesCreateEditAndForkRules(t *testing.T) {
expectLoadRepo(mock, "alice", "demo", repo)
rr = httptest.NewRecorder()
s.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/alice/demo/commit?ref=main&id="+mainHash, nil))
if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "Commit ") || !strings.Contains(rr.Body.String(), "add render files") || !strings.Contains(rr.Body.String(), "diff --git") || !strings.Contains(rr.Body.String(), "docs.md") {
if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "Commit ") || !strings.Contains(rr.Body.String(), "add render files") || !strings.Contains(rr.Body.String(), "diff-shell") || !strings.Contains(rr.Body.String(), "diff-file") || !strings.Contains(rr.Body.String(), "diff-add") || !strings.Contains(rr.Body.String(), "diff --git") || !strings.Contains(rr.Body.String(), "docs.md") {
t.Fatalf("commit view status=%d body=%s", rr.Code, rr.Body.String())
}