Repository: gembaadvantage/uplift
Pull request #24 (2021-07-02 05:11:04)

feat: configurable commit message now supported

codecov-commenter: # [Codecov](https://codecov.io/gh/gembaadvantage/uplift/pull/24?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage) Report
> Merging [#24](https://codecov.io/gh/gembaadvantage/uplift/pull/24?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage) (3a905c7) into [main](https://codecov.io/gh/gembaadvantage/uplift/commit/7fa0592e445a41739928413a75deff16b2cb3305?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage) (7fa0592) will **increase** coverage by `0.47%`.
> The diff coverage is `86.95%`.

[![Impacted file tree graph](https://codecov.io/gh/gembaadvantage/uplift/pull/24/graphs/tree.svg?width=650&height=150&src=pr&token=K13FIQH5ZB&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage)](https://codecov.io/gh/gembaadvantage/uplift/pull/24?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage)

```diff
@@            Coverage Diff             @@
##             main      #24      +/-   ##
==========================================
+ Coverage   82.07%   82.55%   +0.47%     
==========================================
  Files           4        5       +1     
  Lines         212      235      +23     
==========================================
+ Hits          174      194      +20     
- Misses         27       29       +2     
- Partials       11       12       +1     
```


| [Impacted Files](https://codecov.io/gh/gembaadvantage/uplift/pull/24?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage) | Coverage Δ | |
|---|---|---|
| [internal/config/uplift.go](https://codecov.io/gh/gembaadvantage/uplift/pull/24/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage#diff-aW50ZXJuYWwvY29uZmlnL3VwbGlmdC5nbw==) | `80.00% <ø> (ø)` | |
| [internal/git/utils.go](https://codecov.io/gh/gembaadvantage/uplift/pull/24/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage#diff-aW50ZXJuYWwvZ2l0L3V0aWxzLmdv) | `66.10% <66.66%> (-1.14%)` | :arrow_down: |
| [internal/semver/bump.go](https://codecov.io/gh/gembaadvantage/uplift/pull/24/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage#diff-aW50ZXJuYWwvc2VtdmVyL2J1bXAuZ28=) | `87.80% <94.11%> (+1.31%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/gembaadvantage/uplift/pull/24?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/gembaadvantage/uplift/pull/24?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage). Last update [7fa0592...3a905c7](https://codecov.io/gh/gembaadvantage/uplift/pull/24?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gembaadvantage).


Patch:
diff --git a/README.md b/README.md
index acad1b7..10963ac 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,9 @@ $ uplift bump --dry-run --verbose
 ✅ commit contains a bump prefix, increment identified as 'Minor'
 ℹ️ existing version found: 0.1.0
 ✅ bumped version to: 0.2.0
+ℹ️  Any commits will use:
+joe.bloggs <joe.bloggs@gmail.com>
+chore(release): a custom message
 ℹ️ no files to bump, skipping!
 0.2.0
 ```
@@ -57,23 +60,45 @@ $ uplift bump --dry-run --verbose
 Uplift can be configured through the existance of an optional `.uplift.yml` configuration file in the root of your repository. Other supported variations are: `.uplift.yaml`, `uplift.yml` and `uplift.yaml`.
 
 ```yaml
+# An initial version that will be used for the first tag in your repository.
+# Tags with a 'v' prefix are supported. You cannot change format after the first tag
+# Defaults to 0.1.0
 firstVersion: v1.0.0
+
+# Changes the commit message when bumping files
+# Defaults to chore(release): release managed by uplift
+commitMessage: "chore: a custom commit message"
+
+# Changes the commit author when bumping files
+commitAuthor:
+  # Name of the author
+  # Defaults to the author name within the last commit
+  name: "joe.bloggs"
+
+  # Email of the author
+  # Defaults to the author email within the last commit
+  email: "joe.bloggs@gmail.com"
+
+# A list of files whose version numbers should be bumped and kept in sync with the
+# latest calculated repository tag.
+# Defaults to an empty list
 bumps:
-  - file: ./chart/my-chart/Chart.yaml
+  - # The path of the file relative to where uplift is executed
+    file: ./chart/my-chart/Chart.yaml
+
+    # A regex for matching a version within the file
     regex: "version: $VERSION"
+
+    # If the matched version in the file should be replaced with a semantic version.
+    # This will strip any 'v' prefix if needed
+    # Defaults to false
     semver: true
+
+    # The number of times any matched version should be replaced
+    # Defaults to 0, which replaces all matches
     count: 1
 ```
 
-| Option       | Required | Default         | Description                                                                                            |
-| ------------ | -------- | --------------- | ------------------------------------------------------------------------------------------------------ |
-| firstVersion | No       | 0.1.0           | An initial version for the first tag against the repository                                            |
-| bumps        | No       | []              | A list of files whose version numbers should be bumped and kept in sync with the latest repository tag |
-| bumps.file   | Yes      | -               | The path of the file, relative to the root of the repository                                           |
-| bumps.regex  | Yes      | -               | A regex used to identify a version within the file that should be bumped                               |
-| bumps.semver | No       | false           | The version in the file must be a semantic version, ensuring any `v` prefix is removed                 |
-| bumps.count  | No       | 0 (All Matches) | The number of times the matching version should be bumped within the file                              |
-
 ### $VERSION
 
 **`$VERSION`** is a placeholder and will match any semantic version, including a version with an optional `v` prefix.
diff --git a/internal/config/uplift.go b/internal/config/uplift.go
index 6319d30..02936b9 100644
--- a/internal/config/uplift.go
+++ b/internal/config/uplift.go
@@ -31,8 +31,10 @@ import (
 
 // Uplift defines the root configuration of the application
 type Uplift struct {
-	FirstVersion string `yaml:"firstVersion"`
-	Bumps        []Bump `yaml:"bumps"`
+	FirstVersion  string       `yaml:"firstVersion"`
+	Bumps         []Bump       `yaml:"bumps"`
+	CommitMessage string       `yaml:"commitMessage"`
+	CommitAuthor  CommitAuthor `yaml:"commitAuthor"`
 }
 
 // Bump defines configuration for bumping indvidual files based
@@ -44,6 +46,12 @@ type Bump struct {
 	SemVer bool   `yaml:"semver"`
 }
 
+// CommitAuthor defines configuration about the author of a git commit
+type CommitAuthor struct {
+	Name  string `yaml:"name"`
+	Email string `yaml:"email"`
+}
+
 // Load the YAML config file
 func Load(f string) (Uplift, error) {
 	fh, err := os.Open(f)
diff --git a/internal/git/utils.go b/internal/git/utils.go
index 29e4086..bc3fc91 100644
--- a/internal/git/utils.go
+++ b/internal/git/utils.go
@@ -29,13 +29,18 @@ import (
 	"strings"
 )
 
-// CommitMetadata contains metadata about a specific git commit
-type CommitMetadata struct {
+// CommitDetails contains mandatory details about a specific git commit
+type CommitDetails struct {
 	Message string
 	Author  string
 	Email   string
 }
 
+// String prints out a user friendly string representation
+func (c CommitDetails) String() string {
+	return fmt.Sprintf("%s <%s>\n%s", c.Author, c.Email, c.Message)
+}
+
 // Run executes a git command and returns its output or errors
 func Run(args ...string) (string, error) {
 	var cmd = exec.Command("git", args...)
@@ -70,10 +75,10 @@ func LatestTag() string {
 }
 
 // LatestCommit retrieves the latest commit within the repository
-func LatestCommit() (CommitMetadata, error) {
+func LatestCommit() (CommitDetails, error) {
 	out, err := Clean(Run("log", "-1", `--pretty=format:'"%an","%ae","%B"'`))
 	if err != nil {
-		return CommitMetadata{}, err
+		return CommitDetails{}, err
 	}
 
 	// Split the formatted string into its component parts
@@ -84,7 +89,7 @@ func LatestCommit() (CommitMetadata, error) {
 	email := p[1][1 : len(p[1])-1]
 	msg := p[2][1 : len(p[2])-1]
 
-	return CommitMetadata{
+	return CommitDetails{
 		Author: author,
 		Email:  email,
 		// Strip trailing newline
@@ -112,15 +117,15 @@ func Tag(tag string) error {
 
 // Commit will generate a commit against the repository and push it to the origin.
 // The commit will be associated with the provided author and email address
-func Commit(author, email, commit string) error {
+func Commit(cd CommitDetails) error {
 	args := []string{
 		"-c",
-		fmt.Sprintf("user.name='%s'", author),
+		fmt.Sprintf("user.name='%s'", cd.Author),
 		"-c",
-		fmt.Sprintf("user.email='%s'", email),
+		fmt.Sprintf("user.email='%s'", cd.Email),
 		"commit",
 		"-m",
-		commit,
+		cd.Message,
 	}
 
 	if _, err := Clean(Run(args...)); err != nil {
diff --git a/internal/semver/bump.go b/internal/semver/bump.go
index aa5cc07..f1c3c06 100644
--- a/internal/semver/bump.go
+++ b/internal/semver/bump.go
@@ -100,14 +100,14 @@ func (b Bumper) Bump() error {
 
 	b.logger.Success("git repo found")
 
-	meta, err := git.LatestCommit()
+	commit, err := git.LatestCommit()
 	if err != nil {
 		b.logger.Warn("no commits found in repository")
 		return err
 	}
-	b.logger.Success("retrieved latest commit:\n'%s'", meta.Message)
+	b.logger.Success("retrieved latest commit:\n'%s'", commit.Message)
 
-	inc := ParseCommit(meta.Message)
+	inc := ParseCommit(commit.Message)
 	if inc == NoIncrement {
 		b.logger.Warn("commit doesn't contain a bump prefix, skipping!")
 		return nil
@@ -124,7 +124,10 @@ func (b Bumper) Bump() error {
 		}
 	}
 
-	if err := b.bumpFiles(ver, meta); err != nil {
+	// Ensure any files that are bumped are associated with the expected commit
+	commit = b.buildCommit(commit)
+
+	if err := b.bumpFiles(ver, commit); err != nil {
 		return err
 	}
 
@@ -177,7 +180,7 @@ func (b Bumper) bumpVersion(v string, inc Increment) (string, error) {
 	return bv, nil
 }
 
-func (b Bumper) bumpFiles(v string, meta git.CommitMetadata) error {
+func (b Bumper) bumpFiles(v string, commit git.CommitDetails) error {
 	if len(b.config.Bumps) == 0 {
 		b.logger.Info("no files to bump, skipping!")
 		return nil
@@ -217,13 +220,36 @@ func (b Bumper) bumpFiles(v string, meta git.CommitMetadata) error {
 		return nil
 	}
 
-	if err := git.Commit(meta.Author, meta.Email, "chore(release): release managed by uplift"); err != nil {
+	if err := git.Commit(commit); err != nil {
 		return err
 	}
 
 	return git.Push()
 }
 
+func (b Bumper) buildCommit(commit git.CommitDetails) git.CommitDetails {
+	c := git.CommitDetails{
+		Author:  commit.Author,
+		Email:   commit.Email,
+		Message: "chore(release): release managed by uplift",
+	}
+
+	if b.config.CommitAuthor.Name != "" {
+		c.Author = b.config.CommitAuthor.Name
+	}
+
+	if b.config.CommitAuthor.Email != "" {
+		c.Email = b.config.CommitAuthor.Email
+	}
+
+	if b.config.CommitMessage != "" {
+		c.Message = b.config.CommitMessage
+	}
+
+	b.logger.Info("Any commits will use:\n%s", c)
+	return c
+}
+
 func (b Bumper) bumpFile(path string, bump FileBump) (bool, error) {
 	data, err := ioutil.ReadFile(path)
 	if err != nil {


Test patch:
diff --git a/internal/config/uplift_test.go b/internal/config/uplift_test.go
new file mode 100644
index 0000000..9f4a04b
--- /dev/null
+++ b/internal/config/uplift_test.go
@@ -0,0 +1,72 @@
+/*
+Copyright (c) 2021 Gemba Advantage
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+package config
+
+import (
+	"io/ioutil"
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestLoadMissingFile(t *testing.T) {
+	_, err := Load("missing_file.yml")
+	require.Error(t, err)
+}
+
+func TestLoadUnsupportedYaml(t *testing.T) {
+	path := WriteFile(t, `
+unrecognised_field: ""`)
+
+	_, err := Load(path)
+	require.Error(t, err)
+}
+
+func TestLoadInvalidYaml(t *testing.T) {
+	path := WriteFile(t, `
+doc: [`)
+
+	_, err := Load(path)
+	require.Error(t, err)
+}
+
+func WriteFile(t *testing.T, s string) string {
+	t.Helper()
+
+	current, err := os.Getwd()
+	require.NoError(t, err)
+
+	file, err := ioutil.TempFile(current, "*")
+	require.NoError(t, err)
+
+	_, err = file.WriteString(s)
+	require.NoError(t, err)
+	require.NoError(t, file.Close())
+
+	t.Cleanup(func() {
+		require.NoError(t, os.Remove(file.Name()))
+	})
+
+	return file.Name()
+}
diff --git a/internal/git/utils_test.go b/internal/git/utils_test.go
index a22f3dd..03f11af 100644
--- a/internal/git/utils_test.go
+++ b/internal/git/utils_test.go
@@ -125,12 +125,18 @@ func TestCommit(t *testing.T) {
 	InitRepo(t)
 	StagedFile(t)
 
-	err := Commit("john.doe", "john.doe@gmail.com", "first commit")
+	cmt := CommitDetails{
+		Author:  "joe.bloggs",
+		Email:   "joe.bloggs@gmail.com",
+		Message: "first commit",
+	}
+
+	err := Commit(cmt)
 	require.NoError(t, err)
 
 	out, err := Clean(Run("log", "-1", `--pretty=format:'%an:%ae:%B'`))
 	require.NoError(t, err)
-	assert.Equal(t, "john.doe:john.doe@gmail.com:first commit", out)
+	assert.Equal(t, "joe.bloggs:joe.bloggs@gmail.com:first commit", out)
 }
 
 func UnstagedFile(t *testing.T) string {
diff --git a/internal/semver/bump_test.go b/internal/semver/bump_test.go
index 71d5c2e..88bb160 100644
--- a/internal/semver/bump_test.go
+++ b/internal/semver/bump_test.go
@@ -385,6 +385,40 @@ func TestBumpFileDefaultCommitMessage(t *testing.T) {
 	assert.Equal(t, "chore(release): release managed by uplift", commit.Message)
 }
 
+func TestBumpFileWithCustomisedCommit(t *testing.T) {
+	MkRepo(t, "0.1.0", "fix: Lorem ipsum dolor sit amet")
+
+	file := "version: 0.1.0"
+	path := WriteFile(t, file)
+
+	opts := BumpOptions{
+		Config: config.Uplift{
+			Bumps: []config.Bump{
+				{
+					File:  path,
+					Regex: "version: $VERSION",
+				},
+			},
+			CommitMessage: "chore: Lorem ipsum dolor sit amet",
+			CommitAuthor: config.CommitAuthor{
+				Name:  "joe.bloggs",
+				Email: "joe.bloggs@gmail.com",
+			},
+		},
+	}
+
+	b := NewBumper(io.Discard, opts)
+	err := b.Bump()
+	require.NoError(t, err)
+
+	commit, err := git.LatestCommit()
+	require.NoError(t, err)
+
+	assert.Equal(t, "joe.bloggs", commit.Author)
+	assert.Equal(t, "joe.bloggs@gmail.com", commit.Email)
+	assert.Equal(t, "chore: Lorem ipsum dolor sit amet", commit.Message)
+}
+
 func TestBumpFileFirstTagMatchesVersionInFile(t *testing.T) {
 	git.InitRepo(t)
 	git.EmptyCommit(t, "feat: Lorem ipsum dolor sit amet")