@@ -270,19 +270,34 @@ func (repo *GitRepo) StoreTree(entries []TreeEntry) (git.Hash, error) {
// StoreCommit will store a Git commit with the given Git tree
func (repo *GitRepo) StoreCommit(treeHash git.Hash) (git.Hash, error) {
- stdout, err := repo.runGitCommand("commit-tree", string(treeHash))
-
- if err != nil {
- return "", err
- }
-
- return git.Hash(stdout), nil
+ return repo.storeCommitRaw(treeHash)
}
// StoreCommitWithParent will store a Git commit with the given Git tree
func (repo *GitRepo) StoreCommitWithParent(treeHash git.Hash, parent git.Hash) (git.Hash, error) {
- stdout, err := repo.runGitCommand("commit-tree", string(treeHash),
- "-p", string(parent))
+ return repo.storeCommitRaw(treeHash, "-p", string(parent))
+}
+
+func (repo *GitRepo) storeCommitRaw(treeHash git.Hash, extraArgs ...string) (git.Hash, error) {
+ args := []string{"commit-tree"}
+
+ // `git commit-tree` uses user.signingkey and gpg.program, but not commit.gpgsign.
+ // We read commit.gpgsign ourselves and simply pass -S to `git commit-tree`.
+ config := repo.LocalConfig()
+ gpgsign, err := config.ReadBool("commit.gpgsign")
+ if err != nil && err != ErrNoConfigEntry {
+ // There are more than one entries, or some other error.
+ return "", errors.Wrap(err, "failed to read local commit.gpgsign")
+ }
+ if gpgsign {
+ args = append(args, "-S")
+ }
+
+ args = append(args, extraArgs...)
+
+ args = append(args, string(treeHash))
+
+ stdout, err := repo.runGitCommand(args...)
if err != nil {
return "", err
@@ -2,6 +2,7 @@
package repository
import (
+ "fmt"
"testing"
"github.com/stretchr/testify/assert"
@@ -65,3 +66,34 @@ func TestConfig(t *testing.T) {
err = repo.LocalConfig().RemoveAll("section.key")
assert.Error(t, err)
}
+
+// checkStoreCommit creates a commit and checks if it has been signed.
+// See https://git-scm.com/docs/git-log#Documentation/git-log.txt-emGem
+// for possible signature status values.
+func checkStoreCommit(t *testing.T, repo *GitRepo, expectedSignedStatus string) {
+ content := fmt.Sprintf("file content %d", repo.CreateTime())
+ blobHash, err := repo.StoreData([]byte(content))
+ assert.NoError(t, err)
+
+ var entries = []TreeEntry{{Blob, blobHash, "filename"}}
+ treeHash, err := repo.StoreTree(entries)
+ assert.NoError(t, err)
+ commitHash, err := repo.StoreCommit(treeHash)
+ assert.NoError(t, err)
+
+ signedStatus, err := repo.runGitCommand("log", "--pretty=%G?", commitHash.String())
+ assert.NoError(t, err)
+ assert.Equal(t, expectedSignedStatus, signedStatus)
+}
+
+func TestGitRepo_StoreCommit(t *testing.T) {
+ repo := CreateTestRepo(false)
+ defer CleanupTestRepos(t, repo)
+
+ // Commit and expect no signature.
+ checkStoreCommit(t,repo, "N")
+
+ // Commit and expect a good signature with unknown validity.
+ setupSigningKey(t, repo)
+ checkStoreCommit(t, repo, "U")
+}
@@ -1,11 +1,17 @@
package repository
import (
+ "fmt"
"io/ioutil"
"log"
"os"
+ "os/exec"
"strings"
"testing"
+
+ "github.com/stretchr/testify/require"
+ "golang.org/x/crypto/openpgp"
+ "golang.org/x/crypto/openpgp/armor"
)
// This is intended for testing only
@@ -42,6 +48,94 @@ func CreateTestRepo(bare bool) *GitRepo {
return repo
}
+// setupSigningKey creates a GPG key and sets up the local config so it's used.
+// The key id is set as "user.signingkey". For the key to be found, a `gpg`
+// wrapper which uses only a custom keyring is created and set as "gpg.program".
+// Finally "commit.gpgsign" is set to true so the signing takes place.
+func setupSigningKey(t *testing.T, repo *GitRepo) {
+ config := repo.LocalConfig()
+
+ // Generate a key pair for signing commits.
+ entity, err := openpgp.NewEntity("First Last", "", "fl@example.org", nil)
+ require.NoError(t, err)
+
+ err = config.StoreString("user.signingkey", entity.PrivateKey.KeyIdString())
+ require.NoError(t, err)
+
+ // Armor the private part.
+ privBuilder := &strings.Builder{}
+ w, err := armor.Encode(privBuilder, openpgp.PrivateKeyType, nil)
+ require.NoError(t, err)
+ err = entity.SerializePrivate(w, nil)
+ require.NoError(t, err)
+ err = w.Close()
+ require.NoError(t, err)
+ armoredPriv := privBuilder.String()
+
+ // Armor the public part.
+ pubBuilder := &strings.Builder{}
+ w, err = armor.Encode(pubBuilder, openpgp.PublicKeyType, nil)
+ require.NoError(t, err)
+ err = entity.Serialize(w)
+ require.NoError(t, err)
+ err = w.Close()
+ require.NoError(t, err)
+ armoredPub := pubBuilder.String()
+
+ // Create a custom gpg keyring to be used when creating commits with `git`.
+ keyring, err := ioutil.TempFile("", "keyring")
+ require.NoError(t, err)
+
+ // Import the armored private key to the custom keyring.
+ priv, err := ioutil.TempFile("", "privkey")
+ require.NoError(t, err)
+ _, err = fmt.Fprint(priv, armoredPriv)
+ require.NoError(t, err)
+ err = priv.Close()
+ require.NoError(t, err)
+ err = exec.Command("gpg", "--no-default-keyring", "--keyring", keyring.Name(), "--import", priv.Name()).Run()
+ require.NoError(t, err)
+
+ // Import the armored public key to the custom keyring.
+ pub, err := ioutil.TempFile("", "pubkey")
+ require.NoError(t, err)
+ _, err = fmt.Fprint(pub, armoredPub)
+ require.NoError(t, err)
+ err = pub.Close()
+ require.NoError(t, err)
+ err = exec.Command("gpg", "--no-default-keyring", "--keyring", keyring.Name(), "--import", pub.Name()).Run()
+ require.NoError(t, err)
+
+ // Use a gpg wrapper to use a custom keyring containing GPGKeyID.
+ gpgWrapper := createGPGWrapper(t, keyring.Name())
+ if err := config.StoreString("gpg.program", gpgWrapper); err != nil {
+ log.Fatal("failed to set gpg.program for test repository: ", err)
+ }
+
+ if err := config.StoreString("commit.gpgsign", "true"); err != nil {
+ log.Fatal("failed to set commit.gpgsign for test repository: ", err)
+ }
+}
+
+// createGPGWrapper creates a shell script running gpg with a specific keyring.
+func createGPGWrapper(t *testing.T, keyringPath string) string {
+ file, err := ioutil.TempFile("", "gpgwrapper")
+ require.NoError(t, err)
+
+ _, err = fmt.Fprintf(file, `#!/bin/sh
+exec gpg --no-default-keyring --keyring="%s" "$@"
+`, keyringPath)
+ require.NoError(t, err)
+
+ err = file.Close()
+ require.NoError(t, err)
+
+ err = os.Chmod(file.Name(), os.FileMode(0700))
+ require.NoError(t, err)
+
+ return file.Name()
+}
+
func CleanupTestRepos(t testing.TB, repos ...Repo) {
var firstErr error
for _, repo := range repos {