b4nst/turbogit

View on GitHub
pkg/format/commit.go

Summary

Maintainability
A
0 mins
Test Coverage
C
78%
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
}