Skip to content

Commit dac48bf

Browse files
feat!: update Commit struct
1 parent d2b0d77 commit dac48bf

6 files changed

Lines changed: 266 additions & 262 deletions

File tree

commit.go

Lines changed: 66 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,82 @@ package parser
22

33
// Commit represents a commit that adheres to the conventional commits specification
44
type Commit struct {
5-
Header Header
6-
Body string
7-
Footer Footer
5+
message string
86

9-
BreakingChange bool
7+
header string
8+
body string
9+
footer string
1010

11-
FullCommit string
11+
commitType string
12+
scope string
13+
description string
14+
notes []Note
15+
16+
isBreakingChange bool
1217
}
1318

14-
// Header represents Header in commit message
15-
type Header struct {
16-
Type string
17-
Scope string
18-
Description string
19+
// Message returns input commit message
20+
func (c *Commit) Message() string {
21+
return c.message
22+
}
1923

20-
FullHeader string
24+
// Header returns header of the commit
25+
func (c *Commit) Header() string {
26+
return c.header
2127
}
2228

23-
// Footer represents Footer in commit message
24-
type Footer struct {
25-
Notes []FooterNote
29+
// Body returns body of the commit
30+
func (c *Commit) Body() string {
31+
return c.body
32+
}
33+
34+
// Footer returns footer of the commit
35+
func (c *Commit) Footer() string {
36+
return c.footer
37+
}
38+
39+
// Type returns type of the commit
40+
func (c *Commit) Type() string {
41+
return c.commitType
42+
}
43+
44+
// Scope returns scope of the commit
45+
func (c *Commit) Scope() string {
46+
return c.scope
47+
}
48+
49+
// Description returns description of the commit
50+
func (c *Commit) Description() string {
51+
return c.description
52+
}
53+
54+
// Notes returns footer notes of the commit
55+
func (c *Commit) Notes() []Note {
56+
return c.notes
57+
}
58+
59+
// IsBreakingChange returns true if breaking change
60+
func (c *Commit) IsBreakingChange() bool {
61+
return c.isBreakingChange
62+
}
63+
64+
// Note represents one footer note
65+
type Note struct {
66+
token string
67+
value string
68+
}
2669

27-
FullFooter string
70+
func newNote(token, value string) Note {
71+
return Note{
72+
token: token,
73+
value: value,
74+
}
2875
}
2976

30-
// FooterNote represents one footer note in Footer
31-
type FooterNote struct {
32-
Token string
33-
Value string
77+
func (n *Note) Token() string {
78+
return n.token
3479
}
3580

36-
func newFooterNote(token, value string) FooterNote {
37-
return FooterNote{Token: token, Value: value}
81+
func (n *Note) Value() string {
82+
return n.value
3883
}

errors.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package parser
2+
3+
import "errors"
4+
5+
var (
6+
errHeader = errors.New("unable to parse commit header")
7+
errNoBlankLine = errors.New("commit description not followed by an empty line")
8+
)
9+
10+
// IsHeaderErr checks if given error is header parse error
11+
func IsHeaderErr(err error) bool {
12+
return errors.Is(err, errHeader)
13+
}
14+
15+
// IsNoBlankLineErr checks if given error is no new line error
16+
func IsNoBlankLineErr(err error) bool {
17+
return errors.Is(err, errNoBlankLine)
18+
}

example_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@ By: John Doe`
2323
}
2424
fmt.Printf("%#v", commit)
2525

26-
// Output: &parser.Commit{Header:parser.Header{Type:"feat", Scope:"scope", Description:"description", FullHeader:"feat(scope): description"}, Body:"this is first line in body\n\nthis is second line in body", Footer:parser.Footer{Notes:[]parser.FooterNote{parser.FooterNote{Token:"Ref", Value:"123"}, parser.FooterNote{Token:"Date", Value:"01-01-2021"}, parser.FooterNote{Token:"By", Value:"John Doe"}}, FullFooter:"Ref #123\nDate: 01-01-2021\nBy: John Doe"}, BreakingChange:false, FullCommit:"feat(scope): description\n\nthis is first line in body\n\nthis is second line in body\n\nRef #123\nDate: 01-01-2021\nBy: John Doe"}
26+
// Output: &parser.Commit{message:"feat(scope): description\n\nthis is first line in body\n\nthis is second line in body\n\nRef #123\nDate: 01-01-2021\nBy: John Doe", header:"feat(scope): description", body:"this is first line in body\n\nthis is second line in body", footer:"Ref #123\nDate: 01-01-2021\nBy: John Doe", commitType:"feat", scope:"scope", description:"description", notes:[]parser.Note{parser.Note{token:"Ref", value:"123"}, parser.Note{token:"Date", value:"01-01-2021"}, parser.Note{token:"By", value:"John Doe"}}, isBreakingChange:false}
2727
}

parser.go

Lines changed: 57 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
package parser
33

44
import (
5-
"errors"
65
"regexp"
76
"strings"
87
)
@@ -19,39 +18,46 @@ const (
1918
footRegExStr = `^(?:(BREAKING[- ]CHANGE|(?:[A-Za-z-])+): |((?:[A-Za-z-])+) #)(.+)$`
2019
)
2120

22-
var (
23-
headerRegexp = regexp.MustCompile(headRegExStr)
24-
footerRegexp = regexp.MustCompile(footRegExStr)
25-
)
26-
27-
var (
28-
errHeader = errors.New("unable to parse commit header")
29-
errNoBlankLine = errors.New("commit description not followed by an empty line")
30-
)
21+
var defParser = newParser()
3122

3223
// Parse attempts to parse a commit message to a conventional commit
3324
func Parse(message string) (*Commit, error) {
25+
return defParser.parse(message)
26+
}
27+
28+
type parser struct {
29+
headerRegex, footerRegex *regexp.Regexp
30+
}
31+
32+
func newParser() *parser {
33+
headerRegex := regexp.MustCompile(headRegExStr)
34+
footerRegex := regexp.MustCompile(footRegExStr)
35+
36+
return &parser{
37+
headerRegex: headerRegex,
38+
footerRegex: footerRegex,
39+
}
40+
}
41+
42+
func (p *parser) parse(message string) (*Commit, error) {
43+
c := &Commit{
44+
message: message,
45+
}
46+
3447
message = strings.TrimRight(message, "\n\t ")
3548
messageLines := strings.Split(message, "\n")
3649

37-
commit := &Commit{
38-
FullCommit: message,
39-
}
4050
currKeyValue := ""
4151
currFooterValue := ""
4252

43-
foot := Footer{}
44-
4553
inFooters := false
4654
for i, msgLine := range messageLines {
4755
// First Line
4856
if i == 0 {
49-
head, isBreak, err := parseHeader(msgLine)
57+
err := p.parseHeader(c, msgLine)
5058
if err != nil {
5159
return nil, err
5260
}
53-
commit.Header = head
54-
commit.BreakingChange = isBreak
5561
continue
5662
}
5763

@@ -64,60 +70,62 @@ func Parse(message string) (*Commit, error) {
6470
}
6571

6672
// Remaining Line
67-
key, value, isFooter := parseLineAsFooter(msgLine)
73+
key, value, isFooter := p.parseLineAsFooter(msgLine)
74+
// Is Footer
6875
if isFooter {
6976
inFooters = true
7077

7178
// Check if we have previously found a footer. If we have, set the current footer,
7279
// otherwise just record it.
7380
if currKeyValue != "" {
74-
foot.Notes = append(foot.Notes, newFooterNote(currKeyValue, currFooterValue))
75-
foot.FullFooter += messageLines[i-1] + "\n" // add previous line to FullFooter
81+
c.notes = append(c.notes, newNote(currKeyValue, currFooterValue))
82+
c.footer += messageLines[i-1] + "\n" // add previous line to FullFooter
7683
}
7784

7885
currKeyValue = key
7986
currFooterValue = value
87+
continue
88+
}
89+
90+
// Not a Footer Line
91+
if inFooters {
92+
currFooterValue = currFooterValue + "\n" + msgLine
8093
} else {
81-
if inFooters {
82-
currFooterValue = currFooterValue + "\n" + msgLine
94+
if c.body == "" {
95+
c.body = msgLine
8396
} else {
84-
if commit.Body == "" {
85-
commit.Body = msgLine
86-
} else {
87-
commit.Body += "\n" + msgLine
88-
}
97+
c.body += "\n" + msgLine
8998
}
9099
}
91100
}
92101

93102
// We reached the end of the commit message, so check if we need to record the footers
94103
if inFooters {
95-
foot.Notes = append(foot.Notes, newFooterNote(currKeyValue, currFooterValue))
96-
foot.FullFooter += messageLines[len(messageLines)-1]
104+
c.notes = append(c.notes, newNote(currKeyValue, currFooterValue))
105+
c.footer += messageLines[len(messageLines)-1]
97106
}
98107

99108
// Remove whitespace in the Full Footer
100-
foot.FullFooter = strings.TrimSpace(foot.FullFooter)
109+
c.footer = strings.TrimSpace(c.footer)
101110

102111
// Remove whitespace in the commit body
103-
commit.Body = strings.TrimSpace(commit.Body)
104-
commit.Footer = foot
112+
c.body = strings.TrimSpace(c.body)
105113

106114
// Check if a footer contains a breaking change
107-
for _, footer := range commit.Footer.Notes {
108-
if footer.Token == "BREAKING CHANGE" || footer.Token == "BREAKING-CHANGE" {
109-
commit.BreakingChange = true
115+
for _, note := range c.notes {
116+
if note.Token() == "BREAKING CHANGE" || note.Token() == "BREAKING-CHANGE" {
117+
c.isBreakingChange = true
110118
break
111119
}
112120
}
113121

114-
return commit, nil
122+
return c, nil
115123
}
116124

117125
// parseLineAsFooter attempts to parse the given line as a footer, returning both the key and the value of the header.
118126
// If the line cannot be parsed then isFooter is false
119-
func parseLineAsFooter(line string) (key, value string, isFooter bool) {
120-
matches := footerRegexp.FindStringSubmatch(line)
127+
func (p *parser) parseLineAsFooter(line string) (key, value string, isFooter bool) {
128+
matches := p.footerRegex.FindStringSubmatch(line)
121129
if len(matches) != 4 {
122130
return "", "", false
123131
}
@@ -129,42 +137,28 @@ func parseLineAsFooter(line string) (key, value string, isFooter bool) {
129137
}
130138

131139
// parseHeader attempts to parse the commit description line and set the appropriate values in the the given commit
132-
func parseHeader(header string) (Header, bool, error) {
133-
matches := headerRegexp.FindStringSubmatch(header)
140+
func (p *parser) parseHeader(c *Commit, header string) error {
141+
matches := p.headerRegex.FindStringSubmatch(header)
134142
if matches == nil {
135-
return Header{}, false, errHeader
143+
return errHeader
136144
}
137145

138-
head := Header{
139-
FullHeader: header,
140-
}
141-
142-
isBreakingChange := false
146+
c.header = header
143147

144-
names := headerRegexp.SubexpNames()
148+
names := p.headerRegex.SubexpNames()
145149
for i, match := range matches {
146150
switch names[i] {
147151
case "type":
148-
head.Type = match
152+
c.commitType = match
149153
case "scope":
150154
// TODO: comma separated multiple scopes?
151-
head.Scope = match
155+
c.scope = match
152156
case "description":
153-
head.Description = match
157+
c.description = match
154158
case "breaking":
155-
isBreakingChange = (match == "!")
159+
c.isBreakingChange = (match == "!")
156160
}
157161
}
158162

159-
return head, isBreakingChange, nil
160-
}
161-
162-
// IsHeaderErr checks if given error is header parse error
163-
func IsHeaderErr(err error) bool {
164-
return errors.Is(err, errHeader)
165-
}
166-
167-
// IsNoBlankLineErr checks if given error is no new line error
168-
func IsNoBlankLineErr(err error) bool {
169-
return errors.Is(err, errNoBlankLine)
163+
return nil
170164
}

parser_header_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@ func TestParseHeaderValid(t *testing.T) {
1919
"1245#feat1234(@scope/scope1,scope2): description, \n\n body 1 2, 3 and 4?",
2020
}
2121

22+
p := newParser()
23+
2224
for index, validCase := range validCases {
2325

2426
testName := "case#" + strconv.Itoa(index+1)
2527
t.Run(testName, func(innerT *testing.T) {
2628
headerLine := strings.Split(validCase, "\n")[0]
27-
_, _, err := parseHeader(headerLine)
29+
c := &Commit{}
30+
err := p.parseHeader(c, headerLine)
2831
if err != nil {
2932
innerT.Error("parseHeader failed for", headerLine, err)
3033
return
@@ -47,11 +50,14 @@ func TestParseHeaderInvalid(t *testing.T) {
4750
`feat(scope))!: A description with name.txt`,
4851
}
4952

53+
p := newParser()
54+
5055
for index, validCase := range validCases {
5156
testName := "case#" + strconv.Itoa(index+1)
5257
t.Run(testName, func(innerT *testing.T) {
5358
headerLine := strings.Split(validCase, "\n")[0]
54-
_, _, err := parseHeader(headerLine)
59+
c := &Commit{}
60+
err := p.parseHeader(c, headerLine)
5561
if err == nil {
5662
innerT.Error("parseHeader passed without error for", headerLine)
5763
}

0 commit comments

Comments
 (0)