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