Skip to content

Commit bf44c4a

Browse files
committed
wip(parser): partly finished Message parser
1 parent 5174ed3 commit bf44c4a

File tree

7 files changed

+759
-40
lines changed

7 files changed

+759
-40
lines changed

buffer.go

Lines changed: 18 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,5 @@
11
package conventionalcommit
22

3-
import (
4-
"regexp"
5-
)
6-
7-
// footerToken will match against all variations of Conventional Commit footer
8-
// formats.
9-
//
10-
// Examples of valid footer tokens:
11-
//
12-
// Approved-by: John Carter
13-
// ReviewdBy: Noctis
14-
// Fixes #49
15-
// Reverts #SOL-42
16-
// BREAKING CHANGE: Flux capacitor no longer exists.
17-
// BREAKING-CHANGE: Time will flow backwads
18-
//
19-
// Examples of invalid footer tokens:
20-
//
21-
// Approved-by:
22-
// Approved-by:John Carter
23-
// Approved by: John Carter
24-
// ReviewdBy: Noctis
25-
// Fixes#49
26-
// Fixes #
27-
// Fixes 49
28-
// BREAKING CHANGE:Flux capacitor no longer exists.
29-
// Breaking Change: Flux capacitor no longer exists.
30-
// Breaking-Change: Time will flow backwads
31-
//
32-
var footerToken = regexp.MustCompile(
33-
`^(?:([\w-]+)\s+(#.+)|([\w-]+|BREAKING[\s-]CHANGE):\s+(.+))$`,
34-
)
35-
363
// Buffer represents a commit message in a more structured form than a simple
374
// string or byte slice. This makes it easier to process a message for the
385
// purposes of extracting detailed information, linting, and formatting.
@@ -119,11 +86,11 @@ func NewBuffer(message []byte) *Buffer {
11986
lastLen++
12087
}
12188

