Skip to content

Commit 2b7fe45

Browse files
committed
wip(parser): partly finished Message parser
1 parent 758deef commit 2b7fe45

File tree

6 files changed

+464
-36
lines changed

6 files changed

+464
-36
lines changed

buffer.go

Lines changed: 1 addition & 34 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.
@@ -123,7 +90,7 @@ func NewBuffer(message []byte) *Buffer {
12390
// 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
}

line.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import (
66
)
77

88
const (
9-
lf = 10 // linefeed ("\n") character
10-
cr = 13 // carriage return ("\r") character
9+
lf = 10 // ASCII linefeed ("\n") character.
10+
cr = 13 // ASCII carriage return ("\r") character.
1111
)
1212

1313
// Line represents a single line of text defined as; A continuous sequence of

message.go

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

message_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package conventionalcommit
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestMessage_IsBreakingChange(t *testing.T) {
10+
type fields struct {
11+
Breaking bool
12+
BreakingChanges []string
13+
}
14+
tests := []struct {
15+
name string
16+
fields fields
17+
want bool
18+
}{
19+
{
20+
name: "false breaking flag, no change texts",
21+
fields: fields{
22+
Breaking: false,
23+
BreakingChanges: []string{},
24+
},
25+
want: false,
26+
},
27+
{
28+
name: "true breaking flag, no change texts",
29+
fields: fields{
30+
Breaking: true,
31+
BreakingChanges: []string{},
32+
},
33+
want: true,
34+
},
35+
{
36+
name: "false breaking flag, 1 change texts",
37+
fields: fields{
38+
Breaking: false,
39+
BreakingChanges: []string{"be careful"},
40+
},
41+
want: true,
42+
},
43+
{
44+
name: "true breaking flag, 1 change texts",
45+
fields: fields{
46+
Breaking: true,
47+
BreakingChanges: []string{"be careful"},
48+
},
49+
want: true,
50+
},
51+
{
52+
name: "false breaking flag, 3 change texts",
53+
fields: fields{
54+
Breaking: false,
55+
BreakingChanges: []string{"be careful", "oops", "ouch"},
56+
},
57+
want: true,
58+
},
59+
{
60+
name: "true breaking flag, 3 change texts",
61+
fields: fields{
62+
Breaking: true,
63+
BreakingChanges: []string{"be careful", "oops", "ouch"},
64+
},
65+
want: true,
66+
},
67+
}
68+
for _, tt := range tests {
69+
t.Run(tt.name, func(t *testing.T) {
70+
msg := &Message{
71+
Breaking: tt.fields.Breaking,
72+
BreakingChanges: tt.fields.BreakingChanges,
73+
}
74+
75+
got := msg.IsBreakingChange()
76+
77+
assert.Equal(t, tt.want, got)
78+
})
79+
}
80+
}

parse.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package conventionalcommit
2+
3+
// Parse parses a conventional commit message and returns it as a *Message
4+
// struct.
5+
func Parse(message []byte) (*Message, error) {
6+
buffer := NewBuffer(message)
7+
8+
return NewMessage(buffer)
9+
}

0 commit comments

Comments
 (0)