ref: 6a7fc936c4a969aa05b3941feedafe59f4bd2ffd
parent: c9eefd50557a5436da84e0a38ee96c812d453336
author: Runxi Yu <me@runxiyu.org>
date: Sat Feb 21 10:54:26 EST 2026
*: Add more tests
--- a/object/commit_parse_test.go
+++ b/object/commit_parse_test.go
@@ -2,6 +2,7 @@
import (
"bytes"
+ "fmt"
"testing"
"codeberg.org/lindenii/furgit/internal/testgit"
@@ -34,6 +35,46 @@
}
if !bytes.Contains(commit.Message, []byte("subject")) { t.Fatalf("commit message missing subject: %q", commit.Message)+ }
+ })
+}
+
+func TestCommitParseMultipleParents(t *testing.T) {+ t.Parallel()
+ testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper+ testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})+
+ _, treeID := testRepo.MakeSingleFileTree(t, "file.txt", []byte("merge-content\n"))+ parent1 := testRepo.CommitTree(t, treeID, "parent-one")
+ parent2 := testRepo.CommitTree(t, treeID, "parent-two", parent1)
+
+ rawCommit := fmt.Sprintf(
+ "tree %s\nparent %s\nparent %s\nauthor Test Author <test@example.org> 1234567890 +0000\ncommitter Test Committer <committer@example.org> 1234567890 +0000\n\nMerge commit\n",
+ treeID,
+ parent1,
+ parent2,
+ )
+ mergeID := testRepo.HashObject(t, "commit", []byte(rawCommit))
+ rawBody := testRepo.CatFile(t, "commit", mergeID)
+
+ commit, err := object.ParseCommit(rawBody, algo)
+ if err != nil {+ t.Fatalf("ParseCommit(merge): %v", err)+ }
+ if commit.Tree != treeID {+ t.Fatalf("merge tree = %s, want %s", commit.Tree, treeID)+ }
+ if len(commit.Parents) != 2 {+ t.Fatalf("merge parent count = %d, want 2", len(commit.Parents))+ }
+ if commit.Parents[0] != parent1 {+ t.Fatalf("merge parent[0] = %s, want %s", commit.Parents[0], parent1)+ }
+ if commit.Parents[1] != parent2 {+ t.Fatalf("merge parent[1] = %s, want %s", commit.Parents[1], parent2)+ }
+ if !bytes.Equal(commit.Message, []byte("Merge commit\n")) {+ t.Fatalf("merge message = %q, want %q", commit.Message, "Merge commit\n")}
})
}
--- a/refstore/loose/loose_test.go
+++ b/refstore/loose/loose_test.go
@@ -133,6 +133,70 @@
})
}
+func TestLooseListPatternMatrix(t *testing.T) {+ t.Parallel()
+ testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper+ testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})+ _, _, commitID := testRepo.MakeCommit(t, "loose refs pattern matrix")
+ testRepo.UpdateRef(t, "refs/heads/main", commitID)
+ testRepo.UpdateRef(t, "refs/heads/feature/one", commitID)
+ testRepo.UpdateRef(t, "refs/notes/review", commitID)
+ testRepo.UpdateRef(t, "refs/tags/v1", commitID)
+ testRepo.SymbolicRef(t, "HEAD", "refs/heads/main")
+
+ store := openLooseStore(t, testRepo.Dir(), algo)
+
+ tests := []struct {+ pattern string
+ want []string
+ }{+ {+ pattern: "refs/heads/*",
+ want: []string{"refs/heads/main"},+ },
+ {+ pattern: "refs/heads/*/*",
+ want: []string{"refs/heads/feature/one"},+ },
+ {+ pattern: "refs/*/feature/one",
+ want: []string{"refs/heads/feature/one"},+ },
+ {+ pattern: "refs/heads/feat?re/one",
+ want: []string{"refs/heads/feature/one"},+ },
+ {+ pattern: "refs/tags/v[0-9]",
+ want: []string{"refs/tags/v1"},+ },
+ {+ pattern: "refs/*/*",
+ want: []string{"refs/heads/main", "refs/notes/review", "refs/tags/v1"},+ },
+ }
+
+ for _, tt := range tests {+ t.Run(tt.pattern, func(t *testing.T) {+ got, err := store.List(tt.pattern)
+ if err != nil {+ t.Fatalf("List(%q): %v", tt.pattern, err)+ }
+ gotNames := make([]string, 0, len(got))
+ for _, entry := range got {+ gotNames = append(gotNames, entry.Name())
+ }
+ slices.Sort(gotNames)
+ wantNames := append([]string(nil), tt.want...)
+ slices.Sort(wantNames)
+ if !slices.Equal(gotNames, wantNames) {+ t.Fatalf("List(%q) names = %v, want %v", tt.pattern, gotNames, wantNames)+ }
+ })
+ }
+ })
+}
+
func TestLooseMalformedDetachedRef(t *testing.T) {t.Parallel()
testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper--- a/refstore/packed/packed_test.go
+++ b/refstore/packed/packed_test.go
@@ -133,6 +133,67 @@
})
}
+func TestPackedListPatternMatrix(t *testing.T) {+ t.Parallel()
+ testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper+ testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})+ _, _, commitID := testRepo.MakeCommit(t, "packed refs pattern matrix")
+ testRepo.UpdateRef(t, "refs/heads/main", commitID)
+ testRepo.UpdateRef(t, "refs/heads/feature/one", commitID)
+ testRepo.UpdateRef(t, "refs/notes/review", commitID)
+ testRepo.UpdateRef(t, "refs/tags/v1", commitID)
+ testRepo.PackRefs(t, "--all", "--prune")
+
+ store := openPackedRefStoreFromRepo(t, testRepo.Dir(), algo)
+
+ tests := []struct {+ pattern string
+ want []string
+ }{+ {+ pattern: "refs/heads/*",
+ want: []string{"refs/heads/main"},+ },
+ {+ pattern: "refs/heads/*/*",
+ want: []string{"refs/heads/feature/one"},+ },
+ {+ pattern: "refs/*/feature/one",
+ want: []string{"refs/heads/feature/one"},+ },
+ {+ pattern: "refs/heads/feat?re/one",
+ want: []string{"refs/heads/feature/one"},+ },
+ {+ pattern: "refs/tags/v[0-9]",
+ want: []string{"refs/tags/v1"},+ },
+ {+ pattern: "refs/*/*",
+ want: []string{"refs/heads/main", "refs/notes/review", "refs/tags/v1"},+ },
+ }
+
+ for _, tt := range tests {+ t.Run(tt.pattern, func(t *testing.T) {+ got, err := store.List(tt.pattern)
+ if err != nil {+ t.Fatalf("List(%q): %v", tt.pattern, err)+ }
+ gotNames := refNames(got)
+ slices.Sort(gotNames)
+ wantNames := append([]string(nil), tt.want...)
+ slices.Sort(wantNames)
+ if !slices.Equal(gotNames, wantNames) {+ t.Fatalf("List(%q) names = %v, want %v", tt.pattern, gotNames, wantNames)+ }
+ })
+ }
+ })
+}
+
func TestPackedParseErrors(t *testing.T) {t.Parallel()
testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper@@ -173,6 +234,14 @@
if _, err := packed.New(nil, objectid.AlgorithmSHA1); err == nil { t.Fatalf("packed.New nil reader expected error")}
+}
+
+func refNames(refs []ref.Ref) []string {+ names := make([]string, 0, len(refs))
+ for _, entry := range refs {+ names = append(names, entry.Name())
+ }
+ return names
}
func stringsOfLen(ch string, n int) string {--- a/repository/refs_test.go
+++ b/repository/refs_test.go
@@ -94,3 +94,75 @@
}
})
}
+
+func TestListRefsLooseOverridesPacked(t *testing.T) {+ t.Parallel()
+
+ testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper+ repoHarness := testgit.NewRepo(t, testgit.RepoOptions{+ ObjectFormat: algo,
+ Bare: true,
+ RefFormat: "files",
+ })
+
+ repoHarness.SymbolicRef(t, "HEAD", "refs/heads/main")
+ _, _, commit1 := repoHarness.MakeCommit(t, "commit-one")
+ repoHarness.UpdateRef(t, "refs/heads/main", commit1)
+ repoHarness.UpdateRef(t, "refs/heads/feature", commit1)
+ repoHarness.PackRefs(t, "--all", "--prune")
+
+ _, _, commit2 := repoHarness.MakeCommit(t, "commit-two")
+ repoHarness.UpdateRef(t, "refs/heads/main", commit2)
+
+ repo, err := repository.Open(repoHarness.Dir())
+ if err != nil {+ t.Fatalf("repository.Open: %v", err)+ }
+ defer func() { _ = repo.Close() }()+
+ mainRef, err := repo.ResolveRefFully("refs/heads/main")+ if err != nil {+ t.Fatalf("ResolveRefFully(main): %v", err)+ }
+ if mainRef.ID != commit2 {+ t.Fatalf("ResolveRefFully(main) id = %s, want %s", mainRef.ID, commit2)+ }
+
+ refs, err := repo.ListRefs("refs/heads/*")+ if err != nil {+ t.Fatalf("ListRefs(refs/heads/*): %v", err)+ }
+ byName := make(map[string]ref.Ref, len(refs))
+ for _, entry := range refs {+ name := entry.Name()
+ if _, exists := byName[name]; exists {+ t.Fatalf("duplicate ref %q in ListRefs output", name)+ }
+ byName[name] = entry
+ }
+
+ main, ok := byName["refs/heads/main"]
+ if !ok {+ t.Fatalf("missing refs/heads/main in ListRefs output")+ }
+ mainDetached, ok := main.(ref.Detached)
+ if !ok {+ t.Fatalf("refs/heads/main type = %T, want ref.Detached", main)+ }
+ if mainDetached.ID != commit2 {+ t.Fatalf("refs/heads/main id = %s, want %s", mainDetached.ID, commit2)+ }
+
+ feature, ok := byName["refs/heads/feature"]
+ if !ok {+ t.Fatalf("missing refs/heads/feature in ListRefs output")+ }
+ featureDetached, ok := feature.(ref.Detached)
+ if !ok {+ t.Fatalf("refs/heads/feature type = %T, want ref.Detached", feature)+ }
+ if featureDetached.ID != commit1 {+ t.Fatalf("refs/heads/feature id = %s, want %s", featureDetached.ID, commit1)+ }
+ })
+}
--- a/repository/stored_test.go
+++ b/repository/stored_test.go
@@ -161,3 +161,106 @@
})
})
}
+
+func TestResolveTreeEntryDeepPath(t *testing.T) {+ t.Parallel()
+
+ testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper+ const depth = 50
+
+ repoHarness := testgit.NewRepo(t, testgit.RepoOptions{+ ObjectFormat: algo,
+ Bare: true,
+ RefFormat: "files",
+ })
+
+ leafBlobID := repoHarness.HashObject(t, "blob", []byte("deep-content\n"))+ currentTree := repoHarness.Mktree(t, fmt.Sprintf("100644 blob %s\tleaf.txt\n", leafBlobID))+
+ parts := make([][]byte, 0, depth+1)
+ for i := depth - 1; i >= 0; i-- {+ name := fmt.Sprintf("level%02d", i)+ currentTree = repoHarness.Mktree(t, fmt.Sprintf("040000 tree %s\t%s\n", currentTree, name))+ parts = append([][]byte{[]byte(name)}, parts...)+ }
+ parts = append(parts, []byte("leaf.txt"))+
+ repo, err := repository.Open(repoHarness.Dir())
+ if err != nil {+ t.Fatalf("repository.Open: %v", err)+ }
+ defer func() { _ = repo.Close() }()+
+ rootTree, err := repo.ReadStoredTree(currentTree)
+ if err != nil {+ t.Fatalf("ReadStoredTree(root): %v", err)+ }
+
+ entry, err := repo.ResolveTreeEntry(rootTree, parts)
+ if err != nil {+ t.Fatalf("ResolveTreeEntry(deep): %v", err)+ }
+ if entry.Mode != object.FileModeRegular {+ t.Fatalf("ResolveTreeEntry(deep) mode = %o, want %o", entry.Mode, object.FileModeRegular)+ }
+ if entry.ID != leafBlobID {+ t.Fatalf("ResolveTreeEntry(deep) id = %s, want %s", entry.ID, leafBlobID)+ }
+ })
+}
+
+func TestReadStoredTreeMixedModes(t *testing.T) {+ t.Parallel()
+
+ testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper+ repoHarness := testgit.NewRepo(t, testgit.RepoOptions{+ ObjectFormat: algo,
+ Bare: true,
+ RefFormat: "files",
+ })
+
+ normalID := repoHarness.HashObject(t, "blob", []byte("normal-file\n"))+ execID := repoHarness.HashObject(t, "blob", []byte("#!/bin/sh\necho hi\n"))+ symID := repoHarness.HashObject(t, "blob", []byte("normal.txt"))+ nestedBlobID := repoHarness.HashObject(t, "blob", []byte("nested\n"))+ nestedTreeID := repoHarness.Mktree(t, fmt.Sprintf("100644 blob %s\tleaf.txt\n", nestedBlobID))+
+ rootTreeID := repoHarness.Mktree(t,
+ fmt.Sprintf(
+ "100644 blob %s\tnormal.txt\n100755 blob %s\trun.sh\n120000 blob %s\tlink.txt\n040000 tree %s\tdir\n",
+ normalID,
+ execID,
+ symID,
+ nestedTreeID,
+ ),
+ )
+
+ repo, err := repository.Open(repoHarness.Dir())
+ if err != nil {+ t.Fatalf("repository.Open: %v", err)+ }
+ defer func() { _ = repo.Close() }()+
+ rootTree, err := repo.ReadStoredTree(rootTreeID)
+ if err != nil {+ t.Fatalf("ReadStoredTree(root): %v", err)+ }
+
+ expect := map[string]object.FileMode{+ "normal.txt": object.FileModeRegular,
+ "run.sh": object.FileModeExecutable,
+ "link.txt": object.FileModeSymlink,
+ "dir": object.FileModeDir,
+ }
+
+ for name, wantMode := range expect {+ entry := rootTree.Tree().Entry([]byte(name))
+ if entry == nil {+ t.Fatalf("Entry(%q) returned nil", name)+ }
+ if entry.Mode != wantMode {+ t.Fatalf("Entry(%q) mode = %o, want %o", name, entry.Mode, wantMode)+ }
+ }
+ })
+}
--- /dev/null
+++ b/repository/traversal_test.go
@@ -1,0 +1,79 @@
+package repository_test
+
+import (
+ "testing"
+
+ "codeberg.org/lindenii/furgit/internal/testgit"
+ "codeberg.org/lindenii/furgit/object"
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/repository"
+)
+
+func TestRepositoryDepthFirstEnumerationFromHEAD(t *testing.T) {+ t.Parallel()
+
+ testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper+ repoHarness := testgit.NewRepo(t, testgit.RepoOptions{+ ObjectFormat: algo,
+ Bare: true,
+ RefFormat: "files",
+ })
+
+ _, _, commit1 := repoHarness.MakeCommit(t, "walk-one")
+ blob2, tree2 := repoHarness.MakeSingleFileTree(t, "second.txt", []byte("second\n"))+ commit2 := repoHarness.CommitTree(t, tree2, "walk-two", commit1)
+ _ = blob2
+ repoHarness.UpdateRef(t, "refs/heads/main", commit2)
+ repoHarness.SymbolicRef(t, "HEAD", "refs/heads/main")
+
+ repo, err := repository.Open(repoHarness.Dir())
+ if err != nil {+ t.Fatalf("repository.Open: %v", err)+ }
+ defer func() { _ = repo.Close() }()+
+ head, err := repo.ResolveRefFully("HEAD")+ if err != nil {+ t.Fatalf("ResolveRefFully(HEAD): %v", err)+ }
+
+ visited := make(map[objectid.ObjectID]bool)
+ queue := []objectid.ObjectID{head.ID}+ objectsRead := 0
+
+ for len(queue) > 0 {+ id := queue[0]
+ queue = queue[1:]
+
+ if visited[id] {+ continue
+ }
+ visited[id] = true
+
+ stored, err := repo.ReadStored(id)
+ if err != nil {+ t.Fatalf("ReadStored(%s): %v", id, err)+ }
+ objectsRead++
+
+ switch obj := stored.Object().(type) {+ case *object.Commit:
+ queue = append(queue, obj.Tree)
+ queue = append(queue, obj.Parents...)
+ case *object.Tree:
+ for _, entry := range obj.Entries {+ queue = append(queue, entry.ID)
+ }
+ case *object.Tag:
+ queue = append(queue, obj.Target)
+ case *object.Blob:
+ default:
+ t.Fatalf("unexpected object type: %T", obj)+ }
+ }
+
+ if objectsRead == 0 {+ t.Fatalf("no objects were enumerated from HEAD")+ }
+ })
+}
--
⑨