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}