-
Notifications
You must be signed in to change notification settings - Fork 0
/
log.go
257 lines (219 loc) · 7.74 KB
/
log.go
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
package git
import (
"bufio"
"fmt"
"strings"
"github.com/purpleclay/gitz/scan"
)
// LogOption provides a way for setting specific options during a log operation.
// Each supported option can customize the way the log history of the current
// repository (working directory) is processed before retrieval
type LogOption func(*logOptions)
type logOptions struct {
RefRange string
LogPaths []string
SkipParse bool
SkipCount int
TakeCount int
Matches []string
InverseMatch bool
MatchAll bool
}
// WithRef provides a starting point other than HEAD (most recent commit)
// when retrieving the log history of the current repository (working
// directory). Typically a reference can be either a commit hash, branch
// name or tag. The output of this option will typically be a shorter,
// fine-tuned history. This option is mutually exclusive with
// [WithRefRange]. All leading and trailing whitespace are trimmed
// from the reference, allowing empty references to be ignored
func WithRef(ref string) LogOption {
return func(opts *logOptions) {
opts.RefRange = strings.TrimSpace(ref)
}
}
// WithRefRange provides both a start and end point when retrieving a
// focused snapshot of the log history from the current repository
// (working directory). Typically a reference can be either a commit
// hash, branch name or tag. The output of this option will be a shorter,
// fine-tuned history, for example, the history between two tags.
// This option is mutually exclusive with [WithRef]. All leading
// and trailing whitespace are trimmed from the references, allowing
// empty references to be ignored
func WithRefRange(fromRef string, toRef string) LogOption {
return func(opts *logOptions) {
from := strings.TrimSpace(fromRef)
if from == "" {
from = "HEAD"
}
to := strings.TrimSpace(toRef)
if to != "" {
to = fmt.Sprintf("...%s", to)
}
opts.RefRange = fmt.Sprintf("%s%s", from, to)
}
}
// WithPaths allows the log history to be retrieved for any number of
// files and folders within the current repository (working directory).
// Only commits that have had a direct impact on those files and folders
// will be retrieved. Paths to files and folders are relative to the
// root of the repository. All leading and trailing whitespace will be
// trimmed from the file paths, allowing empty paths to be ignored.
//
// A relative path can be resolved using [ToRelativePath].
func WithPaths(paths ...string) LogOption {
return func(opts *logOptions) {
opts.LogPaths = trim(paths...)
}
}
// WithRawOnly ensures only the raw output from the git log of the current
// repository (working directory) is retrieved. No post-processing is
// carried out, resulting in an empty [Log.Commits] slice
func WithRawOnly() LogOption {
return func(opts *logOptions) {
opts.SkipParse = true
}
}
// WithSkip skips any number of most recent commits from within the log
// history. A positive number (greater than zero) is expected. Skipping
// more commits than exists, will result in no history being retrieved.
// Skipping zero commits, will retrieve the entire log. This option has
// a higher order of precedence than [git.WithTake]
func WithSkip(n int) LogOption {
return func(opts *logOptions) {
opts.SkipCount = n
}
}
// WithTake limits the number of commits that will be output within the
// log history. A positive number (greater than zero) is expected. Taking
// more commits than exists, has the same effect as retrieving the entire
// log. Taking zero commits, will retrieve an empty log. This option has
// a lower order of precedence than [git.WithSkip]
func WithTake(n int) LogOption {
return func(opts *logOptions) {
opts.TakeCount = n
}
}
// WithGrep limits the number of commits that will be output within the
// log history to any with a log message that contains one of the provided
// matches (regular expressions). All leading and trailing whitespace
// will be trimmed, allowing empty matches to be ignored
func WithGrep(matches ...string) LogOption {
return func(opts *logOptions) {
opts.Matches = trim(matches...)
}
}
// WithInvertGrep limits the number of commits that will be output within
// the log history to any with a log message that does not contain one of
// the provided matches (regular expressions). All leading and trailing
// whitespace will be trimmed, allowing empty matches to be ignored
func WithInvertGrep(matches ...string) LogOption {
return func(opts *logOptions) {
WithGrep(matches...)(opts)
opts.InverseMatch = true
}
}
// WithMatchAll when used in combination with [git.WithGrep] will limit
// the number of returned commits to those whose log message contains all
// of the provided matches (regular expressions)
func WithMatchAll() LogOption {
return func(opts *logOptions) {
opts.MatchAll = true
}
}
// Log represents a snapshot of commit history from a repository
type Log struct {
// Raw contains the raw commit log
Raw string
// Commits contains the optionally parsed commit log. By default
// the parsed history will always be present, unless the
// [WithRawOnly] option is provided during retrieval
Commits []LogEntry
}
// LogEntry represents a single parsed entry from within the commit
// history of a repository
type LogEntry struct {
// Hash contains the unique identifier associated with the commit
Hash string
// AbbrevHash contains the seven character abbreviated commit hash
AbbrevHash string
// Message contains the message associated with the commit
Message string
}
// Log retrieves the commit log of the current repository (working directory)
// in an easy-to-parse format. Options can be provided to customize log
// retrieval, creating a targeted snapshot. By default, the entire history
// from the repository HEAD (most recent commit) will be retrieved. The logs
// are generated using the default git options:
//
// git log --pretty='format:> %H %B%-N' --no-color
func (c *Client) Log(opts ...LogOption) (*Log, error) {
options := &logOptions{
// Disable both counts by default
SkipCount: disabledNumericOption,
TakeCount: disabledNumericOption,
}
for _, opt := range opts {
opt(options)
}
// Build command based on the provided options
var logCmd strings.Builder
logCmd.WriteString("git log ")
if options.SkipCount > 0 {
logCmd.WriteString(" ")
logCmd.WriteString(fmt.Sprintf("--skip %d", options.SkipCount))
}
if options.TakeCount > disabledNumericOption {
logCmd.WriteString(" ")
logCmd.WriteString(fmt.Sprintf("-n%d", options.TakeCount))
}
if len(options.Matches) > 0 {
for _, match := range options.Matches {
logCmd.WriteString(" ")
logCmd.WriteString(fmt.Sprintf("--grep %s", match))
}
}
if options.InverseMatch {
logCmd.WriteString(" --invert-grep")
}
if options.MatchAll {
logCmd.WriteString(" --all-match")
}
if options.RefRange != "" {
logCmd.WriteString(" ")
logCmd.WriteString(options.RefRange)
}
logCmd.WriteString(" --pretty='format:> %H %B%-N' --no-color")
if len(options.LogPaths) > 0 {
logCmd.WriteString(" --")
for _, path := range options.LogPaths {
logCmd.WriteString(fmt.Sprintf(" '%s'", path))
}
}
out, err := c.exec(logCmd.String())
if err != nil {
return nil, err
}
log := &Log{Raw: out}
// Support the option to skip parsing of the log into a structured format
if !options.SkipParse {
log.Commits = parseLog(out)
}
return log, nil
}
func parseLog(log string) []LogEntry {
var entries []LogEntry
scanner := bufio.NewScanner(strings.NewReader(log))
scanner.Split(scan.PrefixedLines('>'))
for scanner.Scan() {
// Expected format of log from using the --online format is: <hash><space><message>
if hash, msg, found := strings.Cut(scanner.Text(), " "); found {
msg = cleanLineEndings(msg)
entries = append(entries, LogEntry{
Hash: hash,
AbbrevHash: hash[:7],
Message: msg,
})
}
}
return entries
}