ref: 05e07f6c6aca1662c33359f41c66e6f9b6eb935a
parent: 5a18c85bdc99035f163d891d538bf6dd87553a2d
author: Runxi Yu <me@runxiyu.org>
date: Fri Feb 20 16:19:47 EST 2026
testgit: Add test harnesses
--- /dev/null
+++ b/internal/testgit/algorithms.go
@@ -1,0 +1,25 @@
+package testgit
+
+import (
+ "testing"
+
+ "codeberg.org/lindenii/furgit/oid"
+)
+
+// SupportedAlgorithms returns all object ID algorithms supported by furgit.
+func SupportedAlgorithms() []oid.Algorithm {+ return []oid.Algorithm{+ oid.AlgorithmSHA1,
+ oid.AlgorithmSHA256,
+ }
+}
+
+// ForEachAlgorithm runs a subtest for every supported algorithm.
+func ForEachAlgorithm(t *testing.T, fn func(t *testing.T, algo oid.Algorithm)) {+ t.Helper()
+ for _, algo := range SupportedAlgorithms() {+ t.Run(algo.String(), func(t *testing.T) {+ fn(t, algo)
+ })
+ }
+}
--- /dev/null
+++ b/internal/testgit/repo.go
@@ -1,0 +1,10 @@
+package testgit
+
+import "codeberg.org/lindenii/furgit/oid"
+
+// TestRepo is a temporary git repository harness for integration tests.
+type TestRepo struct {+ dir string
+ algo oid.Algorithm
+ env []string
+}
--- /dev/null
+++ b/internal/testgit/repo_cat_file.go
@@ -1,0 +1,13 @@
+package testgit
+
+import (
+ "testing"
+
+ "codeberg.org/lindenii/furgit/oid"
+)
+
+// CatFile returns raw output from git cat-file.
+func (repo *TestRepo) CatFile(tb testing.TB, mode string, id oid.ObjectID) []byte {+ tb.Helper()
+ return repo.RunBytes(tb, "cat-file", mode, id.String())
+}
--- /dev/null
+++ b/internal/testgit/repo_commit_tree.go
@@ -1,0 +1,23 @@
+package testgit
+
+import (
+ "testing"
+
+ "codeberg.org/lindenii/furgit/oid"
+)
+
+// CommitTree creates a commit from a tree and message, optionally with parents.
+func (repo *TestRepo) CommitTree(tb testing.TB, tree oid.ObjectID, message string, parents ...oid.ObjectID) oid.ObjectID {+ tb.Helper()
+ args := []string{"commit-tree", tree.String()}+ for _, p := range parents {+ args = append(args, "-p", p.String())
+ }
+ args = append(args, "-m", message)
+ hex := repo.Run(tb, args...)
+ id, err := oid.ParseHex(repo.algo, hex)
+ if err != nil {+ tb.Fatalf("parse commit-tree output %q: %v", hex, err)+ }
+ return id
+}
--- /dev/null
+++ b/internal/testgit/repo_hash_object.go
@@ -1,0 +1,18 @@
+package testgit
+
+import (
+ "testing"
+
+ "codeberg.org/lindenii/furgit/oid"
+)
+
+// HashObject hashes and writes an object and returns its object ID.
+func (repo *TestRepo) HashObject(tb testing.TB, objType string, body []byte) oid.ObjectID {+ tb.Helper()
+ hex := repo.RunInput(tb, body, "hash-object", "-t", objType, "-w", "--stdin")
+ id, err := oid.ParseHex(repo.algo, hex)
+ if err != nil {+ tb.Fatalf("parse git hash-object output %q: %v", hex, err)+ }
+ return id
+}
--- /dev/null
+++ b/internal/testgit/repo_make_commit.go
@@ -1,0 +1,15 @@
+package testgit
+
+import (
+ "testing"
+
+ "codeberg.org/lindenii/furgit/oid"
+)
+
+// MakeCommit creates a commit over a single-file tree and returns (blobID, treeID, commitID).
+func (repo *TestRepo) MakeCommit(tb testing.TB, message string) (oid.ObjectID, oid.ObjectID, oid.ObjectID) {+ tb.Helper()
+ blobID, treeID := repo.MakeSingleFileTree(tb, "file.txt", []byte("commit-body\n"))+ commitID := repo.CommitTree(tb, treeID, message)
+ return blobID, treeID, commitID
+}
--- /dev/null
+++ b/internal/testgit/repo_make_single_file_tree.go
@@ -1,0 +1,17 @@
+package testgit
+
+import (
+ "fmt"
+ "testing"
+
+ "codeberg.org/lindenii/furgit/oid"
+)
+
+// MakeSingleFileTree writes one blob and one tree entry for it and returns (blobID, treeID).
+func (repo *TestRepo) MakeSingleFileTree(tb testing.TB, fileName string, fileContent []byte) (oid.ObjectID, oid.ObjectID) {+ tb.Helper()
+ blobID := repo.HashObject(tb, "blob", fileContent)
+ treeInput := fmt.Sprintf("100644 blob %s\t%s\n", blobID.String(), fileName)+ treeID := repo.Mktree(tb, treeInput)
+ return blobID, treeID
+}
--- /dev/null
+++ b/internal/testgit/repo_mktree.go
@@ -1,0 +1,18 @@
+package testgit
+
+import (
+ "testing"
+
+ "codeberg.org/lindenii/furgit/oid"
+)
+
+// Mktree creates a tree from textual mktree input and returns its ID.
+func (repo *TestRepo) Mktree(tb testing.TB, input string) oid.ObjectID {+ tb.Helper()
+ hex := repo.RunInput(tb, []byte(input), "mktree")
+ id, err := oid.ParseHex(repo.algo, hex)
+ if err != nil {+ tb.Fatalf("parse mktree output %q: %v", hex, err)+ }
+ return id
+}
--- /dev/null
+++ b/internal/testgit/repo_new.go
@@ -1,0 +1,56 @@
+package testgit
+
+import (
+ "os"
+ "testing"
+
+ "codeberg.org/lindenii/furgit/oid"
+)
+
+// NewBareRepo creates a temporary bare repository initialized with the requested algorithm.
+func NewBareRepo(tb testing.TB, algo oid.Algorithm) *TestRepo {+ tb.Helper()
+ return newRepo(tb, algo, true)
+}
+
+// NewWorkRepo creates a temporary non-bare repository initialized with the requested algorithm.
+func NewWorkRepo(tb testing.TB, algo oid.Algorithm) *TestRepo {+ tb.Helper()
+ return newRepo(tb, algo, false)
+}
+
+func newRepo(tb testing.TB, algo oid.Algorithm, bare bool) *TestRepo {+ tb.Helper()
+ if algo.Size() == 0 {+ tb.Fatalf("invalid algorithm: %v", algo)+ }
+
+ dir, err := os.MkdirTemp("", "furgit-testgit-*")+ if err != nil {+ tb.Fatalf("create temp dir: %v", err)+ }
+ tb.Cleanup(func() { _ = os.RemoveAll(dir) })+
+ repo := &TestRepo{+ dir: dir,
+ algo: algo,
+ env: append(os.Environ(),
+ "GIT_CONFIG_GLOBAL=/dev/null",
+ "GIT_CONFIG_SYSTEM=/dev/null",
+ "GIT_AUTHOR_NAME=Test Author",
+ "GIT_AUTHOR_EMAIL=test@example.org",
+ "GIT_COMMITTER_NAME=Test Committer",
+ "GIT_COMMITTER_EMAIL=committer@example.org",
+ "GIT_AUTHOR_DATE=1234567890 +0000",
+ "GIT_COMMITTER_DATE=1234567890 +0000",
+ ),
+ }
+
+ args := []string{"init", "--object-format=" + algo.String()}+ if bare {+ args = append(args, "--bare")
+ }
+ args = append(args, dir)
+ repo.runBytes(tb, nil, "", args...)
+ return repo
+}
--- /dev/null
+++ b/internal/testgit/repo_properties.go
@@ -1,0 +1,13 @@
+package testgit
+
+import "codeberg.org/lindenii/furgit/oid"
+
+// Dir returns the repository directory path.
+func (repo *TestRepo) Dir() string {+ return repo.dir
+}
+
+// Algorithm returns the object ID algorithm configured for this repository.
+func (repo *TestRepo) Algorithm() oid.Algorithm {+ return repo.algo
+}
--- /dev/null
+++ b/internal/testgit/repo_rev_parse.go
@@ -1,0 +1,18 @@
+package testgit
+
+import (
+ "testing"
+
+ "codeberg.org/lindenii/furgit/oid"
+)
+
+// RevParse resolves rev expressions to object IDs.
+func (repo *TestRepo) RevParse(tb testing.TB, spec string) oid.ObjectID {+ tb.Helper()
+ hex := repo.Run(tb, "rev-parse", spec)
+ id, err := oid.ParseHex(repo.algo, hex)
+ if err != nil {+ tb.Fatalf("parse rev-parse output %q: %v", hex, err)+ }
+ return id
+}
--- /dev/null
+++ b/internal/testgit/repo_run.go
@@ -1,0 +1,49 @@
+package testgit
+
+import (
+ "bytes"
+ "os/exec"
+ "strings"
+ "testing"
+)
+
+// Run executes git and returns trimmed textual output.
+func (repo *TestRepo) Run(tb testing.TB, args ...string) string {+ tb.Helper()
+ out := repo.runBytes(tb, nil, repo.dir, args...)
+ return strings.TrimSpace(string(out))
+}
+
+// RunBytes executes git and returns raw output bytes.
+func (repo *TestRepo) RunBytes(tb testing.TB, args ...string) []byte {+ tb.Helper()
+ return repo.runBytes(tb, nil, repo.dir, args...)
+}
+
+// RunInput executes git with stdin and returns trimmed textual output.
+func (repo *TestRepo) RunInput(tb testing.TB, stdin []byte, args ...string) string {+ tb.Helper()
+ out := repo.runBytes(tb, stdin, repo.dir, args...)
+ return strings.TrimSpace(string(out))
+}
+
+// RunInputBytes executes git with stdin and returns raw output bytes.
+func (repo *TestRepo) RunInputBytes(tb testing.TB, stdin []byte, args ...string) []byte {+ tb.Helper()
+ return repo.runBytes(tb, stdin, repo.dir, args...)
+}
+
+func (repo *TestRepo) runBytes(tb testing.TB, stdin []byte, dir string, args ...string) []byte {+ tb.Helper()
+ cmd := exec.Command("git", args...)+ cmd.Dir = dir
+ cmd.Env = repo.env
+ if stdin != nil {+ cmd.Stdin = bytes.NewReader(stdin)
+ }
+ out, err := cmd.CombinedOutput()
+ if err != nil {+ tb.Fatalf("git %v failed: %v\n%s", args, err, out)+ }
+ return out
+}
--- /dev/null
+++ b/internal/testgit/repo_tag_annotated.go
@@ -1,0 +1,15 @@
+package testgit
+
+import (
+ "fmt"
+ "testing"
+
+ "codeberg.org/lindenii/furgit/oid"
+)
+
+// TagAnnotated creates an annotated tag object and returns the resulting tag object ID.
+func (repo *TestRepo) TagAnnotated(tb testing.TB, name string, target oid.ObjectID, message string) oid.ObjectID {+ tb.Helper()
+ repo.Run(tb, "tag", "-a", name, target.String(), "-m", message)
+ return repo.RevParse(tb, fmt.Sprintf("refs/tags/%s", name))+}
--
⑨