shithub: furgit

Download patch

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