Skip to content

Commit

Permalink
feat: include GPG support (#92)
Browse files Browse the repository at this point in the history
  • Loading branch information
purpleclay authored May 15, 2023
1 parent 0d00716 commit 1a02e03
Show file tree
Hide file tree
Showing 12 changed files with 574 additions and 46 deletions.
14 changes: 14 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ tasks:
cmds:
- go mod tidy

test:
desc: Run all of the tests
cmds:
- task: unit-test
- task: docker-test

unit-test:
desc: Run the unit tests
vars:
Expand All @@ -47,6 +53,14 @@ tasks:
cmds:
- go test {{.TEST_OPTIONS}} -covermode=atomic -coverprofile={{.COVER_PROFILE}} {{.TEST_FORMAT}} ./...

docker-test:
desc: Run the docker-based tests
cmds:
- docker build . -t gitz-test:latest -f testdata/Dockerfile --build-arg test=tagsigned
- docker build . -t gitz-test:latest -f testdata/Dockerfile --build-arg test=tagsigningkey
- docker build . -t gitz-test:latest -f testdata/Dockerfile --build-arg test=commitsigned
- docker build . -t gitz-test:latest -f testdata/Dockerfile --build-arg test=commitsigningkey

lint:
desc: Lint the code using golangci-lint
vars:
Expand Down
131 changes: 127 additions & 4 deletions commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,133 @@ SOFTWARE.

package git

import "fmt"
import (
"fmt"
"strings"
)

// CommitOption provides a way for setting specific options during a commit
// operation. Each supported option can customize the way the commit is
// created against the current repository (working directory)
type CommitOption func(*commitOptions)

type commitOptions struct {
AllowEmpty bool
ForceNoSigned bool
Signed bool
SigningKey string
}

// WithAllowEmpty allows a commit to be created without having to track
// any changes. This bypasses the default protection by git, preventing
// a commit from having the exact same tree as its parent
func WithAllowEmpty() CommitOption {
return func(opts *commitOptions) {
opts.AllowEmpty = true
}
}

// WithGpgSign will create a GPG-signed commit using the GPG key associated
// with the committers email address. Overriding this behavior is possible
// through the user.signingkey config setting. This option does not need
// to be explicitly called if the commit.gpgSign config setting is set to
// true
func WithGpgSign() CommitOption {
return func(opts *commitOptions) {
opts.Signed = true
}
}

// WithGpgSigningKey will create a GPG-signed commit using the provided GPG
// key ID, overridding any default GPG key set by the user.signingKey git
// config setting
func WithGpgSigningKey(key string) CommitOption {
return func(opts *commitOptions) {
opts.Signed = true
opts.SigningKey = strings.TrimSpace(key)
}
}

// WithNoGpgSign ensures the created commit will not be GPG signed
// regardless of the value assigned to the repositories commit.gpgSign
// git config setting
func WithNoGpgSign() CommitOption {
return func(opts *commitOptions) {
opts.ForceNoSigned = true
}
}

// Commit a snapshot of changes within the current repository (working directory)
// and describe those changes with a given log message
func (c *Client) Commit(msg string) (string, error) {
return c.exec(fmt.Sprintf("git commit -m '%s'", msg))
// and describe those changes with a given log message. Commit behavior can be
// customized through the use of options
func (c *Client) Commit(msg string, opts ...CommitOption) (string, error) {
options := &commitOptions{}
for _, opt := range opts {
opt(options)
}

var commitCmd strings.Builder
commitCmd.WriteString("git commit")

if options.AllowEmpty {
commitCmd.WriteString(" --allow-empty")
}

if options.Signed {
commitCmd.WriteString(" -S")
}

if options.SigningKey != "" {
commitCmd.WriteString(" --gpg-sign=" + options.SigningKey)
}

if options.ForceNoSigned {
commitCmd.WriteString(" --no-gpg-sign")
}

commitCmd.WriteString(fmt.Sprintf(" -m '%s'", msg))
return c.exec(commitCmd.String())
}

const (
authorPrefix = "author "
committerPrefix = "committer "
emailEnd = '>'
)

// CommitVerification contains details about a GPG signed commit
type CommitVerification struct {
Sha string
Author Author
Committer Author
Fingerprint string
SignedBy *Author
}

// VerifyCommit validates that a given commit has a valid GPG signature
// and returns details about that signature
func (c *Client) VerifyCommit(sha string) (*CommitVerification, error) {
out, err := c.exec("git verify-commit -v " + sha)
if err != nil {
return nil, err
}

author := chompUntil(out[strings.Index(out, authorPrefix)+len(authorPrefix):], emailEnd)
committer := chompUntil(out[strings.Index(out, committerPrefix)+len(committerPrefix):], emailEnd)
fingerprint := chompCRLF(out[strings.Index(out, fingerprintPrefix)+len(fingerprintPrefix):])

var signedByAuthor *Author
if strings.Contains(out, signedByPrefix) {
signedBy := chompUntil(out[strings.Index(out, signedByPrefix)+len(signedByPrefix):], '"')
author := parseAuthor(signedBy)
signedByAuthor = &author
}

return &CommitVerification{
Sha: sha,
Author: parseAuthor(author),
Committer: parseAuthor(committer),
Fingerprint: fingerprint,
SignedBy: signedByAuthor,
}, nil
}
19 changes: 13 additions & 6 deletions commit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,22 @@ func TestCommit(t *testing.T) {
assert.Equal(t, lastCommit.Message, "this is an example commit message")
}

func TestCommitCleanWorkingTreeError(t *testing.T) {
func TestCommitWithAllowEmpty(t *testing.T) {
gittest.InitRepository(t)

client, _ := git.NewClient()
_, err := client.Commit("this is an example commit message")
_, err := client.Commit("this will be a an empty commit", git.WithAllowEmpty())

require.NoError(t, err)
}

var errGit git.ErrGitExecCommand
require.ErrorAs(t, err, &errGit)
func TestCommitWithNoGpgSign(t *testing.T) {
gittest.InitRepository(t, gittest.WithFiles("test.txt"))
gittest.ConfigSet(t, "user.signingkey", "DOES-NOT-EXIST", "commit.gpgsign", "true")
gittest.StageFile(t, "test.txt")

assert.Equal(t, "git commit -m 'this is an example commit message'", errGit.Cmd)
assert.Contains(t, errGit.Out, "nothing to commit, working tree clean")
client, _ := git.NewClient()
_, err := client.Commit("this will be a regular commit", git.WithNoGpgSign())

require.NoError(t, err)
}
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/term v0.3.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/term v0.5.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
14 changes: 9 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ 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/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand All @@ -18,12 +21,13 @@ github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
Loading

0 comments on commit 1a02e03

Please sign in to comment.