diff --git a/README.md b/README.md index 2fae482..4bab651 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ Commands like `commit-log` and `commit-notes` has a range option. Supported rang By default, it's used [--date=short](https://git-scm.com/docs/git-log#Documentation/git-log.txt---dateltformatgt) at `git log`, all dates returned from it will be in `YYYY-MM-DD` format. -Range `tag` will use `git describe` to get the last tag available if `start` is empty, the others types won't use the existing tags. It's recommended to always use a start limit in a old repository with a lot of commits. This behavior was maintained to not break the retrocompatibility. +Range `tag` will use `git for-each-ref refs/tags` to get the last tag available if `start` is empty, the others types won't use the existing tags. It's recommended to always use a start limit in a old repository with a lot of commits. This behavior was maintained to not break the retrocompatibility. Range `date` use git log `--since` and `--until`. It's possible to use all supported formats from [git log](https://git-scm.com/docs/git-log#Documentation/git-log.txt---sinceltdategt). If `end` is in `YYYY-MM-DD` format, `sv` will add a day on git log command to make the end date inclusive. diff --git a/cmd/git-sv/handlers.go b/cmd/git-sv/handlers.go index e1d062d..a93a00b 100644 --- a/cmd/git-sv/handlers.go +++ b/cmd/git-sv/handlers.go @@ -41,11 +41,11 @@ func configShowHandler(cfg Config) func(c *cli.Context) error { func currentVersionHandler(git sv.Git) func(c *cli.Context) error { return func(c *cli.Context) error { - describe := git.Describe() + lastTag := git.LastTag() - currentVer, err := sv.ToVersion(describe) + currentVer, err := sv.ToVersion(lastTag) if err != nil { - return err + return fmt.Errorf("error parsing version: %s from git tag, message: %v", lastTag, err) } fmt.Printf("%d.%d.%d\n", currentVer.Major(), currentVer.Minor(), currentVer.Patch()) return nil @@ -54,19 +54,19 @@ func currentVersionHandler(git sv.Git) func(c *cli.Context) error { func nextVersionHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) func(c *cli.Context) error { return func(c *cli.Context) error { - describe := git.Describe() + lastTag := git.LastTag() - currentVer, err := sv.ToVersion(describe) + currentVer, err := sv.ToVersion(lastTag) if err != nil { - return fmt.Errorf("error parsing version: %s from describe, message: %v", describe, err) + return fmt.Errorf("error parsing version: %s from git tag, message: %v", lastTag, err) } - commits, err := git.Log(sv.NewLogRange(sv.TagRange, describe, "")) + commits, err := git.Log(sv.NewLogRange(sv.TagRange, lastTag, "")) if err != nil { return fmt.Errorf("error getting git log, message: %v", err) } - nextVer := semverProcessor.NextVersion(currentVer, commits) + nextVer, _ := semverProcessor.NextVersion(currentVer, commits) fmt.Printf("%d.%d.%d\n", nextVer.Major(), nextVer.Minor(), nextVer.Patch()) return nil } @@ -119,7 +119,7 @@ func getTagCommits(git sv.Git, tag string) ([]sv.GitCommitLog, error) { 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.Describe()), endFlag), nil + 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): @@ -164,7 +164,8 @@ func releaseNotesHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, if tag := c.String("t"); tag != "" { rnVersion, date, commits, err = getTagVersionInfo(git, semverProcessor, tag) } else { - rnVersion, date, commits, err = getNextVersionInfo(git, semverProcessor) + // TODO: should generate release notes if version was not updated? + rnVersion, _, date, commits, err = getNextVersionInfo(git, semverProcessor) } if err != nil { @@ -223,37 +224,38 @@ func find(tag string, tags []sv.GitTag) int { return -1 } -func getNextVersionInfo(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) (semver.Version, time.Time, []sv.GitCommitLog, error) { - describe := git.Describe() +func getNextVersionInfo(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) (semver.Version, bool, time.Time, []sv.GitCommitLog, error) { + lastTag := git.LastTag() - currentVer, err := sv.ToVersion(describe) + currentVer, err := sv.ToVersion(lastTag) if err != nil { - return semver.Version{}, time.Time{}, nil, fmt.Errorf("error parsing version: %s from describe, message: %v", describe, err) + return semver.Version{}, false, time.Time{}, nil, fmt.Errorf("error parsing version: %s from git tag, message: %v", lastTag, err) } - commits, err := git.Log(sv.NewLogRange(sv.TagRange, describe, "")) + commits, err := git.Log(sv.NewLogRange(sv.TagRange, lastTag, "")) if err != nil { - return semver.Version{}, time.Time{}, nil, fmt.Errorf("error getting git log, message: %v", err) + return semver.Version{}, false, time.Time{}, nil, fmt.Errorf("error getting git log, message: %v", err) } - return semverProcessor.NextVersion(currentVer, commits), time.Now(), commits, nil + 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 { - describe := git.Describe() + lastTag := git.LastTag() - currentVer, err := sv.ToVersion(describe) + currentVer, err := sv.ToVersion(lastTag) if err != nil { - return fmt.Errorf("error parsing version: %s from describe, message: %v", describe, err) + return fmt.Errorf("error parsing version: %s from git tag, message: %v", lastTag, err) } - commits, err := git.Log(sv.NewLogRange(sv.TagRange, describe, "")) + commits, err := git.Log(sv.NewLogRange(sv.TagRange, lastTag, "")) if err != nil { return fmt.Errorf("error getting git log, message: %v", err) } - nextVer := semverProcessor.NextVersion(currentVer, commits) + nextVer, _ := semverProcessor.NextVersion(currentVer, commits) fmt.Printf("%d.%d.%d\n", nextVer.Major(), nextVer.Minor(), nextVer.Patch()) if err := git.Tag(nextVer); err != nil { @@ -344,6 +346,17 @@ func changelogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnP size := c.Int("size") all := c.Bool("all") + addNextVersion := c.Bool("add-next-version") + + 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 @@ -361,7 +374,7 @@ func changelogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnP currentVer, err := sv.ToVersion(tag.Name) if err != nil { - return fmt.Errorf("error parsing version: %s from describe, message: %v", tag.Name, err) + return fmt.Errorf("error parsing version: %s from git tag, message: %v", tag.Name, err) } releaseNotes = append(releaseNotes, rnProcessor.Create(¤tVer, tag.Date, commits)) } diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go index 75d69b5..57ffa62 100644 --- a/cmd/git-sv/main.go +++ b/cmd/git-sv/main.go @@ -128,6 +128,7 @@ func main() { Flags: []cli.Flag{ &cli.IntFlag{Name: "size", Value: 10, 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)"}, }, }, { diff --git a/sv/git.go b/sv/git.go index 5c5b662..dde3242 100644 --- a/sv/git.go +++ b/sv/git.go @@ -20,7 +20,7 @@ const ( // Git commands type Git interface { - Describe() string + LastTag() string Log(lr LogRange) ([]GitCommitLog, error) Commit(header, body, footer string) error Tag(version semver.Version) error @@ -78,9 +78,9 @@ func NewGit(messageProcessor MessageProcessor, cfg TagConfig) *GitImpl { } } -// Describe runs git describe, it no tag found, return empty -func (GitImpl) Describe() string { - cmd := exec.Command("git", "describe", "--abbrev=0", "--tags") +// LastTag get last tag, if no tag found, return empty +func (GitImpl) LastTag() string { + cmd := exec.Command("git", "for-each-ref", "refs/tags", "--sort", "-creatordate", "--format", "%(refname:short)", "--count", "1") out, err := cmd.CombinedOutput() if err != nil { return "" diff --git a/sv/semver.go b/sv/semver.go index c3648e1..06d76d1 100644 --- a/sv/semver.go +++ b/sv/semver.go @@ -26,7 +26,7 @@ func ToVersion(value string) (semver.Version, error) { // SemVerCommitsProcessor interface type SemVerCommitsProcessor interface { - NextVersion(version semver.Version, commits []GitCommitLog) semver.Version + NextVersion(version semver.Version, commits []GitCommitLog) (semver.Version, bool) } // SemVerCommitsProcessorImpl process versions using commit log @@ -50,7 +50,7 @@ func NewSemVerCommitsProcessor(vcfg VersioningConfig, mcfg CommitMessageConfig) } // NextVersion calculates next version based on commit log -func (p SemVerCommitsProcessorImpl) NextVersion(version semver.Version, commits []GitCommitLog) semver.Version { +func (p SemVerCommitsProcessorImpl) NextVersion(version semver.Version, commits []GitCommitLog) (semver.Version, bool) { var versionToUpdate = none for _, commit := range commits { if v := p.versionTypeToUpdate(commit); v > versionToUpdate { @@ -60,13 +60,13 @@ func (p SemVerCommitsProcessorImpl) NextVersion(version semver.Version, commits switch versionToUpdate { case major: - return version.IncMajor() + return version.IncMajor(), true case minor: - return version.IncMinor() + return version.IncMinor(), true case patch: - return version.IncPatch() + return version.IncPatch(), true default: - return version + return version, false } } diff --git a/sv/semver_test.go b/sv/semver_test.go index c1a2678..b84c52e 100644 --- a/sv/semver_test.go +++ b/sv/semver_test.go @@ -14,21 +14,26 @@ func TestSemVerCommitsProcessorImpl_NextVersion(t *testing.T) { version semver.Version commits []GitCommitLog want semver.Version + wantUpdated bool }{ - {"no update", true, version("0.0.0"), []GitCommitLog{}, version("0.0.0")}, - {"no update on unknown type", true, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{})}, version("0.0.0")}, - {"no update on unmapped known type", false, version("0.0.0"), []GitCommitLog{commitlog("none", map[string]string{})}, version("0.0.0")}, - {"update patch on unknown type", false, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{})}, version("0.0.1")}, - {"patch update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{})}, version("0.0.1")}, - {"minor update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("minor", map[string]string{})}, version("0.1.0")}, - {"major update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("major", map[string]string{})}, version("1.0.0")}, - {"breaking change update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("patch", map[string]string{"breaking-change": "break"})}, version("1.0.0")}, + {"no update", true, version("0.0.0"), []GitCommitLog{}, version("0.0.0"), false}, + {"no update on unknown type", true, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{})}, version("0.0.0"), false}, + {"no update on unmapped known type", false, version("0.0.0"), []GitCommitLog{commitlog("none", map[string]string{})}, version("0.0.0"), false}, + {"update patch on unknown type", false, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{})}, version("0.0.1"), true}, + {"patch update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{})}, version("0.0.1"), true}, + {"minor update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("minor", map[string]string{})}, version("0.1.0"), true}, + {"major update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("major", map[string]string{})}, version("1.0.0"), true}, + {"breaking change update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("patch", map[string]string{"breaking-change": "break"})}, version("1.0.0"), true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := NewSemVerCommitsProcessor(VersioningConfig{UpdateMajor: []string{"major"}, UpdateMinor: []string{"minor"}, UpdatePatch: []string{"patch"}, IgnoreUnknown: tt.ignoreUnknown}, CommitMessageConfig{Types: []string{"major", "minor", "patch", "none"}}) - if got := p.NextVersion(tt.version, tt.commits); !reflect.DeepEqual(got, tt.want) { - t.Errorf("SemVerCommitsProcessorImpl.NextVersion() = %v, want %v", got, tt.want) + got, gotUpdated := p.NextVersion(tt.version, tt.commits) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SemVerCommitsProcessorImpl.NextVersion() Version = %v, want %v", got, tt.want) + } + if tt.wantUpdated != gotUpdated { + t.Errorf("SemVerCommitsProcessorImpl.NextVersion() Updated = %v, want %v", gotUpdated, tt.wantUpdated) } }) }