diff options
Diffstat (limited to 'object')
| -rw-r--r-- | object/blob_parse_test.go | 27 | ||||
| -rw-r--r-- | object/blob_serialize_test.go | 27 | ||||
| -rw-r--r-- | object/commit_parse_test.go | 38 | ||||
| -rw-r--r-- | object/commit_serialize_test.go | 31 | ||||
| -rw-r--r-- | object/tag_parse_test.go | 39 | ||||
| -rw-r--r-- | object/tag_serialize_test.go | 32 | ||||
| -rw-r--r-- | object/tree_helpers_test.go | 125 | ||||
| -rw-r--r-- | object/tree_parse_test.go | 66 | ||||
| -rw-r--r-- | object/tree_serialize_test.go | 60 |
9 files changed, 445 insertions, 0 deletions
diff --git a/object/blob_parse_test.go b/object/blob_parse_test.go new file mode 100644 index 00000000..02c059ca --- /dev/null +++ b/object/blob_parse_test.go @@ -0,0 +1,27 @@ +package object_test + +import ( + "bytes" + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/object" + "codeberg.org/lindenii/furgit/oid" +) + +func TestBlobParseFromGit(t *testing.T) { + testgit.ForEachAlgorithm(t, func(t *testing.T, algo oid.Algorithm) { + repo := testgit.NewBareRepo(t, algo) + body := []byte("hello\nblob\n") + blobID := repo.HashObject(t, "blob", body) + + rawBody := repo.CatFile(t, "blob", blobID) + blob, err := object.ParseBlob(rawBody) + if err != nil { + t.Fatalf("ParseBlob: %v", err) + } + if !bytes.Equal(blob.Data, body) { + t.Fatalf("blob body mismatch") + } + }) +} diff --git a/object/blob_serialize_test.go b/object/blob_serialize_test.go new file mode 100644 index 00000000..f0895556 --- /dev/null +++ b/object/blob_serialize_test.go @@ -0,0 +1,27 @@ +package object_test + +import ( + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/object" + "codeberg.org/lindenii/furgit/oid" +) + +func TestBlobSerialize(t *testing.T) { + testgit.ForEachAlgorithm(t, func(t *testing.T, algo oid.Algorithm) { + repo := testgit.NewBareRepo(t, algo) + body := []byte("hello\nblob\n") + wantID := repo.HashObject(t, "blob", body) + + blob := &object.Blob{Data: body} + rawObj, err := blob.Serialize() + if err != nil { + t.Fatalf("Serialize: %v", err) + } + gotID := algo.Sum(rawObj) + if gotID != wantID { + t.Fatalf("object id mismatch: got %s want %s", gotID, wantID) + } + }) +} diff --git a/object/commit_parse_test.go b/object/commit_parse_test.go new file mode 100644 index 00000000..8d1f192d --- /dev/null +++ b/object/commit_parse_test.go @@ -0,0 +1,38 @@ +package object_test + +import ( + "bytes" + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/object" + "codeberg.org/lindenii/furgit/oid" +) + +func TestCommitParseFromGit(t *testing.T) { + testgit.ForEachAlgorithm(t, func(t *testing.T, algo oid.Algorithm) { + repo := testgit.NewBareRepo(t, algo) + _, treeID, commitID := repo.MakeCommit(t, "subject\n\nbody") + + rawBody := repo.CatFile(t, "commit", commitID) + commit, err := object.ParseCommit(rawBody, algo) + if err != nil { + t.Fatalf("ParseCommit: %v", err) + } + if commit.Tree != treeID { + t.Fatalf("tree id mismatch: got %s want %s", commit.Tree, treeID) + } + if len(commit.Parents) != 0 { + t.Fatalf("parent count = %d, want 0", len(commit.Parents)) + } + if !bytes.Equal(commit.Author.Name, []byte("Test Author")) { + t.Fatalf("author name = %q, want %q", commit.Author.Name, "Test Author") + } + if !bytes.Equal(commit.Committer.Name, []byte("Test Committer")) { + t.Fatalf("committer name = %q, want %q", commit.Committer.Name, "Test Committer") + } + if !bytes.Contains(commit.Message, []byte("subject")) { + t.Fatalf("commit message missing subject: %q", commit.Message) + } + }) +} diff --git a/object/commit_serialize_test.go b/object/commit_serialize_test.go new file mode 100644 index 00000000..3f6eb4aa --- /dev/null +++ b/object/commit_serialize_test.go @@ -0,0 +1,31 @@ +package object_test + +import ( + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/object" + "codeberg.org/lindenii/furgit/oid" +) + +func TestCommitSerialize(t *testing.T) { + testgit.ForEachAlgorithm(t, func(t *testing.T, algo oid.Algorithm) { + repo := testgit.NewBareRepo(t, algo) + _, _, commitID := repo.MakeCommit(t, "subject\n\nbody") + + rawBody := repo.CatFile(t, "commit", commitID) + commit, err := object.ParseCommit(rawBody, algo) + if err != nil { + t.Fatalf("ParseCommit: %v", err) + } + + rawObj, err := commit.Serialize() + if err != nil { + t.Fatalf("Serialize: %v", err) + } + gotID := algo.Sum(rawObj) + if gotID != commitID { + t.Fatalf("commit id mismatch: got %s want %s", gotID, commitID) + } + }) +} diff --git a/object/tag_parse_test.go b/object/tag_parse_test.go new file mode 100644 index 00000000..818ea309 --- /dev/null +++ b/object/tag_parse_test.go @@ -0,0 +1,39 @@ +package object_test + +import ( + "bytes" + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/object" + "codeberg.org/lindenii/furgit/oid" +) + +func TestTagParseFromGit(t *testing.T) { + testgit.ForEachAlgorithm(t, func(t *testing.T, algo oid.Algorithm) { + repo := testgit.NewBareRepo(t, algo) + _, _, commitID := repo.MakeCommit(t, "subject\n\nbody") + tagID := repo.TagAnnotated(t, "v1", commitID, "tag message") + + rawBody := repo.CatFile(t, "tag", tagID) + tag, err := object.ParseTag(rawBody, algo) + if err != nil { + t.Fatalf("ParseTag: %v", err) + } + if tag.Target != commitID { + t.Fatalf("tag target mismatch: got %s want %s", tag.Target, commitID) + } + if tag.TargetType != object.TypeCommit { + t.Fatalf("tag target type = %v, want %v", tag.TargetType, object.TypeCommit) + } + if !bytes.Equal(tag.Name, []byte("v1")) { + t.Fatalf("tag name = %q, want %q", tag.Name, "v1") + } + if tag.Tagger == nil { + t.Fatalf("expected tagger") + } + if !bytes.Contains(tag.Message, []byte("tag message")) { + t.Fatalf("tag message mismatch: %q", tag.Message) + } + }) +} diff --git a/object/tag_serialize_test.go b/object/tag_serialize_test.go new file mode 100644 index 00000000..40e346f8 --- /dev/null +++ b/object/tag_serialize_test.go @@ -0,0 +1,32 @@ +package object_test + +import ( + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/object" + "codeberg.org/lindenii/furgit/oid" +) + +func TestTagSerialize(t *testing.T) { + testgit.ForEachAlgorithm(t, func(t *testing.T, algo oid.Algorithm) { + repo := testgit.NewBareRepo(t, algo) + _, _, commitID := repo.MakeCommit(t, "subject\n\nbody") + tagID := repo.TagAnnotated(t, "v1", commitID, "tag message") + + rawBody := repo.CatFile(t, "tag", tagID) + tag, err := object.ParseTag(rawBody, algo) + if err != nil { + t.Fatalf("ParseTag: %v", err) + } + + rawObj, err := tag.Serialize() + if err != nil { + t.Fatalf("Serialize: %v", err) + } + gotID := algo.Sum(rawObj) + if gotID != tagID { + t.Fatalf("tag id mismatch: got %s want %s", gotID, tagID) + } + }) +} diff --git a/object/tree_helpers_test.go b/object/tree_helpers_test.go new file mode 100644 index 00000000..ce7160d6 --- /dev/null +++ b/object/tree_helpers_test.go @@ -0,0 +1,125 @@ +package object_test + +import ( + "bytes" + "fmt" + "strings" + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/object" +) + +func mktreeTypeFromMode(t *testing.T, mode object.FileMode) string { + t.Helper() + switch mode { + case object.FileModeDir: + return "tree" + case object.FileModeRegular, object.FileModeExecutable, object.FileModeSymlink: + return "blob" + case object.FileModeGitlink: + return "commit" + default: + t.Fatalf("unsupported file mode: %o", mode) + return "" + } +} + +func buildGitMktreeInput(entries []object.TreeEntry) string { + var b strings.Builder + for _, e := range entries { + fmt.Fprintf(&b, "%o %s %s\t%s\n", e.Mode, mktreeTypeFromModeNoTB(e.Mode), e.ID.String(), e.Name) + } + return b.String() +} + +func mktreeTypeFromModeNoTB(mode object.FileMode) string { + switch mode { + case object.FileModeDir: + return "tree" + case object.FileModeRegular, object.FileModeExecutable, object.FileModeSymlink: + return "blob" + case object.FileModeGitlink: + return "commit" + default: + return "" + } +} + +func gitLsTreeNames(out []byte) [][]byte { + if len(out) == 0 { + return nil + } + parts := bytes.Split(out, []byte{0}) + if len(parts) > 0 && len(parts[len(parts)-1]) == 0 { + parts = parts[:len(parts)-1] + } + names := make([][]byte, 0, len(parts)) + for _, name := range parts { + names = append(names, append([]byte(nil), name...)) + } + return names +} + +func adversarialRootEntries(t *testing.T, repo *testgit.TestRepo) []object.TreeEntry { + t.Helper() + + blobA := repo.HashObject(t, "blob", []byte("blob-A\n")) + blobB := repo.HashObject(t, "blob", []byte("blob-B\n")) + blobC := repo.HashObject(t, "blob", []byte("blob-C\n")) + + subDirA := repo.Mktree(t, + fmt.Sprintf("100644 blob %s\tnested-a.txt\n100755 blob %s\trun-a.sh\n", blobA.String(), blobB.String())) + subDirB := repo.Mktree(t, + fmt.Sprintf("100644 blob %s\tnested-b.txt\n100644 blob %s\tz-last\n", blobB.String(), blobC.String())) + subDirC := repo.Mktree(t, + fmt.Sprintf("120000 blob %s\tlink-c\n100644 blob %s\tchild\n", blobC.String(), blobA.String())) + subDirD := repo.Mktree(t, + fmt.Sprintf("100644 blob %s\tleaf\n", blobA.String())) + + return []object.TreeEntry{ + {Mode: object.FileModeRegular, Name: []byte("z"), ID: blobA}, + {Mode: object.FileModeRegular, Name: []byte("A"), ID: blobB}, + {Mode: object.FileModeRegular, Name: []byte("aa"), ID: blobC}, + {Mode: object.FileModeRegular, Name: []byte("a0"), ID: blobA}, + {Mode: object.FileModeRegular, Name: []byte("a-"), ID: blobB}, + {Mode: object.FileModeRegular, Name: []byte("a."), ID: blobC}, + {Mode: object.FileModeRegular, Name: []byte("a_"), ID: blobA}, + {Mode: object.FileModeRegular, Name: []byte("a~"), ID: blobB}, + {Mode: object.FileModeRegular, Name: []byte("Z"), ID: blobC}, + {Mode: object.FileModeRegular, Name: []byte("0"), ID: blobA}, + {Mode: object.FileModeRegular, Name: []byte("9"), ID: blobB}, + {Mode: object.FileModeRegular, Name: []byte("00"), ID: blobC}, + {Mode: object.FileModeRegular, Name: []byte("这是一些非 ASCII 的字符"), ID: blobC}, + {Mode: object.FileModeRegular, Name: []byte("是新进入 Unicode 的字符"), ID: blobC}, + {Mode: object.FileModeRegular, Name: []byte("Emoji 👀"), ID: blobC}, + {Mode: object.FileModeRegular, Name: []byte("_"), ID: blobA}, + {Mode: object.FileModeRegular, Name: []byte("-dash"), ID: blobB}, + {Mode: object.FileModeRegular, Name: []byte("dot.file"), ID: blobC}, + {Mode: object.FileModeRegular, Name: []byte(".hidden"), ID: blobA}, + {Mode: object.FileModeRegular, Name: []byte("CAPS"), ID: blobB}, + {Mode: object.FileModeRegular, Name: []byte("caps"), ID: blobC}, + {Mode: object.FileModeRegular, Name: []byte("mixCase"), ID: blobA}, + {Mode: object.FileModeRegular, Name: []byte("name with space"), ID: blobB}, + {Mode: object.FileModeRegular, Name: []byte("name-with-dash"), ID: blobC}, + {Mode: object.FileModeRegular, Name: []byte("name.with.dot"), ID: blobA}, + {Mode: object.FileModeRegular, Name: []byte("name_with_underscore"), ID: blobB}, + {Mode: object.FileModeRegular, Name: []byte("tilde~name"), ID: blobC}, + {Mode: object.FileModeRegular, Name: []byte("brace{name}"), ID: blobA}, + {Mode: object.FileModeRegular, Name: []byte("plus+name"), ID: blobB}, + {Mode: object.FileModeRegular, Name: []byte("equal=name"), ID: blobC}, + {Mode: object.FileModeRegular, Name: []byte("at@name"), ID: blobA}, + {Mode: object.FileModeRegular, Name: []byte("percent%name"), ID: blobB}, + {Mode: object.FileModeRegular, Name: []byte("caret^name"), ID: blobC}, + {Mode: object.FileModeRegular, Name: []byte("comma,name"), ID: blobA}, + {Mode: object.FileModeRegular, Name: []byte("semi;name"), ID: blobB}, + {Mode: object.FileModeRegular, Name: []byte("paren(name)"), ID: blobC}, + {Mode: object.FileModeRegular, Name: []byte("bracket[name]"), ID: blobA}, + {Mode: object.FileModeExecutable, Name: []byte("exec.sh"), ID: blobB}, + {Mode: object.FileModeSymlink, Name: []byte("sym.link"), ID: blobC}, + {Mode: object.FileModeDir, Name: []byte("dir"), ID: subDirA}, + {Mode: object.FileModeDir, Name: []byte("dir0"), ID: subDirB}, + {Mode: object.FileModeDir, Name: []byte("dir.space"), ID: subDirC}, + {Mode: object.FileModeDir, Name: []byte("x"), ID: subDirD}, + } +} diff --git a/object/tree_parse_test.go b/object/tree_parse_test.go new file mode 100644 index 00000000..bbe7c69b --- /dev/null +++ b/object/tree_parse_test.go @@ -0,0 +1,66 @@ +package object_test + +import ( + "bytes" + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/object" + "codeberg.org/lindenii/furgit/oid" +) + +func TestTreeParseFromGit(t *testing.T) { + testgit.ForEachAlgorithm(t, func(t *testing.T, algo oid.Algorithm) { + repo := testgit.NewBareRepo(t, algo) + entries := adversarialRootEntries(t, repo) + inserted := &object.Tree{} + for _, entry := range entries { + if err := inserted.InsertEntry(entry); err != nil { + t.Fatalf("InsertEntry(%q): %v", entry.Name, err) + } + } + + treeID := repo.Mktree(t, buildGitMktreeInput(inserted.Entries)) + + rawBody := repo.CatFile(t, "tree", treeID) + tree, err := object.ParseTree(rawBody, algo) + if err != nil { + t.Fatalf("ParseTree: %v", err) + } + if len(tree.Entries) != len(inserted.Entries) { + t.Fatalf("entry count = %d, want %d", len(tree.Entries), len(inserted.Entries)) + } + + for i := range inserted.Entries { + got := tree.Entries[i] + want := inserted.Entries[i] + if got.Mode != want.Mode || got.ID != want.ID || !bytes.Equal(got.Name, want.Name) { + t.Fatalf("entry[%d] mismatch: got (%o,%q,%s) want (%o,%q,%s)", + i, got.Mode, got.Name, got.ID, want.Mode, want.Name, want.ID) + } + } + + lsNames := gitLsTreeNames(repo.RunBytes(t, "ls-tree", "--name-only", "-z", treeID.String())) + if len(lsNames) != len(tree.Entries) { + t.Fatalf("ls-tree names = %d, want %d", len(lsNames), len(tree.Entries)) + } + for i := range lsNames { + if !bytes.Equal(lsNames[i], tree.Entries[i].Name) { + t.Fatalf("ordering mismatch at %d: git=%q parsed=%q", i, lsNames[i], tree.Entries[i].Name) + } + } + + for _, want := range inserted.Entries { + got := tree.Entry(want.Name) + if got == nil { + t.Fatalf("Entry(%q) returned nil", want.Name) + } + if got.Mode != want.Mode || got.ID != want.ID { + t.Fatalf("Entry(%q) mismatch", want.Name) + } + } + if tree.Entry([]byte("does-not-exist")) != nil { + t.Fatalf("Entry on missing name should be nil") + } + }) +} diff --git a/object/tree_serialize_test.go b/object/tree_serialize_test.go new file mode 100644 index 00000000..a0da877c --- /dev/null +++ b/object/tree_serialize_test.go @@ -0,0 +1,60 @@ +package object_test + +import ( + "errors" + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/object" + "codeberg.org/lindenii/furgit/oid" +) + +func TestTreeSerialize(t *testing.T) { + testgit.ForEachAlgorithm(t, func(t *testing.T, algo oid.Algorithm) { + repo := testgit.NewBareRepo(t, algo) + entries := adversarialRootEntries(t, repo) + tree := &object.Tree{} + + for i := len(entries) - 1; i >= 0; i-- { + if err := tree.InsertEntry(entries[i]); err != nil { + t.Fatalf("InsertEntry(%q): %v", entries[i].Name, err) + } + } + if len(tree.Entries) < 32 { + t.Fatalf("expected at least 32 entries, got %d", len(tree.Entries)) + } + + dup := tree.Entries[0] + if err := tree.InsertEntry(dup); err == nil { + t.Fatalf("duplicate InsertEntry should fail") + } + + removed := tree.Entries[len(tree.Entries)/2] + if err := tree.RemoveEntry(removed.Name); err != nil { + t.Fatalf("RemoveEntry(%q): %v", removed.Name, err) + } + if tree.Entry(removed.Name) != nil { + t.Fatalf("Entry(%q) should be nil after remove", removed.Name) + } + if err := tree.RemoveEntry([]byte("no-such-entry")); !errors.Is(err, object.ErrNotFound) { + t.Fatalf("RemoveEntry missing err = %v, want ErrNotFound", err) + } + if err := tree.InsertEntry(removed); err != nil { + t.Fatalf("re-InsertEntry(%q): %v", removed.Name, err) + } + if tree.Entry(removed.Name) == nil { + t.Fatalf("Entry(%q) should exist after reinsert", removed.Name) + } + + wantTreeID := repo.Mktree(t, buildGitMktreeInput(tree.Entries)) + + rawObj, err := tree.Serialize() + if err != nil { + t.Fatalf("Serialize: %v", err) + } + gotTreeID := algo.Sum(rawObj) + if gotTreeID != wantTreeID { + t.Fatalf("tree id mismatch: got %s want %s", gotTreeID, wantTreeID) + } + }) +} |