122-
// If last paragraph starts with a Convention Commit footer token, it is the
123-
// foot section, otherwise it is part of the body.
89+
// If last paragraph starts with a Conventional Commit footer token, it is
90+
// the foot section, otherwise it is part of the body.
12491
if lastLen > 0 {
12592
line := buf.lines[buf.lastLine-lastLen+1]
126-
if footerToken.Match(line.Content) {
93+
if FooterToken.Match(line.Content) {
12794
buf.footLen = lastLen
12895
}
12996
}
@@ -176,6 +143,15 @@ func (s *Buffer) Lines() Lines {
176143
return s.lines[s.firstLine : s.lastLine+1]
177144
}
178145

146+
// LinesRaw returns all lines of the buffer including any blank lines at the
147+
// beginning and end of the buffer.
148+
func (s *Buffer) LinesRaw() Lines {
149+
return s.lines
150+
}
151+
152+
// LineCount returns number of lines in the buffer after discarding blank lines
153+
// from the beginning and end of the buffer. Effectively counting all lines from
154+
// the first to the last line which contain any non-whitespace characters.
179155
func (s *Buffer) LineCount() int {
180156
if s.headLen == 0 {
181157
return 0
@@ -184,6 +160,12 @@ func (s *Buffer) LineCount() int {
184160
return (s.lastLine + 1) - s.firstLine
185161
}
186162

163+
// LineCountRaw returns the number of lines in the buffer including any blank
164+
// lines at the beginning and end of the buffer.
165+
func (s *Buffer) LineCountRaw() int {
166+
return len(s.lines)
167+
}
168+
187169
// Bytes renders the Buffer back into a byte slice, without any leading or
188170
// trailing whitespace lines. Leading whitespace on the first line which
189171
// contains non-whitespace characters is retained. It is only whole lines

buffer_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,55 @@ func BenchmarkBuffer_Lines(b *testing.B) {
994994
}
995995
}
996996

997+
func TestBuffer_LinesRaw(t *testing.T) {
998+
for _, tt := range bufferTestCases {
999+
t.Run(tt.name, func(t *testing.T) {
1000+
want := tt.wantBuffer.lines[0:]
1001+
1002+
got := tt.wantBuffer.LinesRaw()
1003+
1004+
assert.Equal(t, want, got)
1005+
})
1006+
}
1007+
}
1008+
1009+
func TestBuffer_LineCount(t *testing.T) {
1010+
for _, tt := range bufferTestCases {
1011+
t.Run(tt.name, func(t *testing.T) {
1012+
want := tt.wantLines[1]
1013+
1014+
got := tt.wantBuffer.LineCount()
1015+
1016+
assert.Equal(t, want, got)
1017+
})
1018+
}
1019+
}
1020+
1021+
func BenchmarkBuffer_LineCount(b *testing.B) {
1022+
for _, tt := range bufferTestCases {
1023+
if tt.bytes == nil {
1024+
continue
1025+
}
1026+
b.Run(tt.name, func(b *testing.B) {
1027+
for n := 0; n < b.N; n++ {
1028+
_ = tt.wantBuffer.LineCount()
1029+
}
1030+
})
1031+
}
1032+
}
1033+
1034+
func TestBuffer_LineCountRaw(t *testing.T) {
1035+
for _, tt := range bufferTestCases {
1036+
t.Run(tt.name, func(t *testing.T) {
1037+
want := len(tt.wantBuffer.lines)
1038+
1039+
got := tt.wantBuffer.LineCountRaw()
1040+
1041+
assert.Equal(t, want, got)
1042+
})
1043+
}
1044+
}
1045+
9971046
func TestBuffer_Bytes(t *testing.T) {
9981047
for _, tt := range bufferTestCases {
9991048
if tt.bytes == nil {

line.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,23 +58,23 @@ type Lines []*Line
5858
// basis.
5959
func NewLines(content []byte) Lines {
6060
r := Lines{}
61-
cLen := len(content)
61+
length := len(content)
6262

63-
if cLen == 0 {
63+
if length == 0 {
6464
return r
6565
}
6666

6767
// List of start/end offsets for each line break.
6868
var breaks [][]int
6969

7070
// Locate each line break within content.
71-
for i := 0; i < cLen; i++ {
71+
for i := 0; i < length; i++ {
7272
switch content[i] {
7373
case lf:
7474
breaks = append(breaks, []int{i, i + 1})
7575
case cr:
7676
b := []int{i, i + 1}
77-
if i+1 < cLen && content[i+1] == lf {
77+
if i+1 < length && content[i+1] == lf {
7878
b[1]++
7979
i++
8080
}

message.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package conventionalcommit
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"regexp"
7+
"strings"
8+
)
9+
10+
var (
11+
Err = errors.New("conventionalcommit")
12+
ErrEmptyMessage = fmt.Errorf("%w: empty message", Err)
13+
)
14+
15+
// HeaderToken will match a Conventional Commit formatted subject line, to
16+
// extract type, scope, breaking change (bool), and description.
17+
//
18+
// It is intentionally VERY forgiving so as to be able to extract the various
19+
// parts even when things aren't quite right.
20+
var HeaderToken = regexp.MustCompile(
21+
`^([^\(\)\r\n]*?)(\((.*?)\)\s*)?(!)?(\s*\:)\s(.*)$`,
22+
)
23+
24+
// FooterToken will match against all variations of Conventional Commit footer
25+
// formats.
26+
//
27+
// Examples of valid footer tokens:
28+
//
29+
// Approved-by: John Carter
30+
// ReviewdBy: Noctis
31+
// Fixes #49
32+
// Reverts #SOL-42
33+
// BREAKING CHANGE: Flux capacitor no longer exists.
34+
// BREAKING-CHANGE: Time will flow backwads
35+
//
36+
// Examples of invalid footer tokens:
37+
//
38+
// Approved-by:
39+
// Approved-by:John Carter
40+
// Approved by: John Carter
41+
// ReviewdBy: Noctis
42+
// Fixes#49
43+
// Fixes #
44+
// Fixes 49
45+
// BREAKING CHANGE:Flux capacitor no longer exists.
46+
// Breaking Change: Flux capacitor no longer exists.
47+
// Breaking-Change: Time will flow backwads
48+
//
49+
var FooterToken = regexp.MustCompile(
50+
`^([\w-]+|BREAKING[\s-]CHANGE)(?:\s*(:)\s+|\s+(#))(.+)$`,
51+
)
52+
53+
// Message represents a Conventional Commit message in a structured way.
54+
type Message struct {
55+
// Type indicates what kind of a change the commit message describes.
56+
Type string
57+
58+
// Scope indicates the context/component/area that the change affects.
59+
Scope string
60+
61+
// Description is the primary description for the commit.
62+
Description string
63+
64+
// Body is the main text body of the commit message. Effectively all text
65+
// between the subject line, and any footers if present.
66+
Body string
67+
68+
// Footers are all footers which are not references or breaking changes.
69+
Footers []*Footer
70+
71+
// References are all footers defined with a reference style token, for
72+
// example:
73+
//
74+
// Fixes #42
75+
References []*Reference
76+
77+
// Breaking is set to true if the message subject included the "!" breaking
78+
// change indicator.
79+
Breaking bool
80+
81+
// BreakingChanges includes the descriptions from all BREAKING CHANGE
82+
// footers.
83+
BreakingChanges []string
84+
}
85+
86+
func NewMessage(buf *Buffer) (*Message, error) {
87+
msg := &Message{}
88+
count := buf.LineCount()
89+
90+
if count == 0 {
91+
return nil, ErrEmptyMessage
92+
}
93+
94+
msg.Description = buf.Head().Join("\n")
95+
if m := HeaderToken.FindStringSubmatch(msg.Description); len(m) > 0 {
96+
msg.Type = strings.TrimSpace(m[1])
97+
msg.Scope = strings.TrimSpace(m[3])
98+
msg.Breaking = m[4] == "!"
99+
msg.Description = m[6]
100+
}
101+
102+
msg.Body = buf.Body().Join("\n")
103+
104+
if foot := buf.Foot(); len(foot) > 0 {
105+
footers := parseFooters(foot)
106+
107+
for _, f := range footers {
108+
name := string(f.name)
109+
value := string(f.value)
110+
111+
switch {
112+
case f.ref:
113+
msg.References = append(msg.References, &Reference{
114+
Name: name,
115+
Value: value,
116+
})
117+
case name == "BREAKING CHANGE" || name == "BREAKING-CHANGE":
118+
msg.BreakingChanges = append(msg.BreakingChanges, value)
119+
default:
120+
msg.Footers = append(msg.Footers, &Footer{
121+
Name: name,
122+
Value: value,
123+
})
124+
}
125+
}
126+
}
127+
128+
return msg, nil
129+
}
130+
131+
func (s *Message) IsBreakingChange() bool {
132+
return s.Breaking || len(s.BreakingChanges) > 0
133+
}
134+
135+
func parseFooters(lines Lines) []*rawFooter {
136+
var footers []*rawFooter
137+
footer := &rawFooter{}
138+
for _, line := range lines {
139+
if m := FooterToken.FindSubmatch(line.Content); m != nil {
140+
if len(footer.name) > 0 {
141+
footers = append(footers, footer)
142+
}
143+
144+
footer = &rawFooter{}
145+
if len(m[3]) > 0 {
146+
footer.ref = true
147+
footer.value = []byte{hash}
148+
}
149+
footer.name = m[1]
150+
footer.value = append(footer.value, m[4]...)
151+
} else if len(footer.name) > 0 {
152+
footer.value = append(footer.value, lf)
153+
footer.value = append(footer.value, line.Content...)
154+
}
155+
}
156+
157+
if len(footer.name) > 0 {
158+
footers = append(footers, footer)
159+
}
160+
161+
return footers
162+
}
163+
164+
type rawFooter struct {
165+
name []byte
166+
value []byte
167+
ref bool
168+
}
169+
170+
type Footer struct {
171+
Name string
172+
Value string
173+
}
174+
175+
type Reference struct {
176+
Name string
177+
Value string
178+
}

0 commit comments

Comments
 (0)