Improve web diff viewer
This commit is contained in:
@@ -48,7 +48,7 @@ CLI auth config defaults to:
|
|||||||
|
|
||||||
## Web UI
|
## 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
|
## Development
|
||||||
|
|
||||||
|
|||||||
+105
-3
@@ -104,9 +104,18 @@ type webCommit struct {
|
|||||||
AuthorEmail string
|
AuthorEmail string
|
||||||
Date time.Time
|
Date time.Time
|
||||||
Diff string
|
Diff string
|
||||||
|
DiffLines []webDiffLine
|
||||||
DiffTruncated bool
|
DiffTruncated bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type webDiffLine struct {
|
||||||
|
Index int
|
||||||
|
Kind string
|
||||||
|
OldLine string
|
||||||
|
NewLine string
|
||||||
|
Text string
|
||||||
|
}
|
||||||
|
|
||||||
type webCommitHistoryData struct {
|
type webCommitHistoryData struct {
|
||||||
Repo Repository
|
Repo Repository
|
||||||
Ref string
|
Ref string
|
||||||
@@ -148,7 +157,23 @@ var webTemplates = template.Must(template.New("web").Funcs(template.FuncMap{
|
|||||||
"shortHash": shortHash,
|
"shortHash": shortHash,
|
||||||
"truncate": truncateString,
|
"truncate": truncateString,
|
||||||
}).Parse(`{{define "layout"}}<!doctype html>
|
}).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>
|
<body>
|
||||||
<header style="margin-bottom: 1.5rem">
|
<header style="margin-bottom: 1.5rem">
|
||||||
<strong><a href="/repos">gitocean</a></strong>
|
<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>
|
<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}}
|
{{if $d.Commit.Body}}<pre style="white-space: pre-wrap">{{$d.Commit.Body}}</pre>{{end}}
|
||||||
<h2>Diff</h2>
|
<h2>Diff</h2>
|
||||||
{{if $d.Commit.DiffTruncated}}<p>Diff truncated.</p>{{end}}
|
<div class="diff-toolbar">
|
||||||
<pre style="white-space: pre-wrap">{{$d.Commit.Diff}}</pre>
|
{{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}}
|
{{end}}
|
||||||
|
|
||||||
{{define "File"}}
|
{{define "File"}}
|
||||||
@@ -1546,6 +1583,7 @@ func gitCommitDetail(repoPath, ref, id string) (webCommit, error) {
|
|||||||
return webCommit{}, err
|
return webCommit{}, err
|
||||||
}
|
}
|
||||||
commit.Diff = diff
|
commit.Diff = diff
|
||||||
|
commit.DiffLines = parseDiffLines(diff)
|
||||||
commit.DiffTruncated = truncated
|
commit.DiffTruncated = truncated
|
||||||
return commit, nil
|
return commit, nil
|
||||||
}
|
}
|
||||||
@@ -1617,6 +1655,70 @@ func gitCommitDiff(repoPath, full string) (string, bool, error) {
|
|||||||
return string(out), truncated, nil
|
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) {
|
func gitReadBlob(repoPath, ref, p string) (string, error) {
|
||||||
out, err := gitReadBlobBytes(repoPath, ref, p)
|
out, err := gitReadBlobBytes(repoPath, ref, p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ func TestWebRepoPagesCreateEditAndForkRules(t *testing.T) {
|
|||||||
expectLoadRepo(mock, "alice", "demo", repo)
|
expectLoadRepo(mock, "alice", "demo", repo)
|
||||||
rr = httptest.NewRecorder()
|
rr = httptest.NewRecorder()
|
||||||
s.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/alice/demo/commit?ref=main&id="+mainHash, nil))
|
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())
|
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:
|
Current default coverage snapshot after Phase 4 implementation:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
internal/app 60.8%
|
internal/app 61.0%
|
||||||
internal/backup 42.3%
|
internal/backup 42.3%
|
||||||
internal/config 79.5%
|
internal/config 79.5%
|
||||||
internal/dbutil 8.6%
|
internal/dbutil 8.6%
|
||||||
|
|||||||
Reference in New Issue
Block a user