/
log_ent.go
150 lines (133 loc) · 3.28 KB
/
log_ent.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
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"regexp"
)
// LogEnt represents data extracted from a git log entry.
type LogEnt struct {
commit string
subject string
attrs map[string]string
mess []byte
}
// LogEntScanner scans log entries.
type LogEntScanner struct {
scanner *bufio.Scanner
err error
ent LogEnt
next [][]byte
}
// NewLogEntScanner creates a new log entry scanner around an io.Reader.
func NewLogEntScanner(r io.Reader) *LogEntScanner {
return &LogEntScanner{
scanner: bufio.NewScanner(r),
}
}
// Ent returns teh last scanned entry.
func (es *LogEntScanner) Ent() LogEnt {
return es.ent
}
// Err returns any scanning error.
func (es *LogEntScanner) Err() error {
if es.err != nil {
return es.err
}
return es.scanner.Err()
}
var (
commitPattern = regexp.MustCompile(`commit ([0-9a-fA-F]+)`)
keyValPattern = regexp.MustCompile(`(.+?):\s+(.+)`)
prMergeRegex = regexp.MustCompile(`Merge .*#(\d+) from ([^ ]+)`)
prSquashRegex = regexp.MustCompile(`(.+) +\(#(\d+)\)$`)
)
// Scan try to scan a log entry, returning true if another log entry may be
// scanned. When false is returned, either an error is set or EOF has been hit.
func (es *LogEntScanner) Scan() bool {
if es.err != nil {
return false
}
es.ent = LogEnt{
attrs: make(map[string]string),
}
if es.next == nil {
// scan "commit SHA.*" line
for es.scanner.Scan() {
if m := commitPattern.FindSubmatch(es.scanner.Bytes()); m != nil {
es.next = m
break
}
}
if es.next == nil {
return false
}
}
es.ent.commit = string(es.next[1])
return es.scanKeyVals()
}
func (es *LogEntScanner) scanKeyVals() bool {
// scan all "key: val..." contiguous lines
for es.scanner.Scan() {
if len(es.scanner.Bytes()) == 0 {
return es.scanSubject()
}
if m := keyValPattern.FindSubmatch(es.scanner.Bytes()); m != nil {
es.ent.attrs[string(m[1])] = string(m[2])
continue
}
es.err = fmt.Errorf("expected `key: val` line, instead of %q", es.scanner.Bytes())
break
}
return false
}
func (es *LogEntScanner) scanSubject() bool {
var buf bytes.Buffer
for {
// scan a message paragraph, normalizing newlines
buf.Reset()
for es.scanner.Scan() {
b := es.scanner.Bytes()
b = bytes.TrimLeft(b, " ")
if len(b) == 0 {
break
}
if buf.Len() > 0 {
buf.Write([]byte{' '})
}
buf.Write(b)
}
if buf.Len() == 0 {
return false
}
// extract an PR annotations, and then scan another paragraph for the subject
if m := prMergeRegex.FindSubmatch(buf.Bytes()); m != nil {
es.ent.attrs["prNumber"] = string(m[1])
es.ent.attrs["prFrom"] = string(m[2])
continue
}
if m := prSquashRegex.FindSubmatch(buf.Bytes()); m != nil {
es.ent.attrs["prNumber"] = string(m[2])
es.ent.subject = string(m[1])
return es.scanMessage()
}
// otherwise we've found the subject, and we can move on to the rest of the message
es.ent.subject = string(buf.String())
return es.scanMessage()
}
}
func (es *LogEntScanner) scanMessage() bool {
// scan until next "commit SHA.*" line, collecting message bytes
var buf bytes.Buffer
for es.scanner.Scan() {
if m := commitPattern.FindSubmatch(es.scanner.Bytes()); m != nil {
es.ent.mess = buf.Bytes()
es.next = m
return true
}
buf.Write(es.scanner.Bytes())
buf.Write([]byte{'\n'})
}
return false
}