mirror of
https://github.com/romdo/go-conventionalcommit.git
synced 2026-02-19 08:06:41 +00:00
wip(parser): partly finished Message parser
This commit is contained in:
178
message.go
Normal file
178
message.go
Normal file
@@ -0,0 +1,178 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user