133728e barerepo 1mo
1
package gitread
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"context"
133728e barerepo 1mo
5
"strconv"
133728e barerepo 1mo
6
"strings"
133728e barerepo 1mo
8
"github.com/barerepo/server/internal/gitx"
133728e barerepo 1mo
11
// Comparison takes free text both sides, because a dropdown cannot say refs/proposals/47.
133728e barerepo 1mo
12
type Comparison struct {
133728e barerepo 1mo
13
A, B string
133728e barerepo 1mo
14
Commits int
133728e barerepo 1mo
15
Files []FileDiff
133728e barerepo 1mo
16
Add int
133728e barerepo 1mo
17
Del int
133728e barerepo 1mo
18
Conflict bool
133728e barerepo 1mo
19
// Missing names the side that did not resolve, so the page says which one was wrong.
133728e barerepo 1mo
20
Missing string
133728e barerepo 1mo
23
// Compare uses three dots, from the merge base, which is what a person means by a comparison.
133728e barerepo 1mo
24
func Compare(ctx context.Context, dir, a, b string) (*Comparison, error) {
133728e barerepo 1mo
25
c := &Comparison{A: a, B: b}
133728e barerepo 1mo
26
for _, side := range []struct{ name, rev string }{{"a", a}, {"b", b}} {
133728e barerepo 1mo
27
if !gitx.ValidRev(side.rev) {
133728e barerepo 1mo
28
c.Missing = side.rev
133728e barerepo 1mo
29
return c, nil
133728e barerepo 1mo
31
if _, err := gitx.Run(ctx, dir, "rev-parse", "--verify", "--quiet", side.rev+"^{commit}"); err != nil {
133728e barerepo 1mo
32
c.Missing = side.rev
133728e barerepo 1mo
33
return c, nil
133728e barerepo 1mo
37
if out, err := gitx.Run(ctx, dir, "rev-list", "--count", a+".."+b); err == nil {
133728e barerepo 1mo
38
c.Commits, _ = strconv.Atoi(strings.TrimSpace(out))
133728e barerepo 1mo
41
patch, err := gitx.Run(ctx, dir, "diff", "--unified=3", "--no-color", "--find-renames", a+"..."+b, "--")
133728e barerepo 1mo
42
if err != nil {
133728e barerepo 1mo
43
return nil, err
133728e barerepo 1mo
45
c.Files = parsePatch(patch)
133728e barerepo 1mo
46
for _, f := range c.Files {
133728e barerepo 1mo
47
c.Add += f.Add
133728e barerepo 1mo
48
c.Del += f.Del
133728e barerepo 1mo
51
// Answered without merging or a working tree, because rule 1 says the server only reports.
133728e barerepo 1mo
52
if _, err := gitx.Run(ctx, dir, "merge-tree", "--write-tree", "--name-only", a, b); err != nil {
133728e barerepo 1mo
53
c.Conflict = true
133728e barerepo 1mo
55
return c, nil
133728e barerepo 1mo
58
// DiffStat is the mockup's +81 -12 in one process, where a full Compare costs five.
133728e barerepo 1mo
59
func DiffStat(ctx context.Context, dir, a, b string) (add, del int) {
133728e barerepo 1mo
60
add, del, _ = diffStat(ctx, dir, a, b)
133728e barerepo 1mo
61
return add, del
133728e barerepo 1mo
64
// diffStat reports whether git answered, because a failed run counts +0 -0 and so does an empty one, and only one is worth keeping.
133728e barerepo 1mo
65
func diffStat(ctx context.Context, dir, a, b string) (add, del int, ok bool) {
133728e barerepo 1mo
66
if !gitx.ValidRev(a) || !gitx.ValidRev(b) {
133728e barerepo 1mo
67
return 0, 0, false
133728e barerepo 1mo
69
out, err := gitx.Run(ctx, dir, "diff", "--numstat", "--no-color", a+"..."+b, "--")
133728e barerepo 1mo
70
if err != nil {
133728e barerepo 1mo
71
return 0, 0, false
133728e barerepo 1mo
73
for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
133728e barerepo 1mo
74
fields := strings.Fields(line)
133728e barerepo 1mo
75
if len(fields) < 3 {
133728e barerepo 1mo
76
continue
133728e barerepo 1mo
78
// A binary file is reported as "-", which counts as no lines either way.
133728e barerepo 1mo
79
if n, err := strconv.Atoi(fields[0]); err == nil {
133728e barerepo 1mo
80
add += n
133728e barerepo 1mo
82
if n, err := strconv.Atoi(fields[1]); err == nil {
133728e barerepo 1mo
83
del += n
133728e barerepo 1mo
86
return add, del, true
133728e barerepo 1mo
89
// DiffStatBetween is DiffStat keyed by two object hashes, which cannot change, so it never invalidates.
133728e barerepo 1mo
90
func DiffStatBetween(ctx context.Context, dir, aSHA, bSHA string) (add, del int) {
133728e barerepo 1mo
91
if aSHA == "" || bSHA == "" {
133728e barerepo 1mo
92
return 0, 0
133728e barerepo 1mo
94
if aSHA == bSHA {
133728e barerepo 1mo
95
return 0, 0
133728e barerepo 1mo
97
key := aSHA + "-" + bSHA
133728e barerepo 1mo
98
if body, ok := Cache.Get("diffstat", key); ok {
133728e barerepo 1mo
99
if a, d, ok := parseStat(string(body)); ok {
133728e barerepo 1mo
100
return a, d
133728e barerepo 1mo
101
}
133728e barerepo 1mo
102
}
133728e barerepo 1mo
103
add, del, ok := diffStat(ctx, dir, aSHA, bSHA)
133728e barerepo 1mo
104
if !ok {
133728e barerepo 1mo
105
// This key never expires, so a reader who left mid-render must not pin +0 -0 on it forever.
133728e barerepo 1mo
106
return 0, 0
133728e barerepo 1mo
107
}
133728e barerepo 1mo
108
Cache.Put("diffstat", key, []byte(strconv.Itoa(add)+" "+strconv.Itoa(del)))
133728e barerepo 1mo
109
return add, del
133728e barerepo 1mo
112
// parseStat reads back what DiffStatBetween wrote.
133728e barerepo 1mo
113
func parseStat(body string) (add, del int, ok bool) {
133728e barerepo 1mo
114
a, d, found := strings.Cut(strings.TrimSpace(body), " ")
133728e barerepo 1mo
115
if !found {
133728e barerepo 1mo
116
return 0, 0, false
133728e barerepo 1mo
117
}
133728e barerepo 1mo
118
add, err := strconv.Atoi(a)
133728e barerepo 1mo
119
if err != nil {
133728e barerepo 1mo
120
return 0, 0, false
133728e barerepo 1mo
121
}
133728e barerepo 1mo
122
del, err = strconv.Atoi(d)
133728e barerepo 1mo
123
if err != nil {
133728e barerepo 1mo
124
return 0, 0, false
133728e barerepo 1mo
125
}
133728e barerepo 1mo
126
return add, del, true