Improve web diff viewer
This commit is contained in:
@@ -48,7 +48,7 @@ CLI auth config defaults to:
|
||||
|
||||
## Web UI
|
||||
|
||||
The web UI is intentionally minimal and server-rendered. It supports repository search, user profile pages with public repositories, login/register/logout, repository creation, branch selection, a branches page for branch creation/deletion, selected-branch latest commit and commit count status, commit history pages, single commit pages with limited diff rendering, repository file browsing, Markdown rendering, image rendering for common image files, plain-text file viewing, simple textarea-based file editing with commit messages for users with write access, pull request list/create/view/comment/close/merge workflows, and forking public repositories. Private repositories cannot be forked. Branch renaming is intentionally unsupported in both the API and web UI.
|
||||
The web UI is intentionally minimal and server-rendered. It supports repository search, user profile pages with public repositories, login/register/logout, repository creation, branch selection, a branches page for branch creation/deletion, selected-branch latest commit and commit count status, commit history pages, single commit pages with a line-numbered diff viewer and limited diff rendering, repository file browsing, Markdown rendering, image rendering for common image files, plain-text file viewing, simple textarea-based file editing with commit messages for users with write access, pull request list/create/view/comment/close/merge workflows, and forking public repositories. Private repositories cannot be forked. Branch renaming is intentionally unsupported in both the API and web UI.
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
+105
-3
@@ -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}} <{{$d.Commit.AuthorEmail}}> 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 {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ go test -cover ./...
|
||||
Current default coverage snapshot after Phase 4 implementation:
|
||||
|
||||
```text
|
||||
internal/app 60.8%
|
||||
internal/app 61.0%
|
||||
internal/backup 42.3%
|
||||
internal/config 79.5%
|
||||
internal/dbutil 8.6%
|
||||
|
||||
Reference in New Issue
Block a user