gittuf/pkg/gitinterface/commit_test.go
Paulo Gomes 9e269f46fb
Introduce gitstore.Storer and make gittuf's core dependency-light
Restructure storage so gittuf verification can run over backends other
than the git binary (e.g. go-git):

- pkg/githash: concrete Git object hash, stdlib-only.
  gitinterface.Hash aliases it.
- pkg/gitstore: the single Storer interface (24 methods) that all
  storage consumers program against, plus the shared
  ErrReferenceNotFound sentinel. *gitinterface.Repository satisfies it
  structurally (compile-time asserted). Also defines ConfigKey, the
  canonical type for the Git config settings gittuf reads.
- pkg/rsl (from internal/rsl): entry model, codec, and readers over
  gitstore.Storer; zero gitinterface/sigstore dependencies. rsl.Hash
  aliases githash.Hash; nil is the unset-Hash sentinel and IsZero
  matches nil and empty as well as both format zeros (no
  object-format-unaware ZeroHash). Entry commits (empty tree on the
  RSL ref) are owned by the package; no storer adapter.
- internal/signerverifier/gitobject: verifies commit/tag signatures
  over (payload, signature) bytes, Rekor URL as an option. The storage
  half is Repository.GetObjectSignature. Removes sigstore, cosign, and
  gitsign from gitinterface's dependency tree.
- internal/propagation: propagation workflow, moved off pkg/rsl's
  public API (its tuf directive types are internal).
- internal/{attestations,cache,policy}: storage via gitstore.Storer;
  tree writing via WriteTree(blobs, subtrees).

Breaking changes to pkg/gitinterface: Repository.VerifySignature and
the verification sentinels are removed (use gitobject.Verify);
ErrReferenceNotFound now aliases gitstore's. Repository.GetGitConfig
(which returned the whole config map) is replaced by
LookupConfig(gitstore.ConfigKey), returning a single setting's value.
Policy resolves the Rekor override from git config once per
verification and extracts signed payloads once per object instead of
per key attempt.

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Paulo Gomes <paulo@entire.io>
2026-08-03 21:27:49 +01:00

879 lines
26 KiB
Go

