From 01d15bccf3b1dcc51516b1f64d50950b31d7f8fb Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Fri, 6 Mar 2026 21:19:56 +0800 Subject: Urgh I made some wrong amends and I'm too tired to separate the commits out this time ancestor: Split out of reachability mergebase: Add merge base routines internal/commitquery: Add commit query context engine thingy internal/peel: Shared tag peeling errors: Shared object query errors internal/testgit: Add rooted repo helpers; remove raw path access objectstore/memory: Add in-memory object store objectid: Add Compare helper --- internal/testgit/repo_commit_tree_env.go | 51 ++++++++++++++++ internal/testgit/repo_fs.go | 86 +++++++++++++++++++++++++++ internal/testgit/repo_open_commit_graph.go | 26 +++++++++ internal/testgit/repo_open_object_store.go | 29 ++++++++++ internal/testgit/repo_open_repository.go | 25 ++++++++ internal/testgit/repo_open_root.go | 87 ++++++++++++++++++++++++++++ internal/testgit/repo_properties.go | 5 -- internal/testgit/repo_remove_loose_object.go | 22 +++++++ internal/testgit/repo_run.go | 52 ++++++++++++++--- 9 files changed, 371 insertions(+), 12 deletions(-) create mode 100644 internal/testgit/repo_commit_tree_env.go create mode 100644 internal/testgit/repo_fs.go create mode 100644 internal/testgit/repo_open_commit_graph.go create mode 100644 internal/testgit/repo_open_object_store.go create mode 100644 internal/testgit/repo_open_repository.go create mode 100644 internal/testgit/repo_open_root.go create mode 100644 internal/testgit/repo_remove_loose_object.go (limited to 'internal/testgit') diff --git a/internal/testgit/repo_commit_tree_env.go b/internal/testgit/repo_commit_tree_env.go new file mode 100644 index 00000000..ee949b5c --- /dev/null +++ b/internal/testgit/repo_commit_tree_env.go @@ -0,0 +1,51 @@ +package testgit + +import ( + "slices" + "strings" + "testing" + + "codeberg.org/lindenii/furgit/objectid" +) + +// CommitTreeWithEnv creates one commit from a tree and message, optionally with +// parents, using additional environment variables for the git subprocess. +func (testRepo *TestRepo) CommitTreeWithEnv( + tb testing.TB, + extraEnv []string, + tree objectid.ObjectID, + message string, + parents ...objectid.ObjectID, +) objectid.ObjectID { + tb.Helper() + + args := make([]string, 0, 2+2*len(parents)+2) + + args = append(args, "commit-tree", tree.String()) + for _, parent := range parents { + args = append(args, "-p", parent.String()) + } + + args = append(args, "-m", message) + hex := testRepo.runWithExtraEnv(tb, extraEnv, args...) + + id, err := objectid.ParseHex(testRepo.algo, hex) + if err != nil { + tb.Fatalf("parse commit-tree output %q: %v", hex, err) + } + + return id +} + +func (testRepo *TestRepo) runWithExtraEnv(tb testing.TB, extraEnv []string, args ...string) string { + tb.Helper() + + env := slices.Concat(testRepo.env, extraEnv) + + out, err := testRepo.runBytesWithEnv(tb, nil, testRepo.dir, env, args...) + if err != nil { + tb.Fatalf("git %v failed: %v\n%s", args, err, out) + } + + return strings.TrimSpace(string(out)) +} diff --git a/internal/testgit/repo_fs.go b/internal/testgit/repo_fs.go new file mode 100644 index 00000000..56acbfcf --- /dev/null +++ b/internal/testgit/repo_fs.go @@ -0,0 +1,86 @@ +package testgit + +import ( + "os" + "path/filepath" + "testing" +) + +// OpenFile opens one file relative to the repository root. +func (testRepo *TestRepo) OpenFile(tb testing.TB, name string) *os.File { + tb.Helper() + + root := testRepo.OpenRoot(tb) + + file, err := root.Open(name) + if err != nil { + tb.Fatalf("Open(%q): %v", name, err) + } + + return file +} + +// ReadFile reads one file relative to the repository root. +func (testRepo *TestRepo) ReadFile(tb testing.TB, name string) []byte { + tb.Helper() + + root := testRepo.OpenRoot(tb) + + data, err := root.ReadFile(name) + if err != nil { + tb.Fatalf("ReadFile(%q): %v", name, err) + } + + return data +} + +// WriteFile writes one file relative to the repository root. +func (testRepo *TestRepo) WriteFile(tb testing.TB, name string, data []byte, perm os.FileMode) { + tb.Helper() + + root := testRepo.OpenRoot(tb) + + err := root.WriteFile(name, data, perm) + if err != nil { + tb.Fatalf("WriteFile(%q): %v", name, err) + } +} + +// WriteFileAll writes one file relative to the repository root, creating any +// missing parent directories first. +func (testRepo *TestRepo) WriteFileAll( + tb testing.TB, + name string, + data []byte, + dirPerm os.FileMode, + filePerm os.FileMode, +) { + tb.Helper() + + root := testRepo.OpenRoot(tb) + + dir := filepath.Dir(name) + if dir != "." { + err := root.MkdirAll(dir, dirPerm) + if err != nil { + tb.Fatalf("MkdirAll(%q): %v", dir, err) + } + } + + err := root.WriteFile(name, data, filePerm) + if err != nil { + tb.Fatalf("WriteFile(%q): %v", name, err) + } +} + +// Remove removes one path relative to the repository root. +func (testRepo *TestRepo) Remove(tb testing.TB, name string) { + tb.Helper() + + root := testRepo.OpenRoot(tb) + + err := root.Remove(name) + if err != nil { + tb.Fatalf("Remove(%q): %v", name, err) + } +} diff --git a/internal/testgit/repo_open_commit_graph.go b/internal/testgit/repo_open_commit_graph.go new file mode 100644 index 00000000..4db7261b --- /dev/null +++ b/internal/testgit/repo_open_commit_graph.go @@ -0,0 +1,26 @@ +package testgit + +import ( + "testing" + + commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" +) + +// OpenCommitGraph opens the repository commit-graph and registers cleanup on +// the caller. +func (testRepo *TestRepo) OpenCommitGraph(tb testing.TB) *commitgraphread.Reader { + tb.Helper() + + objectsRoot := testRepo.OpenObjectsRoot(tb) + + graph, err := commitgraphread.Open(objectsRoot, testRepo.Algorithm(), commitgraphread.OpenSingle) + if err != nil { + tb.Fatalf("commitgraphread.Open: %v", err) + } + + tb.Cleanup(func() { + _ = graph.Close() + }) + + return graph +} diff --git a/internal/testgit/repo_open_object_store.go b/internal/testgit/repo_open_object_store.go new file mode 100644 index 00000000..aac71d60 --- /dev/null +++ b/internal/testgit/repo_open_object_store.go @@ -0,0 +1,29 @@ +package testgit + +import ( + "testing" + + "codeberg.org/lindenii/furgit/objectstore" + "codeberg.org/lindenii/furgit/repository" +) + +// OpenObjectStore opens the repository object store and registers cleanup on +// the caller. +// +//nolint:ireturn +func (testRepo *TestRepo) OpenObjectStore(tb testing.TB) objectstore.Store { + tb.Helper() + + root := testRepo.OpenGitRoot(tb) + + repo, err := repository.Open(root) + if err != nil { + tb.Fatalf("repository.Open: %v", err) + } + + tb.Cleanup(func() { + _ = repo.Close() + }) + + return repo.Objects() +} diff --git a/internal/testgit/repo_open_repository.go b/internal/testgit/repo_open_repository.go new file mode 100644 index 00000000..fbc98383 --- /dev/null +++ b/internal/testgit/repo_open_repository.go @@ -0,0 +1,25 @@ +package testgit + +import ( + "testing" + + "codeberg.org/lindenii/furgit/repository" +) + +// OpenRepository opens the repository and registers cleanup on the caller. +func (testRepo *TestRepo) OpenRepository(tb testing.TB) *repository.Repository { + tb.Helper() + + root := testRepo.OpenGitRoot(tb) + + repo, err := repository.Open(root) + if err != nil { + tb.Fatalf("repository.Open: %v", err) + } + + tb.Cleanup(func() { + _ = repo.Close() + }) + + return repo +} diff --git a/internal/testgit/repo_open_root.go b/internal/testgit/repo_open_root.go new file mode 100644 index 00000000..4530c604 --- /dev/null +++ b/internal/testgit/repo_open_root.go @@ -0,0 +1,87 @@ +package testgit + +import ( + "errors" + "os" + "testing" +) + +// OpenRoot opens the repository root directory and registers cleanup on the +// caller. +func (testRepo *TestRepo) OpenRoot(tb testing.TB) *os.Root { + tb.Helper() + + root, err := os.OpenRoot(testRepo.dir) + if err != nil { + tb.Fatalf("os.OpenRoot: %v", err) + } + + tb.Cleanup(func() { + _ = root.Close() + }) + + return root +} + +// OpenGitRoot opens the repository gitdir and registers cleanup on the caller. +// +// For bare repositories, this is the repository root itself. For non-bare +// repositories, this is the .git directory under the worktree root. +func (testRepo *TestRepo) OpenGitRoot(tb testing.TB) *os.Root { + tb.Helper() + + repoRoot := testRepo.OpenRoot(tb) + + gitRoot, err := repoRoot.OpenRoot(".git") + if err == nil { + tb.Cleanup(func() { + _ = gitRoot.Close() + }) + + return gitRoot + } + + if !errors.Is(err, os.ErrNotExist) { + tb.Fatalf("OpenRoot(.git): %v", err) + } + + return repoRoot +} + +// OpenObjectsRoot opens the objects directory and registers cleanup on the +// caller. +func (testRepo *TestRepo) OpenObjectsRoot(tb testing.TB) *os.Root { + tb.Helper() + + gitRoot := testRepo.OpenGitRoot(tb) + + objectsRoot, err := gitRoot.OpenRoot("objects") + if err != nil { + tb.Fatalf("OpenRoot(objects): %v", err) + } + + tb.Cleanup(func() { + _ = objectsRoot.Close() + }) + + return objectsRoot +} + +// OpenPackRoot opens the objects/pack directory and registers cleanup on the +// caller. +func (testRepo *TestRepo) OpenPackRoot(tb testing.TB) *os.Root { + tb.Helper() + + objectsRoot := testRepo.OpenObjectsRoot(tb) + + packRoot, err := objectsRoot.OpenRoot("pack") + if err != nil { + tb.Fatalf("OpenRoot(pack): %v", err) + } + + tb.Cleanup(func() { + _ = packRoot.Close() + }) + + return packRoot +} diff --git a/internal/testgit/repo_properties.go b/internal/testgit/repo_properties.go index 3a489124..703cef1c 100644 --- a/internal/testgit/repo_properties.go +++ b/internal/testgit/repo_properties.go @@ -2,11 +2,6 @@ package testgit import "codeberg.org/lindenii/furgit/objectid" -// Dir returns the repository directory path. -func (testRepo *TestRepo) Dir() string { - return testRepo.dir -} - // Algorithm returns the object ID algorithm configured for this repository. func (testRepo *TestRepo) Algorithm() objectid.Algorithm { return testRepo.algo diff --git a/internal/testgit/repo_remove_loose_object.go b/internal/testgit/repo_remove_loose_object.go new file mode 100644 index 00000000..ec3d8c2d --- /dev/null +++ b/internal/testgit/repo_remove_loose_object.go @@ -0,0 +1,22 @@ +package testgit + +import ( + "fmt" + "testing" + + "codeberg.org/lindenii/furgit/objectid" +) + +// RemoveLooseObject removes one loose object file from the repository. +func (testRepo *TestRepo) RemoveLooseObject(tb testing.TB, id objectid.ObjectID) { + tb.Helper() + + root := testRepo.OpenObjectsRoot(tb) + hex := id.String() + path := fmt.Sprintf("%s/%s", hex[:2], hex[2:]) + + err := root.Remove(path) + if err != nil { + tb.Fatalf("remove loose object %s: %v", id, err) + } +} diff --git a/internal/testgit/repo_run.go b/internal/testgit/repo_run.go index 162a0d72..448b88f0 100644 --- a/internal/testgit/repo_run.go +++ b/internal/testgit/repo_run.go @@ -22,6 +22,15 @@ func (testRepo *TestRepo) RunBytes(tb testing.TB, args ...string) []byte { return testRepo.runBytes(tb, nil, testRepo.dir, args...) } +// RunE executes git and returns trimmed textual output plus any command error. +func (testRepo *TestRepo) RunE(tb testing.TB, args ...string) (string, error) { + tb.Helper() + + out, err := testRepo.runBytesE(nil, testRepo.dir, args...) + + return strings.TrimSpace(string(out)), err +} + // RunInput executes git with stdin and returns trimmed textual output. func (testRepo *TestRepo) RunInput(tb testing.TB, stdin []byte, args ...string) string { tb.Helper() @@ -39,19 +48,48 @@ func (testRepo *TestRepo) RunInputBytes(tb testing.TB, stdin []byte, args ...str func (testRepo *TestRepo) runBytes(tb testing.TB, stdin []byte, dir string, args ...string) []byte { tb.Helper() + + out, err := testRepo.runBytesE(stdin, dir, args...) + if err != nil { + tb.Fatalf("git %v failed: %v\n%s", args, err, out) + } + + return out +} + +func (testRepo *TestRepo) runBytesE(stdin []byte, dir string, args ...string) ([]byte, error) { + return testRepo.runBytesWithEnvNoHelper(stdin, dir, testRepo.env, args...) +} + +// runBytesWithEnv executes git using the supplied environment. +func (testRepo *TestRepo) runBytesWithEnv( + tb testing.TB, + stdin []byte, + dir string, + env []string, + args ...string, +) ([]byte, error) { + tb.Helper() + + return testRepo.runBytesWithEnvNoHelper(stdin, dir, env, args...) +} + +// runBytesWithEnvNoHelper executes git using the supplied environment without +// touching testing helper state. +func (testRepo *TestRepo) runBytesWithEnvNoHelper( + stdin []byte, + dir string, + env []string, + args ...string, +) ([]byte, error) { //nolint:noctx cmd := exec.Command("git", args...) //#nosec G204 cmd.Dir = dir - cmd.Env = testRepo.env + cmd.Env = 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 + return cmd.CombinedOutput() } -- cgit v1.3.1-10-gc9f91