mirror of
https://github.com/vee1e/gittuf.git
synced 2026-09-01 10:18:18 +00:00
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>
1294 lines
36 KiB
Go
1294 lines
36 KiB
Go
// Copyright The gittuf Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package gitinterface
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/gittuf/gittuf/pkg/gitstore"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestRepositoryEmptyTree(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tempDir, false)
|
|
|
|
hash, err := repo.EmptyTree()
|
|
assert.Nil(t, err)
|
|
|
|
// SHA-1 ID used by Git to denote an empty tree
|
|
// $ git hash-object -t tree --stdin < /dev/null
|
|
assert.Equal(t, "4b825dc642cb6eb9a060e54bf8d69288fbee4904", hash.String())
|
|
}
|
|
|
|
func TestRepositoryEmptyTreeSHA256(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tempDir, false, WithSHA256Format())
|
|
|
|
hash, err := repo.EmptyTree()
|
|
assert.Nil(t, err)
|
|
|
|
// SHA-256 ID used by Git to denote an empty tree
|
|
// $ git init --object-format=sha256 && git hash-object -t tree --stdin < /dev/null
|
|
assert.Equal(t, "6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321", hash.String())
|
|
}
|
|
|
|
func TestGetPathIDInTree(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tempDir, false)
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
blobAID, err := repo.WriteBlob([]byte("a"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
blobBID, err := repo.WriteBlob([]byte("b"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
emptyTreeID := "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
|
|
|
t.Run("no items", func(t *testing.T) {
|
|
treeID, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assert.Equal(t, emptyTreeID, treeID.String())
|
|
|
|
pathID, err := repo.GetPathIDInTree(treeID, "a")
|
|
assert.ErrorIs(t, err, ErrTreeDoesNotHavePath)
|
|
assert.Nil(t, pathID)
|
|
})
|
|
|
|
t.Run("no subdirectories", func(t *testing.T) {
|
|
exhaustiveItems := []TreeEntry{
|
|
NewEntryBlob("a", blobAID),
|
|
NewEntryBlob("b", blobBID),
|
|
}
|
|
|
|
treeID, err := treeBuilder.WriteTreeFromEntries(exhaustiveItems)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
itemID, err := repo.GetPathIDInTree(treeID, "a")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, blobAID, itemID)
|
|
})
|
|
|
|
t.Run("one file in root tree, one file in subdirectory", func(t *testing.T) {
|
|
exhaustiveItems := []TreeEntry{
|
|
NewEntryBlob("foo/a", blobAID),
|
|
NewEntryBlob("b", blobBID),
|
|
}
|
|
|
|
treeID, err := treeBuilder.WriteTreeFromEntries(exhaustiveItems)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
itemID, err := repo.GetPathIDInTree(treeID, "foo/a")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, blobAID, itemID)
|
|
})
|
|
|
|
t.Run("multiple levels", func(t *testing.T) {
|
|
exhaustiveItems := []TreeEntry{
|
|
NewEntryBlob("foo/bar/foobar/a", blobAID),
|
|
NewEntryBlob("foobar/foo/bar/b", blobBID),
|
|
}
|
|
|
|
treeID, err := treeBuilder.WriteTreeFromEntries(exhaustiveItems)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// find tree ID for foo/bar/foobar
|
|
expectedItemID, err := treeBuilder.WriteTreeFromEntries([]TreeEntry{NewEntryBlob("a", blobAID)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
itemID, err := repo.GetPathIDInTree(treeID, "foo/bar/foobar")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, expectedItemID, itemID)
|
|
|
|
// find tree ID for foo/bar
|
|
expectedItemID, err = treeBuilder.WriteTreeFromEntries([]TreeEntry{NewEntryBlob("foobar/a", blobAID)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
itemID, err = repo.GetPathIDInTree(treeID, "foo/bar")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, expectedItemID, itemID)
|
|
|
|
// find tree ID for foobar/foo
|
|
expectedItemID, err = treeBuilder.WriteTreeFromEntries([]TreeEntry{NewEntryBlob("bar/b", blobBID)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
itemID, err = repo.GetPathIDInTree(treeID, "foobar/foo")
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, expectedItemID, itemID)
|
|
|
|
itemID, err = repo.GetPathIDInTree(treeID, "foobar/foo/foobar")
|
|
assert.ErrorIs(t, err, ErrTreeDoesNotHavePath)
|
|
assert.Nil(t, itemID)
|
|
})
|
|
|
|
t.Run("blob id", func(t *testing.T) {
|
|
_, err := repo.GetPathIDInTree(blobAID, "a")
|
|
assert.ErrorContains(t, err, "unable to enumerate items in tree")
|
|
})
|
|
|
|
t.Run("non-existent id", func(t *testing.T) {
|
|
_, err := repo.GetPathIDInTree(ZeroHash, "a")
|
|
assert.ErrorContains(t, err, "unable to enumerate items in tree")
|
|
})
|
|
}
|
|
|
|
func TestGetEntriesInTree(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tempDir, false)
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
blobAID, err := repo.WriteBlob([]byte("a"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
blobBID, err := repo.WriteBlob([]byte("b"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
emptyTreeID := "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
|
|
|
t.Run("no items", func(t *testing.T) {
|
|
treeID, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assert.Equal(t, emptyTreeID, treeID.String())
|
|
|
|
entries, err := repo.GetEntriesInTree(treeID)
|
|
assert.Nil(t, err)
|
|
assert.Nil(t, entries)
|
|
})
|
|
|
|
t.Run("no subdirectories", func(t *testing.T) {
|
|
exhaustiveItems := []TreeEntry{
|
|
NewEntryBlob("a", blobAID),
|
|
NewEntryBlob("b", blobBID),
|
|
}
|
|
|
|
treeID, err := treeBuilder.WriteTreeFromEntries(exhaustiveItems)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expected := []TreeEntry{
|
|
NewEntryBlob("a", blobAID),
|
|
NewEntryBlob("b", blobBID),
|
|
}
|
|
|
|
entries, err := repo.GetEntriesInTree(treeID)
|
|
assert.Nil(t, err)
|
|
assert.ElementsMatch(t, expected, entries)
|
|
})
|
|
|
|
t.Run("one file in root tree, one file in subdirectory", func(t *testing.T) {
|
|
exhaustiveItems := []TreeEntry{
|
|
NewEntryBlob("foo/a", blobAID),
|
|
NewEntryBlob("b", blobBID),
|
|
}
|
|
|
|
treeID, err := treeBuilder.WriteTreeFromEntries(exhaustiveItems)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
fooTreeID, err := treeBuilder.WriteTreeFromEntries([]TreeEntry{NewEntryBlob("a", blobAID)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expected := []TreeEntry{
|
|
NewEntryTree("foo", fooTreeID),
|
|
NewEntryBlob("b", blobBID),
|
|
}
|
|
|
|
entries, err := repo.GetEntriesInTree(treeID)
|
|
assert.Nil(t, err)
|
|
assert.ElementsMatch(t, expected, entries)
|
|
})
|
|
|
|
t.Run("one file in foo tree, one file in bar", func(t *testing.T) {
|
|
exhaustiveItems := []TreeEntry{
|
|
NewEntryBlob("foo/a", blobAID),
|
|
NewEntryBlob("bar/b", blobBID),
|
|
}
|
|
|
|
treeID, err := treeBuilder.WriteTreeFromEntries(exhaustiveItems)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
fooTreeID, err := treeBuilder.WriteTreeFromEntries([]TreeEntry{NewEntryBlob("a", blobAID)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
barTreeID, err := treeBuilder.WriteTreeFromEntries([]TreeEntry{NewEntryBlob("b", blobBID)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expected := []TreeEntry{
|
|
NewEntryTree("foo", fooTreeID),
|
|
NewEntryTree("bar", barTreeID),
|
|
}
|
|
|
|
entries, err := repo.GetEntriesInTree(treeID)
|
|
assert.Nil(t, err)
|
|
assert.ElementsMatch(t, expected, entries)
|
|
})
|
|
|
|
t.Run("blob id", func(t *testing.T) {
|
|
_, err := repo.GetEntriesInTree(blobAID)
|
|
assert.ErrorContains(t, err, "unable to enumerate items in tree")
|
|
})
|
|
|
|
t.Run("non-existent id", func(t *testing.T) {
|
|
_, err := repo.GetEntriesInTree(ZeroHash)
|
|
assert.ErrorContains(t, err, "unable to enumerate items in tree")
|
|
})
|
|
}
|
|
|
|
func TestGetMergeTree(t *testing.T) {
|
|
t.Run("no conflict", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false)
|
|
|
|
// We meed to change the directory for this test because we `checkout`
|
|
// for older Git versions, modifying the worktree. This chdir ensures
|
|
// that the temporary directory is used as the worktree.
|
|
pwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chdir(tmpDir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.Chdir(pwd) //nolint:errcheck
|
|
|
|
emptyBlobID, err := repo.WriteBlob(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
treeAID, err := treeBuilder.WriteTreeFromEntries([]TreeEntry{NewEntryBlob("a", emptyBlobID)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
treeBID, err := treeBuilder.WriteTreeFromEntries([]TreeEntry{NewEntryBlob("b", emptyBlobID)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
combinedTreeID, err := treeBuilder.WriteTreeFromEntries([]TreeEntry{
|
|
NewEntryBlob("a", emptyBlobID),
|
|
NewEntryBlob("b", emptyBlobID),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
mainRef := "refs/heads/main"
|
|
featureRef := "refs/heads/feature"
|
|
|
|
// Add commits to the main branch
|
|
baseCommitID, err := repo.Commit(emptyTreeID, mainRef, "Initial commit", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
commitAID, err := repo.Commit(treeAID, mainRef, "Commit A", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Add commits to the feature branch
|
|
if err := repo.SetReference(featureRef, baseCommitID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
commitBID, err := repo.Commit(treeBID, featureRef, "Commit B", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// fix up checked out worktree
|
|
if _, err := repo.executor("restore", "--staged", ".").executeString(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := repo.executor("checkout", "--", ".").executeString(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
mergeTreeID, err := repo.GetMergeTree(commitAID, commitBID)
|
|
assert.Nil(t, err)
|
|
if !combinedTreeID.Equal(mergeTreeID) {
|
|
mergeTreeContents, err := repo.GetAllFilesInTree(mergeTreeID)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error when debugging non-matched merge trees: %s", err.Error())
|
|
}
|
|
t.Log("merge tree contents:", mergeTreeContents)
|
|
t.Error("merge trees don't match")
|
|
}
|
|
})
|
|
|
|
t.Run("merge conflict", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false)
|
|
|
|
// We meed to change the directory for this test because we `checkout`
|
|
// for older Git versions, modifying the worktree. This chdir ensures
|
|
// that the temporary directory is used as the worktree.
|
|
pwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chdir(tmpDir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.Chdir(pwd) //nolint:errcheck
|
|
|
|
emptyBlobID, err := repo.WriteBlob(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
blobAID, err := repo.WriteBlob([]byte("a"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
blobBID, err := repo.WriteBlob([]byte("b"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
treeAID, err := treeBuilder.WriteTreeFromEntries([]TreeEntry{NewEntryBlob("a", blobAID)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
treeBID, err := treeBuilder.WriteTreeFromEntries([]TreeEntry{
|
|
NewEntryBlob("a", blobBID),
|
|
NewEntryBlob("b", emptyBlobID),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
mainRef := "refs/heads/main"
|
|
featureRef := "refs/heads/feature"
|
|
|
|
// Add commits to the main branch
|
|
baseCommitID, err := repo.Commit(emptyTreeID, mainRef, "Initial commit", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
commitAID, err := repo.Commit(treeAID, mainRef, "Commit A", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Add commits to the feature branch
|
|
if err := repo.SetReference(featureRef, baseCommitID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
commitBID, err := repo.Commit(treeBID, featureRef, "Commit B", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// fix up checked out worktree
|
|
if _, err := repo.executor("restore", "--staged", ".").executeString(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := repo.executor("checkout", "--", ".").executeString(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = repo.GetMergeTree(commitAID, commitBID)
|
|
assert.NotNil(t, err)
|
|
})
|
|
|
|
t.Run("fast forward merge", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false)
|
|
|
|
// We meed to change the directory for this test because we `checkout`
|
|
// for older Git versions, modifying the worktree. This chdir ensures
|
|
// that the temporary directory is used as the worktree.
|
|
pwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chdir(tmpDir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.Chdir(pwd) //nolint:errcheck
|
|
|
|
emptyBlobID, err := repo.WriteBlob(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
treeID, err := treeBuilder.WriteTreeFromEntries([]TreeEntry{NewEntryBlob("empty", emptyBlobID)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
commitID, err := repo.Commit(treeID, "refs/heads/main", "Initial commit\n", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
mergeTreeID, err := repo.GetMergeTree(ZeroHash, commitID)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, treeID, mergeTreeID)
|
|
})
|
|
|
|
t.Run("non-commit target", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false)
|
|
|
|
blobID, err := repo.WriteBlob([]byte("a"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = repo.GetMergeTree(ZeroHash, blobID)
|
|
assert.ErrorContains(t, err, "is not a commit object")
|
|
})
|
|
|
|
t.Run("non-commit base", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false)
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
blobID, err := repo.WriteBlob([]byte("a"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
emptyTreeID, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
commitID, err := repo.Commit(emptyTreeID, "refs/heads/main", "Initial commit\n", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = repo.GetMergeTree(blobID, commitID)
|
|
assert.ErrorContains(t, err, "is not a commit object")
|
|
})
|
|
}
|
|
|
|
func TestCreateSubtreeFromUpstreamRepository(t *testing.T) {
|
|
t.Run("subtree into HEAD", func(t *testing.T) {
|
|
tmpDir1 := t.TempDir()
|
|
downstreamRepository := CreateTestGitRepository(t, tmpDir1, false)
|
|
|
|
blobAID, err := downstreamRepository.WriteBlob([]byte("a"))
|
|
require.Nil(t, err)
|
|
|
|
blobBID, err := downstreamRepository.WriteBlob([]byte("b"))
|
|
require.Nil(t, err)
|
|
|
|
downstreamTreeBuilder := NewTreeBuilder(downstreamRepository)
|
|
|
|
// The downstream tree (if set as exists in test below) is:
|
|
// oof/a -> blobA
|
|
// b -> blobB
|
|
downstreamTreeEntries := []TreeEntry{
|
|
NewEntryBlob("oof/a", blobAID),
|
|
NewEntryBlob("b", blobBID),
|
|
}
|
|
downstreamTreeID, err := downstreamTreeBuilder.WriteTreeFromEntries(downstreamTreeEntries)
|
|
require.Nil(t, err)
|
|
|
|
downstreamCommitID, err := downstreamRepository.Commit(downstreamTreeID, "refs/heads/main", "Initial commit\n", false)
|
|
require.Nil(t, err)
|
|
|
|
err = downstreamRepository.SetSymbolicReference("HEAD", "refs/heads/main")
|
|
require.Nil(t, err)
|
|
|
|
downstreamRepository.RestoreWorktree(t)
|
|
|
|
tmpDir2 := t.TempDir()
|
|
upstreamRepository := CreateTestGitRepository(t, tmpDir2, true)
|
|
|
|
_, err = upstreamRepository.WriteBlob([]byte("a"))
|
|
require.Nil(t, err)
|
|
|
|
_, err = upstreamRepository.WriteBlob([]byte("b"))
|
|
require.Nil(t, err)
|
|
|
|
upstreamTreeBuilder := NewTreeBuilder(upstreamRepository)
|
|
|
|
// The upstream tree is:
|
|
// a -> blobA
|
|
// foo/a -> blobA
|
|
// foo/b -> blobB
|
|
// foobar/foo/bar/b -> blobB
|
|
|
|
upstreamTreeID, err := upstreamTreeBuilder.WriteTreeFromEntries([]TreeEntry{
|
|
NewEntryBlob("a", blobAID),
|
|
NewEntryBlob("foo/a", blobAID),
|
|
NewEntryBlob("foo/b", blobBID),
|
|
NewEntryBlob("foobar/foo/bar/b", blobBID),
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
upstreamRef := "refs/heads/main"
|
|
upstreamCommitID, err := upstreamRepository.Commit(upstreamTreeID, upstreamRef, "Initial commit\n", false)
|
|
require.Nil(t, err)
|
|
|
|
downstreamCommitIDNew, err := downstreamRepository.CreateSubtreeFromUpstreamRepository(upstreamRepository, upstreamCommitID, "", "refs/heads/main", "upstream")
|
|
assert.Nil(t, err)
|
|
assert.NotEqual(t, downstreamCommitID, downstreamCommitIDNew)
|
|
|
|
statuses, err := downstreamRepository.Status()
|
|
require.Nil(t, err)
|
|
assert.Empty(t, statuses)
|
|
})
|
|
|
|
t.Run("various other subtree scenarios", func(t *testing.T) {
|
|
tmpDir1 := t.TempDir()
|
|
downstreamRepository := CreateTestGitRepository(t, tmpDir1, false)
|
|
|
|
blobAID, err := downstreamRepository.WriteBlob([]byte("a"))
|
|
require.Nil(t, err)
|
|
|
|
blobBID, err := downstreamRepository.WriteBlob([]byte("b"))
|
|
require.Nil(t, err)
|
|
|
|
downstreamTreeBuilder := NewTreeBuilder(downstreamRepository)
|
|
|
|
// The downstream tree (if set as exists in test below) is:
|
|
// oof/a -> blobA
|
|
// b -> blobB
|
|
downstreamTreeEntries := []TreeEntry{
|
|
NewEntryBlob("oof/a", blobAID),
|
|
NewEntryBlob("b", blobBID),
|
|
}
|
|
downstreamTreeID, err := downstreamTreeBuilder.WriteTreeFromEntries(downstreamTreeEntries)
|
|
require.Nil(t, err)
|
|
|
|
tmpDir2 := t.TempDir()
|
|
upstreamRepository := CreateTestGitRepository(t, tmpDir2, true)
|
|
|
|
_, err = upstreamRepository.WriteBlob([]byte("a"))
|
|
require.Nil(t, err)
|
|
|
|
_, err = upstreamRepository.WriteBlob([]byte("b"))
|
|
require.Nil(t, err)
|
|
|
|
upstreamTreeBuilder := NewTreeBuilder(upstreamRepository)
|
|
|
|
// The upstream tree is:
|
|
// a -> blobA
|
|
// foo/a -> blobA
|
|
// foo/b -> blobB
|
|
// foobar/foo/bar/b -> blobB
|
|
|
|
upstreamRootTreeID, err := upstreamTreeBuilder.WriteTreeFromEntries([]TreeEntry{
|
|
NewEntryBlob("a", blobAID),
|
|
NewEntryBlob("foo/a", blobAID),
|
|
NewEntryBlob("foo/b", blobBID),
|
|
NewEntryBlob("foobar/foo/bar/b", blobBID),
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
upstreamRef := "refs/heads/main"
|
|
upstreamCommitID, err := upstreamRepository.Commit(upstreamRootTreeID, upstreamRef, "Initial commit\n", false)
|
|
require.Nil(t, err)
|
|
|
|
tests := map[string]struct {
|
|
upstreamPath string
|
|
localPath string
|
|
refExists bool // refExists -> we must check for other files but no prior propagation has happened
|
|
priorPropagation bool // priorPropagation -> localPath is already populated, mutually exclusive with refExists
|
|
err error
|
|
}{
|
|
"directory in root, ref does not exist": {
|
|
localPath: "upstream",
|
|
refExists: false,
|
|
priorPropagation: false,
|
|
},
|
|
"directory in root, trailing slash, ref does not exist": {
|
|
localPath: "upstream/",
|
|
refExists: false,
|
|
priorPropagation: false,
|
|
},
|
|
"directory in root, ref exists": {
|
|
localPath: "upstream",
|
|
refExists: true,
|
|
priorPropagation: false,
|
|
},
|
|
"directory in root, trailing slash, ref exists": {
|
|
localPath: "upstream/",
|
|
refExists: true,
|
|
priorPropagation: false,
|
|
},
|
|
"directory in root, prior propagation exists": {
|
|
localPath: "upstream",
|
|
refExists: false,
|
|
priorPropagation: true,
|
|
},
|
|
"directory in root, trailing slash, prior propagation exists": {
|
|
localPath: "upstream/",
|
|
refExists: false,
|
|
priorPropagation: true,
|
|
},
|
|
"directory in subdirectory, ref does not exist": {
|
|
localPath: "foo/upstream",
|
|
refExists: false,
|
|
priorPropagation: false,
|
|
},
|
|
"directory in subdirectory, trailing slash, ref does not exist": {
|
|
localPath: "foo/upstream/",
|
|
refExists: false,
|
|
priorPropagation: false,
|
|
},
|
|
"directory in subdirectory, ref exists": {
|
|
localPath: "foo/upstream",
|
|
refExists: true,
|
|
priorPropagation: false,
|
|
},
|
|
"directory in subdirectory, trailing slash, ref exists": {
|
|
localPath: "foo/upstream/",
|
|
refExists: true,
|
|
priorPropagation: false,
|
|
},
|
|
"directory in subdirectory, prior propagation exists": {
|
|
localPath: "foo/upstream",
|
|
refExists: false,
|
|
priorPropagation: true,
|
|
},
|
|
"directory in subdirectory, trailing slash, prior propagation exists": {
|
|
localPath: "foo/upstream/",
|
|
refExists: false,
|
|
priorPropagation: true,
|
|
},
|
|
"with upstream path, directory in root, ref does not exist": {
|
|
upstreamPath: "foo",
|
|
localPath: "upstream",
|
|
refExists: false,
|
|
priorPropagation: false,
|
|
},
|
|
"with upstream path, directory in root, trailing slash, ref does not exist": {
|
|
upstreamPath: "foo/",
|
|
localPath: "upstream/",
|
|
refExists: false,
|
|
priorPropagation: false,
|
|
},
|
|
"with upstream path, directory in root, ref exists": {
|
|
upstreamPath: "foo",
|
|
localPath: "upstream",
|
|
refExists: true,
|
|
priorPropagation: false,
|
|
},
|
|
"with upstream path, directory in root, trailing slash, ref exists": {
|
|
upstreamPath: "foo/",
|
|
localPath: "upstream/",
|
|
refExists: true,
|
|
priorPropagation: false,
|
|
},
|
|
"with upstream path, directory in root, prior propagation exists": {
|
|
upstreamPath: "foo",
|
|
localPath: "upstream",
|
|
refExists: false,
|
|
priorPropagation: true,
|
|
},
|
|
"with upstream path, directory in root, trailing slash, prior propagation exists": {
|
|
upstreamPath: "foo/",
|
|
localPath: "upstream/",
|
|
refExists: false,
|
|
priorPropagation: true,
|
|
},
|
|
"with upstream path, directory in subdirectory, ref does not exist": {
|
|
upstreamPath: "foo",
|
|
localPath: "foo/upstream",
|
|
refExists: false,
|
|
priorPropagation: false,
|
|
},
|
|
"with upstream path, directory in subdirectory, trailing slash, ref does not exist": {
|
|
upstreamPath: "foo/",
|
|
localPath: "foo/upstream/",
|
|
refExists: false,
|
|
priorPropagation: false,
|
|
},
|
|
"with upstream path, directory in subdirectory, ref exists": {
|
|
upstreamPath: "foo",
|
|
localPath: "foo/upstream",
|
|
refExists: true,
|
|
priorPropagation: false,
|
|
},
|
|
"with upstream path, directory in subdirectory, trailing slash, ref exists": {
|
|
upstreamPath: "foo/",
|
|
localPath: "foo/upstream/",
|
|
refExists: true,
|
|
priorPropagation: false,
|
|
},
|
|
"with upstream path, directory in subdirectory, prior propagation exists": {
|
|
upstreamPath: "foo",
|
|
localPath: "foo/upstream",
|
|
refExists: false,
|
|
priorPropagation: true,
|
|
},
|
|
"with upstream path, directory in subdirectory, trailing slash, prior propagation exists": {
|
|
upstreamPath: "foo",
|
|
localPath: "foo/upstream/",
|
|
refExists: false,
|
|
priorPropagation: true,
|
|
},
|
|
"with upstream path as subdirectory, directory in root, ref does not exist": {
|
|
upstreamPath: "foobar/foo",
|
|
localPath: "upstream",
|
|
refExists: false,
|
|
priorPropagation: false,
|
|
},
|
|
"with upstream path as subdirectory, directory in root, trailing slash, ref does not exist": {
|
|
upstreamPath: "foobar/foo/",
|
|
localPath: "upstream/",
|
|
refExists: false,
|
|
priorPropagation: false,
|
|
},
|
|
"with upstream path as subdirectory, directory in root, ref exists": {
|
|
upstreamPath: "foobar/foo",
|
|
localPath: "upstream",
|
|
refExists: true,
|
|
priorPropagation: false,
|
|
},
|
|
"with upstream path as subdirectory, directory in root, trailing slash, ref exists": {
|
|
upstreamPath: "foobar/foo/",
|
|
localPath: "upstream/",
|
|
refExists: true,
|
|
priorPropagation: false,
|
|
},
|
|
"with upstream path as subdirectory, directory in root, prior propagation exists": {
|
|
upstreamPath: "foobar/foo",
|
|
localPath: "upstream",
|
|
refExists: false,
|
|
priorPropagation: true,
|
|
},
|
|
"with upstream path as subdirectory, directory in root, trailing slash, prior propagation exists": {
|
|
upstreamPath: "foobar/foo/",
|
|
localPath: "upstream/",
|
|
refExists: false,
|
|
priorPropagation: true,
|
|
},
|
|
"with upstream path as subdirectory, directory in subdirectory, ref does not exist": {
|
|
upstreamPath: "foobar/foo",
|
|
localPath: "foo/upstream",
|
|
refExists: false,
|
|
priorPropagation: false,
|
|
},
|
|
"with upstream path as subdirectory, directory in subdirectory, trailing slash, ref does not exist": {
|
|
upstreamPath: "foobar/foo/",
|
|
localPath: "foo/upstream/",
|
|
refExists: false,
|
|
priorPropagation: false,
|
|
},
|
|
"with upstream path as subdirectory, directory in subdirectory, ref exists": {
|
|
upstreamPath: "foobar/foo",
|
|
localPath: "foo/upstream",
|
|
refExists: true,
|
|
priorPropagation: false,
|
|
},
|
|
"with upstream path as subdirectory, directory in subdirectory, trailing slash, ref exists": {
|
|
upstreamPath: "foobar/foo/",
|
|
localPath: "foo/upstream/",
|
|
refExists: true,
|
|
priorPropagation: false,
|
|
},
|
|
"with upstream path as subdirectory, directory in subdirectory, prior propagation exists": {
|
|
upstreamPath: "foobar/foo",
|
|
localPath: "foo/upstream",
|
|
refExists: false,
|
|
priorPropagation: true,
|
|
},
|
|
"with upstream path as subdirectory, directory in subdirectory, trailing slash, prior propagation exists": {
|
|
upstreamPath: "foobar/foo/",
|
|
localPath: "foo/upstream/",
|
|
refExists: false,
|
|
priorPropagation: true,
|
|
},
|
|
"upstream path does not exist": {
|
|
upstreamPath: "does-not-exist",
|
|
localPath: "foo/upstream/",
|
|
err: ErrTreeDoesNotHavePath,
|
|
},
|
|
"empty localPath": {
|
|
err: ErrCannotCreateSubtreeIntoRootTree,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
require.False(t, test.refExists && test.priorPropagation, "refExists and priorPropagation can't both be true")
|
|
|
|
downstreamRef := testNameToRefName(name)
|
|
|
|
if test.refExists {
|
|
_, err := downstreamRepository.Commit(downstreamTreeID, downstreamRef, "Initial commit\n", false)
|
|
require.Nil(t, err)
|
|
} else if test.priorPropagation {
|
|
// We set the upstream path to contain the same tree as the
|
|
// downstreamTree, so:
|
|
// oof/a -> blobA
|
|
// b -> blobB
|
|
// <upstream>/oof/a -> blobA
|
|
// <upstream>/b -> blobB
|
|
|
|
entries := []TreeEntry{NewEntryTree(test.localPath, downstreamTreeID)}
|
|
entries = append(entries, downstreamTreeEntries...)
|
|
|
|
rootTreeID, err := downstreamTreeBuilder.WriteTreeFromEntries(entries)
|
|
require.Nil(t, err)
|
|
|
|
_, err = downstreamRepository.Commit(rootTreeID, downstreamRef, "Initial commit\n", false)
|
|
require.Nil(t, err)
|
|
}
|
|
|
|
downstreamCommitID, err := downstreamRepository.CreateSubtreeFromUpstreamRepository(upstreamRepository, upstreamCommitID, test.upstreamPath, downstreamRef, test.localPath)
|
|
if test.err != nil {
|
|
assert.ErrorIs(t, err, test.err)
|
|
} else {
|
|
assert.Nil(t, err)
|
|
|
|
rootTreeID, err := downstreamRepository.GetCommitTreeID(downstreamCommitID)
|
|
require.Nil(t, err)
|
|
|
|
itemID, err := downstreamRepository.GetPathIDInTree(rootTreeID, test.localPath)
|
|
require.Nil(t, err)
|
|
|
|
upstreamTreeID := upstreamRootTreeID
|
|
if test.upstreamPath != "" {
|
|
upstreamTreeID, err = upstreamRepository.GetPathIDInTree(upstreamRootTreeID, test.upstreamPath)
|
|
require.Nil(t, err)
|
|
}
|
|
assert.Equal(t, upstreamTreeID, itemID)
|
|
|
|
if test.refExists {
|
|
// check that other items are still present
|
|
itemID, err := downstreamRepository.GetPathIDInTree(downstreamTreeID, "oof/a")
|
|
require.Nil(t, err)
|
|
assert.Equal(t, blobAID, itemID)
|
|
|
|
itemID, err = downstreamRepository.GetPathIDInTree(downstreamTreeID, "b")
|
|
require.Nil(t, err)
|
|
assert.Equal(t, blobBID, itemID)
|
|
}
|
|
|
|
// We don't need to similarly check when test.priorPropagation is
|
|
// true because we already checked that those contents don't exist
|
|
// in that localPath when we checked the tree ID patches
|
|
// upstreamTreeID
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestTreeBuilder(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tempDir, false)
|
|
|
|
blobAID, err := repo.WriteBlob([]byte("a"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
blobBID, err := repo.WriteBlob([]byte("b"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
emptyTreeID := "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
|
|
|
t.Run("no blobs", func(t *testing.T) {
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
treeID, err := treeBuilder.WriteTreeFromEntries(nil)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, emptyTreeID, treeID.String())
|
|
|
|
treeID, err = treeBuilder.WriteTreeFromEntries(nil)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, emptyTreeID, treeID.String())
|
|
})
|
|
|
|
t.Run("both blobs in the root directory", func(t *testing.T) {
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
input := []TreeEntry{
|
|
NewEntryBlob("a", blobAID),
|
|
NewEntryBlob("b", blobBID),
|
|
}
|
|
|
|
rootTreeID, err := treeBuilder.WriteTreeFromEntries(input)
|
|
assert.Nil(t, err)
|
|
|
|
files, err := repo.GetAllFilesInTree(rootTreeID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expectedOutput := map[string]Hash{
|
|
"a": blobAID,
|
|
"b": blobBID,
|
|
}
|
|
assert.Equal(t, expectedOutput, files)
|
|
})
|
|
|
|
t.Run("both blobs in same subdirectory", func(t *testing.T) {
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
input := []TreeEntry{
|
|
NewEntryBlob("dir/a", blobAID),
|
|
NewEntryBlob("dir/b", blobBID),
|
|
}
|
|
|
|
rootTreeID, err := treeBuilder.WriteTreeFromEntries(input)
|
|
assert.Nil(t, err)
|
|
|
|
files, err := repo.GetAllFilesInTree(rootTreeID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expectedOutput := map[string]Hash{
|
|
"dir/a": blobAID,
|
|
"dir/b": blobBID,
|
|
}
|
|
|
|
assert.Equal(t, expectedOutput, files)
|
|
})
|
|
|
|
t.Run("same blobs in the multiple directories", func(t *testing.T) {
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
input := []TreeEntry{
|
|
NewEntryBlob("a", blobAID),
|
|
NewEntryBlob("b", blobBID),
|
|
NewEntryBlob("foo/a", blobAID),
|
|
NewEntryBlob("foo/b", blobBID),
|
|
NewEntryBlob("bar/a", blobAID),
|
|
NewEntryBlob("bar/b", blobBID),
|
|
}
|
|
|
|
rootTreeID, err := treeBuilder.WriteTreeFromEntries(input)
|
|
assert.Nil(t, err)
|
|
|
|
files, err := repo.GetAllFilesInTree(rootTreeID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expectedOutput := map[string]Hash{
|
|
"a": blobAID,
|
|
"b": blobBID,
|
|
"foo/a": blobAID,
|
|
"foo/b": blobBID,
|
|
"bar/a": blobAID,
|
|
"bar/b": blobBID,
|
|
}
|
|
|
|
assert.Equal(t, expectedOutput, files)
|
|
})
|
|
|
|
t.Run("both blobs in different subdirectories", func(t *testing.T) {
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
input := []TreeEntry{
|
|
NewEntryBlob("foo/a", blobAID),
|
|
NewEntryBlob("bar/b", blobBID),
|
|
}
|
|
|
|
rootTreeID, err := treeBuilder.WriteTreeFromEntries(input)
|
|
assert.Nil(t, err)
|
|
|
|
files, err := repo.GetAllFilesInTree(rootTreeID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expectedOutput := map[string]Hash{
|
|
"foo/a": blobAID,
|
|
"bar/b": blobBID,
|
|
}
|
|
|
|
assert.Equal(t, expectedOutput, files)
|
|
})
|
|
|
|
t.Run("blobs in mix of root directory and subdirectories", func(t *testing.T) {
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
input := []TreeEntry{
|
|
NewEntryBlob("a", blobAID),
|
|
NewEntryBlob("foo/bar/foobar/b", blobBID),
|
|
}
|
|
|
|
rootTreeID, err := treeBuilder.WriteTreeFromEntries(input)
|
|
assert.Nil(t, err)
|
|
|
|
files, err := repo.GetAllFilesInTree(rootTreeID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
expectedOutput := map[string]Hash{
|
|
"a": blobAID,
|
|
"foo/bar/foobar/b": blobBID,
|
|
}
|
|
|
|
assert.Equal(t, expectedOutput, files)
|
|
})
|
|
|
|
t.Run("build tree from intermediate tree", func(t *testing.T) {
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
intermediateTreeInput := []TreeEntry{
|
|
NewEntryBlob("a", blobAID),
|
|
}
|
|
|
|
intermediateTreeID, err := treeBuilder.WriteTreeFromEntries(intermediateTreeInput)
|
|
assert.Nil(t, err)
|
|
|
|
rootTreeInput := []TreeEntry{
|
|
NewEntryTree("intermediate", intermediateTreeID),
|
|
NewEntryBlob("b", blobBID),
|
|
}
|
|
|
|
rootTreeID, err := treeBuilder.WriteTreeFromEntries(rootTreeInput)
|
|
assert.Nil(t, err)
|
|
|
|
expectedFiles := map[string]Hash{
|
|
"intermediate/a": blobAID,
|
|
"b": blobBID,
|
|
}
|
|
|
|
files, err := repo.GetAllFilesInTree(rootTreeID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
assert.Equal(t, expectedFiles, files)
|
|
})
|
|
|
|
t.Run("build tree from nested intermediate tree", func(t *testing.T) {
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
intermediateTreeInput := []TreeEntry{
|
|
NewEntryBlob("a", blobAID),
|
|
}
|
|
|
|
intermediateTreeID, err := treeBuilder.WriteTreeFromEntries(intermediateTreeInput)
|
|
assert.Nil(t, err)
|
|
|
|
rootTreeInput := []TreeEntry{
|
|
NewEntryTree("foo/intermediate", intermediateTreeID),
|
|
NewEntryBlob("b", blobBID),
|
|
}
|
|
|
|
rootTreeID, err := treeBuilder.WriteTreeFromEntries(rootTreeInput)
|
|
assert.Nil(t, err)
|
|
|
|
expectedFiles := map[string]Hash{
|
|
"foo/intermediate/a": blobAID,
|
|
"b": blobBID,
|
|
}
|
|
|
|
files, err := repo.GetAllFilesInTree(rootTreeID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
assert.Equal(t, expectedFiles, files)
|
|
})
|
|
|
|
t.Run("build tree from nested multi-level intermediate tree", func(t *testing.T) {
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
|
|
intermediateTreeInput := []TreeEntry{
|
|
NewEntryBlob("intermediate/a", blobAID),
|
|
}
|
|
|
|
intermediateTreeID, err := treeBuilder.WriteTreeFromEntries(intermediateTreeInput)
|
|
assert.Nil(t, err)
|
|
|
|
rootTreeInput := []TreeEntry{
|
|
NewEntryTree("foo/intermediate", intermediateTreeID),
|
|
NewEntryBlob("b", blobBID),
|
|
}
|
|
|
|
rootTreeID, err := treeBuilder.WriteTreeFromEntries(rootTreeInput)
|
|
assert.Nil(t, err)
|
|
|
|
expectedFiles := map[string]Hash{
|
|
"foo/intermediate/intermediate/a": blobAID,
|
|
"b": blobBID,
|
|
}
|
|
|
|
files, err := repo.GetAllFilesInTree(rootTreeID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
assert.Equal(t, expectedFiles, files)
|
|
})
|
|
}
|
|
|
|
func TestEnsureIsTree(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, true)
|
|
|
|
blobID, err := repo.WriteBlob([]byte("foo"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
treeBuilder := NewTreeBuilder(repo)
|
|
treeID, err := treeBuilder.WriteTreeFromEntries([]TreeEntry{NewEntryBlob("foo", blobID)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = repo.ensureIsTree(treeID)
|
|
assert.Nil(t, err)
|
|
|
|
err = repo.ensureIsTree(blobID)
|
|
assert.NotNil(t, err)
|
|
|
|
err = repo.ensureIsTree(ZeroHash)
|
|
assert.ErrorContains(t, err, "unable to inspect if object is tree")
|
|
}
|
|
|
|
func TestGetAllFilesInTree(t *testing.T) {
|
|
t.Run("error cases", func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false)
|
|
|
|
blobID, err := repo.WriteBlob([]byte("a"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
_, err = repo.GetAllFilesInTree(blobID)
|
|
assert.ErrorContains(t, err, "unable to enumerate all files in tree")
|
|
|
|
_, err = repo.GetAllFilesInTree(ZeroHash)
|
|
assert.ErrorContains(t, err, "unable to enumerate all files in tree")
|
|
})
|
|
}
|
|
|
|
func TestWriteTree(t *testing.T) {
|
|
t.Parallel()
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false)
|
|
|
|
blobAID, err := repo.WriteBlob([]byte("a"))
|
|
require.Nil(t, err)
|
|
blobBID, err := repo.WriteBlob([]byte("b"))
|
|
require.Nil(t, err)
|
|
|
|
treeID, err := repo.WriteTree([]TreeEntry{
|
|
NewEntryBlob("a", blobAID),
|
|
NewEntryBlob("dir/b", blobBID),
|
|
})
|
|
assert.Nil(t, err)
|
|
|
|
files, err := repo.GetAllFilesInTree(treeID)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, map[string]Hash{"a": blobAID, "dir/b": blobBID}, files)
|
|
|
|
emptyTreeID, err := repo.WriteTree(nil)
|
|
assert.Nil(t, err)
|
|
expectedEmpty, err := repo.EmptyTree()
|
|
require.Nil(t, err)
|
|
assert.Equal(t, expectedEmpty, emptyTreeID)
|
|
|
|
// subtree case: write a tree with a blob, then graft it as a subtree
|
|
blobCID, err := repo.WriteBlob([]byte("c"))
|
|
require.Nil(t, err)
|
|
|
|
firstTreeID, err := repo.WriteTree([]TreeEntry{NewEntryBlob("c", blobCID)})
|
|
require.Nil(t, err)
|
|
|
|
combinedTreeID, err := repo.WriteTree([]TreeEntry{
|
|
NewEntryBlob("a", blobAID),
|
|
NewEntryTree("nested", firstTreeID),
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
allFiles, err := repo.GetAllFilesInTree(combinedTreeID)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, map[string]Hash{
|
|
"a": blobAID,
|
|
"nested/c": blobCID,
|
|
}, allFiles)
|
|
|
|
// duplicate path is rejected
|
|
_, err = repo.WriteTree([]TreeEntry{
|
|
NewEntryBlob("a", blobAID),
|
|
NewEntryBlob("a", blobBID),
|
|
})
|
|
assert.ErrorIs(t, err, gitstore.ErrDuplicateTreePath)
|
|
}
|
|
|
|
func TestWriteTreeDeterministic(t *testing.T) {
|
|
t.Parallel()
|
|
tmpDir := t.TempDir()
|
|
repo := CreateTestGitRepository(t, tmpDir, false)
|
|
|
|
entries := []TreeEntry{}
|
|
for _, name := range []string{"a", "b", "c", "d", "e", "dir/f", "dir/g"} {
|
|
blobID, err := repo.WriteBlob([]byte(name))
|
|
require.Nil(t, err)
|
|
entries = append(entries, NewEntryBlob(name, blobID))
|
|
}
|
|
|
|
// The tree ID must not depend on entry order.
|
|
first, err := repo.WriteTree(entries)
|
|
require.Nil(t, err)
|
|
|
|
reversed := make([]TreeEntry, len(entries))
|
|
for i, entry := range entries {
|
|
reversed[len(entries)-1-i] = entry
|
|
}
|
|
next, err := repo.WriteTree(reversed)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, first, next)
|
|
}
|