From db1691ada40c083a33c0760eacecf1a98b4b8033 Mon Sep 17 00:00:00 2001 From: Robert Kaussow Date: Sun, 15 Oct 2023 21:29:29 +0200 Subject: [PATCH] refactor: rework packages and interfaces (#3) --- .dictionary | 1 - README.md | 16 - sv/git.go => app/app.go | 100 ++- sv/git_test.go => app/app_test.go | 8 +- app/commands/changelog.go | 98 +++ app/commands/commit.go | 108 +++ app/commands/commitlog.go | 86 ++ app/commands/commitnotes.go | 60 ++ app/commands/config.go | 37 + app/commands/currentversion.go | 24 + app/commands/nextversion.go | 31 + {cmd/git-sv => app/commands}/prompt.go | 2 +- app/commands/releasenotes.go | 55 ++ app/commands/tag.go | 36 + app/commands/utils.go | 220 +++++ app/commands/validatecommitmessage.go | 85 ++ {cmd/git-sv => app}/config.go | 90 ++- {cmd/git-sv => app}/config_test.go | 8 +- cmd/git-sv/handlers.go | 754 ------------------ cmd/git-sv/log.go | 10 - cmd/git-sv/main.go | 123 +-- cmd/git-sv/resources_test.go | 26 - go.mod | 14 + go.sum | 74 ++ sv/{semver.go => commit.go} | 39 +- sv/{semver_test.go => commit_test.go} | 78 +- sv/config.go | 100 --- sv/errors.go | 11 - sv/{ => formatter}/formatter.go | 32 +- sv/{ => formatter}/formatter_test.go | 59 +- sv/formatter_functions.go | 21 - sv/formatter_functions_test.go | 57 -- sv/message.go | 114 ++- sv/message_test.go | 56 +- sv/releasenotes.go | 48 +- sv/releasenotes_test.go | 54 +- sv/{helpers_test.go => testutils.go} | 12 +- .../assets}/changelog-md.tpl | 0 .../assets}/releasenotes-md.tpl | 2 +- .../rn-md-section-breaking-changes.tpl | 0 .../assets}/rn-md-section-commits.tpl | 0 templates/templates.go | 78 ++ templates/templates_test.go | 80 ++ 43 files changed, 1549 insertions(+), 1358 deletions(-) rename sv/git.go => app/app.go (71%) rename sv/git_test.go => app/app_test.go (83%) create mode 100644 app/commands/changelog.go create mode 100644 app/commands/commit.go create mode 100644 app/commands/commitlog.go create mode 100644 app/commands/commitnotes.go create mode 100644 app/commands/config.go create mode 100644 app/commands/currentversion.go create mode 100644 app/commands/nextversion.go rename {cmd/git-sv => app/commands}/prompt.go (99%) create mode 100644 app/commands/releasenotes.go create mode 100644 app/commands/tag.go create mode 100644 app/commands/utils.go create mode 100644 app/commands/validatecommitmessage.go rename {cmd/git-sv => app}/config.go (69%) rename {cmd/git-sv => app}/config_test.go (98%) delete mode 100644 cmd/git-sv/handlers.go delete mode 100644 cmd/git-sv/log.go delete mode 100644 cmd/git-sv/resources_test.go rename sv/{semver.go => commit.go} (64%) rename sv/{semver_test.go => commit_test.go} (63%) delete mode 100644 sv/config.go delete mode 100644 sv/errors.go rename sv/{ => formatter}/formatter.go (64%) rename sv/{ => formatter}/formatter_test.go (60%) delete mode 100644 sv/formatter_functions.go delete mode 100644 sv/formatter_functions_test.go rename sv/{helpers_test.go => testutils.go} (67%) rename {cmd/git-sv/resources/templates => templates/assets}/changelog-md.tpl (100%) rename {cmd/git-sv/resources/templates => templates/assets}/releasenotes-md.tpl (75%) rename {cmd/git-sv/resources/templates => templates/assets}/rn-md-section-breaking-changes.tpl (100%) rename {cmd/git-sv/resources/templates => templates/assets}/rn-md-section-commits.tpl (100%) create mode 100644 templates/templates.go create mode 100644 templates/templates_test.go diff --git a/.dictionary b/.dictionary index 48a8d97..cad2110 100644 --- a/.dictionary +++ b/.dictionary @@ -3,7 +3,6 @@ YAML .gitsv cli getsection -timefmt cfg json changelog diff --git a/README.md b/README.md index 4981101..064c71f 100644 --- a/README.md +++ b/README.md @@ -168,22 +168,6 @@ Each `ReleaseNoteSection` will be configured according with `release-notes.secti > :warning: currently only `commits` and `breaking-changes` are supported as `section-types`, using a different value for this field will make the section to be removed from the template variables. -##### Functions - -Beside the [go template functions](https://pkg.go.dev/text/template#hdr-Functions), the following functions are available to use in the templates. Check [formatter_functions.go](sv/formatter_functions.go) to see the functions implementation. - -###### timefmt - -**Usage:** timefmt time "2006-01-02" - -Receive a time.Time and a layout string and returns a textual representation of the time according with the layout provided. Check for more information. - -###### getsection - -**Usage:** getsection sections "Features" - -Receive a list of ReleaseNoteSection and a Section name and returns a section with the provided name. If no section is found, it will return `nil`. - ### Running Run `git-sv` to get the list of available parameters: diff --git a/sv/git.go b/app/app.go similarity index 71% rename from sv/git.go rename to app/app.go index 14b1d5b..3b88fd2 100644 --- a/sv/git.go +++ b/app/app.go @@ -1,8 +1,9 @@ -package sv +package app import ( "bufio" "bytes" + "errors" "fmt" "os" "os/exec" @@ -11,35 +12,22 @@ import ( "time" "github.com/Masterminds/semver/v3" + "github.com/thegeeklab/git-sv/v2/sv" + "github.com/thegeeklab/git-sv/v2/sv/formatter" ) const ( logSeparator = "###" endLine = "~~~" -) -// Git commands. -type Git interface { - LastTag() string - Log(lr LogRange) ([]GitCommitLog, error) - Commit(header, body, footer string) error - Tag(version semver.Version) (string, error) - Tags() ([]GitTag, error) - Branch() string - IsDetached() (bool, error) -} + configFilename = "config.yml" + configDir = ".gitsv" +) -// GitCommitLog description of a single commit log. -type GitCommitLog struct { - Date string `json:"date,omitempty"` - Timestamp int `json:"timestamp,omitempty"` - AuthorName string `json:"authorName,omitempty"` - Hash string `json:"hash,omitempty"` - Message CommitMessage `json:"message,omitempty"` -} +var errUnknownGitError = errors.New("git command failed") -// GitTag git tag info. -type GitTag struct { +// Tag git tag info. +type Tag struct { Name string Date time.Time } @@ -66,27 +54,35 @@ func NewLogRange(t LogRangeType, start, end string) LogRange { return LogRange{rangeType: t, start: start, end: end} } -// GitImpl git command implementation. -type GitImpl struct { - messageProcessor MessageProcessor - tagCfg TagConfig +// Impl git command implementation. +type GitSV struct { + Config *Config + + MessageProcessor sv.MessageProcessor + CommitProcessor sv.CommitProcessor + ReleasenotesProcessor sv.ReleaseNoteProcessor + + OutputFormatter formatter.OutputFormatter } -// NewGit constructor. -func NewGit(messageProcessor MessageProcessor, cfg TagConfig) *GitImpl { - return &GitImpl{ - messageProcessor: messageProcessor, - tagCfg: cfg, +// New constructor. +func New() GitSV { + g := GitSV{ + Config: NewConfig(configDir, configFilename), } + + g.MessageProcessor = sv.NewMessageProcessor(g.Config.CommitMessage, g.Config.Branches) + + return g } // LastTag get last tag, if no tag found, return empty. -func (g GitImpl) LastTag() string { +func (g GitSV) LastTag() string { //nolint:gosec cmd := exec.Command( "git", "for-each-ref", - fmt.Sprintf("refs/tags/%s", *g.tagCfg.Filter), + fmt.Sprintf("refs/tags/%s", *g.Config.Tag.Filter), "--sort", "-creatordate", "--format", @@ -104,7 +100,7 @@ func (g GitImpl) LastTag() string { } // Log return git log. -func (g GitImpl) Log(lr LogRange) ([]GitCommitLog, error) { +func (g GitSV) Log(lr LogRange) ([]sv.CommitLog, error) { format := "--pretty=format:\"%ad" + logSeparator + "%at" + logSeparator + "%cN" + logSeparator + @@ -133,7 +129,7 @@ func (g GitImpl) Log(lr LogRange) ([]GitCommitLog, error) { return nil, combinedOutputErr(err, out) } - logs, parseErr := parseLogOutput(g.messageProcessor, string(out)) + logs, parseErr := parseLogOutput(g.MessageProcessor, string(out)) if parseErr != nil { return nil, parseErr } @@ -141,8 +137,8 @@ func (g GitImpl) Log(lr LogRange) ([]GitCommitLog, error) { return logs, nil } -// Commit runs git commit. -func (g GitImpl) Commit(header, body, footer string) error { +// Commit runs git sv. +func (g GitSV) Commit(header, body, footer string) error { cmd := exec.Command("git", "commit", "-m", header, "-m", "", "-m", body, "-m", "", "-m", footer) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -151,8 +147,8 @@ func (g GitImpl) Commit(header, body, footer string) error { } // Tag create a git tag. -func (g GitImpl) Tag(version semver.Version) (string, error) { - tag := fmt.Sprintf(*g.tagCfg.Pattern, version.Major(), version.Minor(), version.Patch()) +func (g GitSV) Tag(version semver.Version) (string, error) { + tag := fmt.Sprintf(*g.Config.Tag.Pattern, version.Major(), version.Minor(), version.Patch()) tagMsg := fmt.Sprintf("Version %d.%d.%d", version.Major(), version.Minor(), version.Patch()) tagCommand := exec.Command("git", "tag", "-a", tag, "-m", tagMsg) @@ -169,7 +165,7 @@ func (g GitImpl) Tag(version semver.Version) (string, error) { } // Tags list repository tags. -func (g GitImpl) Tags() ([]GitTag, error) { +func (g GitSV) Tags() ([]Tag, error) { //nolint:gosec cmd := exec.Command( "git", @@ -178,7 +174,7 @@ func (g GitImpl) Tags() ([]GitTag, error) { "creatordate", "--format", "%(creatordate:iso8601)#%(refname:short)", - fmt.Sprintf("refs/tags/%s", *g.tagCfg.Filter), + fmt.Sprintf("refs/tags/%s", *g.Config.Tag.Filter), ) out, err := cmd.CombinedOutput() @@ -190,7 +186,7 @@ func (g GitImpl) Tags() ([]GitTag, error) { } // Branch get git branch. -func (GitImpl) Branch() string { +func (g GitSV) Branch() string { cmd := exec.Command("git", "symbolic-ref", "--short", "HEAD") out, err := cmd.CombinedOutput() @@ -202,7 +198,7 @@ func (GitImpl) Branch() string { } // IsDetached check if is detached. -func (GitImpl) IsDetached() (bool, error) { +func (g GitSV) IsDetached() (bool, error) { cmd := exec.Command("git", "symbolic-ref", "-q", "HEAD") out, err := cmd.CombinedOutput() @@ -219,27 +215,27 @@ func (GitImpl) IsDetached() (bool, error) { return false, nil } -func parseTagsOutput(input string) ([]GitTag, error) { +func parseTagsOutput(input string) ([]Tag, error) { scanner := bufio.NewScanner(strings.NewReader(input)) - var result []GitTag + var result []Tag for scanner.Scan() { if line := strings.TrimSpace(scanner.Text()); line != "" { values := strings.Split(line, "#") date, _ := time.Parse("2006-01-02 15:04:05 -0700", values[0]) // ignore invalid dates - result = append(result, GitTag{Name: values[1], Date: date}) + result = append(result, Tag{Name: values[1], Date: date}) } } return result, nil } -func parseLogOutput(messageProcessor MessageProcessor, log string) ([]GitCommitLog, error) { +func parseLogOutput(messageProcessor sv.MessageProcessor, log string) ([]sv.CommitLog, error) { scanner := bufio.NewScanner(strings.NewReader(log)) scanner.Split(splitAt([]byte(endLine))) - var logs []GitCommitLog + var logs []sv.CommitLog for scanner.Scan() { if text := strings.TrimSpace(strings.Trim(scanner.Text(), "\"")); text != "" { @@ -255,16 +251,16 @@ func parseLogOutput(messageProcessor MessageProcessor, log string) ([]GitCommitL return logs, nil } -func parseCommitLog(messageProcessor MessageProcessor, commit string) (GitCommitLog, error) { - content := strings.Split(strings.Trim(commit, "\""), logSeparator) +func parseCommitLog(messageProcessor sv.MessageProcessor, c string) (sv.CommitLog, error) { + content := strings.Split(strings.Trim(c, "\""), logSeparator) timestamp, _ := strconv.Atoi(content[1]) message, err := messageProcessor.Parse(content[4], content[5]) if err != nil { - return GitCommitLog{}, err + return sv.CommitLog{}, err } - return GitCommitLog{ + return sv.CommitLog{ Date: content[0], Timestamp: timestamp, AuthorName: content[2], diff --git a/sv/git_test.go b/app/app_test.go similarity index 83% rename from sv/git_test.go rename to app/app_test.go index 94680f9..818b3d5 100644 --- a/sv/git_test.go +++ b/app/app_test.go @@ -1,4 +1,4 @@ -package sv +package app import ( "reflect" @@ -10,19 +10,19 @@ func Test_parseTagsOutput(t *testing.T) { tests := []struct { name string input string - want []GitTag + want []Tag wantErr bool }{ { "with date", "2020-05-01 18:00:00 -0300#1.0.0", - []GitTag{{Name: "1.0.0", Date: date("2020-05-01 18:00:00 -0300")}}, + []Tag{{Name: "1.0.0", Date: date("2020-05-01 18:00:00 -0300")}}, false, }, { "without date", "#1.0.0", - []GitTag{{Name: "1.0.0", Date: time.Time{}}}, + []Tag{{Name: "1.0.0", Date: time.Time{}}}, false, }, } diff --git a/app/commands/changelog.go b/app/commands/changelog.go new file mode 100644 index 0000000..a0ac015 --- /dev/null +++ b/app/commands/changelog.go @@ -0,0 +1,98 @@ +package commands + +import ( + "fmt" + "sort" + + "github.com/thegeeklab/git-sv/v2/app" + "github.com/thegeeklab/git-sv/v2/sv" + "github.com/urfave/cli/v2" +) + +func ChangelogFlags() []cli.Flag { + return []cli.Flag{ + &cli.IntFlag{ + Name: "size", + Value: 10, //nolint:gomnd + Aliases: []string{"n"}, + Usage: "get changelog from last 'n' tags", + }, + &cli.BoolFlag{ + Name: "all", + Usage: "ignore size parameter, get changelog for every tag", + }, + &cli.BoolFlag{ + Name: "add-next-version", + Usage: "add next version on change log (commits since last tag, but only if there is a new version to release)", + }, + &cli.BoolFlag{ + Name: "semantic-version-only", + Usage: "only show tags 'SemVer-ish'", + }, + } +} + +func ChangelogHandler( + g app.GitSV, +) cli.ActionFunc { + return func(c *cli.Context) error { + tags, err := g.Tags() + if err != nil { + return err + } + + sort.Slice(tags, func(i, j int) bool { + return tags[i].Date.After(tags[j].Date) + }) + + var releaseNotes []sv.ReleaseNote + + size := c.Int("size") + all := c.Bool("all") + addNextVersion := c.Bool("add-next-version") + semanticVersionOnly := c.Bool("semantic-version-only") + + if addNextVersion { + rnVersion, updated, date, commits, uerr := getNextVersionInfo(g, g.CommitProcessor) + if uerr != nil { + return uerr + } + + if updated { + releaseNotes = append(releaseNotes, g.ReleasenotesProcessor.Create(rnVersion, "", date, commits)) + } + } + + for i, tag := range tags { + if !all && i >= size { + break + } + + previousTag := "" + if i+1 < len(tags) { + previousTag = tags[i+1].Name + } + + if semanticVersionOnly && !sv.IsValidVersion(tag.Name) { + continue + } + + commits, err := g.Log(app.NewLogRange(app.TagRange, previousTag, tag.Name)) + if err != nil { + return fmt.Errorf("error getting git log from tag: %s, message: %w", tag.Name, err) + } + + currentVer, _ := sv.ToVersion(tag.Name) + releaseNotes = append(releaseNotes, g.ReleasenotesProcessor.Create(currentVer, tag.Name, tag.Date, commits)) + } + + output, err := g.OutputFormatter.FormatChangelog(releaseNotes) + if err != nil { + return fmt.Errorf("could not format changelog, message: %w", err) + } + + fmt.Println(output) + + return nil + } +} diff --git a/app/commands/commit.go b/app/commands/commit.go new file mode 100644 index 0000000..03d43ed --- /dev/null +++ b/app/commands/commit.go @@ -0,0 +1,108 @@ +package commands + +import ( + "fmt" + + "github.com/thegeeklab/git-sv/v2/app" + "github.com/thegeeklab/git-sv/v2/sv" + "github.com/urfave/cli/v2" +) + +func CommitFlags() []cli.Flag { + return []cli.Flag{ + &cli.BoolFlag{ + Name: "no-scope", + Aliases: []string{"nsc"}, + Usage: "do not prompt for commit scope", + }, + &cli.BoolFlag{ + Name: "no-body", + Aliases: []string{"nbd"}, + Usage: "do not prompt for commit body", + }, + &cli.BoolFlag{ + Name: "no-issue", + Aliases: []string{"nis"}, + Usage: "do not prompt for commit issue, will try to recover from branch if enabled", + }, + &cli.BoolFlag{ + Name: "no-breaking", + Aliases: []string{"nbc"}, + Usage: "do not prompt for breaking changes", + }, + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Usage: "define commit type", + }, + &cli.StringFlag{ + Name: "scope", + Aliases: []string{"s"}, + Usage: "define commit scope", + }, + &cli.StringFlag{ + Name: "description", + Aliases: []string{"d"}, + Usage: "define commit description", + }, + &cli.StringFlag{ + Name: "breaking-change", + Aliases: []string{"b"}, + Usage: "define commit breaking change message", + }, + } +} + +func CommitHandler(g app.GitSV) cli.ActionFunc { + return func(c *cli.Context) error { + noBreaking := c.Bool("no-breaking") + noBody := c.Bool("no-body") + noIssue := c.Bool("no-issue") + noScope := c.Bool("no-scope") + inputType := c.String("type") + inputScope := c.String("scope") + inputDescription := c.String("description") + inputBreakingChange := c.String("breaking-change") + + ctype, err := getCommitType(g.Config, g.MessageProcessor, inputType) + if err != nil { + return err + } + + scope, err := getCommitScope(g.Config, g.MessageProcessor, inputScope, noScope) + if err != nil { + return err + } + + subject, err := getCommitDescription(g.MessageProcessor, inputDescription) + if err != nil { + return err + } + + fullBody, err := getCommitBody(noBody) + if err != nil { + return err + } + + issue, err := getCommitIssue(g.Config, g.MessageProcessor, g.Branch(), noIssue) + if err != nil { + return err + } + + breakingChange, err := getCommitBreakingChange(noBreaking, inputBreakingChange) + if err != nil { + return err + } + + header, body, footer := g.MessageProcessor.Format( + sv.NewCommitMessage(ctype, scope, subject, fullBody, issue, breakingChange), + ) + + err = g.Commit(header, body, footer) + if err != nil { + return fmt.Errorf("error executing git commit, message: %w", err) + } + + return nil + } +} diff --git a/app/commands/commitlog.go b/app/commands/commitlog.go new file mode 100644 index 0000000..f9aa871 --- /dev/null +++ b/app/commands/commitlog.go @@ -0,0 +1,86 @@ +package commands + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/thegeeklab/git-sv/v2/app" + "github.com/thegeeklab/git-sv/v2/sv" + "github.com/urfave/cli/v2" +) + +var ( + errCanNotCreateTagFlag = errors.New("cannot define tag flag with range, start or end flags") + errInvalidRange = errors.New("invalid log range") + errUnknownTag = errors.New("unknown tag") +) + +func CommitLogFlags() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: "t", + Aliases: []string{"tag"}, + Usage: "get commit log from a specific tag", + }, + &cli.StringFlag{ + Name: "r", + Aliases: []string{"range"}, + Usage: "type of range of commits, use: tag, date or hash", + Value: string(app.TagRange), + }, + &cli.StringFlag{ + Name: "s", + Aliases: []string{"start"}, + Usage: "start range of git log revision range, if date, the value is used on since flag instead", + }, + &cli.StringFlag{ + Name: "e", + Aliases: []string{"end"}, + Usage: "end range of git log revision range, if date, the value is used on until flag instead", + }, + } +} + +func CommitLogHandler(g app.GitSV) cli.ActionFunc { + return func(c *cli.Context) error { + var ( + commits []sv.CommitLog + err error + ) + + tagFlag := c.String("t") + rangeFlag := c.String("r") + startFlag := c.String("s") + endFlag := c.String("e") + + if tagFlag != "" && (rangeFlag != string(app.TagRange) || startFlag != "" || endFlag != "") { + return errCanNotCreateTagFlag + } + + if tagFlag != "" { + commits, err = getTagCommits(g, tagFlag) + } else { + r, rerr := logRange(g, rangeFlag, startFlag, endFlag) + if rerr != nil { + return rerr + } + commits, err = g.Log(r) + } + + if err != nil { + return fmt.Errorf("error getting git log, message: %w", err) + } + + for _, commit := range commits { + content, err := json.Marshal(commit) + if err != nil { + return err + } + + fmt.Println(string(content)) + } + + return nil + } +} diff --git a/app/commands/commitnotes.go b/app/commands/commitnotes.go new file mode 100644 index 0000000..0f43b77 --- /dev/null +++ b/app/commands/commitnotes.go @@ -0,0 +1,60 @@ +package commands + +import ( + "fmt" + "time" + + "github.com/thegeeklab/git-sv/v2/app" + "github.com/urfave/cli/v2" +) + +func CommitNotesFlags() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: "r", Aliases: []string{"range"}, + Usage: "type of range of commits, use: tag, date or hash", + Required: true, + }, + &cli.StringFlag{ + Name: "s", + Aliases: []string{"start"}, + Usage: "start range of git log revision range, if date, the value is used on since flag instead", + }, + &cli.StringFlag{ + Name: "e", + Aliases: []string{"end"}, + Usage: "end range of git log revision range, if date, the value is used on until flag instead", + }, + } +} + +func CommitNotesHandler(g app.GitSV) cli.ActionFunc { + return func(c *cli.Context) error { + var date time.Time + + rangeFlag := c.String("r") + + lr, err := logRange(g, rangeFlag, c.String("s"), c.String("e")) + if err != nil { + return err + } + + commits, err := g.Log(lr) + if err != nil { + return fmt.Errorf("error getting git log from range: %s, message: %w", rangeFlag, err) + } + + if len(commits) > 0 { + date, _ = time.Parse("2006-01-02", commits[0].Date) + } + + output, err := g.OutputFormatter.FormatReleaseNote(g.ReleasenotesProcessor.Create(nil, "", date, commits)) + if err != nil { + return fmt.Errorf("could not format release notes, message: %w", err) + } + + fmt.Println(output) + + return nil + } +} diff --git a/app/commands/config.go b/app/commands/config.go new file mode 100644 index 0000000..84d68a3 --- /dev/null +++ b/app/commands/config.go @@ -0,0 +1,37 @@ +package commands + +import ( + "fmt" + + "github.com/thegeeklab/git-sv/v2/app" + "github.com/urfave/cli/v2" + "gopkg.in/yaml.v2" +) + +func ConfigDefaultHandler() cli.ActionFunc { + return func(c *cli.Context) error { + cfg := app.GetDefault() + + content, err := yaml.Marshal(&cfg) + if err != nil { + return err + } + + fmt.Println(string(content)) + + return nil + } +} + +func ConfigShowHandler(cfg *app.Config) cli.ActionFunc { + return func(c *cli.Context) error { + content, err := yaml.Marshal(cfg) + if err != nil { + return err + } + + fmt.Println(string(content)) + + return nil + } +} diff --git a/app/commands/currentversion.go b/app/commands/currentversion.go new file mode 100644 index 0000000..689141a --- /dev/null +++ b/app/commands/currentversion.go @@ -0,0 +1,24 @@ +package commands + +import ( + "fmt" + + "github.com/thegeeklab/git-sv/v2/app" + "github.com/thegeeklab/git-sv/v2/sv" + "github.com/urfave/cli/v2" +) + +func CurrentVersionHandler(gsv app.GitSV) cli.ActionFunc { + return func(c *cli.Context) error { + lastTag := gsv.LastTag() + + currentVer, err := sv.ToVersion(lastTag) + if err != nil { + return fmt.Errorf("error parsing version: %s from git tag, message: %w", lastTag, err) + } + + fmt.Printf("%d.%d.%d\n", currentVer.Major(), currentVer.Minor(), currentVer.Patch()) + + return nil + } +} diff --git a/app/commands/nextversion.go b/app/commands/nextversion.go new file mode 100644 index 0000000..eb562f5 --- /dev/null +++ b/app/commands/nextversion.go @@ -0,0 +1,31 @@ +package commands + +import ( + "fmt" + + "github.com/thegeeklab/git-sv/v2/app" + "github.com/thegeeklab/git-sv/v2/sv" + "github.com/urfave/cli/v2" +) + +func NextVersionHandler(g app.GitSV) cli.ActionFunc { + return func(c *cli.Context) error { + lastTag := g.LastTag() + + currentVer, err := sv.ToVersion(lastTag) + if err != nil { + return fmt.Errorf("error parsing version: %s from git tag, message: %w", lastTag, err) + } + + commits, err := g.Log(app.NewLogRange(app.TagRange, lastTag, "")) + if err != nil { + return fmt.Errorf("error getting git log, message: %w", err) + } + + nextVer, _ := g.CommitProcessor.NextVersion(currentVer, commits) + + fmt.Printf("%d.%d.%d\n", nextVer.Major(), nextVer.Minor(), nextVer.Patch()) + + return nil + } +} diff --git a/cmd/git-sv/prompt.go b/app/commands/prompt.go similarity index 99% rename from cmd/git-sv/prompt.go rename to app/commands/prompt.go index c8744c3..7e8dc21 100644 --- a/cmd/git-sv/prompt.go +++ b/app/commands/prompt.go @@ -1,4 +1,4 @@ -package main +package commands import ( "errors" diff --git a/app/commands/releasenotes.go b/app/commands/releasenotes.go new file mode 100644 index 0000000..e920cf4 --- /dev/null +++ b/app/commands/releasenotes.go @@ -0,0 +1,55 @@ +package commands + +import ( + "fmt" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/thegeeklab/git-sv/v2/app" + "github.com/thegeeklab/git-sv/v2/sv" + "github.com/urfave/cli/v2" +) + +func ReleaseNotesFlags() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: "t", + Aliases: []string{"tag"}, + Usage: "get release note from tag", + }, + } +} + +func ReleaseNotesHandler(g app.GitSV) cli.ActionFunc { + return func(c *cli.Context) error { + var ( + commits []sv.CommitLog + rnVersion *semver.Version + tag string + date time.Time + err error + ) + + if tag = c.String("t"); tag != "" { + rnVersion, date, commits, err = getTagVersionInfo(g, tag) + } else { + // TODO: should generate release notes if version was not updated? + rnVersion, _, date, commits, err = getNextVersionInfo(g, g.CommitProcessor) + } + + if err != nil { + return err + } + + releasenote := g.ReleasenotesProcessor.Create(rnVersion, tag, date, commits) + + output, err := g.OutputFormatter.FormatReleaseNote(releasenote) + if err != nil { + return fmt.Errorf("could not format release notes, message: %w", err) + } + + fmt.Println(output) + + return nil + } +} diff --git a/app/commands/tag.go b/app/commands/tag.go new file mode 100644 index 0000000..a902a70 --- /dev/null +++ b/app/commands/tag.go @@ -0,0 +1,36 @@ +package commands + +import ( + "fmt" + + "github.com/thegeeklab/git-sv/v2/app" + "github.com/thegeeklab/git-sv/v2/sv" + "github.com/urfave/cli/v2" +) + +func TagHandler(g app.GitSV) cli.ActionFunc { + return func(c *cli.Context) error { + lastTag := g.LastTag() + + currentVer, err := sv.ToVersion(lastTag) + if err != nil { + return fmt.Errorf("error parsing version: %s from git tag, message: %w", lastTag, err) + } + + commits, err := g.Log(app.NewLogRange(app.TagRange, lastTag, "")) + if err != nil { + return fmt.Errorf("error getting git log, message: %w", err) + } + + nextVer, _ := g.CommitProcessor.NextVersion(currentVer, commits) + tagname, err := g.Tag(*nextVer) + + fmt.Println(tagname) + + if err != nil { + return fmt.Errorf("error generating tag version: %s, message: %w", nextVer.String(), err) + } + + return nil + } +} diff --git a/app/commands/utils.go b/app/commands/utils.go new file mode 100644 index 0000000..613aa2e --- /dev/null +++ b/app/commands/utils.go @@ -0,0 +1,220 @@ +package commands + +import ( + "fmt" + "io/fs" + "os" + "strings" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/thegeeklab/git-sv/v2/app" + "github.com/thegeeklab/git-sv/v2/sv" +) + +func getTagCommits(gsv app.GitSV, tag string) ([]sv.CommitLog, error) { + prev, _, err := getTags(gsv, tag) + if err != nil { + return nil, err + } + + return gsv.Log(app.NewLogRange(app.TagRange, prev, tag)) +} + +func getTags(gsv app.GitSV, tag string) (string, app.Tag, error) { + tags, err := gsv.Tags() + if err != nil { + return "", app.Tag{}, err + } + + index := find(tag, tags) + if index < 0 { + return "", app.Tag{}, fmt.Errorf("%w: %s not found, check tag filter", errUnknownTag, tag) + } + + previousTag := "" + if index > 0 { + previousTag = tags[index-1].Name + } + + return previousTag, tags[index], nil +} + +func find(tag string, tags []app.Tag) int { + for i := 0; i < len(tags); i++ { + if tag == tags[i].Name { + return i + } + } + + return -1 +} + +func logRange(gsv app.GitSV, rangeFlag, startFlag, endFlag string) (app.LogRange, error) { + switch rangeFlag { + case string(app.TagRange): + return app.NewLogRange(app.TagRange, str(startFlag, gsv.LastTag()), endFlag), nil + case string(app.DateRange): + return app.NewLogRange(app.DateRange, startFlag, endFlag), nil + case string(app.HashRange): + return app.NewLogRange(app.HashRange, startFlag, endFlag), nil + default: + return app.LogRange{}, fmt.Errorf( + "%w: %s, expected: %s, %s or %s", + errInvalidRange, + rangeFlag, + app.TagRange, + app.DateRange, + app.HashRange, + ) + } +} + +func str(value, defaultValue string) string { + if value != "" { + return value + } + + return defaultValue +} + +func getTagVersionInfo(gsv app.GitSV, tag string) (*semver.Version, time.Time, []sv.CommitLog, error) { + tagVersion, _ := sv.ToVersion(tag) + + previousTag, currentTag, err := getTags(gsv, tag) + if err != nil { + return nil, time.Time{}, nil, fmt.Errorf("error listing tags, message: %w", err) + } + + commits, err := gsv.Log(app.NewLogRange(app.TagRange, previousTag, tag)) + if err != nil { + return nil, time.Time{}, nil, fmt.Errorf("error getting git log from tag: %s, message: %w", tag, err) + } + + return tagVersion, currentTag.Date, commits, nil +} + +func getNextVersionInfo( + gsv app.GitSV, semverProcessor sv.CommitProcessor, +) (*semver.Version, bool, time.Time, []sv.CommitLog, error) { + lastTag := gsv.LastTag() + + commits, err := gsv.Log(app.NewLogRange(app.TagRange, lastTag, "")) + if err != nil { + return nil, false, time.Time{}, nil, fmt.Errorf("error getting git log, message: %w", err) + } + + currentVer, _ := sv.ToVersion(lastTag) + version, updated := semverProcessor.NextVersion(currentVer, commits) + + return version, updated, time.Now(), commits, nil +} + +func getCommitType(cfg *app.Config, p sv.MessageProcessor, input string) (string, error) { + if input == "" { + t, err := promptType(cfg.CommitMessage.Types) + + return t.Type, err + } + + return input, p.ValidateType(input) +} + +func getCommitScope(cfg *app.Config, p sv.MessageProcessor, input string, noScope bool) (string, error) { + if input == "" && !noScope { + return promptScope(cfg.CommitMessage.Scope.Values) + } + + return input, p.ValidateScope(input) +} + +func getCommitDescription(p sv.MessageProcessor, input string) (string, error) { + if input == "" { + return promptSubject() + } + + return input, p.ValidateDescription(input) +} + +func getCommitBody(noBody bool) (string, error) { + if noBody { + return "", nil + } + + var fullBody strings.Builder + + for body, err := promptBody(); body != "" || err != nil; body, err = promptBody() { + if err != nil { + return "", err + } + + if fullBody.Len() > 0 { + fullBody.WriteString("\n") + } + + if body != "" { + fullBody.WriteString(body) + } + } + + return fullBody.String(), nil +} + +func getCommitIssue(cfg *app.Config, p sv.MessageProcessor, branch string, noIssue bool) (string, error) { + branchIssue, err := p.IssueID(branch) + if err != nil { + return "", err + } + + if cfg.CommitMessage.IssueFooterConfig().Key == "" || cfg.CommitMessage.Issue.Regex == "" { + return "", nil + } + + if noIssue { + return branchIssue, nil + } + + return promptIssueID("issue id", cfg.CommitMessage.Issue.Regex, branchIssue) +} + +func getCommitBreakingChange(noBreaking bool, input string) (string, error) { + if noBreaking { + return "", nil + } + + if strings.TrimSpace(input) != "" { + return input, nil + } + + hasBreakingChanges, err := promptConfirm("has breaking change?") + if err != nil { + return "", err + } + + if !hasBreakingChanges { + return "", nil + } + + return promptBreakingChanges() +} + +func readFile(filepath string) (string, error) { + f, err := os.ReadFile(filepath) + if err != nil { + return "", err + } + + return string(f), nil +} + +func appendOnFile(message, filepath string, permissions fs.FileMode) error { + f, err := os.OpenFile(filepath, os.O_APPEND|os.O_WRONLY, permissions) + if err != nil { + return err + } + defer f.Close() + + _, err = f.WriteString(message) + + return err +} diff --git a/app/commands/validatecommitmessage.go b/app/commands/validatecommitmessage.go new file mode 100644 index 0000000..573d710 --- /dev/null +++ b/app/commands/validatecommitmessage.go @@ -0,0 +1,85 @@ +package commands + +import ( + "errors" + "fmt" + "path/filepath" + + "github.com/rs/zerolog/log" + "github.com/thegeeklab/git-sv/v2/app" + "github.com/urfave/cli/v2" +) + +const laxFilePerm = 0o644 + +var ( + errReadCommitMessage = errors.New("failed to read commit message") + errAppendFooter = errors.New("failed to append meta-informations on footer") +) + +func ValidateCommitMessageFlags() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: "path", + Required: true, + Usage: "git working directory", + }, + &cli.StringFlag{ + Name: "file", + Required: true, + Usage: "name of the file that contains the commit log message", + }, + &cli.StringFlag{ + Name: "source", + Required: true, + Usage: "source of the commit message", + }, + } +} + +func ValidateCommitMessageHandler(g app.GitSV) cli.ActionFunc { + return func(c *cli.Context) error { + branch := g.Branch() + detached, derr := g.IsDetached() + + if g.MessageProcessor.SkipBranch(branch, derr == nil && detached) { + log.Warn().Msg("commit message validation skipped, branch in ignore list or detached...") + + return nil + } + + if source := c.String("source"); source == "merge" { + log.Warn().Msgf("commit message validation skipped, ignoring source: %s...", source) + + return nil + } + + filepath := filepath.Join(c.String("path"), c.String("file")) + + commitMessage, err := readFile(filepath) + if err != nil { + return fmt.Errorf("%w: %s", errReadCommitMessage, err.Error()) + } + + if err := g.MessageProcessor.Validate(commitMessage); err != nil { + return fmt.Errorf("%w: %s", errReadCommitMessage, err.Error()) + } + + msg, err := g.MessageProcessor.Enhance(branch, commitMessage) + if err != nil { + log.Warn().Err(err).Msg("could not enhance commit message") + + return nil + } + + if msg == "" { + return nil + } + + if err := appendOnFile(msg, filepath, laxFilePerm); err != nil { + return fmt.Errorf("%w: %s", errAppendFooter, err.Error()) + } + + return nil + } +} diff --git a/cmd/git-sv/config.go b/app/config.go similarity index 69% rename from cmd/git-sv/config.go rename to app/config.go index ff5fd83..4eb2977 100644 --- a/cmd/git-sv/config.go +++ b/app/config.go @@ -1,13 +1,14 @@ -package main +package app import ( "fmt" - "log" "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" ) @@ -17,28 +18,57 @@ type EnvConfig struct { Home string `envconfig:"GITSV_HOME" default:""` } -func loadEnvConfig() EnvConfig { - var c EnvConfig - - err := envconfig.Process("", &c) - if err != nil { - log.Fatal("failed to load env config, error: ", err.Error()) - } - - return c -} - // Config cli yaml config. type Config struct { Version string `yaml:"version"` + LogLevel string `yaml:"log-level"` Versioning sv.VersioningConfig `yaml:"versioning"` - Tag sv.TagConfig `yaml:"tag"` + Tag TagConfig `yaml:"tag"` ReleaseNotes sv.ReleaseNotesConfig `yaml:"release-notes"` Branches sv.BranchesConfig `yaml:"branches"` CommitMessage sv.CommitMessageConfig `yaml:"commit-message"` } -func readConfig(filepath string) (Config, error) { +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 @@ -54,12 +84,12 @@ func readConfig(filepath string) (Config, error) { return cfg, nil } -func defaultConfig() Config { +func GetDefault() *Config { skipDetached := false pattern := "%d.%d.%d" filter := "" - return Config{ + return &Config{ Version: "1.1", Versioning: sv.VersioningConfig{ UpdateMajor: []string{}, @@ -67,7 +97,7 @@ func defaultConfig() Config { UpdatePatch: []string{"build", "ci", "chore", "docs", "fix", "perf", "refactor", "style", "test"}, IgnoreUnknown: false, }, - Tag: sv.TagConfig{ + Tag: TagConfig{ Pattern: &pattern, Filter: &filter, }, @@ -134,26 +164,26 @@ func (t *mergeTransformer) Transformer(typ reflect.Type) func(dst, src reflect.V return nil } -func migrateConfig(cfg Config, filename string) Config { +func migrate(cfg Config, filename string) Config { if cfg.ReleaseNotes.Headers == nil { return cfg } - warnf("config 'release-notes.headers' on %s is deprecated, please use 'sections' instead!", filename) + 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: migrateReleaseNotesConfig(cfg.ReleaseNotes.Headers), + Sections: migrateReleaseNotes(cfg.ReleaseNotes.Headers), }, Branches: cfg.Branches, CommitMessage: cfg.CommitMessage, } } -func migrateReleaseNotesConfig(headers map[string]string) []sv.ReleaseNotesSectionConfig { +func migrateReleaseNotes(headers map[string]string) []sv.ReleaseNotesSectionConfig { order := []string{"feat", "fix", "refactor", "perf", "test", "build", "ci", "chore", "docs", "style"} var sections []sv.ReleaseNotesSectionConfig @@ -181,3 +211,19 @@ func migrateReleaseNotesConfig(headers map[string]string) []sv.ReleaseNotesSecti 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"` +} diff --git a/cmd/git-sv/config_test.go b/app/config_test.go similarity index 98% rename from cmd/git-sv/config_test.go rename to app/config_test.go index 5b149fb..cbd45d8 100644 --- a/cmd/git-sv/config_test.go +++ b/app/config_test.go @@ -1,4 +1,4 @@ -package main +package app import ( "reflect" @@ -128,21 +128,21 @@ func Test_merge(t *testing.T) { "overwrite tag config", Config{ Version: "a", - Tag: sv.TagConfig{ + Tag: TagConfig{ Pattern: &nonEmptyStr, Filter: &nonEmptyStr, }, }, Config{ Version: "", - Tag: sv.TagConfig{ + Tag: TagConfig{ Pattern: &emptyStr, Filter: &emptyStr, }, }, Config{ Version: "a", - Tag: sv.TagConfig{ + Tag: TagConfig{ Pattern: &emptyStr, Filter: &emptyStr, }, diff --git a/cmd/git-sv/handlers.go b/cmd/git-sv/handlers.go deleted file mode 100644 index 2e31a47..0000000 --- a/cmd/git-sv/handlers.go +++ /dev/null @@ -1,754 +0,0 @@ -package main - -import ( - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "sort" - "strings" - "time" - - "github.com/Masterminds/semver/v3" - "github.com/thegeeklab/git-sv/v2/sv" - "github.com/urfave/cli/v2" - "gopkg.in/yaml.v3" -) - -const laxFilePerm = 0o644 - -var ( - errCanNotCreateTagFlag = errors.New("cannot define tag flag with range, start or end flags") - errUnknownTag = errors.New("unknown tag") - errReadCommitMessage = errors.New("failed to read commit message") - errAppendFooter = errors.New("failed to append meta-informations on footer") - errInvalidRange = errors.New("invalid log range") -) - -func configDefaultHandler() func(c *cli.Context) error { - cfg := defaultConfig() - - return func(c *cli.Context) error { - content, err := yaml.Marshal(&cfg) - if err != nil { - return err - } - - fmt.Println(string(content)) - - return nil - } -} - -func configShowHandler(cfg Config) func(c *cli.Context) error { - return func(c *cli.Context) error { - content, err := yaml.Marshal(&cfg) - if err != nil { - return err - } - - fmt.Println(string(content)) - - return nil - } -} - -func currentVersionHandler(git sv.Git) func(c *cli.Context) error { - return func(c *cli.Context) error { - lastTag := git.LastTag() - - currentVer, err := sv.ToVersion(lastTag) - if err != nil { - return fmt.Errorf("error parsing version: %s from git tag, message: %w", lastTag, err) - } - - fmt.Printf("%d.%d.%d\n", currentVer.Major(), currentVer.Minor(), currentVer.Patch()) - - return nil - } -} - -func nextVersionHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) func(c *cli.Context) error { - return func(c *cli.Context) error { - lastTag := git.LastTag() - - currentVer, err := sv.ToVersion(lastTag) - if err != nil { - return fmt.Errorf("error parsing version: %s from git tag, message: %w", lastTag, err) - } - - commits, err := git.Log(sv.NewLogRange(sv.TagRange, lastTag, "")) - if err != nil { - return fmt.Errorf("error getting git log, message: %w", err) - } - - nextVer, _ := semverProcessor.NextVersion(currentVer, commits) - - fmt.Printf("%d.%d.%d\n", nextVer.Major(), nextVer.Minor(), nextVer.Patch()) - - return nil - } -} - -func commitLogFlags() []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{ - Name: "t", - Aliases: []string{"tag"}, - Usage: "get commit log from a specific tag", - }, - &cli.StringFlag{ - Name: "r", - Aliases: []string{"range"}, - Usage: "type of range of commits, use: tag, date or hash", - Value: string(sv.TagRange), - }, - &cli.StringFlag{ - Name: "s", - Aliases: []string{"start"}, - Usage: "start range of git log revision range, if date, the value is used on since flag instead", - }, - &cli.StringFlag{ - Name: "e", - Aliases: []string{"end"}, - Usage: "end range of git log revision range, if date, the value is used on until flag instead", - }, - } -} - -func commitLogHandler(git sv.Git) func(c *cli.Context) error { - return func(c *cli.Context) error { - var ( - commits []sv.GitCommitLog - err error - ) - - tagFlag := c.String("t") - rangeFlag := c.String("r") - startFlag := c.String("s") - endFlag := c.String("e") - - if tagFlag != "" && (rangeFlag != string(sv.TagRange) || startFlag != "" || endFlag != "") { - return errCanNotCreateTagFlag - } - - if tagFlag != "" { - commits, err = getTagCommits(git, tagFlag) - } else { - r, rerr := logRange(git, rangeFlag, startFlag, endFlag) - if rerr != nil { - return rerr - } - commits, err = git.Log(r) - } - - if err != nil { - return fmt.Errorf("error getting git log, message: %w", err) - } - - for _, commit := range commits { - content, err := json.Marshal(commit) - if err != nil { - return err - } - - fmt.Println(string(content)) - } - - return nil - } -} - -func getTagCommits(git sv.Git, tag string) ([]sv.GitCommitLog, error) { - prev, _, err := getTags(git, tag) - if err != nil { - return nil, err - } - - return git.Log(sv.NewLogRange(sv.TagRange, prev, tag)) -} - -func logRange(git sv.Git, rangeFlag, startFlag, endFlag string) (sv.LogRange, error) { - switch rangeFlag { - case string(sv.TagRange): - return sv.NewLogRange(sv.TagRange, str(startFlag, git.LastTag()), endFlag), nil - case string(sv.DateRange): - return sv.NewLogRange(sv.DateRange, startFlag, endFlag), nil - case string(sv.HashRange): - return sv.NewLogRange(sv.HashRange, startFlag, endFlag), nil - default: - return sv.LogRange{}, fmt.Errorf( - "%w: %s, expected: %s, %s or %s", - errInvalidRange, - rangeFlag, - sv.TagRange, - sv.DateRange, - sv.HashRange, - ) - } -} - -func commitNotesFlags() []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{ - Name: "r", Aliases: []string{"range"}, - Usage: "type of range of commits, use: tag, date or hash", - Required: true, - }, - &cli.StringFlag{ - Name: "s", - Aliases: []string{"start"}, - Usage: "start range of git log revision range, if date, the value is used on since flag instead", - }, - &cli.StringFlag{ - Name: "e", - Aliases: []string{"end"}, - Usage: "end range of git log revision range, if date, the value is used on until flag instead", - }, - } -} - -func commitNotesHandler( - git sv.Git, rnProcessor sv.ReleaseNoteProcessor, outputFormatter sv.OutputFormatter, -) func(c *cli.Context) error { - return func(c *cli.Context) error { - var date time.Time - - rangeFlag := c.String("r") - - lr, err := logRange(git, rangeFlag, c.String("s"), c.String("e")) - if err != nil { - return err - } - - commits, err := git.Log(lr) - if err != nil { - return fmt.Errorf("error getting git log from range: %s, message: %w", rangeFlag, err) - } - - if len(commits) > 0 { - date, _ = time.Parse("2006-01-02", commits[0].Date) - } - - output, err := outputFormatter.FormatReleaseNote(rnProcessor.Create(nil, "", date, commits)) - if err != nil { - return fmt.Errorf("could not format release notes, message: %w", err) - } - - fmt.Println(output) - - return nil - } -} - -func releaseNotesFlags() []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{ - Name: "t", - Aliases: []string{"tag"}, - Usage: "get release note from tag", - }, - } -} - -func releaseNotesHandler( - git sv.Git, - semverProcessor sv.SemVerCommitsProcessor, - rnProcessor sv.ReleaseNoteProcessor, - outputFormatter sv.OutputFormatter, -) func(c *cli.Context) error { - return func(c *cli.Context) error { - var ( - commits []sv.GitCommitLog - rnVersion *semver.Version - tag string - date time.Time - err error - ) - - if tag = c.String("t"); tag != "" { - rnVersion, date, commits, err = getTagVersionInfo(git, tag) - } else { - // TODO: should generate release notes if version was not updated? - rnVersion, _, date, commits, err = getNextVersionInfo(git, semverProcessor) - } - - if err != nil { - return err - } - - releasenote := rnProcessor.Create(rnVersion, tag, date, commits) - - output, err := outputFormatter.FormatReleaseNote(releasenote) - if err != nil { - return fmt.Errorf("could not format release notes, message: %w", err) - } - - fmt.Println(output) - - return nil - } -} - -func getTagVersionInfo(git sv.Git, tag string) (*semver.Version, time.Time, []sv.GitCommitLog, error) { - tagVersion, _ := sv.ToVersion(tag) - - previousTag, currentTag, err := getTags(git, tag) - if err != nil { - return nil, time.Time{}, nil, fmt.Errorf("error listing tags, message: %w", err) - } - - commits, err := git.Log(sv.NewLogRange(sv.TagRange, previousTag, tag)) - if err != nil { - return nil, time.Time{}, nil, fmt.Errorf("error getting git log from tag: %s, message: %w", tag, err) - } - - return tagVersion, currentTag.Date, commits, nil -} - -func getTags(git sv.Git, tag string) (string, sv.GitTag, error) { - tags, err := git.Tags() - if err != nil { - return "", sv.GitTag{}, err - } - - index := find(tag, tags) - if index < 0 { - return "", sv.GitTag{}, fmt.Errorf("%w: %s not found, check tag filter", errUnknownTag, tag) - } - - previousTag := "" - if index > 0 { - previousTag = tags[index-1].Name - } - - return previousTag, tags[index], nil -} - -func find(tag string, tags []sv.GitTag) int { - for i := 0; i < len(tags); i++ { - if tag == tags[i].Name { - return i - } - } - - return -1 -} - -func getNextVersionInfo( - git sv.Git, semverProcessor sv.SemVerCommitsProcessor, -) (*semver.Version, bool, time.Time, []sv.GitCommitLog, error) { - lastTag := git.LastTag() - - commits, err := git.Log(sv.NewLogRange(sv.TagRange, lastTag, "")) - if err != nil { - return nil, false, time.Time{}, nil, fmt.Errorf("error getting git log, message: %w", err) - } - - currentVer, _ := sv.ToVersion(lastTag) - version, updated := semverProcessor.NextVersion(currentVer, commits) - - return version, updated, time.Now(), commits, nil -} - -func tagHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) func(c *cli.Context) error { - return func(c *cli.Context) error { - lastTag := git.LastTag() - - currentVer, err := sv.ToVersion(lastTag) - if err != nil { - return fmt.Errorf("error parsing version: %s from git tag, message: %w", lastTag, err) - } - - commits, err := git.Log(sv.NewLogRange(sv.TagRange, lastTag, "")) - if err != nil { - return fmt.Errorf("error getting git log, message: %w", err) - } - - nextVer, _ := semverProcessor.NextVersion(currentVer, commits) - tagname, err := git.Tag(*nextVer) - - fmt.Println(tagname) - - if err != nil { - return fmt.Errorf("error generating tag version: %s, message: %w", nextVer.String(), err) - } - - return nil - } -} - -func getCommitType(cfg Config, p sv.MessageProcessor, input string) (string, error) { - if input == "" { - t, err := promptType(cfg.CommitMessage.Types) - - return t.Type, err - } - - return input, p.ValidateType(input) -} - -func getCommitScope(cfg Config, p sv.MessageProcessor, input string, noScope bool) (string, error) { - if input == "" && !noScope { - return promptScope(cfg.CommitMessage.Scope.Values) - } - - return input, p.ValidateScope(input) -} - -func getCommitDescription(p sv.MessageProcessor, input string) (string, error) { - if input == "" { - return promptSubject() - } - - return input, p.ValidateDescription(input) -} - -func getCommitBody(noBody bool) (string, error) { - if noBody { - return "", nil - } - - var fullBody strings.Builder - - for body, err := promptBody(); body != "" || err != nil; body, err = promptBody() { - if err != nil { - return "", err - } - - if fullBody.Len() > 0 { - fullBody.WriteString("\n") - } - - if body != "" { - fullBody.WriteString(body) - } - } - - return fullBody.String(), nil -} - -func getCommitIssue(cfg Config, p sv.MessageProcessor, branch string, noIssue bool) (string, error) { - branchIssue, err := p.IssueID(branch) - if err != nil { - return "", err - } - - if cfg.CommitMessage.IssueFooterConfig().Key == "" || cfg.CommitMessage.Issue.Regex == "" { - return "", nil - } - - if noIssue { - return branchIssue, nil - } - - return promptIssueID("issue id", cfg.CommitMessage.Issue.Regex, branchIssue) -} - -func getCommitBreakingChange(noBreaking bool, input string) (string, error) { - if noBreaking { - return "", nil - } - - if strings.TrimSpace(input) != "" { - return input, nil - } - - hasBreakingChanges, err := promptConfirm("has breaking change?") - if err != nil { - return "", err - } - - if !hasBreakingChanges { - return "", nil - } - - return promptBreakingChanges() -} - -func commitFlags() []cli.Flag { - return []cli.Flag{ - &cli.BoolFlag{ - Name: "no-scope", - Aliases: []string{"nsc"}, - Usage: "do not prompt for commit scope", - }, - &cli.BoolFlag{ - Name: "no-body", - Aliases: []string{"nbd"}, - Usage: "do not prompt for commit body", - }, - &cli.BoolFlag{ - Name: "no-issue", - Aliases: []string{"nis"}, - Usage: "do not prompt for commit issue, will try to recover from branch if enabled", - }, - &cli.BoolFlag{ - Name: "no-breaking", - Aliases: []string{"nbc"}, - Usage: "do not prompt for breaking changes", - }, - &cli.StringFlag{ - Name: "type", - Aliases: []string{"t"}, - Usage: "define commit type", - }, - &cli.StringFlag{ - Name: "scope", - Aliases: []string{"s"}, - Usage: "define commit scope", - }, - &cli.StringFlag{ - Name: "description", - Aliases: []string{"d"}, - Usage: "define commit description", - }, - &cli.StringFlag{ - Name: "breaking-change", - Aliases: []string{"b"}, - Usage: "define commit breaking change message", - }, - } -} - -func commitHandler(cfg Config, git sv.Git, messageProcessor sv.MessageProcessor) func(c *cli.Context) error { - return func(c *cli.Context) error { - noBreaking := c.Bool("no-breaking") - noBody := c.Bool("no-body") - noIssue := c.Bool("no-issue") - noScope := c.Bool("no-scope") - inputType := c.String("type") - inputScope := c.String("scope") - inputDescription := c.String("description") - inputBreakingChange := c.String("breaking-change") - - ctype, err := getCommitType(cfg, messageProcessor, inputType) - if err != nil { - return err - } - - scope, err := getCommitScope(cfg, messageProcessor, inputScope, noScope) - if err != nil { - return err - } - - subject, err := getCommitDescription(messageProcessor, inputDescription) - if err != nil { - return err - } - - fullBody, err := getCommitBody(noBody) - if err != nil { - return err - } - - issue, err := getCommitIssue(cfg, messageProcessor, git.Branch(), noIssue) - if err != nil { - return err - } - - breakingChange, err := getCommitBreakingChange(noBreaking, inputBreakingChange) - if err != nil { - return err - } - - header, body, footer := messageProcessor.Format( - sv.NewCommitMessage(ctype, scope, subject, fullBody, issue, breakingChange), - ) - - err = git.Commit(header, body, footer) - if err != nil { - return fmt.Errorf("error executing git commit, message: %w", err) - } - - return nil - } -} - -func changelogFlags() []cli.Flag { - return []cli.Flag{ - &cli.IntFlag{ - Name: "size", - Value: 10, //nolint:gomnd - Aliases: []string{"n"}, - Usage: "get changelog from last 'n' tags", - }, - &cli.BoolFlag{ - Name: "all", - Usage: "ignore size parameter, get changelog for every tag", - }, - &cli.BoolFlag{ - Name: "add-next-version", - Usage: "add next version on change log (commits since last tag, but only if there is a new version to release)", - }, - &cli.BoolFlag{ - Name: "semantic-version-only", - Usage: "only show tags 'SemVer-ish'", - }, - } -} - -func changelogHandler( - git sv.Git, - semverProcessor sv.SemVerCommitsProcessor, - rnProcessor sv.ReleaseNoteProcessor, - formatter sv.OutputFormatter, -) func(c *cli.Context) error { - return func(c *cli.Context) error { - tags, err := git.Tags() - if err != nil { - return err - } - - sort.Slice(tags, func(i, j int) bool { - return tags[i].Date.After(tags[j].Date) - }) - - var releaseNotes []sv.ReleaseNote - - size := c.Int("size") - all := c.Bool("all") - addNextVersion := c.Bool("add-next-version") - semanticVersionOnly := c.Bool("semantic-version-only") - - if addNextVersion { - rnVersion, updated, date, commits, uerr := getNextVersionInfo(git, semverProcessor) - if uerr != nil { - return uerr - } - - if updated { - releaseNotes = append(releaseNotes, rnProcessor.Create(rnVersion, "", date, commits)) - } - } - - for i, tag := range tags { - if !all && i >= size { - break - } - - previousTag := "" - if i+1 < len(tags) { - previousTag = tags[i+1].Name - } - - if semanticVersionOnly && !sv.IsValidVersion(tag.Name) { - continue - } - - commits, err := git.Log(sv.NewLogRange(sv.TagRange, previousTag, tag.Name)) - if err != nil { - return fmt.Errorf("error getting git log from tag: %s, message: %w", tag.Name, err) - } - - currentVer, _ := sv.ToVersion(tag.Name) - releaseNotes = append(releaseNotes, rnProcessor.Create(currentVer, tag.Name, tag.Date, commits)) - } - - output, err := formatter.FormatChangelog(releaseNotes) - if err != nil { - return fmt.Errorf("could not format changelog, message: %w", err) - } - - fmt.Println(output) - - return nil - } -} - -func validateCommitMessageFlags() []cli.Flag { - return []cli.Flag{ - &cli.StringFlag{ - Name: "path", - Required: true, - Usage: "git working directory", - }, - &cli.StringFlag{ - Name: "file", - Required: true, - Usage: "name of the file that contains the commit log message", - }, - &cli.StringFlag{ - Name: "source", - Required: true, - Usage: "source of the commit message", - }, - } -} - -func validateCommitMessageHandler(git sv.Git, messageProcessor sv.MessageProcessor) func(c *cli.Context) error { - return func(c *cli.Context) error { - branch := git.Branch() - detached, derr := git.IsDetached() - - if messageProcessor.SkipBranch(branch, derr == nil && detached) { - warnf("commit message validation skipped, branch in ignore list or detached...") - - return nil - } - - if source := c.String("source"); source == "merge" { - warnf("commit message validation skipped, ignoring source: %s...", source) - - return nil - } - - filepath := filepath.Join(c.String("path"), c.String("file")) - - commitMessage, err := readFile(filepath) - if err != nil { - return fmt.Errorf("%w: %s", errReadCommitMessage, err.Error()) - } - - if err := messageProcessor.Validate(commitMessage); err != nil { - return fmt.Errorf("%w: %s", errReadCommitMessage, err.Error()) - } - - msg, err := messageProcessor.Enhance(branch, commitMessage) - if err != nil { - warnf("could not enhance commit message, %s", err.Error()) - - return nil - } - - if msg == "" { - return nil - } - - if err := appendOnFile(msg, filepath); err != nil { - return fmt.Errorf("%w: %s", errAppendFooter, err.Error()) - } - - return nil - } -} - -func readFile(filepath string) (string, error) { - f, err := os.ReadFile(filepath) - if err != nil { - return "", err - } - - return string(f), nil -} - -func appendOnFile(message, filepath string) error { - f, err := os.OpenFile(filepath, os.O_APPEND|os.O_WRONLY, laxFilePerm) - if err != nil { - return err - } - defer f.Close() - - _, err = f.WriteString(message) - - return err -} - -func str(value, defaultValue string) string { - if value != "" { - return value - } - - return defaultValue -} diff --git a/cmd/git-sv/log.go b/cmd/git-sv/log.go deleted file mode 100644 index d5a57c5..0000000 --- a/cmd/git-sv/log.go +++ /dev/null @@ -1,10 +0,0 @@ -package main - -import ( - "fmt" - "os" -) - -func warnf(format string, values ...interface{}) { - fmt.Fprintf(os.Stderr, "WARN: "+format+"\n", values...) -} diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go index df2fecf..2543a7f 100644 --- a/cmd/git-sv/main.go +++ b/cmd/git-sv/main.go @@ -1,14 +1,14 @@ package main import ( - "embed" "fmt" - "io/fs" - "log" "os" - "path/filepath" - "github.com/thegeeklab/git-sv/v2/sv" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + + "github.com/thegeeklab/git-sv/v2/app" + "github.com/thegeeklab/git-sv/v2/app/commands" "github.com/urfave/cli/v2" ) @@ -18,39 +18,8 @@ var ( BuildDate = "00000000" ) -const ( - configFilename = "config.yml" - configDir = ".gitsv" -) - -//go:embed resources/templates/*.tpl -var defaultTemplatesFS embed.FS - -func templateFS(filepath string) fs.FS { - if _, err := os.Stat(filepath); err != nil { - defaultTemplatesFS, _ := fs.Sub(defaultTemplatesFS, "resources/templates") - - return defaultTemplatesFS - } - - return os.DirFS(filepath) -} - func main() { - log.SetFlags(0) - - wd, err := os.Getwd() - if err != nil { - log.Fatal("error while retrieving working directory: %w", err) - } - - cfg := loadCfg(wd) - messageProcessor := sv.NewMessageProcessor(cfg.CommitMessage, cfg.Branches) - git := sv.NewGit(messageProcessor, cfg.Tag) - semverProcessor := sv.NewSemVerCommitsProcessor(cfg.Versioning, cfg.CommitMessage) - releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotes) - outputFormatter := sv.NewOutputFormatter(templateFS(filepath.Join(wd, configDir, "templates"))) - + gsv := app.New() cli.VersionPrinter = func(c *cli.Context) { fmt.Printf("%s version=%s date=%s\n", c.App.Name, c.App.Version, BuildDate) } @@ -59,6 +28,23 @@ func main() { Name: "git-sv", Usage: "Semantic version for git.", Version: BuildVersion, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "log-level", + Usage: "log level", + }, + }, + Before: func(ctx *cli.Context) error { + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + lvl, err := zerolog.ParseLevel(ctx.String("log-level")) + if err != nil { + return err + } + + zerolog.SetGlobalLevel(lvl) + + return nil + }, Commands: []*cli.Command{ { Name: "config", @@ -68,12 +54,12 @@ func main() { { Name: "default", Usage: "show default config", - Action: configDefaultHandler(), + Action: commands.ConfigDefaultHandler(), }, { Name: "show", Usage: "show current config", - Action: configShowHandler(cfg), + Action: commands.ConfigShowHandler(gsv.Config), }, }, }, @@ -81,13 +67,13 @@ func main() { Name: "current-version", Aliases: []string{"cv"}, Usage: "get last released version from git", - Action: currentVersionHandler(git), + Action: commands.CurrentVersionHandler(gsv), }, { Name: "next-version", Aliases: []string{"nv"}, Usage: "generate the next version based on git commit messages", - Action: nextVersionHandler(git, semverProcessor), + Action: commands.NextVersionHandler(gsv), }, { Name: "commit-log", @@ -96,8 +82,8 @@ func main() { Description: `The range filter is used based on git log filters, check https://git-scm.com/docs/git-log for more info. When flag range is "tag" and start is empty, last tag created will be used instead. When flag range is "date", if "end" is YYYY-MM-DD the range will be inclusive.`, - Action: commitLogHandler(git), - Flags: commitLogFlags(), + Action: commands.CommitLogHandler(gsv), + Flags: commands.CommitLogFlags(), }, { Name: "commit-notes", @@ -106,74 +92,47 @@ When flag range is "date", if "end" is YYYY-MM-DD the range will be inclusive.`, Description: `The range filter is used based on git log filters, check https://git-scm.com/docs/git-log for more info. When flag range is "tag" and start is empty, last tag created will be used instead. When flag range is "date", if "end" is YYYY-MM-DD the range will be inclusive.`, - Action: commitNotesHandler(git, releasenotesProcessor, outputFormatter), - Flags: commitNotesFlags(), + Action: commands.CommitNotesHandler(gsv), + Flags: commands.CommitNotesFlags(), }, { Name: "release-notes", Aliases: []string{"rn"}, Usage: "generate release notes", - Action: releaseNotesHandler(git, semverProcessor, releasenotesProcessor, outputFormatter), - Flags: releaseNotesFlags(), + Action: commands.ReleaseNotesHandler(gsv), + Flags: commands.ReleaseNotesFlags(), }, { Name: "changelog", Aliases: []string{"cgl"}, Usage: "generate changelog", - Action: changelogHandler(git, semverProcessor, releasenotesProcessor, outputFormatter), - Flags: changelogFlags(), + Action: commands.ChangelogHandler(gsv), + Flags: commands.ChangelogFlags(), }, { Name: "tag", Aliases: []string{"tg"}, Usage: "generate tag with version based on git commit messages", - Action: tagHandler(git, semverProcessor), + Action: commands.TagHandler(gsv), }, { Name: "commit", Aliases: []string{"cmt"}, Usage: "execute git commit with conventional commit message helper", - Action: commitHandler(cfg, git, messageProcessor), - Flags: commitFlags(), + Action: commands.CommitHandler(gsv), + Flags: commands.CommitFlags(), }, { Name: "validate-commit-message", Aliases: []string{"vcm"}, Usage: "use as prepare-commit-message hook to validate and enhance commit message", - Action: validateCommitMessageHandler(git, messageProcessor), - Flags: validateCommitMessageFlags(), + Action: commands.ValidateCommitMessageHandler(gsv), + Flags: commands.ValidateCommitMessageFlags(), }, }, } if apperr := app.Run(os.Args); apperr != nil { - log.Fatal("ERROR: ", apperr) - } -} - -func loadCfg(wd string) Config { - cfg := defaultConfig() - - envCfg := loadEnvConfig() - if envCfg.Home != "" { - homeCfgFilepath := filepath.Join(envCfg.Home, configFilename) - if homeCfg, err := readConfig(homeCfgFilepath); err == nil { - if merr := merge(&cfg, migrateConfig(homeCfg, homeCfgFilepath)); merr != nil { - log.Fatal("failed to merge user config, error: ", merr) - } - } + log.Fatal().Err(apperr).Msg("Execution error") } - - repoCfgFilepath := filepath.Join(wd, configDir, configFilename) - if repoCfg, err := readConfig(repoCfgFilepath); err == nil { - if merr := merge(&cfg, migrateConfig(repoCfg, repoCfgFilepath)); merr != nil { - log.Fatal("failed to merge repo config, error: ", merr) - } - - if len(repoCfg.ReleaseNotes.Headers) > 0 { // mergo is merging maps, headers will be overwritten - cfg.ReleaseNotes.Headers = repoCfg.ReleaseNotes.Headers - } - } - - return cfg } diff --git a/cmd/git-sv/resources_test.go b/cmd/git-sv/resources_test.go deleted file mode 100644 index 4bbd0d4..0000000 --- a/cmd/git-sv/resources_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - "testing" -) - -func Test_checkTemplatesFiles(t *testing.T) { - tests := []string{ - "resources/templates/changelog-md.tpl", - "resources/templates/releasenotes-md.tpl", - } - for _, tt := range tests { - t.Run(tt, func(t *testing.T) { - got, err := defaultTemplatesFS.ReadFile(tt) - if err != nil { - t.Errorf("missing template error = %v", err) - - return - } - - if len(got) == 0 { - t.Errorf("empty template") - } - }) - } -} diff --git a/go.mod b/go.mod index 75d6aa8..fa20ff8 100644 --- a/go.mod +++ b/go.mod @@ -5,19 +5,33 @@ go 1.19 require ( dario.cat/mergo v1.0.0 github.com/Masterminds/semver/v3 v3.2.1 + github.com/Masterminds/sprig/v3 v3.2.3 github.com/kelseyhightower/envconfig v1.4.0 github.com/manifoldco/promptui v0.9.0 + github.com/rs/zerolog v1.31.0 github.com/urfave/cli/v2 v2.25.7 + gopkg.in/yaml.v2 v2.3.0 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/Masterminds/goutils v1.1.1 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/google/uuid v1.1.1 // indirect + github.com/huandu/xstrings v1.3.3 // indirect + github.com/imdario/mergo v0.3.11 // indirect github.com/kr/pretty v0.3.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shopspring/decimal v1.2.0 // indirect + github.com/spf13/cast v1.3.1 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + golang.org/x/crypto v0.3.0 // indirect golang.org/x/sys v0.13.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index f5c6dd3..c100da2 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,12 @@ dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= @@ -11,9 +16,20 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -25,22 +41,80 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/sv/semver.go b/sv/commit.go similarity index 64% rename from sv/semver.go rename to sv/commit.go index f90c9e1..f584f83 100644 --- a/sv/semver.go +++ b/sv/commit.go @@ -11,6 +11,15 @@ const ( major ) +// CommitLog description of a single commit log. +type CommitLog struct { + Date string `json:"date,omitempty"` + Timestamp int `json:"timestamp,omitempty"` + AuthorName string `json:"authorName,omitempty"` + Hash string `json:"hash,omitempty"` + Message CommitMessage `json:"message,omitempty"` +} + // IsValidVersion return true when a version is valid. func IsValidVersion(value string) bool { _, err := semver.NewVersion(value) @@ -28,13 +37,13 @@ func ToVersion(value string) (*semver.Version, error) { return semver.NewVersion(version) } -// SemVerCommitsProcessor interface. -type SemVerCommitsProcessor interface { - NextVersion(version *semver.Version, commits []GitCommitLog) (*semver.Version, bool) +// CommitProcessor interface. +type CommitProcessor interface { + NextVersion(version *semver.Version, commits []CommitLog) (*semver.Version, bool) } -// SemVerCommitsProcessorImpl process versions using commit log. -type SemVerCommitsProcessorImpl struct { +// SemVerCommitProcessor process versions using commit log. +type SemVerCommitProcessor struct { MajorVersionTypes map[string]struct{} MinorVersionTypes map[string]struct{} PatchVersionTypes map[string]struct{} @@ -42,9 +51,17 @@ type SemVerCommitsProcessorImpl struct { IncludeUnknownTypeAsPatch bool } -// NewSemVerCommitsProcessor SemanticVersionCommitsProcessorImpl constructor. -func NewSemVerCommitsProcessor(vcfg VersioningConfig, mcfg CommitMessageConfig) *SemVerCommitsProcessorImpl { - return &SemVerCommitsProcessorImpl{ +// VersioningConfig versioning preferences. +type VersioningConfig struct { + UpdateMajor []string `yaml:"update-major,flow"` + UpdateMinor []string `yaml:"update-minor,flow"` + UpdatePatch []string `yaml:"update-patch,flow"` + IgnoreUnknown bool `yaml:"ignore-unknown"` +} + +// NewSemVerCommitProcessor SemanticVersionCommitProcessorImpl constructor. +func NewSemVerCommitProcessor(vcfg VersioningConfig, mcfg CommitMessageConfig) *SemVerCommitProcessor { + return &SemVerCommitProcessor{ IncludeUnknownTypeAsPatch: !vcfg.IgnoreUnknown, MajorVersionTypes: toMap(vcfg.UpdateMajor), MinorVersionTypes: toMap(vcfg.UpdateMinor), @@ -54,8 +71,8 @@ func NewSemVerCommitsProcessor(vcfg VersioningConfig, mcfg CommitMessageConfig) } // NextVersion calculates next version based on commit log. -func (p SemVerCommitsProcessorImpl) NextVersion( - version *semver.Version, commits []GitCommitLog, +func (p SemVerCommitProcessor) NextVersion( + version *semver.Version, commits []CommitLog, ) (*semver.Version, bool) { versionToUpdate := none for _, commit := range commits { @@ -87,7 +104,7 @@ func updateVersion(version semver.Version, versionToUpdate versionType) semver.V } } -func (p SemVerCommitsProcessorImpl) versionTypeToUpdate(commit GitCommitLog) versionType { +func (p SemVerCommitProcessor) versionTypeToUpdate(commit CommitLog) versionType { if commit.Message.IsBreakingChange { return major } diff --git a/sv/semver_test.go b/sv/commit_test.go similarity index 63% rename from sv/semver_test.go rename to sv/commit_test.go index 34cefdf..73aad75 100644 --- a/sv/semver_test.go +++ b/sv/commit_test.go @@ -7,106 +7,106 @@ import ( "github.com/Masterminds/semver/v3" ) -func TestSemVerCommitsProcessorImpl_NextVersion(t *testing.T) { +func TestSemVerCommitProcessor_NextVersion(t *testing.T) { tests := []struct { name string ignoreUnknown bool version *semver.Version - commits []GitCommitLog + commits []CommitLog want *semver.Version wantUpdated bool }{ { "no update", true, - version("0.0.0"), - []GitCommitLog{}, - version("0.0.0"), + TestVersion("0.0.0"), + []CommitLog{}, + TestVersion("0.0.0"), false, }, { "no update without version", true, nil, - []GitCommitLog{}, + []CommitLog{}, nil, false, }, { "no update on unknown type", true, - version("0.0.0"), - []GitCommitLog{commitlog("a", map[string]string{}, "a")}, - version("0.0.0"), + TestVersion("0.0.0"), + []CommitLog{TestCommitlog("a", map[string]string{}, "a")}, + TestVersion("0.0.0"), false, }, { "no update on unmapped known type", false, - version("0.0.0"), - []GitCommitLog{commitlog("none", map[string]string{}, "a")}, - version("0.0.0"), + TestVersion("0.0.0"), + []CommitLog{TestCommitlog("none", map[string]string{}, "a")}, + TestVersion("0.0.0"), false, }, { "update patch on unknown type", false, - version("0.0.0"), - []GitCommitLog{commitlog("a", map[string]string{}, "a")}, - version("0.0.1"), + TestVersion("0.0.0"), + []CommitLog{TestCommitlog("a", map[string]string{}, "a")}, + TestVersion("0.0.1"), true, }, { "patch update", - false, version("0.0.0"), - []GitCommitLog{commitlog("patch", map[string]string{}, "a")}, - version("0.0.1"), true, + false, TestVersion("0.0.0"), + []CommitLog{TestCommitlog("patch", map[string]string{}, "a")}, + TestVersion("0.0.1"), true, }, { "patch update without version", false, nil, - []GitCommitLog{commitlog("patch", map[string]string{}, "a")}, + []CommitLog{TestCommitlog("patch", map[string]string{}, "a")}, nil, true, }, { "minor update", false, - version("0.0.0"), - []GitCommitLog{ - commitlog("patch", map[string]string{}, "a"), - commitlog("minor", map[string]string{}, "a"), + TestVersion("0.0.0"), + []CommitLog{ + TestCommitlog("patch", map[string]string{}, "a"), + TestCommitlog("minor", map[string]string{}, "a"), }, - version("0.1.0"), + TestVersion("0.1.0"), true, }, { "major update", false, - version("0.0.0"), - []GitCommitLog{ - commitlog("patch", map[string]string{}, "a"), - commitlog("major", map[string]string{}, "a"), + TestVersion("0.0.0"), + []CommitLog{ + TestCommitlog("patch", map[string]string{}, "a"), + TestCommitlog("major", map[string]string{}, "a"), }, - version("1.0.0"), + TestVersion("1.0.0"), true, }, { "breaking change update", false, - version("0.0.0"), - []GitCommitLog{ - commitlog("patch", map[string]string{}, "a"), - commitlog("patch", map[string]string{"breaking-change": "break"}, "a"), + TestVersion("0.0.0"), + []CommitLog{ + TestCommitlog("patch", map[string]string{}, "a"), + TestCommitlog("patch", map[string]string{"breaking-change": "break"}, "a"), }, - version("1.0.0"), + TestVersion("1.0.0"), true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - p := NewSemVerCommitsProcessor( + p := NewSemVerCommitProcessor( VersioningConfig{ UpdateMajor: []string{"major"}, UpdateMinor: []string{"minor"}, @@ -116,10 +116,10 @@ func TestSemVerCommitsProcessorImpl_NextVersion(t *testing.T) { CommitMessageConfig{Types: []string{"major", "minor", "patch", "none"}}) got, gotUpdated := p.NextVersion(tt.version, tt.commits) if !reflect.DeepEqual(got, tt.want) { - t.Errorf("SemVerCommitsProcessorImpl.NextVersion() Version = %v, want %v", got, tt.want) + t.Errorf("SemVerCommitProcessor.NextVersion() Version = %v, want %v", got, tt.want) } if tt.wantUpdated != gotUpdated { - t.Errorf("SemVerCommitsProcessorImpl.NextVersion() Updated = %v, want %v", gotUpdated, tt.wantUpdated) + t.Errorf("SemVerCommitProcessor.NextVersion() Updated = %v, want %v", gotUpdated, tt.wantUpdated) } }) } @@ -132,9 +132,9 @@ func TestToVersion(t *testing.T) { want *semver.Version wantErr bool }{ - {"empty version", "", version("0.0.0"), false}, + {"empty version", "", TestVersion("0.0.0"), false}, {"invalid version", "abc", nil, true}, - {"valid version", "1.2.3", version("1.2.3"), false}, + {"valid version", "1.2.3", TestVersion("1.2.3"), false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/sv/config.go b/sv/config.go deleted file mode 100644 index c504355..0000000 --- a/sv/config.go +++ /dev/null @@ -1,100 +0,0 @@ -package sv - -// ==== Message ==== - -// CommitMessageConfig config a commit message. -type CommitMessageConfig struct { - Types []string `yaml:"types,flow"` - HeaderSelector string `yaml:"header-selector"` - Scope CommitMessageScopeConfig `yaml:"scope"` - Footer map[string]CommitMessageFooterConfig `yaml:"footer"` - Issue CommitMessageIssueConfig `yaml:"issue"` -} - -// IssueFooterConfig config for issue. -func (c CommitMessageConfig) IssueFooterConfig() CommitMessageFooterConfig { - if v, exists := c.Footer[issueMetadataKey]; exists { - return v - } - - return CommitMessageFooterConfig{} -} - -// CommitMessageScopeConfig config scope preferences. -type CommitMessageScopeConfig struct { - Values []string `yaml:"values"` -} - -// CommitMessageFooterConfig config footer metadata. -type CommitMessageFooterConfig struct { - Key string `yaml:"key"` - KeySynonyms []string `yaml:"key-synonyms,flow"` - UseHash bool `yaml:"use-hash"` - AddValuePrefix string `yaml:"add-value-prefix"` -} - -// CommitMessageIssueConfig issue preferences. -type CommitMessageIssueConfig struct { - Regex string `yaml:"regex"` -} - -// ==== Branches ==== - -// BranchesConfig branches preferences. -type BranchesConfig struct { - Prefix string `yaml:"prefix"` - Suffix string `yaml:"suffix"` - DisableIssue bool `yaml:"disable-issue"` - Skip []string `yaml:"skip,flow"` - SkipDetached *bool `yaml:"skip-detached"` -} - -// ==== Versioning ==== - -// VersioningConfig versioning preferences. -type VersioningConfig struct { - UpdateMajor []string `yaml:"update-major,flow"` - UpdateMinor []string `yaml:"update-minor,flow"` - UpdatePatch []string `yaml:"update-patch,flow"` - IgnoreUnknown bool `yaml:"ignore-unknown"` -} - -// ==== Tag ==== - -// TagConfig tag preferences. -type TagConfig struct { - Pattern *string `yaml:"pattern"` - Filter *string `yaml:"filter"` -} - -// ==== Release Notes ==== - -// ReleaseNotesConfig release notes preferences. -type ReleaseNotesConfig struct { - Headers map[string]string `yaml:"headers,omitempty"` - Sections []ReleaseNotesSectionConfig `yaml:"sections"` -} - -func (cfg ReleaseNotesConfig) sectionConfig(sectionType string) *ReleaseNotesSectionConfig { - for _, sectionCfg := range cfg.Sections { - if sectionCfg.SectionType == sectionType { - return §ionCfg - } - } - - return nil -} - -// ReleaseNotesSectionConfig preferences for a single section on release notes. -type ReleaseNotesSectionConfig struct { - Name string `yaml:"name"` - SectionType string `yaml:"section-type"` - CommitTypes []string `yaml:"commit-types,flow,omitempty"` -} - -const ( - // ReleaseNotesSectionTypeCommits ReleaseNotesSectionConfig.SectionType value. - ReleaseNotesSectionTypeCommits = "commits" - // ReleaseNotesSectionTypeBreakingChanges ReleaseNotesSectionConfig.SectionType value. - ReleaseNotesSectionTypeBreakingChanges = "breaking-changes" -) diff --git a/sv/errors.go b/sv/errors.go deleted file mode 100644 index ace87e8..0000000 --- a/sv/errors.go +++ /dev/null @@ -1,11 +0,0 @@ -package sv - -import "errors" - -var ( - errUnknownGitError = errors.New("git command failed") - errInvalidCommitMessage = errors.New("commit message not valid") - errIssueIDNotFound = errors.New("could not find issue id using configured regex") - errInvalidIssueRegex = errors.New("could not compile issue regex") - errInvalidHeaderRegex = errors.New("invalid regex on header-selector") -) diff --git a/sv/formatter.go b/sv/formatter/formatter.go similarity index 64% rename from sv/formatter.go rename to sv/formatter/formatter.go index 2f97e2f..dddc97e 100644 --- a/sv/formatter.go +++ b/sv/formatter/formatter.go @@ -1,14 +1,13 @@ -package sv +package formatter import ( "bytes" - "io/fs" - "os" "sort" "text/template" "time" "github.com/Masterminds/semver/v3" + "github.com/thegeeklab/git-sv/v2/sv" ) type releaseNoteTemplateVariables struct { @@ -16,35 +15,28 @@ type releaseNoteTemplateVariables struct { Tag string Version *semver.Version Date time.Time - Sections []ReleaseNoteSection + Sections []sv.ReleaseNoteSection AuthorNames []string } // OutputFormatter output formatter interface. type OutputFormatter interface { - FormatReleaseNote(releasenote ReleaseNote) (string, error) - FormatChangelog(releasenotes []ReleaseNote) (string, error) + FormatReleaseNote(releasenote sv.ReleaseNote) (string, error) + FormatChangelog(releasenotes []sv.ReleaseNote) (string, error) } -// OutputFormatterImpl formater for release note and changelog. -type OutputFormatterImpl struct { +// BaseOutputFormatter formater for release note and changelog. +type BaseOutputFormatter struct { templates *template.Template } // NewOutputFormatter TemplateProcessor constructor. -func NewOutputFormatter(templatesFS fs.FS) *OutputFormatterImpl { - templateFNs := map[string]interface{}{ - "timefmt": timeFormat, - "getsection": getSection, - "getenv": os.Getenv, - } - tpls := template.Must(template.New("templates").Funcs(templateFNs).ParseFS(templatesFS, "*")) - - return &OutputFormatterImpl{templates: tpls} +func NewOutputFormatter(tpls *template.Template) *BaseOutputFormatter { + return &BaseOutputFormatter{templates: tpls} } // FormatReleaseNote format a release note. -func (p OutputFormatterImpl) FormatReleaseNote(releasenote ReleaseNote) (string, error) { +func (p BaseOutputFormatter) FormatReleaseNote(releasenote sv.ReleaseNote) (string, error) { var b bytes.Buffer if err := p.templates.ExecuteTemplate(&b, "releasenotes-md.tpl", releaseNoteVariables(releasenote)); err != nil { return "", err @@ -54,7 +46,7 @@ func (p OutputFormatterImpl) FormatReleaseNote(releasenote ReleaseNote) (string, } // FormatChangelog format a changelog. -func (p OutputFormatterImpl) FormatChangelog(releasenotes []ReleaseNote) (string, error) { +func (p BaseOutputFormatter) FormatChangelog(releasenotes []sv.ReleaseNote) (string, error) { templateVars := make([]releaseNoteTemplateVariables, len(releasenotes)) for i, v := range releasenotes { templateVars[i] = releaseNoteVariables(v) @@ -68,7 +60,7 @@ func (p OutputFormatterImpl) FormatChangelog(releasenotes []ReleaseNote) (string return b.String(), nil } -func releaseNoteVariables(releasenote ReleaseNote) releaseNoteTemplateVariables { +func releaseNoteVariables(releasenote sv.ReleaseNote) releaseNoteTemplateVariables { release := releasenote.Tag if releasenote.Version != nil { release = "v" + releasenote.Version.String() diff --git a/sv/formatter_test.go b/sv/formatter/formatter_test.go similarity index 60% rename from sv/formatter_test.go rename to sv/formatter/formatter_test.go index 9f74bca..175313b 100644 --- a/sv/formatter_test.go +++ b/sv/formatter/formatter_test.go @@ -1,15 +1,16 @@ -package sv +package formatter import ( "bytes" - "os" "testing" "time" "github.com/Masterminds/semver/v3" + "github.com/thegeeklab/git-sv/v2/sv" + "github.com/thegeeklab/git-sv/v2/templates" ) -var templatesFS = os.DirFS("../cmd/git-sv/resources/templates") +var tmpls = templates.New("") var dateChangelog = `## v1.0.0 (2020-05-01)` @@ -37,12 +38,12 @@ var fullChangeLog = `## v1.0.0 (2020-05-01) - break change message` -func TestOutputFormatterImpl_FormatReleaseNote(t *testing.T) { +func TestBaseOutputFormatter_FormatReleaseNote(t *testing.T) { date, _ := time.Parse("2006-01-02", "2020-05-01") tests := []struct { name string - input ReleaseNote + input sv.ReleaseNote want string wantErr bool }{ @@ -54,54 +55,54 @@ func TestOutputFormatterImpl_FormatReleaseNote(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := NewOutputFormatter(templatesFS).FormatReleaseNote(tt.input) + got, err := NewOutputFormatter(tmpls).FormatReleaseNote(tt.input) if got != tt.want { - t.Errorf("OutputFormatterImpl.FormatReleaseNote() = %v, want %v", got, tt.want) + t.Errorf("BaseOutputFormatter.FormatReleaseNote() = %v, want %v", got, tt.want) } if (err != nil) != tt.wantErr { - t.Errorf("OutputFormatterImpl.FormatReleaseNote() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("BaseOutputFormatter.FormatReleaseNote() error = %v, wantErr %v", err, tt.wantErr) } }) } } -func emptyReleaseNote(tag string, date time.Time) ReleaseNote { +func emptyReleaseNote(tag string, date time.Time) sv.ReleaseNote { v, _ := semver.NewVersion(tag) - return ReleaseNote{ + return sv.ReleaseNote{ Version: v, Tag: tag, Date: date, } } -func fullReleaseNote(tag string, date time.Time) ReleaseNote { +func fullReleaseNote(tag string, date time.Time) sv.ReleaseNote { v, _ := semver.NewVersion(tag) - sections := []ReleaseNoteSection{ - newReleaseNoteCommitsSection( + sections := []sv.ReleaseNoteSection{ + sv.TestNewReleaseNoteCommitsSection( "Features", []string{"feat"}, - []GitCommitLog{commitlog("feat", map[string]string{}, "a")}, + []sv.CommitLog{sv.TestCommitlog("feat", map[string]string{}, "a")}, ), - newReleaseNoteCommitsSection( + sv.TestNewReleaseNoteCommitsSection( "Bug Fixes", []string{"fix"}, - []GitCommitLog{commitlog("fix", map[string]string{}, "a")}, + []sv.CommitLog{sv.TestCommitlog("fix", map[string]string{}, "a")}, ), - newReleaseNoteCommitsSection( + sv.TestNewReleaseNoteCommitsSection( "Build", []string{"build"}, - []GitCommitLog{commitlog("build", map[string]string{}, "a")}, + []sv.CommitLog{sv.TestCommitlog("build", map[string]string{}, "a")}, ), - ReleaseNoteBreakingChangeSection{"Breaking Changes", []string{"break change message"}}, + sv.ReleaseNoteBreakingChangeSection{Name: "Breaking Changes", Messages: []string{"break change message"}}, } - return releaseNote(v, tag, date, sections, map[string]struct{}{"a": {}}) + return sv.TestReleaseNote(v, tag, date, sections, map[string]struct{}{"a": {}}) } func Test_checkTemplatesExecution(t *testing.T) { - tpls := NewOutputFormatter(templatesFS).templates + tpls := NewOutputFormatter(tmpls).templates tests := []struct { template string variables interface{} @@ -131,20 +132,20 @@ func releaseNotesVariables(release string) releaseNoteTemplateVariables { return releaseNoteTemplateVariables{ Release: release, Date: time.Date(2006, 1, 0o2, 0, 0, 0, 0, time.UTC), - Sections: []ReleaseNoteSection{ - newReleaseNoteCommitsSection("Features", + Sections: []sv.ReleaseNoteSection{ + sv.TestNewReleaseNoteCommitsSection("Features", []string{"feat"}, - []GitCommitLog{commitlog("feat", map[string]string{}, "a")}, + []sv.CommitLog{sv.TestCommitlog("feat", map[string]string{}, "a")}, ), - newReleaseNoteCommitsSection("Bug Fixes", + sv.TestNewReleaseNoteCommitsSection("Bug Fixes", []string{"fix"}, - []GitCommitLog{commitlog("fix", map[string]string{}, "a")}, + []sv.CommitLog{sv.TestCommitlog("fix", map[string]string{}, "a")}, ), - newReleaseNoteCommitsSection("Build", + sv.TestNewReleaseNoteCommitsSection("Build", []string{"build"}, - []GitCommitLog{commitlog("build", map[string]string{}, "a")}, + []sv.CommitLog{sv.TestCommitlog("build", map[string]string{}, "a")}, ), - ReleaseNoteBreakingChangeSection{"Breaking Changes", []string{"break change message"}}, + sv.ReleaseNoteBreakingChangeSection{Name: "Breaking Changes", Messages: []string{"break change message"}}, }, } } diff --git a/sv/formatter_functions.go b/sv/formatter_functions.go deleted file mode 100644 index 284090e..0000000 --- a/sv/formatter_functions.go +++ /dev/null @@ -1,21 +0,0 @@ -package sv - -import "time" - -func timeFormat(t time.Time, format string) string { - if t.IsZero() { - return "" - } - - return t.Format(format) -} - -func getSection(sections []ReleaseNoteSection, name string) ReleaseNoteSection { //nolint:ireturn - for _, section := range sections { - if section.SectionName() == name { - return section - } - } - - return nil -} diff --git a/sv/formatter_functions_test.go b/sv/formatter_functions_test.go deleted file mode 100644 index a9d63cc..0000000 --- a/sv/formatter_functions_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package sv - -import ( - "reflect" - "testing" - "time" -) - -func Test_timeFormat(t *testing.T) { - tests := []struct { - name string - time time.Time - format string - want string - }{ - {"valid time", time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC), "2006-01-02", "2022-01-01"}, - {"empty time", time.Time{}, "2006-01-02", ""}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := timeFormat(tt.time, tt.format); got != tt.want { - t.Errorf("timeFormat() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_getSection(t *testing.T) { - tests := []struct { - name string - sections []ReleaseNoteSection - sectionName string - want ReleaseNoteSection - }{ - { - "existing section", []ReleaseNoteSection{ - ReleaseNoteCommitsSection{Name: "section 0"}, - ReleaseNoteCommitsSection{Name: "section 1"}, - ReleaseNoteCommitsSection{Name: "section 2"}, - }, "section 1", ReleaseNoteCommitsSection{Name: "section 1"}, - }, - { - "nonexisting section", []ReleaseNoteSection{ - ReleaseNoteCommitsSection{Name: "section 0"}, - ReleaseNoteCommitsSection{Name: "section 1"}, - ReleaseNoteCommitsSection{Name: "section 2"}, - }, "section 10", nil, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := getSection(tt.sections, tt.sectionName); !reflect.DeepEqual(got, tt.want) { - t.Errorf("getSection() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/sv/message.go b/sv/message.go index c172540..c3bf4ef 100644 --- a/sv/message.go +++ b/sv/message.go @@ -2,16 +2,24 @@ package sv import ( "bufio" + "errors" "fmt" "regexp" "strings" ) const ( - breakingChangeFooterKey = "BREAKING CHANGE" - breakingChangeMetadataKey = "breaking-change" - issueMetadataKey = "issue" - messageRegexGroupName = "header" + BreakingChangeFooterKey = "BREAKING CHANGE" + BreakingChangeMetadataKey = "breaking-change" + IssueMetadataKey = "issue" + MessageRegexGroupName = "header" +) + +var ( + errInvalidCommitMessage = errors.New("commit message not valid") + errIssueIDNotFound = errors.New("could not find issue id using configured regex") + errInvalidIssueRegex = errors.New("could not compile issue regex") + errInvalidHeaderRegex = errors.New("invalid regex on header-selector") ) // CommitMessage is a message using conventional commits. @@ -24,15 +32,59 @@ type CommitMessage struct { Metadata map[string]string `json:"metadata,omitempty"` } +type CommitMessageConfig struct { + Types []string `yaml:"types,flow"` + HeaderSelector string `yaml:"header-selector"` + Scope CommitMessageScopeConfig `yaml:"scope"` + Footer map[string]CommitMessageFooterConfig `yaml:"footer"` + Issue CommitMessageIssueConfig `yaml:"issue"` +} + +// IssueFooterConfig config for issue. +func (c CommitMessageConfig) IssueFooterConfig() CommitMessageFooterConfig { + if v, exists := c.Footer[IssueMetadataKey]; exists { + return v + } + + return CommitMessageFooterConfig{} +} + +// CommitMessageScopeConfig config scope preferences. +type CommitMessageScopeConfig struct { + Values []string `yaml:"values"` +} + +// CommitMessageFooterConfig config footer metadata. +type CommitMessageFooterConfig struct { + Key string `yaml:"key"` + KeySynonyms []string `yaml:"key-synonyms,flow"` + UseHash bool `yaml:"use-hash"` + AddValuePrefix string `yaml:"add-value-prefix"` +} + +// CommitMessageIssueConfig issue preferences. +type CommitMessageIssueConfig struct { + Regex string `yaml:"regex"` +} + +// BranchesConfig branches preferences. +type BranchesConfig struct { + Prefix string `yaml:"prefix"` + Suffix string `yaml:"suffix"` + DisableIssue bool `yaml:"disable-issue"` + Skip []string `yaml:"skip,flow"` + SkipDetached *bool `yaml:"skip-detached"` +} + // NewCommitMessage commit message constructor. func NewCommitMessage(ctype, scope, description, body, issue, breakingChanges string) CommitMessage { metadata := make(map[string]string) if issue != "" { - metadata[issueMetadataKey] = issue + metadata[IssueMetadataKey] = issue } if breakingChanges != "" { - metadata[breakingChangeMetadataKey] = breakingChanges + metadata[BreakingChangeMetadataKey] = breakingChanges } return CommitMessage{ @@ -47,12 +99,12 @@ func NewCommitMessage(ctype, scope, description, body, issue, breakingChanges st // Issue return issue from metadata. func (m CommitMessage) Issue() string { - return m.Metadata[issueMetadataKey] + return m.Metadata[IssueMetadataKey] } // BreakingMessage return breaking change message from metadata. func (m CommitMessage) BreakingMessage() string { - return m.Metadata[breakingChangeMetadataKey] + return m.Metadata[BreakingChangeMetadataKey] } // MessageProcessor interface. @@ -68,28 +120,28 @@ type MessageProcessor interface { Parse(subject, body string) (CommitMessage, error) } -// NewMessageProcessor MessageProcessorImpl constructor. -func NewMessageProcessor(mcfg CommitMessageConfig, bcfg BranchesConfig) *MessageProcessorImpl { - return &MessageProcessorImpl{ +// NewMessageProcessor BaseMessageProcessor constructor. +func NewMessageProcessor(mcfg CommitMessageConfig, bcfg BranchesConfig) *BaseMessageProcessor { + return &BaseMessageProcessor{ messageCfg: mcfg, branchesCfg: bcfg, } } -// MessageProcessorImpl process validate message hook. -type MessageProcessorImpl struct { +// BaseMessageProcessor process validate message hook. +type BaseMessageProcessor struct { messageCfg CommitMessageConfig branchesCfg BranchesConfig } // SkipBranch check if branch should be ignored. -func (p MessageProcessorImpl) SkipBranch(branch string, detached bool) bool { +func (p BaseMessageProcessor) SkipBranch(branch string, detached bool) bool { return contains(branch, p.branchesCfg.Skip) || (p.branchesCfg.SkipDetached != nil && *p.branchesCfg.SkipDetached && detached) } // Validate commit message. -func (p MessageProcessorImpl) Validate(message string) error { +func (p BaseMessageProcessor) Validate(message string) error { subject, body := splitCommitMessageContent(message) msg, parseErr := p.Parse(subject, body) @@ -113,7 +165,7 @@ func (p MessageProcessorImpl) Validate(message string) error { } // ValidateType check if commit type is valid. -func (p MessageProcessorImpl) ValidateType(ctype string) error { +func (p BaseMessageProcessor) ValidateType(ctype string) error { if ctype == "" || !contains(ctype, p.messageCfg.Types) { return fmt.Errorf( "%w: type must be one of [%s]", @@ -126,7 +178,7 @@ func (p MessageProcessorImpl) ValidateType(ctype string) error { } // ValidateScope check if commit scope is valid. -func (p MessageProcessorImpl) ValidateScope(scope string) error { +func (p BaseMessageProcessor) ValidateScope(scope string) error { if len(p.messageCfg.Scope.Values) > 0 && !contains(scope, p.messageCfg.Scope.Values) { return fmt.Errorf( "%w: scope must one of [%s]", @@ -139,7 +191,7 @@ func (p MessageProcessorImpl) ValidateScope(scope string) error { } // ValidateDescription check if commit description is valid. -func (p MessageProcessorImpl) ValidateDescription(description string) error { +func (p BaseMessageProcessor) ValidateDescription(description string) error { if !regexp.MustCompile("^[a-z]+.*$").MatchString(description) { return fmt.Errorf("%w: description [%s] must start with lowercase", errInvalidCommitMessage, description) } @@ -148,7 +200,7 @@ func (p MessageProcessorImpl) ValidateDescription(description string) error { } // Enhance add metadata on commit message. -func (p MessageProcessorImpl) Enhance(branch, message string) (string, error) { +func (p BaseMessageProcessor) Enhance(branch, message string) (string, error) { if p.branchesCfg.DisableIssue || p.messageCfg.IssueFooterConfig().Key == "" || hasIssueID(message, p.messageCfg.IssueFooterConfig()) { return "", nil // enhance disabled @@ -184,7 +236,7 @@ func formatIssueFooter(cfg CommitMessageFooterConfig, issue string) string { } // IssueID try to extract issue id from branch, return empty if not found. -func (p MessageProcessorImpl) IssueID(branch string) (string, error) { +func (p BaseMessageProcessor) IssueID(branch string) (string, error) { if p.branchesCfg.DisableIssue || p.messageCfg.Issue.Regex == "" { return "", nil } @@ -205,7 +257,7 @@ func (p MessageProcessorImpl) IssueID(branch string) (string, error) { } // Format a commit message returning header, body and footer. -func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string) { +func (p BaseMessageProcessor) Format(msg CommitMessage) (string, string, string) { var header strings.Builder header.WriteString(msg.Type) @@ -219,10 +271,10 @@ func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string) var footer strings.Builder if msg.BreakingMessage() != "" { - footer.WriteString(fmt.Sprintf("%s: %s", breakingChangeFooterKey, msg.BreakingMessage())) + footer.WriteString(fmt.Sprintf("%s: %s", BreakingChangeFooterKey, msg.BreakingMessage())) } - if issue, exists := msg.Metadata[issueMetadataKey]; exists && p.messageCfg.IssueFooterConfig().Key != "" { + if issue, exists := msg.Metadata[IssueMetadataKey]; exists && p.messageCfg.IssueFooterConfig().Key != "" { if footer.Len() > 0 { footer.WriteString("\n") } @@ -238,7 +290,7 @@ func removeCarriage(commit string) string { } // Parse a commit message. -func (p MessageProcessorImpl) Parse(subject, body string) (CommitMessage, error) { +func (p BaseMessageProcessor) Parse(subject, body string) (CommitMessage, error) { preparedSubject, err := p.prepareHeader(subject) commitBody := removeCarriage(body) @@ -263,8 +315,8 @@ func (p MessageProcessorImpl) Parse(subject, body string) (CommitMessage, error) } } - if tagValue := extractFooterMetadata(breakingChangeFooterKey, commitBody, false); tagValue != "" { - metadata[breakingChangeMetadataKey] = tagValue + if tagValue := extractFooterMetadata(BreakingChangeFooterKey, commitBody, false); tagValue != "" { + metadata[BreakingChangeMetadataKey] = tagValue hasBreakingChange = true } @@ -278,7 +330,7 @@ func (p MessageProcessorImpl) Parse(subject, body string) (CommitMessage, error) }, nil } -func (p MessageProcessorImpl) prepareHeader(header string) (string, error) { +func (p BaseMessageProcessor) prepareHeader(header string) (string, error) { if p.messageCfg.HeaderSelector == "" { return header, nil } @@ -288,9 +340,9 @@ func (p MessageProcessorImpl) prepareHeader(header string) (string, error) { return "", fmt.Errorf("%w: %s: %s", errInvalidHeaderRegex, p.messageCfg.HeaderSelector, err.Error()) } - index := regex.SubexpIndex(messageRegexGroupName) + index := regex.SubexpIndex(MessageRegexGroupName) if index < 0 { - return "", fmt.Errorf("%w: could not find group %s", errInvalidHeaderRegex, messageRegexGroupName) + return "", fmt.Errorf("%w: could not find group %s", errInvalidHeaderRegex, MessageRegexGroupName) } match := regex.FindStringSubmatch(header) @@ -299,7 +351,7 @@ func (p MessageProcessorImpl) prepareHeader(header string) (string, error) { return "", fmt.Errorf( "%w: could not find group %s in match result for '%s'", errInvalidHeaderRegex, - messageRegexGroupName, + MessageRegexGroupName, header, ) } @@ -335,7 +387,7 @@ func extractFooterMetadata(key, text string, useHash bool) string { } func hasFooter(message string) bool { - r := regexp.MustCompile("^[a-zA-Z-]+: .*|^[a-zA-Z-]+ #.*|^" + breakingChangeFooterKey + ": .*") + r := regexp.MustCompile("^[a-zA-Z-]+: .*|^[a-zA-Z-]+ #.*|^" + BreakingChangeFooterKey + ": .*") scanner := bufio.NewScanner(strings.NewReader(message)) lines := 0 diff --git a/sv/message_test.go b/sv/message_test.go index 49b01dc..4ae0c36 100644 --- a/sv/message_test.go +++ b/sv/message_test.go @@ -115,7 +115,7 @@ BREAKING CHANGE: refactor to use JavaScript features not available in Node 6.` // multiline samples end -func TestMessageProcessorImpl_SkipBranch(t *testing.T) { +func TestBaseMessageProcessor_SkipBranch(t *testing.T) { tests := []struct { name string bcfg BranchesConfig @@ -133,13 +133,13 @@ func TestMessageProcessorImpl_SkipBranch(t *testing.T) { 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) + t.Errorf("BaseMessageProcessor.SkipBranch() = %v, want %v", got, tt.want) } }) } } -func TestMessageProcessorImpl_Validate(t *testing.T) { +func TestBaseMessageProcessor_Validate(t *testing.T) { tests := []struct { name string cfg CommitMessageConfig @@ -200,13 +200,13 @@ func TestMessageProcessorImpl_Validate(t *testing.T) { t.Run(tt.name, func(t *testing.T) { 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) + t.Errorf("BaseMessageProcessor.Validate() error = %v, wantErr %v", err, tt.wantErr) } }) } } -func TestMessageProcessorImpl_ValidateType(t *testing.T) { +func TestBaseMessageProcessor_ValidateType(t *testing.T) { tests := []struct { name string cfg CommitMessageConfig @@ -233,13 +233,13 @@ func TestMessageProcessorImpl_ValidateType(t *testing.T) { t.Run(tt.name, func(t *testing.T) { p := NewMessageProcessor(tt.cfg, newBranchCfg(false)) if err := p.ValidateType(tt.ctype); (err != nil) != tt.wantErr { - t.Errorf("MessageProcessorImpl.ValidateType() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("BaseMessageProcessor.ValidateType() error = %v, wantErr %v", err, tt.wantErr) } }) } } -func TestMessageProcessorImpl_ValidateScope(t *testing.T) { +func TestBaseMessageProcessor_ValidateScope(t *testing.T) { tests := []struct { name string cfg CommitMessageConfig @@ -258,13 +258,13 @@ func TestMessageProcessorImpl_ValidateScope(t *testing.T) { t.Run(tt.name, func(t *testing.T) { p := NewMessageProcessor(tt.cfg, newBranchCfg(false)) if err := p.ValidateScope(tt.scope); (err != nil) != tt.wantErr { - t.Errorf("MessageProcessorImpl.ValidateScope() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("BaseMessageProcessor.ValidateScope() error = %v, wantErr %v", err, tt.wantErr) } }) } } -func TestMessageProcessorImpl_ValidateDescription(t *testing.T) { +func TestBaseMessageProcessor_ValidateDescription(t *testing.T) { tests := []struct { name string cfg CommitMessageConfig @@ -301,13 +301,13 @@ func TestMessageProcessorImpl_ValidateDescription(t *testing.T) { t.Run(tt.name, func(t *testing.T) { p := NewMessageProcessor(tt.cfg, newBranchCfg(false)) if err := p.ValidateDescription(tt.description); (err != nil) != tt.wantErr { - t.Errorf("MessageProcessorImpl.ValidateDescription() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("BaseMessageProcessor.ValidateDescription() error = %v, wantErr %v", err, tt.wantErr) } }) } } -func TestMessageProcessorImpl_Enhance(t *testing.T) { +func TestBaseMessageProcessor_Enhance(t *testing.T) { tests := []struct { name string cfg CommitMessageConfig @@ -381,18 +381,18 @@ func TestMessageProcessorImpl_Enhance(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := NewMessageProcessor(tt.cfg, newBranchCfg(false)).Enhance(tt.branch, tt.message) if (err != nil) != tt.wantErr { - t.Errorf("MessageProcessorImpl.Enhance() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("BaseMessageProcessor.Enhance() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { - t.Errorf("MessageProcessorImpl.Enhance() = %v, want %v", got, tt.want) + t.Errorf("BaseMessageProcessor.Enhance() = %v, want %v", got, tt.want) } }) } } -func TestMessageProcessorImpl_IssueID(t *testing.T) { +func TestBaseMessageProcessor_IssueID(t *testing.T) { p := NewMessageProcessor(ccfg, newBranchCfg(false)) tests := []struct { @@ -412,12 +412,12 @@ func TestMessageProcessorImpl_IssueID(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := p.IssueID(tt.branch) if (err != nil) != tt.wantErr { - t.Errorf("MessageProcessorImpl.IssueID() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("BaseMessageProcessor.IssueID() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { - t.Errorf("MessageProcessorImpl.IssueID() = %v, want %v", got, tt.want) + t.Errorf("BaseMessageProcessor.IssueID() = %v, want %v", got, tt.want) } }) } @@ -514,7 +514,7 @@ var hashMetadataBody = `some descriptions Jira: JIRA-999 Refs #123` -func TestMessageProcessorImpl_Parse(t *testing.T) { +func TestBaseMessageProcessor_Parse(t *testing.T) { tests := []struct { name string cfg CommitMessageConfig @@ -572,8 +572,8 @@ func TestMessageProcessorImpl_Parse(t *testing.T) { Body: completeBody, IsBreakingChange: true, Metadata: map[string]string{ - issueMetadataKey: "JIRA-123", - breakingChangeMetadataKey: "this change breaks everything", + IssueMetadataKey: "JIRA-123", + BreakingChangeMetadataKey: "this change breaks everything", }, }, }, @@ -587,7 +587,7 @@ func TestMessageProcessorImpl_Parse(t *testing.T) { Description: "something new", Body: issueOnlyBody, IsBreakingChange: false, - Metadata: map[string]string{issueMetadataKey: "JIRA-456"}, + Metadata: map[string]string{IssueMetadataKey: "JIRA-456"}, }, }, { @@ -600,7 +600,7 @@ func TestMessageProcessorImpl_Parse(t *testing.T) { Description: "something new", Body: issueSynonymsBody, IsBreakingChange: false, - Metadata: map[string]string{issueMetadataKey: "JIRA-789"}, + Metadata: map[string]string{IssueMetadataKey: "JIRA-789"}, }, }, { @@ -626,7 +626,7 @@ func TestMessageProcessorImpl_Parse(t *testing.T) { Description: "something new", Body: hashMetadataBody, IsBreakingChange: false, - Metadata: map[string]string{issueMetadataKey: "JIRA-999", "refs": "#123"}, + Metadata: map[string]string{IssueMetadataKey: "JIRA-999", "refs": "#123"}, }, }, { @@ -652,7 +652,7 @@ func TestMessageProcessorImpl_Parse(t *testing.T) { Description: "something new", Body: expectedBodyWithCarriage, IsBreakingChange: false, - Metadata: map[string]string{issueMetadataKey: "JIRA-123"}, + Metadata: map[string]string{IssueMetadataKey: "JIRA-123"}, }, }, } @@ -661,13 +661,13 @@ func TestMessageProcessorImpl_Parse(t *testing.T) { if got, err := NewMessageProcessor( tt.cfg, newBranchCfg(false), ).Parse(tt.subject, tt.body); !reflect.DeepEqual(got, tt.want) && err == nil { - t.Errorf("MessageProcessorImpl.Parse() = [%+v], want [%+v]", got, tt.want) + t.Errorf("BaseMessageProcessor.Parse() = [%+v], want [%+v]", got, tt.want) } }) } } -func TestMessageProcessorImpl_Format(t *testing.T) { +func TestBaseMessageProcessor_Format(t *testing.T) { tests := []struct { name string cfg CommitMessageConfig @@ -777,13 +777,13 @@ func TestMessageProcessorImpl_Format(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, got1, got2 := NewMessageProcessor(tt.cfg, newBranchCfg(false)).Format(tt.msg) if got != tt.wantHeader { - t.Errorf("MessageProcessorImpl.Format() header got = %v, want %v", got, tt.wantHeader) + t.Errorf("BaseMessageProcessor.Format() header got = %v, want %v", got, tt.wantHeader) } if got1 != tt.wantBody { - t.Errorf("MessageProcessorImpl.Format() body got = %v, want %v", got1, tt.wantBody) + t.Errorf("BaseMessageProcessor.Format() body got = %v, want %v", got1, tt.wantBody) } if got2 != tt.wantFooter { - t.Errorf("MessageProcessorImpl.Format() footer got = %v, want %v", got2, tt.wantFooter) + t.Errorf("BaseMessageProcessor.Format() footer got = %v, want %v", got2, tt.wantFooter) } }) } diff --git a/sv/releasenotes.go b/sv/releasenotes.go index c07dfc2..62fd53d 100644 --- a/sv/releasenotes.go +++ b/sv/releasenotes.go @@ -6,27 +6,57 @@ import ( "github.com/Masterminds/semver/v3" ) +// ReleaseNotesConfig release notes preferences. +type ReleaseNotesConfig struct { + Headers map[string]string `yaml:"headers,omitempty"` + Sections []ReleaseNotesSectionConfig `yaml:"sections"` +} + +func (cfg ReleaseNotesConfig) sectionConfig(sectionType string) *ReleaseNotesSectionConfig { + for _, sectionCfg := range cfg.Sections { + if sectionCfg.SectionType == sectionType { + return §ionCfg + } + } + + return nil +} + +// ReleaseNotesSectionConfig preferences for a single section on release notes. +type ReleaseNotesSectionConfig struct { + Name string `yaml:"name"` + SectionType string `yaml:"section-type"` + CommitTypes []string `yaml:"commit-types,flow,omitempty"` +} + +const ( + // ReleaseNotesSectionTypeCommits ReleaseNotesSectionConfig.SectionType value. + ReleaseNotesSectionTypeCommits = "commits" + // ReleaseNotesSectionTypeBreakingChanges ReleaseNotesSectionConfig.SectionType value. + ReleaseNotesSectionTypeBreakingChanges = "breaking-changes" +) + // ReleaseNoteProcessor release note processor interface. type ReleaseNoteProcessor interface { - Create(version *semver.Version, tag string, date time.Time, commits []GitCommitLog) ReleaseNote + Create(version *semver.Version, tag string, date time.Time, commits []CommitLog) ReleaseNote } -// ReleaseNoteProcessorImpl release note based on commit log. -type ReleaseNoteProcessorImpl struct { +// BaseReleaseNoteProcessor release note based on commit log. +type BaseReleaseNoteProcessor struct { cfg ReleaseNotesConfig } // NewReleaseNoteProcessor ReleaseNoteProcessor constructor. -func NewReleaseNoteProcessor(cfg ReleaseNotesConfig) *ReleaseNoteProcessorImpl { - return &ReleaseNoteProcessorImpl{cfg: cfg} +func NewReleaseNoteProcessor(cfg ReleaseNotesConfig) *BaseReleaseNoteProcessor { + return &BaseReleaseNoteProcessor{cfg: cfg} } // Create create a release note based on commits. -func (p ReleaseNoteProcessorImpl) Create( +func (p BaseReleaseNoteProcessor) Create( version *semver.Version, tag string, date time.Time, - commits []GitCommitLog, + commits []CommitLog, ) ReleaseNote { mapping := commitSectionMapping(p.cfg.Sections) @@ -68,7 +98,7 @@ func (p ReleaseNoteProcessorImpl) Create( } } -func (p ReleaseNoteProcessorImpl) toReleaseNoteSections( +func (p BaseReleaseNoteProcessor) toReleaseNoteSections( commitSections map[string]ReleaseNoteCommitsSection, breakingChange ReleaseNoteBreakingChangeSection, ) []ReleaseNoteSection { @@ -144,7 +174,7 @@ func (s ReleaseNoteBreakingChangeSection) SectionName() string { type ReleaseNoteCommitsSection struct { Name string Types []string - Items []GitCommitLog + Items []CommitLog } // SectionType section type. diff --git a/sv/releasenotes_test.go b/sv/releasenotes_test.go index 5614992..05e1cc1 100644 --- a/sv/releasenotes_test.go +++ b/sv/releasenotes_test.go @@ -8,7 +8,7 @@ import ( "github.com/Masterminds/semver/v3" ) -func TestReleaseNoteProcessorImpl_Create(t *testing.T) { +func TestBaseReleaseNoteProcessor_Create(t *testing.T) { date := time.Now() tests := []struct { @@ -16,7 +16,7 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) { version *semver.Version tag string date time.Time - commits []GitCommitLog + commits []CommitLog want ReleaseNote }{ { @@ -24,13 +24,15 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) { version: semver.MustParse("1.0.0"), tag: "v1.0.0", date: date, - commits: []GitCommitLog{commitlog("t1", map[string]string{}, "a")}, - want: releaseNote( + commits: []CommitLog{TestCommitlog("t1", map[string]string{}, "a")}, + want: TestReleaseNote( semver.MustParse("1.0.0"), "v1.0.0", date, []ReleaseNoteSection{ - newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")}), + TestNewReleaseNoteCommitsSection( + "Tag 1", []string{"t1"}, []CommitLog{TestCommitlog("t1", map[string]string{}, "a")}, + ), }, map[string]struct{}{"a": {}}, ), @@ -40,13 +42,17 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) { version: semver.MustParse("1.0.0"), tag: "v1.0.0", date: date, - commits: []GitCommitLog{commitlog("t1", map[string]string{}, "a"), commitlog("unmapped", map[string]string{}, "a")}, - want: releaseNote( + commits: []CommitLog{ + TestCommitlog("t1", map[string]string{}, "a"), TestCommitlog("unmapped", map[string]string{}, "a"), + }, + want: TestReleaseNote( semver.MustParse("1.0.0"), "v1.0.0", date, []ReleaseNoteSection{ - newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")}), + TestNewReleaseNoteCommitsSection( + "Tag 1", []string{"t1"}, []CommitLog{TestCommitlog("t1", map[string]string{}, "a")}, + ), }, map[string]struct{}{"a": {}}, ), @@ -56,16 +62,18 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) { version: semver.MustParse("1.0.0"), tag: "v1.0.0", date: date, - commits: []GitCommitLog{ - commitlog("t1", map[string]string{}, "a"), - commitlog("unmapped", map[string]string{"breaking-change": "breaks"}, "a"), + commits: []CommitLog{ + TestCommitlog("t1", map[string]string{}, "a"), + TestCommitlog("unmapped", map[string]string{"breaking-change": "breaks"}, "a"), }, - want: releaseNote( + want: TestReleaseNote( semver.MustParse("1.0.0"), "v1.0.0", date, []ReleaseNoteSection{ - newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")}), + TestNewReleaseNoteCommitsSection( + "Tag 1", []string{"t1"}, []CommitLog{TestCommitlog("t1", map[string]string{}, "a")}, + ), ReleaseNoteBreakingChangeSection{Name: "Breaking Changes", Messages: []string{"breaks"}}, }, map[string]struct{}{"a": {}}, @@ -76,20 +84,20 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) { version: semver.MustParse("1.0.0"), tag: "v1.0.0", date: date, - commits: []GitCommitLog{ - commitlog("t1", map[string]string{}, "author3"), - commitlog("t1", map[string]string{}, "author2"), - commitlog("t1", map[string]string{}, "author1"), + commits: []CommitLog{ + TestCommitlog("t1", map[string]string{}, "author3"), + TestCommitlog("t1", map[string]string{}, "author2"), + TestCommitlog("t1", map[string]string{}, "author1"), }, - want: releaseNote( + want: TestReleaseNote( semver.MustParse("1.0.0"), "v1.0.0", date, []ReleaseNoteSection{ - newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{ - commitlog("t1", map[string]string{}, "author3"), - commitlog("t1", map[string]string{}, "author2"), - commitlog("t1", map[string]string{}, "author1"), + TestNewReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []CommitLog{ + TestCommitlog("t1", map[string]string{}, "author3"), + TestCommitlog("t1", map[string]string{}, "author2"), + TestCommitlog("t1", map[string]string{}, "author1"), }), }, map[string]struct{}{"author1": {}, "author2": {}, "author3": {}}, @@ -107,7 +115,7 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) { }, }) if got := p.Create(tt.version, tt.tag, tt.date, tt.commits); !reflect.DeepEqual(got, tt.want) { - t.Errorf("ReleaseNoteProcessorImpl.Create() = %v, want %v", got, tt.want) + t.Errorf("BaseReleaseNoteProcessor.Create() = %v, want %v", got, tt.want) } }) } diff --git a/sv/helpers_test.go b/sv/testutils.go similarity index 67% rename from sv/helpers_test.go rename to sv/testutils.go index ab8baef..0f66859 100644 --- a/sv/helpers_test.go +++ b/sv/testutils.go @@ -6,19 +6,19 @@ import ( "github.com/Masterminds/semver/v3" ) -func version(v string) *semver.Version { +func TestVersion(v string) *semver.Version { r, _ := semver.NewVersion(v) return r } -func commitlog(ctype string, metadata map[string]string, author string) GitCommitLog { +func TestCommitlog(ctype string, metadata map[string]string, author string) CommitLog { breaking := false - if _, found := metadata[breakingChangeMetadataKey]; found { + if _, found := metadata[BreakingChangeMetadataKey]; found { breaking = true } - return GitCommitLog{ + return CommitLog{ Message: CommitMessage{ Type: ctype, Description: "subject text", @@ -29,7 +29,7 @@ func commitlog(ctype string, metadata map[string]string, author string) GitCommi } } -func releaseNote( +func TestReleaseNote( version *semver.Version, tag string, date time.Time, @@ -45,7 +45,7 @@ func releaseNote( } } -func newReleaseNoteCommitsSection(name string, types []string, items []GitCommitLog) ReleaseNoteCommitsSection { +func TestNewReleaseNoteCommitsSection(name string, types []string, items []CommitLog) ReleaseNoteCommitsSection { return ReleaseNoteCommitsSection{ Name: name, Types: types, diff --git a/cmd/git-sv/resources/templates/changelog-md.tpl b/templates/assets/changelog-md.tpl similarity index 100% rename from cmd/git-sv/resources/templates/changelog-md.tpl rename to templates/assets/changelog-md.tpl diff --git a/cmd/git-sv/resources/templates/releasenotes-md.tpl b/templates/assets/releasenotes-md.tpl similarity index 75% rename from cmd/git-sv/resources/templates/releasenotes-md.tpl rename to templates/assets/releasenotes-md.tpl index 9a06cac..6e6e0bd 100644 --- a/cmd/git-sv/resources/templates/releasenotes-md.tpl +++ b/templates/assets/releasenotes-md.tpl @@ -1,4 +1,4 @@ -## {{ if .Release }}{{ .Release }}{{ end }}{{ if and (not .Date.IsZero) .Release }} ({{ end }}{{ timefmt .Date "2006-01-02" }}{{ if and (not .Date.IsZero) .Release }}){{ end }} +## {{ if .Release }}{{ .Release }}{{ end }}{{ if and (not .Date.IsZero) .Release }} ({{ end }}{{ .Date | date "2006-01-02" }}{{ if and (not .Date.IsZero) .Release }}){{ end }} {{- range $section := .Sections }} {{- if (eq $section.SectionType "commits") }} {{- template "rn-md-section-commits.tpl" $section }} diff --git a/cmd/git-sv/resources/templates/rn-md-section-breaking-changes.tpl b/templates/assets/rn-md-section-breaking-changes.tpl similarity index 100% rename from cmd/git-sv/resources/templates/rn-md-section-breaking-changes.tpl rename to templates/assets/rn-md-section-breaking-changes.tpl diff --git a/cmd/git-sv/resources/templates/rn-md-section-commits.tpl b/templates/assets/rn-md-section-commits.tpl similarity index 100% rename from cmd/git-sv/resources/templates/rn-md-section-commits.tpl rename to templates/assets/rn-md-section-commits.tpl diff --git a/templates/templates.go b/templates/templates.go new file mode 100644 index 0000000..262e8a3 --- /dev/null +++ b/templates/templates.go @@ -0,0 +1,78 @@ +package templates + +import ( + "embed" + "os" + "path/filepath" + "text/template" + "time" + + "github.com/Masterminds/sprig/v3" + "github.com/rs/zerolog/log" + "github.com/thegeeklab/git-sv/v2/sv" +) + +//go:embed assets +var templateFs embed.FS + +// New loads the template to make it parseable. +func New(configDir string) *template.Template { + workDir, err := os.Getwd() + if err != nil { + log.Fatal().Err(err).Msg("error while retrieving working directory") + } + + tplsDir := filepath.Join(workDir, configDir, "templates") + + tpls, err := template.New("templates").Funcs(Funcs()).ParseFS(templateFs, "**/*.tpl") + if err != nil { + log.Warn(). + Err(err). + Msg("Failed to parse builtin templates") + } + + custom, _ := filepath.Glob(filepath.Join(tplsDir, "*.tpl")) + if len(custom) == 0 { + return tpls + } + + for _, v := range custom { + tpls, err = template.New("templates").Funcs(Funcs()).ParseFiles(v) + if err != nil { + log.Warn(). + Err(err). + Str("filename", v). + Msg("Failed to parse custom template") + } + } + + return tpls +} + +// Funcs provides some general usefule template helpers. +func Funcs() template.FuncMap { + functs := sprig.FuncMap() + + functs["date"] = zeroDate + // functs["getsection"] = getSection + + return functs +} + +func zeroDate(fmt string, date time.Time) string { + if date.IsZero() { + return "" + } + + return date.Format(fmt) +} + +func getSection(name string, sections []sv.ReleaseNoteSection) sv.ReleaseNoteSection { //nolint:ireturn + for _, section := range sections { + if section.SectionName() == name { + return section + } + } + + return nil +} diff --git a/templates/templates_test.go b/templates/templates_test.go new file mode 100644 index 0000000..434bf53 --- /dev/null +++ b/templates/templates_test.go @@ -0,0 +1,80 @@ +package templates + +import ( + "reflect" + "testing" + "time" + + "github.com/thegeeklab/git-sv/v2/sv" +) + +func Test_checkTemplatesFiles(t *testing.T) { + tests := []string{ + "assets/changelog-md.tpl", + "assets/releasenotes-md.tpl", + } + for _, tt := range tests { + t.Run(tt, func(t *testing.T) { + got, err := templateFs.ReadFile(tt) + if err != nil { + t.Errorf("missing template error = %v", err) + + return + } + + if len(got) == 0 { + t.Errorf("empty template") + } + }) + } +} + +func Test_timeFormat(t *testing.T) { + tests := []struct { + name string + time time.Time + format string + want string + }{ + {"valid time", time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC), "2006-01-02", "2022-01-01"}, + {"empty time", time.Time{}, "2006-01-02", ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := zeroDate(tt.format, tt.time); got != tt.want { + t.Errorf("timeFormat() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getSection(t *testing.T) { + tests := []struct { + name string + sections []sv.ReleaseNoteSection + sectionName string + want sv.ReleaseNoteSection + }{ + { + "existing section", []sv.ReleaseNoteSection{ + sv.ReleaseNoteCommitsSection{Name: "section 0"}, + sv.ReleaseNoteCommitsSection{Name: "section 1"}, + sv.ReleaseNoteCommitsSection{Name: "section 2"}, + }, "section 1", sv.ReleaseNoteCommitsSection{Name: "section 1"}, + }, + { + "nonexisting section", []sv.ReleaseNoteSection{ + sv.ReleaseNoteCommitsSection{Name: "section 0"}, + sv.ReleaseNoteCommitsSection{Name: "section 1"}, + sv.ReleaseNoteCommitsSection{Name: "section 2"}, + }, "section 10", nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getSection(tt.sectionName, tt.sections); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getSection() = %v, want %v", got, tt.want) + } + }) + } +}