From 53e166913d9e76b82aa53361b251390a2c0726bd Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Sat, 21 Feb 2026 15:18:32 +0800 Subject: repository: Add passthrough ReadStored*; add ref convenience funcs --- repository/read_stored_passthrough.go | 38 +++++++++++ repository/read_stored_passthrough_test.go | 100 +++++++++++++++++++++++++++++ repository/refs.go | 25 ++++++++ repository/refs_test.go | 96 +++++++++++++++++++++++++++ 4 files changed, 259 insertions(+) create mode 100644 repository/read_stored_passthrough.go create mode 100644 repository/read_stored_passthrough_test.go create mode 100644 repository/refs.go create mode 100644 repository/refs_test.go diff --git a/repository/read_stored_passthrough.go b/repository/read_stored_passthrough.go new file mode 100644 index 00000000..43864bde --- /dev/null +++ b/repository/read_stored_passthrough.go @@ -0,0 +1,38 @@ +package repository + +import ( + "io" + + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/objecttype" +) + +// ReadStoredHeader reads an object's type and declared content length. +func (repo *Repository) ReadStoredHeader(id objectid.ObjectID) (objecttype.Type, int64, error) { + return repo.objects.ReadHeader(id) +} + +// ReadStoredBytesFull reads a full serialized object as "type size\\x00content". +func (repo *Repository) ReadStoredBytesFull(id objectid.ObjectID) ([]byte, error) { + return repo.objects.ReadBytesFull(id) +} + +// ReadStoredBytesContent reads an object's type and content bytes. +func (repo *Repository) ReadStoredBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) { + return repo.objects.ReadBytesContent(id) +} + +// ReadStoredReaderFull reads a full serialized object stream. +// +// Caller must close the returned reader. +func (repo *Repository) ReadStoredReaderFull(id objectid.ObjectID) (io.ReadCloser, error) { + return repo.objects.ReadReaderFull(id) +} + +// ReadStoredReaderContent reads an object's type, declared content length, and +// content stream. +// +// Caller must close the returned reader. +func (repo *Repository) ReadStoredReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) { + return repo.objects.ReadReaderContent(id) +} diff --git a/repository/read_stored_passthrough_test.go b/repository/read_stored_passthrough_test.go new file mode 100644 index 00000000..f320907a --- /dev/null +++ b/repository/read_stored_passthrough_test.go @@ -0,0 +1,100 @@ +package repository_test + +import ( + "bytes" + "io" + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/objecttype" + "codeberg.org/lindenii/furgit/repository" +) + +func TestReadStoredPassThroughs(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, "pass-through") + + repo, err := repository.Open(repoHarness.Dir()) + if err != nil { + t.Fatalf("repository.Open: %v", err) + } + defer func() { _ = repo.Close() }() + + headerTy, headerSize, err := repo.ReadStoredHeader(commitID) + if err != nil { + t.Fatalf("ReadStoredHeader: %v", err) + } + if headerTy != objecttype.TypeCommit { + t.Fatalf("ReadStoredHeader type = %v, want %v", headerTy, objecttype.TypeCommit) + } + if headerSize <= 0 { + t.Fatalf("ReadStoredHeader size = %d, want > 0", headerSize) + } + + full, err := repo.ReadStoredBytesFull(commitID) + if err != nil { + t.Fatalf("ReadStoredBytesFull: %v", err) + } + if len(full) == 0 { + t.Fatalf("ReadStoredBytesFull returned empty payload") + } + + contentTy, content, err := repo.ReadStoredBytesContent(commitID) + if err != nil { + t.Fatalf("ReadStoredBytesContent: %v", err) + } + if contentTy != objecttype.TypeCommit { + t.Fatalf("ReadStoredBytesContent type = %v, want %v", contentTy, objecttype.TypeCommit) + } + if len(content) == 0 { + t.Fatalf("ReadStoredBytesContent returned empty content") + } + + fullReader, err := repo.ReadStoredReaderFull(commitID) + if err != nil { + t.Fatalf("ReadStoredReaderFull: %v", err) + } + fullReaderBytes, readErr := io.ReadAll(fullReader) + closeErr := fullReader.Close() + if readErr != nil { + t.Fatalf("ReadStoredReaderFull read: %v", readErr) + } + if closeErr != nil { + t.Fatalf("ReadStoredReaderFull close: %v", closeErr) + } + if !bytes.Equal(fullReaderBytes, full) { + t.Fatalf("ReadStoredReaderFull bytes mismatch against ReadStoredBytesFull") + } + + readerTy, readerSize, contentReader, err := repo.ReadStoredReaderContent(commitID) + if err != nil { + t.Fatalf("ReadStoredReaderContent: %v", err) + } + if readerTy != objecttype.TypeCommit { + t.Fatalf("ReadStoredReaderContent type = %v, want %v", readerTy, objecttype.TypeCommit) + } + if readerSize != int64(len(content)) { + t.Fatalf("ReadStoredReaderContent size = %d, want %d", readerSize, len(content)) + } + readerContentBytes, readErr := io.ReadAll(contentReader) + closeErr = contentReader.Close() + if readErr != nil { + t.Fatalf("ReadStoredReaderContent read: %v", readErr) + } + if closeErr != nil { + t.Fatalf("ReadStoredReaderContent close: %v", closeErr) + } + if !bytes.Equal(readerContentBytes, content) { + t.Fatalf("ReadStoredReaderContent bytes mismatch against ReadStoredBytesContent") + } + }) +} diff --git a/repository/refs.go b/repository/refs.go new file mode 100644 index 00000000..1eaf5c97 --- /dev/null +++ b/repository/refs.go @@ -0,0 +1,25 @@ +package repository + +import ( + "codeberg.org/lindenii/furgit/ref" +) + +// ResolveRef resolves one reference name to symbolic or detached form. +func (repo *Repository) ResolveRef(name string) (ref.Ref, error) { + return repo.refs.Resolve(name) +} + +// ResolveRefFully resolves one reference name to detached form. +func (repo *Repository) ResolveRefFully(name string) (ref.Detached, error) { + return repo.refs.ResolveFully(name) +} + +// ListRefs lists references matching pattern. +func (repo *Repository) ListRefs(pattern string) ([]ref.Ref, error) { + return repo.refs.List(pattern) +} + +// ShortenRef returns the shortest unambiguous shorthand for a full reference name. +func (repo *Repository) ShortenRef(name string) (string, error) { + return repo.refs.Shorten(name) +} diff --git a/repository/refs_test.go b/repository/refs_test.go new file mode 100644 index 00000000..4418c707 --- /dev/null +++ b/repository/refs_test.go @@ -0,0 +1,96 @@ +package repository_test + +import ( + "strings" + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/ref" + "codeberg.org/lindenii/furgit/repository" +) + +func TestRefConvenienceMethods(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, "refs wrappers") + repoHarness.UpdateRef(t, "refs/heads/main", commitID) + repoHarness.SymbolicRef(t, "HEAD", "refs/heads/main") + repoHarness.UpdateRef(t, "refs/tags/v1", commitID) + + repo, err := repository.Open(repoHarness.Dir()) + if err != nil { + t.Fatalf("repository.Open: %v", err) + } + defer func() { _ = repo.Close() }() + + resolved, err := repo.ResolveRef("HEAD") + if err != nil { + t.Fatalf("ResolveRef(HEAD): %v", err) + } + sym, ok := resolved.(ref.Symbolic) + if !ok { + t.Fatalf("ResolveRef(HEAD) type = %T, want ref.Symbolic", resolved) + } + if sym.Target != "refs/heads/main" { + t.Fatalf("ResolveRef(HEAD) target = %q, want %q", sym.Target, "refs/heads/main") + } + + fully, err := repo.ResolveRefFully("HEAD") + if err != nil { + t.Fatalf("ResolveRefFully(HEAD): %v", err) + } + if fully.ID != commitID { + t.Fatalf("ResolveRefFully(HEAD) id = %s, want %s", fully.ID, commitID) + } + + refs, err := repo.ListRefs("refs/*/*") + if err != nil { + t.Fatalf("ListRefs: %v", err) + } + if len(refs) < 2 { + t.Fatalf("ListRefs returned %d refs, want >= 2", len(refs)) + } + + short, err := repo.ShortenRef("refs/heads/main") + if err != nil { + t.Fatalf("ShortenRef: %v", err) + } + if short != "heads/main" && short != "main" { + t.Fatalf("ShortenRef = %q, want %q or %q", short, "heads/main", "main") + } + }) +} + +func TestResolveRefErrorSurface(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", + }) + + repo, err := repository.Open(repoHarness.Dir()) + if err != nil { + t.Fatalf("repository.Open: %v", err) + } + defer func() { _ = repo.Close() }() + + _, err = repo.ResolveRef("refs/heads/does-not-exist") + if err == nil { + t.Fatalf("ResolveRef missing: expected error") + } + if !strings.Contains(err.Error(), "not found") { + t.Fatalf("ResolveRef missing error = %v, want not found detail", err) + } + }) +} -- cgit v1.3.1-10-gc9f91