-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathspecification.go
More file actions
160 lines (138 loc) · 4.46 KB
/
specification.go
File metadata and controls
160 lines (138 loc) · 4.46 KB
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
package main
import (
"fmt"
"regexp"
"strings"
)
var (
// DefaultPatchTypes is the default list of commit types that should be treates as a patch change
DefaultPatchTypes = []string{"fix"}
)
// ConventionalCommit represents a commit that adheres to the conventional commits specification
type ConventionalCommit struct {
Body string `json:"body"`
BreakingChange bool `json:"breaking_change"`
CommitScope string `json:"scope"`
CommitType string `json:"type"`
Description string `json:"description"`
Footers map[string]string `json:"footers"`
}
// ParseMessages attempts to parse a slice of commit messages to a slice of
// conventional commits. Returns a slice of errors to indicate all errors
// occurred during parsing
func ParseMessages(messages []string) ([]ConventionalCommit, []error) {
commits := []ConventionalCommit{}
errs := []error{}
for _, m := range messages {
c, err := ParseMessage(m)
if err != nil {
errs = append(errs, err)
continue
}
commits = append(commits, c)
}
return commits, errs
}
// ParseMessage attempts to parse a commit message to a conventional commit
func ParseMessage(message string) (ConventionalCommit, error) {
messageLines := strings.Split(strings.TrimRight(message, "\n\t "), "\n")
commit := ConventionalCommit{
Footers: make(map[string]string),
}
currKeyValue := ""
currFooterValue := ""
inFooters := false
for i, line := range messageLines {
switch i {
case 0:
if err := parseHeader(line, &commit); err != nil {
return commit, err
}
case 1:
if line != "" {
return commit, fmt.Errorf("commit description not followed by an empty line")
}
default:
key, value := parseLineAsFooter(line)
if key != "" && value != "" {
inFooters = true
// Check if we have previously found a footer. If we have, set the current footer,
// otherwise just record it.
if currKeyValue != "" {
commit.Footers[currKeyValue] = currFooterValue
}
currKeyValue = key
currFooterValue = value
} else {
if inFooters {
currFooterValue = fmt.Sprintf("%s\n%s", currFooterValue, line)
} else {
if commit.Body == "" {
commit.Body = line
} else {
commit.Body = commit.Body + fmt.Sprintf("\n%s", line)
}
}
}
}
}
// We reached the end of the commit message, so check if we need to record the footers
if inFooters {
commit.Footers[currKeyValue] = currFooterValue
}
// Remove whitespace in the commit body
commit.Body = strings.TrimSpace(commit.Body)
// Check if a footer contained a breaking change
for footer := range commit.Footers {
if footer == "BREAKING CHANGE" || footer == "BREAKING-CHANGE" {
commit.BreakingChange = true
break
}
}
return commit, nil
}
// parseLineAsFooter attempts to parse the given line as a footer, returning both the key and the value of the header.
// If the line cannot be parsed then both return values will be empty.
func parseLineAsFooter(line string) (key, value string) {
footerRegexp := regexp.MustCompile(`^(?:(BREAKING[- ]CHANGE|(?:[A-Za-z-])+): |((?:[A-Za-z-])+) #)(.+)$`)
matches := footerRegexp.FindStringSubmatch(line)
if len(matches) != 4 {
return "", ""
}
if matches[1] == "" {
return matches[2], matches[3]
}
return matches[1], matches[3]
}
// parseHeader attempts to parse the commit description line and set the appropriate values in the the given commit
func parseHeader(header string, commit *ConventionalCommit) error {
headerRegexp := regexp.MustCompile(`^(?P<type>[A-Za-z]+)(?:\((?P<scope>[A-Za-z]+)\))?(?P<breaking>!)?: (?P<description>[\w| ]+)(?:\n\s*\n(?P<body>(?:.|\n)*)(?:\n\s+\n(?P<footers>(?:[A-Za-z-]+: (?:.|\n)*)|(?:BREAKING CHANGE: (?:.|\n)*)|(?:[A-Za-z]+ \#(?:.|\n)*)))?)?$`)
matches := headerRegexp.FindStringSubmatch(header)
if matches == nil {
return fmt.Errorf("unable to parse commit header: %s", header)
}
names := headerRegexp.SubexpNames()
for i, match := range matches {
switch names[i] {
case "type":
commit.CommitType = match
case "scope":
commit.CommitScope = match
case "description":
commit.Description = match
case "breaking":
commit.BreakingChange = (match == "!")
}
}
return nil
}
// typeIsPatch returns true if the type represents a patch increment
func typeIsPatch(t string, patchTypes []string) bool {
upperT := strings.ToUpper(t)
for _, patchType := range patchTypes {
if strings.ToUpper(patchType) == upperT {
return true
}
}
return false
}