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
cli
getsection
timefmt
cfg
json
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.
##### 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
Run `git-sv` to get the list of available parameters:

View File

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

View File

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

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 (
"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 (
"fmt"
"log"
"os"
"path/filepath"
"reflect"
"dario.cat/mergo"
"github.com/kelseyhightower/envconfig"
"github.com/rs/zerolog/log"
"github.com/thegeeklab/git-sv/v2/sv"
"gopkg.in/yaml.v3"
)
@ -17,28 +18,57 @@ type EnvConfig struct {
Home string `envconfig:"GITSV_HOME" default:""`
}
func loadEnvConfig() EnvConfig {
var c EnvConfig
err := envconfig.Process("", &c)
if err != nil {
log.Fatal("failed to load env config, error: ", err.Error())
}
return c
}
// Config cli yaml config.
type Config struct {
Version string `yaml:"version"`
LogLevel string `yaml:"log-level"`
Versioning sv.VersioningConfig `yaml:"versioning"`
Tag sv.TagConfig `yaml:"tag"`
Tag TagConfig `yaml:"tag"`
ReleaseNotes sv.ReleaseNotesConfig `yaml:"release-notes"`
Branches sv.BranchesConfig `yaml:"branches"`
CommitMessage sv.CommitMessageConfig `yaml:"commit-message"`
}
func readConfig(filepath string) (Config, error) {
func NewConfig(configDir, configFilename string) *Config {
workDir, _ := os.Getwd()
cfg := GetDefault()
envCfg := loadEnv()
if envCfg.Home != "" {
homeCfgFilepath := filepath.Join(envCfg.Home, configFilename)
if homeCfg, err := readFile(homeCfgFilepath); err == nil {
if merr := merge(cfg, migrate(homeCfg, homeCfgFilepath)); merr != nil {
log.Fatal().Err(merr).Msg("failed to merge user config")
}
}
}
repoCfgFilepath := filepath.Join(workDir, configDir, configFilename)
if repoCfg, err := readFile(repoCfgFilepath); err == nil {
if merr := merge(cfg, migrate(repoCfg, repoCfgFilepath)); merr != nil {
log.Fatal().Err(merr).Msg("failed to merge repo config")
}
if len(repoCfg.ReleaseNotes.Headers) > 0 { // mergo is merging maps, headers will be overwritten
cfg.ReleaseNotes.Headers = repoCfg.ReleaseNotes.Headers
}
}
return cfg
}
func loadEnv() EnvConfig {
var c EnvConfig
err := envconfig.Process("", &c)
if err != nil {
log.Fatal().Err(err).Msg("failed to load env config")
}
return c
}
func readFile(filepath string) (Config, error) {
content, rerr := os.ReadFile(filepath)
if rerr != nil {
return Config{}, rerr
@ -54,12 +84,12 @@ func readConfig(filepath string) (Config, error) {
return cfg, nil
}
func defaultConfig() Config {
func GetDefault() *Config {
skipDetached := false
pattern := "%d.%d.%d"
filter := ""
return Config{
return &Config{
Version: "1.1",
Versioning: sv.VersioningConfig{
UpdateMajor: []string{},
@ -67,7 +97,7 @@ func defaultConfig() Config {
UpdatePatch: []string{"build", "ci", "chore", "docs", "fix", "perf", "refactor", "style", "test"},
IgnoreUnknown: false,
},
Tag: sv.TagConfig{
Tag: TagConfig{
Pattern: &pattern,
Filter: &filter,
},
@ -134,26 +164,26 @@ func (t *mergeTransformer) Transformer(typ reflect.Type) func(dst, src reflect.V
return nil
}
func migrateConfig(cfg Config, filename string) Config {
func migrate(cfg Config, filename string) Config {
if cfg.ReleaseNotes.Headers == nil {
return cfg
}
warnf("config 'release-notes.headers' on %s is deprecated, please use 'sections' instead!", filename)
log.Warn().Msgf("config 'release-notes.headers' on %s is deprecated, please use 'sections' instead!", filename)
return Config{
Version: cfg.Version,
Versioning: cfg.Versioning,
Tag: cfg.Tag,
ReleaseNotes: sv.ReleaseNotesConfig{
Sections: migrateReleaseNotesConfig(cfg.ReleaseNotes.Headers),
Sections: migrateReleaseNotes(cfg.ReleaseNotes.Headers),
},
Branches: cfg.Branches,
CommitMessage: cfg.CommitMessage,
}
}
func migrateReleaseNotesConfig(headers map[string]string) []sv.ReleaseNotesSectionConfig {
func migrateReleaseNotes(headers map[string]string) []sv.ReleaseNotesSectionConfig {
order := []string{"feat", "fix", "refactor", "perf", "test", "build", "ci", "chore", "docs", "style"}
var sections []sv.ReleaseNotesSectionConfig
@ -181,3 +211,19 @@ func migrateReleaseNotesConfig(headers map[string]string) []sv.ReleaseNotesSecti
return sections
}
// ==== Message ====
// CommitMessageConfig config a commit message.
// ==== Branches ====
// ==== Versioning ====
// ==== Tag ====
// TagConfig tag preferences.
type TagConfig struct {
Pattern *string `yaml:"pattern"`
Filter *string `yaml:"filter"`
}

View File

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

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
import (
"embed"
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"github.com/thegeeklab/git-sv/v2/sv"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/thegeeklab/git-sv/v2/app"
"github.com/thegeeklab/git-sv/v2/app/commands"
"github.com/urfave/cli/v2"
)
@ -18,39 +18,8 @@ var (
BuildDate = "00000000"
)
const (
configFilename = "config.yml"
configDir = ".gitsv"
)
//go:embed resources/templates/*.tpl
var defaultTemplatesFS embed.FS
func templateFS(filepath string) fs.FS {
if _, err := os.Stat(filepath); err != nil {
defaultTemplatesFS, _ := fs.Sub(defaultTemplatesFS, "resources/templates")
return defaultTemplatesFS
}
return os.DirFS(filepath)
}
func main() {
log.SetFlags(0)
wd, err := os.Getwd()
if err != nil {
log.Fatal("error while retrieving working directory: %w", err)
}
cfg := loadCfg(wd)
messageProcessor := sv.NewMessageProcessor(cfg.CommitMessage, cfg.Branches)
git := sv.NewGit(messageProcessor, cfg.Tag)
semverProcessor := sv.NewSemVerCommitsProcessor(cfg.Versioning, cfg.CommitMessage)
releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotes)
outputFormatter := sv.NewOutputFormatter(templateFS(filepath.Join(wd, configDir, "templates")))
gsv := app.New()
cli.VersionPrinter = func(c *cli.Context) {
fmt.Printf("%s version=%s date=%s\n", c.App.Name, c.App.Version, BuildDate)
}
@ -59,6 +28,23 @@ func main() {
Name: "git-sv",
Usage: "Semantic version for git.",
Version: BuildVersion,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "log-level",
Usage: "log level",
},
},
Before: func(ctx *cli.Context) error {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
lvl, err := zerolog.ParseLevel(ctx.String("log-level"))
if err != nil {
return err
}
zerolog.SetGlobalLevel(lvl)
return nil
},
Commands: []*cli.Command{
{
Name: "config",
@ -68,12 +54,12 @@ func main() {
{
Name: "default",
Usage: "show default config",
Action: configDefaultHandler(),
Action: commands.ConfigDefaultHandler(),
},
{
Name: "show",
Usage: "show current config",
Action: configShowHandler(cfg),
Action: commands.ConfigShowHandler(gsv.Config),
},
},
},
@ -81,13 +67,13 @@ func main() {
Name: "current-version",
Aliases: []string{"cv"},
Usage: "get last released version from git",
Action: currentVersionHandler(git),
Action: commands.CurrentVersionHandler(gsv),
},
{
Name: "next-version",
Aliases: []string{"nv"},
Usage: "generate the next version based on git commit messages",
Action: nextVersionHandler(git, semverProcessor),
Action: commands.NextVersionHandler(gsv),
},
{
Name: "commit-log",
@ -96,8 +82,8 @@ func main() {
Description: `The range filter is used based on git log filters, check https://git-scm.com/docs/git-log
for more info. When flag range is "tag" and start is empty, last tag created will be used instead.
When flag range is "date", if "end" is YYYY-MM-DD the range will be inclusive.`,
Action: commitLogHandler(git),
Flags: commitLogFlags(),
Action: commands.CommitLogHandler(gsv),
Flags: commands.CommitLogFlags(),
},
{
Name: "commit-notes",
@ -106,74 +92,47 @@ When flag range is "date", if "end" is YYYY-MM-DD the range will be inclusive.`,
Description: `The range filter is used based on git log filters, check https://git-scm.com/docs/git-log
for more info. When flag range is "tag" and start is empty, last tag created will be used instead.
When flag range is "date", if "end" is YYYY-MM-DD the range will be inclusive.`,
Action: commitNotesHandler(git, releasenotesProcessor, outputFormatter),
Flags: commitNotesFlags(),
Action: commands.CommitNotesHandler(gsv),
Flags: commands.CommitNotesFlags(),
},
{
Name: "release-notes",
Aliases: []string{"rn"},
Usage: "generate release notes",
Action: releaseNotesHandler(git, semverProcessor, releasenotesProcessor, outputFormatter),
Flags: releaseNotesFlags(),
Action: commands.ReleaseNotesHandler(gsv),
Flags: commands.ReleaseNotesFlags(),
},
{
Name: "changelog",
Aliases: []string{"cgl"},
Usage: "generate changelog",
Action: changelogHandler(git, semverProcessor, releasenotesProcessor, outputFormatter),
Flags: changelogFlags(),
Action: commands.ChangelogHandler(gsv),
Flags: commands.ChangelogFlags(),
},
{
Name: "tag",
Aliases: []string{"tg"},
Usage: "generate tag with version based on git commit messages",
Action: tagHandler(git, semverProcessor),
Action: commands.TagHandler(gsv),
},
{
Name: "commit",
Aliases: []string{"cmt"},
Usage: "execute git commit with conventional commit message helper",
Action: commitHandler(cfg, git, messageProcessor),
Flags: commitFlags(),
Action: commands.CommitHandler(gsv),
Flags: commands.CommitFlags(),
},
{
Name: "validate-commit-message",
Aliases: []string{"vcm"},
Usage: "use as prepare-commit-message hook to validate and enhance commit message",
Action: validateCommitMessageHandler(git, messageProcessor),
Flags: validateCommitMessageFlags(),
Action: commands.ValidateCommitMessageHandler(gsv),
Flags: commands.ValidateCommitMessageFlags(),
},
},
}
if apperr := app.Run(os.Args); apperr != nil {
log.Fatal("ERROR: ", apperr)
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 (
dario.cat/mergo v1.0.0
github.com/Masterminds/semver/v3 v3.2.1
github.com/Masterminds/sprig/v3 v3.2.3
github.com/kelseyhightower/envconfig v1.4.0
github.com/manifoldco/promptui v0.9.0
github.com/rs/zerolog v1.31.0
github.com/urfave/cli/v2 v2.25.7
gopkg.in/yaml.v2 v2.3.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/google/uuid v1.1.1 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/crypto v0.3.0 // indirect
golang.org/x/sys v0.13.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)

74
go.sum
View File

@ -1,7 +1,12 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
@ -11,9 +16,20 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
@ -25,22 +41,80 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -11,6 +11,15 @@ const (
major
)
// CommitLog description of a single commit log.
type CommitLog struct {
Date string `json:"date,omitempty"`
Timestamp int `json:"timestamp,omitempty"`
AuthorName string `json:"authorName,omitempty"`
Hash string `json:"hash,omitempty"`
Message CommitMessage `json:"message,omitempty"`
}
// IsValidVersion return true when a version is valid.
func IsValidVersion(value string) bool {
_, err := semver.NewVersion(value)
@ -28,13 +37,13 @@ func ToVersion(value string) (*semver.Version, error) {
return semver.NewVersion(version)
}
// SemVerCommitsProcessor interface.
type SemVerCommitsProcessor interface {
NextVersion(version *semver.Version, commits []GitCommitLog) (*semver.Version, bool)
// CommitProcessor interface.
type CommitProcessor interface {
NextVersion(version *semver.Version, commits []CommitLog) (*semver.Version, bool)
}
// SemVerCommitsProcessorImpl process versions using commit log.
type SemVerCommitsProcessorImpl struct {
// SemVerCommitProcessor process versions using commit log.
type SemVerCommitProcessor struct {
MajorVersionTypes map[string]struct{}
MinorVersionTypes map[string]struct{}
PatchVersionTypes map[string]struct{}
@ -42,9 +51,17 @@ type SemVerCommitsProcessorImpl struct {
IncludeUnknownTypeAsPatch bool
}
// NewSemVerCommitsProcessor SemanticVersionCommitsProcessorImpl constructor.
func NewSemVerCommitsProcessor(vcfg VersioningConfig, mcfg CommitMessageConfig) *SemVerCommitsProcessorImpl {
return &SemVerCommitsProcessorImpl{
// VersioningConfig versioning preferences.
type VersioningConfig struct {
UpdateMajor []string `yaml:"update-major,flow"`
UpdateMinor []string `yaml:"update-minor,flow"`
UpdatePatch []string `yaml:"update-patch,flow"`
IgnoreUnknown bool `yaml:"ignore-unknown"`
}
// NewSemVerCommitProcessor SemanticVersionCommitProcessorImpl constructor.
func NewSemVerCommitProcessor(vcfg VersioningConfig, mcfg CommitMessageConfig) *SemVerCommitProcessor {
return &SemVerCommitProcessor{
IncludeUnknownTypeAsPatch: !vcfg.IgnoreUnknown,
MajorVersionTypes: toMap(vcfg.UpdateMajor),
MinorVersionTypes: toMap(vcfg.UpdateMinor),
@ -54,8 +71,8 @@ func NewSemVerCommitsProcessor(vcfg VersioningConfig, mcfg CommitMessageConfig)
}
// NextVersion calculates next version based on commit log.
func (p SemVerCommitsProcessorImpl) NextVersion(
version *semver.Version, commits []GitCommitLog,
func (p SemVerCommitProcessor) NextVersion(
version *semver.Version, commits []CommitLog,
) (*semver.Version, bool) {
versionToUpdate := none
for _, commit := range commits {
@ -87,7 +104,7 @@ func updateVersion(version semver.Version, versionToUpdate versionType) semver.V
}
}
func (p SemVerCommitsProcessorImpl) versionTypeToUpdate(commit GitCommitLog) versionType {
func (p SemVerCommitProcessor) versionTypeToUpdate(commit CommitLog) versionType {
if commit.Message.IsBreakingChange {
return major
}

View File

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

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

View File

@ -1,15 +1,16 @@
package sv
package formatter
import (
"bytes"
"os"
"testing"
"time"
"github.com/Masterminds/semver/v3"
"github.com/thegeeklab/git-sv/v2/sv"
"github.com/thegeeklab/git-sv/v2/templates"
)
var templatesFS = os.DirFS("../cmd/git-sv/resources/templates")
var tmpls = templates.New("")
var dateChangelog = `## v1.0.0 (2020-05-01)`
@ -37,12 +38,12 @@ var fullChangeLog = `## v1.0.0 (2020-05-01)
- break change message`
func TestOutputFormatterImpl_FormatReleaseNote(t *testing.T) {
func TestBaseOutputFormatter_FormatReleaseNote(t *testing.T) {
date, _ := time.Parse("2006-01-02", "2020-05-01")
tests := []struct {
name string
input ReleaseNote
input sv.ReleaseNote
want string
wantErr bool
}{
@ -54,54 +55,54 @@ func TestOutputFormatterImpl_FormatReleaseNote(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewOutputFormatter(templatesFS).FormatReleaseNote(tt.input)
got, err := NewOutputFormatter(tmpls).FormatReleaseNote(tt.input)
if got != tt.want {
t.Errorf("OutputFormatterImpl.FormatReleaseNote() = %v, want %v", got, tt.want)
t.Errorf("BaseOutputFormatter.FormatReleaseNote() = %v, want %v", got, tt.want)
}
if (err != nil) != tt.wantErr {
t.Errorf("OutputFormatterImpl.FormatReleaseNote() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("BaseOutputFormatter.FormatReleaseNote() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func emptyReleaseNote(tag string, date time.Time) ReleaseNote {
func emptyReleaseNote(tag string, date time.Time) sv.ReleaseNote {
v, _ := semver.NewVersion(tag)
return ReleaseNote{
return sv.ReleaseNote{
Version: v,
Tag: tag,
Date: date,
}
}
func fullReleaseNote(tag string, date time.Time) ReleaseNote {
func fullReleaseNote(tag string, date time.Time) sv.ReleaseNote {
v, _ := semver.NewVersion(tag)
sections := []ReleaseNoteSection{
newReleaseNoteCommitsSection(
sections := []sv.ReleaseNoteSection{
sv.TestNewReleaseNoteCommitsSection(
"Features",
[]string{"feat"},
[]GitCommitLog{commitlog("feat", map[string]string{}, "a")},
[]sv.CommitLog{sv.TestCommitlog("feat", map[string]string{}, "a")},
),
newReleaseNoteCommitsSection(
sv.TestNewReleaseNoteCommitsSection(
"Bug Fixes",
[]string{"fix"},
[]GitCommitLog{commitlog("fix", map[string]string{}, "a")},
[]sv.CommitLog{sv.TestCommitlog("fix", map[string]string{}, "a")},
),
newReleaseNoteCommitsSection(
sv.TestNewReleaseNoteCommitsSection(
"Build",
[]string{"build"},
[]GitCommitLog{commitlog("build", map[string]string{}, "a")},
[]sv.CommitLog{sv.TestCommitlog("build", map[string]string{}, "a")},
),
ReleaseNoteBreakingChangeSection{"Breaking Changes", []string{"break change message"}},
sv.ReleaseNoteBreakingChangeSection{Name: "Breaking Changes", Messages: []string{"break change message"}},
}
return releaseNote(v, tag, date, sections, map[string]struct{}{"a": {}})
return sv.TestReleaseNote(v, tag, date, sections, map[string]struct{}{"a": {}})
}
func Test_checkTemplatesExecution(t *testing.T) {
tpls := NewOutputFormatter(templatesFS).templates
tpls := NewOutputFormatter(tmpls).templates
tests := []struct {
template string
variables interface{}
@ -131,20 +132,20 @@ func releaseNotesVariables(release string) releaseNoteTemplateVariables {
return releaseNoteTemplateVariables{
Release: release,
Date: time.Date(2006, 1, 0o2, 0, 0, 0, 0, time.UTC),
Sections: []ReleaseNoteSection{
newReleaseNoteCommitsSection("Features",
Sections: []sv.ReleaseNoteSection{
sv.TestNewReleaseNoteCommitsSection("Features",
[]string{"feat"},
[]GitCommitLog{commitlog("feat", map[string]string{}, "a")},
[]sv.CommitLog{sv.TestCommitlog("feat", map[string]string{}, "a")},
),
newReleaseNoteCommitsSection("Bug Fixes",
sv.TestNewReleaseNoteCommitsSection("Bug Fixes",
[]string{"fix"},
[]GitCommitLog{commitlog("fix", map[string]string{}, "a")},
[]sv.CommitLog{sv.TestCommitlog("fix", map[string]string{}, "a")},
),
newReleaseNoteCommitsSection("Build",
sv.TestNewReleaseNoteCommitsSection("Build",
[]string{"build"},
[]GitCommitLog{commitlog("build", map[string]string{}, "a")},
[]sv.CommitLog{sv.TestCommitlog("build", map[string]string{}, "a")},
),
ReleaseNoteBreakingChangeSection{"Breaking Changes", []string{"break change message"}},
sv.ReleaseNoteBreakingChangeSection{Name: "Breaking Changes", Messages: []string{"break change message"}},
},
}
}

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 (
"bufio"
"errors"
"fmt"
"regexp"
"strings"
)
const (
breakingChangeFooterKey = "BREAKING CHANGE"
breakingChangeMetadataKey = "breaking-change"
issueMetadataKey = "issue"
messageRegexGroupName = "header"
BreakingChangeFooterKey = "BREAKING CHANGE"
BreakingChangeMetadataKey = "breaking-change"
IssueMetadataKey = "issue"
MessageRegexGroupName = "header"
)
var (
errInvalidCommitMessage = errors.New("commit message not valid")
errIssueIDNotFound = errors.New("could not find issue id using configured regex")
errInvalidIssueRegex = errors.New("could not compile issue regex")
errInvalidHeaderRegex = errors.New("invalid regex on header-selector")
)
// CommitMessage is a message using conventional commits.
@ -24,15 +32,59 @@ type CommitMessage struct {
Metadata map[string]string `json:"metadata,omitempty"`
}
type CommitMessageConfig struct {
Types []string `yaml:"types,flow"`
HeaderSelector string `yaml:"header-selector"`
Scope CommitMessageScopeConfig `yaml:"scope"`
Footer map[string]CommitMessageFooterConfig `yaml:"footer"`
Issue CommitMessageIssueConfig `yaml:"issue"`
}
// IssueFooterConfig config for issue.
func (c CommitMessageConfig) IssueFooterConfig() CommitMessageFooterConfig {
if v, exists := c.Footer[IssueMetadataKey]; exists {
return v
}
return CommitMessageFooterConfig{}
}
// CommitMessageScopeConfig config scope preferences.
type CommitMessageScopeConfig struct {
Values []string `yaml:"values"`
}
// CommitMessageFooterConfig config footer metadata.
type CommitMessageFooterConfig struct {
Key string `yaml:"key"`
KeySynonyms []string `yaml:"key-synonyms,flow"`
UseHash bool `yaml:"use-hash"`
AddValuePrefix string `yaml:"add-value-prefix"`
}
// CommitMessageIssueConfig issue preferences.
type CommitMessageIssueConfig struct {
Regex string `yaml:"regex"`
}
// BranchesConfig branches preferences.
type BranchesConfig struct {
Prefix string `yaml:"prefix"`
Suffix string `yaml:"suffix"`
DisableIssue bool `yaml:"disable-issue"`
Skip []string `yaml:"skip,flow"`
SkipDetached *bool `yaml:"skip-detached"`
}
// NewCommitMessage commit message constructor.
func NewCommitMessage(ctype, scope, description, body, issue, breakingChanges string) CommitMessage {
metadata := make(map[string]string)
if issue != "" {
metadata[issueMetadataKey] = issue
metadata[IssueMetadataKey] = issue
}
if breakingChanges != "" {
metadata[breakingChangeMetadataKey] = breakingChanges
metadata[BreakingChangeMetadataKey] = breakingChanges
}
return CommitMessage{
@ -47,12 +99,12 @@ func NewCommitMessage(ctype, scope, description, body, issue, breakingChanges st
// Issue return issue from metadata.
func (m CommitMessage) Issue() string {
return m.Metadata[issueMetadataKey]
return m.Metadata[IssueMetadataKey]
}
// BreakingMessage return breaking change message from metadata.
func (m CommitMessage) BreakingMessage() string {
return m.Metadata[breakingChangeMetadataKey]
return m.Metadata[BreakingChangeMetadataKey]
}
// MessageProcessor interface.
@ -68,28 +120,28 @@ type MessageProcessor interface {
Parse(subject, body string) (CommitMessage, error)
}
// NewMessageProcessor MessageProcessorImpl constructor.
func NewMessageProcessor(mcfg CommitMessageConfig, bcfg BranchesConfig) *MessageProcessorImpl {
return &MessageProcessorImpl{
// NewMessageProcessor BaseMessageProcessor constructor.
func NewMessageProcessor(mcfg CommitMessageConfig, bcfg BranchesConfig) *BaseMessageProcessor {
return &BaseMessageProcessor{
messageCfg: mcfg,
branchesCfg: bcfg,
}
}
// MessageProcessorImpl process validate message hook.
type MessageProcessorImpl struct {
// BaseMessageProcessor process validate message hook.
type BaseMessageProcessor struct {
messageCfg CommitMessageConfig
branchesCfg BranchesConfig
}
// SkipBranch check if branch should be ignored.
func (p MessageProcessorImpl) SkipBranch(branch string, detached bool) bool {
func (p BaseMessageProcessor) SkipBranch(branch string, detached bool) bool {
return contains(branch, p.branchesCfg.Skip) ||
(p.branchesCfg.SkipDetached != nil && *p.branchesCfg.SkipDetached && detached)
}
// Validate commit message.
func (p MessageProcessorImpl) Validate(message string) error {
func (p BaseMessageProcessor) Validate(message string) error {
subject, body := splitCommitMessageContent(message)
msg, parseErr := p.Parse(subject, body)
@ -113,7 +165,7 @@ func (p MessageProcessorImpl) Validate(message string) error {
}
// ValidateType check if commit type is valid.
func (p MessageProcessorImpl) ValidateType(ctype string) error {
func (p BaseMessageProcessor) ValidateType(ctype string) error {
if ctype == "" || !contains(ctype, p.messageCfg.Types) {
return fmt.Errorf(
"%w: type must be one of [%s]",
@ -126,7 +178,7 @@ func (p MessageProcessorImpl) ValidateType(ctype string) error {
}
// ValidateScope check if commit scope is valid.
func (p MessageProcessorImpl) ValidateScope(scope string) error {
func (p BaseMessageProcessor) ValidateScope(scope string) error {
if len(p.messageCfg.Scope.Values) > 0 && !contains(scope, p.messageCfg.Scope.Values) {
return fmt.Errorf(
"%w: scope must one of [%s]",
@ -139,7 +191,7 @@ func (p MessageProcessorImpl) ValidateScope(scope string) error {
}
// ValidateDescription check if commit description is valid.
func (p MessageProcessorImpl) ValidateDescription(description string) error {
func (p BaseMessageProcessor) ValidateDescription(description string) error {
if !regexp.MustCompile("^[a-z]+.*$").MatchString(description) {
return fmt.Errorf("%w: description [%s] must start with lowercase", errInvalidCommitMessage, description)
}
@ -148,7 +200,7 @@ func (p MessageProcessorImpl) ValidateDescription(description string) error {
}
// Enhance add metadata on commit message.
func (p MessageProcessorImpl) Enhance(branch, message string) (string, error) {
func (p BaseMessageProcessor) Enhance(branch, message string) (string, error) {
if p.branchesCfg.DisableIssue || p.messageCfg.IssueFooterConfig().Key == "" ||
hasIssueID(message, p.messageCfg.IssueFooterConfig()) {
return "", nil // enhance disabled
@ -184,7 +236,7 @@ func formatIssueFooter(cfg CommitMessageFooterConfig, issue string) string {
}
// IssueID try to extract issue id from branch, return empty if not found.
func (p MessageProcessorImpl) IssueID(branch string) (string, error) {
func (p BaseMessageProcessor) IssueID(branch string) (string, error) {
if p.branchesCfg.DisableIssue || p.messageCfg.Issue.Regex == "" {
return "", nil
}
@ -205,7 +257,7 @@ func (p MessageProcessorImpl) IssueID(branch string) (string, error) {
}
// Format a commit message returning header, body and footer.
func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string) {
func (p BaseMessageProcessor) Format(msg CommitMessage) (string, string, string) {
var header strings.Builder
header.WriteString(msg.Type)
@ -219,10 +271,10 @@ func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string)
var footer strings.Builder
if msg.BreakingMessage() != "" {
footer.WriteString(fmt.Sprintf("%s: %s", breakingChangeFooterKey, msg.BreakingMessage()))
footer.WriteString(fmt.Sprintf("%s: %s", BreakingChangeFooterKey, msg.BreakingMessage()))
}
if issue, exists := msg.Metadata[issueMetadataKey]; exists && p.messageCfg.IssueFooterConfig().Key != "" {
if issue, exists := msg.Metadata[IssueMetadataKey]; exists && p.messageCfg.IssueFooterConfig().Key != "" {
if footer.Len() > 0 {
footer.WriteString("\n")
}
@ -238,7 +290,7 @@ func removeCarriage(commit string) string {
}
// Parse a commit message.
func (p MessageProcessorImpl) Parse(subject, body string) (CommitMessage, error) {
func (p BaseMessageProcessor) Parse(subject, body string) (CommitMessage, error) {
preparedSubject, err := p.prepareHeader(subject)
commitBody := removeCarriage(body)
@ -263,8 +315,8 @@ func (p MessageProcessorImpl) Parse(subject, body string) (CommitMessage, error)
}
}
if tagValue := extractFooterMetadata(breakingChangeFooterKey, commitBody, false); tagValue != "" {
metadata[breakingChangeMetadataKey] = tagValue
if tagValue := extractFooterMetadata(BreakingChangeFooterKey, commitBody, false); tagValue != "" {
metadata[BreakingChangeMetadataKey] = tagValue
hasBreakingChange = true
}
@ -278,7 +330,7 @@ func (p MessageProcessorImpl) Parse(subject, body string) (CommitMessage, error)
}, nil
}
func (p MessageProcessorImpl) prepareHeader(header string) (string, error) {
func (p BaseMessageProcessor) prepareHeader(header string) (string, error) {
if p.messageCfg.HeaderSelector == "" {
return header, nil
}
@ -288,9 +340,9 @@ func (p MessageProcessorImpl) prepareHeader(header string) (string, error) {
return "", fmt.Errorf("%w: %s: %s", errInvalidHeaderRegex, p.messageCfg.HeaderSelector, err.Error())
}
index := regex.SubexpIndex(messageRegexGroupName)
index := regex.SubexpIndex(MessageRegexGroupName)
if index < 0 {
return "", fmt.Errorf("%w: could not find group %s", errInvalidHeaderRegex, messageRegexGroupName)
return "", fmt.Errorf("%w: could not find group %s", errInvalidHeaderRegex, MessageRegexGroupName)
}
match := regex.FindStringSubmatch(header)
@ -299,7 +351,7 @@ func (p MessageProcessorImpl) prepareHeader(header string) (string, error) {
return "", fmt.Errorf(
"%w: could not find group %s in match result for '%s'",
errInvalidHeaderRegex,
messageRegexGroupName,
MessageRegexGroupName,
header,
)
}
@ -335,7 +387,7 @@ func extractFooterMetadata(key, text string, useHash bool) string {
}
func hasFooter(message string) bool {
r := regexp.MustCompile("^[a-zA-Z-]+: .*|^[a-zA-Z-]+ #.*|^" + breakingChangeFooterKey + ": .*")
r := regexp.MustCompile("^[a-zA-Z-]+: .*|^[a-zA-Z-]+ #.*|^" + BreakingChangeFooterKey + ": .*")
scanner := bufio.NewScanner(strings.NewReader(message))
lines := 0

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import (
"github.com/Masterminds/semver/v3"
)
func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
func TestBaseReleaseNoteProcessor_Create(t *testing.T) {
date := time.Now()
tests := []struct {
@ -16,7 +16,7 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
version *semver.Version
tag string
date time.Time
commits []GitCommitLog
commits []CommitLog
want ReleaseNote
}{
{
@ -24,13 +24,15 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
version: semver.MustParse("1.0.0"),
tag: "v1.0.0",
date: date,
commits: []GitCommitLog{commitlog("t1", map[string]string{}, "a")},
want: releaseNote(
commits: []CommitLog{TestCommitlog("t1", map[string]string{}, "a")},
want: TestReleaseNote(
semver.MustParse("1.0.0"),
"v1.0.0",
date,
[]ReleaseNoteSection{
newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")}),
TestNewReleaseNoteCommitsSection(
"Tag 1", []string{"t1"}, []CommitLog{TestCommitlog("t1", map[string]string{}, "a")},
),
},
map[string]struct{}{"a": {}},
),
@ -40,13 +42,17 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
version: semver.MustParse("1.0.0"),
tag: "v1.0.0",
date: date,
commits: []GitCommitLog{commitlog("t1", map[string]string{}, "a"), commitlog("unmapped", map[string]string{}, "a")},
want: releaseNote(
commits: []CommitLog{
TestCommitlog("t1", map[string]string{}, "a"), TestCommitlog("unmapped", map[string]string{}, "a"),
},
want: TestReleaseNote(
semver.MustParse("1.0.0"),
"v1.0.0",
date,
[]ReleaseNoteSection{
newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")}),
TestNewReleaseNoteCommitsSection(
"Tag 1", []string{"t1"}, []CommitLog{TestCommitlog("t1", map[string]string{}, "a")},
),
},
map[string]struct{}{"a": {}},
),
@ -56,16 +62,18 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
version: semver.MustParse("1.0.0"),
tag: "v1.0.0",
date: date,
commits: []GitCommitLog{
commitlog("t1", map[string]string{}, "a"),
commitlog("unmapped", map[string]string{"breaking-change": "breaks"}, "a"),
commits: []CommitLog{
TestCommitlog("t1", map[string]string{}, "a"),
TestCommitlog("unmapped", map[string]string{"breaking-change": "breaks"}, "a"),
},
want: releaseNote(
want: TestReleaseNote(
semver.MustParse("1.0.0"),
"v1.0.0",
date,
[]ReleaseNoteSection{
newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{commitlog("t1", map[string]string{}, "a")}),
TestNewReleaseNoteCommitsSection(
"Tag 1", []string{"t1"}, []CommitLog{TestCommitlog("t1", map[string]string{}, "a")},
),
ReleaseNoteBreakingChangeSection{Name: "Breaking Changes", Messages: []string{"breaks"}},
},
map[string]struct{}{"a": {}},
@ -76,20 +84,20 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
version: semver.MustParse("1.0.0"),
tag: "v1.0.0",
date: date,
commits: []GitCommitLog{
commitlog("t1", map[string]string{}, "author3"),
commitlog("t1", map[string]string{}, "author2"),
commitlog("t1", map[string]string{}, "author1"),
commits: []CommitLog{
TestCommitlog("t1", map[string]string{}, "author3"),
TestCommitlog("t1", map[string]string{}, "author2"),
TestCommitlog("t1", map[string]string{}, "author1"),
},
want: releaseNote(
want: TestReleaseNote(
semver.MustParse("1.0.0"),
"v1.0.0",
date,
[]ReleaseNoteSection{
newReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []GitCommitLog{
commitlog("t1", map[string]string{}, "author3"),
commitlog("t1", map[string]string{}, "author2"),
commitlog("t1", map[string]string{}, "author1"),
TestNewReleaseNoteCommitsSection("Tag 1", []string{"t1"}, []CommitLog{
TestCommitlog("t1", map[string]string{}, "author3"),
TestCommitlog("t1", map[string]string{}, "author2"),
TestCommitlog("t1", map[string]string{}, "author1"),
}),
},
map[string]struct{}{"author1": {}, "author2": {}, "author3": {}},
@ -107,7 +115,7 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
},
})
if got := p.Create(tt.version, tt.tag, tt.date, tt.commits); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ReleaseNoteProcessorImpl.Create() = %v, want %v", got, tt.want)
t.Errorf("BaseReleaseNoteProcessor.Create() = %v, want %v", got, tt.want)
}
})
}

View File

@ -6,19 +6,19 @@ import (
"github.com/Masterminds/semver/v3"
)
func version(v string) *semver.Version {
func TestVersion(v string) *semver.Version {
r, _ := semver.NewVersion(v)
return r
}
func commitlog(ctype string, metadata map[string]string, author string) GitCommitLog {
func TestCommitlog(ctype string, metadata map[string]string, author string) CommitLog {
breaking := false
if _, found := metadata[breakingChangeMetadataKey]; found {
if _, found := metadata[BreakingChangeMetadataKey]; found {
breaking = true
}
return GitCommitLog{
return CommitLog{
Message: CommitMessage{
Type: ctype,
Description: "subject text",
@ -29,7 +29,7 @@ func commitlog(ctype string, metadata map[string]string, author string) GitCommi
}
}
func releaseNote(
func TestReleaseNote(
version *semver.Version,
tag string,
date time.Time,
@ -45,7 +45,7 @@ func releaseNote(
}
}
func newReleaseNoteCommitsSection(name string, types []string, items []GitCommitLog) ReleaseNoteCommitsSection {
func TestNewReleaseNoteCommitsSection(name string, types []string, items []CommitLog) ReleaseNoteCommitsSection {
return ReleaseNoteCommitsSection{
Name: name,
Types: types,

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 }}
{{- if (eq $section.SectionType "commits") }}
{{- 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)
}
})
}
}