0
0
mirror of https://github.com/thegeeklab/git-sv.git synced 2024-11-22 08:20:39 +00:00
git-sv/app/config.go

230 lines
5.8 KiB
Go

package app
import (
"fmt"
"os"
"path/filepath"
"reflect"
"dario.cat/mergo"
"github.com/kelseyhightower/envconfig"
"github.com/rs/zerolog/log"
"github.com/thegeeklab/git-sv/v2/sv"
"gopkg.in/yaml.v3"
)
// EnvConfig env vars for cli configuration.
type EnvConfig struct {
Home string `envconfig:"GITSV_HOME" default:""`
}
// Config cli yaml config.
type Config struct {
Version string `yaml:"version"`
LogLevel string `yaml:"log-level"`
Versioning sv.VersioningConfig `yaml:"versioning"`
Tag TagConfig `yaml:"tag"`
ReleaseNotes sv.ReleaseNotesConfig `yaml:"release-notes"`
Branches sv.BranchesConfig `yaml:"branches"`
CommitMessage sv.CommitMessageConfig `yaml:"commit-message"`
}
func NewConfig(configDir, configFilename string) *Config {
workDir, _ := os.Getwd()
cfg := GetDefault()
envCfg := loadEnv()
if envCfg.Home != "" {
homeCfgFilepath := filepath.Join(envCfg.Home, configFilename)
if homeCfg, err := readFile(homeCfgFilepath); err == nil {
if merr := merge(cfg, migrate(homeCfg, homeCfgFilepath)); merr != nil {
log.Fatal().Err(merr).Msg("failed to merge user config")
}
}
}
repoCfgFilepath := filepath.Join(workDir, configDir, configFilename)
if repoCfg, err := readFile(repoCfgFilepath); err == nil {
if merr := merge(cfg, migrate(repoCfg, repoCfgFilepath)); merr != nil {
log.Fatal().Err(merr).Msg("failed to merge repo config")
}
if len(repoCfg.ReleaseNotes.Headers) > 0 { // mergo is merging maps, headers will be overwritten
cfg.ReleaseNotes.Headers = repoCfg.ReleaseNotes.Headers
}
}
return cfg
}
func loadEnv() EnvConfig {
var c EnvConfig
err := envconfig.Process("", &c)
if err != nil {
log.Fatal().Err(err).Msg("failed to load env config")
}
return c
}
func readFile(filepath string) (Config, error) {
content, rerr := os.ReadFile(filepath)
if rerr != nil {
return Config{}, rerr
}
var cfg Config
cerr := yaml.Unmarshal(content, &cfg)
if cerr != nil {
return Config{}, fmt.Errorf("could not parse config from path: %s, error: %w", filepath, cerr)
}
return cfg, nil
}
func GetDefault() *Config {
skipDetached := false
pattern := "%d.%d.%d"
filter := ""
return &Config{
Version: "1.1",
Versioning: sv.VersioningConfig{
UpdateMajor: []string{},
UpdateMinor: []string{"feat"},
UpdatePatch: []string{"build", "ci", "chore", "docs", "fix", "perf", "refactor", "style", "test"},
IgnoreUnknown: false,
},
Tag: TagConfig{
Pattern: &pattern,
Filter: &filter,
},
ReleaseNotes: sv.ReleaseNotesConfig{
Sections: []sv.ReleaseNotesSectionConfig{
{Name: "Features", SectionType: sv.ReleaseNotesSectionTypeCommits, CommitTypes: []string{"feat"}},
{Name: "Bug Fixes", SectionType: sv.ReleaseNotesSectionTypeCommits, CommitTypes: []string{"fix"}},
{Name: "Breaking Changes", SectionType: sv.ReleaseNotesSectionTypeBreakingChanges},
},
},
Branches: sv.BranchesConfig{
Prefix: "([a-z]+\\/)?",
Suffix: "(-.*)?",
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"},
Scope: sv.CommitMessageScopeConfig{},
Footer: map[string]sv.CommitMessageFooterConfig{
"issue": {Key: "jira", KeySynonyms: []string{"Jira", "JIRA"}},
},
Issue: sv.CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"},
HeaderSelector: "",
},
}
}
func merge(dst *Config, src Config) error {
err := mergo.Merge(dst, src, mergo.WithOverride, mergo.WithTransformers(&mergeTransformer{}))
if err == nil {
if len(src.ReleaseNotes.Headers) > 0 { // mergo is merging maps, ReleaseNotes.Headers should be overwritten
dst.ReleaseNotes.Headers = src.ReleaseNotes.Headers
}
}
return err
}
type mergeTransformer struct{}
func (t *mergeTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error {
if typ.Kind() == reflect.Slice {
return func(dst, src reflect.Value) error {
if dst.CanSet() && !src.IsNil() {
dst.Set(src)
}
return nil
}
}
if typ.Kind() == reflect.Ptr {
return func(dst, src reflect.Value) error {
if dst.CanSet() && !src.IsNil() {
dst.Set(src)
}
return nil
}
}
return nil
}
func migrate(cfg Config, filename string) Config {
if cfg.ReleaseNotes.Headers == nil {
return cfg
}
log.Warn().Msgf("config 'release-notes.headers' on %s is deprecated, please use 'sections' instead!", filename)
return Config{
Version: cfg.Version,
Versioning: cfg.Versioning,
Tag: cfg.Tag,
ReleaseNotes: sv.ReleaseNotesConfig{
Sections: migrateReleaseNotes(cfg.ReleaseNotes.Headers),
},
Branches: cfg.Branches,
CommitMessage: cfg.CommitMessage,
}
}
func migrateReleaseNotes(headers map[string]string) []sv.ReleaseNotesSectionConfig {
order := []string{"feat", "fix", "refactor", "perf", "test", "build", "ci", "chore", "docs", "style"}
var sections []sv.ReleaseNotesSectionConfig
for _, key := range order {
if name, exists := headers[key]; exists {
sections = append(
sections,
sv.ReleaseNotesSectionConfig{
Name: name,
SectionType: sv.ReleaseNotesSectionTypeCommits,
CommitTypes: []string{key},
})
}
}
if name, exists := headers["breaking-change"]; exists {
sections = append(
sections,
sv.ReleaseNotesSectionConfig{
Name: name,
SectionType: sv.ReleaseNotesSectionTypeBreakingChanges,
})
}
return sections
}
// ==== Message ====
// CommitMessageConfig config a commit message.
// ==== Branches ====
// ==== Versioning ====
// ==== Tag ====
// TagConfig tag preferences.
type TagConfig struct {
Pattern *string `yaml:"pattern"`
Filter *string `yaml:"filter"`
}