11 files changed,
508 insertions(+),
130 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed by:
GitHub
noreply@github.com
Committed at:
2026-05-07 19:10:07 +0300
Parent:
7218955
jump to
A
internal/git/compare.go
路路路 1 +package git 2 + 3 +import ( 4 + "errors" 5 + "fmt" 6 + "strconv" 7 + "strings" 8 + 9 + "github.com/go-git/go-git/v5/plumbing" 10 +) 11 + 12 +type Compare struct { 13 + BaseRef, BaseHash string 14 + HeadRef, HeadHash string 15 + MergeBase string 16 + Ahead int 17 + Behind int 18 + Commits []*Commit 19 + Diff *NiceDiff 20 +} 21 + 22 +func (g *Repo) Compare(baseRef, headRef string) (*Compare, error) { 23 + if baseRef == "" || headRef == "" { 24 + return nil, errors.New("base and head refs can not be empty") 25 + } 26 + 27 + baseHash, err := g.resolveRef(baseRef) 28 + if err != nil { 29 + return nil, fmt.Errorf("resolving base ref %q: %w", baseRef, err) 30 + } 31 + 32 + headHash, err := g.resolveRef(headRef) 33 + if err != nil { 34 + return nil, fmt.Errorf("resolving head ref %q: %w", headRef, err) 35 + } 36 + 37 + mergeBaseOut, err := g.mergeBase(baseHash.String(), headHash.String()) 38 + if err != nil { 39 + return nil, fmt.Errorf("merge-base for %q and %q: %w", baseRef, headRef, err) 40 + } 41 + 42 + mergeBase := strings.TrimSpace(string(mergeBaseOut)) 43 + if mergeBase == "" { 44 + return nil, fmt.Errorf("merge-base for %q and %q: empty output", baseRef, headRef) 45 + } 46 + 47 + countsOut, err := g.revList("--left-right", "--count", fmt.Sprintf("%s...%s", baseHash.String(), headHash.String())) 48 + if err != nil { 49 + return nil, fmt.Errorf("ahead/behind for %q and %q: %w", baseRef, headRef, err) 50 + } 51 + 52 + behind, ahead, err := parseAheadBehind(countsOut) 53 + if err != nil { 54 + return nil, err 55 + } 56 + 57 + commits, err := g.commitsInRange(baseHash, headHash) 58 + if err != nil { 59 + return nil, err 60 + } 61 + 62 + diff, err := g.diffBetween(plumbing.NewHash(mergeBase), headHash) 63 + if err != nil { 64 + return nil, err 65 + } 66 + 67 + return &Compare{ 68 + BaseRef: baseRef, 69 + HeadRef: headRef, 70 + BaseHash: baseHash.String(), 71 + HeadHash: headHash.String(), 72 + MergeBase: mergeBase, 73 + Ahead: ahead, 74 + Behind: behind, 75 + Commits: commits, 76 + Diff: diff, 77 + }, nil 78 +} 79 + 80 +func (g *Repo) resolveRef(ref string) (plumbing.Hash, error) { 81 + hash, err := g.r.ResolveRevision(plumbing.Revision(ref)) 82 + if err != nil { 83 + return plumbing.ZeroHash, err 84 + } 85 + return *hash, nil 86 +} 87 + 88 +func parseAheadBehind(counts []byte) (behind, ahead int, err error) { 89 + fields := strings.Fields(strings.TrimSpace(string(counts))) 90 + if len(fields) != 2 { 91 + return 0, 0, fmt.Errorf("unexpected ahead/behind format: %q", counts) 92 + } 93 + 94 + behind, err = strconv.Atoi(fields[0]) 95 + if err != nil { 96 + return 0, 0, fmt.Errorf("invalid behind count %q: %w", fields[0], err) 97 + } 98 + 99 + ahead, err = strconv.Atoi(fields[1]) 100 + if err != nil { 101 + return 0, 0, fmt.Errorf("invalid ahead count %q: %w", fields[1], err) 102 + } 103 + 104 + return behind, ahead, nil 105 +} 106 + 107 +func (g *Repo) commitsInRange(base, head plumbing.Hash) ([]*Commit, error) { 108 + out, err := g.runGitCmd("log", "--format=%H", fmt.Sprintf("%s..%s", base.String(), head.String())) 109 + if err != nil { 110 + return nil, fmt.Errorf("commits in range %s..%s: %w", base, head, err) 111 + } 112 + 113 + lines := strings.Split(strings.TrimSpace(string(out)), "\n") 114 + if len(lines) == 1 && lines[0] == "" { 115 + return []*Commit{}, nil 116 + } 117 + 118 + commits := make([]*Commit, 0, len(lines)) 119 + for _, hash := range lines { 120 + hash = strings.TrimSpace(hash) 121 + if hash == "" { 122 + continue 123 + } 124 + 125 + c, err := g.r.CommitObject(plumbing.NewHash(hash)) 126 + if err != nil { 127 + return nil, fmt.Errorf("commit object %s: %w", hash, err) 128 + } 129 + commits = append(commits, newCommit(c)) 130 + } 131 + return commits, nil 132 +} 133 + 134 +func (g *Repo) revList(args ...string) ([]byte, error) { 135 + return g.runGitCmd("rev-list", args...) 136 +} 137 + 138 +func (g *Repo) mergeBase(args ...string) ([]byte, error) { 139 + return g.runGitCmd("merge-base", args...) 140 +}
A
internal/git/compare_test.go
路路路 1 +package git 2 + 3 +import ( 4 + "testing" 5 + 6 + "olexsmir.xyz/x/is" 7 +) 8 + 9 +func TestRepo_Compare(t *testing.T) { 10 + t.Run("compares two refs", func(t *testing.T) { 11 + r := newTestRepo(t) 12 + base := r.commitFile("README.md", "base\n", "base commit") 13 + r.createBranch("develop", base) 14 + 15 + r.commitFile("master.txt", "master only\n", "master change") 16 + r.checkoutBranch("develop", false) 17 + r.commitFile("develop.txt", "develop only\n", "develop change") 18 + 19 + cmp, err := r.open().Compare("master", "develop") 20 + is.Err(t, err, nil) 21 + is.Equal(t, cmp.BaseRef, "master") 22 + is.Equal(t, cmp.HeadRef, "develop") 23 + is.Equal(t, cmp.Behind, 1) 24 + is.Equal(t, cmp.Ahead, 1) 25 + is.Equal(t, cmp.MergeBase, base.String()) 26 + is.Equal(t, len(cmp.Commits), 1) 27 + is.Equal(t, cmp.Commits[0].Message, "develop change") 28 + is.Equal(t, cmp.Diff.Stat.FilesChanged, 1) 29 + is.Equal(t, cmp.Diff.Diff[0].Name.New, "develop.txt") 30 + }) 31 + 32 + t.Run("returns empty range when refs are equal", func(t *testing.T) { 33 + r := newTestRepo(t) 34 + r.commitFile("README.md", "base\n", "base commit") 35 + 36 + cmp, err := r.open().Compare("master", "master") 37 + is.Err(t, err, nil) 38 + is.Equal(t, cmp.Behind, 0) 39 + is.Equal(t, cmp.Ahead, 0) 40 + is.Equal(t, len(cmp.Commits), 0) 41 + is.Equal(t, cmp.Diff.Stat.FilesChanged, 0) 42 + }) 43 + 44 + t.Run("fails on invalid ref", func(t *testing.T) { 45 + r := newTestRepo(t) 46 + r.commitFile("README.md", "base\n", "base commit") 47 + 48 + _, err := r.open().Compare("master", "does-not-exist") 49 + is.Err(t, err, "resolving head ref") 50 + }) 51 +}
M
internal/git/diff.go
路路路 5 5 "strings" 6 6 7 7 "github.com/bluekeyes/go-gitdiff/gitdiff" 8 + "github.com/go-git/go-git/v5/plumbing" 8 9 "github.com/go-git/go-git/v5/plumbing/object" 9 10 ) 10 11 路路路 49 50 return nil, err 50 51 } 51 52 52 - diffs, _, err := gitdiff.Parse(strings.NewReader(patch.String())) 53 + nd, err := parseNiceDiff(patch.String()) 54 + if err != nil { 55 + return nil, err 56 + } 57 + nd.Commit = newCommit(c) 58 + nd.Parents = parents 59 + return nd, nil 60 +} 61 + 62 +func parseNiceDiff(patch string) (*NiceDiff, error) { 63 + nd := &NiceDiff{} 64 + if strings.TrimSpace(patch) == "" { 65 + return nd, nil 66 + } 67 + 68 + diffs, _, err := gitdiff.Parse(strings.NewReader(patch)) 53 69 if err != nil { 54 70 return nil, fmt.Errorf("parsing diff: %w", err) 55 71 } 56 72 57 - nd := NiceDiff{} 58 - nd.Commit = newCommit(c) 59 - nd.Parents = parents 60 73 nd.Stat.FilesChanged = len(diffs) 61 74 nd.Diff = make([]Diff, len(diffs)) 62 75 for i, d := range diffs { 路路路 87 100 } 88 101 } 89 102 } 90 - return &nd, nil 103 + return nd, nil 91 104 } 92 105 93 106 func (g *Repo) getPatch(c *object.Commit) (*object.Patch, []string, error) { 路路路 119 132 120 133 return patch, parents, nil 121 134 } 135 + 136 +func (g *Repo) diffBetween(base, head plumbing.Hash) (*NiceDiff, error) { 137 + baseCommit, err := g.r.CommitObject(base) 138 + if err != nil { 139 + return nil, fmt.Errorf("base commit object %s: %w", base, err) 140 + } 141 + headCommit, err := g.r.CommitObject(head) 142 + if err != nil { 143 + return nil, fmt.Errorf("head commit object %s: %w", head, err) 144 + } 145 + 146 + baseTree, err := baseCommit.Tree() 147 + if err != nil { 148 + return nil, fmt.Errorf("base tree %s: %w", base, err) 149 + } 150 + headTree, err := headCommit.Tree() 151 + if err != nil { 152 + return nil, fmt.Errorf("head tree %s: %w", head, err) 153 + } 154 + 155 + patch, err := baseTree.Patch(headTree) 156 + if err != nil { 157 + return nil, fmt.Errorf("tree patch %s..%s: %w", base, head, err) 158 + } 159 + 160 + diff, err := parseNiceDiff(patch.String()) 161 + if err != nil { 162 + return nil, fmt.Errorf("parse tree diff %s..%s: %w", base, head, err) 163 + } 164 + return diff, nil 165 +}
M
internal/git/testutil_test.go
路路路 91 91 is.Err(t.tb, t.r.Storer.SetReference(ref), nil) 92 92 } 93 93 94 +func (t *testRepo) checkoutBranch(name string, create bool) { 95 + t.tb.Helper() 96 + 97 + wt, err := t.r.Worktree() 98 + is.Err(t.tb, err, nil) 99 + 100 + err = wt.Checkout(&git.CheckoutOptions{ 101 + Branch: plumbing.NewBranchReferenceName(name), 102 + Create: create, 103 + }) 104 + is.Err(t.tb, err, nil) 105 +} 106 + 94 107 func (t *testRepo) createTag(name string, hash plumbing.Hash) { 95 108 t.tb.Helper() 96 109 ref := plumbing.NewHashReference(plumbing.NewTagReferenceName(name), hash)
M
internal/handlers/handlers.go
路路路 1 1 package handlers 2 2 3 3 import ( 4 + "errors" 4 5 "html/template" 5 6 "net/http" 6 7 "net/url" 路路路 49 50 mux.HandleFunc("GET /{name}/raw/{ref}/{rest...}", h.rawFileContentsHandler) 50 51 mux.HandleFunc("GET /{name}/log/{ref}", h.logHandler) 51 52 mux.HandleFunc("GET /{name}/commit/{ref}", h.commitHandler) 53 + mux.HandleFunc("GET /{name}/compare/{ref1}/{ref2}", h.compareHandler) 52 54 mux.HandleFunc("GET /{name}/refs/{$}", h.refsHandler) 53 55 mux.HandleFunc("GET /{name}/archive/{ref}", h.archiveHandler) 54 56 路路路 78 80 "humanizeRelTime": humanize.Time, 79 81 "urlencode": url.PathEscape, 80 82 "commitSummary": commitSummary, 83 + "dict": dict, 81 84 } 82 85 83 86 func commitSummary(commitMsg string) string { 路路路 95 98 96 99 return first 97 100 } 101 + 102 +func dict(v ...any) (map[string]any, error) { 103 + if len(v)%2 != 0 { 104 + return nil, errors.New("dict requires an even number of arguments") 105 + } 106 + 107 + out := make(map[string]any, len(v)/2) 108 + for i := 0; i < len(v); i += 2 { 109 + key, ok := v[i].(string) 110 + if !ok { 111 + return nil, errors.New("dict keys must be strings") 112 + } 113 + out[key] = v[i+1] 114 + } 115 + return out, nil 116 +}
M
internal/handlers/repo.go
路路路 331 331 })) 332 332 } 333 333 334 +type RepoCompare struct { 335 + Desc string 336 + Ref string 337 + Compare *git.Compare 338 +} 339 + 340 +func (h *handlers) compareHandler(w http.ResponseWriter, r *http.Request) { 341 + name := r.PathValue("name") 342 + ref1 := h.parseRef(r.PathValue("ref1")) 343 + ref2 := h.parseRef(r.PathValue("ref2")) 344 + 345 + repo, err := h.openPublicRepo(name, ref2) 346 + if err != nil { 347 + h.write404(w, r.URL.Path, err) 348 + return 349 + } 350 + 351 + desc, err := repo.Description() 352 + if err != nil { 353 + h.write500(w, err) 354 + return 355 + } 356 + 357 + compare, err := repo.Compare(ref1, ref2) 358 + if err != nil { 359 + h.write404(w, r.URL.Path, err) 360 + return 361 + } 362 + 363 + h.templ(w, "repo_compare", h.pageData(repo, RepoCompare{ 364 + Desc: desc, 365 + Ref: ref2, 366 + Compare: compare, 367 + })) 368 +} 369 + 334 370 type RepoRefs struct { 335 371 Desc string 336 372 Ref string
A
web/templates/_diff_partials.html
路路路 1 +{{ define "_diff_type" }} 2 +{{ if .IsNew }}<span class="diff-type diff-add">A</span> 3 +{{ else if .IsDelete }}<span class="diff-type diff-del">D</span> 4 +{{ else }}<span class="diff-type diff-mod">M</span>{{ end }} 5 +{{ end }} 6 + 7 +{{ define "_diff_stat" }} 8 +<div class="commit-refs"> 9 + <span class="diff-mod">{{ .FilesChanged }} files changed</span>, 10 + <span class="diff-add">{{ .Insertions }} insertions(+)</span>, 11 + <span class="diff-del">{{ .Deletions }} deletions(-)</span> 12 +</div> 13 +{{ end }} 14 + 15 +{{ define "_diff_table" }} 16 +{{ if gt (len .) 1 -}} 17 +<div class="jump"> 18 + <strong>jump to</strong> 19 + <table class="table jump-table"> 20 + <tbody> 21 + {{ range . }} 22 + {{ $anchor := .Name.New }} 23 + {{ if not $anchor }}{{ $anchor = .Name.Old }}{{ end }} 24 + <tr> 25 + <td class="mono">{{ template "_diff_type" . }}</td> 26 + <td class="fill"> 27 + <a href="#{{ $anchor }}"> 28 + {{ if .IsRename }}{{ .Name.Old }} → {{ .Name.New }} 29 + {{ else }}{{ $anchor }}{{ end }} 30 + </a> 31 + </td> 32 + </tr> 33 + {{ end }} 34 + </tbody> 35 + </table> 36 +</div> 37 +{{ end }} 38 +{{ end }} 39 + 40 +{{ define "_diff_files" }} 41 +{{ $repo := .Repo }} 42 +{{ $leftHash := .LeftHash }} 43 +{{ $rightHash := .RightHash }} 44 +{{ range .Diff }} 45 +{{ $anchor := .Name.New }} 46 +{{ if not $anchor }}{{ $anchor = .Name.Old }}{{ end }} 47 +<div id="{{ $anchor }}"> 48 + <div class="diff"> 49 + {{ template "_diff_type" . }} 50 + 51 + {{ $primaryName := .Name.New }} 52 + {{ $primaryHash := $rightHash }} 53 + {{ if or .IsDelete .IsRename }} 54 + {{ $primaryName = .Name.Old }} 55 + {{ $primaryHash = $leftHash }} 56 + {{ end }} 57 + 58 + {{ if $primaryHash }}<a href="/{{ $repo }}/blob/{{ $primaryHash }}/{{ $primaryName }}">{{ $primaryName }}</a>{{ else }}{{ $primaryName }}{{ end }} 59 + {{ if .IsRename }} → <a href="/{{ $repo }}/blob/{{ $rightHash }}/{{ .Name.New }}">{{ .Name.New }}</a>{{ end }} 60 + 61 + {{ if .IsBinary }} 62 + <p>Not showing binary file.</p> 63 + {{ else }} 64 + <pre> 65 + {{- range .TextFragments -}} 66 + <span class="diff-line diff-noop diff-separator">路路路</span> 67 + {{- $o := .OldPosition -}} 68 + {{- $n := .NewPosition -}} 69 + {{- range .Lines -}} 70 + {{- $op := .Op.String -}} 71 + 72 + {{- if eq $op "+" -}} 73 + <span class="diff-line diff-add" id="{{ $anchor }}-N{{ $n }}"> 74 + <span class="line-number"></span> 75 + <a class="line-number" href="#{{ $anchor }}-N{{ $n }}">{{ $n }}</a> 76 + <span><span class="diff-op">{{ $op }}</span>{{ .Line }}</span> 77 + </span> 78 + {{- $n = inc64 $n -}} 79 + 80 + {{- else if eq $op "-" -}} 81 + <span class="diff-line diff-del" id="{{ $anchor }}-O{{ $o }}"> 82 + <a class="line-number" href="#{{ $anchor }}-O{{ $o }}">{{ $o }}</a> 83 + <span class="line-number"></span> 84 + <span><span class="diff-op">{{ $op }}</span>{{ .Line }}</span> 85 + </span> 86 + {{- $o = inc64 $o -}} 87 + 88 + {{- else -}} 89 + <span class="diff-line diff-noop" id="{{ $anchor }}-L{{ $o }}"> 90 + <a class="line-number" href="#{{ $anchor }}-L{{ $o }}">{{ $o }}</a> 91 + <a class="line-number" href="#{{ $anchor }}-L{{ $o }}">{{ $n }}</a> 92 + <span><span class="diff-op">{{ $op }}</span>{{ .Line }}</span> 93 + </span> 94 + {{- $o = inc64 $o -}} 95 + {{- $n = inc64 $n -}} 96 + {{- end -}} 97 + 98 + {{- end -}} 99 + {{- end -}} 100 + </pre> 101 + {{ end }} 102 + </div> 103 +</div> 104 +{{ end }} 105 +{{ end }}
M
web/templates/repo_commit.html
路路路 1 -{{ define "_diff_type" }} 2 -{{ if .IsNew }}<span class="diff-type diff-add">A</span> 3 -{{ else if .IsDelete }}<span class="diff-type diff-del">D</span> 4 -{{ else }}<span class="diff-type diff-mod">M</span>{{ end }} 1 +{{ define "_commit_table" }} 2 +<table class="table log"> 3 + <thead> 4 + <tr class="nohover"> 5 + <th class="fill">Message</th> 6 + <th class="author nowrap">Author</th> 7 + <th>Hash</th> 8 + <th class="age nowrap">Age</th> 9 + </tr> 10 + </thead> 11 + <tbody> 12 + {{ range $.Commits }} 13 + <tr> 14 + <td class="fill"> 15 + <a href="/{{ $.Repo }}/commit/{{ .Hash }}"> 16 + {{- if .Message }}{{- commitSummary .Message -}} 17 + {{- else -}}<span class="muted">Empty message</span>{{- end -}} 18 + </a> 19 + </td> 20 + <td class="has-tip nowrap"> 21 + <span class="author-short"> 22 + {{ .AuthorName }} <a href="mailto:{{ .AuthorEmail }}" class="commit-email">{{ .AuthorEmail }}</a> 23 + </span> 24 + <span class="tooltip" role="tooltip"> 25 + <strong>{{ .AuthorName }}</strong><br> 26 + <a href="mailto:{{ .AuthorEmail }}" class="commit-email">{{ .AuthorEmail }}</a> 27 + </span> 28 + </td> 29 + <td class="mono"> 30 + <a href="/{{ $.Repo }}/commit/{{ .Hash }}">{{ .HashShort }}</a> 31 + </td> 32 + <td class="has-tip nowrap"> 33 + {{ humanizeRelTime .Committed }} 34 + <span class="tooltip" role="tooltip">{{ humanizeTime .Committed }}</span> 35 + </td> 36 + </tr> 37 + {{ end }} 38 + </tbody> 39 +</table> 5 40 {{ end }} 6 41 7 42 {{ define "repo_commit" }} 路路路 18 53 {{ template "repo_header" . }} 19 54 <main> 20 55 <section class="commit"> 21 - <div class="commit-refs"> 22 - <span class="diff-mod">{{ $stat.FilesChanged }} files changed</span>, 23 - <span class="diff-add">{{ $stat.Insertions }} insertions(+)</span>, 24 - <span class="diff-del">{{ $stat.Deletions }} deletions(-)</span> 25 - </div> 56 + {{ template "_diff_stat" $stat }} 26 57 27 58 <div class="box"> 28 59 <pre class="commit-message"> 路路路 68 99 {{ end }} 69 100 </div> 70 101 71 - {{ if gt (len $diff) 1 -}} 72 - <div class="jump"> 73 - <strong>jump to</strong> 74 - <table class="table jump-table"> 75 - <tbody> 76 - {{ range $diff }} 77 - {{ $path := .Name.New }} 78 - {{ if not $path }}{{ $path = .Name.Old }}{{ end }} 79 - <tr> 80 - <td class="mono">{{ template "_diff_type" . }}</td> 81 - <td class="fill"> 82 - <a href="#{{ $path }}"> 83 - {{ if .IsRename }}{{ .Name.Old }} → {{ .Name.New }} 84 - {{ else }}{{ $path }}{{ end }} 85 - </a> 86 - </td> 87 - </tr> 88 - {{ end }} 89 - </tbody> 90 - </table> 91 - </div> 92 - {{ end }} 102 + {{ template "_diff_table" $diff }} 93 103 </section> 94 104 95 105 <section> 96 - {{ $this := $commit.Hash }} 97 106 {{ $parent := "" }} 98 107 {{ if $parents }}{{ $parent = index $parents 0 }}{{ end }} 99 - {{ range $diff }} 100 - {{ $path := .Name.New }} 101 - {{ if not $path }}{{ $path = .Name.Old }}{{ end }} 102 - <div id="{{ $path }}"> 103 - <div class="diff"> 104 - {{ template "_diff_type" . }} 105 - 106 - {{ $name := .Name.New }}{{ $hash := $this }} 107 - {{ if or .IsDelete .IsRename }}{{ $name = .Name.Old }}{{ $hash = $parent }}{{ end }} 108 - {{ if $hash }}<a href="/{{ $.RepoName }}/blob/{{ $hash }}/{{ $name }}">{{ $name }}</a>{{ else }}{{ $name }}{{ end }} 109 - {{ if .IsRename }} → <a href="/{{ $.RepoName }}/blob/{{ $this }}/{{ .Name.New }}">{{ .Name.New }}</a>{{ end }} 110 - 111 - {{ if .IsBinary }}<p>Not showing binary file.</p> 112 - {{ else }} 113 - <pre> 114 - {{- range .TextFragments -}} 115 - <span class="diff-line diff-noop diff-separator">路路路</span> 116 - {{- $o := .OldPosition -}} 117 - {{- $n := .NewPosition -}} 118 - {{- range .Lines -}} 119 - {{- $op := .Op.String -}} 120 - 121 - {{- if eq $op "+" -}} 122 - <span class="diff-line diff-add" id="{{ $path }}-N{{ $n }}"> 123 - <span class="line-number"></span> 124 - <a class="line-number" href="#{{ $path }}-N{{ $n }}">{{ $n }}</a> 125 - <span><span class="diff-op">{{ $op }}</span>{{ .Line }}</span> 126 - </span> 127 - {{- $n = inc64 $n -}} 128 - 129 - {{- else if eq $op "-" -}} 130 - <span class="diff-line diff-del" id="{{ $path }}-O{{ $o }}"> 131 - <a class="line-number" href="#{{ $path }}-O{{ $o }}">{{ $o }}</a> 132 - <span class="line-number"></span> 133 - <span><span class="diff-op">{{ $op }}</span>{{ .Line }}</span> 134 - </span> 135 - {{- $o = inc64 $o -}} 136 - 137 - {{- else -}} 138 - <span class="diff-line diff-noop" id="{{ $path }}-L{{ $o }}"> 139 - <a class="line-number" href="#{{ $path }}-L{{ $o }}">{{ $o }}</a> 140 - <a class="line-number" href="#{{ $path }}-L{{ $o }}">{{ $n }}</a> 141 - <span><span class="diff-op">{{ $op }}</span>{{ .Line }}</span> 142 - </span> 143 - {{- $o = inc64 $o -}} 144 - {{- $n = inc64 $n -}} 145 - {{- end -}} 146 - 147 - {{- end -}} 148 - {{- end -}} 149 - </pre> 150 - {{ end }} 151 - </div> 152 - </div> 153 - {{ end }} 108 + {{ template "_diff_files" (dict "Repo" .RepoName "Diff" $diff "RightHash" $commit.Hash "LeftHash" $parent) }} 154 109 </section> 155 110 </main> 156 111 </body>
A
web/templates/repo_compare.html
路路路 1 +{{ define "repo_compare" }} 2 +{{ $cmp := .P.Compare }} 3 +{{ $diff := $cmp.Diff.Diff }} 4 +<html> 5 + <head> 6 + {{ template "head" . }} 7 + <title>{{ $.RepoName }}: compare {{ $cmp.BaseRef }}...{{ $cmp.HeadRef }}</title> 8 + </head> 9 + <body> 10 + {{ template "repo_header" . }} 11 + <main> 12 + <section class="commit"> 13 + <div class="commit-refs"> 14 + <strong>{{ $cmp.BaseRef }}</strong>...<strong>{{ $cmp.HeadRef }}</strong> 15 + <span class="pl"> 16 + <span class="diff-add">{{ $cmp.Ahead }} ahead</span>, 17 + <span class="diff-del">{{ $cmp.Behind }} behind</span> 18 + </span> 19 + </div> 20 + <div class="box"> 21 + <span> 22 + <strong>Merge base:</strong> 23 + <a class="link" href="/{{ $.RepoName }}/commit/{{ $cmp.MergeBase }}">{{ printf "%.7s" $cmp.MergeBase }}</a>, 24 + </span> 25 + <span> 26 + <strong>Base:</strong> 27 + <a class="link" href="/{{ $.RepoName }}/commit/{{ $cmp.BaseHash }}">{{ printf "%.7s" $cmp.BaseHash }}</a>, 28 + </span> 29 + <span> 30 + <strong>Head:</strong> 31 + <a class="link" href="/{{ $.RepoName }}/commit/{{ $cmp.HeadHash }}">{{ printf "%.7s" $cmp.HeadHash }}</a> 32 + </span> 33 + </div> 34 + </section> 35 + 36 + <section class="commit"> 37 + <h3>Commits in {{ $cmp.HeadRef }} not in {{ $cmp.BaseRef }}</h3> 38 + {{ template "_diff_stat" $cmp.Diff.Stat }} 39 + {{ if $cmp.Commits }}{{ template "_commit_table" (dict "Repo" $.RepoName "Commits" $cmp.Commits) }} 40 + {{ else }}<p class="muted">No commits to compare.</p>{{ end }} 41 + </section> 42 + 43 + <section class="commit">{{ template "_diff_table" $diff }}</section> 44 + <section> 45 + {{ template "_diff_files" (dict "Repo" $.RepoName "Diff" $diff "RightHash" $cmp.HeadHash "LeftHash" $cmp.MergeBase) }} 46 + </section> 47 + </main> 48 + </body> 49 +</html> 50 +{{ end }}
M
web/templates/repo_log.html
路路路 8 8 <body> 9 9 {{ template "repo_header" . }} 10 10 <main> 11 - <table class="table log"> 12 - <thead> 13 - <tr class="nohover"> 14 - <th class="fill">Message</th> 15 - <th class="author nowrap">Author</th> 16 - <th>Hash</th> 17 - <th class="age nowrap">Age</th> 18 - </tr> 19 - </thead> 20 - <tbody> 21 - {{ range .P.Commits }} 22 - <tr> 23 - <td class="fill"> 24 - <a href="/{{ $repo }}/commit/{{ .Hash }}"> 25 - {{- if .Message }}{{- commitSummary .Message -}} 26 - {{- else -}}<span class="muted">Empty message</span>{{- end -}} 27 - </a> 28 - </td> 29 - <td class="has-tip nowrap"> 30 - <span class="author-short"> 31 - {{ .AuthorName }} <a href="mailto:{{ .AuthorEmail }}" class="commit-email">{{ .AuthorEmail }}</a> 32 - </span> 33 - <span class="tooltip" role="tooltip"> 34 - <strong>{{ .AuthorName }}</strong><br> 35 - <a href="mailto:{{ .AuthorEmail }}" class="commit-email">{{ .AuthorEmail }}</a> 36 - </span> 37 - </td> 38 - <td class="mono"> 39 - <a href="/{{ $repo }}/commit/{{ .Hash }}">{{ .HashShort }}</a> 40 - </td> 41 - <td class="has-tip nowrap"> 42 - {{ humanizeRelTime .Committed }} 43 - <span class="tooltip" role="tooltip">{{ humanizeTime .Committed }}</span> 44 - </td> 45 - </tr> 46 - {{ end }} 47 - </tbody> 48 - </table> 11 + {{ template "_commit_table" (dict "Repo" $repo "Commits" .P.Commits) }} 49 12 <div class="center"> 50 13 {{ if .P.NextAfter }} 51 14 <a href="?after={{ urlencode .P.NextAfter }}">[load more]</a>
M
web/templates/repo_refs.html
路路路 14 14 <div> 15 15 <strong>{{ .Name }}</strong> 16 16 <a class="link" href="/{{ $repo }}/tree/{{ urlencode .Name }}/">browse</a> 17 + {{ if ne $.P.Ref .Name }}<a class="link" href="/{{ $.RepoName }}/compare/{{ urlencode $.P.Ref }}/{{ urlencode .Name }}">compare</a>{{ end }} 17 18 <a class="link" href="/{{ $repo }}/log/{{ urlencode .Name }}">log</a> 18 19 <a class="link" href="/{{ $repo }}/archive/{{ urlencode .Name }}">tar.gz</a> 19 20 </div> 路路路 26 27 <div> 27 28 <strong>{{ .Name }}</strong> 28 29 <a class="link" href="/{{ $repo }}/tree/{{ urlencode .Name }}/">browse</a> 30 + {{ if ne $.P.Ref .Name }}<a class="link" href="/{{ $.RepoName }}/compare/{{ urlencode $.P.Ref }}/{{ urlencode .Name }}">compare</a>{{ end }} 29 31 <a class="link" href="/{{ $repo }}/log/{{ urlencode .Name }}">log</a> 30 32 <a class="link" href="/{{ $repo }}/archive/{{ urlencode .Name }}">tar.gz</a> 31 33 {{ if .Message }}