From 8acbdc5a88c0822e7ad387af2be83bd701335a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Le=20Meur?= Date: Sat, 12 Aug 2023 16:48:08 +0200 Subject: [PATCH] feat: allow retrieving commits headline messages from `-commit-headlines` CLI flag --- README.md | 8 ++- main.go | 12 ++-- pkg/strategy/semantic/semantic.go | 77 +++++++++++++++++++------- pkg/strategy/semantic/semantic_test.go | 42 ++++++++++++++ 4 files changed, 115 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 9c744a9f..8bb31df9 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Just run `jx-release-version` in your project's top directory, and it should jus It accepts the following CLI flags: - `-dir`: the location on the filesystem of your project's top directory - default to the current working directory. - `-previous-version`: the [strategy to use to read the previous version](#reading-the-previous-version). Can also be set using the `PREVIOUS_VERSION` environment variable. Default to `auto`. +- `-commit-headlines`: the [commit headlines to use to generate the next semantic version](#pass-commit-headlines). Can also be set using the `COMMIT_HEADLINES` environment variable. Default to ``. - `-next-version`: the [strategy to use to calculate the next version](#calculating—the-next-version). Can also be set using the `NEXT_VERSION` environment variable. Default to `auto`. - `-output-format`: the [output format of the next release version](#output-format). Can also be set using the `OUTPUT_FORMAT` environment variable. Default to `{{.Major}}.{{.Minor}}.{{.Patch}}`. - `-tag`: if enabled, [a new tag will be created](#tag). Can also be set using the `TAG` environment variable with the `"TRUE"` value. @@ -111,13 +112,18 @@ The `semantic` strategy finds all commits between the previous version's git tag - at least 1 commit with a `feat:` prefix, then it will bump the minor component of the version - otherwise it will bump the patch component of the version -Note that if it can't find a tag for the previous version, it will fail. +Note that if it can't find a tag for the previous version, it will fail, except if you use the `-commit-headlines` flags to generate semantic next version from a single/multiline string instead of repository commits/tags. **Usage**: - `jx-release-version -next-version=semantic` - if you want to strip any prerelease information from the build before performing the version bump you can use: - `jx-release-version -next-version=semantic:strip-prerelease` +#### Pass commit headlines +If you want to retrieve a semantic version without using tags or commits from a repository, you can manually set the previous version and the commit headlines to use: + - `jx-release-version -previous-version=1.2.3 -commit-headlines="feat: a feature"` + + ### From file The `from-file` strategy will read the next version from a file. Supported formats are: diff --git a/main.go b/main.go index e27d22cf..df9f97ca 100644 --- a/main.go +++ b/main.go @@ -32,6 +32,7 @@ var ( debug bool dir string previousVersion string + commitHeadlines string nextVersion string outputFormat string tag bool @@ -47,6 +48,7 @@ func init() { wd, _ := os.Getwd() flag.StringVar(&options.dir, "dir", wd, "The directory that contains the git repository. Default to the current working directory.") flag.StringVar(&options.previousVersion, "previous-version", getEnvWithDefault("PREVIOUS_VERSION", "auto"), "The strategy to detect the previous version: auto, from-tag, from-file or manual. Default to the PREVIOUS_VERSION env var.") + flag.StringVar(&options.commitHeadlines, "commit-headlines", getEnvWithDefault("COMMIT_HEADLINES", ""), "The commit headline(s) to use for semantic next version instead of the commit()s of a repository. Default to empty.") flag.StringVar(&options.nextVersion, "next-version", getEnvWithDefault("NEXT_VERSION", "auto"), "The strategy to calculate the next version: auto, semantic, from-file, increment or manual. Default to the NEXT_VERSION env var.") flag.StringVar(&options.outputFormat, "output-format", getEnvWithDefault("OUTPUT_FORMAT", "{{.Major}}.{{.Minor}}.{{.Patch}}"), "The output format of the next version. Default to the OUTPUT_FORMAT env var.") flag.BoolVar(&options.debug, "debug", os.Getenv("JX_LOG_LEVEL") == "debug", "Print debug logs. Enabled by default if the JX_LOG_LEVEL env var is set to 'debug'.") @@ -167,14 +169,16 @@ func versionBumper() strategy.VersionBumper { case "auto", "": versionBumper = auto.Strategy{ SemanticStrategy: semantic.Strategy{ - Dir: options.dir, - StripPrerelease: strings.Contains(strategyArg, "strip-prerelease"), + Dir: options.dir, + StripPrerelease: strings.Contains(strategyArg, "strip-prerelease"), + CommitHeadlinesString: options.commitHeadlines, }, } case "semantic": versionBumper = semantic.Strategy{ - Dir: options.dir, - StripPrerelease: strings.Contains(strategyArg, "strip-prerelease"), + Dir: options.dir, + StripPrerelease: strings.Contains(strategyArg, "strip-prerelease"), + CommitHeadlinesString: options.commitHeadlines, } case "from-file": versionBumper = fromfile.Strategy{ diff --git a/pkg/strategy/semantic/semantic.go b/pkg/strategy/semantic/semantic.go index 18d24f0c..312f84af 100644 --- a/pkg/strategy/semantic/semantic.go +++ b/pkg/strategy/semantic/semantic.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "regexp" "github.com/Masterminds/semver/v3" "github.com/go-git/go-git/v5" @@ -19,35 +20,45 @@ var ( ) type Strategy struct { - Dir string - StripPrerelease bool + Dir string + StripPrerelease bool + CommitHeadlinesString string } func (s Strategy) BumpVersion(previous semver.Version) (*semver.Version, error) { var ( - dir = s.Dir - err error + dir = s.Dir + err error + commitHeadlinesString = s.CommitHeadlinesString + summary *conventionalCommitsSummary ) - if dir == "" { - dir, err = os.Getwd() + if commitHeadlinesString != "" { + summary, err = s.parseCommitHeadlines(commitHeadlinesString) if err != nil { - return nil, fmt.Errorf("failed to get current working directory: %w", err) + return nil, err + } + } else { + if dir == "" { + dir, err = os.Getwd() + if err != nil { + return nil, fmt.Errorf("failed to get current working directory: %w", err) + } } - } - repo, err := git.PlainOpen(dir) - if err != nil { - return nil, fmt.Errorf("failed to open git repository at %q: %w", dir, err) - } + repo, err := git.PlainOpen(dir) + if err != nil { + return nil, fmt.Errorf("failed to open git repository at %q: %w", dir, err) + } - tagCommit, err := s.extractTagCommit(repo, previous.String()) - if err != nil { - return nil, err - } + tagCommit, err := s.extractTagCommit(repo, previous.String()) + if err != nil { + return nil, err + } - summary, err := s.parseCommitsSince(repo, tagCommit) - if err != nil { - return nil, err + summary, err = s.parseCommitsSince(repo, tagCommit) + if err != nil { + return nil, err + } } if s.StripPrerelease { @@ -170,3 +181,31 @@ func (s Strategy) parseCommitsSince(repo *git.Repository, firstCommit *object.Co log.Logger().Debugf("Summary of conventional commits since %s: %#v", firstCommit.Committer.When, summary) return &summary, nil } + +func (s Strategy) parseCommitHeadlines(commitHeadlinesString string) (*conventionalCommitsSummary, error) { + summary := conventionalCommitsSummary{ + types: map[string]bool{}, + } + + log.Logger().Debugf("Iterating over all commits headline passed as a string") + + commitHeadlines := regexp.MustCompile("\r?\n").Split(commitHeadlinesString, -1) + + for index, commitHeadline := range commitHeadlines { + log.Logger().Debugf("Parsing commit headline number %d with message %s", index, commitHeadline) + c, err := cc.Parse(commitHeadline) + if err != nil { + log.Logger().WithError(err).Debugf("Skipping non-conventional commit headline number %d", index) + continue + } + + summary.conventionalCommitsCount++ + summary.types[c.Header.Type] = true + if len(c.BreakingMessage()) > 0 { + summary.breakingChanges = true + } + } + + log.Logger().Debugf("Summary of conventional commits: %#v", summary) + return &summary, nil +} diff --git a/pkg/strategy/semantic/semantic_test.go b/pkg/strategy/semantic/semantic_test.go index 1b838c2a..5191e9b6 100644 --- a/pkg/strategy/semantic/semantic_test.go +++ b/pkg/strategy/semantic/semantic_test.go @@ -36,6 +36,48 @@ func TestBumpVersion(t *testing.T) { previous: *semver.MustParse("1.1.0"), expected: semver.MustParse("2.0.0"), }, + { + name: "feat from commit headline", + strategy: Strategy{ + CommitHeadlinesString: "feat: a feature", + }, + previous: *semver.MustParse("2.0.0"), + expected: semver.MustParse("2.1.0"), + }, + { + name: "feat from commit headlines", + strategy: Strategy{ + CommitHeadlinesString: `chore: a chore +feat: a feature`, + }, + previous: *semver.MustParse("2.0.0"), + expected: semver.MustParse("2.1.0"), + }, + { + name: "breaking change from commit headline", + strategy: Strategy{ + CommitHeadlinesString: "feat!: a breaking feature", + }, + previous: *semver.MustParse("1.1.0"), + expected: semver.MustParse("2.0.0"), + }, + { + name: "breaking change from commit headlines", + strategy: Strategy{ + CommitHeadlinesString: `chore: a chore +feat!: a breaking feature`, + }, + previous: *semver.MustParse("1.1.0"), + expected: semver.MustParse("2.0.0"), + }, + { + name: "patch from unrecognized commit headline", + strategy: Strategy{ + CommitHeadlinesString: "nothing", + }, + previous: *semver.MustParse("1.1.0"), + expected: semver.MustParse("1.1.1"), + }, } // the git repo is stored as a tar.gz archive to make it easy to commit