diff --git a/README.md b/README.md index fce07fc..4f25f2f 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ branches: # git branches config - master - main - developer + skip-detached: false # set true if a detached branch should be ignored on commit message validation commit-message: types: # supported commit types @@ -203,7 +204,7 @@ COMMIT_MSG_FILE=$1 COMMIT_SOURCE=$2 SHA1=$3 -git sv vcm --path "$(pwd)" --file $COMMIT_MSG_FILE --source $COMMIT_SOURCE +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! diff --git a/cmd/git-sv/config.go b/cmd/git-sv/config.go index 42f8e98..ab587f4 100644 --- a/cmd/git-sv/config.go +++ b/cmd/git-sv/config.go @@ -62,6 +62,7 @@ func loadConfig(filepath string) (Config, error) { } func defaultConfig() Config { + skipDetached := false return Config{ Version: "1.0", Versioning: sv.VersioningConfig{ @@ -77,6 +78,7 @@ func defaultConfig() Config { SuffixRegex: "(-.*)?", DisableIssue: false, Skip: []string{"master", "main", "developer"}, + SkipDetached: &skipDetached, }, CommitMessage: sv.CommitMessageConfig{ Types: []string{"build", "ci", "chore", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test"}, diff --git a/cmd/git-sv/handlers.go b/cmd/git-sv/handlers.go index 206f194..efe94e2 100644 --- a/cmd/git-sv/handlers.go +++ b/cmd/git-sv/handlers.go @@ -371,8 +371,15 @@ func changelogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnP func validateCommitMessageHandler(git sv.Git, messageProcessor sv.MessageProcessor) func(c *cli.Context) error { return func(c *cli.Context) error { branch := git.Branch() - if messageProcessor.SkipBranch(branch) { - warn("commit message validation skipped, branch in ignore list...") + detached, derr := git.IsDetached() + + if messageProcessor.SkipBranch(branch, derr == nil && detached) { + warn("commit message validation skipped, branch in ignore list or detached...") + return nil + } + + if source := c.String("source"); source == "merge" { + warn("commit message validation skipped, ignoring source: %s...", source) return nil } diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go index d39f5c5..75d69b5 100644 --- a/cmd/git-sv/main.go +++ b/cmd/git-sv/main.go @@ -4,6 +4,7 @@ import ( "log" "os" "path/filepath" + "reflect" "sv4git/sv" "github.com/imdario/mergo" @@ -27,7 +28,7 @@ func main() { if envCfg.Home != "" { if homeCfg, err := loadConfig(filepath.Join(envCfg.Home, configFilename)); err == nil { - if merr := mergo.Merge(&cfg, homeCfg, mergo.WithOverride); merr != nil { + if merr := mergo.Merge(&cfg, homeCfg, mergo.WithOverride, mergo.WithTransformers(&nullTransformer{})); merr != nil { log.Fatal(merr) } } @@ -39,7 +40,7 @@ func main() { } if repoCfg, err := loadConfig(filepath.Join(repoPath, repoConfigFilename)); err == nil { - if merr := mergo.Merge(&cfg, repoCfg, mergo.WithOverride); merr != nil { + if merr := mergo.Merge(&cfg, repoCfg, mergo.WithOverride, mergo.WithTransformers(&nullTransformer{})); merr != nil { log.Fatal(merr) } if len(repoCfg.ReleaseNotes.Headers) > 0 { // mergo is merging maps, headers will be overwritten @@ -144,7 +145,7 @@ func main() { { Name: "validate-commit-message", Aliases: []string{"vcm"}, - Usage: "use as prepare-commit-message hook to validate message", + Usage: "use as prepare-commit-message hook to validate and enhance commit message", Action: validateCommitMessageHandler(git, messageProcessor), Flags: []cli.Flag{ &cli.StringFlag{Name: "path", Required: true, Usage: "git working directory"}, @@ -159,3 +160,18 @@ func main() { log.Fatal(apperr) } } + +type nullTransformer struct { +} + +func (t *nullTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error { + if typ.Kind() == reflect.Ptr { + return func(dst, src reflect.Value) error { + if dst.CanSet() && !src.IsNil() { + dst.Set(src) + } + return nil + } + } + return nil +} diff --git a/sv/config.go b/sv/config.go index cba5234..4f86b10 100644 --- a/sv/config.go +++ b/sv/config.go @@ -43,6 +43,7 @@ type BranchesConfig struct { SuffixRegex string `yaml:"suffix"` DisableIssue bool `yaml:"disable-issue"` Skip []string `yaml:"skip"` + SkipDetached *bool `yaml:"skip-detached"` } // ==== Versioning ==== diff --git a/sv/git.go b/sv/git.go index 0a130c7..0b94c13 100644 --- a/sv/git.go +++ b/sv/git.go @@ -3,6 +3,7 @@ package sv import ( "bufio" "bytes" + "errors" "fmt" "os" "os/exec" @@ -25,6 +26,7 @@ type Git interface { Tag(version semver.Version) error Tags() ([]GitTag, error) Branch() string + IsDetached() (bool, error) } // GitCommitLog description of a single commit log @@ -154,6 +156,19 @@ func (GitImpl) Branch() string { return strings.TrimSpace(strings.Trim(string(out), "\n")) } +// IsDetached check if is detached. +func (GitImpl) IsDetached() (bool, error) { + cmd := exec.Command("git", "symbolic-ref", "-q", "HEAD") + out, err := cmd.CombinedOutput() + if output := string(out); err != nil { //-q: do not issue an error message if the is not a symbolic ref, but a detached HEAD; instead exit with non-zero status silently. + if output == "" { + return true, nil + } + return false, errors.New(output) + } + return false, nil +} + func parseTagsOutput(input string) ([]GitTag, error) { scanner := bufio.NewScanner(strings.NewReader(input)) var result []GitTag diff --git a/sv/message.go b/sv/message.go index 2208b06..53fc2bc 100644 --- a/sv/message.go +++ b/sv/message.go @@ -2,7 +2,6 @@ package sv import ( "bufio" - "errors" "fmt" "regexp" "strings" @@ -48,7 +47,7 @@ func (m CommitMessage) BreakingMessage() string { // MessageProcessor interface. type MessageProcessor interface { - SkipBranch(branch string) bool + SkipBranch(branch string, detached bool) bool Validate(message string) error Enhance(branch string, message string) (string, error) IssueID(branch string) (string, error) @@ -71,8 +70,8 @@ type MessageProcessorImpl struct { } // SkipBranch check if branch should be ignored. -func (p MessageProcessorImpl) SkipBranch(branch string) bool { - return contains(branch, p.branchesCfg.Skip) +func (p MessageProcessorImpl) SkipBranch(branch string, detached bool) bool { + return contains(branch, p.branchesCfg.Skip) || (p.branchesCfg.SkipDetached != nil && *p.branchesCfg.SkipDetached && detached) } // Validate commit message. @@ -81,7 +80,7 @@ func (p MessageProcessorImpl) Validate(message string) error { msg := p.Parse(subject, body) if !regexp.MustCompile("^[a-z+]+(\\(.+\\))?!?: .+$").MatchString(subject) { - return errors.New("message should be valid according with conventional commits") + return fmt.Errorf("subject [%s] should be valid according with conventional commits", subject) } if msg.Type == "" || !contains(msg.Type, p.messageCfg.Types) { diff --git a/sv/message_test.go b/sv/message_test.go index 2bdeb2a..7a5f5a2 100644 --- a/sv/message_test.go +++ b/sv/message_test.go @@ -25,10 +25,13 @@ var ccfgWithScope = CommitMessageConfig{ Issue: CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"}, } -var bcfg = BranchesConfig{ - PrefixRegex: "([a-z]+\\/)?", - SuffixRegex: "(-.*)?", - Skip: []string{"develop", "master"}, +func newBranchCfg(skipDetached bool) BranchesConfig { + return BranchesConfig{ + PrefixRegex: "([a-z]+\\/)?", + SuffixRegex: "(-.*)?", + Skip: []string{"develop", "master"}, + SkipDetached: &skipDetached, + } } // messages samples start @@ -67,6 +70,30 @@ BREAKING CHANGE: refactor to use JavaScript features not available in Node 6.` // multiline samples end +func TestMessageProcessorImpl_SkipBranch(t *testing.T) { + tests := []struct { + name string + bcfg BranchesConfig + branch string + detached bool + want bool + }{ + {"normal branch", newBranchCfg(false), "JIRA-123", false, false}, + {"dont ignore detached branch", newBranchCfg(false), "JIRA-123", true, false}, + {"ignore branch on skip list", newBranchCfg(false), "master", false, true}, + {"ignore detached branch", newBranchCfg(true), "JIRA-123", true, true}, + {"null skip detached", BranchesConfig{Skip: []string{}}, "JIRA-123", true, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := NewMessageProcessor(ccfg, tt.bcfg) + if got := p.SkipBranch(tt.branch, tt.detached); got != tt.want { + t.Errorf("MessageProcessorImpl.SkipBranch() = %v, want %v", got, tt.want) + } + }) + } +} + func TestMessageProcessorImpl_Validate(t *testing.T) { tests := []struct { name string @@ -94,7 +121,7 @@ func TestMessageProcessorImpl_Validate(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - p := NewMessageProcessor(tt.cfg, bcfg) + p := NewMessageProcessor(tt.cfg, newBranchCfg(false)) if err := p.Validate(tt.message); (err != nil) != tt.wantErr { t.Errorf("MessageProcessorImpl.Validate() error = %v, wantErr %v", err, tt.wantErr) } @@ -103,7 +130,7 @@ func TestMessageProcessorImpl_Validate(t *testing.T) { } func TestMessageProcessorImpl_Enhance(t *testing.T) { - p := NewMessageProcessor(ccfg, bcfg) + p := NewMessageProcessor(ccfg, newBranchCfg(false)) tests := []struct { name string @@ -136,7 +163,7 @@ func TestMessageProcessorImpl_Enhance(t *testing.T) { } func TestMessageProcessorImpl_IssueID(t *testing.T) { - p := NewMessageProcessor(ccfg, bcfg) + p := NewMessageProcessor(ccfg, newBranchCfg(false)) tests := []struct { name string @@ -248,7 +275,7 @@ Jira: JIRA-999 Refs #123` func TestMessageProcessorImpl_Parse(t *testing.T) { - p := NewMessageProcessor(ccfg, bcfg) + p := NewMessageProcessor(ccfg, newBranchCfg(false)) tests := []struct { name string @@ -275,7 +302,7 @@ func TestMessageProcessorImpl_Parse(t *testing.T) { } func TestMessageProcessorImpl_Format(t *testing.T) { - p := NewMessageProcessor(ccfg, bcfg) + p := NewMessageProcessor(ccfg, newBranchCfg(false)) tests := []struct { name string