0
0
mirror of https://github.com/thegeeklab/git-sv.git synced 2024-11-13 21:30:40 +00:00

refactor: rework packages and interfaces (#3)

This commit is contained in:
Robert Kaussow 2023-10-15 21:29:29 +02:00 committed by GitHub
parent c06748910e
commit db1691ada4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 1551 additions and 1360 deletions

View File

@ -3,7 +3,6 @@ YAML
.gitsv .gitsv
cli cli
getsection getsection
timefmt
cfg cfg
json json
changelog changelog

View File

@ -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. > :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 <https://pkg.go.dev/time#Time.Format> 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 ### Running
Run `git-sv` to get the list of available parameters: Run `git-sv` to get the list of available parameters:

View File

@ -1,8 +1,9 @@
package sv package app
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"errors"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@ -11,35 +12,22 @@ import (
"time" "time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/thegeeklab/git-sv/v2/sv"
"github.com/thegeeklab/git-sv/v2/sv/formatter"
) )
const ( const (
logSeparator = "###" logSeparator = "###"
endLine = "~~~" endLine = "~~~"
configFilename = "config.yml"
configDir = ".gitsv"
) )
// Git commands. var errUnknownGitError = errors.New("git command failed")
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)
}
// GitCommitLog description of a single commit log. // Tag git tag info.
type GitCommitLog struct { type Tag 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"`
}
// GitTag git tag info.
type GitTag struct {
Name string Name string
Date time.Time Date time.Time
} }
@ -66,27 +54,35 @@ func NewLogRange(t LogRangeType, start, end string) LogRange {
return LogRange{rangeType: t, start: start, end: end} return LogRange{rangeType: t, start: start, end: end}
} }
// GitImpl git command implementation. // Impl git command implementation.
type GitImpl struct { type GitSV struct {
messageProcessor MessageProcessor Config *Config
tagCfg TagConfig
MessageProcessor sv.MessageProcessor
CommitProcessor sv.CommitProcessor
ReleasenotesProcessor sv.ReleaseNoteProcessor
OutputFormatter formatter.OutputFormatter
} }
// NewGit constructor. // New constructor.
func NewGit(messageProcessor MessageProcessor, cfg TagConfig) *GitImpl { func New() GitSV {
return &GitImpl{ g := GitSV{
messageProcessor: messageProcessor, Config: NewConfig(configDir, configFilename),
tagCfg: cfg,
} }
g.MessageProcessor = sv.NewMessageProcessor(g.Config.CommitMessage, g.Config.Branches)
return g
} }
// LastTag get last tag, if no tag found, return empty. // LastTag get last tag, if no tag found, return empty.
func (g GitImpl) LastTag() string { func (g GitSV) LastTag() string {
//nolint:gosec //nolint:gosec
cmd := exec.Command( cmd := exec.Command(
"git", "git",
"for-each-ref", "for-each-ref",
fmt.Sprintf("refs/tags/%s", *g.tagCfg.Filter), fmt.Sprintf("refs/tags/%s", *g.Config.Tag.Filter),
"--sort", "--sort",
"-creatordate", "-creatordate",
"--format", "--format",
@ -104,7 +100,7 @@ func (g GitImpl) LastTag() string {
} }
// Log return git log. // 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 + format := "--pretty=format:\"%ad" + logSeparator +
"%at" + logSeparator + "%at" + logSeparator +
"%cN" + logSeparator + "%cN" + logSeparator +
@ -133,7 +129,7 @@ func (g GitImpl) Log(lr LogRange) ([]GitCommitLog, error) {
return nil, combinedOutputErr(err, out) return nil, combinedOutputErr(err, out)
} }
logs, parseErr := parseLogOutput(g.messageProcessor, string(out)) logs, parseErr := parseLogOutput(g.MessageProcessor, string(out))
if parseErr != nil { if parseErr != nil {
return nil, parseErr return nil, parseErr
} }
@ -141,8 +137,8 @@ func (g GitImpl) Log(lr LogRange) ([]GitCommitLog, error) {
return logs, nil return logs, nil
} }
// Commit runs git commit. // Commit runs git sv.
func (g GitImpl) Commit(header, body, footer string) error { func (g GitSV) Commit(header, body, footer string) error {
cmd := exec.Command("git", "commit", "-m", header, "-m", "", "-m", body, "-m", "", "-m", footer) cmd := exec.Command("git", "commit", "-m", header, "-m", "", "-m", body, "-m", "", "-m", footer)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
@ -151,8 +147,8 @@ func (g GitImpl) Commit(header, body, footer string) error {
} }
// Tag create a git tag. // Tag create a git tag.
func (g GitImpl) Tag(version semver.Version) (string, error) { func (g GitSV) Tag(version semver.Version) (string, error) {
tag := fmt.Sprintf(*g.tagCfg.Pattern, version.Major(), version.Minor(), version.Patch()) 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()) tagMsg := fmt.Sprintf("Version %d.%d.%d", version.Major(), version.Minor(), version.Patch())
tagCommand := exec.Command("git", "tag", "-a", tag, "-m", tagMsg) 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. // Tags list repository tags.
func (g GitImpl) Tags() ([]GitTag, error) { func (g GitSV) Tags() ([]Tag, error) {
//nolint:gosec //nolint:gosec
cmd := exec.Command( cmd := exec.Command(
"git", "git",
@ -178,7 +174,7 @@ func (g GitImpl) Tags() ([]GitTag, error) {
"creatordate", "creatordate",
"--format", "--format",
"%(creatordate:iso8601)#%(refname:short)", "%(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() out, err := cmd.CombinedOutput()
@ -190,7 +186,7 @@ func (g GitImpl) Tags() ([]GitTag, error) {
} }
// Branch get git branch. // Branch get git branch.
func (GitImpl) Branch() string { func (g GitSV) Branch() string {
cmd := exec.Command("git", "symbolic-ref", "--short", "HEAD") cmd := exec.Command("git", "symbolic-ref", "--short", "HEAD")
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()
@ -202,7 +198,7 @@ func (GitImpl) Branch() string {
} }
// IsDetached check if is detached. // IsDetached check if is detached.
func (GitImpl) IsDetached() (bool, error) { func (g GitSV) IsDetached() (bool, error) {
cmd := exec.Command("git", "symbolic-ref", "-q", "HEAD") cmd := exec.Command("git", "symbolic-ref", "-q", "HEAD")
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()
@ -219,27 +215,27 @@ func (GitImpl) IsDetached() (bool, error) {
return false, nil return false, nil
} }
func parseTagsOutput(input string) ([]GitTag, error) { func parseTagsOutput(input string) ([]Tag, error) {
scanner := bufio.NewScanner(strings.NewReader(input)) scanner := bufio.NewScanner(strings.NewReader(input))
var result []GitTag var result []Tag
for scanner.Scan() { for scanner.Scan() {
if line := strings.TrimSpace(scanner.Text()); line != "" { if line := strings.TrimSpace(scanner.Text()); line != "" {
values := strings.Split(line, "#") values := strings.Split(line, "#")
date, _ := time.Parse("2006-01-02 15:04:05 -0700", values[0]) // ignore invalid dates 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 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 := bufio.NewScanner(strings.NewReader(log))
scanner.Split(splitAt([]byte(endLine))) scanner.Split(splitAt([]byte(endLine)))
var logs []GitCommitLog var logs []sv.CommitLog
for scanner.Scan() { for scanner.Scan() {
if text := strings.TrimSpace(strings.Trim(scanner.Text(), "\"")); text != "" { if text := strings.TrimSpace(strings.Trim(scanner.Text(), "\"")); text != "" {
@ -255,16 +251,16 @@ func parseLogOutput(messageProcessor MessageProcessor, log string) ([]GitCommitL
return logs, nil return logs, nil
} }
func parseCommitLog(messageProcessor MessageProcessor, commit string) (GitCommitLog, error) { func parseCommitLog(messageProcessor sv.MessageProcessor, c string) (sv.CommitLog, error) {
content := strings.Split(strings.Trim(commit, "\""), logSeparator) content := strings.Split(strings.Trim(c, "\""), logSeparator)
timestamp, _ := strconv.Atoi(content[1]) timestamp, _ := strconv.Atoi(content[1])
message, err := messageProcessor.Parse(content[4], content[5]) message, err := messageProcessor.Parse(content[4], content[5])
if err != nil { if err != nil {
return GitCommitLog{}, err return sv.CommitLog{}, err
} }
return GitCommitLog{ return sv.CommitLog{
Date: content[0], Date: content[0],
Timestamp: timestamp, Timestamp: timestamp,
AuthorName: content[2], AuthorName: content[2],

View File

@ -1,4 +1,4 @@
package sv package app
import ( import (
"reflect" "reflect"
@ -10,19 +10,19 @@ func Test_parseTagsOutput(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input string input string
want []GitTag want []Tag
wantErr bool wantErr bool
}{ }{
{ {
"with date", "with date",
"2020-05-01 18:00:00 -0300#1.0.0", "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, false,
}, },
{ {
"without date", "without date",
"#1.0.0", "#1.0.0",
[]GitTag{{Name: "1.0.0", Date: time.Time{}}}, []Tag{{Name: "1.0.0", Date: time.Time{}}},
false, false,
}, },
} }

98
app/commands/changelog.go Normal file
View File

@ -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
}
}

108
app/commands/commit.go Normal file
View File

@ -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
}
}

86
app/commands/commitlog.go Normal file
View File

@ -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
}
}

View File

@ -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
}
}

37
app/commands/config.go Normal file
View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -1,4 +1,4 @@
package main package commands
import ( import (
"errors" "errors"

View File

@ -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
}
}

36
app/commands/tag.go Normal file
View File

@ -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
}
}

220
app/commands/utils.go Normal file
View File

@ -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
}

View File

@ -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
}
}

View File

@ -1,13 +1,14 @@
package main package app
import ( import (
"fmt" "fmt"
"log"
"os" "os"
"path/filepath"
"reflect" "reflect"
"dario.cat/mergo" "dario.cat/mergo"
"github.com/kelseyhightower/envconfig" "github.com/kelseyhightower/envconfig"
"github.com/rs/zerolog/log"
"github.com/thegeeklab/git-sv/v2/sv" "github.com/thegeeklab/git-sv/v2/sv"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@ -17,28 +18,57 @@ type EnvConfig struct {
Home string `envconfig:"GITSV_HOME" default:""` 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. // Config cli yaml config.
type Config struct { type Config struct {
Version string `yaml:"version"` Version string `yaml:"version"`
LogLevel string `yaml:"log-level"`
Versioning sv.VersioningConfig `yaml:"versioning"` Versioning sv.VersioningConfig `yaml:"versioning"`
Tag sv.TagConfig `yaml:"tag"` Tag TagConfig `yaml:"tag"`
ReleaseNotes sv.ReleaseNotesConfig `yaml:"release-notes"` ReleaseNotes sv.ReleaseNotesConfig `yaml:"release-notes"`
Branches sv.BranchesConfig `yaml:"branches"` Branches sv.BranchesConfig `yaml:"branches"`
CommitMessage sv.CommitMessageConfig `yaml:"commit-message"` 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) content, rerr := os.ReadFile(filepath)
if rerr != nil { if rerr != nil {
return Config{}, rerr return Config{}, rerr
@ -54,12 +84,12 @@ func readConfig(filepath string) (Config, error) {
return cfg, nil return cfg, nil
} }
func defaultConfig() Config { func GetDefault() *Config {
skipDetached := false skipDetached := false
pattern := "%d.%d.%d" pattern := "%d.%d.%d"
filter := "" filter := ""
return Config{ return &Config{
Version: "1.1", Version: "1.1",
Versioning: sv.VersioningConfig{ Versioning: sv.VersioningConfig{
UpdateMajor: []string{}, UpdateMajor: []string{},
@ -67,7 +97,7 @@ func defaultConfig() Config {
UpdatePatch: []string{"build", "ci", "chore", "docs", "fix", "perf", "refactor", "style", "test"}, UpdatePatch: []string{"build", "ci", "chore", "docs", "fix", "perf", "refactor", "style", "test"},
IgnoreUnknown: false, IgnoreUnknown: false,
}, },
Tag: sv.TagConfig{ Tag: TagConfig{
Pattern: &pattern, Pattern: &pattern,
Filter: &filter, Filter: &filter,
}, },
@ -134,26 +164,26 @@ func (t *mergeTransformer) Transformer(typ reflect.Type) func(dst, src reflect.V
return nil return nil
} }
func migrateConfig(cfg Config, filename string) Config { func migrate(cfg Config, filename string) Config {
if cfg.ReleaseNotes.Headers == nil { if cfg.ReleaseNotes.Headers == nil {
return cfg 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{ return Config{
Version: cfg.Version, Version: cfg.Version,
Versioning: cfg.Versioning, Versioning: cfg.Versioning,
Tag: cfg.Tag, Tag: cfg.Tag,
ReleaseNotes: sv.ReleaseNotesConfig{ ReleaseNotes: sv.ReleaseNotesConfig{
Sections: migrateReleaseNotesConfig(cfg.ReleaseNotes.Headers), Sections: migrateReleaseNotes(cfg.ReleaseNotes.Headers),
}, },
Branches: cfg.Branches, Branches: cfg.Branches,
CommitMessage: cfg.CommitMessage, 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"} order := []string{"feat", "fix", "refactor", "perf", "test", "build", "ci", "chore", "docs", "style"}
var sections []sv.ReleaseNotesSectionConfig var sections []sv.ReleaseNotesSectionConfig
@ -181,3 +211,19 @@ func migrateReleaseNotesConfig(headers map[string]string) []sv.ReleaseNotesSecti
return sections 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"`
}

View File

@ -1,4 +1,4 @@
package main package app
import ( import (
"reflect" "reflect"
@ -128,21 +128,21 @@ func Test_merge(t *testing.T) {
"overwrite tag config", "overwrite tag config",
Config{ Config{
Version: "a", Version: "a",
Tag: sv.TagConfig{ Tag: TagConfig{
Pattern: &nonEmptyStr, Pattern: &nonEmptyStr,
Filter: &nonEmptyStr, Filter: &nonEmptyStr,
}, },
}, },
Config{ Config{
Version: "", Version: "",
Tag: sv.TagConfig{ Tag: TagConfig{
Pattern: &emptyStr, Pattern: &emptyStr,
Filter: &emptyStr, Filter: &emptyStr,
}, },
}, },
Config{ Config{
Version: "a", Version: "a",
Tag: sv.TagConfig{ Tag: TagConfig{
Pattern: &emptyStr, Pattern: &emptyStr,
Filter: &emptyStr, Filter: &emptyStr,
}, },

View File

@ -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
}

View File

@ -1,10 +0,0 @@
package main
import (
"fmt"
"os"
)
func warnf(format string, values ...interface{}) {
fmt.Fprintf(os.Stderr, "WARN: "+format+"\n", values...)
}

View File

@ -1,14 +1,14 @@
package main package main
import ( import (
"embed"
"fmt" "fmt"
"io/fs"
"log"
"os" "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" "github.com/urfave/cli/v2"
) )
@ -18,39 +18,8 @@ var (
BuildDate = "00000000" 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() { func main() {
log.SetFlags(0) gsv := app.New()
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")))
cli.VersionPrinter = func(c *cli.Context) { cli.VersionPrinter = func(c *cli.Context) {
fmt.Printf("%s version=%s date=%s\n", c.App.Name, c.App.Version, BuildDate) fmt.Printf("%s version=%s date=%s\n", c.App.Name, c.App.Version, BuildDate)
} }
@ -59,6 +28,23 @@ func main() {
Name: "git-sv", Name: "git-sv",
Usage: "Semantic version for git.", Usage: "Semantic version for git.",
Version: BuildVersion, 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{ Commands: []*cli.Command{
{ {
Name: "config", Name: "config",
@ -68,12 +54,12 @@ func main() {
{ {
Name: "default", Name: "default",
Usage: "show default config", Usage: "show default config",
Action: configDefaultHandler(), Action: commands.ConfigDefaultHandler(),
}, },
{ {
Name: "show", Name: "show",
Usage: "show current config", Usage: "show current config",
Action: configShowHandler(cfg), Action: commands.ConfigShowHandler(gsv.Config),
}, },
}, },
}, },
@ -81,13 +67,13 @@ func main() {
Name: "current-version", Name: "current-version",
Aliases: []string{"cv"}, Aliases: []string{"cv"},
Usage: "get last released version from git", Usage: "get last released version from git",
Action: currentVersionHandler(git), Action: commands.CurrentVersionHandler(gsv),
}, },
{ {
Name: "next-version", Name: "next-version",
Aliases: []string{"nv"}, Aliases: []string{"nv"},
Usage: "generate the next version based on git commit messages", Usage: "generate the next version based on git commit messages",
Action: nextVersionHandler(git, semverProcessor), Action: commands.NextVersionHandler(gsv),
}, },
{ {
Name: "commit-log", 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 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. 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.`, When flag range is "date", if "end" is YYYY-MM-DD the range will be inclusive.`,
Action: commitLogHandler(git), Action: commands.CommitLogHandler(gsv),
Flags: commitLogFlags(), Flags: commands.CommitLogFlags(),
}, },
{ {
Name: "commit-notes", 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 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. 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.`, When flag range is "date", if "end" is YYYY-MM-DD the range will be inclusive.`,
Action: commitNotesHandler(git, releasenotesProcessor, outputFormatter), Action: commands.CommitNotesHandler(gsv),
Flags: commitNotesFlags(), Flags: commands.CommitNotesFlags(),
}, },
{ {
Name: "release-notes", Name: "release-notes",
Aliases: []string{"rn"}, Aliases: []string{"rn"},
Usage: "generate release notes", Usage: "generate release notes",
Action: releaseNotesHandler(git, semverProcessor, releasenotesProcessor, outputFormatter), Action: commands.ReleaseNotesHandler(gsv),
Flags: releaseNotesFlags(), Flags: commands.ReleaseNotesFlags(),
}, },
{ {
Name: "changelog", Name: "changelog",
Aliases: []string{"cgl"}, Aliases: []string{"cgl"},
Usage: "generate changelog", Usage: "generate changelog",
Action: changelogHandler(git, semverProcessor, releasenotesProcessor, outputFormatter), Action: commands.ChangelogHandler(gsv),
Flags: changelogFlags(), Flags: commands.ChangelogFlags(),
}, },
{ {
Name: "tag", Name: "tag",
Aliases: []string{"tg"}, Aliases: []string{"tg"},
Usage: "generate tag with version based on git commit messages", Usage: "generate tag with version based on git commit messages",
Action: tagHandler(git, semverProcessor), Action: commands.TagHandler(gsv),
}, },
{ {
Name: "commit", Name: "commit",
Aliases: []string{"cmt"}, Aliases: []string{"cmt"},
Usage: "execute git commit with conventional commit message helper", Usage: "execute git commit with conventional commit message helper",
Action: commitHandler(cfg, git, messageProcessor), Action: commands.CommitHandler(gsv),
Flags: commitFlags(), Flags: commands.CommitFlags(),
}, },
{ {
Name: "validate-commit-message", Name: "validate-commit-message",
Aliases: []string{"vcm"}, Aliases: []string{"vcm"},
Usage: "use as prepare-commit-message hook to validate and enhance commit message", Usage: "use as prepare-commit-message hook to validate and enhance commit message",
Action: validateCommitMessageHandler(git, messageProcessor), Action: commands.ValidateCommitMessageHandler(gsv),
Flags: validateCommitMessageFlags(), Flags: commands.ValidateCommitMessageFlags(),
}, },
}, },
} }
if apperr := app.Run(os.Args); apperr != nil { if apperr := app.Run(os.Args); apperr != nil {
log.Fatal("ERROR: ", apperr) log.Fatal().Err(apperr).Msg("Execution error")
} }
} }
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)
}
}
}
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
}

View File

@ -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")
}
})
}
}

14
go.mod
View File

@ -5,19 +5,33 @@ go 1.19
require ( require (
dario.cat/mergo v1.0.0 dario.cat/mergo v1.0.0
github.com/Masterminds/semver/v3 v3.2.1 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/kelseyhightower/envconfig v1.4.0
github.com/manifoldco/promptui v0.9.0 github.com/manifoldco/promptui v0.9.0
github.com/rs/zerolog v1.31.0
github.com/urfave/cli/v2 v2.25.7 github.com/urfave/cli/v2 v2.25.7
gopkg.in/yaml.v2 v2.3.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/chzyer/readline v1.5.1 // indirect github.com/chzyer/readline v1.5.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // 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/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/rogpeppe/go-internal v1.11.0 // indirect
github.com/russross/blackfriday/v2 v2.1.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 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 golang.org/x/sys v0.13.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
) )

74
go.sum
View File

@ -1,7 +1,12 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 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 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= 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.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 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 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 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 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 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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/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 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= 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= 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/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 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 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/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.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 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 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 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= 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 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 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-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-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 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -11,6 +11,15 @@ const (
major 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. // IsValidVersion return true when a version is valid.
func IsValidVersion(value string) bool { func IsValidVersion(value string) bool {
_, err := semver.NewVersion(value) _, err := semver.NewVersion(value)
@ -28,13 +37,13 @@ func ToVersion(value string) (*semver.Version, error) {
return semver.NewVersion(version) return semver.NewVersion(version)
} }
// SemVerCommitsProcessor interface. // CommitProcessor interface.
type SemVerCommitsProcessor interface { type CommitProcessor interface {
NextVersion(version *semver.Version, commits []GitCommitLog) (*semver.Version, bool) NextVersion(version *semver.Version, commits []CommitLog) (*semver.Version, bool)
} }
// SemVerCommitsProcessorImpl process versions using commit log. // SemVerCommitProcessor process versions using commit log.
type SemVerCommitsProcessorImpl struct { type SemVerCommitProcessor struct {
MajorVersionTypes map[string]struct{} MajorVersionTypes map[string]struct{}
MinorVersionTypes map[string]struct{} MinorVersionTypes map[string]struct{}
PatchVersionTypes map[string]struct{} PatchVersionTypes map[string]struct{}
@ -42,9 +51,17 @@ type SemVerCommitsProcessorImpl struct {
IncludeUnknownTypeAsPatch bool IncludeUnknownTypeAsPatch bool
} }
// NewSemVerCommitsProcessor SemanticVersionCommitsProcessorImpl constructor. // VersioningConfig versioning preferences.
func NewSemVerCommitsProcessor(vcfg VersioningConfig, mcfg CommitMessageConfig) *SemVerCommitsProcessorImpl { type VersioningConfig struct {
return &SemVerCommitsProcessorImpl{ 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, IncludeUnknownTypeAsPatch: !vcfg.IgnoreUnknown,
MajorVersionTypes: toMap(vcfg.UpdateMajor), MajorVersionTypes: toMap(vcfg.UpdateMajor),
MinorVersionTypes: toMap(vcfg.UpdateMinor), MinorVersionTypes: toMap(vcfg.UpdateMinor),
@ -54,8 +71,8 @@ func NewSemVerCommitsProcessor(vcfg VersioningConfig, mcfg CommitMessageConfig)
} }
// NextVersion calculates next version based on commit log. // NextVersion calculates next version based on commit log.
func (p SemVerCommitsProcessorImpl) NextVersion( func (p SemVerCommitProcessor) NextVersion(
version *semver.Version, commits []GitCommitLog, version *semver.Version, commits []CommitLog,
) (*semver.Version, bool) { ) (*semver.Version, bool) {
versionToUpdate := none versionToUpdate := none
for _, commit := range commits { 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 { if commit.Message.IsBreakingChange {
return major return major
} }

View File

@ -7,106 +7,106 @@ import (
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
) )
func TestSemVerCommitsProcessorImpl_NextVersion(t *testing.T) { func TestSemVerCommitProcessor_NextVersion(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
ignoreUnknown bool ignoreUnknown bool
version *semver.Version version *semver.Version
commits []GitCommitLog commits []CommitLog
want *semver.Version want *semver.Version
wantUpdated bool wantUpdated bool
}{ }{
{ {
"no update", "no update",
true, true,
version("0.0.0"), TestVersion("0.0.0"),
[]GitCommitLog{}, []CommitLog{},
version("0.0.0"), TestVersion("0.0.0"),
false, false,
}, },
{ {
"no update without version", "no update without version",
true, true,
nil, nil,
[]GitCommitLog{}, []CommitLog{},
nil, nil,
false, false,
}, },
{ {
"no update on unknown type", "no update on unknown type",
true, true,
version("0.0.0"), TestVersion("0.0.0"),
[]GitCommitLog{commitlog("a", map[string]string{}, "a")}, []CommitLog{TestCommitlog("a", map[string]string{}, "a")},
version("0.0.0"), TestVersion("0.0.0"),
false, false,
}, },
{ {
"no update on unmapped known type", "no update on unmapped known type",
false, false,
version("0.0.0"), TestVersion("0.0.0"),
[]GitCommitLog{commitlog("none", map[string]string{}, "a")}, []CommitLog{TestCommitlog("none", map[string]string{}, "a")},
version("0.0.0"), TestVersion("0.0.0"),
false, false,
}, },
{ {
"update patch on unknown type", "update patch on unknown type",
false, false,
version("0.0.0"), TestVersion("0.0.0"),
[]GitCommitLog{commitlog("a", map[string]string{}, "a")}, []CommitLog{TestCommitlog("a", map[string]string{}, "a")},
version("0.0.1"), TestVersion("0.0.1"),
true, true,
}, },
{ {
"patch update", "patch update",
false, version("0.0.0"), false, TestVersion("0.0.0"),
[]GitCommitLog{commitlog("patch", map[string]string{}, "a")}, []CommitLog{TestCommitlog("patch", map[string]string{}, "a")},
version("0.0.1"), true, TestVersion("0.0.1"), true,
}, },
{ {
"patch update without version", "patch update without version",
false, false,
nil, nil,
[]GitCommitLog{commitlog("patch", map[string]string{}, "a")}, []CommitLog{TestCommitlog("patch", map[string]string{}, "a")},
nil, nil,
true, true,
}, },
{ {
"minor update", "minor update",
false, false,
version("0.0.0"), TestVersion("0.0.0"),
[]GitCommitLog{ []CommitLog{
commitlog("patch", map[string]string{}, "a"), TestCommitlog("patch", map[string]string{}, "a"),
commitlog("minor", map[string]string{}, "a"), TestCommitlog("minor", map[string]string{}, "a"),
}, },
version("0.1.0"), TestVersion("0.1.0"),
true, true,
}, },
{ {
"major update", "major update",
false, false,
version("0.0.0"), TestVersion("0.0.0"),
[]GitCommitLog{ []CommitLog{
commitlog("patch", map[string]string{}, "a"), TestCommitlog("patch", map[string]string{}, "a"),
commitlog("major", map[string]string{}, "a"), TestCommitlog("major", map[string]string{}, "a"),
}, },
version("1.0.0"), TestVersion("1.0.0"),
true, true,
}, },
{ {
"breaking change update", "breaking change update",
false, false,
version("0.0.0"), TestVersion("0.0.0"),
[]GitCommitLog{ []CommitLog{
commitlog("patch", map[string]string{}, "a"), TestCommitlog("patch", map[string]string{}, "a"),
commitlog("patch", map[string]string{"breaking-change": "break"}, "a"), TestCommitlog("patch", map[string]string{"breaking-change": "break"}, "a"),
}, },
version("1.0.0"), TestVersion("1.0.0"),
true, true,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
p := NewSemVerCommitsProcessor( p := NewSemVerCommitProcessor(
VersioningConfig{ VersioningConfig{
UpdateMajor: []string{"major"}, UpdateMajor: []string{"major"},
UpdateMinor: []string{"minor"}, UpdateMinor: []string{"minor"},
@ -116,10 +116,10 @@ func TestSemVerCommitsProcessorImpl_NextVersion(t *testing.T) {
CommitMessageConfig{Types: []string{"major", "minor", "patch", "none"}}) CommitMessageConfig{Types: []string{"major", "minor", "patch", "none"}})
got, gotUpdated := p.NextVersion(tt.version, tt.commits) got, gotUpdated := p.NextVersion(tt.version, tt.commits)
if !reflect.DeepEqual(got, tt.want) { 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 { 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 want *semver.Version
wantErr bool wantErr bool
}{ }{
{"empty version", "", version("0.0.0"), false}, {"empty version", "", TestVersion("0.0.0"), false},
{"invalid version", "abc", nil, true}, {"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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View File

@ -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 &sectionCfg
}
}
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"
)

View File

@ -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")
)

View File

@ -1,14 +1,13 @@
package sv package formatter
import ( import (
"bytes" "bytes"
"io/fs"
"os"
"sort" "sort"
"text/template" "text/template"
"time" "time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/thegeeklab/git-sv/v2/sv"
) )
type releaseNoteTemplateVariables struct { type releaseNoteTemplateVariables struct {
@ -16,35 +15,28 @@ type releaseNoteTemplateVariables struct {
Tag string Tag string
Version *semver.Version Version *semver.Version
Date time.Time Date time.Time
Sections []ReleaseNoteSection Sections []sv.ReleaseNoteSection
AuthorNames []string AuthorNames []string
} }
// OutputFormatter output formatter interface. // OutputFormatter output formatter interface.
type OutputFormatter interface { type OutputFormatter interface {
FormatReleaseNote(releasenote ReleaseNote) (string, error) FormatReleaseNote(releasenote sv.ReleaseNote) (string, error)
FormatChangelog(releasenotes []ReleaseNote) (string, error) FormatChangelog(releasenotes []sv.ReleaseNote) (string, error)
} }
// OutputFormatterImpl formater for release note and changelog. // BaseOutputFormatter formater for release note and changelog.
type OutputFormatterImpl struct { type BaseOutputFormatter struct {
templates *template.Template templates *template.Template
} }
// NewOutputFormatter TemplateProcessor constructor. // NewOutputFormatter TemplateProcessor constructor.
func NewOutputFormatter(templatesFS fs.FS) *OutputFormatterImpl { func NewOutputFormatter(tpls *template.Template) *BaseOutputFormatter {
templateFNs := map[string]interface{}{ return &BaseOutputFormatter{templates: tpls}
"timefmt": timeFormat,
"getsection": getSection,
"getenv": os.Getenv,
}
tpls := template.Must(template.New("templates").Funcs(templateFNs).ParseFS(templatesFS, "*"))
return &OutputFormatterImpl{templates: tpls}
} }
// FormatReleaseNote format a release note. // 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 var b bytes.Buffer
if err := p.templates.ExecuteTemplate(&b, "releasenotes-md.tpl", releaseNoteVariables(releasenote)); err != nil { if err := p.templates.ExecuteTemplate(&b, "releasenotes-md.tpl", releaseNoteVariables(releasenote)); err != nil {
return "", err return "", err
@ -54,7 +46,7 @@ func (p OutputFormatterImpl) FormatReleaseNote(releasenote ReleaseNote) (string,
} }
// FormatChangelog format a changelog. // 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)) templateVars := make([]releaseNoteTemplateVariables, len(releasenotes))
for i, v := range releasenotes { for i, v := range releasenotes {
templateVars[i] = releaseNoteVariables(v) templateVars[i] = releaseNoteVariables(v)
@ -68,7 +60,7 @@ func (p OutputFormatterImpl) FormatChangelog(releasenotes []ReleaseNote) (string
return b.String(), nil return b.String(), nil
} }
func releaseNoteVariables(releasenote ReleaseNote) releaseNoteTemplateVariables { func releaseNoteVariables(releasenote sv.ReleaseNote) releaseNoteTemplateVariables {
release := releasenote.Tag release := releasenote.Tag
if releasenote.Version != nil { if releasenote.Version != nil {
release = "v" + releasenote.Version.String() release = "v" + releasenote.Version.String()

View File

@ -1,15 +1,16 @@
package sv package formatter
import ( import (
"bytes" "bytes"
"os"
"testing" "testing"
"time" "time"
"github.com/Masterminds/semver/v3" "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)` var dateChangelog = `## v1.0.0 (2020-05-01)`
@ -37,12 +38,12 @@ var fullChangeLog = `## v1.0.0 (2020-05-01)
- break change message` - break change message`
func TestOutputFormatterImpl_FormatReleaseNote(t *testing.T) { func TestBaseOutputFormatter_FormatReleaseNote(t *testing.T) {
date, _ := time.Parse("2006-01-02", "2020-05-01") date, _ := time.Parse("2006-01-02", "2020-05-01")
tests := []struct { tests := []struct {
name string name string
input ReleaseNote input sv.ReleaseNote
want string want string
wantErr bool wantErr bool
}{ }{
@ -54,54 +55,54 @@ func TestOutputFormatterImpl_FormatReleaseNote(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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 { 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 { 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) v, _ := semver.NewVersion(tag)
return ReleaseNote{ return sv.ReleaseNote{
Version: v, Version: v,
Tag: tag, Tag: tag,
Date: date, Date: date,
} }
} }
func fullReleaseNote(tag string, date time.Time) ReleaseNote { func fullReleaseNote(tag string, date time.Time) sv.ReleaseNote {
v, _ := semver.NewVersion(tag) v, _ := semver.NewVersion(tag)
sections := []ReleaseNoteSection{ sections := []sv.ReleaseNoteSection{
newReleaseNoteCommitsSection( sv.TestNewReleaseNoteCommitsSection(
"Features", "Features",
[]string{"feat"}, []string{"feat"},
[]GitCommitLog{commitlog("feat", map[string]string{}, "a")}, []sv.CommitLog{sv.TestCommitlog("feat", map[string]string{}, "a")},
), ),
newReleaseNoteCommitsSection( sv.TestNewReleaseNoteCommitsSection(
"Bug Fixes", "Bug Fixes",
[]string{"fix"}, []string{"fix"},
[]GitCommitLog{commitlog("fix", map[string]string{}, "a")}, []sv.CommitLog{sv.TestCommitlog("fix", map[string]string{}, "a")},
), ),
newReleaseNoteCommitsSection( sv.TestNewReleaseNoteCommitsSection(
"Build", "Build",
[]string{"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) { func Test_checkTemplatesExecution(t *testing.T) {
tpls := NewOutputFormatter(templatesFS).templates tpls := NewOutputFormatter(tmpls).templates
tests := []struct { tests := []struct {
template string template string
variables interface{} variables interface{}
@ -131,20 +132,20 @@ func releaseNotesVariables(release string) releaseNoteTemplateVariables {
return releaseNoteTemplateVariables{ return releaseNoteTemplateVariables{
Release: release, Release: release,
Date: time.Date(2006, 1, 0o2, 0, 0, 0, 0, time.UTC), Date: time.Date(2006, 1, 0o2, 0, 0, 0, 0, time.UTC),
Sections: []ReleaseNoteSection{ Sections: []sv.ReleaseNoteSection{
newReleaseNoteCommitsSection("Features", sv.TestNewReleaseNoteCommitsSection("Features",
[]string{"feat"}, []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"}, []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"}, []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"}},
}, },
} }
} }

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -2,16 +2,24 @@ package sv
import ( import (
"bufio" "bufio"
"errors"
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
) )
const ( const (
breakingChangeFooterKey = "BREAKING CHANGE" BreakingChangeFooterKey = "BREAKING CHANGE"
breakingChangeMetadataKey = "breaking-change" BreakingChangeMetadataKey = "breaking-change"
issueMetadataKey = "issue" IssueMetadataKey = "issue"
messageRegexGroupName = "header" 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. // CommitMessage is a message using conventional commits.
@ -24,15 +32,59 @@ type CommitMessage struct {
Metadata map[string]string `json:"metadata,omitempty"` 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. // NewCommitMessage commit message constructor.
func NewCommitMessage(ctype, scope, description, body, issue, breakingChanges string) CommitMessage { func NewCommitMessage(ctype, scope, description, body, issue, breakingChanges string) CommitMessage {
metadata := make(map[string]string) metadata := make(map[string]string)
if issue != "" { if issue != "" {
metadata[issueMetadataKey] = issue metadata[IssueMetadataKey] = issue
} }
if breakingChanges != "" { if breakingChanges != "" {
metadata[breakingChangeMetadataKey] = breakingChanges metadata[BreakingChangeMetadataKey] = breakingChanges
} }
return CommitMessage{ return CommitMessage{
@ -47,12 +99,12 @@ func NewCommitMessage(ctype, scope, description, body, issue, breakingChanges st
// Issue return issue from metadata. // Issue return issue from metadata.
func (m CommitMessage) Issue() string { func (m CommitMessage) Issue() string {
return m.Metadata[issueMetadataKey] return m.Metadata[IssueMetadataKey]
} }
// BreakingMessage return breaking change message from metadata. // BreakingMessage return breaking change message from metadata.
func (m CommitMessage) BreakingMessage() string { func (m CommitMessage) BreakingMessage() string {
return m.Metadata[breakingChangeMetadataKey] return m.Metadata[BreakingChangeMetadataKey]
} }
// MessageProcessor interface. // MessageProcessor interface.
@ -68,28 +120,28 @@ type MessageProcessor interface {
Parse(subject, body string) (CommitMessage, error) Parse(subject, body string) (CommitMessage, error)
} }
// NewMessageProcessor MessageProcessorImpl constructor. // NewMessageProcessor BaseMessageProcessor constructor.
func NewMessageProcessor(mcfg CommitMessageConfig, bcfg BranchesConfig) *MessageProcessorImpl { func NewMessageProcessor(mcfg CommitMessageConfig, bcfg BranchesConfig) *BaseMessageProcessor {
return &MessageProcessorImpl{ return &BaseMessageProcessor{
messageCfg: mcfg, messageCfg: mcfg,
branchesCfg: bcfg, branchesCfg: bcfg,
} }
} }
// MessageProcessorImpl process validate message hook. // BaseMessageProcessor process validate message hook.
type MessageProcessorImpl struct { type BaseMessageProcessor struct {
messageCfg CommitMessageConfig messageCfg CommitMessageConfig
branchesCfg BranchesConfig branchesCfg BranchesConfig
} }
// SkipBranch check if branch should be ignored. // 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) || return contains(branch, p.branchesCfg.Skip) ||
(p.branchesCfg.SkipDetached != nil && *p.branchesCfg.SkipDetached && detached) (p.branchesCfg.SkipDetached != nil && *p.branchesCfg.SkipDetached && detached)
} }
// Validate commit message. // Validate commit message.
func (p MessageProcessorImpl) Validate(message string) error { func (p BaseMessageProcessor) Validate(message string) error {
subject, body := splitCommitMessageContent(message) subject, body := splitCommitMessageContent(message)
msg, parseErr := p.Parse(subject, body) msg, parseErr := p.Parse(subject, body)
@ -113,7 +165,7 @@ func (p MessageProcessorImpl) Validate(message string) error {
} }
// ValidateType check if commit type is valid. // 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) { if ctype == "" || !contains(ctype, p.messageCfg.Types) {
return fmt.Errorf( return fmt.Errorf(
"%w: type must be one of [%s]", "%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. // 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) { if len(p.messageCfg.Scope.Values) > 0 && !contains(scope, p.messageCfg.Scope.Values) {
return fmt.Errorf( return fmt.Errorf(
"%w: scope must one of [%s]", "%w: scope must one of [%s]",
@ -139,7 +191,7 @@ func (p MessageProcessorImpl) ValidateScope(scope string) error {
} }
// ValidateDescription check if commit description is valid. // 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) { if !regexp.MustCompile("^[a-z]+.*$").MatchString(description) {
return fmt.Errorf("%w: description [%s] must start with lowercase", errInvalidCommitMessage, 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. // 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 == "" || if p.branchesCfg.DisableIssue || p.messageCfg.IssueFooterConfig().Key == "" ||
hasIssueID(message, p.messageCfg.IssueFooterConfig()) { hasIssueID(message, p.messageCfg.IssueFooterConfig()) {
return "", nil // enhance disabled 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. // 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 == "" { if p.branchesCfg.DisableIssue || p.messageCfg.Issue.Regex == "" {
return "", nil return "", nil
} }
@ -205,7 +257,7 @@ func (p MessageProcessorImpl) IssueID(branch string) (string, error) {
} }
// Format a commit message returning header, body and footer. // 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 var header strings.Builder
header.WriteString(msg.Type) header.WriteString(msg.Type)
@ -219,10 +271,10 @@ func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string)
var footer strings.Builder var footer strings.Builder
if msg.BreakingMessage() != "" { 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 { if footer.Len() > 0 {
footer.WriteString("\n") footer.WriteString("\n")
} }
@ -238,7 +290,7 @@ func removeCarriage(commit string) string {
} }
// Parse a commit message. // 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) preparedSubject, err := p.prepareHeader(subject)
commitBody := removeCarriage(body) commitBody := removeCarriage(body)
@ -263,8 +315,8 @@ func (p MessageProcessorImpl) Parse(subject, body string) (CommitMessage, error)
} }
} }
if tagValue := extractFooterMetadata(breakingChangeFooterKey, commitBody, false); tagValue != "" { if tagValue := extractFooterMetadata(BreakingChangeFooterKey, commitBody, false); tagValue != "" {
metadata[breakingChangeMetadataKey] = tagValue metadata[BreakingChangeMetadataKey] = tagValue
hasBreakingChange = true hasBreakingChange = true
} }
@ -278,7 +330,7 @@ func (p MessageProcessorImpl) Parse(subject, body string) (CommitMessage, error)
}, nil }, nil
} }
func (p MessageProcessorImpl) prepareHeader(header string) (string, error) { func (p BaseMessageProcessor) prepareHeader(header string) (string, error) {
if p.messageCfg.HeaderSelector == "" { if p.messageCfg.HeaderSelector == "" {
return header, nil 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()) return "", fmt.Errorf("%w: %s: %s", errInvalidHeaderRegex, p.messageCfg.HeaderSelector, err.Error())
} }
index := regex.SubexpIndex(messageRegexGroupName) index := regex.SubexpIndex(MessageRegexGroupName)
if index < 0 { 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) match := regex.FindStringSubmatch(header)
@ -299,7 +351,7 @@ func (p MessageProcessorImpl) prepareHeader(header string) (string, error) {
return "", fmt.Errorf( return "", fmt.Errorf(
"%w: could not find group %s in match result for '%s'", "%w: could not find group %s in match result for '%s'",
errInvalidHeaderRegex, errInvalidHeaderRegex,
messageRegexGroupName, MessageRegexGroupName,
header, header,
) )
} }
@ -335,7 +387,7 @@ func extractFooterMetadata(key, text string, useHash bool) string {
} }
func hasFooter(message string) bool { 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)) scanner := bufio.NewScanner(strings.NewReader(message))
lines := 0 lines := 0

View File

@ -115,7 +115,7 @@ BREAKING CHANGE: refactor to use JavaScript features not available in Node 6.`
// multiline samples end // multiline samples end
func TestMessageProcessorImpl_SkipBranch(t *testing.T) { func TestBaseMessageProcessor_SkipBranch(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
bcfg BranchesConfig bcfg BranchesConfig
@ -133,13 +133,13 @@ func TestMessageProcessorImpl_SkipBranch(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
p := NewMessageProcessor(ccfg, tt.bcfg) p := NewMessageProcessor(ccfg, tt.bcfg)
if got := p.SkipBranch(tt.branch, tt.detached); got != tt.want { 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 { tests := []struct {
name string name string
cfg CommitMessageConfig cfg CommitMessageConfig
@ -200,13 +200,13 @@ func TestMessageProcessorImpl_Validate(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
p := NewMessageProcessor(tt.cfg, newBranchCfg(false)) p := NewMessageProcessor(tt.cfg, newBranchCfg(false))
if err := p.Validate(tt.message); (err != nil) != tt.wantErr { 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 { tests := []struct {
name string name string
cfg CommitMessageConfig cfg CommitMessageConfig
@ -233,13 +233,13 @@ func TestMessageProcessorImpl_ValidateType(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
p := NewMessageProcessor(tt.cfg, newBranchCfg(false)) p := NewMessageProcessor(tt.cfg, newBranchCfg(false))
if err := p.ValidateType(tt.ctype); (err != nil) != tt.wantErr { 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 { tests := []struct {
name string name string
cfg CommitMessageConfig cfg CommitMessageConfig
@ -258,13 +258,13 @@ func TestMessageProcessorImpl_ValidateScope(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
p := NewMessageProcessor(tt.cfg, newBranchCfg(false)) p := NewMessageProcessor(tt.cfg, newBranchCfg(false))
if err := p.ValidateScope(tt.scope); (err != nil) != tt.wantErr { 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 { tests := []struct {
name string name string
cfg CommitMessageConfig cfg CommitMessageConfig
@ -301,13 +301,13 @@ func TestMessageProcessorImpl_ValidateDescription(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
p := NewMessageProcessor(tt.cfg, newBranchCfg(false)) p := NewMessageProcessor(tt.cfg, newBranchCfg(false))
if err := p.ValidateDescription(tt.description); (err != nil) != tt.wantErr { 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 { tests := []struct {
name string name string
cfg CommitMessageConfig cfg CommitMessageConfig
@ -381,18 +381,18 @@ func TestMessageProcessorImpl_Enhance(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := NewMessageProcessor(tt.cfg, newBranchCfg(false)).Enhance(tt.branch, tt.message) got, err := NewMessageProcessor(tt.cfg, newBranchCfg(false)).Enhance(tt.branch, tt.message)
if (err != nil) != tt.wantErr { 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 return
} }
if got != tt.want { 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)) p := NewMessageProcessor(ccfg, newBranchCfg(false))
tests := []struct { tests := []struct {
@ -412,12 +412,12 @@ func TestMessageProcessorImpl_IssueID(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := p.IssueID(tt.branch) got, err := p.IssueID(tt.branch)
if (err != nil) != tt.wantErr { 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 return
} }
if got != tt.want { 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 Jira: JIRA-999
Refs #123` Refs #123`
func TestMessageProcessorImpl_Parse(t *testing.T) { func TestBaseMessageProcessor_Parse(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
cfg CommitMessageConfig cfg CommitMessageConfig
@ -572,8 +572,8 @@ func TestMessageProcessorImpl_Parse(t *testing.T) {
Body: completeBody, Body: completeBody,
IsBreakingChange: true, IsBreakingChange: true,
Metadata: map[string]string{ Metadata: map[string]string{
issueMetadataKey: "JIRA-123", IssueMetadataKey: "JIRA-123",
breakingChangeMetadataKey: "this change breaks everything", BreakingChangeMetadataKey: "this change breaks everything",
}, },
}, },
}, },
@ -587,7 +587,7 @@ func TestMessageProcessorImpl_Parse(t *testing.T) {
Description: "something new", Description: "something new",
Body: issueOnlyBody, Body: issueOnlyBody,
IsBreakingChange: false, 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", Description: "something new",
Body: issueSynonymsBody, Body: issueSynonymsBody,
IsBreakingChange: false, 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", Description: "something new",
Body: hashMetadataBody, Body: hashMetadataBody,
IsBreakingChange: false, 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", Description: "something new",
Body: expectedBodyWithCarriage, Body: expectedBodyWithCarriage,
IsBreakingChange: false, 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( if got, err := NewMessageProcessor(
tt.cfg, newBranchCfg(false), tt.cfg, newBranchCfg(false),
).Parse(tt.subject, tt.body); !reflect.DeepEqual(got, tt.want) && err == nil { ).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 { tests := []struct {
name string name string
cfg CommitMessageConfig cfg CommitMessageConfig
@ -777,13 +777,13 @@ func TestMessageProcessorImpl_Format(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, got1, got2 := NewMessageProcessor(tt.cfg, newBranchCfg(false)).Format(tt.msg) got, got1, got2 := NewMessageProcessor(tt.cfg, newBranchCfg(false)).Format(tt.msg)
if got != tt.wantHeader { 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 { 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 { 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)
} }
}) })
} }

View File

@ -6,27 +6,57 @@ import (
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
) )
// ReleaseNoteProcessor release note processor interface. // ReleaseNotesConfig release notes preferences.
type ReleaseNoteProcessor interface { type ReleaseNotesConfig struct {
Create(version *semver.Version, tag string, date time.Time, commits []GitCommitLog) ReleaseNote Headers map[string]string `yaml:"headers,omitempty"`
Sections []ReleaseNotesSectionConfig `yaml:"sections"`
} }
// ReleaseNoteProcessorImpl release note based on commit log. func (cfg ReleaseNotesConfig) sectionConfig(sectionType string) *ReleaseNotesSectionConfig {
type ReleaseNoteProcessorImpl struct { for _, sectionCfg := range cfg.Sections {
if sectionCfg.SectionType == sectionType {
return &sectionCfg
}
}
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 []CommitLog) ReleaseNote
}
// BaseReleaseNoteProcessor release note based on commit log.
type BaseReleaseNoteProcessor struct {
cfg ReleaseNotesConfig cfg ReleaseNotesConfig
} }
// NewReleaseNoteProcessor ReleaseNoteProcessor constructor. // NewReleaseNoteProcessor ReleaseNoteProcessor constructor.
func NewReleaseNoteProcessor(cfg ReleaseNotesConfig) *ReleaseNoteProcessorImpl { func NewReleaseNoteProcessor(cfg ReleaseNotesConfig) *BaseReleaseNoteProcessor {
return &ReleaseNoteProcessorImpl{cfg: cfg} return &BaseReleaseNoteProcessor{cfg: cfg}
} }
// Create create a release note based on commits. // Create create a release note based on commits.
func (p ReleaseNoteProcessorImpl) Create( func (p BaseReleaseNoteProcessor) Create(
version *semver.Version, version *semver.Version,
tag string, tag string,
date time.Time, date time.Time,
commits []GitCommitLog, commits []CommitLog,
) ReleaseNote { ) ReleaseNote {
mapping := commitSectionMapping(p.cfg.Sections) 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, commitSections map[string]ReleaseNoteCommitsSection,
breakingChange ReleaseNoteBreakingChangeSection, breakingChange ReleaseNoteBreakingChangeSection,
) []ReleaseNoteSection { ) []ReleaseNoteSection {
@ -144,7 +174,7 @@ func (s ReleaseNoteBreakingChangeSection) SectionName() string {
type ReleaseNoteCommitsSection struct { type ReleaseNoteCommitsSection struct {
Name string Name string
Types []string Types []string
Items []GitCommitLog Items []CommitLog
} }
// SectionType section type. // SectionType section type.

View File

@ -8,7 +8,7 @@ import (
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
) )
func TestReleaseNoteProcessorImpl_Create(t *testing.T) { func TestBaseReleaseNoteProcessor_Create(t *testing.T) {
date := time.Now() date := time.Now()
tests := []struct { tests := []struct {
@ -16,7 +16,7 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
version *semver.Version version *semver.Version
tag string tag string
date time.Time date time.Time
commits []GitCommitLog commits []CommitLog
want ReleaseNote want ReleaseNote
}{ }{
{ {
@ -24,13 +24,15 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
version: semver.MustParse("1.0.0"), version: semver.MustParse("1.0.0"),
tag: "v1.0.0", tag: "v1.0.0",
date: date, date: date,
commits: []GitCommitLog{commitlog("t1", map[string]string{}, "a")}, commits: []CommitLog{TestCommitlog("t1", map[string]string{}, "a")},
want: releaseNote( want: TestReleaseNote(
semver.MustParse("1.0.0"), semver.MustParse("1.0.0"),
"v1.0.0", "v1.0.0",
date, date,
[]ReleaseNoteSection{ []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": {}}, map[string]struct{}{"a": {}},
), ),
@ -40,13 +42,17 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
version: semver.MustParse("1.0.0"), version: semver.MustParse("1.0.0"),
tag: "v1.0.0", tag: "v1.0.0",
date: date, date: date,
commits: []GitCommitLog{commitlog("t1", map[string]string{}, "a"), commitlog("unmapped", map[string]string{}, "a")}, commits: []CommitLog{
want: releaseNote( TestCommitlog("t1", map[string]string{}, "a"), TestCommitlog("unmapped", map[string]string{}, "a"),
},
want: TestReleaseNote(
semver.MustParse("1.0.0"), semver.MustParse("1.0.0"),
"v1.0.0", "v1.0.0",
date, date,
[]ReleaseNoteSection{ []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": {}}, map[string]struct{}{"a": {}},
), ),
@ -56,16 +62,18 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
version: semver.MustParse("1.0.0"), version: semver.MustParse("1.0.0"),
tag: "v1.0.0", tag: "v1.0.0",
date: date, date: date,
commits: []GitCommitLog{ commits: []CommitLog{
commitlog("t1", map[string]string{}, "a"), TestCommitlog("t1", map[string]string{}, "a"),
commitlog("unmapped", map[string]string{"breaking-change": "breaks"}, "a"), TestCommitlog("unmapped", map[string]string{"breaking-change": "breaks"}, "a"),
}, },
want: releaseNote( want: TestReleaseNote(
semver.MustParse("1.0.0"), semver.MustParse("1.0.0"),
"v1.0.0", "v1.0.0",
date, date,
[]ReleaseNoteSection{ []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"}}, ReleaseNoteBreakingChangeSection{Name: "Breaking Changes", Messages: []string{"breaks"}},
}, },
map[string]struct{}{"a": {}}, map[string]struct{}{"a": {}},
@ -76,20 +84,20 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
version: semver.MustParse("1.0.0"), version: semver.MustParse("1.0.0"),
tag: "v1.0.0", tag: "v1.0.0",
date: date, date: date,
commits: []GitCommitLog{ commits: []CommitLog{
commitlog("t1", map[string]string{}, "author3"), TestCommitlog("t1", map[string]string{}, "author3"),
commitlog("t1", map[string]string{}, "author2"), TestCommitlog("t1", map[string]string{}, "author2"),
commitlog("t1", map[string]string{}, "author1"), TestCommitlog("t1", map[string]string{}, "author1"),
}, },
want: releaseNote( want: TestReleaseNote(
semver.MustParse("1.0.0"), semver.MustParse("1.0.0"),
"v1.0.0", "v1.0.0",
date, date,
[]ReleaseNoteSection{ []ReleaseNoteSection{
newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{ TestNewReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []CommitLog{
commitlog("t1", map[string]string{}, "author3"), TestCommitlog("t1", map[string]string{}, "author3"),
commitlog("t1", map[string]string{}, "author2"), TestCommitlog("t1", map[string]string{}, "author2"),
commitlog("t1", map[string]string{}, "author1"), TestCommitlog("t1", map[string]string{}, "author1"),
}), }),
}, },
map[string]struct{}{"author1": {}, "author2": {}, "author3": {}}, 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) { 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)
} }
}) })
} }

View File

@ -6,19 +6,19 @@ import (
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
) )
func version(v string) *semver.Version { func TestVersion(v string) *semver.Version {
r, _ := semver.NewVersion(v) r, _ := semver.NewVersion(v)
return r 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 breaking := false
if _, found := metadata[breakingChangeMetadataKey]; found { if _, found := metadata[BreakingChangeMetadataKey]; found {
breaking = true breaking = true
} }
return GitCommitLog{ return CommitLog{
Message: CommitMessage{ Message: CommitMessage{
Type: ctype, Type: ctype,
Description: "subject text", 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, version *semver.Version,
tag string, tag string,
date time.Time, 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{ return ReleaseNoteCommitsSection{
Name: name, Name: name,
Types: types, Types: types,

View File

@ -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 }} {{- range $section := .Sections }}
{{- if (eq $section.SectionType "commits") }} {{- if (eq $section.SectionType "commits") }}
{{- template "rn-md-section-commits.tpl" $section }} {{- template "rn-md-section-commits.tpl" $section }}

78
templates/templates.go Normal file
View File

@ -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
}

View File

@ -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)
}
})
}
}