internal/gitutil/numstat.go
84 lines · 2075 bytes
1package gitutil
2
3import (
4 "fmt"
5 "os/exec"
6 "strconv"
7 "strings"
8
9 "gitbay.org/gitbay/internal/toolpath"
10)
11
12// NumStat is one file's line counts between two commits. A binary file
13// counts -1 each way. Status is A, M, D or R.
14type NumStat struct {
15 Path string
16 Added int
17 Deleted int
18 Status string
19}
20
21// DiffNumstat lists the files changed from base to head with their line
22// counts and status, renames detected. A renamed file is listed under
23// its new path.
24func DiffNumstat(dir, base, head string) ([]NumStat, error) {
25 run := func(args ...string) (string, error) {
26 cmd := exec.Command(toolpath.Look("git"), append([]string{"-C", dir}, args...)...)
27 out, err := cmd.Output()
28 if err != nil {
29 return "", fmt.Errorf("git %s %s: %w", args[0], args[1], err)
30 }
31 return string(out), nil
32 }
33 num, err := run("diff", "--numstat", "-z", "-M", "--end-of-options", base, head)
34 if err != nil {
35 return nil, err
36 }
37 names, err := run("diff", "--name-status", "-z", "-M", "--end-of-options", base, head)
38 if err != nil {
39 return nil, err
40 }
41 // --name-status -z: "M\0path\0", or "R100\0old\0new\0" for a rename
42 // or copy.
43 status := map[string]string{}
44 f := strings.Split(strings.TrimSuffix(names, "\x00"), "\x00")
45 for i := 0; i < len(f); {
46 s := f[i]
47 if s == "" {
48 i++
49 continue
50 }
51 if s[0] == 'R' || s[0] == 'C' {
52 if i+2 < len(f) {
53 status[f[i+2]] = s[:1]
54 }
55 i += 3
56 continue
57 }
58 if i+1 < len(f) {
59 status[f[i+1]] = s[:1]
60 }
61 i += 2
62 }
63 // --numstat -z: "a\td\tpath\0", or "a\td\t\0old\0new\0" for a rename.
64 var out []NumStat
65 recs := strings.Split(strings.TrimSuffix(num, "\x00"), "\x00")
66 for i := 0; i < len(recs); i++ {
67 parts := strings.SplitN(recs[i], "\t", 3)
68 if len(parts) != 3 {
69 continue
70 }
71 path := parts[2]
72 if path == "" && i+2 < len(recs) {
73 path = recs[i+2]
74 i += 2
75 }
76 ns := NumStat{Path: path, Added: -1, Deleted: -1, Status: status[path]}
77 if parts[0] != "-" {
78 ns.Added, _ = strconv.Atoi(parts[0])
79 ns.Deleted, _ = strconv.Atoi(parts[1])
80 }
81 out = append(out, ns)
82 }
83 return out, nil
84}