mirror of
https://github.com/lukaszraczylo/semver-generator.git
synced 2026-10-13 00:50:34 +00:00
Fuzzy matching compares each keyword with every word of the commit message. A word matches if it contains the keyword's letters in order. Thus "feat" matches "fix(delegation):" and "major" matches "deps(majors):", which gives false minor and major versions. The new top-level config key "matching" accepts "fuzzy" (the default) or "regex". In regex mode every wording keyword is a Go regular expression. The tool compiles all of them when it loads the config and runs them against the full commit message, so "(?m)^" anchors a pattern to the start of a line. An invalid value, an invalid pattern, or any other config parse error in regex mode stops the tool with a non-zero exit code. It never falls back to fuzzy matching. Fuzzy mode does not change. The blacklist check moves into one helper that both modes use. A replay of 15 repositories gives the same versions as before. This change improves version precision for the repositories that opt in. Claude-Session: https://claude.ai/code/session_01Qq1gbqQek1Zjtofo4VDRdW
119 lines
3.6 KiB
Go
119 lines
3.6 KiB
Go
package utils
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
// Wording represents the keywords to look for in commit messages
|
|
type Wording struct {
|
|
Patch []string
|
|
Minor []string
|
|
Major []string
|
|
Release []string
|
|
}
|
|
|
|
// Force represents forced versioning settings
|
|
type Force struct {
|
|
Commit string
|
|
Patch int
|
|
Minor int
|
|
Major int
|
|
Existing bool
|
|
Strict bool
|
|
}
|
|
|
|
// Config represents the application configuration
|
|
type Config struct {
|
|
Wording Wording
|
|
Force Force
|
|
Blacklist []string
|
|
TagPrefixes []string // Prefixes to strip from tags before parsing (e.g., "app-", "infra-", "v")
|
|
Matching string // Effective matching mode: MatchingFuzzy (default) or MatchingRegex
|
|
Matcher KeywordMatcher
|
|
}
|
|
|
|
// ReadConfig reads the configuration from a file
|
|
func ReadConfig(file string) (*Config, error) {
|
|
config := &Config{}
|
|
|
|
viper.SetConfigFile(file)
|
|
err := viper.ReadInConfig()
|
|
if err != nil {
|
|
err = fmt.Errorf("fatal error config file: %s", err)
|
|
return config, err
|
|
}
|
|
|
|
// Read "matching" first, right after ReadInConfig, so every later parse
|
|
// error below can tell whether regex mode is in play. viper.Get (not
|
|
// GetString) lets us reject a non-scalar value, such as a YAML list or
|
|
// map, as a config error instead of silently treating it as "fuzzy".
|
|
switch rawMatching := viper.Get("matching"); v := rawMatching.(type) {
|
|
case nil:
|
|
config.Matching = MatchingFuzzy
|
|
case string:
|
|
switch v {
|
|
case "":
|
|
config.Matching = MatchingFuzzy
|
|
case MatchingFuzzy, MatchingRegex:
|
|
config.Matching = v
|
|
default:
|
|
return config, fmt.Errorf("%w: invalid matching value %q, allowed values are %q and %q", ErrInvalidMatchingConfig, v, MatchingFuzzy, MatchingRegex)
|
|
}
|
|
default:
|
|
return config, fmt.Errorf("%w: matching config must be a string, got %v (%T)", ErrInvalidMatchingConfig, v, v)
|
|
}
|
|
|
|
// In regex mode there is no fuzzy fallback: any config parse error below
|
|
// (wording/force/blacklist/tag_prefixes) is a real misconfiguration, so
|
|
// wrap it with ErrInvalidMatchingConfig, which main.go treats as fatal.
|
|
// In fuzzy mode (default), behaviour is unchanged: the error is returned
|
|
// as-is and the caller logs it and continues with defaults.
|
|
wrapIfRegex := func(err error) error {
|
|
if err == nil || config.Matching != MatchingRegex {
|
|
return err
|
|
}
|
|
return fmt.Errorf("%w: %v", ErrInvalidMatchingConfig, err)
|
|
}
|
|
|
|
if err := viper.UnmarshalKey("wording", &config.Wording); err != nil {
|
|
return config, wrapIfRegex(fmt.Errorf("error parsing wording config: %w", err))
|
|
}
|
|
if err := viper.UnmarshalKey("force", &config.Force); err != nil {
|
|
return config, wrapIfRegex(fmt.Errorf("error parsing force config: %w", err))
|
|
}
|
|
if err := viper.UnmarshalKey("blacklist", &config.Blacklist); err != nil {
|
|
return config, wrapIfRegex(fmt.Errorf("error parsing blacklist config: %w", err))
|
|
}
|
|
if err := viper.UnmarshalKey("tag_prefixes", &config.TagPrefixes); err != nil {
|
|
return config, wrapIfRegex(fmt.Errorf("error parsing tag_prefixes config: %w", err))
|
|
}
|
|
|
|
matcher, err := NewKeywordMatcher(config.Matching, config.Wording)
|
|
if err != nil {
|
|
return config, err
|
|
}
|
|
config.Matcher = matcher
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// ApplyForcedVersioning applies forced versioning settings to a semantic version
|
|
func ApplyForcedVersioning(force Force, semver *SemVer) {
|
|
if force.Major > 0 {
|
|
Debug("Forced versioning (MAJOR)", map[string]interface{}{"major": force.Major})
|
|
semver.Major = force.Major
|
|
}
|
|
|
|
if force.Minor > 0 {
|
|
Debug("Forced versioning (MINOR)", map[string]interface{}{"minor": force.Minor})
|
|
semver.Minor = force.Minor
|
|
}
|
|
|
|
if force.Patch > 0 {
|
|
Debug("Forced versioning (PATCH)", map[string]interface{}{"patch": force.Patch})
|
|
semver.Patch = force.Patch
|
|
}
|
|
}
|