Files
go-conventionalcommit/message.go

179 lines
4.1 KiB
Go

package conventionalcommit
import (
"errors"
"fmt"
"regexp"
"strings"
)
var (
Err = errors.New("conventionalcommit")
ErrEmptyMessage = fmt.Errorf("%w: empty message", Err)
)
// HeaderToken will match a Conventional Commit formatted subject line, to
// extract type, scope, breaking change (bool), and description.
//
// It is intentionally VERY forgiving so as to be able to extract the various
// parts even when things aren't quite right.
var HeaderToken = regexp.MustCompile(
`^([^\(\)\r\n]*?)(\((.*?)\)\s*)?(!)?(\s*\:)\s(.*)$`,
)
// FooterToken will match against all variations of Conventional Commit footer
// formats.
//
// Examples of valid footer tokens:
//
// Approved-by: John Carter
// ReviewdBy: Noctis
// Fixes #49
// Reverts #SOL-42
// BREAKING CHANGE: Flux capacitor no longer exists.
// BREAKING-CHANGE: Time will flow backwads
//
// Examples of invalid footer tokens:
//
// Approved-by:
// Approved-by:John Carter
// Approved by: John Carter
// ReviewdBy: Noctis
// Fixes#49
// Fixes #
// Fixes 49
// BREAKING CHANGE:Flux capacitor no longer exists.
// Breaking Change: Flux capacitor no longer exists.
// Breaking-Change: Time will flow backwads
//
var FooterToken = regexp.MustCompile(
`^([\w-]+|BREAKING[\s-]CHANGE)(?:\s*(:)\s+|\s+(#))(.+)$`,
)
// Message represents a Conventional Commit message in a structured way.
type Message struct {
// Type indicates what kind of a change the commit message describes.
Type string
// Scope indicates the context/component/area that the change affects.
Scope string
// Description is the primary description for the commit.
Description string
// Body is the main text body of the commit message. Effectively all text
// between the subject line, and any footers if present.
Body string
// Footers are all footers which are not references or breaking changes.
Footers []*Footer
// References are all footers defined with a reference style token, for
// example:
//
// Fixes #42
References []*Reference
// Breaking is set to true if the message subject included the "!" breaking
// change indicator.
Breaking bool
// BreakingChanges includes the descriptions from all BREAKING CHANGE
// footers.
BreakingChanges []string
}
func NewMessage(buf *Buffer) (*Message, error) {
msg := &Message{}
count := buf.LineCount()
if count == 0 {
return nil, ErrEmptyMessage
}
msg.Description = buf.Head().Join("\n")
if m := HeaderToken.FindStringSubmatch(msg.Description); len(m) > 0 {
msg.Type = strings.TrimSpace(m[1])
msg.Scope = strings.TrimSpace(m[3])
msg.Breaking = m[4] == "!"
msg.Description = m[6]
}
msg.Body = buf.Body().Join("\n")
if foot := buf.Foot(); len(foot) > 0 {
footers := parseFooters(foot)
for _, f := range footers {
name := string(f.name)
value := string(f.value)
switch {
case f.ref:
msg.References = append(msg.References, &Reference{
Name: name,
Value: value,
})
case name == "BREAKING CHANGE" || name == "BREAKING-CHANGE":
msg.BreakingChanges = append(msg.BreakingChanges, value)
default:
msg.Footers = append(msg.Footers, &Footer{
Name: name,
Value: value,
})
}
}
}
return msg, nil
}
func (s *Message) IsBreakingChange() bool {
return s.Breaking || len(s.BreakingChanges) > 0
}
func parseFooters(lines Lines) []*rawFooter {
var footers []*rawFooter
footer := &rawFooter{}
for _, line := range lines {
if m := FooterToken.FindSubmatch(line.Content); m != nil {
if len(footer.name) > 0 {
footers = append(footers, footer)
}
footer = &rawFooter{}
if len(m[3]) > 0 {
footer.ref = true
footer.value = []byte{hash}
}
footer.name = m[1]
footer.value = append(footer.value, m[4]...)
} else if len(footer.name) > 0 {
footer.value = append(footer.value, lf)
footer.value = append(footer.value, line.Content...)
}
}
if len(footer.name) > 0 {
footers = append(footers, footer)
}
return footers
}
type rawFooter struct {
name []byte
value []byte
ref bool
}
type Footer struct {
Name string
Value string
}
type Reference struct {
Name string
Value string
}