all repos

mugit @ 6bb47d39fa03e2384242dffa38ce0cc1215e7f1f

馃惍 git server that your cow will love
11 files changed, 508 insertions(+), 130 deletions(-)
feat: compare refs (#9)

* git: compare

* http: handler
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2026-05-07 19:10:07 +0300
Parent: 7218955
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 }} &#8594; {{ .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 }} &#8594; <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 }} &#8594; {{ .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 }} &#8594; <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 }}