pkg/format/commit.go
package format
import (
"errors"
"fmt"
"regexp"
"strings"
"github.com/b4nst/turbogit/pkg/constants"
"github.com/imdario/mergo"
)
type CommitType int
const (
NilCommit CommitType = iota
BuildCommit
CiCommit
ChoreCommit
DocCommit
FeatureCommit
FixCommit
PerfCommit
RefactorCommit
StyleCommit
TestCommit
AutoCommit
)
func (b CommitType) String() string {
return [...]string{
"",
"build",
"ci",
"chore",
"docs",
"feat",
"fix",
"perf",
"refactor",
"style",
"test",
"auto",
}[b]
}
func colorize(s string, code int) string {
return fmt.Sprintf("\x1b[38;5;%03dm%s\x1b[0m", code, s)
}
func (b CommitType) ColorString() string {
return [...]string{
colorize("", 0),
colorize("build", 200),
colorize("ci", 92),
colorize("chore", 15),
colorize("docs", 250),
colorize("feat", 2),
colorize("fix", 1),
colorize("perf", 3),
colorize("refactor", 30),
colorize("style", 6),
colorize("test", 11),
}[b]
}
func AllCommitType() []string {
return []string{
BuildCommit.String(),
CiCommit.String(),
ChoreCommit.String(),
DocCommit.String(),
FeatureCommit.String(),
FixCommit.String(),
PerfCommit.String(),
RefactorCommit.String(),
StyleCommit.String(),
TestCommit.String(),
AutoCommit.String(),
}
}
var (
buildCommitRe = regexp.MustCompile(`(?i)^b(?:uilds?)?$`)
ciCommitRe = regexp.MustCompile(`(?i)^ci$`)
choreCommitRe = regexp.MustCompile(`(?i)^ch(?:ores?)?$`)
docCommitRe = regexp.MustCompile(`(?i)^d(?:ocs?)?$`)
featureCommitRe = regexp.MustCompile(`(?i)^fe(?:at(?:ure)?s?)?$`)
fixCommitRe = regexp.MustCompile(`(?i)^fi(?:x(?:es)?)?$`)
perfCommitRe = regexp.MustCompile(`(?i)^p(?:erf(:?ormance)?s?)?$`)
refactorCommitRe = regexp.MustCompile(`(?i)^r(?:efactors?)?$`)
styleCommitRe = regexp.MustCompile(`(?i)^s(?:tyles?)?$`)
testCommitRe = regexp.MustCompile(`(?i)^t(?:ests?)?$`)
autoCommitRe = regexp.MustCompile(`(?i)^auto$`)
)
type CommitMessageOption struct {
// Commit type (optional)
Ctype CommitType
// Commit scope (optional)
Scope string
// Commit subject (required)
Description string
// Commit body (optional)
Body string
// Commit footers (optional)
Footers []string
// Breaking change flag (optional)
BreakingChanges bool
}
// Overwrite values with another CommitMessageOption.
func (cmo *CommitMessageOption) Overwrite(other *CommitMessageOption) error {
return mergo.Merge(cmo, other, mergo.WithOverride)
}
// Check CommitMessageOption compliance
func (cmo *CommitMessageOption) Check() error {
if cmo.Ctype == NilCommit {
return errors.New("A commit type is required")
}
if cmo.Description == "" {
return errors.New("A commit description is required")
}
return nil
}
// Format commit message according to https://www.conventionalcommits.org/en/v1.0.0/
func CommitMessage(o *CommitMessageOption) string {
msg := o.Ctype.String()
// Add scope if any
if o.Scope != "" {
msg += fmt.Sprintf("(%s)", o.Scope)
}
// Mark breaking changes
if o.BreakingChanges {
msg += "!"
}
// Add description
msg += fmt.Sprintf(": %s", o.Description)
// Add body if any
if o.Body != "" {
msg += constants.LINE_BREAK + constants.LINE_BREAK + o.Body
}
// Add footers if any
if len(o.Footers) > 0 {
msg += constants.LINE_BREAK
for _, f := range o.Footers {
msg += constants.LINE_BREAK + f
}
}
return msg
}
// Extract type from string
func FindCommitType(str string) CommitType {
s := []byte(str)
switch {
case buildCommitRe.Match(s):
return BuildCommit
case ciCommitRe.Match(s):
return CiCommit
case choreCommitRe.Match(s):
return ChoreCommit
case docCommitRe.Match(s):
return DocCommit
case featureCommitRe.Match(s):
return FeatureCommit
case fixCommitRe.Match(s):
return FixCommit
case perfCommitRe.Match(s):
return PerfCommit
case refactorCommitRe.Match(s):
return RefactorCommit
case styleCommitRe.Match(s):
return StyleCommit
case testCommitRe.Match(s):
return TestCommit
case autoCommitRe.Match(s):
return AutoCommit
default:
return NilCommit
}
}
func ParseCommitMsg(msg string) *CommitMessageOption {
lines := strings.Split(msg, "\n")
// First line
re := regexp.MustCompile(`(?m)^(?P<type>\w+)(?:\((?P<scope>[^)]+)\))?(?P<bc>!)?: (?P<subject>.+)$`)
match := re.FindStringSubmatch(lines[0])
if len(match) <= 0 {
return nil
}
res := make(map[string]string)
for i, name := range re.SubexpNames() {
if i != 0 && name != "" {
res[name] = match[i]
}
}
cmo := &CommitMessageOption{
Ctype: FindCommitType(res["type"]),
Description: res["subject"],
Scope: res["scope"],
BreakingChanges: res["bc"] == "!",
}
// Body and footers
re = regexp.MustCompile(`(?m)^\w+(?: #|: )`)
for _, l := range lines[1:] {
if re.MatchString(l) {
cmo.Footers = append(cmo.Footers, l)
} else {
cmo.Body += l
}
}
cmo.Body = strings.Trim(cmo.Body, "\n")
return cmo
}
type Bump int
const (
BUMP_NONE Bump = iota
BUMP_PATCH
BUMP_MINOR
BUMP_MAJOR
)
// Get the next bump that this commit message would generate
func NextBump(cmsg string, curr Bump) Bump {
if curr == BUMP_MAJOR {
return curr
}
co := ParseCommitMsg(cmsg)
if co == nil {
return curr
}
if co.BreakingChanges {
return BUMP_MAJOR
}
if co.Ctype == FeatureCommit {
return BUMP_MINOR
}
if curr < BUMP_PATCH && co.Ctype == FixCommit {
return BUMP_PATCH
}
return curr
}