mirror of
https://github.com/thegeeklab/git-sv.git
synced 2024-11-12 15:00:39 +00:00
refactor: rework packages and interfaces (#3)
This commit is contained in:
parent
c06748910e
commit
db1691ada4
@ -3,7 +3,6 @@ YAML
|
||||
.gitsv
|
||||
cli
|
||||
getsection
|
||||
timefmt
|
||||
cfg
|
||||
json
|
||||
changelog
|
||||
|
16
README.md
16
README.md
@ -168,22 +168,6 @@ Each `ReleaseNoteSection` will be configured according with `release-notes.secti
|
||||
|
||||
> :warning: currently only `commits` and `breaking-changes` are supported as `section-types`, using a different value for this field will make the section to be removed from the template variables.
|
||||
|
||||
##### Functions
|
||||
|
||||
Beside the [go template functions](https://pkg.go.dev/text/template#hdr-Functions), the following functions are available to use in the templates. Check [formatter_functions.go](sv/formatter_functions.go) to see the functions implementation.
|
||||
|
||||
###### timefmt
|
||||
|
||||
**Usage:** timefmt time "2006-01-02"
|
||||
|
||||
Receive a time.Time and a layout string and returns a textual representation of the time according with the layout provided. Check <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:
|
||||
|
@ -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],
|
@ -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
98
app/commands/changelog.go
Normal 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
108
app/commands/commit.go
Normal 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
86
app/commands/commitlog.go
Normal 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
|
||||
}
|
||||
}
|
60
app/commands/commitnotes.go
Normal file
60
app/commands/commitnotes.go
Normal 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
37
app/commands/config.go
Normal 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
|
||||
}
|
||||
}
|
24
app/commands/currentversion.go
Normal file
24
app/commands/currentversion.go
Normal 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
|
||||
}
|
||||
}
|
31
app/commands/nextversion.go
Normal file
31
app/commands/nextversion.go
Normal 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
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"errors"
|
55
app/commands/releasenotes.go
Normal file
55
app/commands/releasenotes.go
Normal 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
36
app/commands/tag.go
Normal 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
220
app/commands/utils.go
Normal 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
|
||||
}
|
85
app/commands/validatecommitmessage.go
Normal file
85
app/commands/validatecommitmessage.go
Normal 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
|
||||
}
|
||||
}
|
@ -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"`
|
||||
}
|
@ -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,
|
||||
},
|
@ -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
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func warnf(format string, values ...interface{}) {
|
||||
fmt.Fprintf(os.Stderr, "WARN: "+format+"\n", values...)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
14
go.mod
@ -5,19 +5,33 @@ go 1.19
|
||||
require (
|
||||
dario.cat/mergo v1.0.0
|
||||
github.com/Masterminds/semver/v3 v3.2.1
|
||||
github.com/Masterminds/sprig/v3 v3.2.3
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
github.com/manifoldco/promptui v0.9.0
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/urfave/cli/v2 v2.25.7
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
|
||||
github.com/google/uuid v1.1.1 // indirect
|
||||
github.com/huandu/xstrings v1.3.3 // indirect
|
||||
github.com/imdario/mergo v0.3.11 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mitchellh/copystructure v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
golang.org/x/crypto v0.3.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
)
|
||||
|
74
go.sum
74
go.sum
@ -1,7 +1,12 @@
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
@ -11,9 +16,20 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
|
||||
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
|
||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
@ -25,22 +41,80 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
|
||||
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
@ -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
|
||||
}
|
@ -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) {
|
100
sv/config.go
100
sv/config.go
@ -1,100 +0,0 @@
|
||||
package sv
|
||||
|
||||
// ==== Message ====
|
||||
|
||||
// CommitMessageConfig config a commit message.
|
||||
type CommitMessageConfig struct {
|
||||
Types []string `yaml:"types,flow"`
|
||||
HeaderSelector string `yaml:"header-selector"`
|
||||
Scope CommitMessageScopeConfig `yaml:"scope"`
|
||||
Footer map[string]CommitMessageFooterConfig `yaml:"footer"`
|
||||
Issue CommitMessageIssueConfig `yaml:"issue"`
|
||||
}
|
||||
|
||||
// IssueFooterConfig config for issue.
|
||||
func (c CommitMessageConfig) IssueFooterConfig() CommitMessageFooterConfig {
|
||||
if v, exists := c.Footer[issueMetadataKey]; exists {
|
||||
return v
|
||||
}
|
||||
|
||||
return CommitMessageFooterConfig{}
|
||||
}
|
||||
|
||||
// CommitMessageScopeConfig config scope preferences.
|
||||
type CommitMessageScopeConfig struct {
|
||||
Values []string `yaml:"values"`
|
||||
}
|
||||
|
||||
// CommitMessageFooterConfig config footer metadata.
|
||||
type CommitMessageFooterConfig struct {
|
||||
Key string `yaml:"key"`
|
||||
KeySynonyms []string `yaml:"key-synonyms,flow"`
|
||||
UseHash bool `yaml:"use-hash"`
|
||||
AddValuePrefix string `yaml:"add-value-prefix"`
|
||||
}
|
||||
|
||||
// CommitMessageIssueConfig issue preferences.
|
||||
type CommitMessageIssueConfig struct {
|
||||
Regex string `yaml:"regex"`
|
||||
}
|
||||
|
||||
// ==== Branches ====
|
||||
|
||||
// BranchesConfig branches preferences.
|
||||
type BranchesConfig struct {
|
||||
Prefix string `yaml:"prefix"`
|
||||
Suffix string `yaml:"suffix"`
|
||||
DisableIssue bool `yaml:"disable-issue"`
|
||||
Skip []string `yaml:"skip,flow"`
|
||||
SkipDetached *bool `yaml:"skip-detached"`
|
||||
}
|
||||
|
||||
// ==== Versioning ====
|
||||
|
||||
// VersioningConfig versioning preferences.
|
||||
type VersioningConfig struct {
|
||||
UpdateMajor []string `yaml:"update-major,flow"`
|
||||
UpdateMinor []string `yaml:"update-minor,flow"`
|
||||
UpdatePatch []string `yaml:"update-patch,flow"`
|
||||
IgnoreUnknown bool `yaml:"ignore-unknown"`
|
||||
}
|
||||
|
||||
// ==== Tag ====
|
||||
|
||||
// TagConfig tag preferences.
|
||||
type TagConfig struct {
|
||||
Pattern *string `yaml:"pattern"`
|
||||
Filter *string `yaml:"filter"`
|
||||
}
|
||||
|
||||
// ==== Release Notes ====
|
||||
|
||||
// ReleaseNotesConfig release notes preferences.
|
||||
type ReleaseNotesConfig struct {
|
||||
Headers map[string]string `yaml:"headers,omitempty"`
|
||||
Sections []ReleaseNotesSectionConfig `yaml:"sections"`
|
||||
}
|
||||
|
||||
func (cfg ReleaseNotesConfig) sectionConfig(sectionType string) *ReleaseNotesSectionConfig {
|
||||
for _, sectionCfg := range cfg.Sections {
|
||||
if sectionCfg.SectionType == sectionType {
|
||||
return §ionCfg
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReleaseNotesSectionConfig preferences for a single section on release notes.
|
||||
type ReleaseNotesSectionConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
SectionType string `yaml:"section-type"`
|
||||
CommitTypes []string `yaml:"commit-types,flow,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
// ReleaseNotesSectionTypeCommits ReleaseNotesSectionConfig.SectionType value.
|
||||
ReleaseNotesSectionTypeCommits = "commits"
|
||||
// ReleaseNotesSectionTypeBreakingChanges ReleaseNotesSectionConfig.SectionType value.
|
||||
ReleaseNotesSectionTypeBreakingChanges = "breaking-changes"
|
||||
)
|
11
sv/errors.go
11
sv/errors.go
@ -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")
|
||||
)
|
@ -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()
|
@ -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"}},
|
||||
},
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
114
sv/message.go
114
sv/message.go
@ -2,16 +2,24 @@ package sv
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
breakingChangeFooterKey = "BREAKING CHANGE"
|
||||
breakingChangeMetadataKey = "breaking-change"
|
||||
issueMetadataKey = "issue"
|
||||
messageRegexGroupName = "header"
|
||||
BreakingChangeFooterKey = "BREAKING CHANGE"
|
||||
BreakingChangeMetadataKey = "breaking-change"
|
||||
IssueMetadataKey = "issue"
|
||||
MessageRegexGroupName = "header"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidCommitMessage = errors.New("commit message not valid")
|
||||
errIssueIDNotFound = errors.New("could not find issue id using configured regex")
|
||||
errInvalidIssueRegex = errors.New("could not compile issue regex")
|
||||
errInvalidHeaderRegex = errors.New("invalid regex on header-selector")
|
||||
)
|
||||
|
||||
// CommitMessage is a message using conventional commits.
|
||||
@ -24,15 +32,59 @@ type CommitMessage struct {
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type CommitMessageConfig struct {
|
||||
Types []string `yaml:"types,flow"`
|
||||
HeaderSelector string `yaml:"header-selector"`
|
||||
Scope CommitMessageScopeConfig `yaml:"scope"`
|
||||
Footer map[string]CommitMessageFooterConfig `yaml:"footer"`
|
||||
Issue CommitMessageIssueConfig `yaml:"issue"`
|
||||
}
|
||||
|
||||
// IssueFooterConfig config for issue.
|
||||
func (c CommitMessageConfig) IssueFooterConfig() CommitMessageFooterConfig {
|
||||
if v, exists := c.Footer[IssueMetadataKey]; exists {
|
||||
return v
|
||||
}
|
||||
|
||||
return CommitMessageFooterConfig{}
|
||||
}
|
||||
|
||||
// CommitMessageScopeConfig config scope preferences.
|
||||
type CommitMessageScopeConfig struct {
|
||||
Values []string `yaml:"values"`
|
||||
}
|
||||
|
||||
// CommitMessageFooterConfig config footer metadata.
|
||||
type CommitMessageFooterConfig struct {
|
||||
Key string `yaml:"key"`
|
||||
KeySynonyms []string `yaml:"key-synonyms,flow"`
|
||||
UseHash bool `yaml:"use-hash"`
|
||||
AddValuePrefix string `yaml:"add-value-prefix"`
|
||||
}
|
||||
|
||||
// CommitMessageIssueConfig issue preferences.
|
||||
type CommitMessageIssueConfig struct {
|
||||
Regex string `yaml:"regex"`
|
||||
}
|
||||
|
||||
// BranchesConfig branches preferences.
|
||||
type BranchesConfig struct {
|
||||
Prefix string `yaml:"prefix"`
|
||||
Suffix string `yaml:"suffix"`
|
||||
DisableIssue bool `yaml:"disable-issue"`
|
||||
Skip []string `yaml:"skip,flow"`
|
||||
SkipDetached *bool `yaml:"skip-detached"`
|
||||
}
|
||||
|
||||
// NewCommitMessage commit message constructor.
|
||||
func NewCommitMessage(ctype, scope, description, body, issue, breakingChanges string) CommitMessage {
|
||||
metadata := make(map[string]string)
|
||||
if issue != "" {
|
||||
metadata[issueMetadataKey] = issue
|
||||
metadata[IssueMetadataKey] = issue
|
||||
}
|
||||
|
||||
if breakingChanges != "" {
|
||||
metadata[breakingChangeMetadataKey] = breakingChanges
|
||||
metadata[BreakingChangeMetadataKey] = breakingChanges
|
||||
}
|
||||
|
||||
return CommitMessage{
|
||||
@ -47,12 +99,12 @@ func NewCommitMessage(ctype, scope, description, body, issue, breakingChanges st
|
||||
|
||||
// Issue return issue from metadata.
|
||||
func (m CommitMessage) Issue() string {
|
||||
return m.Metadata[issueMetadataKey]
|
||||
return m.Metadata[IssueMetadataKey]
|
||||
}
|
||||
|
||||
// BreakingMessage return breaking change message from metadata.
|
||||
func (m CommitMessage) BreakingMessage() string {
|
||||
return m.Metadata[breakingChangeMetadataKey]
|
||||
return m.Metadata[BreakingChangeMetadataKey]
|
||||
}
|
||||
|
||||
// MessageProcessor interface.
|
||||
@ -68,28 +120,28 @@ type MessageProcessor interface {
|
||||
Parse(subject, body string) (CommitMessage, error)
|
||||
}
|
||||
|
||||
// NewMessageProcessor MessageProcessorImpl constructor.
|
||||
func NewMessageProcessor(mcfg CommitMessageConfig, bcfg BranchesConfig) *MessageProcessorImpl {
|
||||
return &MessageProcessorImpl{
|
||||
// NewMessageProcessor BaseMessageProcessor constructor.
|
||||
func NewMessageProcessor(mcfg CommitMessageConfig, bcfg BranchesConfig) *BaseMessageProcessor {
|
||||
return &BaseMessageProcessor{
|
||||
messageCfg: mcfg,
|
||||
branchesCfg: bcfg,
|
||||
}
|
||||
}
|
||||
|
||||
// MessageProcessorImpl process validate message hook.
|
||||
type MessageProcessorImpl struct {
|
||||
// BaseMessageProcessor process validate message hook.
|
||||
type BaseMessageProcessor struct {
|
||||
messageCfg CommitMessageConfig
|
||||
branchesCfg BranchesConfig
|
||||
}
|
||||
|
||||
// SkipBranch check if branch should be ignored.
|
||||
func (p MessageProcessorImpl) SkipBranch(branch string, detached bool) bool {
|
||||
func (p BaseMessageProcessor) SkipBranch(branch string, detached bool) bool {
|
||||
return contains(branch, p.branchesCfg.Skip) ||
|
||||
(p.branchesCfg.SkipDetached != nil && *p.branchesCfg.SkipDetached && detached)
|
||||
}
|
||||
|
||||
// Validate commit message.
|
||||
func (p MessageProcessorImpl) Validate(message string) error {
|
||||
func (p BaseMessageProcessor) Validate(message string) error {
|
||||
subject, body := splitCommitMessageContent(message)
|
||||
msg, parseErr := p.Parse(subject, body)
|
||||
|
||||
@ -113,7 +165,7 @@ func (p MessageProcessorImpl) Validate(message string) error {
|
||||
}
|
||||
|
||||
// ValidateType check if commit type is valid.
|
||||
func (p MessageProcessorImpl) ValidateType(ctype string) error {
|
||||
func (p BaseMessageProcessor) ValidateType(ctype string) error {
|
||||
if ctype == "" || !contains(ctype, p.messageCfg.Types) {
|
||||
return fmt.Errorf(
|
||||
"%w: type must be one of [%s]",
|
||||
@ -126,7 +178,7 @@ func (p MessageProcessorImpl) ValidateType(ctype string) error {
|
||||
}
|
||||
|
||||
// ValidateScope check if commit scope is valid.
|
||||
func (p MessageProcessorImpl) ValidateScope(scope string) error {
|
||||
func (p BaseMessageProcessor) ValidateScope(scope string) error {
|
||||
if len(p.messageCfg.Scope.Values) > 0 && !contains(scope, p.messageCfg.Scope.Values) {
|
||||
return fmt.Errorf(
|
||||
"%w: scope must one of [%s]",
|
||||
@ -139,7 +191,7 @@ func (p MessageProcessorImpl) ValidateScope(scope string) error {
|
||||
}
|
||||
|
||||
// ValidateDescription check if commit description is valid.
|
||||
func (p MessageProcessorImpl) ValidateDescription(description string) error {
|
||||
func (p BaseMessageProcessor) ValidateDescription(description string) error {
|
||||
if !regexp.MustCompile("^[a-z]+.*$").MatchString(description) {
|
||||
return fmt.Errorf("%w: description [%s] must start with lowercase", errInvalidCommitMessage, description)
|
||||
}
|
||||
@ -148,7 +200,7 @@ func (p MessageProcessorImpl) ValidateDescription(description string) error {
|
||||
}
|
||||
|
||||
// Enhance add metadata on commit message.
|
||||
func (p MessageProcessorImpl) Enhance(branch, message string) (string, error) {
|
||||
func (p BaseMessageProcessor) Enhance(branch, message string) (string, error) {
|
||||
if p.branchesCfg.DisableIssue || p.messageCfg.IssueFooterConfig().Key == "" ||
|
||||
hasIssueID(message, p.messageCfg.IssueFooterConfig()) {
|
||||
return "", nil // enhance disabled
|
||||
@ -184,7 +236,7 @@ func formatIssueFooter(cfg CommitMessageFooterConfig, issue string) string {
|
||||
}
|
||||
|
||||
// IssueID try to extract issue id from branch, return empty if not found.
|
||||
func (p MessageProcessorImpl) IssueID(branch string) (string, error) {
|
||||
func (p BaseMessageProcessor) IssueID(branch string) (string, error) {
|
||||
if p.branchesCfg.DisableIssue || p.messageCfg.Issue.Regex == "" {
|
||||
return "", nil
|
||||
}
|
||||
@ -205,7 +257,7 @@ func (p MessageProcessorImpl) IssueID(branch string) (string, error) {
|
||||
}
|
||||
|
||||
// Format a commit message returning header, body and footer.
|
||||
func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string) {
|
||||
func (p BaseMessageProcessor) Format(msg CommitMessage) (string, string, string) {
|
||||
var header strings.Builder
|
||||
|
||||
header.WriteString(msg.Type)
|
||||
@ -219,10 +271,10 @@ func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string)
|
||||
|
||||
var footer strings.Builder
|
||||
if msg.BreakingMessage() != "" {
|
||||
footer.WriteString(fmt.Sprintf("%s: %s", breakingChangeFooterKey, msg.BreakingMessage()))
|
||||
footer.WriteString(fmt.Sprintf("%s: %s", BreakingChangeFooterKey, msg.BreakingMessage()))
|
||||
}
|
||||
|
||||
if issue, exists := msg.Metadata[issueMetadataKey]; exists && p.messageCfg.IssueFooterConfig().Key != "" {
|
||||
if issue, exists := msg.Metadata[IssueMetadataKey]; exists && p.messageCfg.IssueFooterConfig().Key != "" {
|
||||
if footer.Len() > 0 {
|
||||
footer.WriteString("\n")
|
||||
}
|
||||
@ -238,7 +290,7 @@ func removeCarriage(commit string) string {
|
||||
}
|
||||
|
||||
// Parse a commit message.
|
||||
func (p MessageProcessorImpl) Parse(subject, body string) (CommitMessage, error) {
|
||||
func (p BaseMessageProcessor) Parse(subject, body string) (CommitMessage, error) {
|
||||
preparedSubject, err := p.prepareHeader(subject)
|
||||
commitBody := removeCarriage(body)
|
||||
|
||||
@ -263,8 +315,8 @@ func (p MessageProcessorImpl) Parse(subject, body string) (CommitMessage, error)
|
||||
}
|
||||
}
|
||||
|
||||
if tagValue := extractFooterMetadata(breakingChangeFooterKey, commitBody, false); tagValue != "" {
|
||||
metadata[breakingChangeMetadataKey] = tagValue
|
||||
if tagValue := extractFooterMetadata(BreakingChangeFooterKey, commitBody, false); tagValue != "" {
|
||||
metadata[BreakingChangeMetadataKey] = tagValue
|
||||
hasBreakingChange = true
|
||||
}
|
||||
|
||||
@ -278,7 +330,7 @@ func (p MessageProcessorImpl) Parse(subject, body string) (CommitMessage, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p MessageProcessorImpl) prepareHeader(header string) (string, error) {
|
||||
func (p BaseMessageProcessor) prepareHeader(header string) (string, error) {
|
||||
if p.messageCfg.HeaderSelector == "" {
|
||||
return header, nil
|
||||
}
|
||||
@ -288,9 +340,9 @@ func (p MessageProcessorImpl) prepareHeader(header string) (string, error) {
|
||||
return "", fmt.Errorf("%w: %s: %s", errInvalidHeaderRegex, p.messageCfg.HeaderSelector, err.Error())
|
||||
}
|
||||
|
||||
index := regex.SubexpIndex(messageRegexGroupName)
|
||||
index := regex.SubexpIndex(MessageRegexGroupName)
|
||||
if index < 0 {
|
||||
return "", fmt.Errorf("%w: could not find group %s", errInvalidHeaderRegex, messageRegexGroupName)
|
||||
return "", fmt.Errorf("%w: could not find group %s", errInvalidHeaderRegex, MessageRegexGroupName)
|
||||
}
|
||||
|
||||
match := regex.FindStringSubmatch(header)
|
||||
@ -299,7 +351,7 @@ func (p MessageProcessorImpl) prepareHeader(header string) (string, error) {
|
||||
return "", fmt.Errorf(
|
||||
"%w: could not find group %s in match result for '%s'",
|
||||
errInvalidHeaderRegex,
|
||||
messageRegexGroupName,
|
||||
MessageRegexGroupName,
|
||||
header,
|
||||
)
|
||||
}
|
||||
@ -335,7 +387,7 @@ func extractFooterMetadata(key, text string, useHash bool) string {
|
||||
}
|
||||
|
||||
func hasFooter(message string) bool {
|
||||
r := regexp.MustCompile("^[a-zA-Z-]+: .*|^[a-zA-Z-]+ #.*|^" + breakingChangeFooterKey + ": .*")
|
||||
r := regexp.MustCompile("^[a-zA-Z-]+: .*|^[a-zA-Z-]+ #.*|^" + BreakingChangeFooterKey + ": .*")
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(message))
|
||||
lines := 0
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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 §ionCfg
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReleaseNotesSectionConfig preferences for a single section on release notes.
|
||||
type ReleaseNotesSectionConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
SectionType string `yaml:"section-type"`
|
||||
CommitTypes []string `yaml:"commit-types,flow,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
// ReleaseNotesSectionTypeCommits ReleaseNotesSectionConfig.SectionType value.
|
||||
ReleaseNotesSectionTypeCommits = "commits"
|
||||
// ReleaseNotesSectionTypeBreakingChanges ReleaseNotesSectionConfig.SectionType value.
|
||||
ReleaseNotesSectionTypeBreakingChanges = "breaking-changes"
|
||||
)
|
||||
|
||||
// ReleaseNoteProcessor release note processor interface.
|
||||
type ReleaseNoteProcessor interface {
|
||||
Create(version *semver.Version, tag string, date time.Time, commits []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.
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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,
|
@ -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
78
templates/templates.go
Normal 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
|
||||
}
|
80
templates/templates_test.go
Normal file
80
templates/templates_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user