internal/gitutil/messages.go
136 lines · 4218 bytes
1package gitutil
2
3import (
4 "fmt"
5 "os/exec"
6 "strings"
7
8 "gitbay.org/gitbay/internal/toolpath"
9)
10
11const zeroSHA = "0000000000000000000000000000000000000000"
12
13type CommitAuthor struct {
14 SHA string
15 Email string
16 Day string // author date, YYYY-MM-DD
17}
18
19// RevListAuthors returns sha, author email, and author date for commits
20// reachable from new but not old, capped at max. Empty or zero old lists
21// from new alone.
22func RevListAuthors(dir, old, new string, max int) ([]CommitAuthor, error) {
23 args := []string{"-C", dir, "log", fmt.Sprintf("--max-count=%d", max), "--format=%H%x00%ae%x00%as", new}
24 if old != "" && old != zeroSHA {
25 args = append(args, "^"+old)
26 }
27 out, err := exec.Command(toolpath.Look("git"), args...).Output()
28 if err != nil {
29 return nil, fmt.Errorf("rev-list authors: %w", err)
30 }
31 var authors []CommitAuthor
32 for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
33 parts := strings.SplitN(line, "\x00", 3)
34 if len(parts) != 3 {
35 continue
36 }
37 authors = append(authors, CommitAuthor{SHA: parts[0], Email: parts[1], Day: parts[2]})
38 }
39 return authors, nil
40}
41
42// Parents returns a commit's parent shas.
43func Parents(dir, sha string) []string {
44 out, err := exec.Command(toolpath.Look("git"), "-C", dir, "log", "-1", "--format=%P", sha).Output()
45 if err != nil {
46 return nil
47 }
48 return strings.Fields(string(out))
49}
50
51// LastCommitDate returns the committer date (YYYY-MM-DD) of the ref tip,
52// or "" for empty repos.
53func LastCommitDate(dir, ref string) string {
54 out, err := exec.Command(toolpath.Look("git"), "-C", dir, "log", "-1", "--format=%cs", ref).Output()
55 if err != nil {
56 return ""
57 }
58 return strings.TrimSpace(string(out))
59}
60
61// HasCommit reports whether sha names a commit object present in dir.
62func HasCommit(dir, sha string) bool {
63 return exec.Command(toolpath.Look("git"), "-C", dir, "cat-file", "-e", sha+"^{commit}").Run() == nil
64}
65
66type CommitMsg struct {
67 SHA string
68 Message string
69 AuthorName string
70 AuthorEmail string
71}
72
73// RevListMessages returns sha and full message for commits reachable from
74// new but not old, newest first, capped at max. An empty or zero old (new
75// branch) lists from new alone, still capped.
76func RevListMessages(dir, old, new string, max int) ([]CommitMsg, error) {
77 args := []string{"-C", dir, "rev-list", fmt.Sprintf("-n%d", max), "--format=%an%x01%ae%x01%B%x00", new}
78 if old != "" && old != zeroSHA {
79 args = append(args, "^"+old)
80 }
81 out, err := exec.Command(toolpath.Look("git"), args...).Output()
82 if err != nil {
83 return nil, fmt.Errorf("rev-list messages: %w", err)
84 }
85 var msgs []CommitMsg
86 for _, chunk := range strings.Split(string(out), "\x00") {
87 chunk = strings.TrimLeft(chunk, "\n")
88 if chunk == "" {
89 continue
90 }
91 header, body, ok := strings.Cut(chunk, "\n")
92 if !ok || !strings.HasPrefix(header, "commit ") {
93 continue
94 }
95 name, rest, ok := strings.Cut(body, "\x01")
96 if !ok {
97 continue
98 }
99 email, message, ok := strings.Cut(rest, "\x01")
100 if !ok {
101 continue
102 }
103 msgs = append(msgs, CommitMsg{
104 SHA: strings.TrimPrefix(header, "commit "),
105 Message: strings.TrimSpace(message),
106 AuthorName: name,
107 AuthorEmail: email,
108 })
109 }
110 return msgs, nil
111}
112
113// Subjects returns the first line of each named commit's message, keyed
114// by sha. One git log for the whole set rather than one per sha: a page
115// of builds names a handful of distinct commits and a subprocess each
116// would show. --ignore-missing keeps a sha git cannot resolve from
117// failing the rest, because a build outlives the commit it ran on once a
118// branch is force-pushed; such a sha is simply absent from the map.
119func Subjects(dir string, shas []string) map[string]string {
120 if len(shas) == 0 {
121 return nil
122 }
123 args := append([]string{"-C", dir, "log", "--no-walk=unsorted", "--ignore-missing", "--format=%H%x00%s"}, shas...)
124 args = append(args, "--")
125 out, err := exec.Command(toolpath.Look("git"), args...).Output()
126 if err != nil {
127 return nil
128 }
129 subjects := map[string]string{}
130 for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
131 if sha, subject, ok := strings.Cut(line, "\x00"); ok {
132 subjects[sha] = subject
133 }
134 }
135 return subjects
136}