internal/gitutil/messages.go

v1.34.0
gitbay/internal/gitutil/messages.go history · blame · raw

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}