-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgit.go
More file actions
118 lines (106 loc) · 3.55 KB
/
Copy pathgit.go
File metadata and controls
118 lines (106 loc) · 3.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package specsync
import (
"context"
"fmt"
"os/exec"
"strings"
)
// GitCommitSource reads commit history via the host `git` CLI. Like the GitHub
// provider it holds no library dependency — everything is shelled out, keeping
// the package stdlib-only and easy to fake in tests by swapping run.
type GitCommitSource struct {
run func(ctx context.Context, args ...string) (string, error)
}
// NewGitCommitSource returns a source driven by the real `git` binary.
func NewGitCommitSource() *GitCommitSource {
return &GitCommitSource{run: runGit}
}
// NewGitCommitSourceFunc returns a source driven by the given runner instead of
// the real `git` binary. Used in tests.
func NewGitCommitSourceFunc(run func(ctx context.Context, args ...string) (string, error)) *GitCommitSource {
return &GitCommitSource{run: run}
}
func runGit(ctx context.Context, args ...string) (string, error) {
out, err := exec.CommandContext(ctx, "git", args...).CombinedOutput()
if err != nil {
return "", fmt.Errorf("git %s: %w\n%s", strings.Join(args, " "), err, out)
}
return strings.TrimSpace(string(out)), nil
}
// Field/record separators chosen to be vanishingly unlikely in commit text, so a
// single `git log` call parses unambiguously without per-commit spawns.
const (
gitFieldSep = "\x1f" // unit separator
gitRecordSep = "\x1e" // record separator
)
// AllHistory, passed as `since`, means "walk the full history" — distinct from
// an empty since (which defaults to the latest tag). Used by change-scope gather.
const AllHistory = "\x00all"
// Commits returns the parsed commits in (since, until]. An empty since defaults
// to the most recent reachable tag, falling back to the full history when the
// repository has no tags; an empty until defaults to HEAD. When paths is
// non-empty, the log is restricted to commits touching those path globs.
func (g *GitCommitSource) Commits(ctx context.Context, since, until string, paths []string) ([]Commit, error) {
if until == "" {
until = "HEAD"
}
switch since {
case AllHistory:
since = "" // full history: no lower bound
case "":
tag, err := g.latestTag(ctx)
if err != nil {
return nil, err
}
since = tag // "" when no tag → full history below
}
revRange := until
if since != "" {
revRange = since + ".." + until
}
format := strings.Join([]string{"%H", "%an", "%aI", "%B"}, gitFieldSep) + gitRecordSep
args := []string{"log", "--pretty=format:" + format, revRange}
if len(paths) > 0 {
args = append(args, "--")
args = append(args, paths...)
}
out, err := g.run(ctx, args...)
if err != nil {
return nil, err
}
return parseGitLog(out), nil
}
// latestTag returns the most recent reachable tag, or "" when none exists.
func (g *GitCommitSource) latestTag(ctx context.Context) (string, error) {
out, err := g.run(ctx, "describe", "--tags", "--abbrev=0")
if err != nil {
// No tags is not an error: fall back to full history.
if strings.Contains(err.Error(), "No names found") ||
strings.Contains(err.Error(), "No tags can describe") ||
strings.Contains(err.Error(), "cannot describe") {
return "", nil
}
return "", err
}
return out, nil
}
func parseGitLog(out string) []Commit {
var commits []Commit
for _, rec := range strings.Split(out, gitRecordSep) {
rec = strings.Trim(rec, "\n")
if strings.TrimSpace(rec) == "" {
continue
}
fields := strings.SplitN(rec, gitFieldSep, 4)
if len(fields) < 4 {
continue
}
commits = append(commits, ParseCommit(
strings.TrimSpace(fields[0]),
strings.TrimSpace(fields[1]),
strings.TrimSpace(fields[2]),
fields[3],
))
}
return commits
}