// Copyright The gittuf Authors
// SPDX-License-Identifier: Apache-2.0
package gitinterface
import (
"bytes"
"io"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/gittuf/gittuf/internal/signerverifier/gitobject"
"github.com/gittuf/gittuf/internal/signerverifier/gpg"
"github.com/gittuf/gittuf/internal/signerverifier/ssh"
artifacts "github.com/gittuf/gittuf/internal/testartifacts"
"github.com/go-git/go-git/v6/plumbing"
"github.com/go-git/go-git/v6/plumbing/object"
"github.com/secure-systems-lab/go-securesystemslib/signerverifier"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRepositoryCommitSHA256(t *testing.T) {
tempDir := t.TempDir()
repo := CreateTestGitRepository(t, tempDir, false, WithSHA256Format())
refName := "refs/heads/main"
treeBuilder := NewTreeBuilder(repo)
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
if err != nil {
t.Fatal(err)
}
// Commit to a ref that does not yet exist: the "current" tip is the zero
// hash, which must match the repository's SHA-256 object format.
commitID, err := repo.Commit(emptyTreeID, refName, "Initial commit\n", false)
assert.Nil(t, err)
assert.Len(t, commitID.String(), 64)
refHead, err := repo.GetReference(refName)
require.Nil(t, err)
assert.Equal(t, commitID, refHead)
}
func TestRepositoryCommit(t *testing.T) {
tempDir := t.TempDir()
repo := CreateTestGitRepository(t, tempDir, false)
refName := "refs/heads/main"
treeBuilder := NewTreeBuilder(repo)
// Write empty tree
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
if err != nil {
t.Fatal(err)
}
// Write second tree
blobID, err := repo.WriteBlob([]byte("Hello, world!\n"))
if err != nil {
t.Fatal(err)
}
treeWithContentsID, err := treeBuilder.WriteTreeFromEntries([]TreeEntry{NewEntryBlob("README.md", blobID)})
if err != nil {
t.Fatal(err)
}
// Create initial commit with no tree
expectedInitialCommitID := "648c569f3958b899e832f04750de52cf5d0db2fa"
commitID, err := repo.Commit(emptyTreeID, refName, "Initial commit\n", false)
assert.Nil(t, err)
assert.Equal(t, expectedInitialCommitID, commitID.String())
refHead, err := repo.GetReference(refName)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, expectedInitialCommitID, refHead.String())
// Create second commit with tree
expectedSecondCommitID := "3d7200c158ccfedf35a68a7d24842d60cac4ec0d"
commitID, err = repo.Commit(treeWithContentsID, refName, "Add README\n", false)
assert.Nil(t, err)
assert.Equal(t, expectedSecondCommitID, commitID.String())
refHead, err = repo.GetReference(refName)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, expectedSecondCommitID, refHead.String())
// Create third commit with same tree but sign this time
expectedThirdCommitID := "eed43c23f781ddc10359ce25e0fc486a000a8c9f"
commitID, err = repo.Commit(treeWithContentsID, refName, "Signing this commit\n", true)
assert.Nil(t, err)
assert.Equal(t, expectedThirdCommitID, commitID.String())
refHead, err = repo.GetReference(refName)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, expectedThirdCommitID, refHead.String())
}
func TestRepositoryCommitUsingSpecificKey(t *testing.T) {
t.Run("success", func(t *testing.T) {
tempDir := t.TempDir()
repo := CreateTestGitRepository(t, tempDir, false)
refName := "refs/heads/main"
treeBuilder := NewTreeBuilder(repo)
// Write empty tree
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
if err != nil {
t.Fatal(err)
}
// Write second tree
blobID, err := repo.WriteBlob([]byte("Hello, world!\n"))
if err != nil {
t.Fatal(err)
}
treeWithContentsID, err := treeBuilder.WriteTreeFromEntries([]TreeEntry{NewEntryBlob("README.md", blobID)})
if err != nil {
t.Fatal(err)
}
// Create initial commit with no tree
expectedInitialCommitID := "b218890d607cdcea53ebf6c640748b4b1c8015ca"
commitID, err := repo.CommitUsingSpecificKey(emptyTreeID, refName, "Initial commit\n", artifacts.SSHED25519Private)
assert.Nil(t, err)
assert.Equal(t, expectedInitialCommitID, commitID.String())
refHead, err := repo.GetReference(refName)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, expectedInitialCommitID, refHead.String())
// Create second commit with tree
expectedSecondCommitID := "2b3f8b1f6af0d0d3c37130ba4d054ff4c2e95a3a"
commitID, err = repo.CommitUsingSpecificKey(treeWithContentsID, refName, "Add README\n", artifacts.SSHED25519Private)
assert.Nil(t, err)
assert.Equal(t, expectedSecondCommitID, commitID.String())
refHead, err = repo.GetReference(refName)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, expectedSecondCommitID, refHead.String())
})
t.Run("invalid key", func(t *testing.T) {
tempDir := t.TempDir()
repo := CreateTestGitRepository(t, tempDir, false)
refName := "refs/heads/main"
treeBuilder := NewTreeBuilder(repo)
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
if err != nil {
t.Fatal(err)
}
unknownKey := []byte("-----BEGIN UNKNOWN KEY-----\nYWJj\n-----END UNKNOWN KEY-----\n")
_, err = repo.CommitUsingSpecificKey(emptyTreeID, refName, "Initial commit\n", unknownKey)
assert.ErrorIs(t, err, ErrUnknownSigningMethod)
})
}
func TestCommitUsingSpecificKey(t *testing.T) {
tempDir := t.TempDir()
repo := CreateTestGitRepository(t, tempDir, false)
refName := "refs/heads/main"
treeBuilder := NewTreeBuilder(repo)
// Write empty tree
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
if err != nil {
t.Fatal(err)
}
// Write second tree
blobID, err := repo.WriteBlob([]byte("Hello, world!\n"))
if err != nil {
t.Fatal(err)
}
treeWithContentsID, err := treeBuilder.WriteTreeFromEntries([]TreeEntry{NewEntryBlob("README.md", blobID)})
if err != nil {
t.Fatal(err)
}
// Create initial commit with no tree
expectedInitialCommitID := "648c569f3958b899e832f04750de52cf5d0db2fa"
commitID, err := repo.Commit(emptyTreeID, refName, "Initial commit\n", false)
assert.Nil(t, err)
assert.Equal(t, expectedInitialCommitID, commitID.String())
refHead, err := repo.GetReference(refName)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, expectedInitialCommitID, refHead.String())
privateKey := artifacts.SSHRSAPrivate
// Create publicKey
keyPath := filepath.Join(tempDir, "ssh-key")
if err := os.WriteFile(keyPath, artifacts.SSHRSAPublicSSH, 0o600); err != nil {
t.Fatal(err)
}
publicKey, err := ssh.NewKeyFromFile(keyPath)
if err != nil {
t.Fatal(err)
}
// Create second commit with tree
expectedSecondCommitID := "11020a7c78c4f903d0592ec2e8f73d00a17ec47e"
commitID, err = repo.CommitUsingSpecificKey(treeWithContentsID, refName, "Add README\n", privateKey)
assert.Nil(t, err)
// Verify commit signature using publicKey
err = verifyObjectSignature(t, repo, commitID, publicKey)
assert.Nil(t, err)
assert.Equal(t, expectedSecondCommitID, commitID.String())
}
func TestRepositoryVerifyCommit(t *testing.T) {
tempDir := t.TempDir()
repo := CreateTestGitRepository(t, tempDir, false)
treeBuilder := NewTreeBuilder(repo)
// Write empty tree
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
if err != nil {
t.Fatal(err)
}
sshSignedCommitID, err := repo.Commit(emptyTreeID, "refs/heads/main", "Initial commit\n", true)
if err != nil {
t.Fatal(err)
}
gpgSignedCommitID := createTestGPGSignedCommit(t, repo)
// FIXME: fix gitsign testing
gitsignSignedCommitID := createTestSigstoreSignedCommit(t, repo)
keyDir := t.TempDir()
keyPath := filepath.Join(keyDir, "ssh-key")
if err := os.WriteFile(keyPath, artifacts.SSHRSAPublicSSH, 0o600); err != nil {
t.Fatal(err)
}
sshKey, err := ssh.NewKeyFromFile(keyPath)
if err != nil {
t.Fatal(err)
}
gpgKey, err := gpg.LoadGPGKeyFromBytes(artifacts.GPGKey1Public)
if err != nil {
t.Fatal(err)
}
t.Run("ssh signed commit, verify with ssh key", func(t *testing.T) {
err = verifyObjectSignature(t, repo, sshSignedCommitID, sshKey)
assert.Nil(t, err)
})
t.Run("ssh signed commit, verify with gpg key", func(t *testing.T) {
err = verifyObjectSignature(t, repo, sshSignedCommitID, gpgKey)
assert.ErrorIs(t, err, gitobject.ErrIncorrectVerificationKey)
})
t.Run("gpg signed commit, verify with gpg key", func(t *testing.T) {
err = verifyObjectSignature(t, repo, gpgSignedCommitID, gpgKey)
assert.Nil(t, err)
})
t.Run("gpg signed commit, verify with ssh key", func(t *testing.T) {
err = verifyObjectSignature(t, repo, gpgSignedCommitID, sshKey)
assert.ErrorIs(t, err, gitobject.ErrIncorrectVerificationKey)
})
t.Run("gitsign signed commit, verify with ssh key", func(t *testing.T) {
err = verifyObjectSignature(t, repo, gitsignSignedCommitID, sshKey)
assert.ErrorIs(t, err, gitobject.ErrIncorrectVerificationKey)
})
t.Run("unknown signing method", func(t *testing.T) {
unknownKey := &signerverifier.SSLibKey{KeyType: "unknown"}
err = verifyObjectSignature(t, repo, sshSignedCommitID, unknownKey)
assert.ErrorIs(t, err, gitobject.ErrUnknownSigningMethod)
})
}
func TestRepositoryVerifyCommitSHA256(t *testing.T) {
tempDir := t.TempDir()
repo := CreateTestGitRepository(t, tempDir, false, WithSHA256Format())
treeBuilder := NewTreeBuilder(repo)
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
if err != nil {
t.Fatal(err)
}
sshSignedCommitID, err := repo.Commit(emptyTreeID, "refs/heads/main", "Initial commit\n", true)
if err != nil {
t.Fatal(err)
}
keyDir := t.TempDir()
keyPath := filepath.Join(keyDir, "ssh-key")
if err := os.WriteFile(keyPath, artifacts.SSHRSAPublicSSH, 0o600); err != nil {
t.Fatal(err)
}
sshKey, err := ssh.NewKeyFromFile(keyPath)
if err != nil {
t.Fatal(err)
}
gpgKey, err := gpg.LoadGPGKeyFromBytes(artifacts.GPGKey1Public)
if err != nil {
t.Fatal(err)
}
// gitsign/sigstore signed commits are not exercised here: that test relies
// on a precomputed signature over a fixed SHA-1 commit object, which cannot
// be reproduced for a SHA-256 repository.
t.Run("ssh signed commit, verify with ssh key", func(t *testing.T) {
err = verifyObjectSignature(t, repo, sshSignedCommitID, sshKey)
assert.Nil(t, err)
})
t.Run("ssh signed commit, verify with gpg key", func(t *testing.T) {
err = verifyObjectSignature(t, repo, sshSignedCommitID, gpgKey)
assert.ErrorIs(t, err, gitobject.ErrIncorrectVerificationKey)
})
t.Run("unknown signing method", func(t *testing.T) {
unknownKey := &signerverifier.SSLibKey{KeyType: "unknown"}
err = verifyObjectSignature(t, repo, sshSignedCommitID, unknownKey)
assert.ErrorIs(t, err, gitobject.ErrUnknownSigningMethod)
})
}
func TestCommitUsingSpecificKeySignatureHeader(t *testing.T) {
for _, objectFormat := range []ObjectFormat{ObjectFormatSHA1, ObjectFormatSHA256} {
t.Run(string(objectFormat), func(t *testing.T) {
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, false, WithObjectFormat(objectFormat))
emptyTreeID, err := NewTreeBuilder(repo).WriteTreeFromEntries(nil)
require.Nil(t, err)
commitID, err := repo.CommitUsingSpecificKey(emptyTreeID, "refs/heads/main", "Initial commit\n", artifacts.SSHRSAPrivate)
require.Nil(t, err)
raw, err := repo.executor("cat-file", "commit", commitID.String()).executeString()
require.Nil(t, err)
// The signature must be stored under the header matching the
// object's hash algorithm.
if objectFormat == ObjectFormatSHA256 {
assert.Contains(t, raw, "\ngpgsig-sha256 ")
assert.NotContains(t, raw, "\ngpgsig -")
} else {
assert.Contains(t, raw, "\ngpgsig -")
assert.NotContains(t, raw, "gpgsig-sha256")
}
keyDir := t.TempDir()
keyPath := filepath.Join(keyDir, "ssh-key")
require.Nil(t, os.WriteFile(keyPath, artifacts.SSHRSAPublicSSH, 0o600))
sshKey, err := ssh.NewKeyFromFile(keyPath)
require.Nil(t, err)
assert.Nil(t, verifyObjectSignature(t, repo, commitID, sshKey))
})
}
}
func TestVerifyCommitSignatureRejectsMultipleSignatures(t *testing.T) {
tests := map[string]struct {
signingKey []byte
verificationKey func(t *testing.T) *signerverifier.SSLibKey
}{
"gpg": {
signingKey: artifacts.GPGKey1Private,
verificationKey: func(t *testing.T) *signerverifier.SSLibKey {
t.Helper()
key, err := gpg.LoadGPGKeyFromBytes(artifacts.GPGKey1Public)
require.Nil(t, err)
return key
},
},
"ssh": {
signingKey: artifacts.SSHED25519Private,
verificationKey: func(t *testing.T) *signerverifier.SSLibKey {
t.Helper()
keyPath := filepath.Join(t.TempDir(), "ssh-key.pub")
require.Nil(t, os.WriteFile(keyPath, artifacts.SSHED25519PublicSSH, 0o600))
key, err := ssh.NewKeyFromFile(keyPath)
require.Nil(t, err)
return key
},
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, false)
goGitRepo, err := repo.GetGoGitRepository()
require.Nil(t, err)
testCommit := &object.Commit{
Author: object.Signature{Name: testName, Email: testEmail, When: testClock.Now()},
Committer: object.Signature{Name: testName, Email: testEmail, When: testClock.Now()},
Message: "Test commit\n",
TreeHash: plumbing.ZeroHash,
}
commitEncoded := goGitRepo.Storer.NewEncodedObject()
require.Nil(t, testCommit.EncodeWithoutSignature(commitEncoded))
reader, err := commitEncoded.Reader()
require.Nil(t, err)
contents, err := io.ReadAll(reader)
require.Nil(t, err)
sig, err := signGitObjectUsingKey(contents, test.signingKey)
require.Nil(t, err)
// Two (individually valid) signature blocks, each on their own
// lines, must be rejected as ambiguous rather than verified
// against the first.
block := strings.TrimRight(sig, "\n") + "\n"
testCommit.Signature = block + block
commitEncoded = goGitRepo.Storer.NewEncodedObject()
require.Nil(t, testCommit.Encode(commitEncoded))
commitID, err := goGitRepo.Storer.SetEncodedObject(commitEncoded)
require.Nil(t, err)
commitHash, err := NewHash(commitID.String())
require.Nil(t, err)
err = verifyObjectSignature(t, repo, commitHash, test.verificationKey(t))
assert.ErrorIs(t, err, gitobject.ErrMultipleSignatures)
assert.ErrorIs(t, err, gitobject.ErrIncorrectVerificationKey)
})
}
}
func TestKnowsCommit(t *testing.T) {
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, false)
refName := "refs/heads/main"
treeBuilder := NewTreeBuilder(repo)
// Write empty tree
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
if err != nil {
t.Fatal(err)
}
firstCommitID, err := repo.Commit(emptyTreeID, refName, "First commit", false)
if err != nil {
t.Fatal(err)
}
secondCommitID, err := repo.Commit(emptyTreeID, refName, "Second commit", false)
if err != nil {
t.Fatal(err)
}
unknownCommitID, err := repo.Commit(emptyTreeID, "refs/heads/unknown", "Unknown commit", false)
if err != nil {
t.Fatal(err)
}
t.Run("check if second commit knows first", func(t *testing.T) {
knows, err := repo.KnowsCommit(secondCommitID, firstCommitID)
assert.Nil(t, err)
assert.True(t, knows)
})
t.Run("check that first commit does not know second", func(t *testing.T) {
knows, err := repo.KnowsCommit(firstCommitID, secondCommitID)
assert.Nil(t, err)
assert.False(t, knows)
})
t.Run("check that both commits know themselves", func(t *testing.T) {
knows, err := repo.KnowsCommit(firstCommitID, firstCommitID)
assert.Nil(t, err)
assert.True(t, knows)
knows, err = repo.KnowsCommit(secondCommitID, secondCommitID)
assert.Nil(t, err)
assert.True(t, knows)
})
t.Run("check that an unknown commit can't know a known commit", func(t *testing.T) {
knows, _ := repo.KnowsCommit(unknownCommitID, firstCommitID)
assert.False(t, knows)
})
t.Run("non-commit object as first arg", func(t *testing.T) {
blobID, err := repo.WriteBlob([]byte("test"))
require.Nil(t, err)
_, err = repo.KnowsCommit(blobID, firstCommitID)
assert.ErrorContains(t, err, "is not a commit object")
})
t.Run("non-commit object as second arg", func(t *testing.T) {
blobID, err := repo.WriteBlob([]byte("test"))
require.Nil(t, err)
_, err = repo.KnowsCommit(firstCommitID, blobID)
assert.ErrorContains(t, err, "is not a commit object")
})
}
func createTestGPGSignedCommit(t *testing.T, repo *Repository) Hash {
t.Helper()
goGitRepo, err := repo.GetGoGitRepository()
if err != nil {
t.Fatal(err)
}
testCommit := &object.Commit{
Author: object.Signature{
Name: testName,
Email: testEmail,
When: testClock.Now(),
},
Committer: object.Signature{
Name: testName,
Email: testEmail,
When: testClock.Now(),
},
Message: "Test commit\n",
TreeHash: plumbing.ZeroHash,
}
commitEncoded := goGitRepo.Storer.NewEncodedObject()
if err := testCommit.EncodeWithoutSignature(commitEncoded); err != nil {
t.Fatal(err)
}
r, err := commitEncoded.Reader()
if err != nil {
t.Fatal(err)
}
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(artifacts.GPGKey1Private))
if err != nil {
t.Fatal(err)
}
sig := new(strings.Builder)
if err := openpgp.ArmoredDetachSign(sig, keyring[0], r, nil); err != nil {
t.Fatal(err)
}
testCommit.Signature = sig.String()
// Re-encode with the signature
commitEncoded = goGitRepo.Storer.NewEncodedObject()
if err := testCommit.Encode(commitEncoded); err != nil {
t.Fatal(err)
}
commitID, err := goGitRepo.Storer.SetEncodedObject(commitEncoded)
if err != nil {
t.Fatal(err)
}
commitHash, err := NewHash(commitID.String())
if err != nil {
t.Fatal(err)
}
return commitHash
}
func createTestSigstoreSignedCommit(t *testing.T, repo *Repository) Hash {
t.Helper()
goGitRepo, err := repo.GetGoGitRepository()
if err != nil {
t.Fatal(err)
}
testCommit := &object.Commit{
Hash: plumbing.NewHash("d6b230478965e25477263aa65f1ca6d23d0c0d97"),
Author: object.Signature{
Name: "Aditya Sirish",
Email: "aditya@saky.in",
When: time.Date(2023, time.August, 1, 15, 44, 23, 0, time.FixedZone("", -4*3600)),
},
Committer: object.Signature{
Name: "Aditya Sirish",
Email: "aditya@saky.in",
When: time.Date(2023, time.August, 1, 15, 44, 23, 0, time.FixedZone("", -4*3600)),
},
Signature: `-----BEGIN SIGNED MESSAGE-----
MIIEMAYJKoZIhvcNAQcCoIIEITCCBB0CAQExDTALBglghkgBZQMEAgEwCwYJKoZI
hvcNAQcBoIIC0DCCAswwggJToAMCAQICFHIJCrBVHxoHlGos++k1xJxcElGaMAoG
CCqGSM49BAMDMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2ln
c3RvcmUtaW50ZXJtZWRpYXRlMB4XDTIzMDgwMTE5NDQzMVoXDTIzMDgwMTE5NTQz
MVowADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD8d752TJfGtANVYoiJJn+o6
JPKj5NwEZs1AcVRT2qElikVun5t+bQ07iDFa/Xiun5ytZrEK2YJVgqdntLd6hSOj
ggFyMIIBbjAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHQYD
VR0OBBYEFAuYzgyBA01YSSN1v0fYenGo7+PcMB8GA1UdIwQYMBaAFN/T6c9WJBGW
+ajY6ShVosYuGGQ/MBwGA1UdEQEB/wQSMBCBDmFkaXR5YUBzYWt5LmluMCwGCisG
AQQBg78wAQEEHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDAuBgorBgEE
AYO/MAEIBCAMHmh0dHBzOi8vZ2l0aHViLmNvbS9sb2dpbi9vYXV0aDCBigYKKwYB
BAHWeQIEAgR8BHoAeAB2AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6O
AAABibKhcJgAAAQDAEcwRQIgcWuz6NhFgdL0fNni6j0SOQnAgFpPEaN8jDH70mbD
uPMCIQCX8koEnIX4c9crMT1hfoBBf1Z/CHJ6HLLHpQwWfEUMIzAKBggqhkjOPQQD
AwNnADBkAjBozIBaBtEu7JUyYLH7Ly698E0o8DdIOmqcUMUYWNC6zyJVdrL5gAla
mQSxfObSQasCMHQuw8youTjmFJXT7pNOYX4DW25knt+6P+W/m6zwcRRe3dMjmUAB
gdBJb32+XXJMRDGCASYwggEiAgEBME8wNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2
MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUCFHIJCrBVHxoHlGos++k1
xJxcElGaMAsGCWCGSAFlAwQCAaBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEw
HAYJKoZIhvcNAQkFMQ8XDTIzMDgwMTE5NDQzMlowLwYJKoZIhvcNAQkEMSIEIBe6
VHcVlkO8jRm/fbUipwxwxNaI7UFDAL38Jl8eUj/5MAoGCCqGSM49BAMCBEgwRgIh
AIYiRbnVeWjjgX2XwljDryzQN5RhUQaVH/AcUj+tbvWxAiEAhm9l3BU58tQsgyJW
oYBpMWLgg6AUzpxx9mITZ2EKr4c=
-----END SIGNED MESSAGE-----
`,
Message: "Test commit\n",
TreeHash: plumbing.NewHash("4b825dc642cb6eb9a060e54bf8d69288fbee4904"),
}
commitEncoded := goGitRepo.Storer.NewEncodedObject()
if err := testCommit.EncodeWithoutSignature(commitEncoded); err != nil {
t.Fatal(err)
}
commitID, err := goGitRepo.Storer.SetEncodedObject(commitEncoded)
if err != nil {
t.Fatal(err)
}
commitHash, err := NewHash(commitID.String())
if err != nil {
t.Fatal(err)
}
return commitHash
}
func TestRepositoryGetCommitMessage(t *testing.T) {
tempDir := t.TempDir()
repo := CreateTestGitRepository(t, tempDir, false)
refName := "refs/heads/main"
treeBuilder := NewTreeBuilder(repo)
// Write empty tree
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
if err != nil {
t.Fatal(err)
}
message := "Initial commit"
commit, err := repo.Commit(emptyTreeID, refName, message, false)
if err != nil {
t.Fatal(err)
}
commitMessage, err := repo.GetCommitMessage(commit)
assert.Nil(t, err)
assert.Equal(t, message, commitMessage)
t.Run("non-commit object", func(t *testing.T) {
blobID, err := repo.WriteBlob([]byte("test"))
require.Nil(t, err)
_, err = repo.GetCommitMessage(blobID)
assert.ErrorContains(t, err, "is not a commit object")
})
}
func TestGetCommitTreeID(t *testing.T) {
tempDir := t.TempDir()
repo := CreateTestGitRepository(t, tempDir, false)
refName := "refs/heads/main"
treeBuilder := NewTreeBuilder(repo)
// Write empty tree
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
if err != nil {
t.Fatal(err)
}
// Write second tree
blobID, err := repo.WriteBlob([]byte("Hello, world!\n"))
if err != nil {
t.Fatal(err)
}
treeWithContentsID, err := treeBuilder.WriteTreeFromEntries([]TreeEntry{NewEntryBlob("README.md", blobID)})
if err != nil {
t.Fatal(err)
}
// Create initial commit with no tree
initialCommitID, err := repo.Commit(emptyTreeID, refName, "Initial commit\n", false)
if err != nil {
t.Fatal(err)
}
initialCommitTreeID, err := repo.GetCommitTreeID(initialCommitID)
assert.Nil(t, err)
assert.Equal(t, emptyTreeID, initialCommitTreeID)
// Create second commit with tree
secondCommitID, err := repo.Commit(treeWithContentsID, refName, "Add README\n", false)
if err != nil {
t.Fatal(err)
}
secondCommitTreeID, err := repo.GetCommitTreeID(secondCommitID)
assert.Nil(t, err)
assert.Equal(t, treeWithContentsID, secondCommitTreeID)
t.Run("non-commit object", func(t *testing.T) {
blobID, err := repo.WriteBlob([]byte("test"))
require.Nil(t, err)
_, err = repo.GetCommitTreeID(blobID)
assert.ErrorContains(t, err, "is not a commit object")
})
}
func TestGetCommitParentIDs(t *testing.T) {
tempDir := t.TempDir()
repo := CreateTestGitRepository(t, tempDir, false)
refName := "refs/heads/main"
treeBuilder := NewTreeBuilder(repo)
// Write empty tree
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
if err != nil {
t.Fatal(err)
}
// Create initial commit
initialCommitID, err := repo.Commit(emptyTreeID, refName, "Initial commit\n", false)
if err != nil {
t.Fatal(err)
}
initialCommitParentIDs, err := repo.GetCommitParentIDs(initialCommitID)
assert.Nil(t, err)
assert.Empty(t, initialCommitParentIDs)
// Create second commit
secondCommitID, err := repo.Commit(emptyTreeID, refName, "Add README\n", false)
if err != nil {
t.Fatal(err)
}
secondCommitParentIDs, err := repo.GetCommitParentIDs(secondCommitID)
assert.Nil(t, err)
assert.Equal(t, []Hash{initialCommitID}, secondCommitParentIDs)
// Create merge commit
mergeCommitID := repo.commitWithParents(t, emptyTreeID, []Hash{initialCommitID, secondCommitID}, "Merge commit\n", false)
mergeCommitParentIDs, err := repo.GetCommitParentIDs(mergeCommitID)
assert.Nil(t, err)
assert.Equal(t, []Hash{initialCommitID, secondCommitID}, mergeCommitParentIDs)
blobID, err := repo.WriteBlob([]byte("test"))
require.Nil(t, err)
_, err = repo.GetCommitParentIDs(blobID)
assert.ErrorContains(t, err, "is not a commit object")
}
func TestGetCommonAncestor(t *testing.T) {
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, false)
refName := "refs/heads/main"
treeBuilder := NewTreeBuilder(repo)
// Write empty tree
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
if err != nil {
t.Fatal(err)
}
initialCommitID, err := repo.Commit(emptyTreeID, refName, "Initial commit\n", false)
if err != nil {
t.Fatal(err)
}
// Add child commit A
commitA, err := repo.Commit(emptyTreeID, refName, "Second commit A\n", false)
if err != nil {
t.Fatal(err)
}
// Add child commit B
commitB := repo.commitWithParents(t, emptyTreeID, []Hash{initialCommitID}, "Second commit B\n", false)
// Test commits, ensure we get back initial commit
commonAncestor, err := repo.GetCommonAncestor(commitA, commitB)
assert.Nil(t, err)
assert.Equal(t, initialCommitID, commonAncestor)
// Test with disjoint commit histories
commitDisconnected := repo.commitWithParents(t, emptyTreeID, nil, "Disconnected initial commit\n", false)
_, err = repo.GetCommonAncestor(commitDisconnected, commitA)
assert.NotNil(t, err)
t.Run("non-commit object as first arg", func(t *testing.T) {
blobID, err := repo.WriteBlob([]byte("test"))
require.Nil(t, err)
_, err = repo.GetCommonAncestor(blobID, commitA)
assert.ErrorContains(t, err, "is not a commit object")
})
t.Run("non-commit object as second arg", func(t *testing.T) {
blobID, err := repo.WriteBlob([]byte("test"))
require.Nil(t, err)
_, err = repo.GetCommonAncestor(commitA, blobID)
assert.ErrorContains(t, err, "is not a commit object")
})
}
func TestEnsureIsCommit(t *testing.T) {
tmpDir := t.TempDir()
repo := CreateTestGitRepository(t, tmpDir, false)
treeBuilder := NewTreeBuilder(repo)
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
require.Nil(t, err)
commitID, err := repo.Commit(emptyTreeID, "refs/heads/main", "Initial commit\n", false)
require.Nil(t, err)
blobID, err := repo.WriteBlob([]byte("test"))
require.Nil(t, err)
t.Run("valid commit", func(t *testing.T) {
err := repo.ensureIsCommit(commitID)
assert.Nil(t, err)
})
t.Run("non-commit object", func(t *testing.T) {
err := repo.ensureIsCommit(blobID)
assert.ErrorContains(t, err, "is not a commit object")
})
t.Run("non-existent object", func(t *testing.T) {
err := repo.ensureIsCommit(ZeroHash)
assert.ErrorContains(t, err, "unable to inspect if object is commit")
})
}