all repos

mugit @ be1cdfdd76f4248e14a065d5b2c6710a5f79b2fe

🐮 git server that your cow will love

mugit/internal/git/compare.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
feat: compare refs (#9)..., 1 month ago
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
}