shithub: furgit

Download patch

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")
+		}
+	})
+}
--