From 120509f0aad0e945d8e0fc90a822fa904fb70b68 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Fri, 6 Mar 2026 01:48:44 +0800 Subject: repository: Refactor --- repository/algorithm.go | 27 +++++++ repository/close.go | 32 ++++++++ repository/config.go | 32 ++++++++ repository/objects.go | 69 +++++++++++++++++ repository/open_config.go | 39 ---------- repository/open_objects.go | 64 ---------------- repository/open_refs.go | 47 ------------ repository/read_stored.go | 111 ---------------------------- repository/refs.go | 52 +++++++++++++ repository/refs_test.go | 139 +++++++++++++++++++++++++++++++++++ repository/repository.go | 54 -------------- repository/repository_test.go | 139 ----------------------------------- repository/stored.go | 111 ++++++++++++++++++++++++++++ repository/traversal_bench_test.go | 73 ------------------ repository/traversal_helpers_test.go | 102 ------------------------- repository/traversal_test.go | 48 ++++++++++++ repository/tree.go | 53 +++++++++++++ repository/tree_resolve.go | 53 ------------- 18 files changed, 563 insertions(+), 682 deletions(-) create mode 100644 repository/algorithm.go create mode 100644 repository/close.go create mode 100644 repository/config.go create mode 100644 repository/objects.go delete mode 100644 repository/open_config.go delete mode 100644 repository/open_objects.go delete mode 100644 repository/open_refs.go delete mode 100644 repository/read_stored.go create mode 100644 repository/refs.go create mode 100644 repository/refs_test.go delete mode 100644 repository/repository_test.go create mode 100644 repository/stored.go delete mode 100644 repository/traversal_bench_test.go delete mode 100644 repository/traversal_helpers_test.go create mode 100644 repository/tree.go delete mode 100644 repository/tree_resolve.go (limited to 'repository') diff --git a/repository/algorithm.go b/repository/algorithm.go new file mode 100644 index 00000000..22326cbd --- /dev/null +++ b/repository/algorithm.go @@ -0,0 +1,27 @@ +package repository + +import ( + "fmt" + + "codeberg.org/lindenii/furgit/config" + "codeberg.org/lindenii/furgit/objectid" +) + +func detectObjectAlgorithm(cfg *config.Config) (objectid.Algorithm, error) { + algoName := cfg.Lookup("extensions", "", "objectformat").Value + if algoName == "" { + algoName = objectid.AlgorithmSHA1.String() + } + + algo, ok := objectid.ParseAlgorithm(algoName) + if !ok { + return objectid.AlgorithmUnknown, fmt.Errorf("repository: unsupported object format %q", algoName) + } + + return algo, nil +} + +// Algorithm returns the repository object ID algorithm. +func (repo *Repository) Algorithm() objectid.Algorithm { + return repo.algo +} diff --git a/repository/close.go b/repository/close.go new file mode 100644 index 00000000..998742d0 --- /dev/null +++ b/repository/close.go @@ -0,0 +1,32 @@ +package repository + +import "errors" + +// Close closes owned stores and filesystem roots. +// The behavior of the repo after Close is undefined. +func (repo *Repository) Close() error { + var errs []error + + if repo.refs != nil { + err := repo.refs.Close() + if err != nil { + errs = append(errs, err) + } + } + + if repo.objects != nil { + err := repo.objects.Close() + if err != nil { + errs = append(errs, err) + } + } + + if repo.objectsLooseForWritingOnly != nil { + err := repo.objectsLooseForWritingOnly.Close() + if err != nil { + errs = append(errs, err) + } + } + + return errors.Join(errs...) +} diff --git a/repository/config.go b/repository/config.go new file mode 100644 index 00000000..de33e2c3 --- /dev/null +++ b/repository/config.go @@ -0,0 +1,32 @@ +package repository + +import ( + "fmt" + "os" + + "codeberg.org/lindenii/furgit/config" +) + +func parseRepositoryConfig(root *os.Root) (*config.Config, error) { + configFile, err := root.Open("config") + if err != nil { + return nil, fmt.Errorf("repository: open config: %w", err) + } + + defer func() { _ = configFile.Close() }() + + cfg, err := config.ParseConfig(configFile) + if err != nil { + return nil, fmt.Errorf("repository: parse config: %w", err) + } + + return cfg, nil +} + +// Config returns the parsed repository configuration snapshot. +// +// The returned pointer is owned by Repository. Callers should treat it as +// read-only. +func (repo *Repository) Config() *config.Config { + return repo.config +} diff --git a/repository/objects.go b/repository/objects.go new file mode 100644 index 00000000..c8278150 --- /dev/null +++ b/repository/objects.go @@ -0,0 +1,69 @@ +package repository + +import ( + "errors" + "fmt" + "os" + + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/objectstore" + objectloose "codeberg.org/lindenii/furgit/objectstore/loose" + objectmix "codeberg.org/lindenii/furgit/objectstore/mix" + objectpacked "codeberg.org/lindenii/furgit/objectstore/packed" +) + +func openObjectStore(root *os.Root, algo objectid.Algorithm) (objectstore.Store, *objectloose.Store, error) { + objectsRoot, err := root.OpenRoot("objects") + if err != nil { + return nil, nil, fmt.Errorf("repository: open objects: %w", err) + } + + looseStore, err := objectloose.New(objectsRoot, algo) + if err != nil { + return nil, nil, err + } + + backends := []objectstore.Store{looseStore} + + packRoot, err := objectsRoot.OpenRoot("pack") + if err == nil { + var packedStore *objectpacked.Store + + packedStore, err = objectpacked.New(packRoot, algo) + if err != nil { + _ = looseStore.Close() + + return nil, nil, err + } + + backends = append(backends, packedStore) + } else if !errors.Is(err, os.ErrNotExist) { + _ = looseStore.Close() + + return nil, nil, fmt.Errorf("repository: open objects/pack: %w", err) + } + + objectsChain := objectmix.New(backends...) + + objectsRootForWriting, err := root.OpenRoot("objects") + if err != nil { + _ = objectsChain.Close() + + return nil, nil, fmt.Errorf("repository: open objects for loose writing: %w", err) + } + + objectsLooseForWritingOnly, err := objectloose.New(objectsRootForWriting, algo) + if err != nil { + _ = objectsRootForWriting.Close() + _ = objectsChain.Close() + + return nil, nil, err + } + + return objectsChain, objectsLooseForWritingOnly, nil +} + +// Objects returns the configured object store. +func (repo *Repository) Objects() objectstore.Store { + return repo.objects +} diff --git a/repository/open_config.go b/repository/open_config.go deleted file mode 100644 index fcc9793c..00000000 --- a/repository/open_config.go +++ /dev/null @@ -1,39 +0,0 @@ -package repository - -import ( - "fmt" - "os" - - "codeberg.org/lindenii/furgit/config" - "codeberg.org/lindenii/furgit/objectid" -) - -func parseRepositoryConfig(root *os.Root) (*config.Config, error) { - configFile, err := root.Open("config") - if err != nil { - return nil, fmt.Errorf("repository: open config: %w", err) - } - - defer func() { _ = configFile.Close() }() - - cfg, err := config.ParseConfig(configFile) - if err != nil { - return nil, fmt.Errorf("repository: parse config: %w", err) - } - - return cfg, nil -} - -func detectObjectAlgorithm(cfg *config.Config) (objectid.Algorithm, error) { - algoName := cfg.Lookup("extensions", "", "objectformat").Value - if algoName == "" { - algoName = objectid.AlgorithmSHA1.String() - } - - algo, ok := objectid.ParseAlgorithm(algoName) - if !ok { - return objectid.AlgorithmUnknown, fmt.Errorf("repository: unsupported object format %q", algoName) - } - - return algo, nil -} diff --git a/repository/open_objects.go b/repository/open_objects.go deleted file mode 100644 index 61fc0f93..00000000 --- a/repository/open_objects.go +++ /dev/null @@ -1,64 +0,0 @@ -package repository - -import ( - "errors" - "fmt" - "os" - - "codeberg.org/lindenii/furgit/objectid" - "codeberg.org/lindenii/furgit/objectstore" - objectloose "codeberg.org/lindenii/furgit/objectstore/loose" - objectmix "codeberg.org/lindenii/furgit/objectstore/mix" - objectpacked "codeberg.org/lindenii/furgit/objectstore/packed" -) - -func openObjectStore(root *os.Root, algo objectid.Algorithm) (objectstore.Store, *objectloose.Store, error) { - objectsRoot, err := root.OpenRoot("objects") - if err != nil { - return nil, nil, fmt.Errorf("repository: open objects: %w", err) - } - - looseStore, err := objectloose.New(objectsRoot, algo) - if err != nil { - return nil, nil, err - } - - backends := []objectstore.Store{looseStore} - - packRoot, err := objectsRoot.OpenRoot("pack") - if err == nil { - var packedStore *objectpacked.Store - - packedStore, err = objectpacked.New(packRoot, algo) - if err != nil { - _ = looseStore.Close() - - return nil, nil, err - } - - backends = append(backends, packedStore) - } else if !errors.Is(err, os.ErrNotExist) { - _ = looseStore.Close() - - return nil, nil, fmt.Errorf("repository: open objects/pack: %w", err) - } - - objectsChain := objectmix.New(backends...) - - objectsRootForWriting, err := root.OpenRoot("objects") - if err != nil { - _ = objectsChain.Close() - - return nil, nil, fmt.Errorf("repository: open objects for loose writing: %w", err) - } - - objectsLooseForWritingOnly, err := objectloose.New(objectsRootForWriting, algo) - if err != nil { - _ = objectsRootForWriting.Close() - _ = objectsChain.Close() - - return nil, nil, err - } - - return objectsChain, objectsLooseForWritingOnly, nil -} diff --git a/repository/open_refs.go b/repository/open_refs.go deleted file mode 100644 index c802b4fa..00000000 --- a/repository/open_refs.go +++ /dev/null @@ -1,47 +0,0 @@ -package repository - -import ( - "errors" - "fmt" - "os" - - "codeberg.org/lindenii/furgit/objectid" - "codeberg.org/lindenii/furgit/refstore" - refchain "codeberg.org/lindenii/furgit/refstore/chain" - refloose "codeberg.org/lindenii/furgit/refstore/loose" - refpacked "codeberg.org/lindenii/furgit/refstore/packed" -) - -func openRefStore(root *os.Root, algo objectid.Algorithm) (out refstore.Store, err error) { - looseRoot, err := root.OpenRoot(".") - if err != nil { - return nil, fmt.Errorf("repository: open root for loose refs: %w", err) - } - - looseStore, err := refloose.New(looseRoot, algo) - if err != nil { - _ = looseRoot.Close() - - return nil, err - } - - backends := []refstore.Store{looseStore} - - _, err = root.Stat("packed-refs") - if err == nil { - packedStore, packedErr := refpacked.New(root, algo) - if packedErr != nil { - _ = looseStore.Close() - - return nil, packedErr - } - - backends = append(backends, packedStore) - } else if !errors.Is(err, os.ErrNotExist) { - _ = looseStore.Close() - - return nil, fmt.Errorf("repository: stat packed-refs: %w", err) - } - - return refchain.New(backends...), nil -} diff --git a/repository/read_stored.go b/repository/read_stored.go deleted file mode 100644 index 1e92dc40..00000000 --- a/repository/read_stored.go +++ /dev/null @@ -1,111 +0,0 @@ -package repository - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/object" - "codeberg.org/lindenii/furgit/objectid" - "codeberg.org/lindenii/furgit/objectstored" - "codeberg.org/lindenii/furgit/objecttype" -) - -// ReadStored reads, parses, and wraps one object by ID. -func (repo *Repository) ReadStored(id objectid.ObjectID) (objectstored.StoredObject, error) { - parsed, err := repo.readParsedObject(id) - if err != nil { - return nil, err - } - - switch parsed := parsed.(type) { - case *object.Blob: - return objectstored.NewStoredBlob(id, parsed), nil - case *object.Tree: - return objectstored.NewStoredTree(id, parsed), nil - case *object.Commit: - return objectstored.NewStoredCommit(id, parsed), nil - case *object.Tag: - return objectstored.NewStoredTag(id, parsed), nil - default: - return nil, fmt.Errorf("repository: unsupported parsed object type %T", parsed) - } -} - -// ReadStoredBlob reads and parses a blob object by ID. -func (repo *Repository) ReadStoredBlob(id objectid.ObjectID) (*objectstored.StoredBlob, error) { - stored, err := repo.ReadStored(id) - if err != nil { - return nil, err - } - - blob, ok := stored.(*objectstored.StoredBlob) - if !ok { - return nil, fmt.Errorf("repository: expected blob object %s, got %v", id, stored.Object().ObjectType()) - } - - return blob, nil -} - -// ReadStoredTree reads and parses a tree object by ID. -func (repo *Repository) ReadStoredTree(id objectid.ObjectID) (*objectstored.StoredTree, error) { - stored, err := repo.ReadStored(id) - if err != nil { - return nil, err - } - - tree, ok := stored.(*objectstored.StoredTree) - if !ok { - return nil, fmt.Errorf("repository: expected tree object %s, got %v", id, stored.Object().ObjectType()) - } - - return tree, nil -} - -// ReadStoredCommit reads and parses a commit object by ID. -func (repo *Repository) ReadStoredCommit(id objectid.ObjectID) (*objectstored.StoredCommit, error) { - stored, err := repo.ReadStored(id) - if err != nil { - return nil, err - } - - commit, ok := stored.(*objectstored.StoredCommit) - if !ok { - return nil, fmt.Errorf("repository: expected commit object %s, got %v", id, stored.Object().ObjectType()) - } - - return commit, nil -} - -// ReadStoredTag reads and parses a tag object by ID. -func (repo *Repository) ReadStoredTag(id objectid.ObjectID) (*objectstored.StoredTag, error) { - stored, err := repo.ReadStored(id) - if err != nil { - return nil, err - } - - tag, ok := stored.(*objectstored.StoredTag) - if !ok { - return nil, fmt.Errorf("repository: expected tag object %s, got %v", id, stored.Object().ObjectType()) - } - - return tag, nil -} - -// readParsedObject reads bytes content from storage and parses one object. -func (repo *Repository) readParsedObject(id objectid.ObjectID) (object.Object, error) { - ty, content, err := repo.objects.ReadBytesContent(id) - if err != nil { - return nil, err - } - - parsed, err := object.ParseObjectWithoutHeader(ty, content, repo.algo) - if err != nil { - tyName, ok := objecttype.Name(ty) - if !ok { - tyName = fmt.Sprintf("type %d", ty) - } - - return nil, fmt.Errorf("repository: parse object %s (%s): %w", id, tyName, err) - } - - return parsed, nil -} diff --git a/repository/refs.go b/repository/refs.go new file mode 100644 index 00000000..bdcab843 --- /dev/null +++ b/repository/refs.go @@ -0,0 +1,52 @@ +package repository + +import ( + "errors" + "fmt" + "os" + + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/refstore" + refchain "codeberg.org/lindenii/furgit/refstore/chain" + refloose "codeberg.org/lindenii/furgit/refstore/loose" + refpacked "codeberg.org/lindenii/furgit/refstore/packed" +) + +func openRefStore(root *os.Root, algo objectid.Algorithm) (out refstore.Store, err error) { + looseRoot, err := root.OpenRoot(".") + if err != nil { + return nil, fmt.Errorf("repository: open root for loose refs: %w", err) + } + + looseStore, err := refloose.New(looseRoot, algo) + if err != nil { + _ = looseRoot.Close() + + return nil, err + } + + backends := []refstore.Store{looseStore} + + _, err = root.Stat("packed-refs") + if err == nil { + packedStore, packedErr := refpacked.New(root, algo) + if packedErr != nil { + _ = looseStore.Close() + + return nil, packedErr + } + + backends = append(backends, packedStore) + } else if !errors.Is(err, os.ErrNotExist) { + _ = looseStore.Close() + + return nil, fmt.Errorf("repository: stat packed-refs: %w", err) + } + + return refchain.New(backends...), nil +} + +// Refs returns the configured ref store. +func (repo *Repository) Refs() refstore.Store { + return repo.refs +} diff --git a/repository/refs_test.go b/repository/refs_test.go new file mode 100644 index 00000000..8d8c604e --- /dev/null +++ b/repository/refs_test.go @@ -0,0 +1,139 @@ +package repository_test + +import ( + "os" + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/objecttype" + "codeberg.org/lindenii/furgit/ref" + "codeberg.org/lindenii/furgit/repository" +) + +func TestOpenFilesRefFormat(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", + }) + + _, _, commitID := repoHarness.MakeCommit(t, "files refs") + repoHarness.UpdateRef(t, "refs/heads/main", commitID) + repoHarness.SymbolicRef(t, "HEAD", "refs/heads/main") + + root, err := os.OpenRoot(repoHarness.Dir()) + if err != nil { + t.Fatalf("os.OpenRoot: %v", err) + } + + defer func() { _ = root.Close() }() + + repo, err := repository.Open(root) + if err != nil { + t.Fatalf("repository.Open: %v", err) + } + + defer func() { _ = repo.Close() }() + + if repo.Algorithm() != algo { + t.Fatalf("Algorithm = %v, want %v", repo.Algorithm(), algo) + } + + headerType, headerSize, err := repo.Objects().ReadHeader(commitID) + if err != nil { + t.Fatalf("ReadHeader(commit): %v", err) + } + + if headerType != objecttype.TypeCommit { + t.Fatalf("ReadHeader(commit) type = %v, want %v", headerType, objecttype.TypeCommit) + } + + if headerSize <= 0 { + t.Fatalf("ReadHeader(commit) size = %d, want > 0", headerSize) + } + + resolved, err := repo.Refs().Resolve("refs/heads/main") + if err != nil { + t.Fatalf("Resolve(refs/heads/main): %v", err) + } + + detached, ok := resolved.(ref.Detached) + if !ok { + t.Fatalf("Resolve(refs/heads/main) type = %T, want ref.Detached", resolved) + } + + if detached.ID != commitID { + t.Fatalf("Resolve(refs/heads/main) id = %s, want %s", detached.ID, commitID) + } + + head, err := repo.Refs().ResolveFully("HEAD") + if err != nil { + t.Fatalf("ResolveFully(HEAD): %v", err) + } + + if head.ID != commitID { + t.Fatalf("ResolveFully(HEAD) id = %s, want %s", head.ID, commitID) + } + }) +} + +func TestOpenFilesWithPackedRefs(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + repoHarness := newRepoForRefs(t, algo, "files") + commitID := writeMainAndHead(t, repoHarness) + repoHarness.PackRefs(t, "--all", "--prune") + assertResolveFully(t, repoHarness, "refs/heads/main", commitID) + }) +} + +func newRepoForRefs(t *testing.T, algo objectid.Algorithm, refFormat string) *testgit.TestRepo { + t.Helper() + + return testgit.NewRepo(t, testgit.RepoOptions{ + ObjectFormat: algo, + Bare: true, + RefFormat: refFormat, + }) +} + +func writeMainAndHead(t *testing.T, repoHarness *testgit.TestRepo) objectid.ObjectID { + t.Helper() + _, _, commitID := repoHarness.MakeCommit(t, "refs") + repoHarness.UpdateRef(t, "refs/heads/main", commitID) + repoHarness.SymbolicRef(t, "HEAD", "refs/heads/main") + + return commitID +} + +func assertResolveFully(t *testing.T, repoHarness *testgit.TestRepo, name string, want objectid.ObjectID) { + t.Helper() + + root, err := os.OpenRoot(repoHarness.Dir()) + if err != nil { + t.Fatalf("os.OpenRoot: %v", err) + } + + defer func() { _ = root.Close() }() + + repo, err := repository.Open(root) + if err != nil { + t.Fatalf("repository.Open: %v", err) + } + + defer func() { _ = repo.Close() }() + + resolved, err := repo.Refs().ResolveFully(name) + if err != nil { + t.Fatalf("ResolveFully(%s): %v", name, err) + } + + if resolved.ID != want { + t.Fatalf("ResolveFully(%s) id = %s, want %s", name, resolved.ID, want) + } +} diff --git a/repository/repository.go b/repository/repository.go index 02945456..aeb048bd 100644 --- a/repository/repository.go +++ b/repository/repository.go @@ -2,8 +2,6 @@ package repository import ( - "errors" - "codeberg.org/lindenii/furgit/config" "codeberg.org/lindenii/furgit/objectid" "codeberg.org/lindenii/furgit/objectstore" @@ -23,55 +21,3 @@ type Repository struct { objectsLooseForWritingOnly *objectloose.Store refs refstore.Store } - -// Algorithm returns the repository object ID algorithm. -func (repo *Repository) Algorithm() objectid.Algorithm { - return repo.algo -} - -// Config returns the parsed repository configuration snapshot. -// -// The returned pointer is owned by Repository. Callers should treat it as -// read-only. -func (repo *Repository) Config() *config.Config { - return repo.config -} - -// Objects returns the configured object store. -func (repo *Repository) Objects() objectstore.Store { - return repo.objects -} - -// Refs returns the configured ref store. -func (repo *Repository) Refs() refstore.Store { - return repo.refs -} - -// Close closes owned stores and filesystem roots. -// The behavior of the repo after Close is undefined. -func (repo *Repository) Close() error { - var errs []error - - if repo.refs != nil { - err := repo.refs.Close() - if err != nil { - errs = append(errs, err) - } - } - - if repo.objects != nil { - err := repo.objects.Close() - if err != nil { - errs = append(errs, err) - } - } - - if repo.objectsLooseForWritingOnly != nil { - err := repo.objectsLooseForWritingOnly.Close() - if err != nil { - errs = append(errs, err) - } - } - - return errors.Join(errs...) -} diff --git a/repository/repository_test.go b/repository/repository_test.go deleted file mode 100644 index 8d8c604e..00000000 --- a/repository/repository_test.go +++ /dev/null @@ -1,139 +0,0 @@ -package repository_test - -import ( - "os" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/objectid" - "codeberg.org/lindenii/furgit/objecttype" - "codeberg.org/lindenii/furgit/ref" - "codeberg.org/lindenii/furgit/repository" -) - -func TestOpenFilesRefFormat(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", - }) - - _, _, commitID := repoHarness.MakeCommit(t, "files refs") - repoHarness.UpdateRef(t, "refs/heads/main", commitID) - repoHarness.SymbolicRef(t, "HEAD", "refs/heads/main") - - root, err := os.OpenRoot(repoHarness.Dir()) - if err != nil { - t.Fatalf("os.OpenRoot: %v", err) - } - - defer func() { _ = root.Close() }() - - repo, err := repository.Open(root) - if err != nil { - t.Fatalf("repository.Open: %v", err) - } - - defer func() { _ = repo.Close() }() - - if repo.Algorithm() != algo { - t.Fatalf("Algorithm = %v, want %v", repo.Algorithm(), algo) - } - - headerType, headerSize, err := repo.Objects().ReadHeader(commitID) - if err != nil { - t.Fatalf("ReadHeader(commit): %v", err) - } - - if headerType != objecttype.TypeCommit { - t.Fatalf("ReadHeader(commit) type = %v, want %v", headerType, objecttype.TypeCommit) - } - - if headerSize <= 0 { - t.Fatalf("ReadHeader(commit) size = %d, want > 0", headerSize) - } - - resolved, err := repo.Refs().Resolve("refs/heads/main") - if err != nil { - t.Fatalf("Resolve(refs/heads/main): %v", err) - } - - detached, ok := resolved.(ref.Detached) - if !ok { - t.Fatalf("Resolve(refs/heads/main) type = %T, want ref.Detached", resolved) - } - - if detached.ID != commitID { - t.Fatalf("Resolve(refs/heads/main) id = %s, want %s", detached.ID, commitID) - } - - head, err := repo.Refs().ResolveFully("HEAD") - if err != nil { - t.Fatalf("ResolveFully(HEAD): %v", err) - } - - if head.ID != commitID { - t.Fatalf("ResolveFully(HEAD) id = %s, want %s", head.ID, commitID) - } - }) -} - -func TestOpenFilesWithPackedRefs(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - repoHarness := newRepoForRefs(t, algo, "files") - commitID := writeMainAndHead(t, repoHarness) - repoHarness.PackRefs(t, "--all", "--prune") - assertResolveFully(t, repoHarness, "refs/heads/main", commitID) - }) -} - -func newRepoForRefs(t *testing.T, algo objectid.Algorithm, refFormat string) *testgit.TestRepo { - t.Helper() - - return testgit.NewRepo(t, testgit.RepoOptions{ - ObjectFormat: algo, - Bare: true, - RefFormat: refFormat, - }) -} - -func writeMainAndHead(t *testing.T, repoHarness *testgit.TestRepo) objectid.ObjectID { - t.Helper() - _, _, commitID := repoHarness.MakeCommit(t, "refs") - repoHarness.UpdateRef(t, "refs/heads/main", commitID) - repoHarness.SymbolicRef(t, "HEAD", "refs/heads/main") - - return commitID -} - -func assertResolveFully(t *testing.T, repoHarness *testgit.TestRepo, name string, want objectid.ObjectID) { - t.Helper() - - root, err := os.OpenRoot(repoHarness.Dir()) - if err != nil { - t.Fatalf("os.OpenRoot: %v", err) - } - - defer func() { _ = root.Close() }() - - repo, err := repository.Open(root) - if err != nil { - t.Fatalf("repository.Open: %v", err) - } - - defer func() { _ = repo.Close() }() - - resolved, err := repo.Refs().ResolveFully(name) - if err != nil { - t.Fatalf("ResolveFully(%s): %v", name, err) - } - - if resolved.ID != want { - t.Fatalf("ResolveFully(%s) id = %s, want %s", name, resolved.ID, want) - } -} diff --git a/repository/stored.go b/repository/stored.go new file mode 100644 index 00000000..1e92dc40 --- /dev/null +++ b/repository/stored.go @@ -0,0 +1,111 @@ +package repository + +import ( + "fmt" + + "codeberg.org/lindenii/furgit/object" + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/objectstored" + "codeberg.org/lindenii/furgit/objecttype" +) + +// ReadStored reads, parses, and wraps one object by ID. +func (repo *Repository) ReadStored(id objectid.ObjectID) (objectstored.StoredObject, error) { + parsed, err := repo.readParsedObject(id) + if err != nil { + return nil, err + } + + switch parsed := parsed.(type) { + case *object.Blob: + return objectstored.NewStoredBlob(id, parsed), nil + case *object.Tree: + return objectstored.NewStoredTree(id, parsed), nil + case *object.Commit: + return objectstored.NewStoredCommit(id, parsed), nil + case *object.Tag: + return objectstored.NewStoredTag(id, parsed), nil + default: + return nil, fmt.Errorf("repository: unsupported parsed object type %T", parsed) + } +} + +// ReadStoredBlob reads and parses a blob object by ID. +func (repo *Repository) ReadStoredBlob(id objectid.ObjectID) (*objectstored.StoredBlob, error) { + stored, err := repo.ReadStored(id) + if err != nil { + return nil, err + } + + blob, ok := stored.(*objectstored.StoredBlob) + if !ok { + return nil, fmt.Errorf("repository: expected blob object %s, got %v", id, stored.Object().ObjectType()) + } + + return blob, nil +} + +// ReadStoredTree reads and parses a tree object by ID. +func (repo *Repository) ReadStoredTree(id objectid.ObjectID) (*objectstored.StoredTree, error) { + stored, err := repo.ReadStored(id) + if err != nil { + return nil, err + } + + tree, ok := stored.(*objectstored.StoredTree) + if !ok { + return nil, fmt.Errorf("repository: expected tree object %s, got %v", id, stored.Object().ObjectType()) + } + + return tree, nil +} + +// ReadStoredCommit reads and parses a commit object by ID. +func (repo *Repository) ReadStoredCommit(id objectid.ObjectID) (*objectstored.StoredCommit, error) { + stored, err := repo.ReadStored(id) + if err != nil { + return nil, err + } + + commit, ok := stored.(*objectstored.StoredCommit) + if !ok { + return nil, fmt.Errorf("repository: expected commit object %s, got %v", id, stored.Object().ObjectType()) + } + + return commit, nil +} + +// ReadStoredTag reads and parses a tag object by ID. +func (repo *Repository) ReadStoredTag(id objectid.ObjectID) (*objectstored.StoredTag, error) { + stored, err := repo.ReadStored(id) + if err != nil { + return nil, err + } + + tag, ok := stored.(*objectstored.StoredTag) + if !ok { + return nil, fmt.Errorf("repository: expected tag object %s, got %v", id, stored.Object().ObjectType()) + } + + return tag, nil +} + +// readParsedObject reads bytes content from storage and parses one object. +func (repo *Repository) readParsedObject(id objectid.ObjectID) (object.Object, error) { + ty, content, err := repo.objects.ReadBytesContent(id) + if err != nil { + return nil, err + } + + parsed, err := object.ParseObjectWithoutHeader(ty, content, repo.algo) + if err != nil { + tyName, ok := objecttype.Name(ty) + if !ok { + tyName = fmt.Sprintf("type %d", ty) + } + + return nil, fmt.Errorf("repository: parse object %s (%s): %w", id, tyName, err) + } + + return parsed, nil +} diff --git a/repository/traversal_bench_test.go b/repository/traversal_bench_test.go deleted file mode 100644 index ada0f8d8..00000000 --- a/repository/traversal_bench_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package repository_test - -import ( - "os" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/object" - "codeberg.org/lindenii/furgit/repository" -) - -const benchRepoPathEnv = "FURGIT_BENCH_REPO" - -// BenchmarkTraverseHeadTree measures iterative traversal of HEAD's root tree. -// -// Set FURGIT_BENCH_REPO to a repository path (typically .git or a bare repo) -// before running this benchmark. -func BenchmarkTraverseHeadTree(b *testing.B) { - repoPath := strings.TrimSpace(os.Getenv(benchRepoPathEnv)) - if repoPath == "" { - b.Fatalf("missing %s", benchRepoPathEnv) - } - - root, err := os.OpenRoot(repoPath) - if err != nil { - b.Fatalf("os.OpenRoot(%q): %v", repoPath, err) - } - - b.Cleanup(func() { - _ = root.Close() - }) - - repo, err := repository.Open(root) - if err != nil { - b.Fatalf("repository.Open(root for %q): %v", repoPath, err) - } - - b.Cleanup(func() { - _ = repo.Close() - }) - - head, err := repo.Refs().ResolveFully("HEAD") - if err != nil { - b.Fatalf("ResolveRefFully(HEAD): %v", err) - } - - stored, err := repo.ReadStored(head.ID) - if err != nil { - b.Fatalf("ReadStored(%s): %v", head.ID, err) - } - - commit, ok := stored.Object().(*object.Commit) - if !ok { - b.Fatalf("HEAD object type %T, want *object.Commit", stored.Object()) - } - - b.ReportAllocs() - b.ResetTimer() - - var lastCount int - for b.Loop() { - lastCount, err = traverseTreeIter(repo, commit.Tree) - if err != nil { - b.Fatalf("traverseTreeIter: %v", err) - } - } - - b.StopTimer() - - if lastCount <= 0 { - b.Fatalf("traverseTreeIter count = %d, want > 0", lastCount) - } -} diff --git a/repository/traversal_helpers_test.go b/repository/traversal_helpers_test.go deleted file mode 100644 index 143d3b62..00000000 --- a/repository/traversal_helpers_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package repository_test - -import ( - "codeberg.org/lindenii/furgit/object" - "codeberg.org/lindenii/furgit/objectid" - "codeberg.org/lindenii/furgit/repository" -) - -type treeWalkFrame struct { - id objectid.ObjectID - isTree bool -} - -func traverseTreeIter(repo *repository.Repository, root objectid.ObjectID) (int, error) { - stack := []treeWalkFrame{{id: root, isTree: true}} - total := 0 - - for len(stack) > 0 { - frame := stack[len(stack)-1] - stack = stack[:len(stack)-1] - id := frame.id - - if !frame.isTree { - _, err := repo.Objects().ReadSize(id) - if err != nil { - return 0, err - } - - total++ - - continue - } - - tree, err := repo.ReadStoredTree(id) - if err != nil { - return 0, err - } - - total++ - - for i := len(tree.Tree().Entries) - 1; i >= 0; i-- { - entry := tree.Tree().Entries[i] - if entry.Mode == object.FileModeGitlink { - continue - } - - stack = append(stack, treeWalkFrame{ - id: entry.ID, - isTree: entry.Mode == object.FileModeDir, - }) - } - } - - return total, nil -} - -func traverseReachableIter(repo *repository.Repository, root objectid.ObjectID) (int, error) { - stack := []objectid.ObjectID{root} - visited := make(map[objectid.ObjectID]struct{}) - total := 0 - - for len(stack) > 0 { - id := stack[len(stack)-1] - stack = stack[:len(stack)-1] - - _, ok := visited[id] - if ok { - continue - } - - visited[id] = struct{}{} - - stored, err := repo.ReadStored(id) - if err != nil { - return 0, err - } - - total++ - - switch obj := stored.Object().(type) { - case *object.Commit: - stack = append(stack, obj.Tree) - stack = append(stack, obj.Parents...) - case *object.Tree: - for i := len(obj.Entries) - 1; i >= 0; i-- { - entry := obj.Entries[i] - if entry.Mode == object.FileModeGitlink { - continue - } - - stack = append(stack, entry.ID) - } - case *object.Tag: - stack = append(stack, obj.Target) - case *object.Blob: - default: - // Unknown parsed object variants are treated as leaves. - } - } - - return total, nil -} diff --git a/repository/traversal_test.go b/repository/traversal_test.go index 47300e7f..ff3614dc 100644 --- a/repository/traversal_test.go +++ b/repository/traversal_test.go @@ -8,6 +8,7 @@ import ( "testing" "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/object" "codeberg.org/lindenii/furgit/objectid" "codeberg.org/lindenii/furgit/repository" ) @@ -131,3 +132,50 @@ func walkRepositoryFromHead(t *testing.T, repoPath string) { t.Fatalf("no objects were enumerated from HEAD (%s)", fmt.Sprintf("%q", repoPath)) } } + +func traverseReachableIter(repo *repository.Repository, root objectid.ObjectID) (int, error) { + stack := []objectid.ObjectID{root} + visited := make(map[objectid.ObjectID]struct{}) + total := 0 + + for len(stack) > 0 { + id := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + _, ok := visited[id] + if ok { + continue + } + + visited[id] = struct{}{} + + stored, err := repo.ReadStored(id) + if err != nil { + return 0, err + } + + total++ + + switch obj := stored.Object().(type) { + case *object.Commit: + stack = append(stack, obj.Tree) + stack = append(stack, obj.Parents...) + case *object.Tree: + for i := len(obj.Entries) - 1; i >= 0; i-- { + entry := obj.Entries[i] + if entry.Mode == object.FileModeGitlink { + continue + } + + stack = append(stack, entry.ID) + } + case *object.Tag: + stack = append(stack, obj.Target) + case *object.Blob: + default: + // Unknown parsed object variants are treated as leaves. + } + } + + return total, nil +} diff --git a/repository/tree.go b/repository/tree.go new file mode 100644 index 00000000..d4ef529e --- /dev/null +++ b/repository/tree.go @@ -0,0 +1,53 @@ +package repository + +import ( + "errors" + "fmt" + + "codeberg.org/lindenii/furgit/object" + "codeberg.org/lindenii/furgit/objectstored" +) + +// ResolveTreeEntry resolves one path within a stored root tree. +// +// parts must contain at least one path segment. Intermediate segments must be +// tree entries. +func (repo *Repository) ResolveTreeEntry(tree *objectstored.StoredTree, parts [][]byte) (object.TreeEntry, error) { + if tree == nil { + return object.TreeEntry{}, errors.New("repository: nil root tree") + } + + if len(parts) == 0 { + return object.TreeEntry{}, errors.New("repository: empty tree path") + } + + current := tree + + for i, part := range parts { + if len(part) == 0 { + return object.TreeEntry{}, errors.New("repository: empty tree path segment") + } + + entry := current.Tree().Entry(part) + if entry == nil { + return object.TreeEntry{}, fmt.Errorf("repository: tree entry %q not found", part) + } + + if i == len(parts)-1 { + return *entry, nil + } + + if entry.Mode != object.FileModeDir { + return object.TreeEntry{}, fmt.Errorf("repository: path segment %q is not a tree", part) + } + + next, err := repo.ReadStoredTree(entry.ID) + if err != nil { + return object.TreeEntry{}, err + } + + current = next + } + + return object.TreeEntry{}, fmt.Errorf("repository: tree entry not found") +} diff --git a/repository/tree_resolve.go b/repository/tree_resolve.go deleted file mode 100644 index d4ef529e..00000000 --- a/repository/tree_resolve.go +++ /dev/null @@ -1,53 +0,0 @@ -package repository - -import ( - "errors" - "fmt" - - "codeberg.org/lindenii/furgit/object" - "codeberg.org/lindenii/furgit/objectstored" -) - -// ResolveTreeEntry resolves one path within a stored root tree. -// -// parts must contain at least one path segment. Intermediate segments must be -// tree entries. -func (repo *Repository) ResolveTreeEntry(tree *objectstored.StoredTree, parts [][]byte) (object.TreeEntry, error) { - if tree == nil { - return object.TreeEntry{}, errors.New("repository: nil root tree") - } - - if len(parts) == 0 { - return object.TreeEntry{}, errors.New("repository: empty tree path") - } - - current := tree - - for i, part := range parts { - if len(part) == 0 { - return object.TreeEntry{}, errors.New("repository: empty tree path segment") - } - - entry := current.Tree().Entry(part) - if entry == nil { - return object.TreeEntry{}, fmt.Errorf("repository: tree entry %q not found", part) - } - - if i == len(parts)-1 { - return *entry, nil - } - - if entry.Mode != object.FileModeDir { - return object.TreeEntry{}, fmt.Errorf("repository: path segment %q is not a tree", part) - } - - next, err := repo.ReadStoredTree(entry.ID) - if err != nil { - return object.TreeEntry{}, err - } - - current = next - } - - return object.TreeEntry{}, fmt.Errorf("repository: tree entry not found") -} -- cgit v1.3.1-10-gc9f91