package gitread import ( "context" "strconv" "strings" "github.com/barerepo/server/internal/gitx" ) // Comparison takes free text both sides, because a dropdown cannot say refs/proposals/47. type Comparison struct { A, B string Commits int Files []FileDiff Add int Del int Conflict bool // Missing names the side that did not resolve, so the page says which one was wrong. Missing string } // Compare uses three dots, from the merge base, which is what a person means by a comparison. func Compare(ctx context.Context, dir, a, b string) (*Comparison, error) { c := &Comparison{A: a, B: b} for _, side := range []struct{ name, rev string }{{"a", a}, {"b", b}} { if !gitx.ValidRev(side.rev) { c.Missing = side.rev return c, nil } if _, err := gitx.Run(ctx, dir, "rev-parse", "--verify", "--quiet", side.rev+"^{commit}"); err != nil { c.Missing = side.rev return c, nil } } if out, err := gitx.Run(ctx, dir, "rev-list", "--count", a+".."+b); err == nil { c.Commits, _ = strconv.Atoi(strings.TrimSpace(out)) } patch, err := gitx.Run(ctx, dir, "diff", "--unified=3", "--no-color", "--find-renames", a+"..."+b, "--") if err != nil { return nil, err } c.Files = parsePatch(patch) for _, f := range c.Files { c.Add += f.Add c.Del += f.Del } // Answered without merging or a working tree, because rule 1 says the server only reports. if _, err := gitx.Run(ctx, dir, "merge-tree", "--write-tree", "--name-only", a, b); err != nil { c.Conflict = true } return c, nil } // DiffStat is the mockup's +81 -12 in one process, where a full Compare costs five. func DiffStat(ctx context.Context, dir, a, b string) (add, del int) { add, del, _ = diffStat(ctx, dir, a, b) return add, del } // diffStat reports whether git answered, because a failed run counts +0 -0 and so does an empty one, and only one is worth keeping. func diffStat(ctx context.Context, dir, a, b string) (add, del int, ok bool) { if !gitx.ValidRev(a) || !gitx.ValidRev(b) { return 0, 0, false } out, err := gitx.Run(ctx, dir, "diff", "--numstat", "--no-color", a+"..."+b, "--") if err != nil { return 0, 0, false } for _, line := range strings.Split(strings.TrimSpace(out), "\n") { fields := strings.Fields(line) if len(fields) < 3 { continue } // A binary file is reported as "-", which counts as no lines either way. if n, err := strconv.Atoi(fields[0]); err == nil { add += n } if n, err := strconv.Atoi(fields[1]); err == nil { del += n } } return add, del, true } // DiffStatBetween is DiffStat keyed by two object hashes, which cannot change, so it never invalidates. func DiffStatBetween(ctx context.Context, dir, aSHA, bSHA string) (add, del int) { if aSHA == "" || bSHA == "" { return 0, 0 } if aSHA == bSHA { return 0, 0 } key := aSHA + "-" + bSHA if body, ok := Cache.Get("diffstat", key); ok { if a, d, ok := parseStat(string(body)); ok { return a, d } } add, del, ok := diffStat(ctx, dir, aSHA, bSHA) if !ok { // This key never expires, so a reader who left mid-render must not pin +0 -0 on it forever. return 0, 0 } Cache.Put("diffstat", key, []byte(strconv.Itoa(add)+" "+strconv.Itoa(del))) return add, del } // parseStat reads back what DiffStatBetween wrote. func parseStat(body string) (add, del int, ok bool) { a, d, found := strings.Cut(strings.TrimSpace(body), " ") if !found { return 0, 0, false } add, err := strconv.Atoi(a) if err != nil { return 0, 0, false } del, err = strconv.Atoi(d) if err != nil { return 0, 0, false } return add, del, true }