1
0
Fork 0

feat: add issue id to footer if defined on branch name

This commit is contained in:
Beatriz Vieira 2020-08-31 22:28:54 -03:00
parent 03a41a8d48
commit c323081132
6 changed files with 211 additions and 21 deletions

View file

@ -12,18 +12,20 @@ download the latest release and add the binary on your path
you can config using the environment variables you can config using the environment variables
| Variable | description | default | | Variable | description | default |
| ------------------------------ | ---------------------------------------------------------------- | ------------------------------------------------------------ | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| MAJOR_VERSION_TYPES | types used to bump major version | | | MAJOR_VERSION_TYPES | types used to bump major version | |
| MINOR_VERSION_TYPES | types used to bump minor version | feat | | MINOR_VERSION_TYPES | types used to bump minor version | feat |
| PATCH_VERSION_TYPES | types used to bump patch version | build,ci,docs,fix,perf,refactor,style,test | | PATCH_VERSION_TYPES | types used to bump patch version | build,ci,docs,fix,perf,refactor,style,test |
| INCLUDE_UNKNOWN_TYPE_AS_PATCH | force patch bump on unknown type | true | | INCLUDE_UNKNOWN_TYPE_AS_PATCH | force patch bump on unknown type | true |
| BRAKING_CHANGE_PREFIXES | list of prefixes that will be used to identify a breaking change | BREAKING CHANGE:,BREAKING CHANGES: | | BRAKING_CHANGE_PREFIXES | list of prefixes that will be used to identify a breaking change | BREAKING CHANGE:,BREAKING CHANGES: |
| ISSUEID_PREFIXES | list of prefixes that will be used to identify an issue id | jira:,JIRA:,Jira: | | ISSUEID_PREFIXES | list of prefixes that will be used to identify an issue id | jira:,JIRA:,Jira: |
| TAG_PATTERN | tag version pattern | %d.%d.%d | | TAG_PATTERN | tag version pattern | %d.%d.%d |
| RELEASE_NOTES_TAGS | release notes headers for each visible type | fix:Bug Fixes,feat:Features | | RELEASE_NOTES_TAGS | release notes headers for each visible type | fix:Bug Fixes,feat:Features |
| VALIDATE_MESSAGE_SKIP_BRANCHES | ignore branches from this list on validate commit message | master,develop | | VALIDATE_MESSAGE_SKIP_BRANCHES | ignore branches from this list on validate commit message | master,develop |
| COMMIT_MESSAGE_TYPES | list of valid commit types for commit message | build,ci,chore,docs,feat,fix,perf,refactor,revert,style,test | | COMMIT_MESSAGE_TYPES | list of valid commit types for commit message | build,ci,chore,docs,feat,fix,perf,refactor,revert,style,test |
| ISSUE_KEY_NAME | metadata key name used on validate commit message hook to enhance footer, if blank footer will not be added | jira |
| BRANCH_ISSUE_REGEX | regex to extract issue id from branch name, must have 3 groups (prefix, id, posfix), if blank footer will not be added | ^([a-z]+\\/)?([A-Z]+-[0-9]+)(-.*)? |
### Running ### Running
@ -68,6 +70,26 @@ git-sv rn -h
| validate-commit-message, vcm | use as prepare-commit-message hook to validate commit message | :heavy_check_mark: | | validate-commit-message, vcm | use as prepare-commit-message hook to validate commit message | :heavy_check_mark: |
| help, h | Shows a list of commands or help for one command | :x: | | help, h | Shows a list of commands or help for one command | :x: |
##### Use validate-commit-message as prepare-commit-msg hook
Configure your .git/hooks/prepare-commit-msg
```bash
#!/bin/sh
COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2
SHA1=$3
git sv vcm --path "$(pwd)" --file $COMMIT_MSG_FILE --source $COMMIT_SOURCE
```
tip: you can configure a directory as your global git templates using the command below, check [git config docs](https://git-scm.com/docs/git-config#Documentation/git-config.txt-inittemplateDir) for more information!
```bash
git config --global init.templatedir '<YOUR TEMPLATE DIR>'
```
## Development ## Development
### Makefile ### Makefile

View file

@ -18,6 +18,8 @@ type Config struct {
ReleaseNotesTags map[string]string `envconfig:"RELEASE_NOTES_TAGS" default:"fix:Bug Fixes,feat:Features"` ReleaseNotesTags map[string]string `envconfig:"RELEASE_NOTES_TAGS" default:"fix:Bug Fixes,feat:Features"`
ValidateMessageSkipBranches []string `envconfig:"VALIDATE_MESSAGE_SKIP_BRANCHES" default:"master,develop"` ValidateMessageSkipBranches []string `envconfig:"VALIDATE_MESSAGE_SKIP_BRANCHES" default:"master,develop"`
CommitMessageTypes []string `envconfig:"COMMIT_MESSAGE_TYPES" default:"build,ci,chore,docs,feat,fix,perf,refactor,revert,style,test"` CommitMessageTypes []string `envconfig:"COMMIT_MESSAGE_TYPES" default:"build,ci,chore,docs,feat,fix,perf,refactor,revert,style,test"`
IssueKeyName string `envconfig:"ISSUE_KEY_NAME" default:"jira"`
BranchIssueRegex string `envconfig:"BRANCH_ISSUE_REGEX" default:"^([a-z]+\\/)?([A-Z]+-[0-9]+)(-.*)?"`
} }
func loadConfig() Config { func loadConfig() Config {

View file

@ -255,6 +255,9 @@ func validateCommitMessageHandler(git sv.Git, validateMessageProcessor sv.Valida
warn("could not enhance commit message, %s", err.Error()) warn("could not enhance commit message, %s", err.Error())
return nil return nil
} }
if msg == "" {
return nil
}
if err := appendOnFile(msg, filepath); err != nil { if err := appendOnFile(msg, filepath); err != nil {
return fmt.Errorf("failed to append meta-informations on footer, error: %s", err.Error()) return fmt.Errorf("failed to append meta-informations on footer, error: %s", err.Error())

View file

@ -18,7 +18,7 @@ func main() {
semverProcessor := sv.NewSemVerCommitsProcessor(cfg.IncludeUnknownTypeAsPatch, cfg.MajorVersionTypes, cfg.MinorVersionTypes, cfg.PatchVersionTypes) semverProcessor := sv.NewSemVerCommitsProcessor(cfg.IncludeUnknownTypeAsPatch, cfg.MajorVersionTypes, cfg.MinorVersionTypes, cfg.PatchVersionTypes)
releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotesTags) releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotesTags)
outputFormatter := sv.NewOutputFormatter() outputFormatter := sv.NewOutputFormatter()
validateMessageProcessor := sv.NewValidateMessageProcessor(cfg.ValidateMessageSkipBranches, cfg.CommitMessageTypes) validateMessageProcessor := sv.NewValidateMessageProcessor(cfg.ValidateMessageSkipBranches, cfg.CommitMessageTypes, cfg.IssueKeyName, cfg.BranchIssueRegex)
app := cli.NewApp() app := cli.NewApp()
app.Name = "sv" app.Name = "sv"

View file

@ -1,6 +1,7 @@
package sv package sv
import ( import (
"bufio"
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
@ -14,17 +15,21 @@ type ValidateMessageProcessor interface {
} }
// NewValidateMessageProcessor ValidateMessageProcessorImpl constructor // NewValidateMessageProcessor ValidateMessageProcessorImpl constructor
func NewValidateMessageProcessor(skipBranches, supportedTypes []string) *ValidateMessageProcessorImpl { func NewValidateMessageProcessor(skipBranches, supportedTypes []string, issueKeyName, branchIssueRegex string) *ValidateMessageProcessorImpl {
return &ValidateMessageProcessorImpl{ return &ValidateMessageProcessorImpl{
skipBranches: skipBranches, skipBranches: skipBranches,
supportedTypes: supportedTypes, supportedTypes: supportedTypes,
issueKeyName: issueKeyName,
branchIssueRegex: branchIssueRegex,
} }
} }
// ValidateMessageProcessorImpl process validate message hook. // ValidateMessageProcessorImpl process validate message hook.
type ValidateMessageProcessorImpl struct { type ValidateMessageProcessorImpl struct {
skipBranches []string skipBranches []string
supportedTypes []string supportedTypes []string
issueKeyName string
branchIssueRegex string
} }
// SkipBranch check if branch should be ignored. // SkipBranch check if branch should be ignored.
@ -46,8 +51,47 @@ func (p ValidateMessageProcessorImpl) Validate(message string) error {
// Enhance add metadata on commit message. // Enhance add metadata on commit message.
func (p ValidateMessageProcessorImpl) Enhance(branch string, message string) (string, error) { func (p ValidateMessageProcessorImpl) Enhance(branch string, message string) (string, error) {
//TODO add issue id (branch format on varenv) if p.branchIssueRegex == "" || p.issueKeyName == "" || hasIssueID(message, p.issueKeyName) {
return "", nil return "", nil //enhance disabled
}
r, err := regexp.Compile(p.branchIssueRegex)
if err != nil {
return "", fmt.Errorf("could not compile issue regex: %s, error: %v", p.branchIssueRegex, err.Error())
}
groups := r.FindStringSubmatch(branch)
if len(groups) != 4 {
return "", fmt.Errorf("could not find issue id group with configured regex")
}
footer := fmt.Sprintf("%s: %s", p.issueKeyName, groups[2])
if !hasFooter(message) {
return "\n" + footer, nil
}
return footer, nil
}
func hasFooter(message string) bool {
r := regexp.MustCompile("^[a-zA-Z-]+: .*|^[a-zA-Z-]+ #.*|^BREAKING CHANGE: .*")
scanner := bufio.NewScanner(strings.NewReader(message))
lines := 0
for scanner.Scan() {
if lines > 0 && r.MatchString(scanner.Text()) {
return true
}
lines++
}
return false
}
func hasIssueID(message, issueKeyName string) bool {
r := regexp.MustCompile(fmt.Sprintf("(?m)^%s: .+$", issueKeyName))
return r.MatchString(message)
} }
func contains(value string, content []string) bool { func contains(value string, content []string) bool {

View file

@ -4,8 +4,46 @@ import (
"testing" "testing"
) )
var issueRegex = "^([a-z]+\\/)?([A-Z]+-[0-9]+)(-.*)?"
// messages samples start
var fullMessage = `fix: correct minor typos in code
see the issue for details
on typos fixed.
Reviewed-by: Z
Refs #133`
var fullMessageWithJira = `fix: correct minor typos in code
see the issue for details
on typos fixed.
Reviewed-by: Z
Refs #133
jira: JIRA-456`
var fullMessageRefs = `fix: correct minor typos in code
see the issue for details
on typos fixed.
Refs #133`
var subjectAndBodyMessage = `fix: correct minor typos in code
see the issue for details
on typos fixed.`
var subjectAndFooterMessage = `refactor!: drop support for Node 6
BREAKING CHANGE: refactor to use JavaScript features not available in Node 6.`
// multiline samples end
func TestValidateMessageProcessorImpl_Validate(t *testing.T) { func TestValidateMessageProcessorImpl_Validate(t *testing.T) {
p := NewValidateMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}) p := NewValidateMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", issueRegex)
tests := []struct { tests := []struct {
name string name string
@ -37,6 +75,39 @@ func TestValidateMessageProcessorImpl_Validate(t *testing.T) {
} }
} }
func TestValidateMessageProcessorImpl_Enhance(t *testing.T) {
p := NewValidateMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", issueRegex)
tests := []struct {
name string
branch string
message string
want string
wantErr bool
}{
{"issue on branch name", "JIRA-123", "fix: fix something", "\njira: JIRA-123", false},
{"issue on branch name with description", "JIRA-123-some-description", "fix: fix something", "\njira: JIRA-123", false},
{"issue on branch name with prefix", "feature/JIRA-123", "fix: fix something", "\njira: JIRA-123", false},
{"with footer", "JIRA-123", fullMessage, "jira: JIRA-123", false},
{"with issue on footer", "JIRA-123", fullMessageWithJira, "", false},
{"issue on branch name with prefix and description", "feature/JIRA-123-some-description", "fix: fix something", "\njira: JIRA-123", false},
{"no issue on branch name", "branch", "fix: fix something", "", true},
{"unexpected branch name", "feature /JIRA-123", "fix: fix something", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := p.Enhance(tt.branch, tt.message)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateMessageProcessorImpl.Enhance() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ValidateMessageProcessorImpl.Enhance() = %v, want %v", got, tt.want)
}
})
}
}
func Test_firstLine(t *testing.T) { func Test_firstLine(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@ -58,3 +129,51 @@ func Test_firstLine(t *testing.T) {
}) })
} }
} }
func Test_hasIssueID(t *testing.T) {
tests := []struct {
name string
message string
issueKeyName string
want bool
}{
{"single line without issue", "feat: something", "jira", false},
{"multi line without issue", `feat: something
yay`, "jira", false},
{"multi line without jira issue", `feat: something
jira1: JIRA-123`, "jira", false},
{"multi line with issue", `feat: something
jira: JIRA-123`, "jira", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := hasIssueID(tt.message, tt.issueKeyName); got != tt.want {
t.Errorf("hasIssueID() = %v, want %v", got, tt.want)
}
})
}
}
func Test_hasFooter(t *testing.T) {
tests := []struct {
name string
message string
want bool
}{
{"simple message", "feat: add something", false},
{"full messsage", fullMessage, true},
{"full messsage with refs", fullMessageRefs, true},
{"subject and footer message", subjectAndFooterMessage, true},
{"subject and body message", subjectAndBodyMessage, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := hasFooter(tt.message); got != tt.want {
t.Errorf("hasFooter() = %v, want %v", got, tt.want)
}
})
}
}