aboutsummaryrefslogtreecommitdiff
path: root/object
diff options
context:
space:
mode:
authorGravatar Runxi Yu2026-04-02 06:23:30 +0000
committerGravatar Runxi Yu2026-04-02 06:28:39 +0000
commita041d523de389b65b98a5373a8034041db2a8d83 (patch)
tree7b423dc735f463be616045f2c3c2095a7737aca7 /object
parentresearch: Add dynamic pack resources (diff)
signatureNo signature
*: Remove
Diffstat (limited to 'object')
-rw-r--r--object/blob/blob.go14
-rw-r--r--object/blob/parse.go6
-rw-r--r--object/blob/parse_test.go30
-rw-r--r--object/blob/serialize.go32
-rw-r--r--object/blob/serialize_test.go30
-rw-r--r--object/blob/test.go10
-rw-r--r--object/commit/commit.go25
-rw-r--r--object/commit/extraheader.go7
-rw-r--r--object/commit/parse.go94
-rw-r--r--object/commit/parse_test.go91
-rw-r--r--object/commit/serialize.go84
-rw-r--r--object/commit/serialize_test.go34
-rw-r--r--object/commit/type.go10
-rw-r--r--object/doc.go7
-rw-r--r--object/fetch/doc.go8
-rw-r--r--object/fetch/exact_blob.go26
-rw-r--r--object/fetch/exact_blob_reader.go16
-rw-r--r--object/fetch/exact_commit.go26
-rw-r--r--object/fetch/exact_object.go20
-rw-r--r--object/fetch/exact_reader.go26
-rw-r--r--object/fetch/exact_tag.go26
-rw-r--r--object/fetch/exact_tree.go26
-rw-r--r--object/fetch/fetcher.go20
-rw-r--r--object/fetch/header.go18
-rw-r--r--object/fetch/object_errors.go19
-rw-r--r--object/fetch/object_parse.go27
-rw-r--r--object/fetch/path.go105
-rw-r--r--object/fetch/peel_to_blob.go31
-rw-r--r--object/fetch/peel_to_blob_id.go38
-rw-r--r--object/fetch/peel_to_blob_reader.go20
-rw-r--r--object/fetch/peel_to_commit.go31
-rw-r--r--object/fetch/peel_to_commit_id.go38
-rw-r--r--object/fetch/peel_to_tree.go35
-rw-r--r--object/fetch/peel_to_tree_id.go45
-rw-r--r--object/fetch/size.go15
-rw-r--r--object/fetch/treefs.go30
-rw-r--r--object/fetch/treefs_entry.go85
-rw-r--r--object/fetch/treefs_info.go75
-rw-r--r--object/fetch/treefs_new.go19
-rw-r--r--object/fetch/treefs_op.go28
-rw-r--r--object/fetch/treefs_open.go122
-rw-r--r--object/fetch/treefs_path.go11
-rw-r--r--object/fetch/treefs_readdir.go20
-rw-r--r--object/fetch/treefs_readfile.go40
-rw-r--r--object/fetch/treefs_stat.go22
-rw-r--r--object/fetch/treefs_sub.go22
-rw-r--r--object/fetch/treefs_test.go111
-rw-r--r--object/header/append.go29
-rw-r--r--object/header/doc.go3
-rw-r--r--object/header/encode.go8
-rw-r--r--object/header/parse.go42
-rw-r--r--object/id/algorithm.go12
-rw-r--r--object/id/algorithm_details.go17
-rw-r--r--object/id/algorithm_emptytree.go7
-rw-r--r--object/id/algorithm_hexlen.go6
-rw-r--r--object/id/algorithm_new.go13
-rw-r--r--object/id/algorithm_packhashid.go8
-rw-r--r--object/id/algorithm_parse.go8
-rw-r--r--object/id/algorithm_signatureheadername.go6
-rw-r--r--object/id/algorithm_size.go6
-rw-r--r--object/id/algorithm_string.go11
-rw-r--r--object/id/algorithm_sum.go6
-rw-r--r--object/id/algorithm_supported.go7
-rw-r--r--object/id/algorithm_tables.go72
-rw-r--r--object/id/algorithm_zero.go11
-rw-r--r--object/id/doc.go2
-rw-r--r--object/id/errors.go10
-rw-r--r--object/id/max_size.go6
-rw-r--r--object/id/objectid.go11
-rw-r--r--object/id/objectid_algorithm.go6
-rw-r--r--object/id/objectid_byte.go19
-rw-r--r--object/id/objectid_compare.go9
-rw-r--r--object/id/objectid_frombytes.go20
-rw-r--r--object/id/objectid_parse.go32
-rw-r--r--object/id/objectid_string.go10
-rw-r--r--object/id/objectid_test.go229
-rw-r--r--object/id/signatureheadername_parse.go9
-rw-r--r--object/object.go10
-rw-r--r--object/parse_with_header.go25
-rw-r--r--object/parse_without_header.go32
-rw-r--r--object/signature/parse.go97
-rw-r--r--object/signature/serialize.go33
-rw-r--r--object/signature/signature.go10
-rw-r--r--object/signature/when.go10
-rw-r--r--object/signed/commit/commit.go15
-rw-r--r--object/signed/commit/doc.go6
-rw-r--r--object/signed/commit/integration_test.go138
-rw-r--r--object/signed/commit/parse.go107
-rw-r--r--object/signed/commit/payload_append.go11
-rw-r--r--object/signed/commit/signature_algorithms.go16
-rw-r--r--object/signed/commit/signature_append.go17
-rw-r--r--object/signed/commit/unit_test.go170
-rw-r--r--object/signed/doc.go7
-rw-r--r--object/signed/tag/doc.go3
-rw-r--r--object/signed/tag/integration_test.go139
-rw-r--r--object/signed/tag/parse.go143
-rw-r--r--object/signed/tag/payload_append.go11
-rw-r--r--object/signed/tag/signature_algorithms.go16
-rw-r--r--object/signed/tag/signature_append.go17
-rw-r--r--object/signed/tag/tag.go15
-rw-r--r--object/signed/tag/unit_test.go257
-rw-r--r--object/store/base_quarantine.go17
-rw-r--r--object/store/chain/bytes.go46
-rw-r--r--object/store/chain/chain.go12
-rw-r--r--object/store/chain/header.go28
-rw-r--r--object/store/chain/new.go14
-rw-r--r--object/store/chain/reader.go47
-rw-r--r--object/store/chain/refresh.go17
-rw-r--r--object/store/chain/size.go27
-rw-r--r--object/store/cursor.go7
-rw-r--r--object/store/doc.go19
-rw-r--r--object/store/dual/doc.go8
-rw-r--r--object/store/dual/dual.go33
-rw-r--r--object/store/dual/dual_test.go266
-rw-r--r--object/store/dual/new.go29
-rw-r--r--object/store/dual/quarantine.go114
-rw-r--r--object/store/dual/quarantine_begin.go22
-rw-r--r--object/store/dual/quarantine_discard.go11
-rw-r--r--object/store/dual/quarantine_promote.go13
-rw-r--r--object/store/dual/reader.go57
-rw-r--r--object/store/dual/writer_object.go32
-rw-r--r--object/store/dual/writer_pack.go12
-rw-r--r--object/store/errors.go8
-rw-r--r--object/store/loose/helpers_test.go107
-rw-r--r--object/store/loose/parse.go55
-rw-r--r--object/store/loose/paths.go43
-rw-r--r--object/store/loose/quarantine.go19
-rw-r--r--object/store/loose/quarantine_begin.go63
-rw-r--r--object/store/loose/quarantine_discard.go18
-rw-r--r--object/store/loose/quarantine_promote.go116
-rw-r--r--object/store/loose/quarantine_test.go119
-rw-r--r--object/store/loose/read_bytes.go55
-rw-r--r--object/store/loose/read_header.go37
-rw-r--r--object/store/loose/read_reader.go114
-rw-r--r--object/store/loose/read_size.go13
-rw-r--r--object/store/loose/read_test.go212
-rw-r--r--object/store/loose/refresh.go6
-rw-r--r--object/store/loose/store.go43
-rw-r--r--object/store/loose/write_bytes.go18
-rw-r--r--object/store/loose/write_reader.go81
-rw-r--r--object/store/loose/write_temp_object_file.go30
-rw-r--r--object/store/loose/write_test.go137
-rw-r--r--object/store/loose/write_writer.go94
-rw-r--r--object/store/loose/write_writer_accept.go61
-rw-r--r--object/store/loose/write_writer_finalize.go89
-rw-r--r--object/store/memory/algorithm.go8
-rw-r--r--object/store/memory/doc.go2
-rw-r--r--object/store/memory/object.go9
-rw-r--r--object/store/memory/read_bytes.go37
-rw-r--r--object/store/memory/read_header.go17
-rw-r--r--object/store/memory/read_reader.go29
-rw-r--r--object/store/memory/read_size.go13
-rw-r--r--object/store/memory/refresh.go6
-rw-r--r--object/store/memory/store.go28
-rw-r--r--object/store/memory/write_bytes.go35
-rw-r--r--object/store/memory/write_reader.go55
-rw-r--r--object/store/memory/write_test.go192
-rw-r--r--object/store/mix/bytes.go51
-rw-r--r--object/store/mix/header.go30
-rw-r--r--object/store/mix/mix.go20
-rw-r--r--object/store/mix/mru.go74
-rw-r--r--object/store/mix/new.go40
-rw-r--r--object/store/mix/reader.go53
-rw-r--r--object/store/mix/refresh.go30
-rw-r--r--object/store/mix/size.go29
-rw-r--r--object/store/packed/doc.go3
-rw-r--r--object/store/packed/internal/doc.go6
-rw-r--r--object/store/packed/internal/ingest/TODO1
-rw-r--r--object/store/packed/internal/ingest/byteslice_reader.go21
-rw-r--r--object/store/packed/internal/ingest/cache.go53
-rw-r--r--object/store/packed/internal/ingest/counting_writer.go17
-rw-r--r--object/store/packed/internal/ingest/crc.go22
-rw-r--r--object/store/packed/internal/ingest/delta_header.go11
-rw-r--r--object/store/packed/internal/ingest/distance.go30
-rw-r--r--object/store/packed/internal/ingest/doc.go3
-rw-r--r--object/store/packed/internal/ingest/drain.go67
-rw-r--r--object/store/packed/internal/ingest/entry.go91
-rw-r--r--object/store/packed/internal/ingest/entry_header.go33
-rw-r--r--object/store/packed/internal/ingest/entry_prefix.go95
-rw-r--r--object/store/packed/internal/ingest/errors.go68
-rw-r--r--object/store/packed/internal/ingest/file_section_writer.go22
-rw-r--r--object/store/packed/internal/ingest/fill.go44
-rw-r--r--object/store/packed/internal/ingest/finalize.go94
-rw-r--r--object/store/packed/internal/ingest/flush.go37
-rw-r--r--object/store/packed/internal/ingest/hash.go27
-rw-r--r--object/store/packed/internal/ingest/header.go54
-rw-r--r--object/store/packed/internal/ingest/idx_write.go262
-rw-r--r--object/store/packed/internal/ingest/ingest.go68
-rw-r--r--object/store/packed/internal/ingest/ingest_test.go411
-rw-r--r--object/store/packed/internal/ingest/options.go26
-rw-r--r--object/store/packed/internal/ingest/progress_write.go11
-rw-r--r--object/store/packed/internal/ingest/record_content.go29
-rw-r--r--object/store/packed/internal/ingest/record_delta.go60
-rw-r--r--object/store/packed/internal/ingest/record_inflate.go46
-rw-r--r--object/store/packed/internal/ingest/record_resolve.go116
-rw-r--r--object/store/packed/internal/ingest/records.go46
-rw-r--r--object/store/packed/internal/ingest/resolve_all.go70
-rw-r--r--object/store/packed/internal/ingest/result.go23
-rw-r--r--object/store/packed/internal/ingest/rev_write.go137
-rw-r--r--object/store/packed/internal/ingest/rewrite_header_trailer.go89
-rw-r--r--object/store/packed/internal/ingest/scan.go105
-rw-r--r--object/store/packed/internal/ingest/state.go70
-rw-r--r--object/store/packed/internal/ingest/stream.go111
-rw-r--r--object/store/packed/internal/ingest/temp.go103
-rw-r--r--object/store/packed/internal/ingest/testdata/fixtures/sha1/METADATA.txt3
-rw-r--r--object/store/packed/internal/ingest/testdata/fixtures/sha1/base.packbin81007 -> 0 bytes
-rw-r--r--object/store/packed/internal/ingest/testdata/fixtures/sha1/nonthin.packbin117458 -> 0 bytes
-rw-r--r--object/store/packed/internal/ingest/testdata/fixtures/sha1/thin.packbin38581 -> 0 bytes
-rw-r--r--object/store/packed/internal/ingest/testdata/fixtures/sha256/METADATA.txt3
-rw-r--r--object/store/packed/internal/ingest/testdata/fixtures/sha256/base.packbin105138 -> 0 bytes
-rw-r--r--object/store/packed/internal/ingest/testdata/fixtures/sha256/nonthin.packbin152284 -> 0 bytes
-rw-r--r--object/store/packed/internal/ingest/testdata/fixtures/sha256/thin.packbin49412 -> 0 bytes
-rw-r--r--object/store/packed/internal/ingest/thin_append.go91
-rw-r--r--object/store/packed/internal/ingest/thin_fix.go99
-rw-r--r--object/store/packed/internal/ingest/thin_unresolved.go34
-rw-r--r--object/store/packed/internal/ingest/trailer.go58
-rw-r--r--object/store/packed/internal/ingest/use.go34
-rw-r--r--object/store/packed/internal/ingest/write.go50
-rw-r--r--object/store/packed/internal/ingest/write_empty.go58
-rw-r--r--object/store/packed/internal/reading/TODO3
-rw-r--r--object/store/packed/internal/reading/close.go35
-rw-r--r--object/store/packed/internal/reading/delta_build_chain.go65
-rw-r--r--object/store/packed/internal/reading/delta_cache.go61
-rw-r--r--object/store/packed/internal/reading/delta_chain.go13
-rw-r--r--object/store/packed/internal/reading/delta_node.go9
-rw-r--r--object/store/packed/internal/reading/delta_resolve_chain.go61
-rw-r--r--object/store/packed/internal/reading/delta_resolve_chain_start.go58
-rw-r--r--object/store/packed/internal/reading/delta_resolve_content.go26
-rw-r--r--object/store/packed/internal/reading/delta_size.go27
-rw-r--r--object/store/packed/internal/reading/doc.go6
-rw-r--r--object/store/packed/internal/reading/entry_inflate.go64
-rw-r--r--object/store/packed/internal/reading/entry_meta.go16
-rw-r--r--object/store/packed/internal/reading/entry_parse.go71
-rw-r--r--object/store/packed/internal/reading/helpers_test.go102
-rw-r--r--object/store/packed/internal/reading/idx.go36
-rw-r--r--object/store/packed/internal/reading/idx_candidates_mru.go136
-rw-r--r--object/store/packed/internal/reading/idx_close.go28
-rw-r--r--object/store/packed/internal/reading/idx_lookup.go91
-rw-r--r--object/store/packed/internal/reading/idx_lookup_candidates.go126
-rw-r--r--object/store/packed/internal/reading/idx_open.go98
-rw-r--r--object/store/packed/internal/reading/idx_parse.go78
-rw-r--r--object/store/packed/internal/reading/location.go7
-rw-r--r--object/store/packed/internal/reading/new.go33
-rw-r--r--object/store/packed/internal/reading/options.go16
-rw-r--r--object/store/packed/internal/reading/pack.go82
-rw-r--r--object/store/packed/internal/reading/pack_idx_checksum.go34
-rw-r--r--object/store/packed/internal/reading/read_bytes.go46
-rw-r--r--object/store/packed/internal/reading/read_closer.go19
-rw-r--r--object/store/packed/internal/reading/read_header.go20
-rw-r--r--object/store/packed/internal/reading/read_header_resolve.go65
-rw-r--r--object/store/packed/internal/reading/read_reader.go92
-rw-r--r--object/store/packed/internal/reading/read_size.go45
-rw-r--r--object/store/packed/internal/reading/read_test.go301
-rw-r--r--object/store/packed/internal/reading/store.go52
-rw-r--r--object/store/packed/internal/reading/store_lookup.go106
-rw-r--r--object/store/packed/internal/reading/store_open_pack.go57
-rw-r--r--object/store/packed/internal/reading/trailer_match.go29
-rw-r--r--object/store/packed/new.go25
-rw-r--r--object/store/packed/options.go7
-rw-r--r--object/store/packed/options_refresh.go11
-rw-r--r--object/store/packed/quarantine.go19
-rw-r--r--object/store/packed/quarantine_begin.go63
-rw-r--r--object/store/packed/quarantine_discard.go18
-rw-r--r--object/store/packed/quarantine_promote.go89
-rw-r--r--object/store/packed/quarantine_test.go215
-rw-r--r--object/store/packed/reader.go65
-rw-r--r--object/store/packed/store.go23
-rw-r--r--object/store/packed/writer.go22
-rw-r--r--object/store/quarantine.go20
-rw-r--r--object/store/reader.go55
-rw-r--r--object/store/writer.go8
-rw-r--r--object/store/writer_object.go37
-rw-r--r--object/store/writer_pack.go58
-rw-r--r--object/stored/doc.go7
-rw-r--r--object/stored/id.go8
-rw-r--r--object/stored/new.go11
-rw-r--r--object/stored/object.go6
-rw-r--r--object/stored/stored.go13
-rw-r--r--object/tag/parse.go89
-rw-r--r--object/tag/parse_test.go47
-rw-r--r--object/tag/serialize.go68
-rw-r--r--object/tag/serialize_test.go35
-rw-r--r--object/tag/tag.go24
-rw-r--r--object/tag/type.go10
-rw-r--r--object/tree/entry.go57
-rw-r--r--object/tree/helpers_test.go114
-rw-r--r--object/tree/insert.go24
-rw-r--r--object/tree/lookup.go18
-rw-r--r--object/tree/mode.go12
-rw-r--r--object/tree/mode_details.go10
-rw-r--r--object/tree/mode_has_same_type.go12
-rw-r--r--object/tree/mode_is_blob_like.go8
-rw-r--r--object/tree/mode_is_regular_file.go6
-rw-r--r--object/tree/mode_table.go24
-rw-r--r--object/tree/name.go51
-rw-r--r--object/tree/parse.go58
-rw-r--r--object/tree/parse_test.go109
-rw-r--r--object/tree/path_append.go14
-rw-r--r--object/tree/path_clone.go16
-rw-r--r--object/tree/path_prefix.go19
-rw-r--r--object/tree/path_split.go19
-rw-r--r--object/tree/remove.go22
-rw-r--r--object/tree/serialize.go55
-rw-r--r--object/tree/serialize_test.go73
-rw-r--r--object/tree/tree.go12
-rw-r--r--object/tree/type.go10
-rw-r--r--object/type/details.go10
-rw-r--r--object/type/is_base.go7
-rw-r--r--object/type/name.go11
-rw-r--r--object/type/parse.go8
-rw-r--r--object/type/table.go21
-rw-r--r--object/type/type.go16
312 files changed, 0 insertions, 13837 deletions
diff --git a/object/blob/blob.go b/object/blob/blob.go
deleted file mode 100644
index 93856c51..00000000
--- a/object/blob/blob.go
+++ /dev/null
@@ -1,14 +0,0 @@
-// Package blob provides representations, parsers, and serializers for blob objects.
-package blob
-
-// Blob represents a Git blob object.
-//
-// Blob is fully materialized in memory.
-//
-// Consider using objectstore.Reader.ReadReaderContent,
-// or appropriate streaming write APIs.
-//
-// Labels: MT-Unsafe.
-type Blob struct {
- Data []byte
-}
diff --git a/object/blob/parse.go b/object/blob/parse.go
deleted file mode 100644
index faee9e46..00000000
--- a/object/blob/parse.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package blob
-
-// Parse decodes a blob object body.
-func Parse(body []byte) (*Blob, error) {
- return &Blob{Data: append([]byte(nil), body...)}, nil
-}
diff --git a/object/blob/parse_test.go b/object/blob/parse_test.go
deleted file mode 100644
index 09d5d5d0..00000000
--- a/object/blob/parse_test.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package blob_test
-
-import (
- "bytes"
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- "codeberg.org/lindenii/furgit/object/blob"
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-func TestBlobParseFromGit(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- body := []byte("hello\nblob\n")
- blobID := testRepo.HashObject(t, "blob", body)
-
- rawBody := testRepo.CatFile(t, "blob", blobID)
-
- parsed, err := blob.Parse(rawBody)
- if err != nil {
- t.Fatalf("ParseBlob: %v", err)
- }
-
- if !bytes.Equal(parsed.Data, body) {
- t.Fatalf("blob body mismatch")
- }
- })
-}
diff --git a/object/blob/serialize.go b/object/blob/serialize.go
deleted file mode 100644
index 80cce8dc..00000000
--- a/object/blob/serialize.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package blob
-
-import (
- "errors"
-
- objectheader "codeberg.org/lindenii/furgit/object/header"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// SerializeWithoutHeader renders the raw blob body bytes.
-func (blob *Blob) SerializeWithoutHeader() ([]byte, error) {
- return append([]byte(nil), blob.Data...), nil
-}
-
-// SerializeWithHeader renders the raw object (header + body).
-func (blob *Blob) SerializeWithHeader() ([]byte, error) {
- body, err := blob.SerializeWithoutHeader()
- if err != nil {
- return nil, err
- }
-
- header, ok := objectheader.Encode(objecttype.TypeBlob, int64(len(body)))
- if !ok {
- return nil, errors.New("object: blob: failed to encode object header")
- }
-
- raw := make([]byte, len(header)+len(body))
- copy(raw, header)
- copy(raw[len(header):], body)
-
- return raw, nil
-}
diff --git a/object/blob/serialize_test.go b/object/blob/serialize_test.go
deleted file mode 100644
index 4292abad..00000000
--- a/object/blob/serialize_test.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package blob_test
-
-import (
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- "codeberg.org/lindenii/furgit/object/blob"
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-func TestBlobSerialize(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- body := []byte("hello\nblob\n")
- wantID := testRepo.HashObject(t, "blob", body)
-
- obj := &blob.Blob{Data: body}
-
- rawObj, err := obj.SerializeWithHeader()
- if err != nil {
- t.Fatalf("SerializeWithHeader: %v", err)
- }
-
- gotID := algo.Sum(rawObj)
- if gotID != wantID {
- t.Fatalf("object id mismatch: got %s want %s", gotID, wantID)
- }
- })
-}
diff --git a/object/blob/test.go b/object/blob/test.go
deleted file mode 100644
index 9e538219..00000000
--- a/object/blob/test.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package blob
-
-import objecttype "codeberg.org/lindenii/furgit/object/type"
-
-// ObjectType returns TypeBlob.
-func (blob *Blob) ObjectType() objecttype.Type {
- _ = blob
-
- return objecttype.TypeBlob
-}
diff --git a/object/commit/commit.go b/object/commit/commit.go
deleted file mode 100644
index 0f7649e1..00000000
--- a/object/commit/commit.go
+++ /dev/null
@@ -1,25 +0,0 @@
-// Package commit provides parsed commit objects and commit serialization.
-//
-// It parses commits into ordinary Go values for reading and construction. It
-// does not preserve the exact original byte layout needed for signature
-// verification; callers that need signature-verification payload fidelity
-// should use [codeberg.org/lindenii/furgit/object/signed/commit].
-package commit
-
-import (
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectsignature "codeberg.org/lindenii/furgit/object/signature"
-)
-
-// Commit represents a fully materialized Git commit object.
-//
-// Labels: MT-Unsafe.
-type Commit struct {
- Tree objectid.ObjectID
- Parents []objectid.ObjectID
- Author objectsignature.Signature
- Committer objectsignature.Signature
- Message []byte
- ChangeID string
- ExtraHeaders []ExtraHeader
-}
diff --git a/object/commit/extraheader.go b/object/commit/extraheader.go
deleted file mode 100644
index 79d4f9cc..00000000
--- a/object/commit/extraheader.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package commit
-
-// ExtraHeader represents an extra header in a Git object.
-type ExtraHeader struct {
- Key string
- Value []byte
-}
diff --git a/object/commit/parse.go b/object/commit/parse.go
deleted file mode 100644
index 9dcc930d..00000000
--- a/object/commit/parse.go
+++ /dev/null
@@ -1,94 +0,0 @@
-package commit
-
-import (
- "bytes"
- "errors"
- "fmt"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectsignature "codeberg.org/lindenii/furgit/object/signature"
-)
-
-// Parse decodes a commit object body.
-func Parse(body []byte, algo objectid.Algorithm) (*Commit, error) {
- c := new(Commit)
-
- i := 0
- for i < len(body) {
- rel := bytes.IndexByte(body[i:], '\n')
- if rel < 0 {
- return nil, errors.New("object: commit: missing newline")
- }
-
- line := body[i : i+rel]
- i += rel + 1
-
- if len(line) == 0 {
- break
- }
-
- key, value, found := bytes.Cut(line, []byte{' '})
- if !found {
- return nil, errors.New("object: commit: malformed header")
- }
-
- switch string(key) {
- case "tree":
- id, err := objectid.ParseHex(algo, string(value))
- if err != nil {
- return nil, fmt.Errorf("object: commit: tree: %w", err)
- }
-
- c.Tree = id
- case "parent":
- id, err := objectid.ParseHex(algo, string(value))
- if err != nil {
- return nil, fmt.Errorf("object: commit: parent: %w", err)
- }
-
- c.Parents = append(c.Parents, id)
- case "author":
- idt, err := objectsignature.Parse(value)
- if err != nil {
- return nil, fmt.Errorf("object: commit: author: %w", err)
- }
-
- c.Author = *idt
- case "committer":
- idt, err := objectsignature.Parse(value)
- if err != nil {
- return nil, fmt.Errorf("object: commit: committer: %w", err)
- }
-
- c.Committer = *idt
- case "change-id":
- c.ChangeID = string(value)
- case "gpgsig", "gpgsig-sha256":
- for i < len(body) {
- nextRel := bytes.IndexByte(body[i:], '\n')
- if nextRel < 0 {
- return nil, errors.New("object: commit: unterminated gpgsig")
- }
-
- if body[i] != ' ' {
- break
- }
-
- i += nextRel + 1
- }
- default:
- c.ExtraHeaders = append(c.ExtraHeaders, ExtraHeader{
- Key: string(key),
- Value: append([]byte(nil), value...),
- })
- }
- }
-
- if i > len(body) {
- return nil, errors.New("object: commit: parser position out of bounds")
- }
-
- c.Message = append([]byte(nil), body[i:]...)
-
- return c, nil
-}
diff --git a/object/commit/parse_test.go b/object/commit/parse_test.go
deleted file mode 100644
index ad2c7aed..00000000
--- a/object/commit/parse_test.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package commit_test
-
-import (
- "bytes"
- "fmt"
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- "codeberg.org/lindenii/furgit/object/commit"
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-func TestCommitParseFromGit(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- _, treeID, commitID := testRepo.MakeCommit(t, "subject\n\nbody")
-
- rawBody := testRepo.CatFile(t, "commit", commitID)
-
- parsed, err := commit.Parse(rawBody, algo)
- if err != nil {
- t.Fatalf("ParseCommit: %v", err)
- }
-
- if parsed.Tree != treeID {
- t.Fatalf("tree id mismatch: got %s want %s", parsed.Tree, treeID)
- }
-
- if len(parsed.Parents) != 0 {
- t.Fatalf("parent count = %d, want 0", len(parsed.Parents))
- }
-
- if !bytes.Equal(parsed.Author.Name, []byte("Test Author")) {
- t.Fatalf("author name = %q, want %q", parsed.Author.Name, "Test Author")
- }
-
- if !bytes.Equal(parsed.Committer.Name, []byte("Test Committer")) {
- t.Fatalf("committer name = %q, want %q", parsed.Committer.Name, "Test Committer")
- }
-
- if !bytes.Contains(parsed.Message, []byte("subject")) {
- t.Fatalf("commit message missing subject: %q", parsed.Message)
- }
- })
-}
-
-func TestCommitParseMultipleParents(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
-
- _, treeID := testRepo.MakeSingleFileTree(t, "file.txt", []byte("merge-content\n"))
- parent1 := testRepo.CommitTree(t, treeID, "parent-one")
- parent2 := testRepo.CommitTree(t, treeID, "parent-two", parent1)
-
- rawCommit := fmt.Sprintf(
- "tree %s\nparent %s\nparent %s\nauthor Test Author <test@example.org> 1234567890 +0000\ncommitter Test Committer <committer@example.org> 1234567890 +0000\n\nMerge commit\n",
- treeID,
- parent1,
- parent2,
- )
- mergeID := testRepo.HashObject(t, "commit", []byte(rawCommit))
- rawBody := testRepo.CatFile(t, "commit", mergeID)
-
- parsed, err := commit.Parse(rawBody, algo)
- if err != nil {
- t.Fatalf("ParseCommit(merge): %v", err)
- }
-
- if parsed.Tree != treeID {
- t.Fatalf("merge tree = %s, want %s", parsed.Tree, treeID)
- }
-
- if len(parsed.Parents) != 2 {
- t.Fatalf("merge parent count = %d, want 2", len(parsed.Parents))
- }
-
- if parsed.Parents[0] != parent1 {
- t.Fatalf("merge parent[0] = %s, want %s", parsed.Parents[0], parent1)
- }
-
- if parsed.Parents[1] != parent2 {
- t.Fatalf("merge parent[1] = %s, want %s", parsed.Parents[1], parent2)
- }
-
- if !bytes.Equal(parsed.Message, []byte("Merge commit\n")) {
- t.Fatalf("merge message = %q, want %q", parsed.Message, "Merge commit\n")
- }
- })
-}
diff --git a/object/commit/serialize.go b/object/commit/serialize.go
deleted file mode 100644
index 3f141550..00000000
--- a/object/commit/serialize.go
+++ /dev/null
@@ -1,84 +0,0 @@
-package commit
-
-import (
- "bytes"
- "errors"
- "fmt"
-
- objectheader "codeberg.org/lindenii/furgit/object/header"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// SerializeWithoutHeader renders the raw commit body bytes.
-func (commit *Commit) SerializeWithoutHeader() ([]byte, error) {
- var buf bytes.Buffer
-
- if commit.Tree.Algorithm().Size() == 0 {
- return nil, errors.New("object: commit: missing tree id")
- }
-
- fmt.Fprintf(&buf, "tree %s\n", commit.Tree.String())
-
- for _, parent := range commit.Parents {
- fmt.Fprintf(&buf, "parent %s\n", parent.String())
- }
-
- authorBytes, err := commit.Author.Serialize()
- if err != nil {
- return nil, err
- }
-
- buf.WriteString("author ")
- buf.Write(authorBytes)
- buf.WriteByte('\n')
-
- committerBytes, err := commit.Committer.Serialize()
- if err != nil {
- return nil, err
- }
-
- buf.WriteString("committer ")
- buf.Write(committerBytes)
- buf.WriteByte('\n')
-
- if commit.ChangeID != "" {
- buf.WriteString("change-id ")
- buf.WriteString(commit.ChangeID)
- buf.WriteByte('\n')
- }
-
- for _, h := range commit.ExtraHeaders {
- if h.Key == "" {
- return nil, errors.New("object: commit: extra header has empty key")
- }
-
- buf.WriteString(h.Key)
- buf.WriteByte(' ')
- buf.Write(h.Value)
- buf.WriteByte('\n')
- }
-
- buf.WriteByte('\n')
- buf.Write(commit.Message)
-
- return buf.Bytes(), nil
-}
-
-// SerializeWithHeader renders the raw object (header + body).
-func (commit *Commit) SerializeWithHeader() ([]byte, error) {
- body, err := commit.SerializeWithoutHeader()
- if err != nil {
- return nil, err
- }
-
- header, ok := objectheader.Encode(objecttype.TypeCommit, int64(len(body)))
- if !ok {
- return nil, errors.New("object: commit: failed to encode object header")
- }
-
- raw := make([]byte, len(header)+len(body))
- copy(raw, header)
- copy(raw[len(header):], body)
-
- return raw, nil
-}
diff --git a/object/commit/serialize_test.go b/object/commit/serialize_test.go
deleted file mode 100644
index e58a8078..00000000
--- a/object/commit/serialize_test.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package commit_test
-
-import (
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- "codeberg.org/lindenii/furgit/object/commit"
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-func TestCommitSerialize(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- _, _, commitID := testRepo.MakeCommit(t, "subject\n\nbody")
-
- rawBody := testRepo.CatFile(t, "commit", commitID)
-
- parsed, err := commit.Parse(rawBody, algo)
- if err != nil {
- t.Fatalf("ParseCommit: %v", err)
- }
-
- rawObj, err := parsed.SerializeWithHeader()
- if err != nil {
- t.Fatalf("SerializeWithHeader: %v", err)
- }
-
- gotID := algo.Sum(rawObj)
- if gotID != commitID {
- t.Fatalf("commit id mismatch: got %s want %s", gotID, commitID)
- }
- })
-}
diff --git a/object/commit/type.go b/object/commit/type.go
deleted file mode 100644
index b8aa11e8..00000000
--- a/object/commit/type.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package commit
-
-import objecttype "codeberg.org/lindenii/furgit/object/type"
-
-// ObjectType returns TypeCommit.
-func (commit *Commit) ObjectType() objecttype.Type {
- _ = commit
-
- return objecttype.TypeCommit
-}
diff --git a/object/doc.go b/object/doc.go
deleted file mode 100644
index f675b963..00000000
--- a/object/doc.go
+++ /dev/null
@@ -1,7 +0,0 @@
-// Package object provides the shared [Object] interface and parsing functions
-// for Git object values.
-//
-// Concrete object forms such as [blob], [tree], [commit], and [tag] live in
-// subpackages. Use [codeberg.org/lindenii/furgit/object/stored] when object
-// values need to be paired with the object IDs they were loaded under.
-package object
diff --git a/object/fetch/doc.go b/object/fetch/doc.go
deleted file mode 100644
index 89bf9a98..00000000
--- a/object/fetch/doc.go
+++ /dev/null
@@ -1,8 +0,0 @@
-// Package fetch loads typed Git objects from object storage and provides
-// higher-level object queries.
-//
-// Fetching is above [objectstore]: it parses stored objects into blobs, trees,
-// commits, and tags, exposes object metadata, peels tree-ish or commit-ish
-// objects, resolves paths within trees, and can expose one tree as an [io/fs]
-// view.
-package fetch
diff --git a/object/fetch/exact_blob.go b/object/fetch/exact_blob.go
deleted file mode 100644
index ef4b84fe..00000000
--- a/object/fetch/exact_blob.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package fetch
-
-import (
- giterrors "codeberg.org/lindenii/furgit/errors"
- "codeberg.org/lindenii/furgit/object/blob"
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/stored"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ExactBlob reads, parses, and wraps the blob at id.
-//
-// Labels: Life-Parent.
-func (r *Fetcher) ExactBlob(id objectid.ObjectID) (*stored.Stored[*blob.Blob], error) {
- parsed, err := r.parseObject(id)
- if err != nil {
- return nil, err
- }
-
- blob, ok := parsed.(*blob.Blob)
- if !ok {
- return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeBlob}
- }
-
- return stored.New(id, blob), nil
-}
diff --git a/object/fetch/exact_blob_reader.go b/object/fetch/exact_blob_reader.go
deleted file mode 100644
index 4a313d3e..00000000
--- a/object/fetch/exact_blob_reader.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package fetch
-
-import (
- "io"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ExactBlobReader returns a reader for the content of the blob at id,
-// together with its content size in bytes.
-//
-// Labels: Life-Parent, Close-Caller.
-func (r *Fetcher) ExactBlobReader(id objectid.ObjectID) (io.ReadCloser, int64, error) {
- return r.exactReader(id, objecttype.TypeBlob)
-}
diff --git a/object/fetch/exact_commit.go b/object/fetch/exact_commit.go
deleted file mode 100644
index 9483b2b1..00000000
--- a/object/fetch/exact_commit.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package fetch
-
-import (
- giterrors "codeberg.org/lindenii/furgit/errors"
- "codeberg.org/lindenii/furgit/object/commit"
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/stored"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ExactCommit reads, parses, and wraps the commit at id.
-//
-// Labels: Life-Parent.
-func (r *Fetcher) ExactCommit(id objectid.ObjectID) (*stored.Stored[*commit.Commit], error) {
- parsed, err := r.parseObject(id)
- if err != nil {
- return nil, err
- }
-
- commit, ok := parsed.(*commit.Commit)
- if !ok {
- return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeCommit}
- }
-
- return stored.New(id, commit), nil
-}
diff --git a/object/fetch/exact_object.go b/object/fetch/exact_object.go
deleted file mode 100644
index 2e4a8217..00000000
--- a/object/fetch/exact_object.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package fetch
-
-import (
- "codeberg.org/lindenii/furgit/object"
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/stored"
-)
-
-// ExactObject reads, parses, and wraps the object at id without constraining
-// its concrete object kind.
-//
-// Labels: Life-Parent.
-func (r *Fetcher) ExactObject(id objectid.ObjectID) (*stored.Stored[object.Object], error) {
- parsed, err := r.parseObject(id)
- if err != nil {
- return nil, err
- }
-
- return stored.New(id, parsed), nil
-}
diff --git a/object/fetch/exact_reader.go b/object/fetch/exact_reader.go
deleted file mode 100644
index d588480d..00000000
--- a/object/fetch/exact_reader.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package fetch
-
-import (
- "io"
-
- giterrors "codeberg.org/lindenii/furgit/errors"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// exactReader reads one object's content stream and verifies that its header
-// type matches wantType.
-func (r *Fetcher) exactReader(id objectid.ObjectID, wantType objecttype.Type) (io.ReadCloser, int64, error) {
- gotType, size, rc, err := r.store.ReadReaderContent(id)
- if err != nil {
- return nil, 0, wrapObjectReadError(id, err)
- }
-
- if gotType != wantType {
- _ = rc.Close()
-
- return nil, 0, &giterrors.ObjectTypeError{OID: id, Got: gotType, Want: wantType}
- }
-
- return rc, size, nil
-}
diff --git a/object/fetch/exact_tag.go b/object/fetch/exact_tag.go
deleted file mode 100644
index 230e7d57..00000000
--- a/object/fetch/exact_tag.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package fetch
-
-import (
- giterrors "codeberg.org/lindenii/furgit/errors"
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/stored"
- "codeberg.org/lindenii/furgit/object/tag"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ExactTag reads, parses, and wraps the tag at id.
-//
-// Labels: Life-Parent.
-func (r *Fetcher) ExactTag(id objectid.ObjectID) (*stored.Stored[*tag.Tag], error) {
- parsed, err := r.parseObject(id)
- if err != nil {
- return nil, err
- }
-
- tag, ok := parsed.(*tag.Tag)
- if !ok {
- return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeTag}
- }
-
- return stored.New(id, tag), nil
-}
diff --git a/object/fetch/exact_tree.go b/object/fetch/exact_tree.go
deleted file mode 100644
index 8bfc87ea..00000000
--- a/object/fetch/exact_tree.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package fetch
-
-import (
- giterrors "codeberg.org/lindenii/furgit/errors"
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/stored"
- "codeberg.org/lindenii/furgit/object/tree"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ExactTree reads, parses, and wraps the tree at id.
-//
-// Labels: Life-Parent.
-func (r *Fetcher) ExactTree(id objectid.ObjectID) (*stored.Stored[*tree.Tree], error) {
- parsed, err := r.parseObject(id)
- if err != nil {
- return nil, err
- }
-
- tree, ok := parsed.(*tree.Tree)
- if !ok {
- return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeTree}
- }
-
- return stored.New(id, tree), nil
-}
diff --git a/object/fetch/fetcher.go b/object/fetch/fetcher.go
deleted file mode 100644
index fcd64d88..00000000
--- a/object/fetch/fetcher.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package fetch
-
-import objectstore "codeberg.org/lindenii/furgit/object/store"
-
-// Fetcher provides ordinary object access above an object store.
-//
-// It exposes object metadata, typed object loading, tree-ish and commit-ish
-// peeling, path resolution, one-tree fs views, and blob content streaming.
-//
-// Labels: MT-Safe.
-type Fetcher struct {
- store objectstore.Reader
-}
-
-// New returns a Fetcher that reads objects from store.
-//
-// Labels: Deps-Borrowed, Life-Parent.
-func New(store objectstore.Reader) *Fetcher {
- return &Fetcher{store: store}
-}
diff --git a/object/fetch/header.go b/object/fetch/header.go
deleted file mode 100644
index 0a535dd9..00000000
--- a/object/fetch/header.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package fetch
-
-import (
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// Header returns the object type and content size at id.
-//
-// Labels: Life-Parent.
-func (r *Fetcher) Header(id objectid.ObjectID) (objecttype.Type, int64, error) {
- ty, size, err := r.store.ReadHeader(id)
- if err != nil {
- return objecttype.TypeInvalid, 0, wrapObjectReadError(id, err)
- }
-
- return ty, size, nil
-}
diff --git a/object/fetch/object_errors.go b/object/fetch/object_errors.go
deleted file mode 100644
index 08de6f75..00000000
--- a/object/fetch/object_errors.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package fetch
-
-import (
- stderrors "errors"
-
- giterrors "codeberg.org/lindenii/furgit/errors"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
-)
-
-// wrapObjectReadError maps raw object-store lookup failures to fetcher-level
-// object lookup errors.
-func wrapObjectReadError(id objectid.ObjectID, err error) error {
- if stderrors.Is(err, objectstore.ErrObjectNotFound) {
- return &giterrors.ObjectMissingError{OID: id}
- }
-
- return err
-}
diff --git a/object/fetch/object_parse.go b/object/fetch/object_parse.go
deleted file mode 100644
index 0a61bb3d..00000000
--- a/object/fetch/object_parse.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package fetch
-
-import (
- "fmt"
-
- "codeberg.org/lindenii/furgit/object"
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-func (r *Fetcher) parseObject(id objectid.ObjectID) (object.Object, error) {
- ty, content, err := r.store.ReadBytesContent(id)
- if err != nil {
- return nil, wrapObjectReadError(id, err)
- }
-
- parsed, err := object.ParseWithoutHeader(ty, content, id.Algorithm())
- if err != nil {
- tyName, ok := ty.Name()
- if !ok {
- tyName = fmt.Sprintf("type %d", ty)
- }
-
- return nil, fmt.Errorf("object/fetch: parse object %s (%s): %w", id, tyName, err)
- }
-
- return parsed, nil
-}
diff --git a/object/fetch/path.go b/object/fetch/path.go
deleted file mode 100644
index e3c468db..00000000
--- a/object/fetch/path.go
+++ /dev/null
@@ -1,105 +0,0 @@
-package fetch
-
-import (
- "fmt"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/tree"
-)
-
-// PathEmptyError indicates that Path received no segments.
-type PathEmptyError struct{}
-
-func (err *PathEmptyError) Error() string {
- return "object/fetch: empty tree path"
-}
-
-// PathSegmentEmptyError indicates that one path segment is empty.
-type PathSegmentEmptyError struct {
- Index int
-}
-
-func (err *PathSegmentEmptyError) Error() string {
- return fmt.Sprintf("object/fetch: empty tree path segment at index %d", err.Index)
-}
-
-// PathNotFoundError indicates that one tree path segment was not found.
-type PathNotFoundError struct {
- Index int
- Name []byte
-}
-
-func (err *PathNotFoundError) Error() string {
- return fmt.Sprintf("object/fetch: tree entry %q not found at index %d", err.Name, err.Index)
-}
-
-// PathNotTreeError indicates that one intermediate path segment was not a tree.
-type PathNotTreeError struct {
- Index int
- Name []byte
-}
-
-func (err *PathNotTreeError) Error() string {
- return fmt.Sprintf("object/fetch: path segment %q at index %d is not a tree", err.Name, err.Index)
-}
-
-// Path resolves parts within the tree identified by root and returns the final
-// tree entry.
-//
-// The root object may be any tree-ish object accepted by PeelToTree.
-//
-// parts must contain at least one path segment. Intermediate path segments
-// must resolve to tree entries. The final entry is returned without loading
-// its object. Path segments may not contain \x00.
-//
-// The path cannot be accurately represented as a string or a single []byte
-// because Git tree entry names may include slashes. While []string is
-// technically possible (since Go strings are not necessarily UTF-8), they
-// do often imply UTF-8 in practice, which would be undesirable.
-//
-// If your entry names are valid UTF-8 and uses / solely as segment separators,
-// it may be convenient to use TreeFS for an io/fs.FS-like interface.
-//
-// Labels: Life-Parent.
-func (r *Fetcher) Path(root objectid.ObjectID, parts [][]byte) (tree.TreeEntry, error) {
- if len(parts) == 0 {
- return tree.TreeEntry{}, &PathEmptyError{}
- }
-
- current, err := r.PeelToTree(root)
- if err != nil {
- return tree.TreeEntry{}, err
- }
-
- for i, part := range parts {
- if len(part) == 0 {
- return tree.TreeEntry{}, &PathSegmentEmptyError{Index: i}
- }
-
- entry := current.Object().Entry(part)
- if entry == nil {
- return tree.TreeEntry{}, &PathNotFoundError{
- Index: i,
- Name: append([]byte(nil), part...),
- }
- }
-
- if i == len(parts)-1 {
- return *entry, nil
- }
-
- if entry.Mode != tree.FileModeDir {
- return tree.TreeEntry{}, &PathNotTreeError{
- Index: i,
- Name: append([]byte(nil), part...),
- }
- }
-
- current, err = r.ExactTree(entry.ID)
- if err != nil {
- return tree.TreeEntry{}, err
- }
- }
-
- return tree.TreeEntry{}, &PathNotFoundError{Index: len(parts) - 1}
-}
diff --git a/object/fetch/peel_to_blob.go b/object/fetch/peel_to_blob.go
deleted file mode 100644
index adf86495..00000000
--- a/object/fetch/peel_to_blob.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package fetch
-
-import (
- giterrors "codeberg.org/lindenii/furgit/errors"
- "codeberg.org/lindenii/furgit/object/blob"
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/stored"
- "codeberg.org/lindenii/furgit/object/tag"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// PeelToBlob peels tags until it reaches a blob.
-//
-// Labels: Life-Parent.
-func (r *Fetcher) PeelToBlob(id objectid.ObjectID) (*stored.Stored[*blob.Blob], error) {
- for {
- obj, err := r.ExactObject(id)
- if err != nil {
- return nil, err
- }
-
- switch parsed := obj.Object().(type) {
- case *blob.Blob:
- return stored.New(id, parsed), nil
- case *tag.Tag:
- id = parsed.Target
- default:
- return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeBlob}
- }
- }
-}
diff --git a/object/fetch/peel_to_blob_id.go b/object/fetch/peel_to_blob_id.go
deleted file mode 100644
index 7a43b4cc..00000000
--- a/object/fetch/peel_to_blob_id.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package fetch
-
-import (
- giterrors "codeberg.org/lindenii/furgit/errors"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// PeelToBlobID peels tags until it reaches a blob object ID.
-func (r *Fetcher) PeelToBlobID(id objectid.ObjectID) (objectid.ObjectID, error) {
- for {
- ty, _, err := r.Header(id)
- if err != nil {
- return objectid.ObjectID{}, err
- }
-
- switch ty {
- case objecttype.TypeBlob:
- return id, nil
- case objecttype.TypeTag:
- tag, err := r.ExactTag(id)
- if err != nil {
- return objectid.ObjectID{}, err
- }
-
- id = tag.Object().Target
- case objecttype.TypeInvalid,
- objecttype.TypeCommit,
- objecttype.TypeTree,
- objecttype.TypeFuture,
- objecttype.TypeOfsDelta,
- objecttype.TypeRefDelta:
- return objectid.ObjectID{}, &giterrors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeBlob}
- default:
- return objectid.ObjectID{}, &giterrors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeBlob}
- }
- }
-}
diff --git a/object/fetch/peel_to_blob_reader.go b/object/fetch/peel_to_blob_reader.go
deleted file mode 100644
index dedffd01..00000000
--- a/object/fetch/peel_to_blob_reader.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package fetch
-
-import (
- "io"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-// PeelToBlobReader returns a reader for the content of the peeled blob at id,
-// together with its content size in bytes.
-//
-// Labels: Life-Parent, Close-Caller.
-func (r *Fetcher) PeelToBlobReader(id objectid.ObjectID) (io.ReadCloser, int64, error) {
- blobID, err := r.PeelToBlobID(id)
- if err != nil {
- return nil, 0, err
- }
-
- return r.ExactBlobReader(blobID)
-}
diff --git a/object/fetch/peel_to_commit.go b/object/fetch/peel_to_commit.go
deleted file mode 100644
index e5fdce2b..00000000
--- a/object/fetch/peel_to_commit.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package fetch
-
-import (
- giterrors "codeberg.org/lindenii/furgit/errors"
- "codeberg.org/lindenii/furgit/object/commit"
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/stored"
- "codeberg.org/lindenii/furgit/object/tag"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// PeelToCommit peels tags until it reaches a commit.
-//
-// Labels: Life-Parent.
-func (r *Fetcher) PeelToCommit(id objectid.ObjectID) (*stored.Stored[*commit.Commit], error) {
- for {
- obj, err := r.ExactObject(id)
- if err != nil {
- return nil, err
- }
-
- switch parsed := obj.Object().(type) {
- case *commit.Commit:
- return stored.New(id, parsed), nil
- case *tag.Tag:
- id = parsed.Target
- default:
- return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeCommit}
- }
- }
-}
diff --git a/object/fetch/peel_to_commit_id.go b/object/fetch/peel_to_commit_id.go
deleted file mode 100644
index 7b58bdea..00000000
--- a/object/fetch/peel_to_commit_id.go
+++ /dev/null
@@ -1,38 +0,0 @@
-package fetch
-
-import (
- giterrors "codeberg.org/lindenii/furgit/errors"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// PeelToCommitID peels tags until it reaches a commit object ID.
-func (r *Fetcher) PeelToCommitID(id objectid.ObjectID) (objectid.ObjectID, error) {
- for {
- ty, _, err := r.Header(id)
- if err != nil {
- return objectid.ObjectID{}, err
- }
-
- switch ty {
- case objecttype.TypeCommit:
- return id, nil
- case objecttype.TypeTag:
- tag, err := r.ExactTag(id)
- if err != nil {
- return objectid.ObjectID{}, err
- }
-
- id = tag.Object().Target
- case objecttype.TypeInvalid,
- objecttype.TypeTree,
- objecttype.TypeBlob,
- objecttype.TypeFuture,
- objecttype.TypeOfsDelta,
- objecttype.TypeRefDelta:
- return objectid.ObjectID{}, &giterrors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeCommit}
- default:
- return objectid.ObjectID{}, &giterrors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeCommit}
- }
- }
-}
diff --git a/object/fetch/peel_to_tree.go b/object/fetch/peel_to_tree.go
deleted file mode 100644
index adc87e6b..00000000
--- a/object/fetch/peel_to_tree.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package fetch
-
-import (
- giterrors "codeberg.org/lindenii/furgit/errors"
- "codeberg.org/lindenii/furgit/object/commit"
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/stored"
- "codeberg.org/lindenii/furgit/object/tag"
- "codeberg.org/lindenii/furgit/object/tree"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// PeelToTree peels tags until it reaches a tree or commit. If it reaches a
-// commit, it returns the commit's root tree.
-//
-// Labels: Life-Parent.
-func (r *Fetcher) PeelToTree(id objectid.ObjectID) (*stored.Stored[*tree.Tree], error) {
- for {
- obj, err := r.ExactObject(id)
- if err != nil {
- return nil, err
- }
-
- switch parsed := obj.Object().(type) {
- case *tree.Tree:
- return stored.New(id, parsed), nil
- case *commit.Commit:
- return r.ExactTree(parsed.Tree)
- case *tag.Tag:
- id = parsed.Target
- default:
- return nil, &giterrors.ObjectTypeError{OID: id, Got: parsed.ObjectType(), Want: objecttype.TypeTree}
- }
- }
-}
diff --git a/object/fetch/peel_to_tree_id.go b/object/fetch/peel_to_tree_id.go
deleted file mode 100644
index 4c9bdac9..00000000
--- a/object/fetch/peel_to_tree_id.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package fetch
-
-import (
- giterrors "codeberg.org/lindenii/furgit/errors"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// PeelToTreeID peels tags until it reaches a tree object ID, or a commit whose
-// root tree object ID is then returned.
-func (r *Fetcher) PeelToTreeID(id objectid.ObjectID) (objectid.ObjectID, error) {
- for {
- ty, _, err := r.Header(id)
- if err != nil {
- return objectid.ObjectID{}, err
- }
-
- switch ty {
- case objecttype.TypeTree:
- return id, nil
- case objecttype.TypeCommit:
- commit, err := r.ExactCommit(id)
- if err != nil {
- return objectid.ObjectID{}, err
- }
-
- return commit.Object().Tree, nil
- case objecttype.TypeTag:
- tag, err := r.ExactTag(id)
- if err != nil {
- return objectid.ObjectID{}, err
- }
-
- id = tag.Object().Target
- case objecttype.TypeInvalid,
- objecttype.TypeBlob,
- objecttype.TypeFuture,
- objecttype.TypeOfsDelta,
- objecttype.TypeRefDelta:
- return objectid.ObjectID{}, &giterrors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeTree}
- default:
- return objectid.ObjectID{}, &giterrors.ObjectTypeError{OID: id, Got: ty, Want: objecttype.TypeTree}
- }
- }
-}
diff --git a/object/fetch/size.go b/object/fetch/size.go
deleted file mode 100644
index da59e12a..00000000
--- a/object/fetch/size.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package fetch
-
-import objectid "codeberg.org/lindenii/furgit/object/id"
-
-// Size returns the object content size at id.
-//
-// Labels: Life-Parent.
-func (r *Fetcher) Size(id objectid.ObjectID) (int64, error) {
- size, err := r.store.ReadSize(id)
- if err != nil {
- return 0, wrapObjectReadError(id, err)
- }
-
- return size, nil
-}
diff --git a/object/fetch/treefs.go b/object/fetch/treefs.go
deleted file mode 100644
index 39ea7ad5..00000000
--- a/object/fetch/treefs.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package fetch
-
-import (
- "io/fs"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/tree"
-)
-
-// TreeFS exposes one Git tree as an fs.FS view backed by a Fetcher.
-//
-// TreeFS interprets names using io/fs path rules. Those rules do not match raw
-// Git tree entry naming exactly: names are UTF-8, slash-separated, and must be
-// valid fs.FS paths. Tree entries that cannot be represented under those rules
-// are not addressable through this API.
-//
-// Labels: MT-Safe.
-type TreeFS struct {
- fetcher *Fetcher
- rootTree objectid.ObjectID
- rootEntry *tree.TreeEntry
-}
-
-var (
- _ fs.FS = (*TreeFS)(nil)
- _ fs.ReadFileFS = (*TreeFS)(nil)
- _ fs.ReadDirFS = (*TreeFS)(nil)
- _ fs.StatFS = (*TreeFS)(nil)
- _ fs.SubFS = (*TreeFS)(nil)
-)
diff --git a/object/fetch/treefs_entry.go b/object/fetch/treefs_entry.go
deleted file mode 100644
index e577d86c..00000000
--- a/object/fetch/treefs_entry.go
+++ /dev/null
@@ -1,85 +0,0 @@
-package fetch
-
-import (
- "errors"
- "fmt"
- "io/fs"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/tree"
-)
-
-func (treeFS *TreeFS) resolvePath(op treeFSOp, name string) (treeEntryValue, error) {
- if !treeFSValidPath(name) {
- return treeEntryValue{}, treeFSPathError(op, name, fs.ErrInvalid)
- }
-
- if name == "." {
- return treeEntryValue{
- name: ".",
- mode: tree.FileModeDir,
- treeID: treeFS.rootTree,
- treeEntry: treeFS.rootEntry,
- }, nil
- }
-
- entry, err := treeFS.fetcher.Path(treeFS.rootTree, tree.SplitPath([]byte(name)))
- if err != nil {
- return treeEntryValue{}, treeFS.pathResolveError(op, name, err)
- }
-
- return treeEntryValue{
- name: string(entry.Name),
- mode: entry.Mode,
- objectID: entry.ID,
- treeEntry: &entry,
- }, nil
-}
-
-func (treeFS *TreeFS) pathResolveError(op treeFSOp, name string, err error) error {
- if _, ok := errors.AsType[*PathNotFoundError](err); ok {
- return treeFSPathError(op, name, fs.ErrNotExist)
- }
-
- if _, ok := errors.AsType[*PathNotTreeError](err); ok {
- return treeFSPathError(op, name, fs.ErrInvalid)
- }
-
- if _, ok := errors.AsType[*PathEmptyError](err); ok {
- return treeFSPathError(op, name, fs.ErrInvalid)
- }
-
- if _, ok := errors.AsType[*PathSegmentEmptyError](err); ok {
- return treeFSPathError(op, name, fs.ErrInvalid)
- }
-
- return treeFSPathError(op, name, err)
-}
-
-type treeEntryValue struct {
- name string
- mode tree.FileMode
- objectID objectid.ObjectID
- treeID objectid.ObjectID
- treeEntry *tree.TreeEntry
-}
-
-func (entry treeEntryValue) isDir() bool {
- return entry.mode == tree.FileModeDir
-}
-
-func (entry treeEntryValue) blobSize(fetcher *Fetcher) (int64, error) {
- return fetcher.Size(entry.objectID)
-}
-
-func (entry treeEntryValue) subtreeID() (objectid.ObjectID, error) {
- if entry.name == "." {
- return entry.treeID, nil
- }
-
- if entry.mode != tree.FileModeDir {
- return objectid.ObjectID{}, fmt.Errorf("object/fetch: path %q is not a tree", entry.name)
- }
-
- return entry.objectID, nil
-}
diff --git a/object/fetch/treefs_info.go b/object/fetch/treefs_info.go
deleted file mode 100644
index f1db7e9a..00000000
--- a/object/fetch/treefs_info.go
+++ /dev/null
@@ -1,75 +0,0 @@
-package fetch
-
-import (
- "io/fs"
- "time"
-
- "codeberg.org/lindenii/furgit/object/tree"
-)
-
-type treeFSInfo struct {
- name string
- mode fs.FileMode
- size int64
- sys any
- isDir bool
-}
-
-var (
- _ fs.FileInfo = (*treeFSInfo)(nil)
- _ fs.DirEntry = (*treeFSInfo)(nil)
-)
-
-func (info *treeFSInfo) Name() string { return info.name }
-func (info *treeFSInfo) Size() int64 { return info.size }
-func (info *treeFSInfo) Mode() fs.FileMode { return info.mode }
-func (info *treeFSInfo) Type() fs.FileMode { return info.mode.Type() }
-func (info *treeFSInfo) IsDir() bool { return info.isDir }
-func (info *treeFSInfo) ModTime() time.Time { return time.Time{} }
-func (info *treeFSInfo) Sys() any { return info.sys }
-func (info *treeFSInfo) Info() (fs.FileInfo, error) {
- return info, nil
-}
-
-func treeFSEntryMode(mode tree.FileMode) fs.FileMode {
- switch mode {
- case tree.FileModeDir:
- return fs.ModeDir | 0o555
- case tree.FileModeRegular:
- return 0o444
- case tree.FileModeExecutable:
- return 0o555
- case tree.FileModeSymlink:
- return fs.ModeSymlink | 0o444
- case tree.FileModeGitlink:
- return fs.ModeIrregular
- default:
- return fs.ModeIrregular
- }
-}
-
-func (treeFS *TreeFS) statEntry(entry treeEntryValue) (*treeFSInfo, error) {
- size := int64(0)
-
- if entry.mode == tree.FileModeRegular || entry.mode == tree.FileModeExecutable || entry.mode == tree.FileModeSymlink {
- var err error
-
- size, err = entry.blobSize(treeFS.fetcher)
- if err != nil {
- return nil, err
- }
- }
-
- var sys any
- if entry.treeEntry != nil {
- sys = *entry.treeEntry
- }
-
- return &treeFSInfo{
- name: entry.name,
- mode: treeFSEntryMode(entry.mode),
- size: size,
- sys: sys,
- isDir: entry.isDir(),
- }, nil
-}
diff --git a/object/fetch/treefs_new.go b/object/fetch/treefs_new.go
deleted file mode 100644
index f1096a3c..00000000
--- a/object/fetch/treefs_new.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package fetch
-
-import objectid "codeberg.org/lindenii/furgit/object/id"
-
-// TreeFS returns a new filesystem view rooted at root, which may be any
-// tree-ish object accepted by PeelToTreeID.
-//
-// Labels: Deps-Borrowed, Life-Parent.
-func (r *Fetcher) TreeFS(root objectid.ObjectID) (*TreeFS, error) {
- rootTree, err := r.PeelToTreeID(root)
- if err != nil {
- return nil, err
- }
-
- return &TreeFS{
- fetcher: r,
- rootTree: rootTree,
- }, nil
-}
diff --git a/object/fetch/treefs_op.go b/object/fetch/treefs_op.go
deleted file mode 100644
index f0472923..00000000
--- a/object/fetch/treefs_op.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package fetch
-
-type treeFSOp uint8
-
-const (
- treeFSOpOpen treeFSOp = iota
- treeFSOpReadFile
- treeFSOpReadDir
- treeFSOpStat
- treeFSOpSub
-)
-
-func (op treeFSOp) pathErrorOp() string {
- switch op {
- case treeFSOpOpen:
- return "open"
- case treeFSOpReadFile:
- return "readfile"
- case treeFSOpReadDir:
- return "readdir"
- case treeFSOpStat:
- return "stat"
- case treeFSOpSub:
- return "sub"
- default:
- return "treefs"
- }
-}
diff --git a/object/fetch/treefs_open.go b/object/fetch/treefs_open.go
deleted file mode 100644
index fc0f7635..00000000
--- a/object/fetch/treefs_open.go
+++ /dev/null
@@ -1,122 +0,0 @@
-package fetch
-
-import (
- "fmt"
- "io"
- "io/fs"
-
- "codeberg.org/lindenii/furgit/object/tree"
-)
-
-// Open opens name for reading.
-//
-// Directories are returned as fs.ReadDirFile values. Gitlink entries are not
-// readable through TreeFS.
-func (treeFS *TreeFS) Open(name string) (fs.File, error) {
- entry, err := treeFS.resolvePath(treeFSOpOpen, name)
- if err != nil {
- return nil, err
- }
-
- info, err := treeFS.statEntry(entry)
- if err != nil {
- return nil, treeFSPathError(treeFSOpOpen, name, err)
- }
-
- if entry.isDir() {
- treeID, err := entry.subtreeID()
- if err != nil {
- return nil, treeFSPathError(treeFSOpOpen, name, err)
- }
-
- tree, err := treeFS.fetcher.ExactTree(treeID)
- if err != nil {
- return nil, treeFSPathError(treeFSOpOpen, name, err)
- }
-
- entries := make([]fs.DirEntry, 0, len(tree.Object().Entries))
- for _, child := range tree.Object().Entries {
- childEntry := treeEntryValue{
- name: string(child.Name),
- mode: child.Mode,
- objectID: child.ID,
- treeEntry: &child,
- }
-
- childInfo, err := treeFS.statEntry(childEntry)
- if err != nil {
- return nil, treeFSPathError(treeFSOpOpen, name, err)
- }
-
- entries = append(entries, childInfo)
- }
-
- return &treeFSDir{
- info: info,
- entries: entries,
- }, nil
- }
-
- if entry.mode == tree.FileModeGitlink {
- return nil, treeFSPathError(treeFSOpOpen, name, fmt.Errorf("object/fetch: gitlink entries are not readable as files"))
- }
-
- reader, _, err := treeFS.fetcher.ExactBlobReader(entry.objectID)
- if err != nil {
- return nil, treeFSPathError(treeFSOpOpen, name, err)
- }
-
- return &treeFSBlob{
- info: info,
- reader: reader,
- }, nil
-}
-
-type treeFSBlob struct {
- info *treeFSInfo
- reader io.ReadCloser
-}
-
-var _ fs.File = (*treeFSBlob)(nil)
-
-func (file *treeFSBlob) Stat() (fs.FileInfo, error) { return file.info, nil }
-func (file *treeFSBlob) Read(p []byte) (int, error) { return file.reader.Read(p) }
-func (file *treeFSBlob) Close() error { return file.reader.Close() }
-
-type treeFSDir struct {
- info *treeFSInfo
- entries []fs.DirEntry
- offset int
-}
-
-var (
- _ fs.File = (*treeFSDir)(nil)
- _ fs.ReadDirFile = (*treeFSDir)(nil)
-)
-
-func (dir *treeFSDir) Stat() (fs.FileInfo, error) { return dir.info, nil }
-func (dir *treeFSDir) Close() error { return nil }
-
-func (dir *treeFSDir) Read(_ []byte) (int, error) {
- return 0, fs.ErrInvalid
-}
-
-func (dir *treeFSDir) ReadDir(n int) ([]fs.DirEntry, error) {
- if dir.offset >= len(dir.entries) && n > 0 {
- return nil, io.EOF
- }
-
- if n <= 0 {
- out := append([]fs.DirEntry(nil), dir.entries[dir.offset:]...)
- dir.offset = len(dir.entries)
-
- return out, nil
- }
-
- end := min(dir.offset+n, len(dir.entries))
-
- out := append([]fs.DirEntry(nil), dir.entries[dir.offset:end]...)
- dir.offset = end
-
- return out, nil
-}
diff --git a/object/fetch/treefs_path.go b/object/fetch/treefs_path.go
deleted file mode 100644
index a2dc3155..00000000
--- a/object/fetch/treefs_path.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package fetch
-
-import "io/fs"
-
-func treeFSValidPath(name string) bool {
- return name == "." || fs.ValidPath(name)
-}
-
-func treeFSPathError(op treeFSOp, path string, err error) error {
- return &fs.PathError{Op: op.pathErrorOp(), Path: path, Err: err}
-}
diff --git a/object/fetch/treefs_readdir.go b/object/fetch/treefs_readdir.go
deleted file mode 100644
index 7518c607..00000000
--- a/object/fetch/treefs_readdir.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package fetch
-
-import "io/fs"
-
-// ReadDir reads and returns all directory entries for name.
-func (treeFS *TreeFS) ReadDir(name string) ([]fs.DirEntry, error) {
- file, err := treeFS.Open(name)
- if err != nil {
- return nil, err
- }
-
- defer func() { _ = file.Close() }()
-
- readDirFile, ok := file.(fs.ReadDirFile)
- if !ok {
- return nil, treeFSPathError(treeFSOpReadDir, name, fs.ErrInvalid)
- }
-
- return readDirFile.ReadDir(-1)
-}
diff --git a/object/fetch/treefs_readfile.go b/object/fetch/treefs_readfile.go
deleted file mode 100644
index b248135f..00000000
--- a/object/fetch/treefs_readfile.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package fetch
-
-import (
- "fmt"
- "io"
-
- "codeberg.org/lindenii/furgit/object/tree"
-)
-
-// ReadFile reads the blob contents at name.
-//
-// Directories and gitlink entries are not readable through TreeFS.
-func (treeFS *TreeFS) ReadFile(name string) ([]byte, error) {
- entry, err := treeFS.resolvePath(treeFSOpReadFile, name)
- if err != nil {
- return nil, err
- }
-
- if entry.isDir() {
- return nil, treeFSPathError(treeFSOpReadFile, name, fmt.Errorf("is a directory"))
- }
-
- if entry.mode == tree.FileModeGitlink {
- return nil, treeFSPathError(treeFSOpReadFile, name, fmt.Errorf("object/fetch: gitlink entries are not readable as files"))
- }
-
- reader, _, err := treeFS.fetcher.ExactBlobReader(entry.objectID)
- if err != nil {
- return nil, treeFSPathError(treeFSOpReadFile, name, err)
- }
-
- defer func() { _ = reader.Close() }()
-
- data, err := io.ReadAll(reader)
- if err != nil {
- return nil, treeFSPathError(treeFSOpReadFile, name, err)
- }
-
- return data, nil
-}
diff --git a/object/fetch/treefs_stat.go b/object/fetch/treefs_stat.go
deleted file mode 100644
index 7d7a6418..00000000
--- a/object/fetch/treefs_stat.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package fetch
-
-import "io/fs"
-
-// Stat returns synthetic file metadata for name.
-//
-// TreeFS metadata reflects Git tree entry mode and blob size where applicable.
-// It does not represent filesystem stat metadata: ModTime is zero, ownership is
-// unavailable, and Sys returns the underlying tree.TreeEntry when one exists.
-func (treeFS *TreeFS) Stat(name string) (fs.FileInfo, error) {
- entry, err := treeFS.resolvePath(treeFSOpStat, name)
- if err != nil {
- return nil, err
- }
-
- info, err := treeFS.statEntry(entry)
- if err != nil {
- return nil, treeFSPathError(treeFSOpStat, name, err)
- }
-
- return info, nil
-}
diff --git a/object/fetch/treefs_sub.go b/object/fetch/treefs_sub.go
deleted file mode 100644
index c303d16d..00000000
--- a/object/fetch/treefs_sub.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package fetch
-
-import "io/fs"
-
-// Sub returns a new TreeFS rooted at dir.
-func (treeFS *TreeFS) Sub(dir string) (fs.FS, error) {
- entry, err := treeFS.resolvePath(treeFSOpSub, dir)
- if err != nil {
- return nil, err
- }
-
- treeID, err := entry.subtreeID()
- if err != nil {
- return nil, treeFSPathError(treeFSOpSub, dir, fs.ErrInvalid)
- }
-
- return &TreeFS{
- fetcher: treeFS.fetcher,
- rootTree: treeID,
- rootEntry: entry.treeEntry,
- }, nil
-}
diff --git a/object/fetch/treefs_test.go b/object/fetch/treefs_test.go
deleted file mode 100644
index ba5d4127..00000000
--- a/object/fetch/treefs_test.go
+++ /dev/null
@@ -1,111 +0,0 @@
-package fetch_test
-
-import (
- "errors"
- "io/fs"
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- "codeberg.org/lindenii/furgit/object/fetch"
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/tree"
- "codeberg.org/lindenii/furgit/repository"
-)
-
-func TestTreeFS(t *testing.T) {
- t.Parallel()
-
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- t.Parallel()
-
- repoData := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo})
- repoData.WriteFile(t, "plain.txt", []byte("plain\n"), 0o644)
- repoData.WriteFileAll(t, "dir/exec.sh", []byte("#!/bin/sh\nexit 0\n"), 0o755, 0o755)
- repoData.SymbolicRef(t, "HEAD", "refs/heads/main")
- _ = repoData.Run(t, "add", ".")
- treeHex := repoData.Run(t, "write-tree")
-
- treeID, err := objectid.ParseHex(algo, treeHex)
- if err != nil {
- t.Fatalf("ParseHex(write-tree): %v", err)
- }
-
- commitID := repoData.CommitTree(t, treeID, "treefs")
-
- root := repoData.OpenGitRoot(t)
-
- repo, err := repository.Open(root)
- if err != nil {
- t.Fatalf("repository.Open: %v", err)
- }
-
- defer func() { _ = repo.Close() }()
-
- fetcher := fetch.New(repo.Objects())
-
- treeFS, err := fetcher.TreeFS(commitID)
- if err != nil {
- t.Fatalf("fetcher.TreeFS: %v", err)
- }
-
- content, err := treeFS.ReadFile("plain.txt")
- if err != nil {
- t.Fatalf("ReadFile(plain.txt): %v", err)
- }
-
- if string(content) != "plain\n" {
- t.Fatalf("ReadFile(plain.txt) = %q, want %q", string(content), "plain\n")
- }
-
- entries, err := treeFS.ReadDir(".")
- if err != nil {
- t.Fatalf("ReadDir(.): %v", err)
- }
-
- if len(entries) != 2 {
- t.Fatalf("len(ReadDir(.)) = %d, want 2", len(entries))
- }
-
- info, err := treeFS.Stat("plain.txt")
- if err != nil {
- t.Fatalf("Stat(plain.txt): %v", err)
- }
-
- entry, ok := info.Sys().(tree.TreeEntry)
- if !ok {
- t.Fatalf("Stat(plain.txt).Sys() type = %T, want tree.TreeEntry", info.Sys())
- }
-
- if entry.Mode != tree.FileModeRegular {
- t.Fatalf("Stat(plain.txt).Sys().Mode = %o, want %o", entry.Mode, tree.FileModeRegular)
- }
-
- subFS, err := treeFS.Sub("dir")
- if err != nil {
- t.Fatalf("Sub(dir): %v", err)
- }
-
- subReadFileFS, ok := subFS.(fs.ReadFileFS)
- if !ok {
- t.Fatalf("Sub(dir) type does not implement fs.ReadFileFS")
- }
-
- subContent, err := subReadFileFS.ReadFile("exec.sh")
- if err != nil {
- t.Fatalf("Sub(dir).ReadFile(exec.sh): %v", err)
- }
-
- if string(subContent) != "#!/bin/sh\nexit 0\n" {
- t.Fatalf("Sub(dir).ReadFile(exec.sh) = %q", string(subContent))
- }
-
- _, err = treeFS.ReadFile("dir")
- if err == nil {
- t.Fatal("ReadFile(dir) unexpectedly succeeded")
- }
-
- if _, ok := errors.AsType[*fs.PathError](err); !ok {
- t.Fatalf("ReadFile(dir) err type = %T, want *fs.PathError", err)
- }
- })
-}
diff --git a/object/header/append.go b/object/header/append.go
deleted file mode 100644
index 6d824740..00000000
--- a/object/header/append.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package objectheader
-
-import (
- "strconv"
-
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// Append appends a canonical loose-object header ("type size\\x00") to dst.
-func Append(dst []byte, ty objecttype.Type, size int64) ([]byte, bool) {
- if size < 0 {
- return nil, false
- }
-
- tyName, ok := ty.Name()
- if !ok {
- return nil, false
- }
-
- sizeStr := strconv.FormatInt(size, 10)
- out := make([]byte, 0, len(dst)+len(tyName)+len(sizeStr)+2)
- out = append(out, dst...)
- out = append(out, tyName...)
- out = append(out, ' ')
- out = append(out, sizeStr...)
- out = append(out, 0)
-
- return out, true
-}
diff --git a/object/header/doc.go b/object/header/doc.go
deleted file mode 100644
index 9c953ebb..00000000
--- a/object/header/doc.go
+++ /dev/null
@@ -1,3 +0,0 @@
-// Package objectheader parses and serializes loose-object headers
-// ("type size\x00").
-package objectheader
diff --git a/object/header/encode.go b/object/header/encode.go
deleted file mode 100644
index a03c1f05..00000000
--- a/object/header/encode.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package objectheader
-
-import objecttype "codeberg.org/lindenii/furgit/object/type"
-
-// Encode returns a canonical loose-object header ("type size\\x00").
-func Encode(ty objecttype.Type, size int64) ([]byte, bool) {
- return Append(nil, ty, size)
-}
diff --git a/object/header/parse.go b/object/header/parse.go
deleted file mode 100644
index cad521e5..00000000
--- a/object/header/parse.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package objectheader
-
-import (
- "bytes"
- "strconv"
-
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// Parse parses a canonical loose-object header ("type size\\x00").
-// It returns the parsed type, size, bytes consumed (including trailing NUL),
-// and whether parsing succeeded.
-func Parse(data []byte) (objecttype.Type, int64, int, bool) {
- space := bytes.IndexByte(data, ' ')
- if space <= 0 {
- return objecttype.TypeInvalid, 0, 0, false
- }
-
- nulRel := bytes.IndexByte(data[space+1:], 0)
- if nulRel < 0 {
- return objecttype.TypeInvalid, 0, 0, false
- }
-
- nul := space + 1 + nulRel
-
- ty, ok := objecttype.Parse(string(data[:space]))
- if !ok {
- return objecttype.TypeInvalid, 0, 0, false
- }
-
- sizeBytes := data[space+1 : nul]
- if len(sizeBytes) == 0 {
- return objecttype.TypeInvalid, 0, 0, false
- }
-
- size, err := strconv.ParseInt(string(sizeBytes), 10, 64)
- if err != nil || size < 0 {
- return objecttype.TypeInvalid, 0, 0, false
- }
-
- return ty, size, nul + 1, true
-}
diff --git a/object/id/algorithm.go b/object/id/algorithm.go
deleted file mode 100644
index a695889c..00000000
--- a/object/id/algorithm.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package objectid
-
-//#nosec gosec
-
-// Algorithm identifies the hash algorithm used for Git object IDs.
-type Algorithm uint8
-
-const (
- AlgorithmUnknown Algorithm = iota
- AlgorithmSHA1
- AlgorithmSHA256
-)
diff --git a/object/id/algorithm_details.go b/object/id/algorithm_details.go
deleted file mode 100644
index 15e96292..00000000
--- a/object/id/algorithm_details.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package objectid
-
-import "hash"
-
-type algorithmDetails struct {
- name string
- size int
- packHashID uint32
- signatureHeaderName string
- sum func([]byte) ObjectID
- new func() hash.Hash
- emptyTree ObjectID
-}
-
-func (algo Algorithm) info() algorithmDetails {
- return algorithmTable[algo]
-}
diff --git a/object/id/algorithm_emptytree.go b/object/id/algorithm_emptytree.go
deleted file mode 100644
index 32f57385..00000000
--- a/object/id/algorithm_emptytree.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package objectid
-
-// EmptyTree returns the object ID of an empty tree ("tree 0\x00") for this
-// algorithm.
-func (algo Algorithm) EmptyTree() ObjectID {
- return algo.info().emptyTree
-}
diff --git a/object/id/algorithm_hexlen.go b/object/id/algorithm_hexlen.go
deleted file mode 100644
index 2b7fa0fa..00000000
--- a/object/id/algorithm_hexlen.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package objectid
-
-// HexLen returns the encoded hexadecimal length.
-func (algo Algorithm) HexLen() int {
- return algo.Size() * 2
-}
diff --git a/object/id/algorithm_new.go b/object/id/algorithm_new.go
deleted file mode 100644
index 8abbaeda..00000000
--- a/object/id/algorithm_new.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package objectid
-
-import "hash"
-
-// New returns a new hash.Hash for this algorithm.
-func (algo Algorithm) New() (hash.Hash, error) {
- newFn := algo.info().new
- if newFn == nil {
- return nil, ErrInvalidAlgorithm
- }
-
- return newFn(), nil
-}
diff --git a/object/id/algorithm_packhashid.go b/object/id/algorithm_packhashid.go
deleted file mode 100644
index 93c0f61b..00000000
--- a/object/id/algorithm_packhashid.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package objectid
-
-// PackHashID returns the Git pack/rev hash-id encoding for this algorithm.
-//
-// Unknown algorithms return 0.
-func (algo Algorithm) PackHashID() uint32 {
- return algo.info().packHashID
-}
diff --git a/object/id/algorithm_parse.go b/object/id/algorithm_parse.go
deleted file mode 100644
index d5fb0c64..00000000
--- a/object/id/algorithm_parse.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package objectid
-
-// ParseAlgorithm parses a canonical algorithm name (e.g. "sha1", "sha256").
-func ParseAlgorithm(s string) (Algorithm, bool) {
- algo, ok := algorithmByName[s]
-
- return algo, ok
-}
diff --git a/object/id/algorithm_signatureheadername.go b/object/id/algorithm_signatureheadername.go
deleted file mode 100644
index 34fa41ce..00000000
--- a/object/id/algorithm_signatureheadername.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package objectid
-
-// SignatureHeaderName returns the signature header name for this algorithm.
-func (algo Algorithm) SignatureHeaderName() string {
- return algo.info().signatureHeaderName
-}
diff --git a/object/id/algorithm_size.go b/object/id/algorithm_size.go
deleted file mode 100644
index 104bfeb2..00000000
--- a/object/id/algorithm_size.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package objectid
-
-// Size returns the hash size in bytes.
-func (algo Algorithm) Size() int {
- return algo.info().size
-}
diff --git a/object/id/algorithm_string.go b/object/id/algorithm_string.go
deleted file mode 100644
index 410ee8a3..00000000
--- a/object/id/algorithm_string.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package objectid
-
-// String returns the canonical algorithm name.
-func (algo Algorithm) String() string {
- inf := algo.info()
- if inf.name == "" {
- return "unknown"
- }
-
- return inf.name
-}
diff --git a/object/id/algorithm_sum.go b/object/id/algorithm_sum.go
deleted file mode 100644
index 26ad2ff6..00000000
--- a/object/id/algorithm_sum.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package objectid
-
-// Sum computes an object ID from raw data using the selected algorithm.
-func (algo Algorithm) Sum(data []byte) ObjectID {
- return algo.info().sum(data)
-}
diff --git a/object/id/algorithm_supported.go b/object/id/algorithm_supported.go
deleted file mode 100644
index 1f61e771..00000000
--- a/object/id/algorithm_supported.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package objectid
-
-// SupportedAlgorithms returns all object ID algorithms supported by furgit.
-// Do not mutate.
-func SupportedAlgorithms() []Algorithm {
- return supportedAlgorithms
-}
diff --git a/object/id/algorithm_tables.go b/object/id/algorithm_tables.go
deleted file mode 100644
index e4ec3257..00000000
--- a/object/id/algorithm_tables.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package objectid
-
-import (
- "crypto/sha1" //#nosec:G505
- "crypto/sha256"
-)
-
-//nolint:gochecknoglobals
-var algorithmTable = [...]algorithmDetails{
- AlgorithmUnknown: {},
- AlgorithmSHA1: {
- name: "sha1",
- size: sha1.Size,
- packHashID: 1,
- signatureHeaderName: "gpgsig",
- sum: func(data []byte) ObjectID {
- sum := sha1.Sum(data) //#nosec G401
-
- var id ObjectID
- copy(id.data[:], sum[:])
- id.algo = AlgorithmSHA1
-
- return id
- },
- new: sha1.New,
- },
- AlgorithmSHA256: {
- name: "sha256",
- size: sha256.Size,
- packHashID: 2,
- signatureHeaderName: "gpgsig-sha256",
- sum: func(data []byte) ObjectID {
- sum := sha256.Sum256(data)
-
- var id ObjectID
- copy(id.data[:], sum[:])
- id.algo = AlgorithmSHA256
-
- return id
- },
- new: sha256.New,
- },
-}
-
-var (
- //nolint:gochecknoglobals
- algorithmByName = map[string]Algorithm{}
- //nolint:gochecknoglobals
- algorithmBySignatureHeaderName = map[string]Algorithm{}
- //nolint:gochecknoglobals
- supportedAlgorithms []Algorithm
-)
-
-func init() { //nolint:gochecknoinits
- emptyTreeInput := []byte("tree 0\x00")
-
- for algo := Algorithm(0); int(algo) < len(algorithmTable); algo++ {
- info := &algorithmTable[algo]
- if info.name == "" {
- continue
- }
-
- info.emptyTree = info.sum(emptyTreeInput)
-
- algorithmByName[info.name] = algo
- if info.signatureHeaderName != "" {
- algorithmBySignatureHeaderName[info.signatureHeaderName] = algo
- }
-
- supportedAlgorithms = append(supportedAlgorithms, algo)
- }
-}
diff --git a/object/id/algorithm_zero.go b/object/id/algorithm_zero.go
deleted file mode 100644
index e8c0abf2..00000000
--- a/object/id/algorithm_zero.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package objectid
-
-// Zero returns the all-zero object ID for this algorithm.
-func (algo Algorithm) Zero() ObjectID {
- id, err := FromBytes(algo, make([]byte, algo.Size()))
- if err != nil {
- panic(err)
- }
-
- return id
-}
diff --git a/object/id/doc.go b/object/id/doc.go
deleted file mode 100644
index 1436535d..00000000
--- a/object/id/doc.go
+++ /dev/null
@@ -1,2 +0,0 @@
-// Package objectid provides Git object IDs and object-ID hash algorithms.
-package objectid
diff --git a/object/id/errors.go b/object/id/errors.go
deleted file mode 100644
index 8e604c44..00000000
--- a/object/id/errors.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package objectid
-
-import "errors"
-
-var (
- // ErrInvalidAlgorithm indicates an unsupported object ID algorithm.
- ErrInvalidAlgorithm = errors.New("objectid: invalid algorithm")
- // ErrInvalidObjectID indicates malformed object ID data.
- ErrInvalidObjectID = errors.New("objectid: invalid object id")
-)
diff --git a/object/id/max_size.go b/object/id/max_size.go
deleted file mode 100644
index d2a64a10..00000000
--- a/object/id/max_size.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package objectid
-
-import "crypto/sha256"
-
-// maxObjectIDSize MUST be >= the largest supported algorithm size.
-const maxObjectIDSize = sha256.Size
diff --git a/object/id/objectid.go b/object/id/objectid.go
deleted file mode 100644
index 33a54225..00000000
--- a/object/id/objectid.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package objectid
-
-//#nosec G505
-
-// ObjectID represents a Git object ID.
-//
-//nolint:recvcheck
-type ObjectID struct {
- algo Algorithm
- data [maxObjectIDSize]byte
-}
diff --git a/object/id/objectid_algorithm.go b/object/id/objectid_algorithm.go
deleted file mode 100644
index cb694b7c..00000000
--- a/object/id/objectid_algorithm.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package objectid
-
-// Algorithm returns the object ID's hash algorithm.
-func (id ObjectID) Algorithm() Algorithm {
- return id.algo
-}
diff --git a/object/id/objectid_byte.go b/object/id/objectid_byte.go
deleted file mode 100644
index 8bd8ab82..00000000
--- a/object/id/objectid_byte.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package objectid
-
-// Bytes returns a copy of the object ID bytes.
-func (id ObjectID) Bytes() []byte {
- size := id.Algorithm().Size()
-
- return append([]byte(nil), id.data[:size]...)
-}
-
-// RawBytes returns a direct byte slice view of the object ID bytes.
-//
-// Use Bytes when an independent copy is required.
-//
-// Labels: Mut-Never.
-func (id *ObjectID) RawBytes() []byte {
- size := id.Algorithm().Size()
-
- return id.data[:size:size]
-}
diff --git a/object/id/objectid_compare.go b/object/id/objectid_compare.go
deleted file mode 100644
index a40bcc89..00000000
--- a/object/id/objectid_compare.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package objectid
-
-import "bytes"
-
-// Compare lexicographically compares two object IDs by their canonical byte
-// representation.
-func Compare(left, right ObjectID) int {
- return bytes.Compare(left.RawBytes(), right.RawBytes())
-}
diff --git a/object/id/objectid_frombytes.go b/object/id/objectid_frombytes.go
deleted file mode 100644
index ea8dacfe..00000000
--- a/object/id/objectid_frombytes.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package objectid
-
-import "fmt"
-
-// FromBytes builds an object ID from raw bytes for the specified algorithm.
-func FromBytes(algo Algorithm, b []byte) (ObjectID, error) {
- var id ObjectID
- if algo.Size() == 0 {
- return id, ErrInvalidAlgorithm
- }
-
- if len(b) != algo.Size() {
- return id, fmt.Errorf("%w: got %d bytes, expected %d", ErrInvalidObjectID, len(b), algo.Size())
- }
-
- copy(id.data[:], b)
- id.algo = algo
-
- return id, nil
-}
diff --git a/object/id/objectid_parse.go b/object/id/objectid_parse.go
deleted file mode 100644
index e6cbb641..00000000
--- a/object/id/objectid_parse.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package objectid
-
-import (
- "encoding/hex"
- "fmt"
-)
-
-// ParseHex parses an object ID from hex for the specified algorithm.
-func ParseHex(algo Algorithm, s string) (ObjectID, error) {
- var id ObjectID
- if algo.Size() == 0 {
- return id, ErrInvalidAlgorithm
- }
-
- if len(s)%2 != 0 {
- return id, fmt.Errorf("%w: odd hex length %d", ErrInvalidObjectID, len(s))
- }
-
- if len(s) != algo.HexLen() {
- return id, fmt.Errorf("%w: got %d chars, expected %d", ErrInvalidObjectID, len(s), algo.HexLen())
- }
-
- decoded, err := hex.DecodeString(s)
- if err != nil {
- return id, fmt.Errorf("%w: decode: %w", ErrInvalidObjectID, err)
- }
-
- copy(id.data[:], decoded)
- id.algo = algo
-
- return id, nil
-}
diff --git a/object/id/objectid_string.go b/object/id/objectid_string.go
deleted file mode 100644
index 36a7177d..00000000
--- a/object/id/objectid_string.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package objectid
-
-import "encoding/hex"
-
-// String returns the canonical hex representation.
-func (id ObjectID) String() string {
- size := id.Algorithm().Size()
-
- return hex.EncodeToString(id.data[:size])
-}
diff --git a/object/id/objectid_test.go b/object/id/objectid_test.go
deleted file mode 100644
index 9d179fb5..00000000
--- a/object/id/objectid_test.go
+++ /dev/null
@@ -1,229 +0,0 @@
-package objectid_test
-
-import (
- "bytes"
- "strings"
- "testing"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-func TestParseAlgorithm(t *testing.T) {
- t.Parallel()
-
- algo, ok := objectid.ParseAlgorithm("sha1")
- if !ok || algo != objectid.AlgorithmSHA1 {
- t.Fatalf("ParseAlgorithm(sha1) = (%v,%v)", algo, ok)
- }
-
- algo, ok = objectid.ParseAlgorithm("sha256")
- if !ok || algo != objectid.AlgorithmSHA256 {
- t.Fatalf("ParseAlgorithm(sha256) = (%v,%v)", algo, ok)
- }
-
- if _, ok := objectid.ParseAlgorithm("md5"); ok {
- t.Fatalf("ParseAlgorithm(md5) should fail")
- }
-}
-
-func TestParseHexRoundtrip(t *testing.T) {
- t.Parallel()
-
- for _, algo := range objectid.SupportedAlgorithms() {
- t.Run(algo.String(), func(t *testing.T) {
- t.Parallel()
-
- hex := strings.Repeat("01", algo.Size())
-
- id, err := objectid.ParseHex(algo, hex)
- if err != nil {
- t.Fatalf("ParseHex failed: %v", err)
- }
-
- if got := id.String(); got != hex {
- t.Fatalf("String() = %q, want %q", got, hex)
- }
-
- if got := id.Algorithm().Size(); got != algo.Size() {
- t.Fatalf("Size() = %d, want %d", got, algo.Size())
- }
-
- raw := id.Bytes()
- if len(raw) != algo.Size() {
- t.Fatalf("Bytes len = %d, want %d", len(raw), algo.Size())
- }
-
- id2, err := objectid.FromBytes(algo, raw)
- if err != nil {
- t.Fatalf("FromBytes failed: %v", err)
- }
-
- if id2.String() != hex {
- t.Fatalf("FromBytes roundtrip = %q, want %q", id2.String(), hex)
- }
- })
- }
-}
-
-func TestParseHexErrors(t *testing.T) {
- t.Parallel()
-
- t.Run("unknown algo", func(t *testing.T) {
- t.Parallel()
-
- _, err := objectid.ParseHex(objectid.AlgorithmUnknown, "00")
- if err == nil {
- t.Fatalf("expected ParseHex error")
- }
- })
-
- for _, algo := range objectid.SupportedAlgorithms() {
- t.Run(algo.String(), func(t *testing.T) {
- t.Parallel()
-
- _, err := objectid.ParseHex(algo, strings.Repeat("0", algo.HexLen()-1))
- if err == nil {
- t.Fatalf("expected ParseHex odd-len error")
- }
-
- _, err = objectid.ParseHex(algo, strings.Repeat("0", algo.HexLen()-2))
- if err == nil {
- t.Fatalf("expected ParseHex wrong-len error")
- }
-
- _, err = objectid.ParseHex(algo, "z"+strings.Repeat("0", algo.HexLen()-1))
- if err == nil {
- t.Fatalf("expected ParseHex invalid-hex error")
- }
- })
- }
-}
-
-func TestFromBytesErrors(t *testing.T) {
- t.Parallel()
-
- _, err := objectid.FromBytes(objectid.AlgorithmUnknown, []byte{1, 2})
- if err == nil {
- t.Fatalf("expected FromBytes unknown algo error")
- }
-
- for _, algo := range objectid.SupportedAlgorithms() {
- _, err = objectid.FromBytes(algo, []byte{1, 2})
- if err == nil {
- t.Fatalf("expected FromBytes wrong size error")
- }
- }
-}
-
-func TestBytesReturnsCopy(t *testing.T) {
- t.Parallel()
-
- for _, algo := range objectid.SupportedAlgorithms() {
- id, err := objectid.ParseHex(algo, strings.Repeat("01", algo.Size()))
- if err != nil {
- t.Fatalf("ParseHex failed: %v", err)
- }
-
- b1 := id.Bytes()
-
- b2 := id.Bytes()
- if !bytes.Equal(b1, b2) {
- t.Fatalf("Bytes mismatch")
- }
-
- b1[0] ^= 0xff
- if bytes.Equal(b1, b2) {
- t.Fatalf("Bytes should return independent copies")
- }
- }
-}
-
-func TestRawBytesAliasesStorage(t *testing.T) {
- t.Parallel()
-
- for _, algo := range objectid.SupportedAlgorithms() {
- id, err := objectid.ParseHex(algo, strings.Repeat("01", algo.Size()))
- if err != nil {
- t.Fatalf("ParseHex failed: %v", err)
- }
-
- b := id.RawBytes()
- if len(b) != id.Algorithm().Size() {
- t.Fatalf("RawBytes len = %d, want %d", len(b), id.Algorithm().Size())
- }
-
- if cap(b) != len(b) {
- t.Fatalf("RawBytes cap = %d, want %d", cap(b), len(b))
- }
-
- orig := id.String()
- b[0] ^= 0xff
-
- if id.String() == orig {
- t.Fatalf("RawBytes should alias object ID storage")
- }
- }
-}
-
-func TestAlgorithmSum(t *testing.T) {
- t.Parallel()
-
- id1 := objectid.AlgorithmSHA1.Sum([]byte("hello"))
- if id1.Algorithm() != objectid.AlgorithmSHA1 || id1.Algorithm().Size() != objectid.AlgorithmSHA1.Size() {
- t.Fatalf("sha1 sum produced invalid object id")
- }
-
- id2 := objectid.AlgorithmSHA256.Sum([]byte("hello"))
- if id2.Algorithm() != objectid.AlgorithmSHA256 || id2.Algorithm().Size() != objectid.AlgorithmSHA256.Size() {
- t.Fatalf("sha256 sum produced invalid object id")
- }
-
- if id1.String() == id2.String() {
- t.Fatalf("sha1 and sha256 should differ")
- }
-}
-
-func TestAlgorithmEmptyTree(t *testing.T) {
- t.Parallel()
-
- tests := []struct {
- name string
- algo objectid.Algorithm
- want string
- }{
- {
- name: "sha1",
- algo: objectid.AlgorithmSHA1,
- want: "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
- },
- {
- name: "sha256",
- algo: objectid.AlgorithmSHA256,
- want: "6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321",
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
-
- got := tt.algo.EmptyTree()
- if got.Algorithm() != tt.algo {
- t.Fatalf("EmptyTree() algorithm = %v, want %v", got.Algorithm(), tt.algo)
- }
-
- if got.String() != tt.want {
- t.Fatalf("EmptyTree() = %q, want %q", got.String(), tt.want)
- }
- })
- }
-}
-
-func TestUnknownAlgorithmEmptyTree(t *testing.T) {
- t.Parallel()
-
- got := objectid.AlgorithmUnknown.EmptyTree()
- if got != (objectid.ObjectID{}) {
- t.Fatalf("EmptyTree() for unknown algorithm = %#v, want zero value", got)
- }
-}
diff --git a/object/id/signatureheadername_parse.go b/object/id/signatureheadername_parse.go
deleted file mode 100644
index dbe0636a..00000000
--- a/object/id/signatureheadername_parse.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package objectid
-
-// ParseSignatureHeaderName parses one canonical signature header name such as
-// "gpgsig" or "gpgsig-sha256".
-func ParseSignatureHeaderName(s string) (Algorithm, bool) {
- algo, ok := algorithmBySignatureHeaderName[s]
-
- return algo, ok
-}
diff --git a/object/object.go b/object/object.go
deleted file mode 100644
index d1b1bc4f..00000000
--- a/object/object.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package object
-
-import objecttype "codeberg.org/lindenii/furgit/object/type"
-
-// Object is a Git object.
-type Object interface {
- ObjectType() objecttype.Type
- SerializeWithoutHeader() ([]byte, error)
- SerializeWithHeader() ([]byte, error)
-}
diff --git a/object/parse_with_header.go b/object/parse_with_header.go
deleted file mode 100644
index 9bcf5a4c..00000000
--- a/object/parse_with_header.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package object
-
-import (
- "fmt"
-
- objectheader "codeberg.org/lindenii/furgit/object/header"
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-// ParseWithHeader parses a loose object in "type size\x00body" format.
-//
-//nolint:ireturn
-func ParseWithHeader(raw []byte, algo objectid.Algorithm) (Object, error) {
- ty, size, headerLen, ok := objectheader.Parse(raw)
- if !ok {
- return nil, fmt.Errorf("object: malformed object header")
- }
-
- body := raw[headerLen:]
- if int64(len(body)) != size {
- return nil, fmt.Errorf("object: size mismatch: header says %d bytes, body has %d", size, len(body))
- }
-
- return ParseWithoutHeader(ty, body, algo)
-}
diff --git a/object/parse_without_header.go b/object/parse_without_header.go
deleted file mode 100644
index c889cb40..00000000
--- a/object/parse_without_header.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package object
-
-import (
- "fmt"
-
- "codeberg.org/lindenii/furgit/object/blob"
- "codeberg.org/lindenii/furgit/object/commit"
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/tag"
- "codeberg.org/lindenii/furgit/object/tree"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ParseWithoutHeader parses a typed object body.
-//
-//nolint:ireturn
-func ParseWithoutHeader(ty objecttype.Type, body []byte, algo objectid.Algorithm) (Object, error) {
- switch ty {
- case objecttype.TypeBlob:
- return blob.Parse(body)
- case objecttype.TypeTree:
- return tree.Parse(body, algo)
- case objecttype.TypeCommit:
- return commit.Parse(body, algo)
- case objecttype.TypeTag:
- return tag.Parse(body, algo)
- case objecttype.TypeInvalid, objecttype.TypeFuture, objecttype.TypeOfsDelta, objecttype.TypeRefDelta:
- return nil, fmt.Errorf("object: unsupported object type %d", ty)
- default:
- return nil, fmt.Errorf("object: unsupported object type %d", ty)
- }
-}
diff --git a/object/signature/parse.go b/object/signature/parse.go
deleted file mode 100644
index a6880eee..00000000
--- a/object/signature/parse.go
+++ /dev/null
@@ -1,97 +0,0 @@
-package signature
-
-import (
- "bytes"
- "errors"
- "fmt"
- "strconv"
-
- "codeberg.org/lindenii/furgit/internal/intconv"
-)
-
-// Parse parses a canonical Git signature line:
-// "Name <email> 123456789 +0000".
-func Parse(line []byte) (*Signature, error) {
- lt := bytes.IndexByte(line, '<')
- if lt < 0 {
- return nil, errors.New("object: signature: missing opening <")
- }
-
- gtRel := bytes.IndexByte(line[lt+1:], '>')
- if gtRel < 0 {
- return nil, errors.New("object: signature: missing closing >")
- }
-
- gt := lt + 1 + gtRel
-
- nameBytes := append([]byte(nil), bytes.TrimRight(line[:lt], " ")...)
- emailBytes := append([]byte(nil), line[lt+1:gt]...)
-
- rest := line[gt+1:]
- if len(rest) == 0 || rest[0] != ' ' {
- return nil, errors.New("object: signature: missing timestamp separator")
- }
-
- rest = rest[1:]
-
- before, after, ok := bytes.Cut(rest, []byte{' '})
- if !ok {
- return nil, errors.New("object: signature: missing timezone separator")
- }
-
- when, err := strconv.ParseInt(string(before), 10, 64)
- if err != nil {
- return nil, fmt.Errorf("object: signature: invalid timestamp: %w", err)
- }
-
- tz := after
- if len(tz) < 5 {
- return nil, errors.New("object: signature: invalid timezone encoding")
- }
-
- sign := 1
-
- switch tz[0] {
- case '-':
- sign = -1
- case '+':
- default:
- return nil, errors.New("object: signature: invalid timezone sign")
- }
-
- hh, err := strconv.Atoi(string(tz[1:3]))
- if err != nil {
- return nil, fmt.Errorf("object: signature: invalid timezone hours: %w", err)
- }
-
- mm, err := strconv.Atoi(string(tz[3:5]))
- if err != nil {
- return nil, fmt.Errorf("object: signature: invalid timezone minutes: %w", err)
- }
-
- if hh < 0 || hh > 23 {
- return nil, errors.New("object: signature: invalid timezone hours range")
- }
-
- if mm < 0 || mm > 59 {
- return nil, errors.New("object: signature: invalid timezone minutes range")
- }
-
- total := int64(hh)*60 + int64(mm)
-
- offset, err := intconv.Int64ToInt32(total)
- if err != nil {
- return nil, errors.New("object: signature: timezone overflow")
- }
-
- if sign < 0 {
- offset = -offset
- }
-
- return &Signature{
- Name: nameBytes,
- Email: emailBytes,
- WhenUnix: when,
- OffsetMinutes: offset,
- }, nil
-}
diff --git a/object/signature/serialize.go b/object/signature/serialize.go
deleted file mode 100644
index 3f60d20d..00000000
--- a/object/signature/serialize.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package signature
-
-import (
- "fmt"
- "strconv"
- "strings"
-)
-
-// Serialize renders the signature in canonical Git format.
-func (signature Signature) Serialize() ([]byte, error) {
- var b strings.Builder
- b.Grow(len(signature.Name) + len(signature.Email) + 32)
- b.Write(signature.Name)
- b.WriteString(" <")
- b.Write(signature.Email)
- b.WriteString("> ")
- b.WriteString(strconv.FormatInt(signature.WhenUnix, 10))
- b.WriteByte(' ')
-
- offset := signature.OffsetMinutes
-
- sign := '+'
- if offset < 0 {
- sign = '-'
- offset = -offset
- }
-
- hh := offset / 60
- mm := offset % 60
- fmt.Fprintf(&b, "%c%02d%02d", sign, hh, mm)
-
- return []byte(b.String()), nil
-}
diff --git a/object/signature/signature.go b/object/signature/signature.go
deleted file mode 100644
index bd8b8d87..00000000
--- a/object/signature/signature.go
+++ /dev/null
@@ -1,10 +0,0 @@
-// Package signature provides Git author, committer, and tagger signatures.
-package signature
-
-// Signature represents a Git signature (author/committer/tagger).
-type Signature struct {
- Name []byte
- Email []byte
- WhenUnix int64
- OffsetMinutes int32
-}
diff --git a/object/signature/when.go b/object/signature/when.go
deleted file mode 100644
index 0a252f68..00000000
--- a/object/signature/when.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package signature
-
-import "time"
-
-// When returns a time.Time with the signature's timezone offset.
-func (signature Signature) When() time.Time {
- loc := time.FixedZone("git", int(signature.OffsetMinutes)*60)
-
- return time.Unix(signature.WhenUnix, 0).In(loc)
-}
diff --git a/object/signed/commit/commit.go b/object/signed/commit/commit.go
deleted file mode 100644
index cd0ff197..00000000
--- a/object/signed/commit/commit.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package signedcommit
-
-import objectid "codeberg.org/lindenii/furgit/object/id"
-
-// Commit represents the payload and signatures parsed from a raw comit object.
-type Commit struct {
- body []byte
- payload []byteRange
- signatures map[objectid.Algorithm][]byteRange
-}
-
-type byteRange struct {
- start int
- end int
-}
diff --git a/object/signed/commit/doc.go b/object/signed/commit/doc.go
deleted file mode 100644
index 91da6fa8..00000000
--- a/object/signed/commit/doc.go
+++ /dev/null
@@ -1,6 +0,0 @@
-// Package signedcommit extracts commit signing payloads and signatures from raw
-// commit object bodies.
-package signedcommit
-
-// TODO: Consider whether we want to fully copy the bytes into here.
-// The Append functions are a bit weird ergonomically.
diff --git a/object/signed/commit/integration_test.go b/object/signed/commit/integration_test.go
deleted file mode 100644
index 82b34b14..00000000
--- a/object/signed/commit/integration_test.go
+++ /dev/null
@@ -1,138 +0,0 @@
-package signedcommit_test
-
-import (
- "bytes"
- "os"
- "os/exec"
- "path/filepath"
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- objectid "codeberg.org/lindenii/furgit/object/id"
- signedcommit "codeberg.org/lindenii/furgit/object/signed/commit"
-)
-
-func setupSSHSignedCommit(
- t *testing.T,
- algo objectid.Algorithm,
-) (payload []byte, allowedSignersPath string, signaturePath string) {
- t.Helper()
-
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo})
-
- signDir := t.TempDir()
-
- signRoot, err := os.OpenRoot(signDir)
- if err != nil {
- t.Fatalf("os.OpenRoot(%q): %v", signDir, err)
- }
-
- t.Cleanup(func() { _ = signRoot.Close() })
-
- privateKeyPath := filepath.Join(signDir, "signing_key")
- allowedSignersPath = filepath.Join(signDir, "allowed_signers")
- signaturePath = filepath.Join(signDir, "commit.sig")
-
- cmd := exec.Command( //nolint:noctx
- "ssh-keygen",
- "-q",
- "-t", "ed25519",
- "-N", "",
- "-C", "runxiyu@umich.edu",
- "-f", privateKeyPath,
- ) //#nosec G204
-
- out, err := cmd.CombinedOutput()
- if err != nil {
- t.Fatalf("ssh-keygen generate failed: %v\n%s", err, out)
- }
-
- publicKey, err := signRoot.ReadFile("signing_key.pub")
- if err != nil {
- t.Fatalf("ReadFile(signing_key.pub): %v", err)
- }
-
- err = signRoot.WriteFile(
- "allowed_signers",
- append([]byte("runxiyu@umich.edu "), publicKey...),
- 0o600,
- )
- if err != nil {
- t.Fatalf("WriteFile(allowed_signers): %v", err)
- }
-
- testRepo.Run(t, "config", "gpg.format", "ssh")
- testRepo.Run(t, "config", "user.signingkey", privateKeyPath)
-
- testRepo.WriteFile(t, "file.txt", []byte("signed\n"), 0o644)
- testRepo.Run(t, "add", "file.txt")
- testRepo.Run(t, "commit", "-S", "-m", "signed commit")
-
- commitID := testRepo.RevParse(t, "HEAD^{commit}")
- body := testRepo.CatFile(t, "commit", commitID)
-
- commit, err := signedcommit.Parse(body)
- if err != nil {
- t.Fatalf("Parse: %v", err)
- }
-
- signature, ok := commit.AppendSignature(nil, algo)
- if !ok {
- t.Fatalf("missing %s signature", algo)
- }
-
- err = signRoot.WriteFile("commit.sig", signature, 0o600)
- if err != nil {
- t.Fatalf("WriteFile(commit.sig): %v", err)
- }
-
- return commit.AppendPayload(nil), allowedSignersPath, signaturePath
-}
-
-func TestSSHSignedCommitIntegration(t *testing.T) {
- t.Parallel()
-
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- payload, allowedSignersPath, signaturePath := setupSSHSignedCommit(t, algo)
-
- cmd := exec.Command( //nolint:noctx
- "ssh-keygen",
- "-Y", "verify",
- "-n", "git",
- "-f", allowedSignersPath,
- "-I", "runxiyu@umich.edu",
- "-s", signaturePath,
- ) //#nosec G204
- cmd.Stdin = bytes.NewReader(payload)
-
- out, err := cmd.CombinedOutput()
- if err != nil {
- t.Fatalf("ssh-keygen verify failed: %v\n%s", err, out)
- }
- })
-}
-
-func TestSSHSignedCommitIntegrationRejectsTamperedPayload(t *testing.T) {
- t.Parallel()
-
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- payload, allowedSignersPath, signaturePath := setupSSHSignedCommit(t, algo)
- payload = append([]byte(nil), payload...)
- payload[len(payload)-2] ^= 1
-
- cmd := exec.Command( //nolint:noctx
- "ssh-keygen",
- "-Y", "verify",
- "-n", "git",
- "-f", allowedSignersPath,
- "-I", "runxiyu@umich.edu",
- "-s", signaturePath,
- ) //#nosec G204
- cmd.Stdin = bytes.NewReader(payload)
-
- out, err := cmd.CombinedOutput()
- if err == nil {
- t.Fatalf("ssh-keygen verify unexpectedly succeeded:\n%s", out)
- }
- })
-}
diff --git a/object/signed/commit/parse.go b/object/signed/commit/parse.go
deleted file mode 100644
index fa498093..00000000
--- a/object/signed/commit/parse.go
+++ /dev/null
@@ -1,107 +0,0 @@
-package signedcommit
-
-import (
- "bytes"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-// Parse parses one raw commit object body for signature extraction.
-//
-// The returned Commit remains valid only while body remains unchanged.
-//
-// Labels: Deps-Borrowed, Life-Parent.
-func Parse(body []byte) (*Commit, error) {
- commit := &Commit{
- body: body,
- signatures: make(map[objectid.Algorithm][]byteRange),
- }
-
- payloadStart := 0
- i := 0
-
- for i < len(body) {
- lineStart := i
-
- rel := bytes.IndexByte(body[i:], '\n')
- next := len(body)
-
- lineEnd := len(body)
- if rel >= 0 {
- lineEnd = i + rel
- next = lineEnd + 1
- }
-
- line := body[lineStart:lineEnd]
- i = next
-
- if len(line) == 0 {
- commit.appendPayloadRange(payloadStart, len(body))
-
- return commit, nil
- }
-
- if line[0] == ' ' {
- continue
- }
-
- if !bytes.HasPrefix(line, []byte("gpgsig")) {
- continue
- }
-
- commit.appendPayloadRange(payloadStart, lineStart)
-
- key, valueStart, found := bytes.Cut(line, []byte{' '})
- if found {
- if algo, ok := objectid.ParseSignatureHeaderName(string(key)); ok {
- commit.signatures[algo] = append(commit.signatures[algo], byteRange{
- start: lineEnd - len(valueStart),
- end: next,
- })
- }
- }
-
- for i < len(body) {
- rel := bytes.IndexByte(body[i:], '\n')
- next = len(body)
-
- lineEnd = len(body)
- if rel >= 0 {
- lineEnd = i + rel
- next = lineEnd + 1
- }
-
- contStart := i
-
- cont := body[contStart:lineEnd]
- if len(cont) == 0 || cont[0] != ' ' {
- break
- }
-
- if found {
- if algo, ok := objectid.ParseSignatureHeaderName(string(key)); ok {
- commit.signatures[algo] = append(commit.signatures[algo], byteRange{
- start: contStart + 1,
- end: next,
- })
- }
- }
-
- i = next
- }
-
- payloadStart = i
- }
-
- commit.appendPayloadRange(payloadStart, len(body))
-
- return commit, nil
-}
-
-func (commit *Commit) appendPayloadRange(start, end int) {
- if start >= end {
- return
- }
-
- commit.payload = append(commit.payload, byteRange{start: start, end: end})
-}
diff --git a/object/signed/commit/payload_append.go b/object/signed/commit/payload_append.go
deleted file mode 100644
index c261910a..00000000
--- a/object/signed/commit/payload_append.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package signedcommit
-
-// AppendPayload appends the commit verification payload to dst, omitting all
-// embedded signature headers.
-func (commit *Commit) AppendPayload(dst []byte) []byte {
- for _, part := range commit.payload {
- dst = append(dst, commit.body[part.start:part.end]...)
- }
-
- return dst
-}
diff --git a/object/signed/commit/signature_algorithms.go b/object/signed/commit/signature_algorithms.go
deleted file mode 100644
index ac763706..00000000
--- a/object/signed/commit/signature_algorithms.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package signedcommit
-
-import objectid "codeberg.org/lindenii/furgit/object/id"
-
-// Algorithms returns the algorithms for which the commit carries signatures.
-func (commit *Commit) Algorithms() []objectid.Algorithm {
- var algorithms []objectid.Algorithm
-
- for _, algo := range objectid.SupportedAlgorithms() {
- if _, ok := commit.signatures[algo]; ok {
- algorithms = append(algorithms, algo)
- }
- }
-
- return algorithms
-}
diff --git a/object/signed/commit/signature_append.go b/object/signed/commit/signature_append.go
deleted file mode 100644
index 7f9144b7..00000000
--- a/object/signed/commit/signature_append.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package signedcommit
-
-import objectid "codeberg.org/lindenii/furgit/object/id"
-
-// AppendSignature appends the unfolded signature for algo to dst.
-func (commit *Commit) AppendSignature(dst []byte, algo objectid.Algorithm) ([]byte, bool) {
- signature, ok := commit.signatures[algo]
- if !ok {
- return dst, false
- }
-
- for _, part := range signature {
- dst = append(dst, commit.body[part.start:part.end]...)
- }
-
- return dst, true
-}
diff --git a/object/signed/commit/unit_test.go b/object/signed/commit/unit_test.go
deleted file mode 100644
index 88d4fa3b..00000000
--- a/object/signed/commit/unit_test.go
+++ /dev/null
@@ -1,170 +0,0 @@
-package signedcommit_test
-
-import (
- "slices"
- "testing"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- signedcommit "codeberg.org/lindenii/furgit/object/signed/commit"
-)
-
-func TestParseUpstreamMultiplySignedCommit(t *testing.T) {
- t.Parallel()
-
- // t/t7510-signed-commit.sh
- body := []byte("" +
- "tree 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f\n" +
- "parent 9da738312d24ef0a29be2c8c2b6fc5cf7085a293\n" +
- "author A U Thor <author@example.com> 1112912653 -0700\n" +
- "committer C O Mitter <committer@example.com> 1112912653 -0700\n" +
- "gpgsig -----BEGIN PGP SIGNATURE-----\n" +
- " \n" +
- " iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy\n" +
- " QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC\n" +
- " AKCTqBh+VabJceXcGIZuF0Ry+udbBQ==\n" +
- " =tQ0N\n" +
- " -----END PGP SIGNATURE-----\n" +
- "gpgsig-sha256 -----BEGIN PGP SIGNATURE-----\n" +
- " \n" +
- " iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy\n" +
- " QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO\n" +
- " AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==\n" +
- " =pIwP\n" +
- " -----END PGP SIGNATURE-----\n" +
- "\n" +
- "second\n")
-
- commit, err := signedcommit.Parse(body)
- if err != nil {
- t.Fatalf("Parse: %v", err)
- }
-
- gotPayload := string(commit.AppendPayload(nil))
-
- wantPayload := "" +
- "tree 0cfbf08886fca9a91cb753ec8734c84fcbe52c9f\n" +
- "parent 9da738312d24ef0a29be2c8c2b6fc5cf7085a293\n" +
- "author A U Thor <author@example.com> 1112912653 -0700\n" +
- "committer C O Mitter <committer@example.com> 1112912653 -0700\n" +
- "\n" +
- "second\n"
- if gotPayload != wantPayload {
- t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload)
- }
-
- gotSHA1, ok := commit.AppendSignature(nil, objectid.AlgorithmSHA1)
- if !ok {
- t.Fatal("missing sha1 signature")
- }
-
- wantSHA1 := "" +
- "-----BEGIN PGP SIGNATURE-----\n" +
- "\n" +
- "iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBDRYcY29tbWl0dGVy\n" +
- "QGV4YW1wbGUuY29tAAoJEBO29R7N3kMNd+8AoK1I8mhLHviPH+q2I5fIVgPsEtYC\n" +
- "AKCTqBh+VabJceXcGIZuF0Ry+udbBQ==\n" +
- "=tQ0N\n" +
- "-----END PGP SIGNATURE-----\n"
- if string(gotSHA1) != wantSHA1 {
- t.Fatalf("sha1 signature mismatch:\n got: %q\nwant: %q", string(gotSHA1), wantSHA1)
- }
-
- gotSHA256, ok := commit.AppendSignature(nil, objectid.AlgorithmSHA256)
- if !ok {
- t.Fatal("missing sha256 signature")
- }
-
- wantSHA256 := "" +
- "-----BEGIN PGP SIGNATURE-----\n" +
- "\n" +
- "iHQEABECADQWIQRz11h0S+chaY7FTocTtvUezd5DDQUCX/uBIBYcY29tbWl0dGVy\n" +
- "QGV4YW1wbGUuY29tAAoJEBO29R7N3kMN/NEAn0XO9RYSBj2dFyozi0JKSbssYMtO\n" +
- "AJwKCQ1BQOtuwz//IjU8TiS+6S4iUw==\n" +
- "=pIwP\n" +
- "-----END PGP SIGNATURE-----\n"
- if string(gotSHA256) != wantSHA256 {
- t.Fatalf("sha256 signature mismatch:\n got: %q\nwant: %q", string(gotSHA256), wantSHA256)
- }
-
- gotAlgorithms := commit.Algorithms()
-
- wantAlgorithms := []objectid.Algorithm{
- objectid.AlgorithmSHA1,
- objectid.AlgorithmSHA256,
- }
- if !slices.Equal(gotAlgorithms, wantAlgorithms) {
- t.Fatalf("Algorithms() = %v, want %v", gotAlgorithms, wantAlgorithms)
- }
-}
-
-func TestParseStripsUnknownGpgsigHeadersFromPayload(t *testing.T) {
- t.Parallel()
-
- body := []byte("" +
- "tree deadbeef\n" +
- "gpgsig-future header\n" +
- " continued\n" +
- "\n" +
- "message\n")
-
- commit, err := signedcommit.Parse(body)
- if err != nil {
- t.Fatalf("Parse: %v", err)
- }
-
- gotPayload := string(commit.AppendPayload(nil))
-
- wantPayload := "" +
- "tree deadbeef\n" +
- "\n" +
- "message\n"
- if gotPayload != wantPayload {
- t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload)
- }
-
- if gotAlgorithms := commit.Algorithms(); len(gotAlgorithms) != 0 {
- t.Fatalf("Algorithms() = %v, want none", gotAlgorithms)
- }
-}
-
-func TestParseAllowsDuplicateSignatureHeaders(t *testing.T) {
- t.Parallel()
-
- body := []byte("" +
- "tree deadbeef\n" +
- "gpgsig one\n" +
- " two\n" +
- "gpgsig three\n" +
- " four\n" +
- "\n" +
- "message\n")
-
- commit, err := signedcommit.Parse(body)
- if err != nil {
- t.Fatalf("Parse: %v", err)
- }
-
- gotPayload := string(commit.AppendPayload(nil))
-
- wantPayload := "" +
- "tree deadbeef\n" +
- "\n" +
- "message\n"
- if gotPayload != wantPayload {
- t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload)
- }
-
- gotSignature, ok := commit.AppendSignature(nil, objectid.AlgorithmSHA1)
- if !ok {
- t.Fatal("missing sha1 signature")
- }
-
- wantSignature := "" +
- "one\n" +
- "two\n" +
- "three\n" +
- "four\n"
- if string(gotSignature) != wantSignature {
- t.Fatalf("signature mismatch:\n got: %q\nwant: %q", string(gotSignature), wantSignature)
- }
-}
diff --git a/object/signed/doc.go b/object/signed/doc.go
deleted file mode 100644
index fb6fc3f8..00000000
--- a/object/signed/doc.go
+++ /dev/null
@@ -1,7 +0,0 @@
-// Package signed encapsulates raw signed-object processing.
-//
-// Its subpackages extract verification payloads and embedded signatures from
-// raw commit and tag object bodies, without depending on the parsed
-// object models in [codeberg.org/lindenii/furgit/object/commit] and
-// [codeberg.org/lindenii/furgit/object/tag].
-package signed
diff --git a/object/signed/tag/doc.go b/object/signed/tag/doc.go
deleted file mode 100644
index 22b1098a..00000000
--- a/object/signed/tag/doc.go
+++ /dev/null
@@ -1,3 +0,0 @@
-// Package signedtag extracts tag signing payloads and signatures from raw tag
-// object bodies.
-package signedtag
diff --git a/object/signed/tag/integration_test.go b/object/signed/tag/integration_test.go
deleted file mode 100644
index af32aa02..00000000
--- a/object/signed/tag/integration_test.go
+++ /dev/null
@@ -1,139 +0,0 @@
-package signedtag_test
-
-import (
- "bytes"
- "os"
- "os/exec"
- "path/filepath"
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- objectid "codeberg.org/lindenii/furgit/object/id"
- signedtag "codeberg.org/lindenii/furgit/object/signed/tag"
-)
-
-func setupSSHSignedTag(
- t *testing.T,
- algo objectid.Algorithm,
-) (payload []byte, allowedSignersPath string, signaturePath string) {
- t.Helper()
-
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo})
-
- signDir := t.TempDir()
-
- signRoot, err := os.OpenRoot(signDir)
- if err != nil {
- t.Fatalf("os.OpenRoot(%q): %v", signDir, err)
- }
-
- t.Cleanup(func() { _ = signRoot.Close() })
-
- privateKeyPath := filepath.Join(signDir, "signing_key")
- allowedSignersPath = filepath.Join(signDir, "allowed_signers")
- signaturePath = filepath.Join(signDir, "tag.sig")
-
- cmd := exec.Command( //nolint:noctx
- "ssh-keygen",
- "-q",
- "-t", "ed25519",
- "-N", "",
- "-C", "runxiyu@umich.edu",
- "-f", privateKeyPath,
- ) //#nosec G204
-
- out, err := cmd.CombinedOutput()
- if err != nil {
- t.Fatalf("ssh-keygen generate failed: %v\n%s", err, out)
- }
-
- publicKey, err := signRoot.ReadFile("signing_key.pub")
- if err != nil {
- t.Fatalf("ReadFile(signing_key.pub): %v", err)
- }
-
- err = signRoot.WriteFile(
- "allowed_signers",
- append([]byte("runxiyu@umich.edu "), publicKey...),
- 0o600,
- )
- if err != nil {
- t.Fatalf("WriteFile(allowed_signers): %v", err)
- }
-
- testRepo.Run(t, "config", "gpg.format", "ssh")
- testRepo.Run(t, "config", "user.signingkey", privateKeyPath)
-
- testRepo.WriteFile(t, "file.txt", []byte("signed\n"), 0o644)
- testRepo.Run(t, "add", "file.txt")
- testRepo.Run(t, "commit", "-m", "base commit")
- testRepo.Run(t, "tag", "-s", "-m", "signed tag", "signed-tag")
-
- tagID := testRepo.RevParse(t, "signed-tag^{tag}")
- body := testRepo.CatFile(t, "tag", tagID)
-
- tag, err := signedtag.Parse(body, algo)
- if err != nil {
- t.Fatalf("Parse: %v", err)
- }
-
- signature, ok := tag.AppendSignature(nil, algo)
- if !ok {
- t.Fatal("missing signature")
- }
-
- err = signRoot.WriteFile("tag.sig", signature, 0o600)
- if err != nil {
- t.Fatalf("WriteFile(tag.sig): %v", err)
- }
-
- return tag.AppendPayload(nil), allowedSignersPath, signaturePath
-}
-
-func TestSSHSignedTagIntegration(t *testing.T) {
- t.Parallel()
-
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- payload, allowedSignersPath, signaturePath := setupSSHSignedTag(t, algo)
-
- cmd := exec.Command( //nolint:noctx
- "ssh-keygen",
- "-Y", "verify",
- "-n", "git",
- "-f", allowedSignersPath,
- "-I", "runxiyu@umich.edu",
- "-s", signaturePath,
- ) //#nosec G204
- cmd.Stdin = bytes.NewReader(payload)
-
- out, err := cmd.CombinedOutput()
- if err != nil {
- t.Fatalf("ssh-keygen verify failed: %v\n%s", err, out)
- }
- })
-}
-
-func TestSSHSignedTagIntegrationRejectsTamperedPayload(t *testing.T) {
- t.Parallel()
-
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- payload, allowedSignersPath, signaturePath := setupSSHSignedTag(t, algo)
- payload = append([]byte(nil), payload...)
- payload[len(payload)-2] ^= 1
-
- cmd := exec.Command( //nolint:noctx
- "ssh-keygen",
- "-Y", "verify",
- "-n", "git",
- "-f", allowedSignersPath,
- "-I", "runxiyu@umich.edu",
- "-s", signaturePath,
- ) //#nosec G204
- cmd.Stdin = bytes.NewReader(payload)
-
- out, err := cmd.CombinedOutput()
- if err == nil {
- t.Fatalf("ssh-keygen verify unexpectedly succeeded:\n%s", out)
- }
- })
-}
diff --git a/object/signed/tag/parse.go b/object/signed/tag/parse.go
deleted file mode 100644
index b2061d3f..00000000
--- a/object/signed/tag/parse.go
+++ /dev/null
@@ -1,143 +0,0 @@
-package signedtag
-
-import (
- "bytes"
- "slices"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-var signatureBeginLines = [][]byte{ //nolint:gochecknoglobals
- []byte("-----BEGIN PGP SIGNATURE-----"),
- []byte("-----BEGIN PGP MESSAGE-----"),
- []byte("-----BEGIN SSH SIGNATURE-----"),
- []byte("-----BEGIN SIGNED MESSAGE-----"),
-}
-
-// Parse parses one raw tag object body for signature extraction.
-//
-// Git stores the signature for storageAlgo as an in-body ASCII-armored
-// trailer, and may store additional signatures for other algorithms in
-// gpgsig* headers.
-//
-// The returned Tag remains valid only while body remains unchanged.
-//
-// Labels: Deps-Borrowed, Life-Parent.
-func Parse(body []byte, storageAlgo objectid.Algorithm) (*Tag, error) {
- tag := &Tag{
- body: body,
- signatures: make(map[objectid.Algorithm][]byteRange),
- }
-
- signatureStart := len(body)
- for i := 0; i < len(body); {
- lineStart := i
- rel := bytes.IndexByte(body[i:], '\n')
- next := len(body)
-
- lineEnd := len(body)
- if rel >= 0 {
- lineEnd = i + rel
- next = lineEnd + 1
- }
-
- line := body[lineStart:lineEnd]
- if slices.ContainsFunc(signatureBeginLines, func(begin []byte) bool {
- return bytes.HasPrefix(line, begin)
- }) {
- signatureStart = lineStart
- }
-
- i = next
- }
-
- payloadStart := 0
-
- payloadEnd := signatureStart
- if signatureStart == len(body) {
- payloadEnd = len(body)
- }
-
- for i := 0; i < payloadEnd; {
- lineStart := i
- rel := bytes.IndexByte(body[i:payloadEnd], '\n')
- next := payloadEnd
-
- lineEnd := payloadEnd
- if rel >= 0 {
- lineEnd = i + rel
- next = lineEnd + 1
- }
-
- line := body[lineStart:lineEnd]
- i = next
-
- if len(line) == 0 {
- break
- }
-
- if line[0] == ' ' {
- continue
- }
-
- key, valueStart, found := bytes.Cut(line, []byte{' '})
- if !found {
- continue
- }
-
- algo, ok := objectid.ParseSignatureHeaderName(string(key))
- if !ok {
- continue
- }
-
- tag.appendPayloadRange(payloadStart, lineStart)
- tag.signatures[algo] = append(tag.signatures[algo], byteRange{
- start: lineEnd - len(valueStart),
- end: next,
- })
-
- for i < payloadEnd {
- rel := bytes.IndexByte(body[i:payloadEnd], '\n')
- next = payloadEnd
-
- lineEnd = payloadEnd
- if rel >= 0 {
- lineEnd = i + rel
- next = lineEnd + 1
- }
-
- cont := body[i:lineEnd]
- if len(cont) == 0 || cont[0] != ' ' {
- break
- }
-
- tag.signatures[algo] = append(tag.signatures[algo], byteRange{
- start: i + 1,
- end: next,
- })
-
- i = next
- }
-
- payloadStart = i
- }
-
- tag.appendPayloadRange(payloadStart, payloadEnd)
-
- if signatureStart != len(body) {
- tag.signatures[storageAlgo] = append(tag.signatures[storageAlgo], byteRange{
- start: signatureStart,
- end: len(body),
- })
- }
-
- return tag, nil
-}
-
-func (tag *Tag) appendPayloadRange(start, end int) {
- if start >= end {
- return
- }
-
- tag.payload = append(tag.payload, byteRange{start: start, end: end})
-}
diff --git a/object/signed/tag/payload_append.go b/object/signed/tag/payload_append.go
deleted file mode 100644
index dae29dd8..00000000
--- a/object/signed/tag/payload_append.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package signedtag
-
-// AppendPayload appends the tag verification payload to dst, omitting all
-// embedded signatures.
-func (tag *Tag) AppendPayload(dst []byte) []byte {
- for _, part := range tag.payload {
- dst = append(dst, tag.body[part.start:part.end]...)
- }
-
- return dst
-}
diff --git a/object/signed/tag/signature_algorithms.go b/object/signed/tag/signature_algorithms.go
deleted file mode 100644
index bc178bce..00000000
--- a/object/signed/tag/signature_algorithms.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package signedtag
-
-import objectid "codeberg.org/lindenii/furgit/object/id"
-
-// Algorithms returns the algorithms for which the tag carries signatures.
-func (tag *Tag) Algorithms() []objectid.Algorithm {
- var algorithms []objectid.Algorithm
-
- for _, algo := range objectid.SupportedAlgorithms() {
- if _, ok := tag.signatures[algo]; ok {
- algorithms = append(algorithms, algo)
- }
- }
-
- return algorithms
-}
diff --git a/object/signed/tag/signature_append.go b/object/signed/tag/signature_append.go
deleted file mode 100644
index 101816eb..00000000
--- a/object/signed/tag/signature_append.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package signedtag
-
-import objectid "codeberg.org/lindenii/furgit/object/id"
-
-// AppendSignature appends the signature for algo to dst.
-func (tag *Tag) AppendSignature(dst []byte, algo objectid.Algorithm) ([]byte, bool) {
- signature, ok := tag.signatures[algo]
- if !ok {
- return dst, false
- }
-
- for _, part := range signature {
- dst = append(dst, tag.body[part.start:part.end]...)
- }
-
- return dst, true
-}
diff --git a/object/signed/tag/tag.go b/object/signed/tag/tag.go
deleted file mode 100644
index 2ebf9369..00000000
--- a/object/signed/tag/tag.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package signedtag
-
-import objectid "codeberg.org/lindenii/furgit/object/id"
-
-// Tag represents the payload and signatures parsed from a raw tag object.
-type Tag struct {
- body []byte
- payload []byteRange
- signatures map[objectid.Algorithm][]byteRange
-}
-
-type byteRange struct {
- start int
- end int
-}
diff --git a/object/signed/tag/unit_test.go b/object/signed/tag/unit_test.go
deleted file mode 100644
index dd4ae66f..00000000
--- a/object/signed/tag/unit_test.go
+++ /dev/null
@@ -1,257 +0,0 @@
-package signedtag_test
-
-import (
- "testing"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- signedtag "codeberg.org/lindenii/furgit/object/signed/tag"
-)
-
-func TestParseSignedTag(t *testing.T) {
- t.Parallel()
-
- body := []byte("" +
- "object 04b871796dc0420f8e7561a895b52484b701d51a\n" +
- "type commit\n" +
- "tag signedtag\n" +
- "tagger C O Mitter <committer@example.com> 1465981006 +0000\n" +
- "gpgsig-sha256 -----BEGIN PGP SIGNATURE-----\n" +
- " Version: GnuPG v1\n" +
- " \n" +
- " header-signature\n" +
- " -----END PGP SIGNATURE-----\n" +
- "\n" +
- "signed tag\n" +
- "\n" +
- "signed tag message body\n" +
- "-----BEGIN PGP SIGNATURE-----\n" +
- "Version: GnuPG v1\n" +
- "\n" +
- "body-signature\n" +
- "-----END PGP SIGNATURE-----\n")
-
- tag, err := signedtag.Parse(body, objectid.AlgorithmSHA1)
- if err != nil {
- t.Fatalf("Parse: %v", err)
- }
-
- gotPayload := string(tag.AppendPayload(nil))
-
- wantPayload := "" +
- "object 04b871796dc0420f8e7561a895b52484b701d51a\n" +
- "type commit\n" +
- "tag signedtag\n" +
- "tagger C O Mitter <committer@example.com> 1465981006 +0000\n" +
- "\n" +
- "signed tag\n" +
- "\n" +
- "signed tag message body\n"
- if gotPayload != wantPayload {
- t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload)
- }
-
- gotAlgorithms := tag.Algorithms()
- if len(gotAlgorithms) != 2 || gotAlgorithms[0] != objectid.AlgorithmSHA1 || gotAlgorithms[1] != objectid.AlgorithmSHA256 {
- t.Fatalf("algorithms mismatch: got %v", gotAlgorithms)
- }
-
- gotSignature, ok := tag.AppendSignature(nil, objectid.AlgorithmSHA1)
- if !ok {
- t.Fatal("missing sha1 signature")
- }
-
- wantSignature := "" +
- "-----BEGIN PGP SIGNATURE-----\n" +
- "Version: GnuPG v1\n" +
- "\n" +
- "body-signature\n" +
- "-----END PGP SIGNATURE-----\n"
- if string(gotSignature) != wantSignature {
- t.Fatalf("signature mismatch:\n got: %q\nwant: %q", string(gotSignature), wantSignature)
- }
-
- gotHeaderSignature, ok := tag.AppendSignature(nil, objectid.AlgorithmSHA256)
- if !ok {
- t.Fatal("missing sha256 signature")
- }
-
- wantHeaderSignature := "" +
- "-----BEGIN PGP SIGNATURE-----\n" +
- "Version: GnuPG v1\n" +
- "\n" +
- "header-signature\n" +
- "-----END PGP SIGNATURE-----\n"
- if string(gotHeaderSignature) != wantHeaderSignature {
- t.Fatalf("header signature mismatch:\n got: %q\nwant: %q", string(gotHeaderSignature), wantHeaderSignature)
- }
-}
-
-func TestParseHeaderOnlyTagStripsHeaderAndKeepsHeaderSignature(t *testing.T) {
- t.Parallel()
-
- body := []byte("" +
- "object deadbeef\n" +
- "type commit\n" +
- "tag signedtag\n" +
- "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" +
- "gpgsig-sha256 header\n" +
- " continued\n" +
- "\n" +
- "message\n")
-
- tag, err := signedtag.Parse(body, objectid.AlgorithmSHA1)
- if err != nil {
- t.Fatalf("Parse: %v", err)
- }
-
- gotPayload := string(tag.AppendPayload(nil))
-
- wantPayload := "" +
- "object deadbeef\n" +
- "type commit\n" +
- "tag signedtag\n" +
- "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" +
- "\n" +
- "message\n"
- if gotPayload != wantPayload {
- t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload)
- }
-
- gotSignature, ok := tag.AppendSignature(nil, objectid.AlgorithmSHA256)
- if !ok {
- t.Fatal("missing sha256 signature")
- }
-
- wantSignature := "" +
- "header\n" +
- "continued\n"
- if string(gotSignature) != wantSignature {
- t.Fatalf("signature mismatch:\n got: %q\nwant: %q", string(gotSignature), wantSignature)
- }
-
- if _, ok := tag.AppendSignature(nil, objectid.AlgorithmSHA1); ok {
- t.Fatal("unexpected sha1 signature")
- }
-}
-
-func TestParseKeepsUnknownHeaderSignatureTextInPayload(t *testing.T) {
- t.Parallel()
-
- body := []byte("" +
- "object deadbeef\n" +
- "type commit\n" +
- "tag signedtag\n" +
- "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" +
- "gpgsig-future header\n" +
- " continued\n" +
- "\n" +
- "message line\n" +
- "-----BEGIN PGP SIGNATURE-----\n" +
- "body-signature\n" +
- "-----END PGP SIGNATURE-----\n")
-
- tag, err := signedtag.Parse(body, objectid.AlgorithmSHA1)
- if err != nil {
- t.Fatalf("Parse: %v", err)
- }
-
- gotPayload := string(tag.AppendPayload(nil))
-
- wantPayload := "" +
- "object deadbeef\n" +
- "type commit\n" +
- "tag signedtag\n" +
- "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" +
- "gpgsig-future header\n" +
- " continued\n" +
- "\n" +
- "message line\n"
- if gotPayload != wantPayload {
- t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload)
- }
-}
-
-func TestParseKeepsMessageGpgsigTextInPayload(t *testing.T) {
- t.Parallel()
-
- body := []byte("" +
- "object deadbeef\n" +
- "type commit\n" +
- "tag signedtag\n" +
- "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" +
- "\n" +
- "message line\n" +
- "gpgsig-future header\n" +
- " continued\n" +
- "-----BEGIN PGP SIGNATURE-----\n" +
- "body-signature\n" +
- "-----END PGP SIGNATURE-----\n")
-
- tag, err := signedtag.Parse(body, objectid.AlgorithmSHA1)
- if err != nil {
- t.Fatalf("Parse: %v", err)
- }
-
- gotPayload := string(tag.AppendPayload(nil))
-
- wantPayload := "" +
- "object deadbeef\n" +
- "type commit\n" +
- "tag signedtag\n" +
- "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" +
- "\n" +
- "message line\n" +
- "gpgsig-future header\n" +
- " continued\n"
- if gotPayload != wantPayload {
- t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload)
- }
-}
-
-func TestParseUsesLastSignatureBeginByPrefix(t *testing.T) {
- t.Parallel()
-
- body := []byte("" +
- "object deadbeef\n" +
- "type commit\n" +
- "tag signedtag\n" +
- "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" +
- "\n" +
- "message\n" +
- "-----BEGIN PGP SIGNATURE----- stray\n" +
- "still message\n" +
- "-----BEGIN PGP SIGNATURE----- trailing\n" +
- "body-signature\n")
-
- tag, err := signedtag.Parse(body, objectid.AlgorithmSHA1)
- if err != nil {
- t.Fatalf("Parse: %v", err)
- }
-
- gotPayload := string(tag.AppendPayload(nil))
-
- wantPayload := "" +
- "object deadbeef\n" +
- "type commit\n" +
- "tag signedtag\n" +
- "tagger T A Gger <tagger@example.com> 1465981006 +0000\n" +
- "\n" +
- "message\n" +
- "-----BEGIN PGP SIGNATURE----- stray\n" +
- "still message\n"
- if gotPayload != wantPayload {
- t.Fatalf("payload mismatch:\n got: %q\nwant: %q", gotPayload, wantPayload)
- }
-
- gotSignature, ok := tag.AppendSignature(nil, objectid.AlgorithmSHA1)
- if !ok {
- t.Fatal("missing signature")
- }
-
- wantSignature := "" +
- "-----BEGIN PGP SIGNATURE----- trailing\n" +
- "body-signature\n"
- if string(gotSignature) != wantSignature {
- t.Fatalf("signature mismatch:\n got: %q\nwant: %q", string(gotSignature), wantSignature)
- }
-}
diff --git a/object/store/base_quarantine.go b/object/store/base_quarantine.go
deleted file mode 100644
index 754fb3ee..00000000
--- a/object/store/base_quarantine.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package objectstore
-
-// BaseQuarantine is one quarantined write. It is intended to be embedded.
-type BaseQuarantine interface {
- // Reader exposes the objects written into this quarantine.
- Reader
-
- // Promote publishes quarantined writes into their final destination.
- //
- // Promote invalidates the receiver.
- Promote() error
-
- // Discard abandons quarantined writes.
- //
- // Discard invalidates the receiver.
- Discard() error
-}
diff --git a/object/store/chain/bytes.go b/object/store/chain/bytes.go
deleted file mode 100644
index dc9b7906..00000000
--- a/object/store/chain/bytes.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package chain
-
-import (
- "errors"
- "fmt"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ReadBytesFull reads a full serialized object from the first backend that has it.
-func (chain *Chain) ReadBytesFull(id objectid.ObjectID) ([]byte, error) {
- for i, backend := range chain.backends {
- full, err := backend.ReadBytesFull(id)
- if err == nil {
- return full, nil
- }
-
- if errors.Is(err, objectstore.ErrObjectNotFound) {
- continue
- }
-
- return nil, fmt.Errorf("objectstore: backend %d read bytes full: %w", i, err)
- }
-
- return nil, objectstore.ErrObjectNotFound
-}
-
-// ReadBytesContent reads an object's type and content bytes from the first backend that has it.
-func (chain *Chain) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) {
- for i, backend := range chain.backends {
- ty, content, err := backend.ReadBytesContent(id)
- if err == nil {
- return ty, content, nil
- }
-
- if errors.Is(err, objectstore.ErrObjectNotFound) {
- continue
- }
-
- return objecttype.TypeInvalid, nil, fmt.Errorf("objectstore: backend %d read bytes content: %w", i, err)
- }
-
- return objecttype.TypeInvalid, nil, objectstore.ErrObjectNotFound
-}
diff --git a/object/store/chain/chain.go b/object/store/chain/chain.go
deleted file mode 100644
index 218c8abd..00000000
--- a/object/store/chain/chain.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// Package chain provides a wrapper object storage backend to query a chain of
-// backends.
-package chain
-
-import objectstore "codeberg.org/lindenii/furgit/object/store"
-
-// Chain queries multiple object databases in order.
-//
-// Labels: Close-Caller.
-type Chain struct {
- backends []objectstore.Reader
-}
diff --git a/object/store/chain/header.go b/object/store/chain/header.go
deleted file mode 100644
index f6c92459..00000000
--- a/object/store/chain/header.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package chain
-
-import (
- "errors"
- "fmt"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ReadHeader reads object header data from the first backend that has it.
-func (chain *Chain) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) {
- for i, backend := range chain.backends {
- ty, size, err := backend.ReadHeader(id)
- if err == nil {
- return ty, size, nil
- }
-
- if errors.Is(err, objectstore.ErrObjectNotFound) {
- continue
- }
-
- return objecttype.TypeInvalid, 0, fmt.Errorf("objectstore: backend %d read header: %w", i, err)
- }
-
- return objecttype.TypeInvalid, 0, objectstore.ErrObjectNotFound
-}
diff --git a/object/store/chain/new.go b/object/store/chain/new.go
deleted file mode 100644
index dd499d38..00000000
--- a/object/store/chain/new.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package chain
-
-import objectstore "codeberg.org/lindenii/furgit/object/store"
-
-// New creates an ordered object database chain.
-//
-// The provided backends must be non-nil and distinct.
-//
-// Labels: Deps-Borrowed, Life-Parent.
-func New(backends ...objectstore.Reader) *Chain {
- return &Chain{
- backends: append([]objectstore.Reader(nil), backends...),
- }
-}
diff --git a/object/store/chain/reader.go b/object/store/chain/reader.go
deleted file mode 100644
index 3991ee9a..00000000
--- a/object/store/chain/reader.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package chain
-
-import (
- "errors"
- "fmt"
- "io"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ReadReaderFull reads a full serialized object stream from the first backend that has it.
-func (chain *Chain) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) {
- for i, backend := range chain.backends {
- reader, err := backend.ReadReaderFull(id)
- if err == nil {
- return reader, nil
- }
-
- if errors.Is(err, objectstore.ErrObjectNotFound) {
- continue
- }
-
- return nil, fmt.Errorf("objectstore: backend %d read reader full: %w", i, err)
- }
-
- return nil, objectstore.ErrObjectNotFound
-}
-
-// ReadReaderContent reads an object's type, declared content length, and content stream from the first backend that has it.
-func (chain *Chain) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) {
- for i, backend := range chain.backends {
- ty, size, reader, err := backend.ReadReaderContent(id)
- if err == nil {
- return ty, size, reader, nil
- }
-
- if errors.Is(err, objectstore.ErrObjectNotFound) {
- continue
- }
-
- return objecttype.TypeInvalid, 0, nil, fmt.Errorf("objectstore: backend %d read reader content: %w", i, err)
- }
-
- return objecttype.TypeInvalid, 0, nil, objectstore.ErrObjectNotFound
-}
diff --git a/object/store/chain/refresh.go b/object/store/chain/refresh.go
deleted file mode 100644
index c47352dc..00000000
--- a/object/store/chain/refresh.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package chain
-
-import "errors"
-
-// Refresh forwards refresh calls to all backends.
-func (chain *Chain) Refresh() error {
- var errs []error
-
- for _, backend := range chain.backends {
- err := backend.Refresh()
- if err != nil {
- errs = append(errs, err)
- }
- }
-
- return errors.Join(errs...)
-}
diff --git a/object/store/chain/size.go b/object/store/chain/size.go
deleted file mode 100644
index f0099028..00000000
--- a/object/store/chain/size.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package chain
-
-import (
- "errors"
- "fmt"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
-)
-
-// ReadSize reads object content length from the first backend that has it.
-func (chain *Chain) ReadSize(id objectid.ObjectID) (int64, error) {
- for i, backend := range chain.backends {
- size, err := backend.ReadSize(id)
- if err == nil {
- return size, nil
- }
-
- if errors.Is(err, objectstore.ErrObjectNotFound) {
- continue
- }
-
- return 0, fmt.Errorf("objectstore: backend %d read size: %w", i, err)
- }
-
- return 0, objectstore.ErrObjectNotFound
-}
diff --git a/object/store/cursor.go b/object/store/cursor.go
deleted file mode 100644
index c6008ccd..00000000
--- a/object/store/cursor.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package objectstore
-
-// type Cursor any
-//
-// Then make all read functions accept and provide a Cursor
-// nil must always be accepted and would exhibit the same behavior as right now
-// Non-nil behavior is implementation-defined: e.g., pack selection
diff --git a/object/store/doc.go b/object/store/doc.go
deleted file mode 100644
index 45acc47c..00000000
--- a/object/store/doc.go
+++ /dev/null
@@ -1,19 +0,0 @@
-// Package objectstore provides interfaces for object storage backends.
-//
-// Reading stores only respond to object-ID queries in terms of headers (type
-// and size), raw bytes, and streaming payloads, but they do not parse commits,
-// trees, blobs, or tags into typed values. Turning stored objects into typed
-// objects is the job of [codeberg.org/lindenii/furgit/object/fetch].
-//
-// This package does not define one unified writing interface. Backends have
-// very different write models: writing one loose object is natural, while
-// writing one object into a packfile backend is wasteful. Instead, we define
-// distinct optional capabilities for object-wise writes, pack-wise writes,
-// and compose them against quarantined writes. Where one logical quarantine
-// supports multiple write shapes together, this package also defines a
-// coordinated writer quarantine capability.
-//
-// Concrete implementations generally inherit the contract documented by the
-// interfaces they satisfy. Implementation docs focus on additional guarantees
-// and implementation-specific behavior.
-package objectstore
diff --git a/object/store/dual/doc.go b/object/store/dual/doc.go
deleted file mode 100644
index 104120ec..00000000
--- a/object/store/dual/doc.go
+++ /dev/null
@@ -1,8 +0,0 @@
-// Package dual provides one logical object store backed by separate object-wise
-// and pack-wise stores.
-//
-// Dual composes a store that handles individual object writes with a store that
-// handles pack-wise writes, while exposing one mixed reader over both.
-// Coordinated quarantine operations span both stores, but quarantine promotion
-// is non-atomic.
-package dual
diff --git a/object/store/dual/dual.go b/object/store/dual/dual.go
deleted file mode 100644
index 3072ae77..00000000
--- a/object/store/dual/dual.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package dual
-
-import objectstore "codeberg.org/lindenii/furgit/object/store"
-
-type objectSide interface {
- objectstore.Reader
- objectstore.ObjectWriter
- objectstore.ObjectQuarantiner
-}
-
-type packSide interface {
- objectstore.Reader
- objectstore.PackWriter
- objectstore.PackQuarantiner
-}
-
-// Dual composes one object-wise store and one pack-wise store into one logical
-// object store.
-//
-// Reads are served from the combined object reader of both stores. Individual
-// object writes are routed to the object-wise store, and pack writes are routed
-// to the pack-wise store. Coordinated quarantines go across both stores.
-type Dual struct {
- object objectSide
- pack packSide
- reader objectstore.Reader
-}
-
-var (
- _ objectstore.Reader = (*Dual)(nil)
- _ objectstore.Writer = (*Dual)(nil)
- _ objectstore.Quarantiner = (*Dual)(nil)
-)
diff --git a/object/store/dual/dual_test.go b/object/store/dual/dual_test.go
deleted file mode 100644
index 1d25a775..00000000
--- a/object/store/dual/dual_test.go
+++ /dev/null
@@ -1,266 +0,0 @@
-package dual_test
-
-import (
- "bytes"
- "os"
- "path/filepath"
- "strings"
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
- "codeberg.org/lindenii/furgit/object/store/dual"
- "codeberg.org/lindenii/furgit/object/store/loose"
- "codeberg.org/lindenii/furgit/object/store/packed"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-func fixturePath(t *testing.T, algo objectid.Algorithm, name string) string {
- t.Helper()
-
- return filepath.Join("..", "packed", "internal", "ingest", "testdata", "fixtures", algo.String(), name)
-}
-
-func fixtureBytes(t *testing.T, algo objectid.Algorithm, name string) []byte {
- t.Helper()
-
- path := fixturePath(t, algo, name)
- dir := filepath.Dir(path)
- base := filepath.Base(path)
-
- root, err := os.OpenRoot(dir)
- if err != nil {
- t.Fatalf("open fixture root %q: %v", dir, err)
- }
-
- defer func() {
- err := root.Close()
- if err != nil {
- t.Fatalf("close fixture root %q: %v", dir, err)
- }
- }()
-
- data, err := root.ReadFile(base)
- if err != nil {
- t.Fatalf("read fixture %q: %v", base, err)
- }
-
- return data
-}
-
-func fixtureMetadata(t *testing.T, algo objectid.Algorithm) map[string]string {
- t.Helper()
-
- data := fixtureBytes(t, algo, "METADATA.txt")
- out := make(map[string]string)
-
- for line := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") {
- line = strings.TrimSpace(line)
- if line == "" {
- continue
- }
-
- key, value, ok := strings.Cut(line, "=")
- if !ok {
- t.Fatalf("invalid fixture metadata line %q", line)
- }
-
- out[strings.TrimSpace(key)] = strings.TrimSpace(value)
- }
-
- return out
-}
-
-func fixtureOID(t *testing.T, algo objectid.Algorithm, key string) objectid.ObjectID {
- t.Helper()
-
- meta := fixtureMetadata(t, algo)
-
- hex, ok := meta[key]
- if !ok {
- t.Fatalf("missing fixture metadata key %q", key)
- }
-
- id, err := objectid.ParseHex(algo, hex)
- if err != nil {
- t.Fatalf("parse fixture metadata oid %q: %v", hex, err)
- }
-
- return id
-}
-
-func newDualStore(t *testing.T, repo *testgit.TestRepo, algo objectid.Algorithm) *dual.Dual {
- t.Helper()
-
- objectsRoot := repo.OpenObjectsRoot(t)
-
- looseStore, err := loose.New(objectsRoot, algo)
- if err != nil {
- t.Fatalf("loose.New: %v", err)
- }
-
- packRoot := repo.OpenPackRoot(t)
-
- packedStore, err := packed.New(packRoot, algo, packed.Options{WriteRev: true})
- if err != nil {
- t.Fatalf("packed.New: %v", err)
- }
-
- return dual.New(looseStore, packedStore)
-}
-
-func TestDualReadsWritesAndQuarantine(t *testing.T) {
- t.Parallel()
-
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- head := fixtureOID(t, algo, "head")
- packBytes := fixtureBytes(t, algo, "nonthin.pack")
-
- repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- store := newDualStore(t, repo, algo)
-
- quarantiner, ok := any(store).(objectstore.Quarantiner)
- if !ok {
- t.Fatal("dual does not implement Quarantiner")
- }
-
- quarantine, err := quarantiner.BeginQuarantine(objectstore.QuarantineOptions{})
- if err != nil {
- t.Fatalf("BeginQuarantine: %v", err)
- }
-
- err = quarantine.WritePack(bytes.NewReader(packBytes), objectstore.PackWriteOptions{RequireTrailingEOF: true})
- if err != nil {
- t.Fatalf("quarantine.WritePack: %v", err)
- }
-
- objectQ, ok := any(quarantine).(objectstore.ObjectQuarantine)
- if !ok {
- t.Fatal("pack quarantine does not also implement ObjectQuarantine")
- }
-
- looseContent := []byte("dual quarantine loose object\n")
-
- looseID, err := objectQ.WriteBytesContent(objecttype.TypeBlob, looseContent)
- if err != nil {
- t.Fatalf("quarantine.WriteBytesContent: %v", err)
- }
-
- ty, _, err := quarantine.ReadHeader(head)
- if err != nil {
- t.Fatalf("quarantine.ReadHeader(pack): %v", err)
- }
-
- if ty != objecttype.TypeCommit {
- t.Fatalf("quarantine.ReadHeader(pack) type = %v, want commit", ty)
- }
-
- ty, got, err := quarantine.ReadBytesContent(looseID)
- if err != nil {
- t.Fatalf("quarantine.ReadBytesContent(loose): %v", err)
- }
-
- if ty != objecttype.TypeBlob {
- t.Fatalf("quarantine.ReadBytesContent(loose) type = %v, want blob", ty)
- }
-
- if !bytes.Equal(got, looseContent) {
- t.Fatal("quarantine.ReadBytesContent(loose) mismatch")
- }
-
- _, _, err = store.ReadHeader(head)
- if err == nil {
- t.Fatal("store.ReadHeader unexpectedly saw quarantined pack object before promote")
- }
-
- _, _, err = store.ReadBytesContent(looseID)
- if err == nil {
- t.Fatal("store.ReadBytesContent unexpectedly saw quarantined loose object before promote")
- }
-
- err = quarantine.Promote()
- if err != nil {
- t.Fatalf("quarantine.Promote: %v", err)
- }
-
- err = store.Refresh()
- if err != nil {
- t.Fatalf("store.Refresh: %v", err)
- }
-
- ty, _, err = store.ReadHeader(head)
- if err != nil {
- t.Fatalf("store.ReadHeader(pack): %v", err)
- }
-
- if ty != objecttype.TypeCommit {
- t.Fatalf("store.ReadHeader(pack) type = %v, want commit", ty)
- }
-
- ty, got, err = store.ReadBytesContent(looseID)
- if err != nil {
- t.Fatalf("store.ReadBytesContent(loose): %v", err)
- }
-
- if ty != objecttype.TypeBlob {
- t.Fatalf("store.ReadBytesContent(loose) type = %v, want blob", ty)
- }
-
- if !bytes.Equal(got, looseContent) {
- t.Fatal("store.ReadBytesContent(loose) mismatch")
- }
- })
-}
-
-func TestDualQuarantineDiscardDropsBothHalves(t *testing.T) {
- t.Parallel()
-
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- head := fixtureOID(t, algo, "head")
- packBytes := fixtureBytes(t, algo, "nonthin.pack")
-
- repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- store := newDualStore(t, repo, algo)
-
- quarantiner, ok := any(store).(objectstore.Quarantiner)
- if !ok {
- t.Fatal("expected objectstore.Quarantiner")
- }
-
- quarantine, err := quarantiner.BeginQuarantine(objectstore.QuarantineOptions{})
- if err != nil {
- t.Fatalf("BeginQuarantine: %v", err)
- }
-
- err = quarantine.WritePack(bytes.NewReader(packBytes), objectstore.PackWriteOptions{RequireTrailingEOF: true})
- if err != nil {
- t.Fatalf("quarantine.WritePack: %v", err)
- }
-
- looseID, err := quarantine.WriteBytesContent(objecttype.TypeBlob, []byte("discarded dual object\n"))
- if err != nil {
- t.Fatalf("quarantine.WriteBytesContent: %v", err)
- }
-
- err = quarantine.Discard()
- if err != nil {
- t.Fatalf("quarantine.Discard: %v", err)
- }
-
- err = store.Refresh()
- if err != nil {
- t.Fatalf("store.Refresh: %v", err)
- }
-
- _, _, err = store.ReadHeader(head)
- if err == nil {
- t.Fatal("store.ReadHeader unexpectedly saw discarded pack object")
- }
-
- _, _, err = store.ReadBytesContent(looseID)
- if err == nil {
- t.Fatal("store.ReadBytesContent unexpectedly saw discarded loose object")
- }
- })
-}
diff --git a/object/store/dual/new.go b/object/store/dual/new.go
deleted file mode 100644
index ef38bc7a..00000000
--- a/object/store/dual/new.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package dual
-
-import (
- objectstore "codeberg.org/lindenii/furgit/object/store"
- objectmix "codeberg.org/lindenii/furgit/object/store/mix"
-)
-
-// New creates one dual object store from borrowed object-wise and pack-wise
-// stores.
-//
-// Labels: Deps-Borrowed, Life-Parent.
-func New(
- object interface {
- objectstore.Reader
- objectstore.ObjectWriter
- objectstore.ObjectQuarantiner
- },
- pack interface {
- objectstore.Reader
- objectstore.PackWriter
- objectstore.PackQuarantiner
- },
-) *Dual {
- return &Dual{
- object: object,
- pack: pack,
- reader: objectmix.New(object, pack),
- }
-}
diff --git a/object/store/dual/quarantine.go b/object/store/dual/quarantine.go
deleted file mode 100644
index fb1048af..00000000
--- a/object/store/dual/quarantine.go
+++ /dev/null
@@ -1,114 +0,0 @@
-package dual
-
-import (
- "io"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
- objectmix "codeberg.org/lindenii/furgit/object/store/mix"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// quarantine is one coordinated dual quarantine over both stores.
-type quarantine struct {
- objectQ objectstore.ObjectQuarantine
- packQ objectstore.PackQuarantine
- reader objectstore.Reader
-}
-
-var (
- _ objectstore.ObjectQuarantine = (*quarantine)(nil)
- _ objectstore.PackQuarantine = (*quarantine)(nil)
- _ objectstore.Quarantine = (*quarantine)(nil)
-)
-
-func newQuarantine(
- objectQ objectstore.ObjectQuarantine,
- packQ objectstore.PackQuarantine,
-) *quarantine {
- return &quarantine{
- objectQ: objectQ,
- packQ: packQ,
- reader: objectmix.New(objectQ, packQ),
- }
-}
-
-// ReadBytesFull reads a full serialized object as "type size\0content" from
-// either quarantined store.
-func (quarantine *quarantine) ReadBytesFull(id objectid.ObjectID) ([]byte, error) {
- return quarantine.reader.ReadBytesFull(id)
-}
-
-// ReadBytesContent reads an object's type and content bytes from either
-// quarantined store.
-func (quarantine *quarantine) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) {
- return quarantine.reader.ReadBytesContent(id)
-}
-
-// ReadReaderFull reads a full serialized object stream as
-// "type size\0content" from either quarantined store.
-func (quarantine *quarantine) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) {
- return quarantine.reader.ReadReaderFull(id)
-}
-
-// ReadReaderContent reads an object's type, declared content length, and
-// content stream from either quarantined store.
-func (quarantine *quarantine) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) {
- return quarantine.reader.ReadReaderContent(id)
-}
-
-// ReadSize reads an object's declared content length from either quarantined
-// store.
-func (quarantine *quarantine) ReadSize(id objectid.ObjectID) (int64, error) {
- return quarantine.reader.ReadSize(id)
-}
-
-// ReadHeader reads an object's type and declared content length from either
-// quarantined store.
-func (quarantine *quarantine) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) {
- return quarantine.reader.ReadHeader(id)
-}
-
-// Refresh refreshes both quarantined stores and the combined quarantined reader.
-func (quarantine *quarantine) Refresh() error {
- err := quarantine.objectQ.Refresh()
- if err != nil {
- return err
- }
-
- err = quarantine.packQ.Refresh()
- if err != nil {
- return err
- }
-
- return quarantine.reader.Refresh()
-}
-
-// WriteReaderContent writes one typed object content stream to the quarantined
-// object-wise store.
-func (quarantine *quarantine) WriteReaderContent(ty objecttype.Type, size int64, src io.Reader) (objectid.ObjectID, error) {
- return quarantine.objectQ.WriteReaderContent(ty, size, src)
-}
-
-// WriteReaderFull writes one full serialized object stream as
-// "type size\0content" to the quarantined object-wise store.
-func (quarantine *quarantine) WriteReaderFull(src io.Reader) (objectid.ObjectID, error) {
- return quarantine.objectQ.WriteReaderFull(src)
-}
-
-// WriteBytesContent writes one typed object content byte slice to the
-// quarantined object-wise store.
-func (quarantine *quarantine) WriteBytesContent(ty objecttype.Type, content []byte) (objectid.ObjectID, error) {
- return quarantine.objectQ.WriteBytesContent(ty, content)
-}
-
-// WriteBytesFull writes one full serialized object byte slice as
-// "type size\0content" to the quarantined object-wise store.
-func (quarantine *quarantine) WriteBytesFull(raw []byte) (objectid.ObjectID, error) {
- return quarantine.objectQ.WriteBytesFull(raw)
-}
-
-// WritePack ingests one pack stream into the quarantined pack-wise store.
-func (quarantine *quarantine) WritePack(src io.Reader, opts objectstore.PackWriteOptions) error {
- return quarantine.packQ.WritePack(src, opts)
-}
diff --git a/object/store/dual/quarantine_begin.go b/object/store/dual/quarantine_begin.go
deleted file mode 100644
index 5c6bc934..00000000
--- a/object/store/dual/quarantine_begin.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package dual
-
-import objectstore "codeberg.org/lindenii/furgit/object/store"
-
-// BeginQuarantine creates one coordinated dual quarantine spanning both stores.
-//
-// Labels: Deps-Borrowed, Life-Parent, Close-No.
-func (dual *Dual) BeginQuarantine(opts objectstore.QuarantineOptions) (objectstore.Quarantine, error) {
- objectQ, err := dual.object.BeginObjectQuarantine(opts.Object)
- if err != nil {
- return nil, err
- }
-
- packQ, err := dual.pack.BeginPackQuarantine(opts.Pack)
- if err != nil {
- _ = objectQ.Discard()
-
- return nil, err
- }
-
- return newQuarantine(objectQ, packQ), nil
-}
diff --git a/object/store/dual/quarantine_discard.go b/object/store/dual/quarantine_discard.go
deleted file mode 100644
index 67f15d6c..00000000
--- a/object/store/dual/quarantine_discard.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package dual
-
-// Discard abandons both quarantine halves and invalidates the receiver.
-func (quarantine *quarantine) Discard() error {
- err := quarantine.packQ.Discard()
- if err != nil {
- return err
- }
-
- return quarantine.objectQ.Discard()
-}
diff --git a/object/store/dual/quarantine_promote.go b/object/store/dual/quarantine_promote.go
deleted file mode 100644
index 4d0a45b8..00000000
--- a/object/store/dual/quarantine_promote.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package dual
-
-// Promote publishes both quarantine halves and invalidates the receiver.
-//
-// Promotion is coordinated and ordered, but not atomic.
-func (quarantine *quarantine) Promote() error {
- err := quarantine.packQ.Promote()
- if err != nil {
- return err
- }
-
- return quarantine.objectQ.Promote()
-}
diff --git a/object/store/dual/reader.go b/object/store/dual/reader.go
deleted file mode 100644
index 7b499d5d..00000000
--- a/object/store/dual/reader.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package dual
-
-import (
- "io"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ReadBytesFull reads a full serialized object as "type size\0content" from
-// either store.
-func (dual *Dual) ReadBytesFull(id objectid.ObjectID) ([]byte, error) {
- return dual.reader.ReadBytesFull(id)
-}
-
-// ReadBytesContent reads an object's type and content bytes from either store.
-func (dual *Dual) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) {
- return dual.reader.ReadBytesContent(id)
-}
-
-// ReadReaderFull reads a full serialized object stream as "type size\0content"
-// from either store.
-func (dual *Dual) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) {
- return dual.reader.ReadReaderFull(id)
-}
-
-// ReadReaderContent reads an object's type, declared content length, and
-// content stream from either store.
-func (dual *Dual) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) {
- return dual.reader.ReadReaderContent(id)
-}
-
-// ReadSize reads an object's declared content length from either store.
-func (dual *Dual) ReadSize(id objectid.ObjectID) (int64, error) {
- return dual.reader.ReadSize(id)
-}
-
-// ReadHeader reads an object's type and declared content length from either
-// store.
-func (dual *Dual) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) {
- return dual.reader.ReadHeader(id)
-}
-
-// Refresh refreshes both underlying stores and the combined read view.
-func (dual *Dual) Refresh() error {
- err := dual.object.Refresh()
- if err != nil {
- return err
- }
-
- err = dual.pack.Refresh()
- if err != nil {
- return err
- }
-
- return dual.reader.Refresh()
-}
diff --git a/object/store/dual/writer_object.go b/object/store/dual/writer_object.go
deleted file mode 100644
index 7aefe9ea..00000000
--- a/object/store/dual/writer_object.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package dual
-
-import (
- "io"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// WriteReaderContent writes one typed object content stream to the object-wise
-// store.
-func (dual *Dual) WriteReaderContent(ty objecttype.Type, size int64, src io.Reader) (objectid.ObjectID, error) {
- return dual.object.WriteReaderContent(ty, size, src)
-}
-
-// WriteReaderFull writes one full serialized object stream as
-// "type size\0content" to the object-wise store.
-func (dual *Dual) WriteReaderFull(src io.Reader) (objectid.ObjectID, error) {
- return dual.object.WriteReaderFull(src)
-}
-
-// WriteBytesContent writes one typed object content byte slice to the
-// object-wise store.
-func (dual *Dual) WriteBytesContent(ty objecttype.Type, content []byte) (objectid.ObjectID, error) {
- return dual.object.WriteBytesContent(ty, content)
-}
-
-// WriteBytesFull writes one full serialized object byte slice as
-// "type size\0content" to the object-wise store.
-func (dual *Dual) WriteBytesFull(raw []byte) (objectid.ObjectID, error) {
- return dual.object.WriteBytesFull(raw)
-}
diff --git a/object/store/dual/writer_pack.go b/object/store/dual/writer_pack.go
deleted file mode 100644
index 5ac8648b..00000000
--- a/object/store/dual/writer_pack.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package dual
-
-import (
- "io"
-
- objectstore "codeberg.org/lindenii/furgit/object/store"
-)
-
-// WritePack ingests one pack stream into the pack-wise store.
-func (dual *Dual) WritePack(src io.Reader, opts objectstore.PackWriteOptions) error {
- return dual.pack.WritePack(src, opts)
-}
diff --git a/object/store/errors.go b/object/store/errors.go
deleted file mode 100644
index 0e36b400..00000000
--- a/object/store/errors.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package objectstore
-
-import "errors"
-
-// ErrObjectNotFound indicates that an object does not exist in a backend.
-// This error must only be produced by object stores, when it has no
-// specified object ID, but no other unexpected conditions were encountered.
-var ErrObjectNotFound = errors.New("objectstore: object not found")
diff --git a/object/store/loose/helpers_test.go b/object/store/loose/helpers_test.go
deleted file mode 100644
index 97cec9d7..00000000
--- a/object/store/loose/helpers_test.go
+++ /dev/null
@@ -1,107 +0,0 @@
-package loose_test
-
-import (
- "io"
- "os"
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- objectheader "codeberg.org/lindenii/furgit/object/header"
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/store/loose"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-func openLooseStore(t *testing.T, testRepo *testgit.TestRepo, algo objectid.Algorithm) *loose.Store {
- t.Helper()
-
- root := testRepo.OpenObjectsRoot(t)
-
- store, err := loose.New(root, algo)
- if err != nil {
- t.Fatalf("loose.New: %v", err)
- }
-
- return store
-}
-
-func mustReadAllAndClose(t *testing.T, reader io.ReadCloser) []byte {
- t.Helper()
-
- data, err := io.ReadAll(reader)
- if err != nil {
- _ = reader.Close()
-
- t.Fatalf("ReadAll: %v", err)
- }
-
- err = reader.Close()
- if err != nil {
- t.Fatalf("Close: %v", err)
- }
-
- return data
-}
-
-func expectedRawObject(t *testing.T, testRepo *testgit.TestRepo, id objectid.ObjectID) (objecttype.Type, []byte, []byte) {
- t.Helper()
-
- typeName := testRepo.Run(t, "cat-file", "-t", id.String())
-
- ty, ok := objecttype.Parse(typeName)
- if !ok {
- t.Fatalf("ParseName(%q) failed", typeName)
- }
-
- body := testRepo.CatFile(t, typeName, id)
-
- header, ok := objectheader.Encode(ty, int64(len(body)))
- if !ok {
- t.Fatalf("objectheader.Encode failed")
- }
-
- raw := make([]byte, len(header)+len(body))
- copy(raw, header)
- copy(raw[len(header):], body)
-
- return ty, body, raw
-}
-
-func corruptLooseObjectTrailer(t *testing.T, testRepo *testgit.TestRepo, id objectid.ObjectID) {
- t.Helper()
-
- root := testRepo.OpenObjectsRoot(t)
-
- hex := id.String()
- relPath := hex[:2] + "/" + hex[2:]
-
- file, err := root.OpenFile(relPath, os.O_RDWR, 0)
- if err != nil {
- t.Fatalf("OpenFile(%q): %v", relPath, err)
- }
-
- defer func() { _ = file.Close() }()
-
- info, err := file.Stat()
- if err != nil {
- t.Fatalf("Stat(%q): %v", relPath, err)
- }
-
- if info.Size() == 0 {
- t.Fatalf("corrupt trailer on empty file %q", relPath)
- }
-
- last := make([]byte, 1)
-
- _, err = file.ReadAt(last, info.Size()-1)
- if err != nil {
- t.Fatalf("ReadAt(%q): %v", relPath, err)
- }
-
- last[0] ^= 0xff
-
- _, err = file.WriteAt(last, info.Size()-1)
- if err != nil {
- t.Fatalf("WriteAt(%q): %v", relPath, err)
- }
-}
diff --git a/object/store/loose/parse.go b/object/store/loose/parse.go
deleted file mode 100644
index dfb420ba..00000000
--- a/object/store/loose/parse.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package loose
-
-import (
- "bufio"
- "errors"
- "io"
- "os"
-
- "codeberg.org/lindenii/furgit/internal/compress/zlib"
- objectheader "codeberg.org/lindenii/furgit/object/header"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// decodeAll inflates the full loose object payload from file.
-func decodeAll(file *os.File) ([]byte, error) {
- zr, err := zlib.NewReader(file)
- if err != nil {
- return nil, err
- }
-
- defer func() { _ = zr.Close() }()
-
- return io.ReadAll(zr)
-}
-
-// parseRaw parses a loose object payload in "type size\0content" format.
-func parseRaw(raw []byte) (objecttype.Type, []byte, error) {
- ty, size, headerLen, ok := objectheader.Parse(raw)
- if !ok {
- return objecttype.TypeInvalid, nil, errors.New("objectstore/loose: malformed object header")
- }
-
- content := raw[headerLen:]
- if int64(len(content)) != size {
- return objecttype.TypeInvalid, nil, errors.New("objectstore/loose: object header size/content mismatch")
- }
-
- return ty, content, nil
-}
-
-// readHeader reads and parses a loose object header from br, and returns
-// the raw header bytes including the trailing NUL.
-func readHeader(br *bufio.Reader) ([]byte, objecttype.Type, int64, error) {
- header, err := br.ReadSlice(0)
- if err != nil {
- return nil, objecttype.TypeInvalid, 0, err
- }
-
- ty, size, _, ok := objectheader.Parse(header)
- if !ok {
- return nil, objecttype.TypeInvalid, 0, errors.New("objectstore/loose: malformed object header")
- }
-
- return header, ty, size, nil
-}
diff --git a/object/store/loose/paths.go b/object/store/loose/paths.go
deleted file mode 100644
index 0593cc0d..00000000
--- a/object/store/loose/paths.go
+++ /dev/null
@@ -1,43 +0,0 @@
-package loose
-
-import (
- "errors"
- "fmt"
- "io/fs"
- "os"
- "path/filepath"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
-)
-
-// objectPath returns the loose object path for id relative to the objects root.
-func (store *Store) objectPath(id objectid.ObjectID) (string, error) {
- if id.Algorithm() != store.algo {
- return "", fmt.Errorf("objectstore/loose: object id algorithm mismatch: got %s want %s", id.Algorithm(), store.algo)
- }
-
- hex := id.String()
-
- return filepath.Join(hex[:2], hex[2:]), nil
-}
-
-// openObject opens the loose object file for id.
-// Missing files cause objectstore.ErrObjectNotFound.
-func (store *Store) openObject(id objectid.ObjectID) (*os.File, error) {
- relPath, err := store.objectPath(id)
- if err != nil {
- return nil, err
- }
-
- file, err := store.root.Open(relPath)
- if err != nil {
- if errors.Is(err, fs.ErrNotExist) {
- return nil, objectstore.ErrObjectNotFound
- }
-
- return nil, err
- }
-
- return file, nil
-}
diff --git a/object/store/loose/quarantine.go b/object/store/loose/quarantine.go
deleted file mode 100644
index 52fb8120..00000000
--- a/object/store/loose/quarantine.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package loose
-
-import (
- "os"
-
- objectstore "codeberg.org/lindenii/furgit/object/store"
-)
-
-var _ objectstore.ObjectQuarantiner = (*Store)(nil)
-
-type objectQuarantine struct {
- *Store
-
- parent *Store
- tempName string
- tempRoot *os.Root
-}
-
-var _ objectstore.ObjectQuarantine = (*objectQuarantine)(nil)
diff --git a/object/store/loose/quarantine_begin.go b/object/store/loose/quarantine_begin.go
deleted file mode 100644
index dd27f968..00000000
--- a/object/store/loose/quarantine_begin.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package loose
-
-import (
- "crypto/rand"
- "errors"
- "fmt"
- "io/fs"
- "os"
-
- objectstore "codeberg.org/lindenii/furgit/object/store"
-)
-
-// BeginObjectQuarantine creates one quarantined loose store rooted privately
-// beneath the destination loose root.
-//
-// Labels: Deps-Borrowed, Life-Parent, Close-No.
-func (store *Store) BeginObjectQuarantine(_ objectstore.ObjectQuarantineOptions) (objectstore.ObjectQuarantine, error) {
- tempName, tempRoot, err := createLooseQuarantineRoot(store.root)
- if err != nil {
- return nil, err
- }
-
- quarantineStore, err := New(tempRoot, store.algo)
- if err != nil {
- _ = tempRoot.Close()
- _ = store.root.RemoveAll(tempName)
-
- return nil, err
- }
-
- return &objectQuarantine{
- Store: quarantineStore,
- parent: store,
- tempName: tempName,
- tempRoot: tempRoot,
- }, nil
-}
-
-func createLooseQuarantineRoot(parent *os.Root) (string, *os.Root, error) {
- for range 32 {
- name := "tmp_looseq_" + rand.Text()
-
- err := parent.Mkdir(name, 0o700)
- if err == nil {
- root, err := parent.OpenRoot(name)
- if err == nil {
- return name, root, nil
- }
-
- _ = parent.RemoveAll(name)
-
- return "", nil, err
- }
-
- if errors.Is(err, fs.ErrExist) {
- continue
- }
-
- return "", nil, err
- }
-
- return "", nil, fmt.Errorf("objectstore/loose: unable to create quarantine directory")
-}
diff --git a/object/store/loose/quarantine_discard.go b/object/store/loose/quarantine_discard.go
deleted file mode 100644
index 3e783d0e..00000000
--- a/object/store/loose/quarantine_discard.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package loose
-
-// Discard removes the quarantine and invalidates the receiver.
-func (quarantine *objectQuarantine) Discard() error {
- closeErr := quarantine.Close()
- tempRootErr := quarantine.tempRoot.Close()
- removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName)
-
- if closeErr != nil {
- return closeErr
- }
-
- if tempRootErr != nil {
- return tempRootErr
- }
-
- return removeErr
-}
diff --git a/object/store/loose/quarantine_promote.go b/object/store/loose/quarantine_promote.go
deleted file mode 100644
index 66bb41df..00000000
--- a/object/store/loose/quarantine_promote.go
+++ /dev/null
@@ -1,116 +0,0 @@
-package loose
-
-import (
- "errors"
- "fmt"
- "io/fs"
- "os"
- "path/filepath"
-)
-
-// Promote publishes all quarantined loose objects into the parent loose store
-// and invalidates the receiver.
-func (quarantine *objectQuarantine) Promote() error {
- closeErr := quarantine.Close()
- promoteErr := promoteLooseQuarantine(quarantine.parent, quarantine.tempName, quarantine.tempRoot)
- tempRootErr := quarantine.tempRoot.Close()
- removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName)
-
- if closeErr != nil {
- return closeErr
- }
-
- if promoteErr != nil {
- return promoteErr
- }
-
- if tempRootErr != nil {
- return tempRootErr
- }
-
- return removeErr
-}
-
-func promoteLooseQuarantine(parent *Store, tempName string, tempRoot *os.Root) error {
- entries, err := fs.ReadDir(tempRoot.FS(), ".")
- if err != nil && !errors.Is(err, fs.ErrNotExist) {
- return err
- }
-
- for _, entry := range entries {
- if !entry.IsDir() {
- return fmt.Errorf("objectstore/loose: quarantine contains unexpected file %q", entry.Name())
- }
-
- if len(entry.Name()) != 2 || !isHexString(entry.Name()) {
- return fmt.Errorf("objectstore/loose: quarantine contains invalid shard %q", entry.Name())
- }
-
- err := promoteLooseQuarantineShard(parent, tempName, tempRoot, entry.Name())
- if err != nil {
- return err
- }
- }
-
- return nil
-}
-
-func promoteLooseQuarantineShard(parent *Store, tempName string, tempRoot *os.Root, shard string) error {
- entries, err := fs.ReadDir(tempRoot.FS(), shard)
- if err != nil {
- return err
- }
-
- err = parent.root.MkdirAll(shard, 0o755)
- if err != nil {
- return err
- }
-
- wantNameLen := parent.algo.HexLen() - 2
-
- for _, entry := range entries {
- if entry.IsDir() {
- return fmt.Errorf("objectstore/loose: quarantine shard %q contains unexpected directory %q", shard, entry.Name())
- }
-
- if len(entry.Name()) != wantNameLen || !isHexString(entry.Name()) {
- return fmt.Errorf("objectstore/loose: quarantine shard %q contains invalid object path %q", shard, entry.Name())
- }
-
- err := promoteLooseQuarantineObject(parent.root, filepath.Join(tempName, shard, entry.Name()), filepath.Join(shard, entry.Name()))
- if err != nil {
- return err
- }
- }
-
- return nil
-}
-
-func promoteLooseQuarantineObject(root *os.Root, src, dst string) error {
- err := root.Link(src, dst)
- if err == nil {
- _ = root.Remove(src)
-
- return nil
- }
-
- if errors.Is(err, fs.ErrExist) {
- _ = root.Remove(src)
-
- return nil
- }
-
- return fmt.Errorf("objectstore/loose: promote quarantine %q -> %q: %w", src, dst, err)
-}
-
-func isHexString(s string) bool {
- for _, ch := range s {
- if ('0' <= ch && ch <= '9') || ('a' <= ch && ch <= 'f') || ('A' <= ch && ch <= 'F') {
- continue
- }
-
- return false
- }
-
- return true
-}
diff --git a/object/store/loose/quarantine_test.go b/object/store/loose/quarantine_test.go
deleted file mode 100644
index 4fd1b8f9..00000000
--- a/object/store/loose/quarantine_test.go
+++ /dev/null
@@ -1,119 +0,0 @@
-package loose_test
-
-import (
- "bytes"
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-func TestLooseQuarantinePromotePublishesWrittenObjects(t *testing.T) {
- t.Parallel()
-
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- store := openLooseStore(t, testRepo, algo)
-
- quarantiner, ok := any(store).(objectstore.ObjectQuarantiner)
- if !ok {
- t.Fatal("loose store does not implement ObjectQuarantiner")
- }
-
- quarantine, err := quarantiner.BeginObjectQuarantine(objectstore.ObjectQuarantineOptions{})
- if err != nil {
- t.Fatalf("BeginObjectQuarantine: %v", err)
- }
-
- content := []byte("quarantined loose object\n")
-
- id, err := quarantine.WriteBytesContent(objecttype.TypeBlob, content)
- if err != nil {
- t.Fatalf("quarantine.WriteBytesContent: %v", err)
- }
-
- ty, got, err := quarantine.ReadBytesContent(id)
- if err != nil {
- t.Fatalf("quarantine.ReadBytesContent: %v", err)
- }
-
- if ty != objecttype.TypeBlob {
- t.Fatalf("quarantine.ReadBytesContent type = %v, want %v", ty, objecttype.TypeBlob)
- }
-
- if !bytes.Equal(got, content) {
- t.Fatal("quarantine.ReadBytesContent mismatch")
- }
-
- _, _, err = store.ReadBytesContent(id)
- if err == nil {
- t.Fatal("store.ReadBytesContent unexpectedly saw quarantined object before promote")
- }
-
- err = quarantine.Promote()
- if err != nil {
- t.Fatalf("quarantine.Promote: %v", err)
- }
-
- err = store.Refresh()
- if err != nil {
- t.Fatalf("store.Refresh: %v", err)
- }
-
- ty, got, err = store.ReadBytesContent(id)
- if err != nil {
- t.Fatalf("store.ReadBytesContent after promote: %v", err)
- }
-
- if ty != objecttype.TypeBlob {
- t.Fatalf("store.ReadBytesContent type = %v, want %v", ty, objecttype.TypeBlob)
- }
-
- if !bytes.Equal(got, content) {
- t.Fatal("store.ReadBytesContent mismatch")
- }
- })
-}
-
-func TestLooseQuarantineDiscardDropsWrittenObjects(t *testing.T) {
- t.Parallel()
-
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- store := openLooseStore(t, testRepo, algo)
-
- quarantiner, ok := any(store).(objectstore.ObjectQuarantiner)
- if !ok {
- t.Fatal("expected objectstore.ObjectQuarantiner")
- }
-
- quarantine, err := quarantiner.BeginObjectQuarantine(objectstore.ObjectQuarantineOptions{})
- if err != nil {
- t.Fatalf("BeginObjectQuarantine: %v", err)
- }
-
- content := []byte("discarded loose object\n")
-
- id, err := quarantine.WriteBytesContent(objecttype.TypeBlob, content)
- if err != nil {
- t.Fatalf("quarantine.WriteBytesContent: %v", err)
- }
-
- err = quarantine.Discard()
- if err != nil {
- t.Fatalf("quarantine.Discard: %v", err)
- }
-
- err = store.Refresh()
- if err != nil {
- t.Fatalf("store.Refresh: %v", err)
- }
-
- _, _, err = store.ReadBytesContent(id)
- if err == nil {
- t.Fatal("store.ReadBytesContent unexpectedly saw discarded object")
- }
- })
-}
diff --git a/object/store/loose/read_bytes.go b/object/store/loose/read_bytes.go
deleted file mode 100644
index 5ed3b82b..00000000
--- a/object/store/loose/read_bytes.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package loose
-
-import (
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// readBytesParsed reads, inflates, and parses a loose object in one pass.
-// It returns the full raw payload and its parsed type and content.
-func (store *Store) readBytesParsed(id objectid.ObjectID) ([]byte, objecttype.Type, []byte, error) {
- file, err := store.openObject(id)
- if err != nil {
- return nil, objecttype.TypeInvalid, nil, err
- }
-
- defer func() { _ = file.Close() }()
-
- raw, err := decodeAll(file)
- if err != nil {
- return nil, objecttype.TypeInvalid, nil, err
- }
-
- ty, content, err := parseRaw(raw)
- if err != nil {
- return nil, objecttype.TypeInvalid, nil, err
- }
-
- return raw, ty, content, nil
-}
-
-// ReadBytesFull reads a full serialized object as "type size\0content".
-//
-// It inflates and parses the full loose object, including verifying the zlib
-// Adler-32 trailer.
-func (store *Store) ReadBytesFull(id objectid.ObjectID) ([]byte, error) {
- raw, _, _, err := store.readBytesParsed(id)
- if err != nil {
- return nil, err
- }
-
- return raw, nil
-}
-
-// ReadBytesContent reads an object's type and content bytes.
-//
-// Like ReadBytesFull, it inflates and parses the full loose object, including
-// verifying the zlib Adler-32 trailer.
-func (store *Store) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) {
- _, ty, content, err := store.readBytesParsed(id)
- if err != nil {
- return objecttype.TypeInvalid, nil, err
- }
-
- return ty, content, nil
-}
diff --git a/object/store/loose/read_header.go b/object/store/loose/read_header.go
deleted file mode 100644
index 37bf40de..00000000
--- a/object/store/loose/read_header.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package loose
-
-import (
- "bufio"
-
- "codeberg.org/lindenii/furgit/internal/compress/zlib"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ReadHeader reads an object's type and declared content length.
-//
-// It parses only enough of the zlib-decoded object to recover the object
-// header. It does not verify that the remaining object content is readable and
-// does not verify the zlib Adler-32 trailer.
-func (store *Store) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) {
- file, err := store.openObject(id)
- if err != nil {
- return objecttype.TypeInvalid, 0, err
- }
-
- defer func() { _ = file.Close() }()
-
- zr, err := zlib.NewReader(file)
- if err != nil {
- return objecttype.TypeInvalid, 0, err
- }
-
- defer func() { _ = zr.Close() }()
-
- _, ty, size, err := readHeader(bufio.NewReader(zr))
- if err != nil {
- return objecttype.TypeInvalid, 0, err
- }
-
- return ty, size, nil
-}
diff --git a/object/store/loose/read_reader.go b/object/store/loose/read_reader.go
deleted file mode 100644
index c8c8d736..00000000
--- a/object/store/loose/read_reader.go
+++ /dev/null
@@ -1,114 +0,0 @@
-package loose
-
-import (
- "bufio"
- "bytes"
- "errors"
- "io"
- "os"
-
- "codeberg.org/lindenii/furgit/internal/compress/zlib"
- "codeberg.org/lindenii/furgit/internal/iolimit"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-type objectReader struct {
- // reader is the stream exposed by Read.
- reader io.Reader
- // file is the underlying loose object file and is closed by Close.
- file *os.File
- // zr is the zlib decoder and is closed by Close.
- zr io.ReadCloser
-}
-
-func (reader *objectReader) Read(dst []byte) (int, error) {
- return reader.reader.Read(dst)
-}
-
-func (reader *objectReader) Close() error {
- errZlib := reader.zr.Close()
- errFile := reader.file.Close()
-
- return errors.Join(errZlib, errFile)
-}
-
-// openInflated opens and zlib-decodes a loose object file.
-// The caller owns both returned closers and must close them.
-func (store *Store) openInflated(id objectid.ObjectID) (*os.File, io.ReadCloser, error) {
- file, err := store.openObject(id)
- if err != nil {
- return nil, nil, err
- }
-
- zr, err := zlib.NewReader(file)
- if err != nil {
- _ = file.Close()
-
- return nil, nil, err
- }
-
- return file, zr, nil
-}
-
-// ReadReaderFull reads a full serialized object stream as "type size\0content".
-//
-// Close releases resources only. It does not drain unread data for additional
-// validation. In particular, malformed trailing compressed data, trailing bytes
-// past the declared object size, and the zlib Adler-32 trailer may go
-// unverified unless the caller reads to io.EOF.
-func (store *Store) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) {
- file, zr, err := store.openInflated(id)
- if err != nil {
- return nil, err
- }
-
- br := bufio.NewReader(zr)
-
- header, _, size, err := readHeader(br)
- if err != nil {
- _ = zr.Close()
- _ = file.Close()
-
- return nil, err
- }
-
- return &objectReader{
- reader: io.MultiReader(
- bytes.NewReader(header),
- iolimit.ExpectLengthReader(br, size),
- ),
- file: file,
- zr: zr,
- }, nil
-}
-
-// ReadReaderContent reads an object's type, declared content length, and
-// content stream.
-//
-// Close releases resources only. It does not drain unread data for additional
-// validation. In particular, malformed trailing compressed data, trailing bytes
-// past the declared object size, and the zlib Adler-32 trailer may go
-// unverified unless the caller reads to io.EOF.
-func (store *Store) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) {
- file, zr, err := store.openInflated(id)
- if err != nil {
- return objecttype.TypeInvalid, 0, nil, err
- }
-
- br := bufio.NewReader(zr)
-
- _, ty, size, err := readHeader(br)
- if err != nil {
- _ = zr.Close()
- _ = file.Close()
-
- return objecttype.TypeInvalid, 0, nil, err
- }
-
- return ty, size, &objectReader{
- reader: iolimit.ExpectLengthReader(br, size),
- file: file,
- zr: zr,
- }, nil
-}
diff --git a/object/store/loose/read_size.go b/object/store/loose/read_size.go
deleted file mode 100644
index 2ececc49..00000000
--- a/object/store/loose/read_size.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package loose
-
-import objectid "codeberg.org/lindenii/furgit/object/id"
-
-// ReadSize reads an object's declared content length.
-//
-// Like ReadHeader, it parses only enough of the zlib-decoded object to recover
-// the header and does not verify the zlib Adler-32 trailer.
-func (store *Store) ReadSize(id objectid.ObjectID) (int64, error) {
- _, size, err := store.ReadHeader(id)
-
- return size, err
-}
diff --git a/object/store/loose/read_test.go b/object/store/loose/read_test.go
deleted file mode 100644
index fcb4fe17..00000000
--- a/object/store/loose/read_test.go
+++ /dev/null
@@ -1,212 +0,0 @@
-package loose_test
-
-import (
- "bytes"
- "errors"
- "os"
- "strings"
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
- "codeberg.org/lindenii/furgit/object/store/loose"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-func TestLooseStoreReadAgainstGit(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- blobID := testRepo.HashObject(t, "blob", []byte("blob body\n"))
- _, treeID, commitID := testRepo.MakeCommit(t, "subject\n\nbody")
- tagID := testRepo.TagAnnotated(t, "v1", commitID, "tag message")
-
- store := openLooseStore(t, testRepo, algo)
-
- tests := []struct {
- name string
- id objectid.ObjectID
- }{
- {name: "blob", id: blobID},
- {name: "tree", id: treeID},
- {name: "commit", id: commitID},
- {name: "tag", id: tagID},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- wantType, wantBody, wantRaw := expectedRawObject(t, testRepo, tt.id)
-
- gotRaw, err := store.ReadBytesFull(tt.id)
- if err != nil {
- t.Fatalf("ReadBytesFull: %v", err)
- }
-
- if !bytes.Equal(gotRaw, wantRaw) {
- t.Fatalf("ReadBytesFull mismatch")
- }
-
- gotType, gotBody, err := store.ReadBytesContent(tt.id)
- if err != nil {
- t.Fatalf("ReadBytesContent: %v", err)
- }
-
- if gotType != wantType {
- t.Fatalf("ReadBytesContent type = %v, want %v", gotType, wantType)
- }
-
- if !bytes.Equal(gotBody, wantBody) {
- t.Fatalf("ReadBytesContent body mismatch")
- }
-
- headType, headSize, err := store.ReadHeader(tt.id)
- if err != nil {
- t.Fatalf("ReadHeader: %v", err)
- }
-
- if headType != wantType {
- t.Fatalf("ReadHeader type = %v, want %v", headType, wantType)
- }
-
- if headSize != int64(len(wantBody)) {
- t.Fatalf("ReadHeader size = %d, want %d", headSize, len(wantBody))
- }
-
- fullReader, err := store.ReadReaderFull(tt.id)
- if err != nil {
- t.Fatalf("ReadReaderFull: %v", err)
- }
-
- got := mustReadAllAndClose(t, fullReader)
- if !bytes.Equal(got, wantRaw) {
- t.Fatalf("ReadReaderFull stream mismatch")
- }
-
- contentType, contentSize, contentReader, err := store.ReadReaderContent(tt.id)
- if err != nil {
- t.Fatalf("ReadReaderContent: %v", err)
- }
-
- if contentType != wantType {
- t.Fatalf("ReadReaderContent type = %v, want %v", contentType, wantType)
- }
-
- if contentSize != int64(len(wantBody)) {
- t.Fatalf("ReadReaderContent size = %d, want %d", contentSize, len(wantBody))
- }
-
- got = mustReadAllAndClose(t, contentReader)
- if !bytes.Equal(got, wantBody) {
- t.Fatalf("ReadReaderContent stream mismatch")
- }
- })
- }
- })
-}
-
-func TestLooseStoreErrors(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- store := openLooseStore(t, testRepo, algo)
-
- notFoundID, err := objectid.ParseHex(algo, strings.Repeat("0", algo.HexLen()))
- if err != nil {
- t.Fatalf("ParseHex(notFoundID): %v", err)
- }
-
- _, err = store.ReadBytesFull(notFoundID)
- if !errors.Is(err, objectstore.ErrObjectNotFound) {
- t.Fatalf("ReadBytesFull not-found error = %v", err)
- }
-
- _, _, err = store.ReadBytesContent(notFoundID)
- if !errors.Is(err, objectstore.ErrObjectNotFound) {
- t.Fatalf("ReadBytesContent not-found error = %v", err)
- }
-
- _, err = store.ReadReaderFull(notFoundID)
- if !errors.Is(err, objectstore.ErrObjectNotFound) {
- t.Fatalf("ReadReaderFull not-found error = %v", err)
- }
-
- _, _, _, err = store.ReadReaderContent(notFoundID)
- if !errors.Is(err, objectstore.ErrObjectNotFound) {
- t.Fatalf("ReadReaderContent not-found error = %v", err)
- }
-
- _, _, err = store.ReadHeader(notFoundID)
- if !errors.Is(err, objectstore.ErrObjectNotFound) {
- t.Fatalf("ReadHeader not-found error = %v", err)
- }
-
- var otherAlgo objectid.Algorithm
- if algo == objectid.AlgorithmSHA1 {
- otherAlgo = objectid.AlgorithmSHA256
- } else {
- otherAlgo = objectid.AlgorithmSHA1
- }
-
- otherID, err := objectid.ParseHex(otherAlgo, strings.Repeat("1", otherAlgo.HexLen()))
- if err != nil {
- t.Fatalf("ParseHex(otherID): %v", err)
- }
-
- _, err = store.ReadBytesFull(otherID)
- if err == nil || !strings.Contains(err.Error(), "algorithm mismatch") {
- t.Fatalf("ReadBytesFull algorithm-mismatch error = %v", err)
- }
- })
-}
-
-func TestLooseStoreNewValidation(t *testing.T) {
- t.Parallel()
-
- root, err := os.OpenRoot(t.TempDir())
- if err != nil {
- t.Fatalf("OpenRoot: %v", err)
- }
-
- defer func() { _ = root.Close() }()
-
- _, err = loose.New(root, objectid.AlgorithmUnknown)
- if err == nil {
- t.Fatalf("loose.New(root, unknown) expected error")
- }
-}
-
-func TestLooseStoreReadHeaderDoesNotVerifyAdler32(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- store := openLooseStore(t, testRepo, algo)
-
- content := []byte("header-only-check\n")
-
- id, err := store.WriteBytesContent(objecttype.TypeBlob, content)
- if err != nil {
- t.Fatalf("WriteBytesContent: %v", err)
- }
-
- corruptLooseObjectTrailer(t, testRepo, id)
-
- ty, size, err := store.ReadHeader(id)
- if err != nil {
- t.Fatalf("ReadHeader: %v", err)
- }
-
- if ty != objecttype.TypeBlob {
- t.Fatalf("ReadHeader type = %v, want %v", ty, objecttype.TypeBlob)
- }
-
- if size != int64(len(content)) {
- t.Fatalf("ReadHeader size = %d, want %d", size, len(content))
- }
-
- _, err = store.ReadBytesFull(id)
- if err == nil {
- t.Fatalf("ReadBytesFull on corrupted trailer succeeded")
- }
- })
-}
diff --git a/object/store/loose/refresh.go b/object/store/loose/refresh.go
deleted file mode 100644
index b720ebc6..00000000
--- a/object/store/loose/refresh.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package loose
-
-// Refresh is a no-op for loose object stores.
-func (store *Store) Refresh() error {
- return nil
-}
diff --git a/object/store/loose/store.go b/object/store/loose/store.go
deleted file mode 100644
index ea466284..00000000
--- a/object/store/loose/store.go
+++ /dev/null
@@ -1,43 +0,0 @@
-// Package loose provides a loose object backend (objects/XX/YYYYY..).
-package loose
-
-import (
- "os"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-// Store reads loose Git objects from an objects directory root.
-//
-// Loose objects are zlib streams whose trailer uses Adler-32. Which reads
-// consume enough of the stream to reach and verify that trailer is documented
-// on the individual methods.
-//
-// Labels: Close-Caller.
-type Store struct {
- // root is the objects directory capability used for all object file access.
- // Object files are opened by relative paths like "<first2>/<rest>".
- // Store borrows this root.
- root *os.Root
- // algo is the expected object ID algorithm for lookups.
- algo objectid.Algorithm
-}
-
-// New creates a loose-object store rooted at an objects directory for algo.
-//
-// Labels: Deps-Borrowed, Life-Parent.
-func New(root *os.Root, algo objectid.Algorithm) (*Store, error) {
- if algo.Size() == 0 {
- return nil, objectid.ErrInvalidAlgorithm
- }
-
- return &Store{
- root: root,
- algo: algo,
- }, nil
-}
-
-// Close releases resources associated with the backend.
-//
-// Labels: MT-Unsafe.
-func (store *Store) Close() error { return nil }
diff --git a/object/store/loose/write_bytes.go b/object/store/loose/write_bytes.go
deleted file mode 100644
index ffc65117..00000000
--- a/object/store/loose/write_bytes.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package loose
-
-import (
- "bytes"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// WriteBytesFull writes a full serialized object as "type size\0content".
-func (store *Store) WriteBytesFull(raw []byte) (objectid.ObjectID, error) {
- return store.WriteReaderFull(bytes.NewReader(raw))
-}
-
-// WriteBytesContent writes typed content bytes as a loose object.
-func (store *Store) WriteBytesContent(ty objecttype.Type, content []byte) (objectid.ObjectID, error) {
- return store.WriteReaderContent(ty, int64(len(content)), bytes.NewReader(content))
-}
diff --git a/object/store/loose/write_reader.go b/object/store/loose/write_reader.go
deleted file mode 100644
index f686f279..00000000
--- a/object/store/loose/write_reader.go
+++ /dev/null
@@ -1,81 +0,0 @@
-package loose
-
-import (
- "fmt"
- "io"
-
- objectheader "codeberg.org/lindenii/furgit/object/header"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// WriteReaderContent writes one loose object from typed content bytes read from src.
-// src must provide exactly size bytes.
-// size is required because loose object headers are "type size\0content", so the
-// header must be emitted before streaming content without buffering.
-func (store *Store) WriteReaderContent(ty objecttype.Type, size int64, src io.Reader) (objectid.ObjectID, error) {
- if size < 0 {
- return objectid.ObjectID{}, fmt.Errorf("objectstore/loose: negative content size: %d", size)
- }
-
- header, ok := objectheader.Encode(ty, size)
- if !ok {
- return objectid.ObjectID{}, fmt.Errorf("objectstore/loose: failed to encode object header for type %v", ty)
- }
-
- writer, err := store.newStreamWriter(false)
- if err != nil {
- return objectid.ObjectID{}, err
- }
-
- writer.headerDone = true
- writer.expectedContentLeft = size
-
- err = writer.writeRawChunk(header)
- if err != nil {
- _ = writer.Close()
- _ = store.root.Remove(writer.tmpRelPath)
-
- return objectid.ObjectID{}, err
- }
-
- return writeReaderIntoStreamWriter(writer, src)
-}
-
-// WriteReaderFull writes one loose object from raw bytes "type size\0content"
-// read from src.
-func (store *Store) WriteReaderFull(src io.Reader) (objectid.ObjectID, error) {
- writer, err := store.newStreamWriter(true)
- if err != nil {
- return objectid.ObjectID{}, err
- }
-
- return writeReaderIntoStreamWriter(writer, src)
-}
-
-// writeReaderIntoStreamWriter copies src into writer and publishes the object.
-func writeReaderIntoStreamWriter(writer *streamWriter, src io.Reader) (objectid.ObjectID, error) {
- _, err := io.Copy(writer, src)
- if err != nil {
- _ = writer.Close()
- _ = writer.store.root.Remove(writer.tmpRelPath)
-
- return objectid.ObjectID{}, err
- }
-
- err = writer.Close()
- if err != nil {
- _ = writer.store.root.Remove(writer.tmpRelPath)
-
- return objectid.ObjectID{}, err
- }
-
- id, err := writer.finalize()
- if err != nil {
- _ = writer.store.root.Remove(writer.tmpRelPath)
-
- return objectid.ObjectID{}, err
- }
-
- return id, nil
-}
diff --git a/object/store/loose/write_temp_object_file.go b/object/store/loose/write_temp_object_file.go
deleted file mode 100644
index 1a78db48..00000000
--- a/object/store/loose/write_temp_object_file.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package loose
-
-import (
- "crypto/rand"
- "errors"
- "io/fs"
- "os"
- "path/filepath"
-)
-
-// createTempObjectFile creates a unique temporary object file within dir.
-// The returned path is relative to the objects root.
-func (store *Store) createTempObjectFile(dir string) (string, *os.File, error) {
- for range 16 {
- relPath := filepath.Join(dir, tempObjectFilePrefix+rand.Text())
-
- file, err := store.root.OpenFile(relPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644)
- if err == nil {
- return relPath, file, nil
- }
-
- if errors.Is(err, fs.ErrExist) {
- continue
- }
-
- return "", nil, err
- }
-
- return "", nil, errors.New("objectstore/loose: failed to create temporary object file")
-}
diff --git a/object/store/loose/write_test.go b/object/store/loose/write_test.go
deleted file mode 100644
index 30d8dbdb..00000000
--- a/object/store/loose/write_test.go
+++ /dev/null
@@ -1,137 +0,0 @@
-package loose_test
-
-import (
- "bytes"
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- objectheader "codeberg.org/lindenii/furgit/object/header"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-func TestLooseStoreWriteReaderContentAgainstGit(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- store := openLooseStore(t, testRepo, algo)
-
- content := []byte("written-by-content-reader\n")
- expectedHex := testRepo.RunInput(t, content, "hash-object", "-t", "blob", "--stdin")
-
- expectedID, err := objectid.ParseHex(algo, expectedHex)
- if err != nil {
- t.Fatalf("ParseHex(expected): %v", err)
- }
-
- writtenID, err := store.WriteReaderContent(objecttype.TypeBlob, int64(len(content)), bytes.NewReader(content))
- if err != nil {
- t.Fatalf("WriteReaderContent: %v", err)
- }
-
- if writtenID != expectedID {
- t.Fatalf("WriteReaderContent id = %s, want %s", writtenID, expectedID)
- }
-
- gotBody := testRepo.CatFile(t, "blob", writtenID)
- if !bytes.Equal(gotBody, content) {
- t.Fatalf("git cat-file body mismatch")
- }
-
- // Writing the same object again should succeed and return the same ID.
- writtenID2, err := store.WriteReaderContent(objecttype.TypeBlob, int64(len(content)), bytes.NewReader(content))
- if err != nil {
- t.Fatalf("WriteReaderContent second: %v", err)
- }
-
- if writtenID2 != expectedID {
- t.Fatalf("WriteReaderContent second id = %s, want %s", writtenID2, expectedID)
- }
- })
-}
-
-func TestLooseStoreWriteReaderFullAgainstGit(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- store := openLooseStore(t, testRepo, algo)
-
- body := []byte("full-reader-body\n")
-
- header, ok := objectheader.Encode(objecttype.TypeBlob, int64(len(body)))
- if !ok {
- t.Fatalf("objectheader.Encode failed")
- }
-
- raw := make([]byte, len(header)+len(body))
- copy(raw, header)
- copy(raw[len(header):], body)
-
- wantID := algo.Sum(raw)
-
- gotID, err := store.WriteReaderFull(bytes.NewReader(raw))
- if err != nil {
- t.Fatalf("WriteReaderFull: %v", err)
- }
-
- if gotID != wantID {
- t.Fatalf("WriteReaderFull id = %s, want %s", gotID, wantID)
- }
-
- gotBody := testRepo.CatFile(t, "blob", gotID)
- if !bytes.Equal(gotBody, body) {
- t.Fatalf("git cat-file body mismatch")
- }
- })
-}
-
-func TestLooseStoreReaderValidationErrors(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- t.Run("content overflow", func(t *testing.T) {
- t.Parallel()
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- store := openLooseStore(t, testRepo, algo)
-
- _, err := store.WriteReaderContent(objecttype.TypeBlob, 1, bytes.NewReader([]byte("hello")))
- if err == nil {
- t.Fatalf("expected error after overflow")
- }
- })
-
- t.Run("content short", func(t *testing.T) {
- t.Parallel()
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- store := openLooseStore(t, testRepo, algo)
-
- _, err := store.WriteReaderContent(objecttype.TypeBlob, 5, bytes.NewReader([]byte("x")))
- if err == nil {
- t.Fatalf("expected error for short content")
- }
- })
-
- t.Run("full malformed header", func(t *testing.T) {
- t.Parallel()
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- store := openLooseStore(t, testRepo, algo)
-
- _, err := store.WriteReaderFull(bytes.NewReader([]byte("not-a-header")))
- if err == nil {
- t.Fatalf("expected error for malformed header")
- }
- })
-
- t.Run("full size mismatch", func(t *testing.T) {
- t.Parallel()
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- store := openLooseStore(t, testRepo, algo)
-
- raw := []byte("blob 1\x00hello")
-
- _, err := store.WriteReaderFull(bytes.NewReader(raw))
- if err == nil {
- t.Fatalf("expected error after mismatch")
- }
- })
- })
-}
diff --git a/object/store/loose/write_writer.go b/object/store/loose/write_writer.go
deleted file mode 100644
index 0d6b5b80..00000000
--- a/object/store/loose/write_writer.go
+++ /dev/null
@@ -1,94 +0,0 @@
-package loose
-
-import (
- "errors"
- "hash"
- "os"
-
- "codeberg.org/lindenii/furgit/internal/compress/zlib"
-)
-
-const tempObjectFilePrefix = "tmp_obj_"
-
-// streamWriter incrementally hashes and deflates an object into a temp file.
-// Finalize validates size accounting and atomically renames the temp file.
-type streamWriter struct {
- // store owns path and root operations used by this write session.
- store *Store
- // file is the temporary destination file under objects/.
- file *os.File
- // zw compresses raw object bytes into file.
- zw *zlib.Writer
- // hash receives the same raw bytes used to compute the resulting object ID.
- hash hash.Hash
-
- // tmpRelPath is the relative path of file under the objects root.
- tmpRelPath string
-
- // fullMode selects full-object input ("type size\0content") as opposed to content-only input.
- fullMode bool
-
- // headerBuf accumulates header bytes while fullMode parses up to the first NUL.
- headerBuf []byte
- // headerDone reports whether the full-object header has been parsed.
- headerDone bool
- // expectedContentLeft tracks remaining declared content bytes.
- expectedContentLeft int64
-
- closed bool
- finalized bool
-}
-
-// newStreamWriter creates a stream writer with a temp file rooted in objects/.
-func (store *Store) newStreamWriter(fullMode bool) (*streamWriter, error) {
- hashFn, err := store.algo.New()
- if err != nil {
- return nil, err
- }
-
- tmpRelPath, file, err := store.createTempObjectFile(".")
- if err != nil {
- return nil, err
- }
-
- return &streamWriter{
- store: store,
- file: file,
- zw: zlib.NewWriter(file),
- hash: hashFn,
- tmpRelPath: tmpRelPath,
- fullMode: fullMode,
- headerBuf: make([]byte, 0, 64),
- }, nil
-}
-
-// Write validates and writes raw bytes into the stream.
-// In full mode, it parses and enforces the streamed header-declared content size.
-func (writer *streamWriter) Write(src []byte) (int, error) {
- if writer.finalized {
- return 0, errors.New("objectstore/loose: write after finalize")
- }
-
- if writer.closed {
- return 0, errors.New("objectstore/loose: write after close")
- }
-
- if writer.fullMode {
- err := writer.acceptFull(src)
- if err != nil {
- return 0, err
- }
- } else {
- err := writer.acceptContent(int64(len(src)))
- if err != nil {
- return 0, err
- }
- }
-
- err := writer.writeRawChunk(src)
- if err != nil {
- return 0, err
- }
-
- return len(src), nil
-}
diff --git a/object/store/loose/write_writer_accept.go b/object/store/loose/write_writer_accept.go
deleted file mode 100644
index bf55966a..00000000
--- a/object/store/loose/write_writer_accept.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package loose
-
-import (
- "bytes"
- "errors"
-
- objectheader "codeberg.org/lindenii/furgit/object/header"
-)
-
-// acceptFull validates and accounts raw full-object input.
-func (writer *streamWriter) acceptFull(src []byte) error {
- if !writer.headerDone {
- nul := bytes.IndexByte(src, 0)
- if nul >= 0 {
- headerChunkLen := nul + 1
- writer.headerBuf = append(writer.headerBuf, src[:headerChunkLen]...)
-
- _, size, _, ok := objectheader.Parse(writer.headerBuf)
- if !ok {
- return errors.New("objectstore/loose: malformed object header")
- }
-
- writer.headerDone = true
- writer.expectedContentLeft = size
-
- return writer.acceptContent(int64(len(src) - headerChunkLen))
- }
-
- writer.headerBuf = append(writer.headerBuf, src...)
-
- return nil
- }
-
- return writer.acceptContent(int64(len(src)))
-}
-
-// acceptContent validates and accounts content byte counts.
-func (writer *streamWriter) acceptContent(n int64) error {
- if n > writer.expectedContentLeft {
- return errors.New("objectstore/loose: object content exceeds declared size")
- }
-
- writer.expectedContentLeft -= n
-
- return nil
-}
-
-// writeRawChunk forwards raw bytes to the hash and deflate pipeline.
-func (writer *streamWriter) writeRawChunk(src []byte) error {
- _, err := writer.hash.Write(src)
- if err != nil {
- return err
- }
-
- _, err = writer.zw.Write(src)
- if err != nil {
- return err
- }
-
- return nil
-}
diff --git a/object/store/loose/write_writer_finalize.go b/object/store/loose/write_writer_finalize.go
deleted file mode 100644
index 71e275db..00000000
--- a/object/store/loose/write_writer_finalize.go
+++ /dev/null
@@ -1,89 +0,0 @@
-package loose
-
-import (
- "errors"
- "io/fs"
- "path/filepath"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-// Close flushes and closes the underlying zlib stream and temp file.
-func (writer *streamWriter) Close() error {
- errZlib := writer.zw.Close()
- errSync := writer.file.Sync()
- errFile := writer.file.Close()
-
- writer.closed = true
- writer.file = nil
-
- return errors.Join(errZlib, errSync, errFile)
-}
-
-// finalize validates write completeness and atomically publishes the object.
-// Publication is no-clobber: it links tmpRelPath to the object path and treats
-// existing destination objects as success.
-func (writer *streamWriter) finalize() (objectid.ObjectID, error) {
- writer.finalized = true
-
- var zero objectid.ObjectID
-
- if !writer.closed {
- err := writer.Close()
- if err != nil {
- return zero, err
- }
- }
-
- if writer.fullMode && !writer.headerDone {
- return zero, errors.New("objectstore/loose: missing full object header")
- }
-
- if writer.expectedContentLeft != 0 {
- return zero, errors.New("objectstore/loose: object content shorter than declared size")
- }
-
- idBytes := writer.hash.Sum(nil)
-
- id, err := objectid.FromBytes(writer.store.algo, idBytes)
- if err != nil {
- return zero, err
- }
-
- relPath, err := writer.store.objectPath(id)
- if err != nil {
- return zero, err
- }
-
- dir := filepath.Dir(relPath)
-
- err = writer.store.root.MkdirAll(dir, 0o755)
- if err != nil {
- return zero, err
- }
-
- cleanup := true
-
- defer func() {
- if cleanup {
- _ = writer.store.root.Remove(writer.tmpRelPath)
- }
- }()
-
- err = writer.store.root.Link(writer.tmpRelPath, relPath)
- if err != nil {
- if errors.Is(err, fs.ErrExist) {
- cleanup = false
- _ = writer.store.root.Remove(writer.tmpRelPath)
-
- return id, nil
- }
-
- return zero, err
- }
-
- cleanup = false
- _ = writer.store.root.Remove(writer.tmpRelPath)
-
- return id, nil
-}
diff --git a/object/store/memory/algorithm.go b/object/store/memory/algorithm.go
deleted file mode 100644
index bf7f3a82..00000000
--- a/object/store/memory/algorithm.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package memory
-
-import objectid "codeberg.org/lindenii/furgit/object/id"
-
-// Algorithm returns the object ID algorithm used by the store.
-func (store *Store) Algorithm() objectid.Algorithm {
- return store.algo
-}
diff --git a/object/store/memory/doc.go b/object/store/memory/doc.go
deleted file mode 100644
index cb40d466..00000000
--- a/object/store/memory/doc.go
+++ /dev/null
@@ -1,2 +0,0 @@
-// Package memory provides one in-memory object store.
-package memory
diff --git a/object/store/memory/object.go b/object/store/memory/object.go
deleted file mode 100644
index a85175c7..00000000
--- a/object/store/memory/object.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package memory
-
-import objecttype "codeberg.org/lindenii/furgit/object/type"
-
-// storedObject is one in-memory object entry.
-type storedObject struct {
- ty objecttype.Type
- content []byte
-}
diff --git a/object/store/memory/read_bytes.go b/object/store/memory/read_bytes.go
deleted file mode 100644
index 48d3694a..00000000
--- a/object/store/memory/read_bytes.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package memory
-
-import (
- objectheader "codeberg.org/lindenii/furgit/object/header"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ReadBytesFull reads one full object, including the object header.
-func (store *Store) ReadBytesFull(id objectid.ObjectID) ([]byte, error) {
- obj, ok := store.objects[id]
- if !ok {
- return nil, objectstore.ErrObjectNotFound
- }
-
- header, ok := objectheader.Encode(obj.ty, int64(len(obj.content)))
- if !ok {
- panic("failed to encode object header")
- }
-
- raw := make([]byte, len(header)+len(obj.content))
- copy(raw, header)
- copy(raw[len(header):], obj.content)
-
- return raw, nil
-}
-
-// ReadBytesContent reads one object body.
-func (store *Store) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) {
- obj, ok := store.objects[id]
- if !ok {
- return objecttype.TypeInvalid, nil, objectstore.ErrObjectNotFound
- }
-
- return obj.ty, append([]byte(nil), obj.content...), nil
-}
diff --git a/object/store/memory/read_header.go b/object/store/memory/read_header.go
deleted file mode 100644
index da3acd1c..00000000
--- a/object/store/memory/read_header.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package memory
-
-import (
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ReadHeader reads one object header.
-func (store *Store) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) {
- obj, ok := store.objects[id]
- if !ok {
- return objecttype.TypeInvalid, 0, objectstore.ErrObjectNotFound
- }
-
- return obj.ty, int64(len(obj.content)), nil
-}
diff --git a/object/store/memory/read_reader.go b/object/store/memory/read_reader.go
deleted file mode 100644
index 425c3034..00000000
--- a/object/store/memory/read_reader.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package memory
-
-import (
- "bytes"
- "io"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ReadReaderFull reads one full object through a reader.
-func (store *Store) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) {
- raw, err := store.ReadBytesFull(id)
- if err != nil {
- return nil, err
- }
-
- return io.NopCloser(bytes.NewReader(raw)), nil
-}
-
-// ReadReaderContent reads one object body through a reader.
-func (store *Store) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) {
- ty, content, err := store.ReadBytesContent(id)
- if err != nil {
- return objecttype.TypeInvalid, 0, nil, err
- }
-
- return ty, int64(len(content)), io.NopCloser(bytes.NewReader(content)), nil
-}
diff --git a/object/store/memory/read_size.go b/object/store/memory/read_size.go
deleted file mode 100644
index 7045bd61..00000000
--- a/object/store/memory/read_size.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package memory
-
-import objectid "codeberg.org/lindenii/furgit/object/id"
-
-// ReadSize reads one object size.
-func (store *Store) ReadSize(id objectid.ObjectID) (int64, error) {
- _, size, err := store.ReadHeader(id)
- if err != nil {
- return 0, err
- }
-
- return size, nil
-}
diff --git a/object/store/memory/refresh.go b/object/store/memory/refresh.go
deleted file mode 100644
index 1e18eef3..00000000
--- a/object/store/memory/refresh.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package memory
-
-// Refresh is a no-op for in-memory object stores.
-func (store *Store) Refresh() error {
- return nil
-}
diff --git a/object/store/memory/store.go b/object/store/memory/store.go
deleted file mode 100644
index ff66da50..00000000
--- a/object/store/memory/store.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package memory
-
-import (
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-// Store is one in-memory object store.
-//
-// Labels: Close-Caller.
-type Store struct {
- algo objectid.Algorithm
- objects map[objectid.ObjectID]storedObject
-}
-
-// New builds one empty in-memory store for one object format.
-func New(algo objectid.Algorithm) *Store {
- return &Store{
- algo: algo,
- objects: make(map[objectid.ObjectID]storedObject),
- }
-}
-
-// Close closes the in-memory store.
-//
-// Labels: MT-Unsafe.
-func (store *Store) Close() error {
- return nil
-}
diff --git a/object/store/memory/write_bytes.go b/object/store/memory/write_bytes.go
deleted file mode 100644
index 241169d9..00000000
--- a/object/store/memory/write_bytes.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package memory
-
-import (
- "bytes"
-
- objectheader "codeberg.org/lindenii/furgit/object/header"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// WriteBytesContent writes one typed object content byte slice.
-func (store *Store) WriteBytesContent(ty objecttype.Type, content []byte) (objectid.ObjectID, error) {
- id := store.algo.Sum(buildRawObject(ty, content))
- store.objects[id] = storedObject{ty: ty, content: append([]byte(nil), content...)}
-
- return id, nil
-}
-
-// WriteBytesFull writes one full serialized object byte slice as "type size\0content".
-func (store *Store) WriteBytesFull(raw []byte) (objectid.ObjectID, error) {
- return store.WriteReaderFull(bytes.NewReader(raw))
-}
-
-func buildRawObject(ty objecttype.Type, body []byte) []byte {
- header, ok := objectheader.Encode(ty, int64(len(body)))
- if !ok {
- panic("failed to encode object header")
- }
-
- raw := make([]byte, len(header)+len(body))
- copy(raw, header)
- copy(raw[len(header):], body)
-
- return raw
-}
diff --git a/object/store/memory/write_reader.go b/object/store/memory/write_reader.go
deleted file mode 100644
index 0fa6a13f..00000000
--- a/object/store/memory/write_reader.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package memory
-
-import (
- "errors"
- "fmt"
- "io"
-
- objectheader "codeberg.org/lindenii/furgit/object/header"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// WriteReaderContent writes one typed object content stream.
-func (store *Store) WriteReaderContent(ty objecttype.Type, size int64, src io.Reader) (objectid.ObjectID, error) {
- if size < 0 {
- return objectid.ObjectID{}, fmt.Errorf("objectstore/memory: negative content size: %d", size)
- }
-
- content, err := io.ReadAll(io.LimitReader(src, size+1))
- if err != nil {
- return objectid.ObjectID{}, err
- }
-
- switch {
- case int64(len(content)) > size:
- return objectid.ObjectID{}, errors.New("objectstore/memory: object content longer than declared size")
- case int64(len(content)) < size:
- return objectid.ObjectID{}, errors.New("objectstore/memory: object content shorter than declared size")
- }
-
- return store.WriteBytesContent(ty, content)
-}
-
-// WriteReaderFull writes one full serialized object stream as "type size\0content".
-func (store *Store) WriteReaderFull(src io.Reader) (objectid.ObjectID, error) {
- raw, err := io.ReadAll(src)
- if err != nil {
- return objectid.ObjectID{}, err
- }
-
- ty, size, headerLen, ok := objectheader.Parse(raw)
- if !ok {
- return objectid.ObjectID{}, errors.New("objectstore/memory: malformed object header")
- }
-
- content := raw[headerLen:]
- if int64(len(content)) != size {
- return objectid.ObjectID{}, errors.New("objectstore/memory: object header size/content mismatch")
- }
-
- id := store.algo.Sum(raw)
- store.objects[id] = storedObject{ty: ty, content: append([]byte(nil), content...)}
-
- return id, nil
-}
diff --git a/object/store/memory/write_test.go b/object/store/memory/write_test.go
deleted file mode 100644
index 9f38a14b..00000000
--- a/object/store/memory/write_test.go
+++ /dev/null
@@ -1,192 +0,0 @@
-package memory_test
-
-import (
- "bytes"
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- objectheader "codeberg.org/lindenii/furgit/object/header"
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/store/memory"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-func TestStoreWriteReaderContent(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- store := memory.New(algo)
- content := []byte("memory-content\n")
-
- gotID, err := store.WriteReaderContent(objecttype.TypeBlob, int64(len(content)), bytes.NewReader(content))
- if err != nil {
- t.Fatalf("WriteReaderContent: %v", err)
- }
-
- wantID := algo.Sum(buildRawObject(t, objecttype.TypeBlob, content))
- if gotID != wantID {
- t.Fatalf("WriteReaderContent id = %s, want %s", gotID, wantID)
- }
-
- gotType, gotContent, err := store.ReadBytesContent(gotID)
- if err != nil {
- t.Fatalf("ReadBytesContent: %v", err)
- }
-
- if gotType != objecttype.TypeBlob {
- t.Fatalf("ReadBytesContent type = %v, want %v", gotType, objecttype.TypeBlob)
- }
-
- if !bytes.Equal(gotContent, content) {
- t.Fatalf("ReadBytesContent content mismatch")
- }
- })
-}
-
-func TestStoreWriteReaderFull(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- store := memory.New(algo)
- content := []byte("memory-full\n")
- raw := buildRawObject(t, objecttype.TypeBlob, content)
-
- gotID, err := store.WriteReaderFull(bytes.NewReader(raw))
- if err != nil {
- t.Fatalf("WriteReaderFull: %v", err)
- }
-
- wantID := algo.Sum(raw)
- if gotID != wantID {
- t.Fatalf("WriteReaderFull id = %s, want %s", gotID, wantID)
- }
-
- gotRaw, err := store.ReadBytesFull(gotID)
- if err != nil {
- t.Fatalf("ReadBytesFull: %v", err)
- }
-
- if !bytes.Equal(gotRaw, raw) {
- t.Fatalf("ReadBytesFull mismatch")
- }
- })
-}
-
-func TestStoreWriteBytes(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- store := memory.New(algo)
- content := []byte("memory-bytes\n")
-
- gotID, err := store.WriteBytesContent(objecttype.TypeBlob, content)
- if err != nil {
- t.Fatalf("WriteBytesContent: %v", err)
- }
-
- wantID := algo.Sum(buildRawObject(t, objecttype.TypeBlob, content))
- if gotID != wantID {
- t.Fatalf("WriteBytesContent id = %s, want %s", gotID, wantID)
- }
-
- raw := buildRawObject(t, objecttype.TypeBlob, content)
-
- gotID2, err := store.WriteBytesFull(raw)
- if err != nil {
- t.Fatalf("WriteBytesFull: %v", err)
- }
-
- if gotID2 != wantID {
- t.Fatalf("WriteBytesFull id = %s, want %s", gotID2, wantID)
- }
- })
-}
-
-func TestStoreWriteReaderValidationErrors(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- t.Run("content overflow", func(t *testing.T) {
- t.Parallel()
-
- store := memory.New(algo)
-
- _, err := store.WriteReaderContent(objecttype.TypeBlob, 1, bytes.NewReader([]byte("hello")))
- if err == nil {
- t.Fatalf("expected error after overflow")
- }
- })
-
- t.Run("content short", func(t *testing.T) {
- t.Parallel()
-
- store := memory.New(algo)
-
- _, err := store.WriteReaderContent(objecttype.TypeBlob, 5, bytes.NewReader([]byte("x")))
- if err == nil {
- t.Fatalf("expected error for short content")
- }
- })
-
- t.Run("full malformed header", func(t *testing.T) {
- t.Parallel()
-
- store := memory.New(algo)
-
- _, err := store.WriteReaderFull(bytes.NewReader([]byte("not-a-header")))
- if err == nil {
- t.Fatalf("expected error for malformed header")
- }
- })
-
- t.Run("full size mismatch", func(t *testing.T) {
- t.Parallel()
-
- store := memory.New(algo)
-
- _, err := store.WriteReaderFull(bytes.NewReader([]byte("blob 1\x00hello")))
- if err == nil {
- t.Fatalf("expected error after mismatch")
- }
- })
-
- t.Run("bytes malformed header", func(t *testing.T) {
- t.Parallel()
-
- store := memory.New(algo)
-
- _, err := store.WriteBytesFull([]byte("not-a-header"))
- if err == nil {
- t.Fatalf("expected error for malformed byte header")
- }
- })
- })
-}
-
-func TestBuildRawObjectMatchesObjectHeaderEncode(t *testing.T) {
- t.Parallel()
-
- content := []byte("body")
- raw := buildRawObject(t, objecttype.TypeBlob, content)
-
- header, ok := objectheader.Encode(objecttype.TypeBlob, int64(len(content)))
- if !ok {
- t.Fatalf("objectheader.Encode failed")
- }
-
- want := append(append([]byte(nil), header...), content...)
- if !bytes.Equal(raw, want) {
- t.Fatalf("buildRawObject mismatch")
- }
-}
-
-func buildRawObject(tb testing.TB, ty objecttype.Type, body []byte) []byte { //nolint:unparam
- tb.Helper()
-
- header, ok := objectheader.Encode(ty, int64(len(body)))
- if !ok {
- tb.Fatalf("objectheader.Encode(%v, %d) failed", ty, len(body))
- }
-
- raw := make([]byte, len(header)+len(body))
- copy(raw, header)
- copy(raw[len(header):], body)
-
- return raw
-}
diff --git a/object/store/mix/bytes.go b/object/store/mix/bytes.go
deleted file mode 100644
index 5b62ff06..00000000
--- a/object/store/mix/bytes.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package mix
-
-import (
- "errors"
- "fmt"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ReadBytesFull reads a full serialized object from one backend that has it.
-func (mix *Mix) ReadBytesFull(id objectid.ObjectID) ([]byte, error) {
- for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) {
- full, err := backend.ReadBytesFull(id)
- if err == nil {
- mix.touchBackend(backend)
-
- return full, nil
- }
-
- if errors.Is(err, objectstore.ErrObjectNotFound) {
- continue
- }
-
- return nil, fmt.Errorf("objectstore: backend %d read bytes full: %w", i, err)
- }
-
- return nil, objectstore.ErrObjectNotFound
-}
-
-// ReadBytesContent reads an object's type and content bytes from one backend
-// that has it.
-func (mix *Mix) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) {
- for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) {
- ty, content, err := backend.ReadBytesContent(id)
- if err == nil {
- mix.touchBackend(backend)
-
- return ty, content, nil
- }
-
- if errors.Is(err, objectstore.ErrObjectNotFound) {
- continue
- }
-
- return objecttype.TypeInvalid, nil, fmt.Errorf("objectstore: backend %d read bytes content: %w", i, err)
- }
-
- return objecttype.TypeInvalid, nil, objectstore.ErrObjectNotFound
-}
diff --git a/object/store/mix/header.go b/object/store/mix/header.go
deleted file mode 100644
index d57375ec..00000000
--- a/object/store/mix/header.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package mix
-
-import (
- "errors"
- "fmt"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ReadHeader reads object header data from one backend that has it.
-func (mix *Mix) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) {
- for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) {
- ty, size, err := backend.ReadHeader(id)
- if err == nil {
- mix.touchBackend(backend)
-
- return ty, size, nil
- }
-
- if errors.Is(err, objectstore.ErrObjectNotFound) {
- continue
- }
-
- return objecttype.TypeInvalid, 0, fmt.Errorf("objectstore: backend %d read header: %w", i, err)
- }
-
- return objecttype.TypeInvalid, 0, objectstore.ErrObjectNotFound
-}
diff --git a/object/store/mix/mix.go b/object/store/mix/mix.go
deleted file mode 100644
index 65ed97c8..00000000
--- a/object/store/mix/mix.go
+++ /dev/null
@@ -1,20 +0,0 @@
-// Package mix provides an adaptive wrapper over multiple object storage
-// backends.
-package mix
-
-import (
- "sync"
-
- objectstore "codeberg.org/lindenii/furgit/object/store"
-)
-
-// Mix queries multiple object databases with an MRU backend preference.
-//
-// Labels: Close-Caller.
-type Mix struct {
- mu sync.RWMutex
-
- backendHead *backendNode
- backendTail *backendNode
- backendNodeByStore map[objectstore.Reader]*backendNode
-}
diff --git a/object/store/mix/mru.go b/object/store/mix/mru.go
deleted file mode 100644
index b48f1448..00000000
--- a/object/store/mix/mru.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package mix
-
-import objectstore "codeberg.org/lindenii/furgit/object/store"
-
-type backendNode struct {
- backend objectstore.Reader
- prev *backendNode
- next *backendNode
-}
-
-//nolint:ireturn
-func (mix *Mix) firstBackend() objectstore.Reader {
- mix.mu.RLock()
- defer mix.mu.RUnlock()
-
- if mix.backendHead == nil {
- return nil
- }
-
- return mix.backendHead.backend
-}
-
-//nolint:ireturn
-func (mix *Mix) nextBackend(current objectstore.Reader) objectstore.Reader {
- mix.mu.RLock()
- defer mix.mu.RUnlock()
-
- node := mix.backendNodeByStore[current]
- if node == nil || node.next == nil {
- return nil
- }
-
- return node.next.backend
-}
-
-func (mix *Mix) touchBackend(backend objectstore.Reader) {
- if backend == nil {
- return
- }
-
- if !mix.mu.TryLock() {
- return
- }
- defer mix.mu.Unlock()
-
- node := mix.backendNodeByStore[backend]
- if node == nil || node == mix.backendHead {
- return
- }
-
- if node.prev != nil {
- node.prev.next = node.next
- }
-
- if node.next != nil {
- node.next.prev = node.prev
- }
-
- if mix.backendTail == node {
- mix.backendTail = node.prev
- }
-
- node.prev = nil
-
- node.next = mix.backendHead
- if mix.backendHead != nil {
- mix.backendHead.prev = node
- }
-
- mix.backendHead = node
- if mix.backendTail == nil {
- mix.backendTail = node
- }
-}
diff --git a/object/store/mix/new.go b/object/store/mix/new.go
deleted file mode 100644
index abc6c8ee..00000000
--- a/object/store/mix/new.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package mix
-
-import objectstore "codeberg.org/lindenii/furgit/object/store"
-
-// New creates a Mix from backends.
-//
-// The provided backends must be non-nil and distinct.
-//
-// Labels: Deps-Borrowed, Life-Parent.
-func New(backends ...objectstore.Reader) *Mix {
- nodeByStore := make(map[objectstore.Reader]*backendNode, len(backends))
-
- var (
- head *backendNode
- tail *backendNode
- )
-
- for _, backend := range backends {
- node := &backendNode{
- backend: backend,
- prev: tail,
- }
- if tail != nil {
- tail.next = node
- }
-
- if head == nil {
- head = node
- }
-
- tail = node
- nodeByStore[backend] = node
- }
-
- return &Mix{
- backendHead: head,
- backendTail: tail,
- backendNodeByStore: nodeByStore,
- }
-}
diff --git a/object/store/mix/reader.go b/object/store/mix/reader.go
deleted file mode 100644
index 8d515c50..00000000
--- a/object/store/mix/reader.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package mix
-
-import (
- "errors"
- "fmt"
- "io"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ReadReaderFull reads a full serialized object stream from one backend that
-// has it.
-func (mix *Mix) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) {
- for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) {
- reader, err := backend.ReadReaderFull(id)
- if err == nil {
- mix.touchBackend(backend)
-
- return reader, nil
- }
-
- if errors.Is(err, objectstore.ErrObjectNotFound) {
- continue
- }
-
- return nil, fmt.Errorf("objectstore: backend %d read reader full: %w", i, err)
- }
-
- return nil, objectstore.ErrObjectNotFound
-}
-
-// ReadReaderContent reads an object's type, declared content length, and
-// content stream from one backend that has it.
-func (mix *Mix) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) {
- for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) {
- ty, size, reader, err := backend.ReadReaderContent(id)
- if err == nil {
- mix.touchBackend(backend)
-
- return ty, size, reader, nil
- }
-
- if errors.Is(err, objectstore.ErrObjectNotFound) {
- continue
- }
-
- return objecttype.TypeInvalid, 0, nil, fmt.Errorf("objectstore: backend %d read reader content: %w", i, err)
- }
-
- return objecttype.TypeInvalid, 0, nil, objectstore.ErrObjectNotFound
-}
diff --git a/object/store/mix/refresh.go b/object/store/mix/refresh.go
deleted file mode 100644
index bbae6efe..00000000
--- a/object/store/mix/refresh.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package mix
-
-import (
- "errors"
-
- objectstore "codeberg.org/lindenii/furgit/object/store"
-)
-
-// Refresh forwards refresh calls to refresh-capable backends.
-func (mix *Mix) Refresh() error {
- mix.mu.RLock()
-
- backends := make([]objectstore.Reader, 0, len(mix.backendNodeByStore))
- for node := mix.backendHead; node != nil; node = node.next {
- backends = append(backends, node.backend)
- }
-
- mix.mu.RUnlock()
-
- var errs []error
-
- for _, backend := range backends {
- err := backend.Refresh()
- if err != nil {
- errs = append(errs, err)
- }
- }
-
- return errors.Join(errs...)
-}
diff --git a/object/store/mix/size.go b/object/store/mix/size.go
deleted file mode 100644
index 4feb142e..00000000
--- a/object/store/mix/size.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package mix
-
-import (
- "errors"
- "fmt"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
-)
-
-// ReadSize reads object content length from one backend that has it.
-func (mix *Mix) ReadSize(id objectid.ObjectID) (int64, error) {
- for i, backend := 0, mix.firstBackend(); backend != nil; i, backend = i+1, mix.nextBackend(backend) {
- size, err := backend.ReadSize(id)
- if err == nil {
- mix.touchBackend(backend)
-
- return size, nil
- }
-
- if errors.Is(err, objectstore.ErrObjectNotFound) {
- continue
- }
-
- return 0, fmt.Errorf("objectstore: backend %d read size: %w", i, err)
- }
-
- return 0, objectstore.ErrObjectNotFound
-}
diff --git a/object/store/packed/doc.go b/object/store/packed/doc.go
deleted file mode 100644
index 55189aa1..00000000
--- a/object/store/packed/doc.go
+++ /dev/null
@@ -1,3 +0,0 @@
-// Package packed provides Git object reading from, and pack writing to,
-// an objects/pack directory.
-package packed
diff --git a/object/store/packed/internal/doc.go b/object/store/packed/internal/doc.go
deleted file mode 100644
index 05a9c2be..00000000
--- a/object/store/packed/internal/doc.go
+++ /dev/null
@@ -1,6 +0,0 @@
-// Package internal encapsulates packed store implementation details.
-//
-// We have separate internal subpackages for ingest vs read and such,
-// because these operations are so different that they almost share
-// no code. This makes things clearer.
-package internal
diff --git a/object/store/packed/internal/ingest/TODO b/object/store/packed/internal/ingest/TODO
deleted file mode 100644
index bfb722c1..00000000
--- a/object/store/packed/internal/ingest/TODO
+++ /dev/null
@@ -1 +0,0 @@
-multi-threaded delta resolution and index computation?
diff --git a/object/store/packed/internal/ingest/byteslice_reader.go b/object/store/packed/internal/ingest/byteslice_reader.go
deleted file mode 100644
index a1570ef3..00000000
--- a/object/store/packed/internal/ingest/byteslice_reader.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package ingest
-
-import "io"
-
-// byteSliceReader implements io.ByteReader on []byte.
-type byteSliceReader struct {
- data []byte
- pos int
-}
-
-// ReadByte reads one byte from receiver.
-func (reader *byteSliceReader) ReadByte() (byte, error) {
- if reader.pos >= len(reader.data) {
- return 0, io.EOF
- }
-
- b := reader.data[reader.pos]
- reader.pos++
-
- return b, nil
-}
diff --git a/object/store/packed/internal/ingest/cache.go b/object/store/packed/internal/ingest/cache.go
deleted file mode 100644
index 9a15f55f..00000000
--- a/object/store/packed/internal/ingest/cache.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package ingest
-
-import (
- "codeberg.org/lindenii/furgit/internal/lru"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// deltaBaseCacheKey identifies one resolved base by record index.
-type deltaBaseCacheKey struct {
- recordIdx int
-}
-
-// deltaBaseCacheValue stores one resolved base object payload.
-type deltaBaseCacheValue struct {
- realType objecttype.Type
- content []byte
-}
-
-// deltaBaseCache is a bounded LRU for resolved base payloads.
-type deltaBaseCache struct {
- lru *lru.Cache[deltaBaseCacheKey, deltaBaseCacheValue]
-}
-
-// newDeltaBaseCache creates one bounded base cache.
-func newDeltaBaseCache(maxBytes int64) *deltaBaseCache {
- return &deltaBaseCache{
- lru: lru.New(
- maxBytes,
- func(_ deltaBaseCacheKey, value deltaBaseCacheValue) int64 {
- return int64(len(value.content))
- },
- nil,
- ),
- }
-}
-
-// get returns one cache entry for recordIdx.
-func (cache *deltaBaseCache) get(recordIdx int) (objecttype.Type, []byte, bool) {
- value, ok := cache.lru.Get(deltaBaseCacheKey{recordIdx: recordIdx})
- if !ok {
- return objecttype.TypeInvalid, nil, false
- }
-
- return value.realType, value.content, true
-}
-
-// add stores one cache entry for recordIdx.
-func (cache *deltaBaseCache) add(recordIdx int, realType objecttype.Type, content []byte) {
- cache.lru.Add(deltaBaseCacheKey{recordIdx: recordIdx}, deltaBaseCacheValue{
- realType: realType,
- content: content,
- })
-}
diff --git a/object/store/packed/internal/ingest/counting_writer.go b/object/store/packed/internal/ingest/counting_writer.go
deleted file mode 100644
index 051ad9d1..00000000
--- a/object/store/packed/internal/ingest/counting_writer.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package ingest
-
-import "io"
-
-// countingWriter counts bytes written to dst.
-type countingWriter struct {
- dst io.Writer
- n int
-}
-
-// Write writes src to dst and tracks output byte count.
-func (writer *countingWriter) Write(src []byte) (int, error) {
- n, err := writer.dst.Write(src)
- writer.n += n
-
- return n, err
-}
diff --git a/object/store/packed/internal/ingest/crc.go b/object/store/packed/internal/ingest/crc.go
deleted file mode 100644
index f55af4ff..00000000
--- a/object/store/packed/internal/ingest/crc.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package ingest
-
-import "fmt"
-
-// beginEntryCRC starts inline CRC accumulation for one packed entry.
-func (scanner *streamScanner) beginEntryCRC() {
- scanner.entryCRC = 0
- scanner.inEntryCRC = true
-}
-
-// endEntryCRC finishes inline CRC accumulation for one packed entry.
-func (scanner *streamScanner) endEntryCRC() (uint32, error) {
- if !scanner.inEntryCRC {
- return 0, fmt.Errorf("packfile/ingest: entry CRC not started")
- }
-
- crc := scanner.entryCRC
- scanner.entryCRC = 0
- scanner.inEntryCRC = false
-
- return crc, nil
-}
diff --git a/object/store/packed/internal/ingest/delta_header.go b/object/store/packed/internal/ingest/delta_header.go
deleted file mode 100644
index 110cf83b..00000000
--- a/object/store/packed/internal/ingest/delta_header.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package ingest
-
-import deltaapply "codeberg.org/lindenii/furgit/format/packfile/delta/apply"
-
-// finalizeStreamPackHash consumes trailer bytes and verifies stream integrity.
-// readDeltaHeaderSizes reads source and destination sizes from one delta payload.
-func readDeltaHeaderSizes(payload []byte) (int, int, error) {
- reader := &byteSliceReader{data: payload}
-
- return deltaapply.ReadHeaderSizes(reader)
-}
diff --git a/object/store/packed/internal/ingest/distance.go b/object/store/packed/internal/ingest/distance.go
deleted file mode 100644
index 9bc4d886..00000000
--- a/object/store/packed/internal/ingest/distance.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package ingest
-
-import (
- "fmt"
- "io"
-)
-
-// readOfsDistanceFromStream reads one ofs-delta encoded distance.
-func readOfsDistanceFromStream(reader io.ByteReader) (uint64, int, error) {
- first, err := reader.ReadByte()
- if err != nil {
- return 0, 0, fmt.Errorf("read ofs distance first byte: %w", err)
- }
-
- dist := uint64(first & 0x7f)
- consumed := 1
-
- b := first
- for b&0x80 != 0 {
- b, err = reader.ReadByte()
- if err != nil {
- return 0, 0, fmt.Errorf("read ofs distance continuation: %w", err)
- }
-
- consumed++
- dist = ((dist + 1) << 7) + uint64(b&0x7f)
- }
-
- return dist, consumed, nil
-}
diff --git a/object/store/packed/internal/ingest/doc.go b/object/store/packed/internal/ingest/doc.go
deleted file mode 100644
index 074012de..00000000
--- a/object/store/packed/internal/ingest/doc.go
+++ /dev/null
@@ -1,3 +0,0 @@
-// Package ingest implements streaming ingestion of one Git pack stream into a
-// packed destination root, producing .pack/.idx and optionally .rev.
-package ingest
diff --git a/object/store/packed/internal/ingest/drain.go b/object/store/packed/internal/ingest/drain.go
deleted file mode 100644
index 7179a823..00000000
--- a/object/store/packed/internal/ingest/drain.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package ingest
-
-import (
- "fmt"
- "io"
-
- "codeberg.org/lindenii/furgit/internal/compress/zlib"
- objectheader "codeberg.org/lindenii/furgit/object/header"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// drainEntryPayload inflates one entry payload from stream and returns
-// (inflatedLength, oidForBaseEntry).
-func drainEntryPayload(state *ingestState, record objectRecord) (int64, objectid.ObjectID, error) {
- var zero objectid.ObjectID
-
- reader, err := zlib.NewReader(state.stream)
- if err != nil {
- return 0, zero, &MalformedPackEntryError{Offset: record.offset, Reason: fmt.Sprintf("open zlib stream: %v", err)}
- }
-
- defer func() { _ = reader.Close() }()
-
- var total int64
-
- if record.packedType.IsBaseObject() {
- header, ok := objectheader.Encode(record.packedType, record.declaredSize)
- if !ok {
- return 0, zero, &MalformedPackEntryError{Offset: record.offset, Reason: "encode object header"}
- }
-
- hashImpl, err := state.algo.New()
- if err != nil {
- return 0, zero, err
- }
-
- _, _ = hashImpl.Write(header)
-
- n, err := io.Copy(hashImpl, reader)
- if err != nil {
- return 0, zero, &MalformedPackEntryError{Offset: record.offset, Reason: fmt.Sprintf("inflate base object: %v", err)}
- }
-
- total = n
-
- oid, err := objectid.FromBytes(state.algo, hashImpl.Sum(nil))
- if err != nil {
- return 0, zero, err
- }
-
- return total, oid, nil
- }
-
- if record.packedType == objecttype.TypeOfsDelta || record.packedType == objecttype.TypeRefDelta {
- n, err := io.Copy(io.Discard, reader)
- if err != nil {
- return 0, zero, &MalformedPackEntryError{Offset: record.offset, Reason: fmt.Sprintf("inflate delta payload: %v", err)}
- }
-
- total = n
-
- return total, zero, nil
- }
-
- return 0, zero, &MalformedPackEntryError{Offset: record.offset, Reason: "unsupported payload type"}
-}
diff --git a/object/store/packed/internal/ingest/entry.go b/object/store/packed/internal/ingest/entry.go
deleted file mode 100644
index 363e213c..00000000
--- a/object/store/packed/internal/ingest/entry.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package ingest
-
-import (
- "fmt"
-
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// scanOneEntry scans one pack entry from stream and appends one record.
-func scanOneEntry(state *ingestState, startOffset uint64) (uint64, error) {
- state.stream.beginEntryCRC()
-
- record, err := parseEntryPrefix(state, startOffset)
- if err != nil {
- return 0, err
- }
-
- payloadStartConsumed := state.stream.consumed
-
- contentLen, oid, err := drainEntryPayload(state, record)
- if err != nil {
- return 0, err
- }
-
- consumedInput := state.stream.consumed - payloadStartConsumed
-
- if contentLen != record.declaredSize {
- return 0, &MalformedPackEntryError{
- Offset: startOffset,
- Reason: fmt.Sprintf("inflated size mismatch got %d want %d", contentLen, record.declaredSize),
- }
- }
-
- endOffset := startOffset + uint64(record.headerLen) + consumedInput
- if endOffset > state.stream.consumed {
- return 0, &MalformedPackEntryError{
- Offset: startOffset,
- Reason: fmt.Sprintf("entry end offset overflow got %d > stream %d", endOffset, state.stream.consumed),
- }
- }
-
- record.packedLen = endOffset - startOffset
-
- record.dataOffset = startOffset + uint64(record.headerLen)
- if record.packedLen < uint64(record.headerLen) {
- return 0, &MalformedPackEntryError{Offset: startOffset, Reason: "negative payload span"}
- }
-
- crc, err := state.stream.endEntryCRC()
- if err != nil {
- return 0, err
- }
-
- record.crc32 = crc
-
- if record.packedType.IsBaseObject() {
- record.objectID = oid
- record.realType = record.packedType
- record.resolved = true
- }
-
- recordIdx := len(state.records)
- state.records = append(state.records, record)
-
- state.offsetToRecord[record.offset] = recordIdx
- if record.resolved {
- state.objectToRecord[record.objectID] = recordIdx
- }
-
- switch record.packedType {
- case objecttype.TypeOfsDelta:
- state.ofsDeltas = append(state.ofsDeltas, ofsDeltaRef{
- baseOffset: record.baseOffset,
- recordIdx: recordIdx,
- })
- case objecttype.TypeRefDelta:
- state.refDeltas = append(state.refDeltas, refDeltaRef{
- baseObject: record.baseObject,
- recordIdx: recordIdx,
- })
- case objecttype.TypeInvalid,
- objecttype.TypeCommit,
- objecttype.TypeTree,
- objecttype.TypeBlob,
- objecttype.TypeTag,
- objecttype.TypeFuture:
- default:
- }
-
- return endOffset, nil
-}
diff --git a/object/store/packed/internal/ingest/entry_header.go b/object/store/packed/internal/ingest/entry_header.go
deleted file mode 100644
index c74fdc16..00000000
--- a/object/store/packed/internal/ingest/entry_header.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package ingest
-
-import (
- "codeberg.org/lindenii/furgit/internal/intconv"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// encodePackEntryHeader encodes one non-delta packed entry header.
-func encodePackEntryHeader(ty objecttype.Type, size int64) []byte {
- var out [16]byte
-
- n := 0
-
- s, err := intconv.Int64ToUint64(size)
- if err != nil {
- panic(err)
- }
-
- c := (uint8(ty) << 4) | byte(s&0x0f)
-
- s >>= 4
- for s != 0 {
- out[n] = c | 0x80
- n++
- c = byte(s & 0x7f)
- s >>= 7
- }
-
- out[n] = c
- n++
-
- return append([]byte(nil), out[:n]...)
-}
diff --git a/object/store/packed/internal/ingest/entry_prefix.go b/object/store/packed/internal/ingest/entry_prefix.go
deleted file mode 100644
index a107b4e8..00000000
--- a/object/store/packed/internal/ingest/entry_prefix.go
+++ /dev/null
@@ -1,95 +0,0 @@
-package ingest
-
-import (
- "fmt"
-
- "codeberg.org/lindenii/furgit/internal/intconv"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// parseEntryPrefix parses one entry prefix from stream.
-func parseEntryPrefix(state *ingestState, startOffset uint64) (objectRecord, error) {
- var record objectRecord
-
- record.offset = startOffset
-
- first, err := state.stream.ReadByte()
- if err != nil {
- return record, &MalformedPackEntryError{Offset: startOffset, Reason: fmt.Sprintf("read first header byte: %v", err)}
- }
-
- record.packedType = objecttype.Type((first >> 4) & 0x07)
- size := int64(first & 0x0f)
- headerLen := uint32(1)
- shift := uint(4)
- b := first
-
- for b&0x80 != 0 {
- b, err = state.stream.ReadByte()
- if err != nil {
- return record, &MalformedPackEntryError{Offset: startOffset, Reason: fmt.Sprintf("read size continuation: %v", err)}
- }
-
- headerLen++
- size |= int64(b&0x7f) << shift
- shift += 7
- }
-
- if size < 0 {
- return record, &MalformedPackEntryError{Offset: startOffset, Reason: "negative declared size"}
- }
-
- record.declaredSize = size
-
- switch record.packedType {
- case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag:
- case objecttype.TypeRefDelta:
- baseRaw := make([]byte, state.algo.Size())
-
- err := state.stream.readFull(baseRaw)
- if err != nil {
- return record, &MalformedPackEntryError{Offset: startOffset, Reason: fmt.Sprintf("read ref base: %v", err)}
- }
-
- baseID, err := objectid.FromBytes(state.algo, baseRaw)
- if err != nil {
- return record, &MalformedPackEntryError{Offset: startOffset, Reason: fmt.Sprintf("parse ref base: %v", err)}
- }
-
- record.baseObject = baseID
-
- baseRawLen, err := intconv.IntToUint32(len(baseRaw))
- if err != nil {
- return record, err
- }
-
- headerLen += baseRawLen
- case objecttype.TypeOfsDelta:
- dist, consumed, err := readOfsDistanceFromStream(state.stream)
- if err != nil {
- return record, &MalformedPackEntryError{Offset: startOffset, Reason: err.Error()}
- }
-
- if startOffset <= dist {
- return record, &MalformedPackEntryError{Offset: startOffset, Reason: "ofs base offset out of bounds"}
- }
-
- record.baseOffset = startOffset - dist
-
- consumedUint32, err := intconv.IntToUint32(consumed)
- if err != nil {
- return record, err
- }
-
- headerLen += consumedUint32
- case objecttype.TypeInvalid, objecttype.TypeFuture:
- return record, &MalformedPackEntryError{Offset: startOffset, Reason: fmt.Sprintf("unsupported object type %d", record.packedType)}
- default:
- return record, &MalformedPackEntryError{Offset: startOffset, Reason: fmt.Sprintf("unsupported object type %d", record.packedType)}
- }
-
- record.headerLen = headerLen
-
- return record, nil
-}
diff --git a/object/store/packed/internal/ingest/errors.go b/object/store/packed/internal/ingest/errors.go
deleted file mode 100644
index cbad1e77..00000000
--- a/object/store/packed/internal/ingest/errors.go
+++ /dev/null
@@ -1,68 +0,0 @@
-package ingest
-
-import (
- "errors"
- "fmt"
-)
-
-// InvalidPackHeaderError reports an invalid or unsupported pack header.
-type InvalidPackHeaderError struct {
- Reason string
-}
-
-// Error implements error.
-func (err *InvalidPackHeaderError) Error() string {
- return "packfile/ingest: invalid pack header: " + err.Reason
-}
-
-// PackTrailerMismatchError reports a mismatch between computed and trailer pack hash.
-type PackTrailerMismatchError struct{}
-
-// Error implements error.
-func (err *PackTrailerMismatchError) Error() string {
- return "packfile/ingest: pack trailer hash mismatch"
-}
-
-// ThinPackUnresolvedError reports unresolved REF deltas when fixThin is disabled
-// or when required bases cannot be found in base.
-type ThinPackUnresolvedError struct {
- Count int
-}
-
-// Error implements error.
-func (err *ThinPackUnresolvedError) Error() string {
- return fmt.Sprintf("packfile/ingest: unresolved thin deltas: %d", err.Count)
-}
-
-// MalformedPackEntryError reports malformed entry encoding at one pack offset.
-type MalformedPackEntryError struct {
- Offset uint64
- Reason string
-}
-
-// Error implements error.
-func (err *MalformedPackEntryError) Error() string {
- return fmt.Sprintf("packfile/ingest: malformed pack entry at offset %d: %s", err.Offset, err.Reason)
-}
-
-// DeltaCycleError reports a detected cycle in delta dependency resolution.
-type DeltaCycleError struct {
- Offset uint64
-}
-
-// Error implements error.
-func (err *DeltaCycleError) Error() string {
- return fmt.Sprintf("packfile/ingest: delta cycle detected at offset %d", err.Offset)
-}
-
-// DestinationWriteError reports destination I/O failures.
-type DestinationWriteError struct {
- Op string
-}
-
-// Error implements error.
-func (err *DestinationWriteError) Error() string {
- return "packfile/ingest: destination write failure: " + err.Op
-}
-
-var errExternalThinBase = errors.New("packfile/ingest: external thin base required")
diff --git a/object/store/packed/internal/ingest/file_section_writer.go b/object/store/packed/internal/ingest/file_section_writer.go
deleted file mode 100644
index fa28c1a9..00000000
--- a/object/store/packed/internal/ingest/file_section_writer.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package ingest
-
-import "os"
-
-// fileSectionWriter writes sequentially to file via WriteAt at one base offset.
-type fileSectionWriter struct {
- file *os.File
- off int64
- pos int64
-}
-
-// Write writes src at current section position.
-func (writer *fileSectionWriter) Write(src []byte) (int, error) {
- if len(src) == 0 {
- return 0, nil
- }
-
- n, err := writer.file.WriteAt(src, writer.off+writer.pos)
- writer.pos += int64(n)
-
- return n, err
-}
diff --git a/object/store/packed/internal/ingest/fill.go b/object/store/packed/internal/ingest/fill.go
deleted file mode 100644
index eca4e4d6..00000000
--- a/object/store/packed/internal/ingest/fill.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package ingest
-
-import (
- "errors"
- "fmt"
- "io"
-)
-
-// fill ensures at least min unread bytes are available in receiver's buffer.
-func (scanner *streamScanner) fill(minLen int) error {
- if minLen <= 0 {
- return nil
- }
-
- if minLen > len(scanner.buf) {
- return fmt.Errorf("packfile/ingest: fill(%d) exceeds scanner buffer", minLen)
- }
-
- for scanner.n-scanner.off < minLen {
- err := scanner.flushConsumedPrefix()
- if err != nil {
- return err
- }
-
- readN, err := scanner.src.Read(scanner.buf[scanner.n:])
- if readN > 0 {
- scanner.n += readN
- }
-
- if err != nil {
- if errors.Is(err, io.EOF) && scanner.n-scanner.off >= minLen {
- return nil
- }
-
- return err
- }
-
- if readN == 0 {
- return io.ErrNoProgress
- }
- }
-
- return nil
-}
diff --git a/object/store/packed/internal/ingest/finalize.go b/object/store/packed/internal/ingest/finalize.go
deleted file mode 100644
index 6fe4edb2..00000000
--- a/object/store/packed/internal/ingest/finalize.go
+++ /dev/null
@@ -1,94 +0,0 @@
-package ingest
-
-import (
- "errors"
- "fmt"
- "io/fs"
- "strings"
-
- "codeberg.org/lindenii/furgit/internal/intconv"
-)
-
-// finalizeArtifacts links temporary files to final names and returns Result.
-func finalizeArtifacts(state *ingestState) (Result, error) {
- base := "pack-" + state.packHash.String()
- packFinal := base + ".pack"
- idxFinal := base + ".idx"
-
- revFinal := ""
- if state.opts.WriteRev {
- revFinal = base + ".rev"
- }
-
- err := linkTempToFinal(state, state.packTmpName, packFinal)
- if err != nil {
- return Result{}, err
- }
-
- err = linkTempToFinal(state, state.idxTmpName, idxFinal)
- if err != nil {
- return Result{}, err
- }
-
- if state.opts.WriteRev {
- err := linkTempToFinal(state, state.revTmpName, revFinal)
- if err != nil {
- return Result{}, err
- }
- }
-
- objectCount, err := intconv.IntToUint32(len(state.records))
- if err != nil {
- return Result{}, err
- }
-
- return Result{
- PackName: packFinal,
- IdxName: idxFinal,
- RevName: revFinal,
- PackHash: state.packHash,
- ObjectCount: objectCount,
- ThinFixed: state.thinFixed,
- }, nil
-}
-
-// rollbackTemporaryArtifacts removes temporary files after failure.
-func rollbackTemporaryArtifacts(state *ingestState) {
- if state.packTmpName != "" {
- _ = state.destination.Remove(state.packTmpName)
- }
-
- if state.idxTmpName != "" {
- _ = state.destination.Remove(state.idxTmpName)
- }
-
- if state.revTmpName != "" {
- _ = state.destination.Remove(state.revTmpName)
- }
-}
-
-// linkTempToFinal hard-links tmp to final, tolerating existing final paths.
-func linkTempToFinal(state *ingestState, tmp, final string) error {
- if tmp == "" || final == "" {
- return fmt.Errorf("packfile/ingest: invalid finalize names tmp=%q final=%q", tmp, final)
- }
-
- if strings.Contains(final, "/") {
- return fmt.Errorf("packfile/ingest: final name must be leaf: %q", final)
- }
-
- err := state.destination.Link(tmp, final)
- if err == nil {
- _ = state.destination.Remove(tmp)
-
- return nil
- }
-
- if errors.Is(err, fs.ErrExist) {
- _ = state.destination.Remove(tmp)
-
- return nil
- }
-
- return err
-}
diff --git a/object/store/packed/internal/ingest/flush.go b/object/store/packed/internal/ingest/flush.go
deleted file mode 100644
index 96753170..00000000
--- a/object/store/packed/internal/ingest/flush.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package ingest
-
-import "fmt"
-
-// flush writes all consumed-but-unflushed bytes to destination pack file.
-func (scanner *streamScanner) flush() error {
- return scanner.flushConsumedPrefix()
-}
-
-// flushConsumedPrefix writes scanner.buf[:scanner.off] and compacts unread
-// bytes to the start of buffer.
-func (scanner *streamScanner) flushConsumedPrefix() error {
- if scanner.off == 0 {
- return nil
- }
-
- written := 0
- for written < scanner.off {
- n, err := scanner.dstFile.Write(scanner.buf[written:scanner.off])
- if err != nil {
- return &DestinationWriteError{Op: fmt.Sprintf("write pack: %v", err)}
- }
-
- if n == 0 {
- return &DestinationWriteError{Op: "write pack: short write"}
- }
-
- written += n
- }
-
- unread := scanner.n - scanner.off
- copy(scanner.buf[:unread], scanner.buf[scanner.off:scanner.n])
- scanner.off = 0
- scanner.n = unread
-
- return nil
-}
diff --git a/object/store/packed/internal/ingest/hash.go b/object/store/packed/internal/ingest/hash.go
deleted file mode 100644
index 4b739c20..00000000
--- a/object/store/packed/internal/ingest/hash.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package ingest
-
-import (
- "fmt"
-
- objectheader "codeberg.org/lindenii/furgit/object/header"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// hashCanonicalObject hashes canonical object bytes (header+content).
-func hashCanonicalObject(algo objectid.Algorithm, ty objecttype.Type, content []byte) (objectid.ObjectID, error) {
- header, ok := objectheader.Encode(ty, int64(len(content)))
- if !ok {
- return objectid.ObjectID{}, fmt.Errorf("packfile/ingest: encode object header for type %d", ty)
- }
-
- hashImpl, err := algo.New()
- if err != nil {
- return objectid.ObjectID{}, err
- }
-
- _, _ = hashImpl.Write(header)
- _, _ = hashImpl.Write(content)
-
- return objectid.FromBytes(algo, hashImpl.Sum(nil))
-}
diff --git a/object/store/packed/internal/ingest/header.go b/object/store/packed/internal/ingest/header.go
deleted file mode 100644
index 6b90becc..00000000
--- a/object/store/packed/internal/ingest/header.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package ingest
-
-import (
- "encoding/binary"
- "fmt"
- "io"
-
- "codeberg.org/lindenii/furgit/format/packfile"
-)
-
-const packHeaderSize = 12
-
-type packHeader struct {
- Version uint32
- ObjectCount uint32
-}
-
-// readAndValidatePackHeader reads one PACK header from src and validates it.
-func readAndValidatePackHeader(src io.Reader) (packHeader, [packHeaderSize]byte, error) {
- var hdr [packHeaderSize]byte
-
- _, err := io.ReadFull(src, hdr[:])
- if err != nil {
- return packHeader{}, [packHeaderSize]byte{}, &InvalidPackHeaderError{
- Reason: fmt.Sprintf("read header: %v", err),
- }
- }
-
- header, err := parseAndValidatePackHeader(hdr)
- if err != nil {
- return packHeader{}, [packHeaderSize]byte{}, err
- }
-
- return header, hdr, nil
-}
-
-// parseAndValidatePackHeader validates one already-read PACK header.
-func parseAndValidatePackHeader(hdr [packHeaderSize]byte) (packHeader, error) {
- if binary.BigEndian.Uint32(hdr[:4]) != packfile.Signature {
- return packHeader{}, &InvalidPackHeaderError{Reason: "signature mismatch"}
- }
-
- version := binary.BigEndian.Uint32(hdr[4:8])
- if !packfile.SupportedVersion(version) {
- return packHeader{}, &InvalidPackHeaderError{
- Reason: fmt.Sprintf("unsupported version %d", version),
- }
- }
-
- return packHeader{
- Version: version,
- ObjectCount: binary.BigEndian.Uint32(hdr[8:12]),
- }, nil
-}
diff --git a/object/store/packed/internal/ingest/idx_write.go b/object/store/packed/internal/ingest/idx_write.go
deleted file mode 100644
index fa139264..00000000
--- a/object/store/packed/internal/ingest/idx_write.go
+++ /dev/null
@@ -1,262 +0,0 @@
-package ingest
-
-import (
- "bytes"
- "encoding/binary"
- "fmt"
- "hash"
- "io"
- "slices"
-
- "codeberg.org/lindenii/furgit/internal/intconv"
- "codeberg.org/lindenii/furgit/internal/progress"
-)
-
-const (
- idxMagicV2 = 0xff744f63
- idxVersionV2 = 2
-)
-
-// writeIdx writes idx v2 for resolved records.
-func writeIdx(state *ingestState) error {
- order := buildIdxOrder(state)
-
- hashImpl, err := state.algo.New()
- if err != nil {
- return err
- }
-
- write := func(src []byte) error {
- _, writeErr := state.idxFile.Write(src)
- if writeErr != nil {
- return writeErr
- }
-
- _, writeErr = hashImpl.Write(src)
- if writeErr != nil {
- return writeErr
- }
-
- return nil
- }
-
- var (
- scratch [8]byte
- fanout [256]uint32
- )
-
- writeProgressf(state, "writing index fanout...\r")
-
- for _, recordIdx := range order {
- idRaw := state.records[recordIdx].objectID.Bytes()
- fanout[idRaw[0]]++
- }
-
- binary.BigEndian.PutUint32(scratch[:4], idxMagicV2)
- binary.BigEndian.PutUint32(scratch[4:8], idxVersionV2)
-
- err = write(scratch[:8])
- if err != nil {
- return err
- }
-
- var cumulative uint32
- for i := range fanout {
- cumulative += fanout[i]
- binary.BigEndian.PutUint32(scratch[:4], cumulative)
-
- err := write(scratch[:4])
- if err != nil {
- return err
- }
- }
-
- writeProgressf(state, "writing index fanout: done.\n")
-
- largeOffsetCount := 0
-
- for idx := range state.records {
- if state.records[idx].offset >= 0x80000000 {
- largeOffsetCount++
- }
- }
-
- oidMeter := progress.New(progress.Options{
- Writer: state.opts.Progress,
- Title: "writing index object ids",
- Total: uint64(len(order)),
- })
-
- var oidDone uint64
-
- for _, recordIdx := range order {
- idRaw := state.records[recordIdx].objectID.Bytes()
-
- err := write(idRaw)
- if err != nil {
- return err
- }
-
- oidDone++
- oidMeter.Set(oidDone, 0)
- }
-
- if oidDone > 0 {
- oidMeter.Stop("done")
- }
-
- crcMeter := progress.New(progress.Options{
- Writer: state.opts.Progress,
- Title: "writing index crc32",
- Total: uint64(len(order)),
- })
-
- var crcDone uint64
-
- for _, recordIdx := range order {
- binary.BigEndian.PutUint32(scratch[:4], state.records[recordIdx].crc32)
-
- err := write(scratch[:4])
- if err != nil {
- return err
- }
-
- crcDone++
- crcMeter.Set(crcDone, 0)
- }
-
- if crcDone > 0 {
- crcMeter.Stop("done")
- }
-
- largeOffsets := make([]uint64, 0)
- offsetMeter := progress.New(progress.Options{
- Writer: state.opts.Progress,
- Title: "writing index offsets",
- Total: uint64(len(order)),
- })
-
- var offsetDone uint64
-
- for _, recordIdx := range order {
- offset := state.records[recordIdx].offset
- if offset >= 0x80000000 {
- largeOffsetIdx, err := intconv.IntToUint32(len(largeOffsets))
- if err != nil {
- return err
- }
-
- word := 0x80000000 | largeOffsetIdx
-
- largeOffsets = append(largeOffsets, offset)
-
- binary.BigEndian.PutUint32(scratch[:4], word)
- } else {
- binary.BigEndian.PutUint32(scratch[:4], uint32(offset))
- }
-
- err := write(scratch[:4])
- if err != nil {
- return err
- }
-
- offsetDone++
- offsetMeter.Set(offsetDone, 0)
- }
-
- if offsetDone > 0 {
- offsetMeter.Stop("done")
- }
-
- total, err := intconv.IntToUint64(largeOffsetCount)
- if err != nil {
- return err
- }
-
- largeOffsetMeter := progress.New(progress.Options{
- Writer: state.opts.Progress,
- Title: "writing index large offsets",
- Total: total,
- })
-
- var largeOffsetDone uint64
-
- for _, off := range largeOffsets {
- binary.BigEndian.PutUint64(scratch[:8], off)
-
- err := write(scratch[:8])
- if err != nil {
- return err
- }
-
- largeOffsetDone++
- largeOffsetMeter.Set(largeOffsetDone, 0)
- }
-
- if largeOffsetDone > 0 {
- largeOffsetMeter.Stop("done")
- }
-
- writeProgressf(state, "writing index trailer...\r")
-
- err = write(state.packHash.Bytes())
- if err != nil {
- return err
- }
-
- idxHash := hashImpl.Sum(nil)
-
- _, err = state.idxFile.Write(idxHash)
- if err != nil {
- return err
- }
-
- err = state.idxFile.Sync()
- if err != nil {
- return err
- }
-
- writeProgressf(state, "writing index trailer: done.\n")
-
- return nil
-}
-
-// buildIdxOrder returns record indexes sorted by ObjectID.
-func buildIdxOrder(state *ingestState) []int {
- out := make([]int, 0, len(state.records))
- for idx := range state.records {
- out = append(out, idx)
- }
-
- slices.SortFunc(out, func(a, b int) int {
- return bytes.Compare(state.records[a].objectID.Bytes(), state.records[b].objectID.Bytes())
- })
-
- return out
-}
-
-// verifyResolvedRecords checks that all records are fully resolved before index writing.
-func verifyResolvedRecords(state *ingestState) error {
- for idx, record := range state.records {
- if !record.resolved {
- return fmt.Errorf("packfile/ingest: unresolved record %d at offset %d", idx, record.offset)
- }
- }
-
- return nil
-}
-
-// writeAndHash writes src to dst and updates hash.
-func writeAndHash(dst io.Writer, hashImpl hash.Hash, src []byte) error {
- _, err := dst.Write(src)
- if err != nil {
- return err
- }
-
- _, err = hashImpl.Write(src)
- if err != nil {
- return err
- }
-
- return nil
-}
diff --git a/object/store/packed/internal/ingest/ingest.go b/object/store/packed/internal/ingest/ingest.go
deleted file mode 100644
index be65ff5f..00000000
--- a/object/store/packed/internal/ingest/ingest.go
+++ /dev/null
@@ -1,68 +0,0 @@
-package ingest
-
-import (
- "fmt"
-)
-
-// ingest initializes transaction state and executes the ingest pipeline.
-func ingest(state *ingestState) (out Result, err error) {
- err = openTemporaryArtifacts(state)
- if err != nil {
- return Result{}, err
- }
-
- defer func() {
- _ = closeTemporaryArtifacts(state)
- if err != nil {
- rollbackTemporaryArtifacts(state)
- }
- }()
-
- err = streamPackAndScan(state)
- if err != nil {
- return Result{}, err
- }
-
- err = resolveAll(state)
- if err != nil {
- return Result{}, err
- }
-
- err = maybeFixThin(state)
- if err != nil {
- return Result{}, err
- }
-
- if state.thinFixed {
- err = resolveAll(state)
- if err != nil {
- return Result{}, err
- }
- }
-
- if len(state.unresolvedRefDeltas) > 0 {
- return Result{}, &ThinPackUnresolvedError{Count: len(state.unresolvedRefDeltas)}
- }
-
- err = verifyResolvedRecords(state)
- if err != nil {
- return Result{}, err
- }
-
- err = state.packFile.Sync()
- if err != nil {
- return Result{}, &DestinationWriteError{Op: fmt.Sprintf("sync pack: %v", err)}
- }
-
- err = writeIdx(state)
- if err != nil {
- return Result{}, err
- }
-
- err = writeRev(state)
- if err != nil {
- return Result{}, err
- }
-
- return finalizeArtifacts(state)
-}
diff --git a/object/store/packed/internal/ingest/ingest_test.go b/object/store/packed/internal/ingest/ingest_test.go
deleted file mode 100644
index c99afe65..00000000
--- a/object/store/packed/internal/ingest/ingest_test.go
+++ /dev/null
@@ -1,411 +0,0 @@
-package ingest_test
-
-import (
- "bytes"
- "encoding/binary"
- "errors"
- "io/fs"
- "os"
- "path/filepath"
- "strings"
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/store/packed/internal/ingest"
-)
-
-type noExtraReadReader struct {
- reader *bytes.Reader
-}
-
-func (r *noExtraReadReader) Read(p []byte) (int, error) {
- if r.reader.Len() == 0 {
- return 0, errors.New("unexpected extra read after pack trailer")
- }
-
- return r.reader.Read(p)
-}
-
-// fixturePath returns one fixture file path for the selected algorithm.
-func fixturePath(t *testing.T, algo objectid.Algorithm, name string) string {
- t.Helper()
-
- dir := algo.String()
- if dir == "" {
- t.Fatalf("unsupported fixture algorithm: %v", algo)
- }
-
- return filepath.Join("testdata", "fixtures", dir, name)
-}
-
-// fixtureBytes reads one fixture file fully.
-func fixtureBytes(t *testing.T, algo objectid.Algorithm, name string) []byte {
- t.Helper()
-
- path := fixturePath(t, algo, name)
- dir := filepath.Dir(path)
- base := filepath.Base(path)
-
- root, err := os.OpenRoot(dir)
- if err != nil {
- t.Fatalf("open fixture root %q: %v", dir, err)
- }
-
- defer func() {
- err := root.Close()
- if err != nil {
- t.Fatalf("close fixture root %q: %v", dir, err)
- }
- }()
-
- data, err := root.ReadFile(base)
- if err != nil {
- t.Fatalf("read fixture %q: %v", base, err)
- }
-
- return data
-}
-
-// fixtureMetadata parses key=value metadata for one algorithm fixture set.
-func fixtureMetadata(t *testing.T, algo objectid.Algorithm) map[string]string {
- t.Helper()
-
- data := fixtureBytes(t, algo, "METADATA.txt")
-
- out := make(map[string]string)
- for line := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") {
- line = strings.TrimSpace(line)
- if line == "" {
- continue
- }
-
- key, value, ok := strings.Cut(line, "=")
- if !ok {
- t.Fatalf("invalid fixture metadata line %q", line)
- }
-
- out[strings.TrimSpace(key)] = strings.TrimSpace(value)
- }
-
- return out
-}
-
-// fixtureOID returns one fixture metadata object ID value.
-func fixtureOID(t *testing.T, algo objectid.Algorithm, key string) objectid.ObjectID {
- t.Helper()
-
- meta := fixtureMetadata(t, algo)
-
- hex, ok := meta[key]
- if !ok {
- t.Fatalf("missing fixture metadata key %q", key)
- }
-
- id, err := objectid.ParseHex(algo, hex)
- if err != nil {
- t.Fatalf("parse fixture metadata oid %q: %v", hex, err)
- }
-
- return id
-}
-
-// verifyReindexOracle regenerates idx/rev with upstream git index-pack and
-// compares bytes with files produced by ingest.
-func verifyReindexOracle(t *testing.T, repo *testgit.TestRepo, packName, idxName, revName string) {
- t.Helper()
-
- oracleDir := t.TempDir()
- oracleIdxPath := filepath.Join(oracleDir, "oracle.idx")
- _ = repo.Run(t, "index-pack", "--rev-index", "-o", oracleIdxPath, filepath.Join("objects", "pack", packName))
- oracleRevPath := strings.TrimSuffix(oracleIdxPath, ".idx") + ".rev"
-
- packRoot := repo.OpenPackRoot(t)
-
- gotIdx, err := packRoot.ReadFile(idxName)
- if err != nil {
- t.Fatalf("read idx: %v", err)
- }
-
- oracleRoot, err := os.OpenRoot(oracleDir)
- if err != nil {
- t.Fatalf("open oracle root: %v", err)
- }
-
- defer func() {
- err := oracleRoot.Close()
- if err != nil {
- t.Fatalf("close oracle root: %v", err)
- }
- }()
-
- wantIdx, err := oracleRoot.ReadFile(filepath.Base(oracleIdxPath))
- if err != nil {
- t.Fatalf("read oracle idx: %v", err)
- }
-
- if !bytes.Equal(gotIdx, wantIdx) {
- t.Fatal("idx bytes differ from git index-pack output")
- }
-
- gotRev, err := packRoot.ReadFile(revName)
- if err != nil {
- t.Fatalf("read rev: %v", err)
- }
-
- wantRev, err := oracleRoot.ReadFile(filepath.Base(oracleRevPath))
- if err != nil {
- t.Fatalf("read oracle rev: %v", err)
- }
-
- if !bytes.Equal(gotRev, wantRev) {
- t.Fatal("rev bytes differ from git index-pack output")
- }
-}
-
-func TestIngestNonThinPackWritesPackIdxRev(t *testing.T) {
- t.Parallel()
-
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- head := fixtureOID(t, algo, "head")
- packBytes := fixtureBytes(t, algo, "nonthin.pack")
-
- receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
-
- packRoot := receiver.OpenPackRoot(t)
-
- result, err := ingest.WritePack(packRoot, algo, bytes.NewReader(packBytes), ingest.Options{
- WriteRev: true,
- RequireTrailingEOF: true,
- })
- if err != nil {
- t.Fatalf("Ingest: %v", err)
- }
-
- if result.ThinFixed {
- t.Fatalf("ThinFixed = true, want false")
- }
-
- if result.RevName == "" {
- t.Fatal("RevName is empty")
- }
-
- _, err = packRoot.Stat(result.PackName)
- if err != nil {
- t.Fatalf("stat pack: %v", err)
- }
-
- _, err = packRoot.Stat(result.IdxName)
- if err != nil {
- t.Fatalf("stat idx: %v", err)
- }
-
- _, err = packRoot.Stat(result.RevName)
- if err != nil {
- t.Fatalf("stat rev: %v", err)
- }
-
- _ = receiver.Run(t, "verify-pack", "-v", filepath.Join("objects", "pack", result.IdxName))
- verifyReindexOracle(t, receiver, result.PackName, result.IdxName, result.RevName)
-
- receiver.UpdateRef(t, "refs/heads/main", head)
- _ = receiver.Run(t, "fsck", "--full", "--strict", "--no-progress", "--no-dangling")
- })
-}
-
-func TestIngestThinPackWithoutFixReturnsUnresolved(t *testing.T) {
- t.Parallel()
-
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- thinPack := fixtureBytes(t, algo, "thin.pack")
-
- receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- packRoot := receiver.OpenPackRoot(t)
-
- _, err := ingest.WritePack(packRoot, algo, bytes.NewReader(thinPack), ingest.Options{
- WriteRev: true,
- RequireTrailingEOF: true,
- })
- if err == nil {
- t.Fatal("Ingest error = nil, want error")
- }
-
- if _, ok := errors.AsType[*ingest.ThinPackUnresolvedError](err); !ok {
- t.Fatalf("Ingest error type = %T (%v), want *ThinPackUnresolvedError", err, err)
- }
-
- entries, err := fs.ReadDir(packRoot.FS(), ".")
- if err != nil {
- t.Fatalf("ReadDir(pack): %v", err)
- }
-
- for _, entry := range entries {
- if strings.HasSuffix(entry.Name(), ".pack") {
- t.Fatalf("found finalized pack file after failure: %v", entry.Name())
- }
- }
- })
-}
-
-func TestIngestThinPackWithFixThin(t *testing.T) {
- t.Parallel()
-
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- head := fixtureOID(t, algo, "head")
- basePack := fixtureBytes(t, algo, "base.pack")
- thinPack := fixtureBytes(t, algo, "thin.pack")
- receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
-
- packRoot := receiver.OpenPackRoot(t)
-
- _, err := ingest.WritePack(packRoot, algo, bytes.NewReader(basePack), ingest.Options{
- RequireTrailingEOF: true,
- })
- if err != nil {
- t.Fatalf("ingest base pack: %v", err)
- }
-
- receiverRepo := receiver.OpenRepository(t)
-
- result, err := ingest.WritePack(packRoot, algo, bytes.NewReader(thinPack), ingest.Options{
- FixThin: true,
- WriteRev: true,
- ThinBase: receiverRepo.Objects(),
- RequireTrailingEOF: true,
- })
- if err != nil {
- t.Fatalf("Ingest(thin): %v", err)
- }
-
- if !result.ThinFixed {
- t.Fatal("ThinFixed = false, want true")
- }
-
- _ = receiver.Run(t, "verify-pack", "-v", filepath.Join("objects", "pack", result.IdxName))
- verifyReindexOracle(t, receiver, result.PackName, result.IdxName, result.RevName)
- receiver.UpdateRef(t, "refs/heads/main", head)
- _ = receiver.Run(t, "fsck", "--full", "--strict", "--no-progress", "--no-dangling")
- })
-}
-
-func TestIngestPackTrailerMismatch(t *testing.T) {
- t.Parallel()
-
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- packBytes := fixtureBytes(t, algo, "nonthin.pack")
- if len(packBytes) == 0 {
- t.Fatal("empty pack stream")
- }
-
- packBytes[len(packBytes)-1] ^= 0xff
-
- receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- packRoot := receiver.OpenPackRoot(t)
-
- _, err := ingest.WritePack(packRoot, algo, bytes.NewReader(packBytes), ingest.Options{
- WriteRev: true,
- RequireTrailingEOF: true,
- })
- if err == nil {
- t.Fatal("Ingest error = nil, want error")
- }
-
- if _, ok := errors.AsType[*ingest.PackTrailerMismatchError](err); !ok {
- t.Fatalf("Ingest error type = %T (%v), want *PackTrailerMismatchError", err, err)
- }
-
- entries, err := fs.ReadDir(packRoot.FS(), ".")
- if err != nil {
- t.Fatalf("ReadDir(pack): %v", err)
- }
-
- for _, entry := range entries {
- if strings.HasSuffix(entry.Name(), ".pack") {
- t.Fatalf("found finalized pack file after failure: %v", entry.Name())
- }
- }
- })
-}
-
-func zeroObjectPackBytes(t *testing.T, algo objectid.Algorithm) []byte {
- t.Helper()
-
- hashImpl, err := algo.New()
- if err != nil {
- t.Fatalf("algo.New: %v", err)
- }
-
- var header [12]byte
- copy(header[:4], []byte{'P', 'A', 'C', 'K'})
- binary.BigEndian.PutUint32(header[4:8], 2)
- binary.BigEndian.PutUint32(header[8:12], 0)
-
- _, _ = hashImpl.Write(header[:])
-
- return append(header[:], hashImpl.Sum(nil)...)
-}
-
-func TestIngestZeroObjectPackIsDiscardedInternally(t *testing.T) {
- t.Parallel()
-
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- packBytes := zeroObjectPackBytes(t, algo)
- receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- packRoot := receiver.OpenPackRoot(t)
-
- result, err := ingest.WritePack(packRoot, algo, bytes.NewReader(packBytes), ingest.Options{
- RequireTrailingEOF: true,
- })
- if err != nil {
- t.Fatalf("WritePack: %v", err)
- }
-
- if result.ObjectCount != 0 {
- t.Fatalf("ObjectCount = %d, want 0", result.ObjectCount)
- }
-
- if result.PackName != "" {
- t.Fatalf("PackName = %q, want empty", result.PackName)
- }
-
- if result.IdxName != "" {
- t.Fatalf("IdxName = %q, want empty", result.IdxName)
- }
-
- if result.RevName != "" {
- t.Fatalf("RevName = %q, want empty", result.RevName)
- }
-
- entries, err := fs.ReadDir(packRoot.FS(), ".")
- if err != nil {
- t.Fatalf("ReadDir(pack): %v", err)
- }
-
- if len(entries) != 0 {
- t.Fatalf("unexpected files after zero-object pack: %d", len(entries))
- }
- })
-}
-
-func TestIngestCanFinishWithoutTrailingEOF(t *testing.T) {
- t.Parallel()
-
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- head := fixtureOID(t, algo, "head")
- packBytes := fixtureBytes(t, algo, "nonthin.pack")
-
- receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- packRoot := receiver.OpenPackRoot(t)
-
- result, err := ingest.WritePack(packRoot, algo, &noExtraReadReader{reader: bytes.NewReader(packBytes)}, ingest.Options{
- WriteRev: true,
- })
- if err != nil {
- t.Fatalf("Ingest without trailing EOF: %v", err)
- }
-
- receiver.UpdateRef(t, "refs/heads/main", head)
- _ = receiver.Run(t, "verify-pack", "-v", filepath.Join("objects", "pack", result.IdxName))
- _ = receiver.Run(t, "fsck", "--full", "--strict", "--no-progress", "--no-dangling")
- })
-}
diff --git a/object/store/packed/internal/ingest/options.go b/object/store/packed/internal/ingest/options.go
deleted file mode 100644
index 06c334c0..00000000
--- a/object/store/packed/internal/ingest/options.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package ingest
-
-import (
- "codeberg.org/lindenii/furgit/common/iowrap"
- objectstore "codeberg.org/lindenii/furgit/object/store"
-)
-
-// Options controls one pack ingest operation.
-type Options struct {
- // FixThin appends missing local bases for thin packs.
- FixThin bool
- // WriteRev writes a .rev alongside the .pack and .idx.
- WriteRev bool
- // ThinBase supplies existing objects for thin-pack fixup.
- ThinBase objectstore.Reader
- // Progress receives human-readable progress messages.
- //
- // When nil, no progress output is emitted.
- Progress iowrap.WriteFlusher
- // RequireTrailingEOF requires the source to hit EOF after the pack trailer.
- //
- // This is suitable for exact pack-file readers, but should be disabled for
- // full-duplex transport streams like receive-pack where the peer keeps the
- // connection open to read the server response.
- RequireTrailingEOF bool
-}
diff --git a/object/store/packed/internal/ingest/progress_write.go b/object/store/packed/internal/ingest/progress_write.go
deleted file mode 100644
index afb39305..00000000
--- a/object/store/packed/internal/ingest/progress_write.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package ingest
-
-import "codeberg.org/lindenii/furgit/internal/utils"
-
-func writeProgressf(state *ingestState, format string, args ...any) {
- utils.BestEffortFprintf(state.opts.Progress, format, args...)
-
- if state.opts.Progress != nil {
- _ = state.opts.Progress.Flush()
- }
-}
diff --git a/object/store/packed/internal/ingest/record_content.go b/object/store/packed/internal/ingest/record_content.go
deleted file mode 100644
index c66a1234..00000000
--- a/object/store/packed/internal/ingest/record_content.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package ingest
-
-import (
- "fmt"
-
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// readBaseRecordContent reads canonical base content for one non-delta record.
-func readBaseRecordContent(state *ingestState, idx int) (objecttype.Type, []byte, error) {
- record := state.records[idx]
- if !record.packedType.IsBaseObject() {
- return objecttype.TypeInvalid, nil, fmt.Errorf("packfile/ingest: record %d is not a base object", idx)
- }
-
- content, err := inflateRecordPayload(state, idx)
- if err != nil {
- return objecttype.TypeInvalid, nil, err
- }
-
- if int64(len(content)) != record.declaredSize {
- return objecttype.TypeInvalid, nil, &MalformedPackEntryError{
- Offset: record.offset,
- Reason: fmt.Sprintf("base content size mismatch got %d want %d", len(content), record.declaredSize),
- }
- }
-
- return record.packedType, content, nil
-}
diff --git a/object/store/packed/internal/ingest/record_delta.go b/object/store/packed/internal/ingest/record_delta.go
deleted file mode 100644
index bc40367f..00000000
--- a/object/store/packed/internal/ingest/record_delta.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package ingest
-
-import (
- "fmt"
-
- deltaapply "codeberg.org/lindenii/furgit/format/packfile/delta/apply"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// applyDeltaRecord applies one delta record onto base content.
-func applyDeltaRecord(state *ingestState, idx int, baseType objecttype.Type, baseContent []byte) (objecttype.Type, []byte, error) {
- record := state.records[idx]
- if record.packedType != objecttype.TypeOfsDelta && record.packedType != objecttype.TypeRefDelta {
- return objecttype.TypeInvalid, nil, fmt.Errorf("packfile/ingest: record %d is not a delta record", idx)
- }
-
- deltaPayload, err := inflateRecordPayload(state, idx)
- if err != nil {
- return objecttype.TypeInvalid, nil, err
- }
-
- if int64(len(deltaPayload)) != record.declaredSize {
- return objecttype.TypeInvalid, nil, &MalformedPackEntryError{
- Offset: record.offset,
- Reason: fmt.Sprintf("delta payload size mismatch got %d want %d", len(deltaPayload), record.declaredSize),
- }
- }
-
- srcSize, dstSize, err := readDeltaHeaderSizes(deltaPayload)
- if err != nil {
- return objecttype.TypeInvalid, nil, &MalformedPackEntryError{
- Offset: record.offset,
- Reason: fmt.Sprintf("read delta header: %v", err),
- }
- }
-
- if srcSize != len(baseContent) {
- return objecttype.TypeInvalid, nil, &MalformedPackEntryError{
- Offset: record.offset,
- Reason: fmt.Sprintf("delta source size mismatch got %d want %d", srcSize, len(baseContent)),
- }
- }
-
- content, err := deltaapply.Apply(baseContent, deltaPayload)
- if err != nil {
- return objecttype.TypeInvalid, nil, &MalformedPackEntryError{
- Offset: record.offset,
- Reason: fmt.Sprintf("apply delta: %v", err),
- }
- }
-
- if len(content) != dstSize {
- return objecttype.TypeInvalid, nil, &MalformedPackEntryError{
- Offset: record.offset,
- Reason: fmt.Sprintf("delta result size mismatch got %d want %d", len(content), dstSize),
- }
- }
-
- return baseType, content, nil
-}
diff --git a/object/store/packed/internal/ingest/record_inflate.go b/object/store/packed/internal/ingest/record_inflate.go
deleted file mode 100644
index b8eca25b..00000000
--- a/object/store/packed/internal/ingest/record_inflate.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package ingest
-
-import (
- "compress/zlib"
- "fmt"
- "io"
-
- "codeberg.org/lindenii/furgit/internal/intconv"
-)
-
-// inflateRecordPayload inflates one record's zlib payload from pack file.
-func inflateRecordPayload(state *ingestState, idx int) ([]byte, error) {
- record := state.records[idx]
- if record.packedLen < uint64(record.headerLen) {
- return nil, &MalformedPackEntryError{Offset: record.offset, Reason: "entry packed span underflow"}
- }
-
- compressedOffset := record.offset + uint64(record.headerLen)
- compressedLen := record.packedLen - uint64(record.headerLen)
-
- compressedOffsetInt64, err := intconv.Uint64ToInt64(compressedOffset)
- if err != nil {
- return nil, err
- }
-
- compressedLenInt64, err := intconv.Uint64ToInt64(compressedLen)
- if err != nil {
- return nil, err
- }
-
- section := io.NewSectionReader(state.packFile, compressedOffsetInt64, compressedLenInt64)
-
- reader, err := zlib.NewReader(section)
- if err != nil {
- return nil, &MalformedPackEntryError{Offset: record.offset, Reason: fmt.Sprintf("open payload zlib: %v", err)}
- }
-
- defer func() { _ = reader.Close() }()
-
- out, err := io.ReadAll(reader)
- if err != nil {
- return nil, &MalformedPackEntryError{Offset: record.offset, Reason: fmt.Sprintf("inflate payload: %v", err)}
- }
-
- return out, nil
-}
diff --git a/object/store/packed/internal/ingest/record_resolve.go b/object/store/packed/internal/ingest/record_resolve.go
deleted file mode 100644
index 7a9471dc..00000000
--- a/object/store/packed/internal/ingest/record_resolve.go
+++ /dev/null
@@ -1,116 +0,0 @@
-package ingest
-
-import (
- "fmt"
-
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// resolveRecord resolves one record and returns canonical type/content.
-func resolveRecord(state *ingestState, idx int, visiting map[int]struct{}) (objecttype.Type, []byte, error) {
- if idx < 0 || idx >= len(state.records) {
- return objecttype.TypeInvalid, nil, fmt.Errorf("packfile/ingest: record index out of bounds")
- }
-
- if _, ok := visiting[idx]; ok {
- return objecttype.TypeInvalid, nil, &DeltaCycleError{Offset: state.records[idx].offset}
- }
-
- visiting[idx] = struct{}{}
- defer delete(visiting, idx)
-
- record := &state.records[idx]
- if ty, content, ok := state.baseCache.get(idx); ok {
- return ty, content, nil
- }
-
- if record.packedType.IsBaseObject() {
- ty, content, err := readBaseRecordContent(state, idx)
- if err != nil {
- return objecttype.TypeInvalid, nil, err
- }
-
- if record.resolved {
- state.baseCache.add(idx, record.realType, content)
-
- return record.realType, content, nil
- }
-
- id, err := hashCanonicalObject(state.algo, ty, content)
- if err != nil {
- return objecttype.TypeInvalid, nil, err
- }
-
- record.objectID = id
- record.realType = ty
- record.resolved = true
- state.objectToRecord[id] = idx
- state.baseCache.add(idx, ty, content)
-
- return ty, content, nil
- }
-
- var (
- baseType objecttype.Type
- baseContent []byte
- err error
- )
- switch record.packedType {
- case objecttype.TypeOfsDelta:
- baseIdx, ok := state.offsetToRecord[record.baseOffset]
- if !ok {
- return objecttype.TypeInvalid, nil, &MalformedPackEntryError{
- Offset: record.offset,
- Reason: "missing ofs-delta base entry",
- }
- }
-
- baseType, baseContent, err = resolveRecord(state, baseIdx, visiting)
- if err != nil {
- return objecttype.TypeInvalid, nil, err
- }
- case objecttype.TypeRefDelta:
- baseIdx, ok := state.objectToRecord[record.baseObject]
- if ok {
- baseType, baseContent, err = resolveRecord(state, baseIdx, visiting)
- if err != nil {
- return objecttype.TypeInvalid, nil, err
- }
- } else {
- return objecttype.TypeInvalid, nil, errExternalThinBase
- }
- case objecttype.TypeInvalid,
- objecttype.TypeCommit,
- objecttype.TypeTree,
- objecttype.TypeBlob,
- objecttype.TypeTag,
- objecttype.TypeFuture:
- return objecttype.TypeInvalid, nil, &MalformedPackEntryError{
- Offset: record.offset,
- Reason: "unsupported delta type",
- }
- default:
- return objecttype.TypeInvalid, nil, &MalformedPackEntryError{
- Offset: record.offset,
- Reason: "unsupported delta type",
- }
- }
-
- ty, content, err := applyDeltaRecord(state, idx, baseType, baseContent)
- if err != nil {
- return objecttype.TypeInvalid, nil, err
- }
-
- id, err := hashCanonicalObject(state.algo, ty, content)
- if err != nil {
- return objecttype.TypeInvalid, nil, err
- }
-
- record.objectID = id
- record.realType = ty
- record.resolved = true
- state.objectToRecord[id] = idx
- state.baseCache.add(idx, ty, content)
-
- return ty, content, nil
-}
diff --git a/object/store/packed/internal/ingest/records.go b/object/store/packed/internal/ingest/records.go
deleted file mode 100644
index 75f157fa..00000000
--- a/object/store/packed/internal/ingest/records.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package ingest
-
-import (
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// objectRecord stores metadata for one packed object entry.
-type objectRecord struct {
- // offset is the entry start offset in the pack file.
- offset uint64
- // headerLen is packed entry header length in bytes.
- headerLen uint32
- // packedLen is total packed entry length in bytes.
- packedLen uint64
- // crc32 is the CRC over the full packed entry.
- crc32 uint32
- // packedType is the entry type tag from the pack stream.
- packedType objecttype.Type
- // realType is canonical object type after delta resolution.
- realType objecttype.Type
- // declaredSize is the declared output object size for this entry.
- declaredSize int64
- // dataOffset is compressed payload start offset for this entry.
- dataOffset uint64
- // baseOffset is OFS base offset when packedType is OFS delta.
- baseOffset uint64
- // baseObject is REF base object ID when packedType is REF delta.
- baseObject objectid.ObjectID
- // objectID is final resolved object ID.
- objectID objectid.ObjectID
- // resolved reports whether objectID/realType are finalized.
- resolved bool
-}
-
-// ofsDeltaRef maps one OFS delta record to its base offset.
-type ofsDeltaRef struct {
- baseOffset uint64
- recordIdx int
-}
-
-// refDeltaRef maps one REF delta record to its base object ID.
-type refDeltaRef struct {
- baseObject objectid.ObjectID
- recordIdx int
-}
diff --git a/object/store/packed/internal/ingest/resolve_all.go b/object/store/packed/internal/ingest/resolve_all.go
deleted file mode 100644
index 90464015..00000000
--- a/object/store/packed/internal/ingest/resolve_all.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package ingest
-
-import (
- "errors"
-
- "codeberg.org/lindenii/furgit/internal/progress"
-)
-
-// resolveAll resolves all delta records and finalizes ObjectID/RealType for every record.
-func resolveAll(state *ingestState) error {
- state.unresolvedRefDeltas = state.unresolvedRefDeltas[:0]
-
- var pending uint32
-
- for idx := range state.records {
- if !state.records[idx].resolved {
- pending++
- }
- }
-
- if pending == 0 {
- return nil
- }
-
- var done uint32
-
- meter := progress.New(progress.Options{
- Writer: state.opts.Progress,
- Title: "resolving deltas",
- Total: uint64(pending),
- })
-
- for idx := range state.records {
- if state.records[idx].resolved {
- continue
- }
-
- done++
- meter.Set(uint64(done), 0)
-
- visiting := make(map[int]struct{})
-
- ty, content, err := resolveRecord(state, idx, visiting)
- if err != nil {
- if errors.Is(err, errExternalThinBase) {
- state.unresolvedRefDeltas = append(state.unresolvedRefDeltas, idx)
-
- continue
- }
-
- return err
- }
-
- id, err := hashCanonicalObject(state.algo, ty, content)
- if err != nil {
- return err
- }
-
- record := &state.records[idx]
- record.realType = ty
- record.objectID = id
- record.resolved = true
- state.objectToRecord[id] = idx
- state.baseCache.add(idx, ty, content)
- }
-
- meter.Stop("done")
-
- return nil
-}
diff --git a/object/store/packed/internal/ingest/result.go b/object/store/packed/internal/ingest/result.go
deleted file mode 100644
index 9a285f09..00000000
--- a/object/store/packed/internal/ingest/result.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package ingest
-
-import objectid "codeberg.org/lindenii/furgit/object/id"
-
-// Result describes one successful ingest transaction.
-type Result struct {
- // PackName is the destination-relative filename of the written .pack.
- PackName string
- // IdxName is the destination-relative filename of the written .idx.
- IdxName string
- // RevName is the destination-relative filename of the written .rev.
- //
- // RevName is empty when writeRev is false.
- RevName string
- // PackHash is the final pack hash (same hash embedded in .idx/.rev trailers).
- PackHash objectid.ObjectID
- // ObjectCount is the final object count in the resulting pack.
- //
- // If thin fixup appends objects, this includes appended base objects.
- ObjectCount uint32
- // ThinFixed reports whether thin fixup appended local bases.
- ThinFixed bool
-}
diff --git a/object/store/packed/internal/ingest/rev_write.go b/object/store/packed/internal/ingest/rev_write.go
deleted file mode 100644
index 16d27085..00000000
--- a/object/store/packed/internal/ingest/rev_write.go
+++ /dev/null
@@ -1,137 +0,0 @@
-package ingest
-
-import (
- "encoding/binary"
- "slices"
-
- "codeberg.org/lindenii/furgit/internal/intconv"
- "codeberg.org/lindenii/furgit/internal/progress"
-)
-
-const (
- revMagic = 0x52494458
- revVersion = 1
-)
-
-// writeRev writes rev index for resolved records.
-func writeRev(state *ingestState) error {
- if !state.opts.WriteRev {
- return nil
- }
-
- idxOrder := buildIdxOrder(state)
-
- recordToIdxPos := make([]int, len(state.records))
- for pos, recordIdx := range idxOrder {
- recordToIdxPos[recordIdx] = pos
- }
-
- packOrder := buildPackOrder(state)
-
- hashImpl, err := state.algo.New()
- if err != nil {
- return err
- }
-
- var scratch [8]byte
-
- writeProgressf(state, "writing reverse index header...\r")
- binary.BigEndian.PutUint32(scratch[:4], revMagic)
-
- err = writeAndHash(state.revFile, hashImpl, scratch[:4])
- if err != nil {
- return err
- }
-
- binary.BigEndian.PutUint32(scratch[:4], revVersion)
-
- err = writeAndHash(state.revFile, hashImpl, scratch[:4])
- if err != nil {
- return err
- }
-
- binary.BigEndian.PutUint32(scratch[:4], state.algo.PackHashID())
-
- err = writeAndHash(state.revFile, hashImpl, scratch[:4])
- if err != nil {
- return err
- }
-
- writeProgressf(state, "writing reverse index header: done.\n")
-
- entriesMeter := progress.New(progress.Options{
- Writer: state.opts.Progress,
- Title: "writing reverse index entries",
- Total: uint64(len(packOrder)),
- })
-
- var entriesDone uint64
-
- for _, recordIdx := range packOrder {
- recordPos, err := intconv.IntToUint32(recordToIdxPos[recordIdx])
- if err != nil {
- return err
- }
-
- binary.BigEndian.PutUint32(scratch[:4], recordPos)
-
- err = writeAndHash(state.revFile, hashImpl, scratch[:4])
- if err != nil {
- return err
- }
-
- entriesDone++
- entriesMeter.Set(entriesDone, 0)
- }
-
- if entriesDone > 0 {
- entriesMeter.Stop("done")
- }
-
- writeProgressf(state, "writing reverse index trailer...\r")
-
- err = writeAndHash(state.revFile, hashImpl, state.packHash.Bytes())
- if err != nil {
- return err
- }
-
- revHash := hashImpl.Sum(nil)
-
- _, err = state.revFile.Write(revHash)
- if err != nil {
- return err
- }
-
- err = state.revFile.Sync()
- if err != nil {
- return err
- }
-
- writeProgressf(state, "writing reverse index trailer: done.\n")
-
- return nil
-}
-
-// buildPackOrder returns record indexes sorted by pack offset.
-func buildPackOrder(state *ingestState) []int {
- out := make([]int, 0, len(state.records))
- for idx := range state.records {
- out = append(out, idx)
- }
-
- slices.SortFunc(out, func(a, b int) int {
- offA := state.records[a].offset
-
- offB := state.records[b].offset
- switch {
- case offA < offB:
- return -1
- case offA > offB:
- return 1
- default:
- return 0
- }
- })
-
- return out
-}
diff --git a/object/store/packed/internal/ingest/rewrite_header_trailer.go b/object/store/packed/internal/ingest/rewrite_header_trailer.go
deleted file mode 100644
index f1f18a39..00000000
--- a/object/store/packed/internal/ingest/rewrite_header_trailer.go
+++ /dev/null
@@ -1,89 +0,0 @@
-package ingest
-
-import (
- "encoding/binary"
- "io"
-
- "codeberg.org/lindenii/furgit/internal/intconv"
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-// rewritePackHeaderAndTrailer rewrites object count and trailer hash using ReadAt/WriteAt.
-func rewritePackHeaderAndTrailer(state *ingestState) error {
- var countRaw [4]byte
-
- recordCountUint32, err := intconv.IntToUint32(len(state.records))
- if err != nil {
- return err
- }
-
- binary.BigEndian.PutUint32(countRaw[:], recordCountUint32)
-
- _, err = state.packFile.WriteAt(countRaw[:], 8)
- if err != nil {
- return err
- }
-
- info, err := state.packFile.Stat()
- if err != nil {
- return err
- }
-
- endWithoutTrailer := info.Size()
-
- hashImpl, err := state.algo.New()
- if err != nil {
- return err
- }
-
- var (
- buf [128 << 10]byte
- pos int64
- )
- for pos < endWithoutTrailer {
- want := int64(len(buf))
-
- remaining := endWithoutTrailer - pos
- if remaining < want {
- want = remaining
- }
-
- n, err := state.packFile.ReadAt(buf[:want], pos)
- if err != nil && err != io.EOF {
- return err
- }
-
- if n == 0 {
- return io.ErrUnexpectedEOF
- }
-
- _, _ = hashImpl.Write(buf[:n])
- pos += int64(n)
- }
-
- sum := hashImpl.Sum(nil)
-
- _, err = state.packFile.WriteAt(sum, endWithoutTrailer)
- if err != nil {
- return err
- }
-
- packHash, err := objectid.FromBytes(state.algo, sum)
- if err != nil {
- return err
- }
-
- state.packHash = packHash
- state.objectCountHeader = recordCountUint32
-
- sumLenInt64 := int64(len(sum))
-
- newConsumed, err := intconv.Int64ToUint64(endWithoutTrailer + sumLenInt64)
- if err != nil {
- return err
- }
-
- state.stream.consumed = newConsumed
-
- return nil
-}
diff --git a/object/store/packed/internal/ingest/scan.go b/object/store/packed/internal/ingest/scan.go
deleted file mode 100644
index ddd1eaf3..00000000
--- a/object/store/packed/internal/ingest/scan.go
+++ /dev/null
@@ -1,105 +0,0 @@
-package ingest
-
-import (
- "fmt"
-
- "codeberg.org/lindenii/furgit/internal/progress"
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-// streamPackAndScan copies src into temp .pack while scanning packed entries.
-func streamPackAndScan(state *ingestState) error {
- hashImpl, err := state.algo.New()
- if err != nil {
- return err
- }
-
- state.stream = newStreamScanner(
- state.src,
- state.packFile,
- hashImpl,
- state.algo.Size(),
- )
-
- writeProgressf(state, "validating pack header...\r")
-
- err = seedStreamWithPackHeader(state)
- if err != nil {
- return err
- }
-
- writeProgressf(state, "validating pack header: done.\n")
-
- state.records = make([]objectRecord, 0, state.objectCountHeader)
- state.ofsDeltas = make([]ofsDeltaRef, 0, state.objectCountHeader)
- state.refDeltas = make([]refDeltaRef, 0, state.objectCountHeader)
-
- total := state.objectCountHeader
- meter := progress.New(progress.Options{
- Writer: state.opts.Progress,
- Title: "receiving objects",
- Total: uint64(total),
- Throughput: true,
- })
-
- for i := range total {
- nextOffset, err := scanOneEntry(state, state.stream.consumed)
- if err != nil {
- return err
- }
-
- if nextOffset != state.stream.consumed {
- return fmt.Errorf("packfile/ingest: internal stream offset mismatch")
- }
-
- done := i + 1
- meter.Set(uint64(done), state.stream.consumed)
- }
-
- meter.Stop("done")
-
- err = state.stream.finishAndFlushTrailer(state.opts.RequireTrailingEOF)
- if err != nil {
- return err
- }
-
- if len(state.stream.packTrailer) != state.algo.Size() {
- return fmt.Errorf("packfile/ingest: invalid trailer size")
- }
-
- packHash, err := objectid.FromBytes(state.algo, state.stream.packTrailer)
- if err != nil {
- return err
- }
-
- state.packHash = packHash
-
- return state.stream.flush()
-}
-
-// seedStreamWithPackHeader writes the already-validated PACK header to output,
-// seeds the running pack hash, and advances stream offset accounting.
-func seedStreamWithPackHeader(state *ingestState) error {
- written := 0
- for written < len(state.packHeaderRaw) {
- n, err := state.packFile.Write(state.packHeaderRaw[written:])
- if err != nil {
- return &DestinationWriteError{Op: fmt.Sprintf("write pack header: %v", err)}
- }
-
- if n == 0 {
- return &DestinationWriteError{Op: "write pack header: short write"}
- }
-
- written += n
- }
-
- _, err := state.stream.hash.Write(state.packHeaderRaw[:])
- if err != nil {
- return err
- }
-
- state.stream.consumed = packHeaderSize
-
- return nil
-}
diff --git a/object/store/packed/internal/ingest/state.go b/object/store/packed/internal/ingest/state.go
deleted file mode 100644
index 0412eb32..00000000
--- a/object/store/packed/internal/ingest/state.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package ingest
-
-import (
- "io"
- "os"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-const (
- defaultDeltaBaseCacheMaxBytes = 32 << 20
-)
-
-// ingestState holds mutable state for one Ingest call.
-type ingestState struct {
- src io.Reader
- destination *os.Root
- algo objectid.Algorithm
- opts Options
-
- packHeaderRaw [packHeaderSize]byte
-
- packFile *os.File
- packTmpName string
- idxFile *os.File
- idxTmpName string
- revFile *os.File
- revTmpName string
-
- stream *streamScanner
-
- records []objectRecord
- ofsDeltas []ofsDeltaRef
- refDeltas []refDeltaRef
- unresolvedRefDeltas []int
- offsetToRecord map[uint64]int
- objectToRecord map[objectid.ObjectID]int
-
- baseCache *deltaBaseCache
- packHash objectid.ObjectID
-
- objectCountHeader uint32
- thinFixed bool
-}
-
-// newIngestState constructs one call-local ingest state.
-func newIngestState(
- src io.Reader,
- destination *os.Root,
- algo objectid.Algorithm,
- opts Options,
- header packHeader,
- headerRaw [packHeaderSize]byte,
-) (*ingestState, error) {
- if algo.Size() == 0 {
- return nil, objectid.ErrInvalidAlgorithm
- }
-
- return &ingestState{
- src: src,
- destination: destination,
- algo: algo,
- opts: opts,
- packHeaderRaw: headerRaw,
- objectCountHeader: header.ObjectCount,
- offsetToRecord: make(map[uint64]int),
- objectToRecord: make(map[objectid.ObjectID]int),
- baseCache: newDeltaBaseCache(defaultDeltaBaseCacheMaxBytes),
- }, nil
-}
diff --git a/object/store/packed/internal/ingest/stream.go b/object/store/packed/internal/ingest/stream.go
deleted file mode 100644
index a403087a..00000000
--- a/object/store/packed/internal/ingest/stream.go
+++ /dev/null
@@ -1,111 +0,0 @@
-package ingest
-
-import (
- "errors"
- "hash"
- "io"
- "os"
-)
-
-const streamScannerBufferSize = 64 << 10
-
-// streamScanner incrementally reads/consumes one pack stream while mirroring
-// consumed bytes into one destination pack file.
-type streamScanner struct {
- src io.Reader
- dstFile *os.File
-
- // Input buffer window: buf[off:n] is unread.
- buf []byte
- off int
- n int
-
- // Absolute consumed stream bytes.
- consumed uint64
-
- // Running pack hash over consumed bytes while hashEnabled is true.
- hash hash.Hash
- hashSize int
- hashEnabled bool
-
- // Entry CRC state while one entry is being consumed.
- entryCRC uint32
- inEntryCRC bool
-
- packTrailer []byte
-}
-
-// newStreamScanner constructs one scanner with fixed input buffering.
-func newStreamScanner(src io.Reader, dstFile *os.File, hash hash.Hash, hashSize int) *streamScanner {
- return &streamScanner{
- src: src,
- dstFile: dstFile,
- buf: make([]byte, streamScannerBufferSize),
- hash: hash,
- hashSize: hashSize,
- hashEnabled: true,
- }
-}
-
-// Read implements io.Reader.
-func (scanner *streamScanner) Read(dst []byte) (int, error) {
- if len(dst) == 0 {
- return 0, nil
- }
-
- if scanner.n-scanner.off == 0 {
- err := scanner.fill(1)
- if err != nil {
- if errors.Is(err, io.EOF) {
- return 0, io.EOF
- }
-
- return 0, err
- }
- }
-
- unread := scanner.n - scanner.off
- if unread == 0 {
- return 0, io.EOF
- }
-
- n := min(len(dst), unread)
-
- copy(dst, scanner.buf[scanner.off:scanner.off+n])
-
- err := scanner.use(n)
- if err != nil {
- return 0, err
- }
-
- return n, nil
-}
-
-// ReadByte implements io.ByteReader without allocation.
-func (scanner *streamScanner) ReadByte() (byte, error) {
- if scanner.n-scanner.off == 0 {
- err := scanner.fill(1)
- if err != nil {
- return 0, err
- }
- }
-
- b := scanner.buf[scanner.off]
-
- err := scanner.use(1)
- if err != nil {
- return 0, err
- }
-
- return b, nil
-}
-
-// readFull reads exactly len(dst) bytes through receiver.
-func (scanner *streamScanner) readFull(dst []byte) error {
- _, err := io.ReadFull(scanner, dst)
- if err != nil {
- return err
- }
-
- return nil
-}
diff --git a/object/store/packed/internal/ingest/temp.go b/object/store/packed/internal/ingest/temp.go
deleted file mode 100644
index d0b7862c..00000000
--- a/object/store/packed/internal/ingest/temp.go
+++ /dev/null
@@ -1,103 +0,0 @@
-package ingest
-
-import (
- "crypto/rand"
- "errors"
- "fmt"
- "io/fs"
- "os"
-)
-
-// openTemporaryArtifacts creates/open temp pack/idx/(rev) files under destination.
-func openTemporaryArtifacts(state *ingestState) error {
- packName, packFile, err := createTempFile(state.destination, "tmp_pack_")
- if err != nil {
- return err
- }
-
- idxName, idxFile, err := createTempFile(state.destination, "tmp_idx_")
- if err != nil {
- _ = packFile.Close()
- _ = state.destination.Remove(packName)
-
- return err
- }
-
- revName := ""
-
- var revFile *os.File
- if state.opts.WriteRev {
- revName, revFile, err = createTempFile(state.destination, "tmp_rev_")
- if err != nil {
- _ = idxFile.Close()
- _ = state.destination.Remove(idxName)
- _ = packFile.Close()
- _ = state.destination.Remove(packName)
-
- return err
- }
- }
-
- state.packTmpName = packName
- state.packFile = packFile
- state.idxTmpName = idxName
- state.idxFile = idxFile
- state.revTmpName = revName
- state.revFile = revFile
-
- return nil
-}
-
-// closeTemporaryArtifacts closes all temporary artifact file descriptors.
-func closeTemporaryArtifacts(state *ingestState) error {
- var out error
-
- if state.packFile != nil {
- err := state.packFile.Close()
- if err != nil && out == nil {
- out = err
- }
-
- state.packFile = nil
- }
-
- if state.idxFile != nil {
- err := state.idxFile.Close()
- if err != nil && out == nil {
- out = err
- }
-
- state.idxFile = nil
- }
-
- if state.revFile != nil {
- err := state.revFile.Close()
- if err != nil && out == nil {
- out = err
- }
-
- state.revFile = nil
- }
-
- return out
-}
-
-// createTempFile creates one temporary file under root using prefix.
-func createTempFile(root *os.Root, prefix string) (string, *os.File, error) {
- for range 32 {
- name := prefix + rand.Text()
-
- file, err := root.OpenFile(name, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0o644)
- if err == nil {
- return name, file, nil
- }
-
- if errors.Is(err, fs.ErrExist) {
- continue
- }
-
- return "", nil, fmt.Errorf("packfile/ingest: create temp file %q: %w", name, err)
- }
-
- return "", nil, fmt.Errorf("packfile/ingest: unable to create temporary file for prefix %q", prefix)
-}
diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha1/METADATA.txt b/object/store/packed/internal/ingest/testdata/fixtures/sha1/METADATA.txt
deleted file mode 100644
index 5fcbfe26..00000000
--- a/object/store/packed/internal/ingest/testdata/fixtures/sha1/METADATA.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-format=sha1
-head=200c960359dad025b4170284c518919eb4a24305
-base=4bc507fc631ea78474d83c47548743c9f1dda0dc
diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha1/base.pack b/object/store/packed/internal/ingest/testdata/fixtures/sha1/base.pack
deleted file mode 100644
index 3d7a4903..00000000
--- a/object/store/packed/internal/ingest/testdata/fixtures/sha1/base.pack
+++ /dev/null
Binary files differ
diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha1/nonthin.pack b/object/store/packed/internal/ingest/testdata/fixtures/sha1/nonthin.pack
deleted file mode 100644
index ea07c9a0..00000000
--- a/object/store/packed/internal/ingest/testdata/fixtures/sha1/nonthin.pack
+++ /dev/null
Binary files differ
diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha1/thin.pack b/object/store/packed/internal/ingest/testdata/fixtures/sha1/thin.pack
deleted file mode 100644
index 95084feb..00000000
--- a/object/store/packed/internal/ingest/testdata/fixtures/sha1/thin.pack
+++ /dev/null
Binary files differ
diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha256/METADATA.txt b/object/store/packed/internal/ingest/testdata/fixtures/sha256/METADATA.txt
deleted file mode 100644
index 8a5ea0a2..00000000
--- a/object/store/packed/internal/ingest/testdata/fixtures/sha256/METADATA.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-format=sha256
-head=35cc0f4cd1c73524187540494058d233a2ecbd071c85d496a2250d8e0c805ef8
-base=b4abe46895f0bb5aa22fd42d28d428413f265359734c288752e3c2d270eec276
diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha256/base.pack b/object/store/packed/internal/ingest/testdata/fixtures/sha256/base.pack
deleted file mode 100644
index 52ceef74..00000000
--- a/object/store/packed/internal/ingest/testdata/fixtures/sha256/base.pack
+++ /dev/null
Binary files differ
diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha256/nonthin.pack b/object/store/packed/internal/ingest/testdata/fixtures/sha256/nonthin.pack
deleted file mode 100644
index 50db05d0..00000000
--- a/object/store/packed/internal/ingest/testdata/fixtures/sha256/nonthin.pack
+++ /dev/null
Binary files differ
diff --git a/object/store/packed/internal/ingest/testdata/fixtures/sha256/thin.pack b/object/store/packed/internal/ingest/testdata/fixtures/sha256/thin.pack
deleted file mode 100644
index b331b915..00000000
--- a/object/store/packed/internal/ingest/testdata/fixtures/sha256/thin.pack
+++ /dev/null
Binary files differ
diff --git a/object/store/packed/internal/ingest/thin_append.go b/object/store/packed/internal/ingest/thin_append.go
deleted file mode 100644
index 779d477f..00000000
--- a/object/store/packed/internal/ingest/thin_append.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package ingest
-
-import (
- "compress/zlib"
- "hash/crc32"
- "io"
-
- "codeberg.org/lindenii/furgit/internal/intconv"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// appendBaseObject appends one base object as a new packed non-delta entry.
-func appendBaseObject(state *ingestState, id objectid.ObjectID, realType objecttype.Type, content []byte) (int, error) {
- start := state.stream.consumed
-
- header := encodePackEntryHeader(realType, int64(len(content)))
-
- startInt64, err := intconv.Uint64ToInt64(start)
- if err != nil {
- return 0, err
- }
-
- _, err = state.packFile.WriteAt(header, startInt64)
- if err != nil {
- return 0, err
- }
-
- headerLenInt64 := int64(len(header))
- section := &fileSectionWriter{file: state.packFile, off: startInt64 + headerLenInt64}
- crc := crc32.NewIEEE()
-
- _, err = crc.Write(header)
- if err != nil {
- return 0, err
- }
-
- counting := &countingWriter{dst: section}
-
- zw := zlib.NewWriter(io.MultiWriter(counting, crc))
-
- _, err = zw.Write(content)
- if err != nil {
- return 0, err
- }
-
- err = zw.Close()
- if err != nil {
- return 0, err
- }
-
- headerLenUint64, err := intconv.IntToUint64(len(header))
- if err != nil {
- return 0, err
- }
-
- countingNUint64, err := intconv.IntToUint64(counting.n)
- if err != nil {
- return 0, err
- }
-
- packedLen := headerLenUint64 + countingNUint64
- end := start + packedLen
- state.stream.consumed = end
-
- headerLenUint32, err := intconv.IntToUint32(len(header))
- if err != nil {
- return 0, err
- }
-
- record := objectRecord{
- offset: start,
- headerLen: headerLenUint32,
- packedLen: packedLen,
- crc32: crc.Sum32(),
- packedType: realType,
- realType: realType,
- declaredSize: int64(len(content)),
- dataOffset: start + headerLenUint64,
- objectID: id,
- resolved: true,
- }
-
- recordIdx := len(state.records)
- state.records = append(state.records, record)
- state.offsetToRecord[start] = recordIdx
- state.objectToRecord[id] = recordIdx
- state.baseCache.add(recordIdx, realType, content)
-
- return recordIdx, nil
-}
diff --git a/object/store/packed/internal/ingest/thin_fix.go b/object/store/packed/internal/ingest/thin_fix.go
deleted file mode 100644
index 5d701c52..00000000
--- a/object/store/packed/internal/ingest/thin_fix.go
+++ /dev/null
@@ -1,99 +0,0 @@
-package ingest
-
-import (
- "errors"
- "fmt"
-
- "codeberg.org/lindenii/furgit/internal/intconv"
- "codeberg.org/lindenii/furgit/internal/progress"
- objectstore "codeberg.org/lindenii/furgit/object/store"
-)
-
-// maybeFixThin appends missing bases and rewrites pack header/trailer when needed.
-func maybeFixThin(state *ingestState) error {
- if len(state.unresolvedRefDeltas) == 0 {
- return nil
- }
-
- writeProgressf(
- state,
- "fixing thin pack: %d unresolved bases\r",
- len(state.unresolvedRefDeltas),
- )
-
- if !state.opts.FixThin {
- return &ThinPackUnresolvedError{Count: len(state.unresolvedRefDeltas)}
- }
-
- if state.opts.ThinBase == nil {
- return &ThinPackUnresolvedError{Count: len(state.unresolvedRefDeltas)}
- }
-
- hashSize := int64(state.algo.Size())
-
- info, err := state.packFile.Stat()
- if err != nil {
- return err
- }
-
- size := info.Size()
- if size < hashSize {
- return fmt.Errorf("packfile/ingest: pack too short to trim trailer")
- }
-
- newEnd := size - hashSize
-
- err = state.packFile.Truncate(newEnd)
- if err != nil {
- return err
- }
-
- consumed, err := intconv.Int64ToUint64(newEnd)
- if err != nil {
- return err
- }
-
- state.stream.consumed = consumed
-
- baseIDs := unresolvedThinBaseIDs(state)
-
- total := len(baseIDs)
- meter := progress.New(progress.Options{
- Writer: state.opts.Progress,
- Title: "fixing thin pack",
- Total: uint64(total),
- })
- meter.Set(0, 0)
-
- var appended uint64
-
- for _, id := range baseIDs {
- ty, content, err := state.opts.ThinBase.ReadBytesContent(id)
- if err != nil {
- if errors.Is(err, objectstore.ErrObjectNotFound) {
- continue
- }
-
- return fmt.Errorf("packfile/ingest: read thin base %s: %w", id, err)
- }
-
- _, err = appendBaseObject(state, id, ty, content)
- if err != nil {
- return err
- }
-
- state.thinFixed = true
-
- appended++
- meter.Set(appended, 0)
- }
-
- err = rewritePackHeaderAndTrailer(state)
- if err != nil {
- return err
- }
-
- meter.Stop(fmt.Sprintf("appended %d/%d, done", appended, total))
-
- return nil
-}
diff --git a/object/store/packed/internal/ingest/thin_unresolved.go b/object/store/packed/internal/ingest/thin_unresolved.go
deleted file mode 100644
index 757cc0e2..00000000
--- a/object/store/packed/internal/ingest/thin_unresolved.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package ingest
-
-import (
- "bytes"
- "slices"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// unresolvedThinBaseIDs returns sorted unique unresolved ref base IDs.
-func unresolvedThinBaseIDs(state *ingestState) []objectid.ObjectID {
- seen := make(map[objectid.ObjectID]struct{})
-
- for _, idx := range state.unresolvedRefDeltas {
- record := state.records[idx]
- if record.packedType != objecttype.TypeRefDelta {
- continue
- }
-
- seen[record.baseObject] = struct{}{}
- }
-
- out := make([]objectid.ObjectID, 0, len(seen))
- for id := range seen {
- out = append(out, id)
- }
-
- slices.SortFunc(out, func(a, b objectid.ObjectID) int {
- return bytes.Compare(a.RawBytes(), b.RawBytes())
- })
-
- return out
-}
diff --git a/object/store/packed/internal/ingest/trailer.go b/object/store/packed/internal/ingest/trailer.go
deleted file mode 100644
index 7a26a8f2..00000000
--- a/object/store/packed/internal/ingest/trailer.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package ingest
-
-import (
- "bytes"
- "errors"
- "fmt"
- "io"
-)
-
-// finishAndFlushTrailer reads trailer hash bytes, verifies trailer checksum,
-// and optionally requires the source stream to hit EOF afterward.
-func (scanner *streamScanner) finishAndFlushTrailer(requireTrailingEOF bool) error {
- if scanner.hashSize <= 0 {
- return fmt.Errorf("packfile/ingest: invalid hash size")
- }
-
- trailer := make([]byte, scanner.hashSize)
-
- scanner.hashEnabled = false
-
- err := scanner.readFull(trailer)
- if err != nil {
- return &PackTrailerMismatchError{}
- }
-
- scanner.packTrailer = append(scanner.packTrailer[:0], trailer...)
-
- if scanner.n-scanner.off > 0 {
- return fmt.Errorf("packfile/ingest: pack has trailing garbage")
- }
-
- if !requireTrailingEOF {
- computed := scanner.hash.Sum(nil)
- if !bytes.Equal(computed, trailer) {
- return &PackTrailerMismatchError{}
- }
-
- return nil
- }
-
- var probe [1]byte
-
- n, err := scanner.Read(probe[:])
- if n > 0 || err == nil {
- return fmt.Errorf("packfile/ingest: pack has trailing garbage")
- }
-
- if !errors.Is(err, io.EOF) {
- return err
- }
-
- computed := scanner.hash.Sum(nil)
- if !bytes.Equal(computed, trailer) {
- return &PackTrailerMismatchError{}
- }
-
- return nil
-}
diff --git a/object/store/packed/internal/ingest/use.go b/object/store/packed/internal/ingest/use.go
deleted file mode 100644
index 97f8757a..00000000
--- a/object/store/packed/internal/ingest/use.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package ingest
-
-import (
- "fmt"
- "hash/crc32"
-)
-
-// use consumes n unread bytes and updates accounting/checksum state.
-func (scanner *streamScanner) use(n int) error {
- if n < 0 || n > scanner.n-scanner.off {
- return fmt.Errorf("packfile/ingest: invalid consume length %d", n)
- }
-
- if n == 0 {
- return nil
- }
-
- chunk := scanner.buf[scanner.off : scanner.off+n]
- if scanner.hashEnabled {
- _, err := scanner.hash.Write(chunk)
- if err != nil {
- return err
- }
- }
-
- if scanner.inEntryCRC {
- scanner.entryCRC = crc32.Update(scanner.entryCRC, crc32.IEEETable, chunk)
- }
-
- scanner.off += n
- scanner.consumed += uint64(n)
-
- return nil
-}
diff --git a/object/store/packed/internal/ingest/write.go b/object/store/packed/internal/ingest/write.go
deleted file mode 100644
index efd27323..00000000
--- a/object/store/packed/internal/ingest/write.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package ingest
-
-import (
- "bufio"
- "io"
- "os"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-// WritePack ingests one pack stream into destination and writes pack artifacts.
-//
-// Artifacts are published under content-addressed final names derived from the
-// resulting pack hash. If those final names already exist, WritePack treats
-// that as success and removes its temporary files.
-func WritePack(
- destination *os.Root,
- algo objectid.Algorithm,
- src io.Reader,
- opts Options,
-) (Result, error) {
- if algo.Size() == 0 {
- return Result{}, objectid.ErrInvalidAlgorithm
- }
-
- reader := bufio.NewReader(src)
-
- header, headerRaw, err := readAndValidatePackHeader(reader)
- if err != nil {
- return Result{}, err
- }
-
- if header.ObjectCount == 0 {
- return discardZeroObjectPack(reader, algo, opts, headerRaw)
- }
-
- state, err := newIngestState(
- reader,
- destination,
- algo,
- opts,
- header,
- headerRaw,
- )
- if err != nil {
- return Result{}, err
- }
-
- return ingest(state)
-}
diff --git a/object/store/packed/internal/ingest/write_empty.go b/object/store/packed/internal/ingest/write_empty.go
deleted file mode 100644
index 0d3401f0..00000000
--- a/object/store/packed/internal/ingest/write_empty.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package ingest
-
-import (
- "bytes"
- "errors"
- "io"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-func discardZeroObjectPack(
- src io.Reader,
- algo objectid.Algorithm,
- opts Options,
- headerRaw [packHeaderSize]byte,
-) (Result, error) {
- hashImpl, err := algo.New()
- if err != nil {
- return Result{}, err
- }
-
- _, _ = hashImpl.Write(headerRaw[:])
-
- trailer := make([]byte, algo.Size())
-
- _, err = io.ReadFull(src, trailer)
- if err != nil {
- return Result{}, &PackTrailerMismatchError{}
- }
-
- computed := hashImpl.Sum(nil)
- if !bytes.Equal(computed, trailer) {
- return Result{}, &PackTrailerMismatchError{}
- }
-
- if opts.RequireTrailingEOF {
- var probe [1]byte
-
- n, err := src.Read(probe[:])
- if n > 0 || err == nil {
- return Result{}, errors.New("packfile/ingest: pack has trailing garbage")
- }
-
- if err != io.EOF {
- return Result{}, err
- }
- }
-
- packHash, err := objectid.FromBytes(algo, trailer)
- if err != nil {
- return Result{}, err
- }
-
- return Result{
- PackHash: packHash,
- ObjectCount: 0,
- }, nil
-}
diff --git a/object/store/packed/internal/reading/TODO b/object/store/packed/internal/reading/TODO
deleted file mode 100644
index f4a5f48e..00000000
--- a/object/store/packed/internal/reading/TODO
+++ /dev/null
@@ -1,3 +0,0 @@
-* Per delta-plan memo map
-* Internal handle/request context (might expose it externally later and add to global interface)
-* Audit on mutex
diff --git a/object/store/packed/internal/reading/close.go b/object/store/packed/internal/reading/close.go
deleted file mode 100644
index 62c62025..00000000
--- a/object/store/packed/internal/reading/close.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package reading
-
-// Close releases mapped pack/index resources associated with the store.
-//
-// Labels: MT-Unsafe.
-func (store *Store) Close() error {
- store.stateMu.Lock()
- packs := store.packs
- store.stateMu.Unlock()
- store.idxMu.RLock()
- indexes := store.idxByPack
- store.idxMu.RUnlock()
-
- var closeErr error
-
- for _, pack := range packs {
- err := pack.close()
- if err != nil && closeErr == nil {
- closeErr = err
- }
- }
-
- for _, index := range indexes {
- err := index.close()
- if err != nil && closeErr == nil {
- closeErr = err
- }
- }
-
- store.cacheMu.Lock()
- store.deltaCache.clear()
- store.cacheMu.Unlock()
-
- return closeErr
-}
diff --git a/object/store/packed/internal/reading/delta_build_chain.go b/object/store/packed/internal/reading/delta_build_chain.go
deleted file mode 100644
index a0e3151d..00000000
--- a/object/store/packed/internal/reading/delta_build_chain.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package reading
-
-import (
- "fmt"
-
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// deltaBuildChain walks one object's chain and builds a reconstruction chain.
-func (store *Store) deltaBuildChain(start location) (deltaChain, error) {
- visited := make(map[location]struct{})
- current := start
-
- var chain deltaChain
-
- for {
- if _, ok := visited[current]; ok {
- return deltaChain{}, fmt.Errorf("objectstore/packed: delta cycle while resolving object")
- }
-
- visited[current] = struct{}{}
-
- _, meta, err := store.entryMetaAt(current)
- if err != nil {
- return deltaChain{}, err
- }
-
- if meta.ty.IsBaseObject() {
- chain.baseLoc = current
- chain.baseType = meta.ty
-
- return chain, nil
- }
-
- switch meta.ty {
- case objecttype.TypeRefDelta:
- chain.deltas = append(chain.deltas, deltaNode{
- loc: current,
- dataOffset: meta.dataOffset,
- })
-
- next, err := store.lookup(meta.baseRefID)
- if err != nil {
- return deltaChain{}, err
- }
-
- current = next
- case objecttype.TypeOfsDelta:
- chain.deltas = append(chain.deltas, deltaNode{
- loc: current,
- dataOffset: meta.dataOffset,
- })
- current = location{
- packName: current.packName,
- offset: meta.baseOfs,
- }
- case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag:
- return deltaChain{}, fmt.Errorf("objectstore/packed: internal invariant violation for base type %d", meta.ty)
- case objecttype.TypeInvalid, objecttype.TypeFuture:
- return deltaChain{}, fmt.Errorf("objectstore/packed: unsupported pack type %d", meta.ty)
- default:
- return deltaChain{}, fmt.Errorf("objectstore/packed: unsupported pack type %d", meta.ty)
- }
- }
-}
diff --git a/object/store/packed/internal/reading/delta_cache.go b/object/store/packed/internal/reading/delta_cache.go
deleted file mode 100644
index 4259eb81..00000000
--- a/object/store/packed/internal/reading/delta_cache.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package reading
-
-import (
- "codeberg.org/lindenii/furgit/internal/lru"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-const defaultDeltaCacheMaxBytes = 32 << 20
-
-// deltaBaseKey identifies one base object by pack location.
-type deltaBaseKey struct {
- packName string
- offset uint64
-}
-
-// deltaBaseValue stores one cached base object body.
-type deltaBaseValue struct {
- ty objecttype.Type
- content []byte
-}
-
-// deltaCache wraps a weighted LRU for resolved delta bases.
-type deltaCache struct {
- lru *lru.Cache[deltaBaseKey, deltaBaseValue]
-}
-
-// newDeltaCache creates a delta base cache with a byte budget.
-func newDeltaCache(maxBytes int64) *deltaCache {
- return &deltaCache{
- lru: lru.New(
- maxBytes,
- func(_ deltaBaseKey, value deltaBaseValue) int64 {
- return int64(len(value.content))
- },
- nil,
- ),
- }
-}
-
-// get returns a cloned cached base object value.
-func (cache *deltaCache) get(key deltaBaseKey) (objecttype.Type, []byte, bool) {
- value, ok := cache.lru.Get(key)
- if !ok {
- return objecttype.TypeInvalid, nil, false
- }
-
- return value.ty, append([]byte(nil), value.content...), true
-}
-
-// add stores a cloned base object value.
-func (cache *deltaCache) add(key deltaBaseKey, ty objecttype.Type, content []byte) {
- cache.lru.Add(key, deltaBaseValue{
- ty: ty,
- content: append([]byte(nil), content...),
- })
-}
-
-// clear removes all cached entries.
-func (cache *deltaCache) clear() {
- cache.lru.Clear()
-}
diff --git a/object/store/packed/internal/reading/delta_chain.go b/object/store/packed/internal/reading/delta_chain.go
deleted file mode 100644
index 6e82873e..00000000
--- a/object/store/packed/internal/reading/delta_chain.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package reading
-
-import objecttype "codeberg.org/lindenii/furgit/object/type"
-
-// deltaChain describes how to reconstruct one requested object.
-type deltaChain struct {
- // baseLoc points to the innermost base object.
- baseLoc location
- // baseType is the canonical object type resolved from baseLoc.
- baseType objecttype.Type
- // deltas contains delta objects from target down toward base.
- deltas []deltaNode
-}
diff --git a/object/store/packed/internal/reading/delta_node.go b/object/store/packed/internal/reading/delta_node.go
deleted file mode 100644
index 56f7b078..00000000
--- a/object/store/packed/internal/reading/delta_node.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package reading
-
-// deltaNode describes one delta object in a reconstruction chain.
-type deltaNode struct {
- // loc identifies the delta object's pack location.
- loc location
- // dataOffset points to the start of the delta zlib payload in pack.
- dataOffset int
-}
diff --git a/object/store/packed/internal/reading/delta_resolve_chain.go b/object/store/packed/internal/reading/delta_resolve_chain.go
deleted file mode 100644
index ec9c39e2..00000000
--- a/object/store/packed/internal/reading/delta_resolve_chain.go
+++ /dev/null
@@ -1,61 +0,0 @@
-package reading
-
-import (
- "fmt"
-
- deltaapply "codeberg.org/lindenii/furgit/format/packfile/delta/apply"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// deltaResolveChain resolves one object chain into content bytes.
-func (store *Store) deltaResolveChain(chain deltaChain, declaredSize int64) (objecttype.Type, []byte, error) {
- ty, out, nextDelta, err := store.deltaResolveChainStart(chain)
- if err != nil {
- return objecttype.TypeInvalid, nil, err
- }
-
- for i := nextDelta; i >= 0; i-- {
- node := chain.deltas[i]
-
- pack, err := store.openPack(node.loc.packName)
- if err != nil {
- return objecttype.TypeInvalid, nil, err
- }
-
- delta, err := inflateAt(pack, node.dataOffset, -1)
- if err != nil {
- return objecttype.TypeInvalid, nil, err
- }
-
- out, err = deltaapply.Apply(out, delta)
- if err != nil {
- return objecttype.TypeInvalid, nil, err
- }
-
- store.cacheMu.Lock()
- store.deltaCache.add(
- deltaBaseKey{packName: node.loc.packName, offset: node.loc.offset},
- ty,
- out,
- )
- store.cacheMu.Unlock()
- }
-
- if int64(len(out)) != declaredSize {
- return objecttype.TypeInvalid, nil, fmt.Errorf(
- "objectstore/packed: resolved content size mismatch: got %d want %d",
- len(out),
- declaredSize,
- )
- }
-
- if ty != chain.baseType {
- return objecttype.TypeInvalid, nil, fmt.Errorf(
- "objectstore/packed: resolved content type mismatch: got %d want %d",
- ty,
- chain.baseType,
- )
- }
-
- return ty, out, nil
-}
diff --git a/object/store/packed/internal/reading/delta_resolve_chain_start.go b/object/store/packed/internal/reading/delta_resolve_chain_start.go
deleted file mode 100644
index 17274027..00000000
--- a/object/store/packed/internal/reading/delta_resolve_chain_start.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package reading
-
-import (
- "fmt"
-
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// deltaResolveChainStart finds the nearest cached chain node or inflates the
-// innermost base object. It returns the starting bytes and the next delta index
-// to apply in reverse order.
-func (store *Store) deltaResolveChainStart(chain deltaChain) (objecttype.Type, []byte, int, error) {
- for i, node := range chain.deltas {
- store.cacheMu.RLock()
- ty, out, ok := store.deltaCache.get(
- deltaBaseKey{packName: node.loc.packName, offset: node.loc.offset},
- )
- store.cacheMu.RUnlock()
-
- if ok {
- return ty, out, i - 1, nil
- }
- }
-
- store.cacheMu.RLock()
- ty, out, ok := store.deltaCache.get(
- deltaBaseKey{packName: chain.baseLoc.packName, offset: chain.baseLoc.offset},
- )
- store.cacheMu.RUnlock()
-
- if ok {
- return ty, out, len(chain.deltas) - 1, nil
- }
-
- pack, meta, err := store.entryMetaAt(chain.baseLoc)
- if err != nil {
- return objecttype.TypeInvalid, nil, 0, err
- }
-
- if !meta.ty.IsBaseObject() {
- return objecttype.TypeInvalid, nil, 0, fmt.Errorf("objectstore/packed: delta chain base is not a base object")
- }
-
- base, err := inflateAt(pack, meta.dataOffset, meta.size)
- if err != nil {
- return objecttype.TypeInvalid, nil, 0, err
- }
-
- store.cacheMu.Lock()
- store.deltaCache.add(
- deltaBaseKey{packName: chain.baseLoc.packName, offset: chain.baseLoc.offset},
- meta.ty,
- base,
- )
- store.cacheMu.Unlock()
-
- return meta.ty, base, len(chain.deltas) - 1, nil
-}
diff --git a/object/store/packed/internal/reading/delta_resolve_content.go b/object/store/packed/internal/reading/delta_resolve_content.go
deleted file mode 100644
index 71eb69cf..00000000
--- a/object/store/packed/internal/reading/delta_resolve_content.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package reading
-
-import objecttype "codeberg.org/lindenii/furgit/object/type"
-
-// deltaResolveContent resolves one object's content bytes from its pack location.
-func (store *Store) deltaResolveContent(start location) (objecttype.Type, []byte, error) {
- chain, err := store.deltaBuildChain(start)
- if err != nil {
- return objecttype.TypeInvalid, nil, err
- }
-
- pack, meta, err := store.entryMetaAt(start)
- if err != nil {
- return objecttype.TypeInvalid, nil, err
- }
-
- declaredSize := meta.size
- if !meta.ty.IsBaseObject() {
- declaredSize, err = deltaDeclaredSizeAt(pack, meta.dataOffset)
- if err != nil {
- return objecttype.TypeInvalid, nil, err
- }
- }
-
- return store.deltaResolveChain(chain, declaredSize)
-}
diff --git a/object/store/packed/internal/reading/delta_size.go b/object/store/packed/internal/reading/delta_size.go
deleted file mode 100644
index 8a85fad9..00000000
--- a/object/store/packed/internal/reading/delta_size.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package reading
-
-import (
- "bufio"
-
- deltaapply "codeberg.org/lindenii/furgit/format/packfile/delta/apply"
-)
-
-// deltaDeclaredSizeAt returns the resolved object size declared by one delta
-// stream header at dataOffset.
-func deltaDeclaredSizeAt(pack *packFile, dataOffset int) (int64, error) {
- reader, err := zlibReaderAt(pack, dataOffset)
- if err != nil {
- return 0, err
- }
-
- defer func() { _ = reader.Close() }()
-
- br := bufio.NewReaderSize(reader, 32)
-
- _, size, err := deltaapply.ReadHeaderSizes(br)
- if err != nil {
- return 0, err
- }
-
- return int64(size), nil
-}
diff --git a/object/store/packed/internal/reading/doc.go b/object/store/packed/internal/reading/doc.go
deleted file mode 100644
index a513d3bd..00000000
--- a/object/store/packed/internal/reading/doc.go
+++ /dev/null
@@ -1,6 +0,0 @@
-// Package reading implements the packed-store read path: pack and index
-// discovery, lookup, caching, and object reads from existing packfiles.
-//
-// Obviously, this internal package is not meant to be used by anyone
-// other than object/store/packed.
-package reading
diff --git a/object/store/packed/internal/reading/entry_inflate.go b/object/store/packed/internal/reading/entry_inflate.go
deleted file mode 100644
index 82b2a7a8..00000000
--- a/object/store/packed/internal/reading/entry_inflate.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package reading
-
-import (
- "bytes"
- "fmt"
- "io"
- "math"
-
- "codeberg.org/lindenii/furgit/internal/compress/zlib"
- "codeberg.org/lindenii/furgit/internal/iolimit"
-)
-
-// zlibReaderAt opens a zlib reader starting at data offset within pack.
-func zlibReaderAt(pack *packFile, offset int) (io.ReadCloser, error) {
- if offset < 0 || offset > len(pack.data) {
- return nil, fmt.Errorf("objectstore/packed: pack %q zlib offset out of bounds", pack.name)
- }
-
- return zlib.NewReader(bytes.NewReader(pack.data[offset:]))
-}
-
-// inflateAt inflates one entry payload from data offset.
-func inflateAt(pack *packFile, offset int, expectedSize int64) ([]byte, error) {
- reader, err := zlibReaderAt(pack, offset)
- if err != nil {
- return nil, err
- }
-
- defer func() { _ = reader.Close() }()
-
- if expectedSize >= 0 {
- if expectedSize > int64(math.MaxInt) {
- return nil, fmt.Errorf(
- "objectstore/packed: pack %q expected inflated size overflows int: %d",
- pack.name,
- expectedSize,
- )
- }
-
- reader := iolimit.ExpectLengthReader(reader, expectedSize)
- body := make([]byte, int(expectedSize))
-
- _, err := io.ReadFull(reader, body)
- if err != nil {
- return nil, err
- }
-
- var probe [1]byte
-
- _, err = reader.Read(probe[:])
- if err != nil && err != io.EOF {
- return nil, err
- }
-
- return body, nil
- }
-
- body, err := io.ReadAll(reader)
- if err != nil {
- return nil, err
- }
-
- return body, nil
-}
diff --git a/object/store/packed/internal/reading/entry_meta.go b/object/store/packed/internal/reading/entry_meta.go
deleted file mode 100644
index 336dc3b9..00000000
--- a/object/store/packed/internal/reading/entry_meta.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package reading
-
-// entryMetaAt parses one pack entry header at location.
-func (store *Store) entryMetaAt(loc location) (*packFile, entryMeta, error) {
- pack, err := store.openPack(loc.packName)
- if err != nil {
- return nil, entryMeta{}, err
- }
-
- meta, err := parseEntryMeta(pack, store.algo, loc.offset)
- if err != nil {
- return nil, entryMeta{}, err
- }
-
- return pack, meta, nil
-}
diff --git a/object/store/packed/internal/reading/entry_parse.go b/object/store/packed/internal/reading/entry_parse.go
deleted file mode 100644
index ecbfb6cb..00000000
--- a/object/store/packed/internal/reading/entry_parse.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package reading
-
-import (
- "fmt"
-
- packfmt "codeberg.org/lindenii/furgit/format/packfile"
- "codeberg.org/lindenii/furgit/internal/intconv"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// entryMeta describes one parsed pack entry header.
-type entryMeta struct {
- // ty is the pack entry type tag.
- ty objecttype.Type
- // size is the declared resulting content size.
- size int64
- // dataOffset points to the zlib payload start.
- dataOffset int
- // baseRefID is set for ref-delta entries.
- baseRefID objectid.ObjectID
- // baseOfs is set for ofs-delta entries.
- baseOfs uint64
-}
-
-// parseEntryMeta parses one pack entry header at offset.
-func parseEntryMeta(pack *packFile, algo objectid.Algorithm, offset uint64) (entryMeta, error) {
- var zero entryMeta
- if offset >= uint64(len(pack.data)) {
- return zero, fmt.Errorf("objectstore/packed: pack %q offset %d out of bounds", pack.name, offset)
- }
-
- pos, err := intconv.Uint64ToInt(offset)
- if err != nil {
- return zero, fmt.Errorf("objectstore/packed: pack %q offset conversion: %w", pack.name, err)
- }
-
- entry, err := packfmt.ParseEntry(pack.data[pos:], algo.Size())
- if err != nil {
- return zero, fmt.Errorf("objectstore/packed: pack %q: %w", pack.name, err)
- }
-
- meta := entryMeta{
- ty: entry.Type,
- size: entry.Size,
- dataOffset: pos + entry.DataOffset,
- }
- switch meta.ty {
- case objecttype.TypeRefDelta:
- baseID, err := objectid.FromBytes(algo, entry.RefBaseID)
- if err != nil {
- return zero, fmt.Errorf("objectstore/packed: pack %q invalid ref-delta base id: %w", pack.name, err)
- }
-
- meta.baseRefID = baseID
- case objecttype.TypeOfsDelta:
- if offset <= entry.OfsBaseDistance {
- return zero, fmt.Errorf("objectstore/packed: pack %q has invalid ofs-delta base", pack.name)
- }
-
- meta.baseOfs = offset - entry.OfsBaseDistance
- case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag:
- // Base object types do not have delta base metadata.
- case objecttype.TypeInvalid, objecttype.TypeFuture:
- return zero, fmt.Errorf("objectstore/packed: pack %q has unsupported entry type %d", pack.name, meta.ty)
- default:
- return zero, fmt.Errorf("objectstore/packed: pack %q has unsupported entry type %d", pack.name, meta.ty)
- }
-
- return meta, nil
-}
diff --git a/object/store/packed/internal/reading/helpers_test.go b/object/store/packed/internal/reading/helpers_test.go
deleted file mode 100644
index 5a37d2f1..00000000
--- a/object/store/packed/internal/reading/helpers_test.go
+++ /dev/null
@@ -1,102 +0,0 @@
-package reading_test
-
-import (
- "fmt"
- "io"
- "strconv"
- "strings"
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- objectheader "codeberg.org/lindenii/furgit/object/header"
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/store/packed"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-func openPackedStore(t *testing.T, testRepo *testgit.TestRepo, algo objectid.Algorithm) *packed.Store {
- t.Helper()
-
- root := testRepo.OpenPackRoot(t)
-
- store, err := packed.New(root, algo, packed.Options{})
- if err != nil {
- t.Fatalf("packed.New: %v", err)
- }
-
- return store
-}
-
-func mustReadAllAndClose(t *testing.T, reader io.ReadCloser) []byte {
- t.Helper()
-
- data, err := io.ReadAll(reader)
- if err != nil {
- _ = reader.Close()
-
- t.Fatalf("ReadAll: %v", err)
- }
-
- err = reader.Close()
- if err != nil {
- t.Fatalf("Close: %v", err)
- }
-
- return data
-}
-
-func expectedRawObject(t *testing.T, testRepo *testgit.TestRepo, id objectid.ObjectID) (objecttype.Type, []byte, []byte) {
- t.Helper()
-
- typeName := testRepo.Run(t, "cat-file", "-t", id.String())
-
- ty, ok := objecttype.Parse(typeName)
- if !ok {
- t.Fatalf("ParseName(%q) failed", typeName)
- }
-
- body := testRepo.CatFile(t, typeName, id)
-
- header, ok := objectheader.Encode(ty, int64(len(body)))
- if !ok {
- t.Fatalf("objectheader.Encode failed")
- }
-
- raw := make([]byte, len(header)+len(body))
- copy(raw, header)
- copy(raw[len(header):], body)
-
- return ty, body, raw
-}
-
-func createPackedFixtureRepo(t *testing.T, algo objectid.Algorithm) (*testgit.TestRepo, []objectid.ObjectID) {
- t.Helper()
-
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- blobID, treeID, commitID := testRepo.MakeCommit(t, "packed store base commit")
- testRepo.Run(t, "update-ref", "refs/heads/main", commitID.String())
- tagID := testRepo.TagAnnotated(t, "v1.0.0", commitID, "packed-store-tag")
-
- parent := commitID
-
- for i := range 24 {
- content := "common-prefix\n" + strings.Repeat("line-"+strconv.Itoa(i%3)+"\n", 256) + fmt.Sprintf("tail-%d\n", i)
- nextBlob, nextTree := testRepo.MakeSingleFileTree(t, fmt.Sprintf("file-%02d.txt", i), []byte(content))
- nextCommit := testRepo.CommitTree(t, nextTree, fmt.Sprintf("commit-%02d", i), parent)
- testRepo.Run(t, "update-ref", "refs/heads/main", nextCommit.String())
- parent = nextCommit
-
- _ = nextBlob
- _ = nextTree
- }
-
- testRepo.Repack(t, "-a", "-d", "-f", "--window=64", "--depth=64")
-
- return testRepo, []objectid.ObjectID{
- blobID,
- treeID,
- commitID,
- tagID,
- parent,
- }
-}
diff --git a/object/store/packed/internal/reading/idx.go b/object/store/packed/internal/reading/idx.go
deleted file mode 100644
index 3c91e1a2..00000000
--- a/object/store/packed/internal/reading/idx.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package reading
-
-import (
- "os"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-// idxFile stores one mapped and validated idx v2 file.
-type idxFile struct {
- // idxName is the basename of this .idx file.
- idxName string
- // packName is the matching .pack basename.
- packName string
- // algo is the hash algorithm encoded by the index.
- algo objectid.Algorithm
-
- // file is the opened index file descriptor.
- file *os.File
- // data is the mapped index bytes.
- data []byte
-
- // fanout stores fanout table values.
- fanout [256]uint32
- // numObjects equals fanout[255].
- numObjects int
-
- // namesOffset starts the sorted object-id table.
- namesOffset int
- // offset32Offset starts the 32-bit offset table.
- offset32Offset int
- // offset64Offset starts the 64-bit offset table.
- offset64Offset int
- // offset64Count is the number of 64-bit offset entries.
- offset64Count int
-}
diff --git a/object/store/packed/internal/reading/idx_candidates_mru.go b/object/store/packed/internal/reading/idx_candidates_mru.go
deleted file mode 100644
index 08ab6f85..00000000
--- a/object/store/packed/internal/reading/idx_candidates_mru.go
+++ /dev/null
@@ -1,136 +0,0 @@
-package reading
-
-// packCandidateNode is one node in the candidate MRU order list.
-type packCandidateNode struct {
- packName string
- prev *packCandidateNode
- next *packCandidateNode
-}
-
-func (store *Store) reconcileMRU(candidates []packCandidate) {
- store.mruMu.Lock()
- defer store.mruMu.Unlock()
-
- if store.mruNodeByPack == nil {
- store.mruNodeByPack = make(map[string]*packCandidateNode, len(candidates))
- }
-
- present := make(map[string]struct{}, len(candidates))
- for _, candidate := range candidates {
- present[candidate.packName] = struct{}{}
- }
-
- ordered := make([]string, 0, len(candidates))
-
- for node := store.mruHead; node != nil; node = node.next {
- if _, ok := present[node.packName]; !ok {
- continue
- }
-
- ordered = append(ordered, node.packName)
- delete(present, node.packName)
- }
-
- for _, candidate := range candidates {
- if _, ok := present[candidate.packName]; !ok {
- continue
- }
-
- ordered = append(ordered, candidate.packName)
- delete(present, candidate.packName)
- }
-
- store.mruHead = nil
- store.mruTail = nil
- store.mruNodeByPack = make(map[string]*packCandidateNode, len(ordered))
-
- for _, packName := range ordered {
- node := &packCandidateNode{
- packName: packName,
- prev: store.mruTail,
- }
- if store.mruTail != nil {
- store.mruTail.next = node
- }
-
- if store.mruHead == nil {
- store.mruHead = node
- }
-
- store.mruTail = node
- store.mruNodeByPack[packName] = node
- }
-}
-
-// touchCandidate moves one candidate to the front of the lookup order.
-// This is done on a best-effort basis.
-func (store *Store) touchCandidate(packName string) {
- if !store.mruMu.TryLock() {
- return
- }
- defer store.mruMu.Unlock()
-
- node := store.mruNodeByPack[packName]
- if node == nil || node == store.mruHead {
- return
- }
-
- if node.prev != nil {
- node.prev.next = node.next
- }
-
- if node.next != nil {
- node.next.prev = node.prev
- }
-
- if store.mruTail == node {
- store.mruTail = node.prev
- }
-
- node.prev = nil
-
- node.next = store.mruHead
- if store.mruHead != nil {
- store.mruHead.prev = node
- }
-
- store.mruHead = node
- if store.mruTail == nil {
- store.mruTail = node
- }
-}
-
-// firstCandidatePackName returns the current head pack name, or "" when none
-// are available.
-func (store *Store) firstCandidatePackName(snapshot *candidateSnapshot) string {
- store.mruMu.RLock()
- defer store.mruMu.RUnlock()
-
- for node := store.mruHead; node != nil; node = node.next {
- if _, ok := snapshot.candidateByPack[node.packName]; ok {
- return node.packName
- }
- }
-
- return ""
-}
-
-// nextCandidatePackName returns the pack name after currentPack in current MRU
-// order, or "" at end / when currentPack is not present.
-func (store *Store) nextCandidatePackName(currentPack string, snapshot *candidateSnapshot) string {
- store.mruMu.RLock()
- defer store.mruMu.RUnlock()
-
- node := store.mruNodeByPack[currentPack]
- if node == nil {
- return ""
- }
-
- for node = node.next; node != nil; node = node.next {
- if _, ok := snapshot.candidateByPack[node.packName]; ok {
- return node.packName
- }
- }
-
- return ""
-}
diff --git a/object/store/packed/internal/reading/idx_close.go b/object/store/packed/internal/reading/idx_close.go
deleted file mode 100644
index 1590854c..00000000
--- a/object/store/packed/internal/reading/idx_close.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package reading
-
-import "syscall"
-
-// close unmaps and closes one idx handle.
-func (index *idxFile) close() error {
- var closeErr error
-
- if index.data != nil {
- err := syscall.Munmap(index.data)
- if err != nil && closeErr == nil {
- closeErr = err
- }
-
- index.data = nil
- }
-
- if index.file != nil {
- err := index.file.Close()
- if err != nil && closeErr == nil {
- closeErr = err
- }
-
- index.file = nil
- }
-
- return closeErr
-}
diff --git a/object/store/packed/internal/reading/idx_lookup.go b/object/store/packed/internal/reading/idx_lookup.go
deleted file mode 100644
index bb02fb20..00000000
--- a/object/store/packed/internal/reading/idx_lookup.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package reading
-
-import (
- "bytes"
- "encoding/binary"
- "fmt"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-// lookup resolves one object ID to its pack offset within this index.
-func (index *idxFile) lookup(id objectid.ObjectID) (uint64, bool, error) {
- if id.Algorithm() != index.algo {
- return 0, false, fmt.Errorf("objectstore/packed: object id algorithm mismatch")
- }
-
- idBytes := (&id).RawBytes()
-
- hashSize := len(idBytes)
- if hashSize != index.algo.Size() {
- return 0, false, fmt.Errorf("objectstore/packed: unexpected object id length")
- }
-
- first := int(idBytes[0])
-
- lo := 0
- if first > 0 {
- lo = int(index.fanout[first-1])
- }
-
- hi := int(index.fanout[first])
- if lo < 0 || hi < 0 || lo > hi || hi > index.numObjects {
- return 0, false, fmt.Errorf("objectstore/packed: idx %q has invalid fanout bounds", index.idxName)
- }
-
- for lo < hi {
- mid := lo + (hi-lo)/2
-
- nameOffset := index.namesOffset + mid*hashSize
- if nameOffset < 0 || nameOffset+hashSize > len(index.data) {
- return 0, false, fmt.Errorf("objectstore/packed: idx %q truncated name table", index.idxName)
- }
-
- cmp := bytes.Compare(index.data[nameOffset:nameOffset+hashSize], idBytes)
- if cmp == 0 {
- offset, err := index.offsetAt(mid)
- if err != nil {
- return 0, false, err
- }
-
- return offset, true, nil
- }
-
- if cmp < 0 {
- lo = mid + 1
- } else {
- hi = mid
- }
- }
-
- return 0, false, nil
-}
-
-// offsetAt resolves the pack offset for one object index entry.
-func (index *idxFile) offsetAt(objectIndex int) (uint64, error) {
- if objectIndex < 0 || objectIndex >= index.numObjects {
- return 0, fmt.Errorf("objectstore/packed: idx %q offset index out of bounds", index.idxName)
- }
-
- wordOffset := index.offset32Offset + objectIndex*4
- if wordOffset < 0 || wordOffset+4 > len(index.data) {
- return 0, fmt.Errorf("objectstore/packed: idx %q truncated 32-bit offset table", index.idxName)
- }
-
- word := binary.BigEndian.Uint32(index.data[wordOffset : wordOffset+4])
- if word&0x80000000 == 0 {
- return uint64(word), nil
- }
-
- pos := int(word & 0x7fffffff)
- if pos < 0 || pos >= index.offset64Count {
- return 0, fmt.Errorf("objectstore/packed: idx %q invalid 64-bit offset position", index.idxName)
- }
-
- offOffset := index.offset64Offset + pos*8
- if offOffset < 0 || offOffset+8 > len(index.data)-2*index.algo.Size() {
- return 0, fmt.Errorf("objectstore/packed: idx %q truncated 64-bit offset table", index.idxName)
- }
-
- return binary.BigEndian.Uint64(index.data[offOffset : offOffset+8]), nil
-}
diff --git a/object/store/packed/internal/reading/idx_lookup_candidates.go b/object/store/packed/internal/reading/idx_lookup_candidates.go
deleted file mode 100644
index c89ada7a..00000000
--- a/object/store/packed/internal/reading/idx_lookup_candidates.go
+++ /dev/null
@@ -1,126 +0,0 @@
-package reading
-
-import (
- "fmt"
- "os"
- "slices"
- "strings"
-)
-
-// packCandidate describes one discovered pack/index pair.
-type packCandidate struct {
- // packName is the .pack basename.
- packName string
- // idxName is the .idx basename.
- idxName string
- // mtime is the pack file modification time for initial ordering.
- mtime int64
-}
-
-type candidateSnapshot struct {
- candidates []packCandidate
- candidateByPack map[string]packCandidate
-}
-
-// Refresh rescans objects/pack and atomically installs a fresh candidate list
-// for future lookups.
-//
-// Refresh does not invalidate existing readers. Cached pack/index mappings,
-// including ones for previously visible candidates, may be retained until
-// Close.
-func (store *Store) Refresh() error {
- store.refreshMu.Lock()
- defer store.refreshMu.Unlock()
-
- candidates, err := store.discoverCandidates()
- if err != nil {
- return err
- }
-
- candidateByPack := make(map[string]packCandidate, len(candidates))
- for _, candidate := range candidates {
- candidateByPack[candidate.packName] = candidate
- }
-
- store.reconcileMRU(candidates)
-
- store.candidates.Store(&candidateSnapshot{
- candidates: candidates,
- candidateByPack: candidateByPack,
- })
-
- return nil
-}
-
-func (store *Store) ensureCandidates() (*candidateSnapshot, error) {
- snapshot := store.candidates.Load()
- if snapshot != nil {
- return snapshot, nil
- }
-
- err := store.Refresh()
- if err != nil {
- return nil, err
- }
-
- return store.candidates.Load(), nil
-}
-
-// discoverCandidates scans the objects/pack root and returns sorted pack/index
-// pairs.
-func (store *Store) discoverCandidates() ([]packCandidate, error) {
- dir, err := store.root.Open(".")
- if err != nil {
- if os.IsNotExist(err) {
- return nil, nil
- }
-
- return nil, err
- }
-
- defer func() { _ = dir.Close() }()
-
- entries, err := dir.ReadDir(-1)
- if err != nil {
- return nil, err
- }
-
- candidates := make([]packCandidate, 0, len(entries))
- for _, entry := range entries {
- if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".idx") {
- continue
- }
-
- idxName := entry.Name()
- packName := strings.TrimSuffix(idxName, ".idx") + ".pack"
-
- packInfo, err := store.root.Stat(packName)
- if err != nil {
- if os.IsNotExist(err) {
- return nil, fmt.Errorf("objectstore/packed: missing pack file for index %q", idxName)
- }
-
- return nil, err
- }
-
- candidates = append(candidates, packCandidate{
- packName: packName,
- idxName: idxName,
- mtime: packInfo.ModTime().UnixNano(),
- })
- }
-
- slices.SortFunc(candidates, func(a, b packCandidate) int {
- if a.mtime != b.mtime {
- if a.mtime > b.mtime {
- return -1
- }
-
- return 1
- }
-
- return strings.Compare(a.packName, b.packName)
- })
-
- return candidates, nil
-}
diff --git a/object/store/packed/internal/reading/idx_open.go b/object/store/packed/internal/reading/idx_open.go
deleted file mode 100644
index 8f73c867..00000000
--- a/object/store/packed/internal/reading/idx_open.go
+++ /dev/null
@@ -1,98 +0,0 @@
-package reading
-
-import (
- "fmt"
- "os"
- "syscall"
-
- "codeberg.org/lindenii/furgit/internal/intconv"
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-// openIndex returns one opened and parsed index, caching it by pack basename.
-func (store *Store) openIndex(candidate packCandidate) (*idxFile, error) {
- store.idxMu.RLock()
-
- index, ok := store.idxByPack[candidate.packName]
- if ok {
- store.idxMu.RUnlock()
-
- return index, nil
- }
-
- store.idxMu.RUnlock()
-
- index, err := openIdxFile(store.root, candidate.idxName, candidate.packName, store.algo)
- if err != nil {
- return nil, err
- }
-
- store.idxMu.Lock()
-
- existing, ok := store.idxByPack[candidate.packName]
- if ok {
- store.idxMu.Unlock()
-
- _ = index.close()
-
- return existing, nil
- }
-
- store.idxByPack[candidate.packName] = index
- store.idxMu.Unlock()
-
- return index, nil
-}
-
-// openIdxFile maps and validates one idx v2 file.
-func openIdxFile(root *os.Root, idxName, packName string, algo objectid.Algorithm) (*idxFile, error) {
- file, err := root.Open(idxName)
- if err != nil {
- return nil, err
- }
-
- info, err := file.Stat()
- if err != nil {
- _ = file.Close()
-
- return nil, err
- }
-
- size := info.Size()
- if size < 0 || size > int64(int(^uint(0)>>1)) {
- _ = file.Close()
-
- return nil, fmt.Errorf("objectstore/packed: idx %q has unsupported size", idxName)
- }
-
- fd, err := intconv.UintptrToInt(file.Fd())
- if err != nil {
- _ = file.Close()
-
- return nil, err
- }
-
- data, err := syscall.Mmap(fd, 0, int(size), syscall.PROT_READ, syscall.MAP_PRIVATE)
- if err != nil {
- _ = file.Close()
-
- return nil, err
- }
-
- index := &idxFile{
- idxName: idxName,
- packName: packName,
- algo: algo,
- file: file,
- data: data,
- }
-
- err = index.parse()
- if err != nil {
- _ = index.close()
-
- return nil, err
- }
-
- return index, nil
-}
diff --git a/object/store/packed/internal/reading/idx_parse.go b/object/store/packed/internal/reading/idx_parse.go
deleted file mode 100644
index d38aaf4d..00000000
--- a/object/store/packed/internal/reading/idx_parse.go
+++ /dev/null
@@ -1,78 +0,0 @@
-package reading
-
-import (
- "encoding/binary"
- "fmt"
-)
-
-const (
- idxMagicV2 = 0xff744f63
- idxVersionV2 = 2
-)
-
-// parse validates mapped idx v2 structure and stores table boundaries.
-func (index *idxFile) parse() error {
- hashSize := index.algo.Size()
- if hashSize <= 0 {
- return fmt.Errorf("objectstore/packed: idx %q has invalid hash algorithm", index.idxName)
- }
-
- minLen := 8 + 256*4 + 2*hashSize
- if len(index.data) < minLen {
- return fmt.Errorf("objectstore/packed: idx %q too short", index.idxName)
- }
-
- if binary.BigEndian.Uint32(index.data[:4]) != idxMagicV2 {
- return fmt.Errorf("objectstore/packed: idx %q invalid magic", index.idxName)
- }
-
- if binary.BigEndian.Uint32(index.data[4:8]) != idxVersionV2 {
- return fmt.Errorf("objectstore/packed: idx %q unsupported version", index.idxName)
- }
-
- prev := uint32(0)
-
- for i := range 256 {
- base := 8 + i*4
-
- cur := binary.BigEndian.Uint32(index.data[base : base+4])
- if cur < prev {
- return fmt.Errorf("objectstore/packed: idx %q has non-monotonic fanout table", index.idxName)
- }
-
- index.fanout[i] = cur
- prev = cur
- }
-
- index.numObjects = int(index.fanout[255])
- if index.numObjects < 0 {
- return fmt.Errorf("objectstore/packed: idx %q has invalid object count", index.idxName)
- }
-
- namesBytes := index.numObjects * hashSize
- crcBytes := index.numObjects * 4
- offset32Bytes := index.numObjects * 4
-
- minSize := 8 + 256*4 + namesBytes + crcBytes + offset32Bytes + 2*hashSize
- if minSize < 0 || len(index.data) < minSize {
- return fmt.Errorf("objectstore/packed: idx %q has truncated tables", index.idxName)
- }
-
- index.namesOffset = 8 + 256*4
- index.offset32Offset = index.namesOffset + namesBytes + crcBytes
- index.offset64Offset = index.offset32Offset + offset32Bytes
-
- offset64Bytes := len(index.data) - index.offset64Offset - 2*hashSize
- if offset64Bytes < 0 || offset64Bytes%8 != 0 {
- return fmt.Errorf("objectstore/packed: idx %q has malformed 64-bit offset table", index.idxName)
- }
-
- index.offset64Count = offset64Bytes / 8
-
- maxOffset64Count := max(index.numObjects-1, 0)
- if index.offset64Count > maxOffset64Count {
- return fmt.Errorf("objectstore/packed: idx %q has oversized 64-bit offset table", index.idxName)
- }
-
- return nil
-}
diff --git a/object/store/packed/internal/reading/location.go b/object/store/packed/internal/reading/location.go
deleted file mode 100644
index f315dd1d..00000000
--- a/object/store/packed/internal/reading/location.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package reading
-
-// location identifies one object entry in a specific pack file.
-type location struct {
- packName string
- offset uint64
-}
diff --git a/object/store/packed/internal/reading/new.go b/object/store/packed/internal/reading/new.go
deleted file mode 100644
index d8a12db3..00000000
--- a/object/store/packed/internal/reading/new.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package reading
-
-import (
- "fmt"
- "os"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-// New creates a packed-object store rooted at an objects/pack directory.
-//
-// Labels: Deps-Borrowed, Life-Parent.
-func New(root *os.Root, algo objectid.Algorithm, opts Options) (*Store, error) {
- if algo.Size() == 0 {
- return nil, objectid.ErrInvalidAlgorithm
- }
-
- switch opts.RefreshPolicy {
- case RefreshPolicyOnMissing, RefreshPolicyNever:
- default:
- return nil, fmt.Errorf("objectstore/packed: invalid refresh policy %d", opts.RefreshPolicy)
- }
-
- return &Store{
- root: root,
- algo: algo,
- refreshPolicy: opts.RefreshPolicy,
- mruNodeByPack: make(map[string]*packCandidateNode),
- idxByPack: make(map[string]*idxFile),
- packs: make(map[string]*packFile),
- deltaCache: newDeltaCache(defaultDeltaCacheMaxBytes),
- }, nil
-}
diff --git a/object/store/packed/internal/reading/options.go b/object/store/packed/internal/reading/options.go
deleted file mode 100644
index 0c5b76af..00000000
--- a/object/store/packed/internal/reading/options.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package reading
-
-// RefreshPolicy configures when candidate pack/index discovery refreshes.
-type RefreshPolicy uint8
-
-const (
- // RefreshPolicyOnMissing refreshes candidates once after a lookup miss.
- RefreshPolicyOnMissing RefreshPolicy = iota
- // RefreshPolicyNever disables automatic refresh after lookup misses.
- RefreshPolicyNever
-)
-
-// Options configures a packed object store.
-type Options struct {
- RefreshPolicy RefreshPolicy
-}
diff --git a/object/store/packed/internal/reading/pack.go b/object/store/packed/internal/reading/pack.go
deleted file mode 100644
index 431ed5f9..00000000
--- a/object/store/packed/internal/reading/pack.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package reading
-
-import (
- "encoding/binary"
- "fmt"
- "os"
- "syscall"
-
- packfmt "codeberg.org/lindenii/furgit/format/packfile"
- "codeberg.org/lindenii/furgit/internal/intconv"
-)
-
-// packFile stores one mapped and validated .pack file.
-type packFile struct {
- // name is the .pack basename.
- name string
- // file is the opened pack file descriptor.
- file *os.File
- // data is the mapped pack bytes.
- data []byte
-}
-
-// openPackFile maps and validates one pack file.
-func openPackFile(name string, file *os.File, size int64) (*packFile, error) {
- if size < 12 {
- return nil, fmt.Errorf("objectstore/packed: pack %q too short", name)
- }
-
- if size > int64(int(^uint(0)>>1)) {
- return nil, fmt.Errorf("objectstore/packed: pack %q has unsupported size", name)
- }
-
- fd, err := intconv.UintptrToInt(file.Fd())
- if err != nil {
- return nil, err
- }
-
- data, err := syscall.Mmap(fd, 0, int(size), syscall.PROT_READ, syscall.MAP_PRIVATE)
- if err != nil {
- return nil, err
- }
-
- if binary.BigEndian.Uint32(data[:4]) != packfmt.Signature {
- _ = syscall.Munmap(data)
-
- return nil, fmt.Errorf("objectstore/packed: pack %q invalid signature", name)
- }
-
- version := binary.BigEndian.Uint32(data[4:8])
- if !packfmt.SupportedVersion(version) {
- _ = syscall.Munmap(data)
-
- return nil, fmt.Errorf("objectstore/packed: pack %q unsupported version %d", name, version)
- }
-
- return &packFile{name: name, file: file, data: data}, nil
-}
-
-// close unmaps and closes one pack handle.
-func (pack *packFile) close() error {
- var closeErr error
-
- if pack.data != nil {
- err := syscall.Munmap(pack.data)
- if err != nil && closeErr == nil {
- closeErr = err
- }
-
- pack.data = nil
- }
-
- if pack.file != nil {
- err := pack.file.Close()
- if err != nil && closeErr == nil {
- closeErr = err
- }
-
- pack.file = nil
- }
-
- return closeErr
-}
diff --git a/object/store/packed/internal/reading/pack_idx_checksum.go b/object/store/packed/internal/reading/pack_idx_checksum.go
deleted file mode 100644
index b2ad09f1..00000000
--- a/object/store/packed/internal/reading/pack_idx_checksum.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package reading
-
-import (
- "bytes"
- "fmt"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-// verifyMappedPackMatchesMappedIdx compares one mapped pack trailer hash with
-// the pack hash recorded in one mapped idx trailer.
-func verifyMappedPackMatchesMappedIdx(packData, idxData []byte, algo objectid.Algorithm) error {
- hashSize := algo.Size()
- if hashSize <= 0 {
- return objectid.ErrInvalidAlgorithm
- }
-
- if len(packData) < hashSize {
- return fmt.Errorf("objectstore/packed: pack too short for trailer hash")
- }
-
- if len(idxData) < hashSize*2 {
- return fmt.Errorf("objectstore/packed: idx too short for trailer hashes")
- }
-
- packTrailerHash := packData[len(packData)-hashSize:]
-
- idxPackHash := idxData[len(idxData)-hashSize*2 : len(idxData)-hashSize]
- if !bytes.Equal(packTrailerHash, idxPackHash) {
- return fmt.Errorf("objectstore/packed: pack hash does not match idx")
- }
-
- return nil
-}
diff --git a/object/store/packed/internal/reading/read_bytes.go b/object/store/packed/internal/reading/read_bytes.go
deleted file mode 100644
index f0821687..00000000
--- a/object/store/packed/internal/reading/read_bytes.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package reading
-
-import (
- "fmt"
-
- objectheader "codeberg.org/lindenii/furgit/object/header"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ReadBytesContent reads an object's type and content bytes.
-//
-// It fully resolves the requested object bytes. For base pack entries, this
-// includes verifying that the zlib stream inflates to exactly the declared
-// object size and verifying the Adler-32 trailer.
-func (store *Store) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) {
- loc, err := store.lookup(id)
- if err != nil {
- return objecttype.TypeInvalid, nil, err
- }
-
- return store.deltaResolveContent(loc)
-}
-
-// ReadBytesFull reads a full serialized object as "type size\0content".
-//
-// Like ReadBytesContent, it fully resolves the requested object bytes. For
-// base pack entries, this includes verifying that the zlib stream inflates to
-// exactly the declared object size and verifying the Adler-32 trailer.
-func (store *Store) ReadBytesFull(id objectid.ObjectID) ([]byte, error) {
- ty, content, err := store.ReadBytesContent(id)
- if err != nil {
- return nil, err
- }
-
- header, ok := objectheader.Encode(ty, int64(len(content)))
- if !ok {
- return nil, fmt.Errorf("objectstore/packed: failed to encode object header for type %d", ty)
- }
-
- out := make([]byte, len(header)+len(content))
- copy(out, header)
- copy(out[len(header):], content)
-
- return out, nil
-}
diff --git a/object/store/packed/internal/reading/read_closer.go b/object/store/packed/internal/reading/read_closer.go
deleted file mode 100644
index 4ef4c039..00000000
--- a/object/store/packed/internal/reading/read_closer.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package reading
-
-import "io"
-
-// readCloser proxies reads and closes one underlying closer.
-type readCloser struct {
- reader io.Reader
- closer io.Closer
-}
-
-// Read proxies reads to the underlying reader.
-func (reader *readCloser) Read(dst []byte) (int, error) {
- return reader.reader.Read(dst)
-}
-
-// Close closes the underlying closer.
-func (reader *readCloser) Close() error {
- return reader.closer.Close()
-}
diff --git a/object/store/packed/internal/reading/read_header.go b/object/store/packed/internal/reading/read_header.go
deleted file mode 100644
index d627a6b3..00000000
--- a/object/store/packed/internal/reading/read_header.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package reading
-
-import (
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ReadHeader reads an object's type and declared content size.
-//
-// It resolves header metadata only. It does not verify that the full pack entry
-// payload is readable and does not verify any zlib Adler-32 trailer for
-// compressed entry data.
-func (store *Store) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) {
- loc, err := store.lookup(id)
- if err != nil {
- return objecttype.TypeInvalid, 0, err
- }
-
- return store.resolveHeaderAt(loc)
-}
diff --git a/object/store/packed/internal/reading/read_header_resolve.go b/object/store/packed/internal/reading/read_header_resolve.go
deleted file mode 100644
index a2916b73..00000000
--- a/object/store/packed/internal/reading/read_header_resolve.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package reading
-
-import (
- "fmt"
-
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// resolveHeaderAt resolves one object's canonical type and declared content size.
-func (store *Store) resolveHeaderAt(start location) (objecttype.Type, int64, error) {
- visited := make(map[location]struct{})
- current := start
- declaredSize := int64(-1)
-
- for {
- if _, ok := visited[current]; ok {
- return objecttype.TypeInvalid, 0, fmt.Errorf("objectstore/packed: delta cycle while resolving object header")
- }
-
- visited[current] = struct{}{}
-
- pack, meta, err := store.entryMetaAt(current)
- if err != nil {
- return objecttype.TypeInvalid, 0, err
- }
-
- if declaredSize < 0 {
- if meta.ty.IsBaseObject() {
- declaredSize = meta.size
- } else {
- size, err := deltaDeclaredSizeAt(pack, meta.dataOffset)
- if err != nil {
- return objecttype.TypeInvalid, 0, err
- }
-
- declaredSize = size
- }
- }
-
- if meta.ty.IsBaseObject() {
- return meta.ty, declaredSize, nil
- }
-
- switch meta.ty {
- case objecttype.TypeRefDelta:
- next, err := store.lookup(meta.baseRefID)
- if err != nil {
- return objecttype.TypeInvalid, 0, err
- }
-
- current = next
- case objecttype.TypeOfsDelta:
- current = location{
- packName: current.packName,
- offset: meta.baseOfs,
- }
- case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag:
- return objecttype.TypeInvalid, 0, fmt.Errorf("objectstore/packed: internal invariant violation for base type %d", meta.ty)
- case objecttype.TypeInvalid, objecttype.TypeFuture:
- return objecttype.TypeInvalid, 0, fmt.Errorf("objectstore/packed: unsupported pack type %d", meta.ty)
- default:
- return objecttype.TypeInvalid, 0, fmt.Errorf("objectstore/packed: unsupported pack type %d", meta.ty)
- }
- }
-}
diff --git a/object/store/packed/internal/reading/read_reader.go b/object/store/packed/internal/reading/read_reader.go
deleted file mode 100644
index 3fa0f592..00000000
--- a/object/store/packed/internal/reading/read_reader.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package reading
-
-import (
- "bytes"
- "fmt"
- "io"
-
- "codeberg.org/lindenii/furgit/internal/iolimit"
- objectheader "codeberg.org/lindenii/furgit/object/header"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ReadReaderContent reads an object's type, declared content size, and content
-// stream.
-//
-// Close releases reader-local resources only. It does not drain unread data for
-// additional validation. In particular, malformed trailing compressed data,
-// trailing bytes past the declared object size, and the zlib Adler-32 trailer
-// may go unverified unless the caller reads to io.EOF.
-func (store *Store) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) {
- loc, err := store.lookup(id)
- if err != nil {
- return objecttype.TypeInvalid, 0, nil, err
- }
-
- pack, meta, err := store.entryMetaAt(loc)
- if err != nil {
- return objecttype.TypeInvalid, 0, nil, err
- }
-
- if meta.ty.IsBaseObject() {
- zr, err := zlibReaderAt(pack, meta.dataOffset)
- if err != nil {
- return objecttype.TypeInvalid, 0, nil, err
- }
-
- return meta.ty, meta.size, &readCloser{
- reader: iolimit.ExpectLengthReader(zr, meta.size),
- closer: zr,
- }, nil
- }
-
- ty, content, err := store.deltaResolveContent(loc)
- if err != nil {
- return objecttype.TypeInvalid, 0, nil, err
- }
-
- return ty, int64(len(content)), io.NopCloser(bytes.NewReader(content)), nil
-}
-
-// ReadReaderFull reads a full serialized object stream as "type size\0content".
-//
-// Close releases reader-local resources only. It does not drain unread data for
-// additional validation. In particular, malformed trailing compressed data,
-// trailing bytes past the declared object size, and the zlib Adler-32 trailer
-// may go unverified unless the caller reads to io.EOF.
-func (store *Store) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) {
- loc, err := store.lookup(id)
- if err != nil {
- return nil, err
- }
-
- pack, meta, err := store.entryMetaAt(loc)
- if err != nil {
- return nil, err
- }
-
- if meta.ty.IsBaseObject() {
- header, ok := objectheader.Encode(meta.ty, meta.size)
- if !ok {
- return nil, fmt.Errorf("objectstore/packed: failed to encode object header for type %d", meta.ty)
- }
-
- zr, err := zlibReaderAt(pack, meta.dataOffset)
- if err != nil {
- return nil, err
- }
-
- return &readCloser{
- reader: io.MultiReader(bytes.NewReader(header), iolimit.ExpectLengthReader(zr, meta.size)),
- closer: zr,
- }, nil
- }
-
- raw, err := store.ReadBytesFull(id)
- if err != nil {
- return nil, err
- }
-
- return io.NopCloser(bytes.NewReader(raw)), nil
-}
diff --git a/object/store/packed/internal/reading/read_size.go b/object/store/packed/internal/reading/read_size.go
deleted file mode 100644
index 3c1e05b1..00000000
--- a/object/store/packed/internal/reading/read_size.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package reading
-
-import (
- "fmt"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ReadSize reads an object's declared content size.
-//
-// Like ReadHeader, it resolves header metadata only. It does not verify that
-// the full pack entry payload is readable and does not verify any zlib
-// Adler-32 trailer for compressed entry data.
-func (store *Store) ReadSize(id objectid.ObjectID) (int64, error) {
- loc, err := store.lookup(id)
- if err != nil {
- return 0, err
- }
-
- return store.resolveSizeAt(loc)
-}
-
-// resolveSizeAt resolves one object's declared content size from location.
-func (store *Store) resolveSizeAt(start location) (int64, error) {
- pack, meta, err := store.entryMetaAt(start)
- if err != nil {
- return 0, err
- }
-
- if meta.ty.IsBaseObject() {
- return meta.size, nil
- }
-
- switch meta.ty {
- case objecttype.TypeRefDelta, objecttype.TypeOfsDelta:
- return deltaDeclaredSizeAt(pack, meta.dataOffset)
- case objecttype.TypeInvalid, objecttype.TypeFuture:
- return 0, fmt.Errorf("objectstore/packed: unsupported pack type %d", meta.ty)
- case objecttype.TypeCommit, objecttype.TypeTree, objecttype.TypeBlob, objecttype.TypeTag:
- return 0, fmt.Errorf("objectstore/packed: internal invariant violation for base type %d", meta.ty)
- default:
- return 0, fmt.Errorf("objectstore/packed: unsupported pack type %d", meta.ty)
- }
-}
diff --git a/object/store/packed/internal/reading/read_test.go b/object/store/packed/internal/reading/read_test.go
deleted file mode 100644
index 8a92b603..00000000
--- a/object/store/packed/internal/reading/read_test.go
+++ /dev/null
@@ -1,301 +0,0 @@
-package reading_test
-
-import (
- "bytes"
- "errors"
- "fmt"
- "io/fs"
- "strconv"
- "strings"
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
- "codeberg.org/lindenii/furgit/object/store/packed"
-)
-
-func TestPackedStoreReadAgainstGit(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- testRepo, ids := createPackedFixtureRepo(t, algo)
- store := openPackedStore(t, testRepo, algo)
-
- for _, id := range ids {
- t.Run(id.String(), func(t *testing.T) {
- wantType, wantBody, wantRaw := expectedRawObject(t, testRepo, id)
-
- gotHeaderType, gotHeaderSize, err := store.ReadHeader(id)
- if err != nil {
- t.Fatalf("ReadHeader: %v", err)
- }
-
- if gotHeaderType != wantType {
- t.Fatalf("ReadHeader type = %v, want %v", gotHeaderType, wantType)
- }
-
- if gotHeaderSize != int64(len(wantBody)) {
- t.Fatalf("ReadHeader size = %d, want %d", gotHeaderSize, len(wantBody))
- }
-
- gotSize, err := store.ReadSize(id)
- if err != nil {
- t.Fatalf("ReadSize: %v", err)
- }
-
- if gotSize != int64(len(wantBody)) {
- t.Fatalf("ReadSize = %d, want %d", gotSize, len(wantBody))
- }
-
- gotRaw, err := store.ReadBytesFull(id)
- if err != nil {
- t.Fatalf("ReadBytesFull: %v", err)
- }
-
- if !bytes.Equal(gotRaw, wantRaw) {
- t.Fatalf("ReadBytesFull mismatch")
- }
-
- gotType, gotBody, err := store.ReadBytesContent(id)
- if err != nil {
- t.Fatalf("ReadBytesContent: %v", err)
- }
-
- if gotType != wantType {
- t.Fatalf("ReadBytesContent type = %v, want %v", gotType, wantType)
- }
-
- if !bytes.Equal(gotBody, wantBody) {
- t.Fatalf("ReadBytesContent mismatch")
- }
-
- fullReader, err := store.ReadReaderFull(id)
- if err != nil {
- t.Fatalf("ReadReaderFull: %v", err)
- }
-
- got := mustReadAllAndClose(t, fullReader)
- if !bytes.Equal(got, wantRaw) {
- t.Fatalf("ReadReaderFull mismatch")
- }
-
- contentType, contentSize, contentReader, err := store.ReadReaderContent(id)
- if err != nil {
- t.Fatalf("ReadReaderContent: %v", err)
- }
-
- if contentType != wantType {
- t.Fatalf("ReadReaderContent type = %v, want %v", contentType, wantType)
- }
-
- if contentSize != int64(len(wantBody)) {
- t.Fatalf("ReadReaderContent size = %d, want %d", contentSize, len(wantBody))
- }
-
- got = mustReadAllAndClose(t, contentReader)
- if !bytes.Equal(got, wantBody) {
- t.Fatalf("ReadReaderContent mismatch")
- }
- })
- }
- })
-}
-
-func TestPackedStoreErrors(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- testRepo, _ := createPackedFixtureRepo(t, algo)
- store := openPackedStore(t, testRepo, algo)
-
- notFoundID, err := objectid.ParseHex(algo, strings.Repeat("0", algo.HexLen()))
- if err != nil {
- t.Fatalf("ParseHex(notFound): %v", err)
- }
-
- _, err = store.ReadBytesFull(notFoundID)
- if !errors.Is(err, objectstore.ErrObjectNotFound) {
- t.Fatalf("ReadBytesFull not-found error = %v", err)
- }
-
- _, _, err = store.ReadBytesContent(notFoundID)
- if !errors.Is(err, objectstore.ErrObjectNotFound) {
- t.Fatalf("ReadBytesContent not-found error = %v", err)
- }
-
- _, err = store.ReadReaderFull(notFoundID)
- if !errors.Is(err, objectstore.ErrObjectNotFound) {
- t.Fatalf("ReadReaderFull not-found error = %v", err)
- }
-
- _, _, _, err = store.ReadReaderContent(notFoundID)
- if !errors.Is(err, objectstore.ErrObjectNotFound) {
- t.Fatalf("ReadReaderContent not-found error = %v", err)
- }
-
- _, _, err = store.ReadHeader(notFoundID)
- if !errors.Is(err, objectstore.ErrObjectNotFound) {
- t.Fatalf("ReadHeader not-found error = %v", err)
- }
-
- _, err = store.ReadSize(notFoundID)
- if !errors.Is(err, objectstore.ErrObjectNotFound) {
- t.Fatalf("ReadSize not-found error = %v", err)
- }
-
- var otherAlgo objectid.Algorithm
-
- for _, candidate := range objectid.SupportedAlgorithms() {
- if candidate != algo {
- otherAlgo = candidate
-
- break
- }
- }
-
- if otherAlgo != objectid.AlgorithmUnknown {
- mismatchID, err := objectid.ParseHex(otherAlgo, strings.Repeat("0", otherAlgo.HexLen()))
- if err != nil {
- t.Fatalf("ParseHex(mismatch): %v", err)
- }
-
- _, err = store.ReadBytesFull(mismatchID)
- if err == nil || !strings.Contains(err.Error(), "algorithm mismatch") {
- t.Fatalf("ReadBytesFull algorithm-mismatch error = %v", err)
- }
- }
- })
-}
-
-func TestPackedStoreNewValidation(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- testRepo, _ := createPackedFixtureRepo(t, algo)
-
- store := openPackedStore(t, testRepo, algo)
-
- err := store.Close()
- if err != nil {
- t.Fatalf("Close: %v", err)
- }
- })
-}
-
-func TestPackedStoreInvalidAlgorithm(t *testing.T) {
- t.Parallel()
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectid.AlgorithmSHA1, Bare: true})
-
- root := testRepo.OpenPackRoot(t)
-
- _, err := packed.New(root, objectid.AlgorithmUnknown, packed.Options{})
- if !errors.Is(err, objectid.ErrInvalidAlgorithm) {
- t.Fatalf("packed.New invalid algorithm error = %v", err)
- }
-}
-
-func TestPackedStoreReadHeaderUsesResolvedObjectSizeForDelta(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
-
- var parent objectid.ObjectID
-
- for i := range 96 {
- content := strings.Repeat("common-line-"+strconv.Itoa(i%7)+"\n", 384) + fmt.Sprintf("tail-%03d\n", i)
-
- _, treeID := testRepo.MakeSingleFileTree(t, "file.txt", []byte(content))
- if i == 0 {
- parent = testRepo.CommitTree(t, treeID, "delta-header-size-0")
-
- continue
- }
-
- parent = testRepo.CommitTree(t, treeID, fmt.Sprintf("delta-header-size-%03d", i), parent)
- }
-
- testRepo.UpdateRef(t, "refs/heads/main", parent)
- testRepo.Repack(t, "-a", "-d", "-f", "--window=128", "--depth=128")
-
- deltaID, wantResolvedSize := findDeltaObjectWithResolvedSizeMismatch(t, testRepo, algo)
- store := openPackedStore(t, testRepo, algo)
-
- _, gotSize, err := store.ReadHeader(deltaID)
- if err != nil {
- t.Fatalf("ReadHeader(%s): %v", deltaID, err)
- }
-
- if gotSize != wantResolvedSize {
- t.Fatalf("ReadHeader(%s) size = %d, want resolved size %d", deltaID, gotSize, wantResolvedSize)
- }
-
- gotReadSize, err := store.ReadSize(deltaID)
- if err != nil {
- t.Fatalf("ReadSize(%s): %v", deltaID, err)
- }
-
- if gotReadSize != wantResolvedSize {
- t.Fatalf("ReadSize(%s) = %d, want resolved size %d", deltaID, gotReadSize, wantResolvedSize)
- }
- })
-}
-
-func findDeltaObjectWithResolvedSizeMismatch(t *testing.T, testRepo *testgit.TestRepo, algo objectid.Algorithm) (objectid.ObjectID, int64) {
- t.Helper()
-
- packRoot := testRepo.OpenPackRoot(t)
-
- entries, err := fs.ReadDir(packRoot.FS(), ".")
- if err != nil {
- t.Fatalf("ReadDir(pack): %v", err)
- }
-
- var idxName string
-
- for _, entry := range entries {
- if strings.HasSuffix(entry.Name(), ".idx") {
- idxName = entry.Name()
-
- break
- }
- }
-
- if idxName == "" {
- t.Fatalf("no idx files found")
- }
-
- verifyOut := testRepo.Run(t, "verify-pack", "-v", "objects/pack/"+idxName)
- for line := range strings.SplitSeq(strings.TrimSpace(verifyOut), "\n") {
- fields := strings.Fields(line)
- if len(fields) < 7 {
- continue
- }
-
- idHex := fields[0]
-
- deltaStreamSize, err := strconv.ParseInt(fields[2], 10, 64)
- if err != nil {
- continue
- }
-
- resolvedSizeStr := testRepo.Run(t, "cat-file", "-s", idHex)
-
- resolvedSize, err := strconv.ParseInt(strings.TrimSpace(resolvedSizeStr), 10, 64)
- if err != nil {
- t.Fatalf("parse cat-file size for %s: %v", idHex, err)
- }
-
- if deltaStreamSize == resolvedSize {
- continue
- }
-
- id, err := objectid.ParseHex(algo, idHex)
- if err != nil {
- t.Fatalf("ParseHex(%s): %v", idHex, err)
- }
-
- return id, resolvedSize
- }
-
- t.Fatalf("did not find a delta object with mismatched stream/resolved size")
-
- return objectid.ObjectID{}, 0
-}
diff --git a/object/store/packed/internal/reading/store.go b/object/store/packed/internal/reading/store.go
deleted file mode 100644
index cb4829ab..00000000
--- a/object/store/packed/internal/reading/store.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package reading
-
-import (
- "os"
- "sync"
- "sync/atomic"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
-)
-
-// Store reads Git objects from pack/index files under an objects/pack root.
-//
-// Cached pack/index mappings are retained until Close.
-//
-// Labels: Close-Caller.
-type Store struct {
- // root is the borrowed objects/pack capability used for all file access.
- root *os.Root
- // algo is the expected object ID algorithm for lookups.
- algo objectid.Algorithm
- // refreshPolicy controls automatic candidate refresh on lookup misses.
- refreshPolicy RefreshPolicy
-
- // candidates stores the latest immutable candidate snapshot.
- candidates atomic.Pointer[candidateSnapshot]
- // refreshMu serializes candidate refresh.
- refreshMu sync.Mutex
- // mruMu guards candidate MRU linked-list state.
- mruMu sync.RWMutex
- // mruHead is the first pack in MRU order.
- mruHead *packCandidateNode
- // mruTail is the last pack in MRU order.
- mruTail *packCandidateNode
- // mruNodeByPack maps pack basename to MRU node.
- mruNodeByPack map[string]*packCandidateNode
- // idxByPack caches opened and parsed indexes by pack basename.
- idxByPack map[string]*idxFile
-
- // stateMu guards pack cache and close state.
- stateMu sync.RWMutex
- // idxMu guards parsed index cache.
- idxMu sync.RWMutex
- // cacheMu guards delta cache operations.
- cacheMu sync.RWMutex
- // packs caches opened .pack handles by basename.
- packs map[string]*packFile
- // deltaCache caches resolved base objects by pack location.
- deltaCache *deltaCache
-}
-
-var _ objectstore.Reader = (*Store)(nil)
diff --git a/object/store/packed/internal/reading/store_lookup.go b/object/store/packed/internal/reading/store_lookup.go
deleted file mode 100644
index 9d863113..00000000
--- a/object/store/packed/internal/reading/store_lookup.go
+++ /dev/null
@@ -1,106 +0,0 @@
-package reading
-
-import (
- "errors"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
-)
-
-// lookup resolves one object ID to its pack location.
-func (store *Store) lookup(id objectid.ObjectID) (location, error) {
- var zero location
- if id.Algorithm() != store.algo {
- return zero, errors.New("objectstore/packed: object id algorithm mismatch")
- }
-
- snapshot, err := store.ensureCandidates()
- if err != nil {
- return zero, err
- }
-
- loc, ok, err := store.lookupInCandidates(id, snapshot)
- if err != nil {
- return zero, err
- }
-
- if ok {
- return loc, nil
- }
-
- if store.refreshPolicy == RefreshPolicyOnMissing { //nolint:nestif
- err = store.Refresh()
- if err != nil {
- return zero, err
- }
-
- refreshed := store.candidates.Load()
- if refreshed != nil && refreshed != snapshot {
- loc, ok, err = store.lookupInCandidates(id, refreshed)
- if err != nil {
- return zero, err
- }
-
- if ok {
- return loc, nil
- }
- }
- }
-
- return zero, objectstore.ErrObjectNotFound
-}
-
-func (store *Store) lookupInCandidates(
- id objectid.ObjectID,
- snapshot *candidateSnapshot,
-) (location, bool, error) {
- var zero location
-
- nextPackName := store.firstCandidatePackName(snapshot)
- for nextPackName != "" {
- candidate, ok := snapshot.candidateByPack[nextPackName]
- if !ok {
- nextPackName = store.firstCandidatePackName(snapshot)
-
- continue
- }
-
- nextPackName = store.nextCandidatePackName(candidate.packName, snapshot)
-
- index, err := store.openIndex(candidate)
- if err != nil {
- return zero, false, err
- }
-
- offset, ok, err := index.lookup(id)
- if err != nil {
- return zero, false, err
- }
-
- if ok {
- store.touchCandidate(candidate.packName)
-
- return location{packName: index.packName, offset: offset}, true, nil
- }
- }
-
- for _, candidate := range snapshot.candidates {
- index, err := store.openIndex(candidate)
- if err != nil {
- return zero, false, err
- }
-
- offset, ok, err := index.lookup(id)
- if err != nil {
- return zero, false, err
- }
-
- if ok {
- store.touchCandidate(candidate.packName)
-
- return location{packName: index.packName, offset: offset}, true, nil
- }
- }
-
- return zero, false, nil
-}
diff --git a/object/store/packed/internal/reading/store_open_pack.go b/object/store/packed/internal/reading/store_open_pack.go
deleted file mode 100644
index 35cb960a..00000000
--- a/object/store/packed/internal/reading/store_open_pack.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package reading
-
-// openPack returns one opened and validated pack handle.
-func (store *Store) openPack(name string) (*packFile, error) {
- store.stateMu.RLock()
-
- pack, ok := store.packs[name]
- if ok {
- store.stateMu.RUnlock()
-
- return pack, nil
- }
-
- store.stateMu.RUnlock()
-
- file, err := store.root.Open(name)
- if err != nil {
- return nil, err
- }
-
- info, err := file.Stat()
- if err != nil {
- _ = file.Close()
-
- return nil, err
- }
-
- pack, err = openPackFile(name, file, info.Size())
- if err != nil {
- _ = file.Close()
-
- return nil, err
- }
-
- err = store.verifyPackMatchesIndexes(pack)
- if err != nil {
- _ = pack.close()
-
- return nil, err
- }
-
- store.stateMu.Lock()
-
- existing, ok := store.packs[name]
- if ok {
- store.stateMu.Unlock()
-
- _ = pack.close()
-
- return existing, nil
- }
-
- store.packs[name] = pack
- store.stateMu.Unlock()
-
- return pack, nil
-}
diff --git a/object/store/packed/internal/reading/trailer_match.go b/object/store/packed/internal/reading/trailer_match.go
deleted file mode 100644
index 8c7500b9..00000000
--- a/object/store/packed/internal/reading/trailer_match.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package reading
-
-import "fmt"
-
-// verifyPackMatchesIndexes checks that one opened pack's trailer hash matches
-// every loaded index that references the same pack name.
-func (store *Store) verifyPackMatchesIndexes(pack *packFile) error {
- snapshot, err := store.ensureCandidates()
- if err != nil {
- return err
- }
-
- candidate, ok := snapshot.candidateByPack[pack.name]
- if !ok {
- return fmt.Errorf("objectstore/packed: missing index for pack %q", pack.name)
- }
-
- index, err := store.openIndex(candidate)
- if err != nil {
- return err
- }
-
- err = verifyMappedPackMatchesMappedIdx(pack.data, index.data, store.algo)
- if err != nil {
- return fmt.Errorf("objectstore/packed: pack %q does not match idx %q: %w", pack.name, index.idxName, err)
- }
-
- return nil
-}
diff --git a/object/store/packed/new.go b/object/store/packed/new.go
deleted file mode 100644
index cdc1b50f..00000000
--- a/object/store/packed/new.go
+++ /dev/null
@@ -1,25 +0,0 @@
-package packed
-
-import (
- "os"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/store/packed/internal/reading"
-)
-
-// New creates a packed-object store rooted at an objects/pack directory.
-//
-// Labels: Deps-Borrowed, Life-Parent.
-func New(root *os.Root, algo objectid.Algorithm, opts Options) (*Store, error) {
- reader, err := reading.New(root, algo, opts.toReadingOptions())
- if err != nil {
- return nil, err
- }
-
- return &Store{
- root: root,
- algo: algo,
- opts: opts,
- reader: reader,
- }, nil
-}
diff --git a/object/store/packed/options.go b/object/store/packed/options.go
deleted file mode 100644
index 718efc29..00000000
--- a/object/store/packed/options.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package packed
-
-// Options configures a packed object store.
-type Options struct {
- RefreshPolicy RefreshPolicy
- WriteRev bool
-}
diff --git a/object/store/packed/options_refresh.go b/object/store/packed/options_refresh.go
deleted file mode 100644
index ee3d5f2e..00000000
--- a/object/store/packed/options_refresh.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package packed
-
-// RefreshPolicy configures when candidate pack/index discovery refreshes.
-type RefreshPolicy uint8
-
-const (
- // RefreshPolicyOnMissing refreshes candidates once after a lookup miss.
- RefreshPolicyOnMissing RefreshPolicy = iota
- // RefreshPolicyNever disables automatic refresh after lookup misses.
- RefreshPolicyNever
-)
diff --git a/object/store/packed/quarantine.go b/object/store/packed/quarantine.go
deleted file mode 100644
index a8f6d08c..00000000
--- a/object/store/packed/quarantine.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package packed
-
-import (
- "os"
-
- objectstore "codeberg.org/lindenii/furgit/object/store"
-)
-
-var _ objectstore.PackQuarantiner = (*Store)(nil)
-
-type packQuarantine struct {
- *Store
-
- parent *Store
- tempName string
- tempRoot *os.Root
-}
-
-var _ objectstore.PackQuarantine = (*packQuarantine)(nil)
diff --git a/object/store/packed/quarantine_begin.go b/object/store/packed/quarantine_begin.go
deleted file mode 100644
index 06b9a8a6..00000000
--- a/object/store/packed/quarantine_begin.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package packed
-
-import (
- "crypto/rand"
- "errors"
- "fmt"
- "io/fs"
- "os"
-
- objectstore "codeberg.org/lindenii/furgit/object/store"
-)
-
-// BeginPackQuarantine creates one quarantined packed store rooted privately
-// beneath the destination pack root.
-//
-// Labels: Deps-Borrowed, Life-Parent, Close-No.
-func (store *Store) BeginPackQuarantine(_ objectstore.PackQuarantineOptions) (objectstore.PackQuarantine, error) {
- tempName, tempRoot, err := createPackQuarantineRoot(store.root)
- if err != nil {
- return nil, err
- }
-
- quarantineStore, err := New(tempRoot, store.algo, store.opts)
- if err != nil {
- _ = tempRoot.Close()
- _ = store.root.RemoveAll(tempName)
-
- return nil, err
- }
-
- return &packQuarantine{
- Store: quarantineStore,
- parent: store,
- tempName: tempName,
- tempRoot: tempRoot,
- }, nil
-}
-
-func createPackQuarantineRoot(parent *os.Root) (string, *os.Root, error) {
- for range 32 {
- name := "tmp_packq_" + rand.Text()
-
- err := parent.Mkdir(name, 0o700)
- if err == nil {
- root, err := parent.OpenRoot(name)
- if err == nil {
- return name, root, nil
- }
-
- _ = parent.RemoveAll(name)
-
- return "", nil, err
- }
-
- if errors.Is(err, fs.ErrExist) {
- continue
- }
-
- return "", nil, err
- }
-
- return "", nil, fmt.Errorf("packed: unable to create quarantine directory")
-}
diff --git a/object/store/packed/quarantine_discard.go b/object/store/packed/quarantine_discard.go
deleted file mode 100644
index a1dc7310..00000000
--- a/object/store/packed/quarantine_discard.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package packed
-
-// Discard removes the quarantine and invalidates the receiver.
-func (quarantine *packQuarantine) Discard() error {
- closeErr := quarantine.Close()
- tempRootErr := quarantine.tempRoot.Close()
- removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName)
-
- if closeErr != nil {
- return closeErr
- }
-
- if tempRootErr != nil {
- return tempRootErr
- }
-
- return removeErr
-}
diff --git a/object/store/packed/quarantine_promote.go b/object/store/packed/quarantine_promote.go
deleted file mode 100644
index a4eb426d..00000000
--- a/object/store/packed/quarantine_promote.go
+++ /dev/null
@@ -1,89 +0,0 @@
-package packed
-
-import (
- "errors"
- "fmt"
- "io/fs"
- "os"
- "slices"
- "strings"
-)
-
-// Promote publishes all finalized pack artifacts in the quarantine into the
-// parent packed store and invalidates the receiver.
-func (quarantine *packQuarantine) Promote() error {
- closeErr := quarantine.Close()
- promoteErr := promotePackQuarantine(quarantine.parent.root, quarantine.tempName, quarantine.tempRoot)
- tempRootErr := quarantine.tempRoot.Close()
- removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName)
-
- if closeErr != nil {
- return closeErr
- }
-
- if tempRootErr != nil {
- return tempRootErr
- }
-
- if promoteErr != nil {
- return promoteErr
- }
-
- return removeErr
-}
-
-func promotePackQuarantine(parent *os.Root, tempName string, tempRoot *os.Root) error {
- entries, err := fs.ReadDir(tempRoot.FS(), ".")
- if err != nil && !errors.Is(err, fs.ErrNotExist) {
- return err
- }
-
- slices.SortFunc(entries, func(left, right fs.DirEntry) int {
- return packPromotionPriority(left.Name()) - packPromotionPriority(right.Name())
- })
-
- for _, entry := range entries {
- if entry.IsDir() {
- return fmt.Errorf("packed: quarantine contains unexpected directory %q", entry.Name())
- }
-
- err := promotePackQuarantineFile(parent, tempName, entry.Name())
- if err != nil {
- return err
- }
- }
-
- return nil
-}
-
-func promotePackQuarantineFile(parent *os.Root, tempName, name string) error {
- src := tempName + "/" + name
-
- err := parent.Link(src, name)
- if err == nil {
- _ = parent.Remove(src)
-
- return nil
- }
-
- if errors.Is(err, fs.ErrExist) {
- _ = parent.Remove(src)
-
- return nil
- }
-
- return fmt.Errorf("packed: promote quarantine %q -> %q: %w", src, name, err)
-}
-
-func packPromotionPriority(name string) int {
- switch {
- case strings.HasPrefix(name, "pack-") && strings.HasSuffix(name, ".pack"):
- return 1
- case strings.HasPrefix(name, "pack-") && strings.HasSuffix(name, ".rev"):
- return 2
- case strings.HasPrefix(name, "pack-") && strings.HasSuffix(name, ".idx"):
- return 3
- default:
- return 0
- }
-}
diff --git a/object/store/packed/quarantine_test.go b/object/store/packed/quarantine_test.go
deleted file mode 100644
index 036da535..00000000
--- a/object/store/packed/quarantine_test.go
+++ /dev/null
@@ -1,215 +0,0 @@
-package packed_test
-
-import (
- "bytes"
- "os"
- "path/filepath"
- "strings"
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
- "codeberg.org/lindenii/furgit/object/store/packed"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-func fixturePath(t *testing.T, algo objectid.Algorithm, name string) string {
- t.Helper()
-
- return filepath.Join("internal", "ingest", "testdata", "fixtures", algo.String(), name)
-}
-
-func fixtureBytes(t *testing.T, algo objectid.Algorithm, name string) []byte {
- t.Helper()
-
- path := fixturePath(t, algo, name)
- dir := filepath.Dir(path)
- base := filepath.Base(path)
-
- root, err := os.OpenRoot(dir)
- if err != nil {
- t.Fatalf("open fixture root %q: %v", dir, err)
- }
-
- defer func() {
- err := root.Close()
- if err != nil {
- t.Fatalf("close fixture root %q: %v", dir, err)
- }
- }()
-
- data, err := root.ReadFile(base)
- if err != nil {
- t.Fatalf("read fixture %q: %v", base, err)
- }
-
- return data
-}
-
-func fixtureMetadata(t *testing.T, algo objectid.Algorithm) map[string]string {
- t.Helper()
-
- data := fixtureBytes(t, algo, "METADATA.txt")
- out := make(map[string]string)
-
- for line := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") {
- line = strings.TrimSpace(line)
- if line == "" {
- continue
- }
-
- key, value, ok := strings.Cut(line, "=")
- if !ok {
- t.Fatalf("invalid fixture metadata line %q", line)
- }
-
- out[strings.TrimSpace(key)] = strings.TrimSpace(value)
- }
-
- return out
-}
-
-func fixtureOID(t *testing.T, algo objectid.Algorithm, key string) objectid.ObjectID {
- t.Helper()
-
- meta := fixtureMetadata(t, algo)
-
- hex, ok := meta[key]
- if !ok {
- t.Fatalf("missing fixture metadata key %q", key)
- }
-
- id, err := objectid.ParseHex(algo, hex)
- if err != nil {
- t.Fatalf("parse fixture metadata oid %q: %v", hex, err)
- }
-
- return id
-}
-
-func TestPackQuarantinePromotePublishesWrittenObjects(t *testing.T) {
- t.Parallel()
-
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- head := fixtureOID(t, algo, "head")
- packBytes := fixtureBytes(t, algo, "nonthin.pack")
-
- repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- packRoot := repo.OpenPackRoot(t)
-
- store, err := packed.New(packRoot, algo, packed.Options{WriteRev: true})
- if err != nil {
- t.Fatalf("packed.New: %v", err)
- }
-
- defer func() {
- err := store.Close()
- if err != nil {
- t.Fatalf("store.Close: %v", err)
- }
- }()
-
- quarantiner, ok := any(store).(objectstore.PackQuarantiner)
- if !ok {
- t.Fatal("packed store does not implement PackQuarantiner")
- }
-
- quarantine, err := quarantiner.BeginPackQuarantine(objectstore.PackQuarantineOptions{})
- if err != nil {
- t.Fatalf("BeginPackQuarantine: %v", err)
- }
-
- err = quarantine.WritePack(bytes.NewReader(packBytes), objectstore.PackWriteOptions{RequireTrailingEOF: true})
- if err != nil {
- t.Fatalf("quarantine.WritePack: %v", err)
- }
-
- ty, _, err := quarantine.ReadHeader(head)
- if err != nil {
- t.Fatalf("quarantine.ReadHeader: %v", err)
- }
-
- if ty != objecttype.TypeCommit {
- t.Fatalf("quarantine.ReadHeader type = %v, want commit", ty)
- }
-
- _, _, err = store.ReadHeader(head)
- if err == nil {
- t.Fatal("store.ReadHeader unexpectedly saw quarantined object before promote")
- }
-
- err = quarantine.Promote()
- if err != nil {
- t.Fatalf("quarantine.Promote: %v", err)
- }
-
- err = store.Refresh()
- if err != nil {
- t.Fatalf("store.Refresh: %v", err)
- }
-
- ty, _, err = store.ReadHeader(head)
- if err != nil {
- t.Fatalf("store.ReadHeader after promote: %v", err)
- }
-
- if ty != objecttype.TypeCommit {
- t.Fatalf("store.ReadHeader type = %v, want commit", ty)
- }
- })
-}
-
-func TestPackQuarantineDiscardDropsWrittenObjects(t *testing.T) {
- t.Parallel()
-
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- head := fixtureOID(t, algo, "head")
- packBytes := fixtureBytes(t, algo, "nonthin.pack")
-
- repo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- packRoot := repo.OpenPackRoot(t)
-
- store, err := packed.New(packRoot, algo, packed.Options{WriteRev: true})
- if err != nil {
- t.Fatalf("packed.New: %v", err)
- }
-
- defer func() {
- err := store.Close()
- if err != nil {
- t.Fatalf("store.Close: %v", err)
- }
- }()
-
- quarantiner, ok := any(store).(objectstore.PackQuarantiner)
- if !ok {
- t.Fatalf("expected objectstore.PackQuarantiner")
- }
-
- quarantine, err := quarantiner.BeginPackQuarantine(objectstore.PackQuarantineOptions{})
- if err != nil {
- t.Fatalf("BeginPackQuarantine: %v", err)
- }
-
- err = quarantine.WritePack(bytes.NewReader(packBytes), objectstore.PackWriteOptions{RequireTrailingEOF: true})
- if err != nil {
- t.Fatalf("quarantine.WritePack: %v", err)
- }
-
- err = quarantine.Discard()
- if err != nil {
- t.Fatalf("quarantine.Discard: %v", err)
- }
-
- err = store.Refresh()
- if err != nil {
- t.Fatalf("store.Refresh: %v", err)
- }
-
- _, _, err = store.ReadHeader(head)
- if err == nil {
- t.Fatal("store.ReadHeader unexpectedly saw discarded object")
- }
- })
-}
diff --git a/object/store/packed/reader.go b/object/store/packed/reader.go
deleted file mode 100644
index 45b9e8d9..00000000
--- a/object/store/packed/reader.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package packed
-
-import (
- "io"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectstore "codeberg.org/lindenii/furgit/object/store"
- "codeberg.org/lindenii/furgit/object/store/packed/internal/reading"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-var _ objectstore.Reader = (*Store)(nil)
-
-// ReadBytesFull reads a full serialized object as "type size\0content".
-func (store *Store) ReadBytesFull(id objectid.ObjectID) ([]byte, error) {
- return store.reader.ReadBytesFull(id)
-}
-
-// ReadBytesContent reads an object's type and content bytes.
-func (store *Store) ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error) {
- return store.reader.ReadBytesContent(id)
-}
-
-// ReadReaderFull reads a full serialized object stream as "type size\0content".
-func (store *Store) ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error) {
- return store.reader.ReadReaderFull(id)
-}
-
-// ReadReaderContent reads an object's type, declared content length, and
-// content stream.
-func (store *Store) ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error) {
- return store.reader.ReadReaderContent(id)
-}
-
-// ReadSize reads an object's declared content length.
-func (store *Store) ReadSize(id objectid.ObjectID) (int64, error) {
- return store.reader.ReadSize(id)
-}
-
-// ReadHeader reads an object's type and declared content length.
-func (store *Store) ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error) {
- return store.reader.ReadHeader(id)
-}
-
-// Refresh updates the packed-store view of on-disk pack/index candidates.
-func (store *Store) Refresh() error {
- return store.reader.Refresh()
-}
-
-func (opts Options) toReadingOptions() reading.Options {
- var refreshPolicy reading.RefreshPolicy
-
- switch opts.RefreshPolicy {
- case RefreshPolicyOnMissing:
- refreshPolicy = reading.RefreshPolicyOnMissing
- case RefreshPolicyNever:
- refreshPolicy = reading.RefreshPolicyNever
- default:
- refreshPolicy = reading.RefreshPolicy(opts.RefreshPolicy)
- }
-
- return reading.Options{
- RefreshPolicy: refreshPolicy,
- }
-}
diff --git a/object/store/packed/store.go b/object/store/packed/store.go
deleted file mode 100644
index 2fe84c81..00000000
--- a/object/store/packed/store.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package packed
-
-import (
- "os"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/store/packed/internal/reading"
-)
-
-// Store reads Git objects from pack/index files under an objects/pack root.
-//
-// Labels: Close-Caller.
-type Store struct {
- root *os.Root
- algo objectid.Algorithm
- opts Options
- reader *reading.Store
-}
-
-// Close releases mapped pack/index resources associated with the store.
-func (store *Store) Close() error {
- return store.reader.Close()
-}
diff --git a/object/store/packed/writer.go b/object/store/packed/writer.go
deleted file mode 100644
index a96ea750..00000000
--- a/object/store/packed/writer.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package packed
-
-import (
- "io"
-
- objectstore "codeberg.org/lindenii/furgit/object/store"
- "codeberg.org/lindenii/furgit/object/store/packed/internal/ingest"
-)
-
-var _ objectstore.PackWriter = (*Store)(nil)
-
-// WritePack ingests one pack stream into the packed store.
-func (store *Store) WritePack(src io.Reader, opts objectstore.PackWriteOptions) error {
- _, err := ingest.WritePack(store.root, store.algo, src, ingest.Options{
- WriteRev: store.opts.WriteRev,
- ThinBase: opts.ThinBase,
- Progress: opts.Progress,
- RequireTrailingEOF: opts.RequireTrailingEOF,
- })
-
- return err
-}
diff --git a/object/store/quarantine.go b/object/store/quarantine.go
deleted file mode 100644
index 5fa97ee7..00000000
--- a/object/store/quarantine.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package objectstore
-
-// Quarantine represents one quarantined write that accepts both object-
-// wise and pack-wise writes.
-type Quarantine interface {
- BaseQuarantine
- Writer
-}
-
-// QuarantineOptions controls the options for one coordinated quarantine creation.
-type QuarantineOptions struct {
- Object ObjectQuarantineOptions
- Pack PackQuarantineOptions
-}
-
-// Quarantiner creates coordinated quarantines that support both object-
-// wise and pack-wise writes.
-type Quarantiner interface {
- BeginQuarantine(opts QuarantineOptions) (Quarantine, error)
-}
diff --git a/object/store/reader.go b/object/store/reader.go
deleted file mode 100644
index 52a556bd..00000000
--- a/object/store/reader.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package objectstore
-
-import (
- "io"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// Reader reads Git objects by object ID.
-//
-// Methods may perform implementation-defined integrity verification beyond
-// successfully producing their documented result.
-//
-// Labels: MT-Safe.
-type Reader interface {
- // ReadBytesFull reads a full serialized object as "type size\0content".
- //
- // In a valid repository, hashing this payload with the same algorithm yields
- // the requested object ID. Readers should treat this as a repository
- // invariant and should not re-verify it on every read.
- //
- // Labels: Life-Parent.
- ReadBytesFull(id objectid.ObjectID) ([]byte, error)
-
- // ReadBytesContent reads an object's type and content bytes.
- //
- // Labels: Life-Parent.
- ReadBytesContent(id objectid.ObjectID) (objecttype.Type, []byte, error)
-
- // ReadReaderFull reads a full serialized object stream as "type size\0content".
- //
- // Labels: Life-Parent, Close-Caller.
- ReadReaderFull(id objectid.ObjectID) (io.ReadCloser, error)
-
- // ReadReaderContent reads an object's type, declared content length,
- // and content stream.
- //
- // Labels: Life-Parent, Close-Caller.
- ReadReaderContent(id objectid.ObjectID) (objecttype.Type, int64, io.ReadCloser, error)
-
- // ReadSize reads an object's declared content length.
- //
- // This is equivalent to ReadHeader(...).size and may be cheaper than
- // ReadHeader when callers do not need object type.
- ReadSize(id objectid.ObjectID) (int64, error)
-
- // ReadHeader reads an object's type and declared content length.
- ReadHeader(id objectid.ObjectID) (objecttype.Type, int64, error)
-
- // Refresh updates any backend-local discovery/cache view of on-disk objects.
- //
- // Backends without dynamic discovery should do nothing and return nil.
- Refresh() error
-}
diff --git a/object/store/writer.go b/object/store/writer.go
deleted file mode 100644
index 9fa05aba..00000000
--- a/object/store/writer.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package objectstore
-
-// Writer represents a store that could perform both pack ingestions
-// and individual object writes.
-type Writer interface {
- PackWriter
- ObjectWriter
-}
diff --git a/object/store/writer_object.go b/object/store/writer_object.go
deleted file mode 100644
index a18a5d84..00000000
--- a/object/store/writer_object.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package objectstore
-
-import (
- "io"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// ObjectWriter writes individual Git objects.
-type ObjectWriter interface {
- // WriteReaderContent writes one typed object content stream.
- WriteReaderContent(ty objecttype.Type, size int64, src io.Reader) (objectid.ObjectID, error)
-
- // WriteReaderFull writes one full serialized object stream as "type size\0content".
- WriteReaderFull(src io.Reader) (objectid.ObjectID, error)
-
- // WriteBytesContent writes one typed object content byte slice.
- WriteBytesContent(ty objecttype.Type, content []byte) (objectid.ObjectID, error)
-
- // WriteBytesFull writes one full serialized object byte slice as "type size\0content".
- WriteBytesFull(raw []byte) (objectid.ObjectID, error)
-}
-
-// ObjectQuarantine represents one quarantined object-wise write.
-type ObjectQuarantine interface {
- BaseQuarantine
- ObjectWriter
-}
-
-// ObjectQuarantineOptions controls the options for one object quarantine creation.
-type ObjectQuarantineOptions struct{}
-
-// ObjectQuarantiner creates quarantines for object-wise writes.
-type ObjectQuarantiner interface {
- BeginObjectQuarantine(opts ObjectQuarantineOptions) (ObjectQuarantine, error)
-}
diff --git a/object/store/writer_pack.go b/object/store/writer_pack.go
deleted file mode 100644
index 0f78c429..00000000
--- a/object/store/writer_pack.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package objectstore
-
-import (
- "io"
-
- "codeberg.org/lindenii/furgit/common/iowrap"
-)
-
-// PackWriteOptions controls one pack write operation.
-type PackWriteOptions struct {
- // ThinBase supplies the wider object reader used to complete thin packs
- // during ingestion.
- //
- // This is an option for the write operation rather than on a particular
- // pack-backed store because any pack-accepting store is not generally
- // expected to know the entire repository object universe around it.
- // In a normal repository, thin bases usually come from a broader view
- // such as mix(loose, packed), and should not be treated as a property of
- // the destination pack-accepting store. Thus, in almost all pack-ingesting
- // operations, a thin base reader would be required, and hence it is
- // included here.
- //
- // When nil, external thin-base repair is disabled and unresolved thin deltas
- // fail ingestion.
- ThinBase Reader
-
- // Progress receives human-readable progress messages.
- //
- // When nil, no progress output is emitted.
- Progress iowrap.WriteFlusher
-
- // RequireTrailingEOF requires the source to hit EOF after the pack trailer.
- //
- // This is suitable for exact pack-file readers, but should be disabled for
- // full-duplex transport streams like receive-pack where the peer keeps the
- // connection open to read the server response.
- RequireTrailingEOF bool
-}
-
-// PackWriter writes Git pack streams.
-type PackWriter interface {
- // WritePack ingests one pack stream.
- WritePack(src io.Reader, opts PackWriteOptions) error
-}
-
-// PackQuarantine represents one quarantined pack-wise write.
-type PackQuarantine interface {
- BaseQuarantine
- PackWriter
-}
-
-// PackQuarantineOptions controls the options for one pack quarantine creation.
-type PackQuarantineOptions struct{}
-
-// PackQuarantiner creates quarantines for pack-wise writes.
-type PackQuarantiner interface {
- BeginPackQuarantine(opts PackQuarantineOptions) (PackQuarantine, error)
-}
diff --git a/object/stored/doc.go b/object/stored/doc.go
deleted file mode 100644
index d57cbd55..00000000
--- a/object/stored/doc.go
+++ /dev/null
@@ -1,7 +0,0 @@
-// Package stored wraps parsed objects with the object IDs they were loaded
-// under.
-//
-// Parsed Git object values do not carry storage identity on their own. This
-// package provides a small generic wrapper for the common case where callers
-// need both the parsed object value and the object ID it was read from.
-package stored
diff --git a/object/stored/id.go b/object/stored/id.go
deleted file mode 100644
index 956d069e..00000000
--- a/object/stored/id.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package stored
-
-import objectid "codeberg.org/lindenii/furgit/object/id"
-
-// ID returns the object ID.
-func (stored *Stored[T]) ID() objectid.ObjectID {
- return stored.id
-}
diff --git a/object/stored/new.go b/object/stored/new.go
deleted file mode 100644
index 8b0ef881..00000000
--- a/object/stored/new.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package stored
-
-import (
- "codeberg.org/lindenii/furgit/object"
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-// New creates one stored object wrapper.
-func New[T object.Object](id objectid.ObjectID, obj T) *Stored[T] {
- return &Stored[T]{id: id, obj: obj}
-}
diff --git a/object/stored/object.go b/object/stored/object.go
deleted file mode 100644
index ab22b9c8..00000000
--- a/object/stored/object.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package stored
-
-// Object returns the wrapped object as itself.
-func (stored *Stored[T]) Object() T {
- return stored.obj
-}
diff --git a/object/stored/stored.go b/object/stored/stored.go
deleted file mode 100644
index eb776f31..00000000
--- a/object/stored/stored.go
+++ /dev/null
@@ -1,13 +0,0 @@
-package stored
-
-import (
- "codeberg.org/lindenii/furgit/object"
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-// Stored represents a stored object,
-// i.e., an object along with its object ID.
-type Stored[T object.Object] struct {
- id objectid.ObjectID
- obj T
-}
diff --git a/object/tag/parse.go b/object/tag/parse.go
deleted file mode 100644
index 92fa0d8b..00000000
--- a/object/tag/parse.go
+++ /dev/null
@@ -1,89 +0,0 @@
-package tag
-
-import (
- "bytes"
- "errors"
- "fmt"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectsignature "codeberg.org/lindenii/furgit/object/signature"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// Parse decodes a tag object body.
-func Parse(body []byte, algo objectid.Algorithm) (*Tag, error) {
- t := new(Tag)
- i := 0
-
- var haveTarget, haveType bool
-
- for i < len(body) {
- rel := bytes.IndexByte(body[i:], '\n')
- if rel < 0 {
- return nil, errors.New("object: tag: missing newline")
- }
-
- line := body[i : i+rel]
- i += rel + 1
-
- if len(line) == 0 {
- break
- }
-
- key, value, found := bytes.Cut(line, []byte{' '})
- if !found {
- return nil, errors.New("object: tag: malformed header")
- }
-
- switch string(key) {
- case "object":
- id, err := objectid.ParseHex(algo, string(value))
- if err != nil {
- return nil, fmt.Errorf("object: tag: object: %w", err)
- }
-
- t.Target = id
- haveTarget = true
- case "type":
- ty, ok := objecttype.Parse(string(value))
- if !ok {
- return nil, errors.New("object: tag: unknown target type")
- }
-
- t.TargetType = ty
- haveType = true
- case "tag":
- t.Name = append([]byte(nil), value...)
- case "tagger":
- idt, err := objectsignature.Parse(value)
- if err != nil {
- return nil, fmt.Errorf("object: tag: tagger: %w", err)
- }
-
- t.Tagger = idt
- case "gpgsig", "gpgsig-sha256":
- for i < len(body) {
- nextRel := bytes.IndexByte(body[i:], '\n')
- if nextRel < 0 {
- return nil, errors.New("object: tag: unterminated gpgsig")
- }
-
- if body[i] != ' ' {
- break
- }
-
- i += nextRel + 1
- }
- default:
- // Ignore unknown headers for now.
- }
- }
-
- if !haveTarget || !haveType {
- return nil, errors.New("object: tag: missing required headers")
- }
-
- t.Message = append([]byte(nil), body[i:]...)
-
- return t, nil
-}
diff --git a/object/tag/parse_test.go b/object/tag/parse_test.go
deleted file mode 100644
index 293350ed..00000000
--- a/object/tag/parse_test.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package tag_test
-
-import (
- "bytes"
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/tag"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-func TestTagParseFromGit(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- _, _, commitID := testRepo.MakeCommit(t, "subject\n\nbody")
- tagID := testRepo.TagAnnotated(t, "v1", commitID, "tag message")
-
- rawBody := testRepo.CatFile(t, "tag", tagID)
-
- parsed, err := tag.Parse(rawBody, algo)
- if err != nil {
- t.Fatalf("ParseTag: %v", err)
- }
-
- if parsed.Target != commitID {
- t.Fatalf("tag target mismatch: got %s want %s", parsed.Target, commitID)
- }
-
- if parsed.TargetType != objecttype.TypeCommit {
- t.Fatalf("tag target type = %v, want %v", parsed.TargetType, objecttype.TypeCommit)
- }
-
- if !bytes.Equal(parsed.Name, []byte("v1")) {
- t.Fatalf("tag name = %q, want %q", parsed.Name, "v1")
- }
-
- if parsed.Tagger == nil {
- t.Fatalf("expected tagger")
- }
-
- if !bytes.Contains(parsed.Message, []byte("tag message")) {
- t.Fatalf("tag message mismatch: %q", parsed.Message)
- }
- })
-}
diff --git a/object/tag/serialize.go b/object/tag/serialize.go
deleted file mode 100644
index 4f9d6664..00000000
--- a/object/tag/serialize.go
+++ /dev/null
@@ -1,68 +0,0 @@
-package tag
-
-import (
- "bytes"
- "errors"
- "fmt"
-
- objectheader "codeberg.org/lindenii/furgit/object/header"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// SerializeWithoutHeader renders the raw tag body bytes.
-func (tag *Tag) SerializeWithoutHeader() ([]byte, error) {
- if tag.Target.Algorithm().Size() == 0 {
- return nil, errors.New("object: tag: missing target id")
- }
-
- var buf bytes.Buffer
- fmt.Fprintf(&buf, "object %s\n", tag.Target.String())
-
- tyName, ok := tag.TargetType.Name()
- if !ok {
- return nil, fmt.Errorf("object: tag: invalid target type %d", tag.TargetType)
- }
-
- buf.WriteString("type ")
- buf.WriteString(tyName)
- buf.WriteByte('\n')
-
- buf.WriteString("tag ")
- buf.Write(tag.Name)
- buf.WriteByte('\n')
-
- if tag.Tagger != nil {
- taggerBytes, err := tag.Tagger.Serialize()
- if err != nil {
- return nil, err
- }
-
- buf.WriteString("tagger ")
- buf.Write(taggerBytes)
- buf.WriteByte('\n')
- }
-
- buf.WriteByte('\n')
- buf.Write(tag.Message)
-
- return buf.Bytes(), nil
-}
-
-// SerializeWithHeader renders the raw object (header + body).
-func (tag *Tag) SerializeWithHeader() ([]byte, error) {
- body, err := tag.SerializeWithoutHeader()
- if err != nil {
- return nil, err
- }
-
- header, ok := objectheader.Encode(objecttype.TypeTag, int64(len(body)))
- if !ok {
- return nil, errors.New("object: tag: failed to encode object header")
- }
-
- raw := make([]byte, len(header)+len(body))
- copy(raw, header)
- copy(raw[len(header):], body)
-
- return raw, nil
-}
diff --git a/object/tag/serialize_test.go b/object/tag/serialize_test.go
deleted file mode 100644
index a1311c39..00000000
--- a/object/tag/serialize_test.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package tag_test
-
-import (
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/tag"
-)
-
-func TestTagSerialize(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- _, _, commitID := testRepo.MakeCommit(t, "subject\n\nbody")
- tagID := testRepo.TagAnnotated(t, "v1", commitID, "tag message")
-
- rawBody := testRepo.CatFile(t, "tag", tagID)
-
- parsed, err := tag.Parse(rawBody, algo)
- if err != nil {
- t.Fatalf("ParseTag: %v", err)
- }
-
- rawObj, err := parsed.SerializeWithHeader()
- if err != nil {
- t.Fatalf("SerializeWithHeader: %v", err)
- }
-
- gotID := algo.Sum(rawObj)
- if gotID != tagID {
- t.Fatalf("tag id mismatch: got %s want %s", gotID, tagID)
- }
- })
-}
diff --git a/object/tag/tag.go b/object/tag/tag.go
deleted file mode 100644
index e01f8ac9..00000000
--- a/object/tag/tag.go
+++ /dev/null
@@ -1,24 +0,0 @@
-// Package tag provides parsed annotated tag objects and tag serialization.
-//
-// It parses annotated tags into ordinary Go values for reading and
-// construction. It does not preserve the exact original byte layout needed for
-// signature verification; callers that need signature-verification payload
-// fidelity should use [codeberg.org/lindenii/furgit/object/signed/tag].
-package tag
-
-import (
- objectid "codeberg.org/lindenii/furgit/object/id"
- objectsignature "codeberg.org/lindenii/furgit/object/signature"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// Tag represents a fully materialized Git annotated tag object.
-//
-// Labels: MT-Unsafe.
-type Tag struct {
- Target objectid.ObjectID
- TargetType objecttype.Type
- Name []byte
- Tagger *objectsignature.Signature
- Message []byte
-}
diff --git a/object/tag/type.go b/object/tag/type.go
deleted file mode 100644
index 215103ab..00000000
--- a/object/tag/type.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package tag
-
-import objecttype "codeberg.org/lindenii/furgit/object/type"
-
-// ObjectType returns TypeTag.
-func (tag *Tag) ObjectType() objecttype.Type {
- _ = tag
-
- return objecttype.TypeTag
-}
diff --git a/object/tree/entry.go b/object/tree/entry.go
deleted file mode 100644
index b3089b74..00000000
--- a/object/tree/entry.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package tree
-
-import (
- "bytes"
- "slices"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-// TreeEntry represents a single entry in a tree.
-type TreeEntry struct {
- Mode FileMode
- // Name is part of the tree ordering. Mutating it after insertion may break
- // Tree ordering and lookup behavior.
- Name []byte
- ID objectid.ObjectID
-}
-
-func (tree *Tree) entry(name []byte, searchIsTree bool) *TreeEntry {
- index, ok := slices.BinarySearchFunc(tree.Entries, name, func(entry TreeEntry, name []byte) int {
- return TreeEntryNameCompare(entry.Name, entry.Mode, name, searchIsTree)
- })
- if !ok {
- return nil
- }
-
- entry := &tree.Entries[index]
- if !bytes.Equal(entry.Name, name) {
- return nil
- }
-
- return entry
-}
-
-func (tree *Tree) entryIndex(name []byte) (int, bool) {
- index, ok := tree.entryIndexWithMode(name, true)
- if ok {
- return index, true
- }
-
- return tree.entryIndexWithMode(name, false)
-}
-
-func (tree *Tree) entryIndexWithMode(name []byte, searchIsTree bool) (int, bool) {
- index, ok := slices.BinarySearchFunc(tree.Entries, name, func(entry TreeEntry, name []byte) int {
- return TreeEntryNameCompare(entry.Name, entry.Mode, name, searchIsTree)
- })
- if !ok {
- return 0, false
- }
-
- if !bytes.Equal(tree.Entries[index].Name, name) {
- return 0, false
- }
-
- return index, true
-}
diff --git a/object/tree/helpers_test.go b/object/tree/helpers_test.go
deleted file mode 100644
index 3da92ce4..00000000
--- a/object/tree/helpers_test.go
+++ /dev/null
@@ -1,114 +0,0 @@
-package tree_test
-
-import (
- "bytes"
- "fmt"
- "strings"
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- "codeberg.org/lindenii/furgit/object/tree"
-)
-
-func buildGitMktreeInput(entries []tree.TreeEntry) string {
- var b strings.Builder
- for _, e := range entries {
- fmt.Fprintf(&b, "%o %s %s\t%s\n", e.Mode, mktreeTypeFromMode(e.Mode), e.ID.String(), e.Name)
- }
-
- return b.String()
-}
-
-func mktreeTypeFromMode(mode tree.FileMode) string {
- switch mode {
- case tree.FileModeDir:
- return "tree"
- case tree.FileModeRegular, tree.FileModeExecutable, tree.FileModeSymlink:
- return "blob"
- case tree.FileModeGitlink:
- return "commit"
- default:
- return ""
- }
-}
-
-func gitLsTreeNames(out []byte) [][]byte {
- if len(out) == 0 {
- return nil
- }
-
- parts := bytes.Split(out, []byte{0})
- if len(parts) > 0 && len(parts[len(parts)-1]) == 0 {
- parts = parts[:len(parts)-1]
- }
-
- names := make([][]byte, 0, len(parts))
- for _, name := range parts {
- names = append(names, append([]byte(nil), name...))
- }
-
- return names
-}
-
-func adversarialRootEntries(t *testing.T, testRepo *testgit.TestRepo) []tree.TreeEntry {
- t.Helper()
-
- blobA := testRepo.HashObject(t, "blob", []byte("blob-A\n"))
- blobB := testRepo.HashObject(t, "blob", []byte("blob-B\n"))
- blobC := testRepo.HashObject(t, "blob", []byte("blob-C\n"))
-
- subDirA := testRepo.Mktree(t,
- fmt.Sprintf("100644 blob %s\tnested-a.txt\n100755 blob %s\trun-a.sh\n", blobA.String(), blobB.String()))
- subDirB := testRepo.Mktree(t,
- fmt.Sprintf("100644 blob %s\tnested-b.txt\n100644 blob %s\tz-last\n", blobB.String(), blobC.String()))
- subDirC := testRepo.Mktree(t,
- fmt.Sprintf("120000 blob %s\tlink-c\n100644 blob %s\tchild\n", blobC.String(), blobA.String()))
- subDirD := testRepo.Mktree(t,
- fmt.Sprintf("100644 blob %s\tleaf\n", blobA.String()))
-
- return []tree.TreeEntry{
- {Mode: tree.FileModeRegular, Name: []byte("z"), ID: blobA},
- {Mode: tree.FileModeRegular, Name: []byte("A"), ID: blobB},
- {Mode: tree.FileModeRegular, Name: []byte("aa"), ID: blobC},
- {Mode: tree.FileModeRegular, Name: []byte("a0"), ID: blobA},
- {Mode: tree.FileModeRegular, Name: []byte("a-"), ID: blobB},
- {Mode: tree.FileModeRegular, Name: []byte("a."), ID: blobC},
- {Mode: tree.FileModeRegular, Name: []byte("a_"), ID: blobA},
- {Mode: tree.FileModeRegular, Name: []byte("a~"), ID: blobB},
- {Mode: tree.FileModeRegular, Name: []byte("Z"), ID: blobC},
- {Mode: tree.FileModeRegular, Name: []byte("0"), ID: blobA},
- {Mode: tree.FileModeRegular, Name: []byte("9"), ID: blobB},
- {Mode: tree.FileModeRegular, Name: []byte("00"), ID: blobC},
- {Mode: tree.FileModeRegular, Name: []byte("这是一些非 ASCII 的字符"), ID: blobC},
- {Mode: tree.FileModeRegular, Name: []byte("𲰼是新进入 Unicode 的字符"), ID: blobC},
- {Mode: tree.FileModeRegular, Name: []byte("Emoji 👀"), ID: blobC},
- {Mode: tree.FileModeRegular, Name: []byte("_"), ID: blobA},
- {Mode: tree.FileModeRegular, Name: []byte("-dash"), ID: blobB},
- {Mode: tree.FileModeRegular, Name: []byte("dot.file"), ID: blobC},
- {Mode: tree.FileModeRegular, Name: []byte(".hidden"), ID: blobA},
- {Mode: tree.FileModeRegular, Name: []byte("CAPS"), ID: blobB},
- {Mode: tree.FileModeRegular, Name: []byte("caps"), ID: blobC},
- {Mode: tree.FileModeRegular, Name: []byte("mixCase"), ID: blobA},
- {Mode: tree.FileModeRegular, Name: []byte("name with space"), ID: blobB},
- {Mode: tree.FileModeRegular, Name: []byte("name-with-dash"), ID: blobC},
- {Mode: tree.FileModeRegular, Name: []byte("name.with.dot"), ID: blobA},
- {Mode: tree.FileModeRegular, Name: []byte("name_with_underscore"), ID: blobB},
- {Mode: tree.FileModeRegular, Name: []byte("tilde~name"), ID: blobC},
- {Mode: tree.FileModeRegular, Name: []byte("brace{name}"), ID: blobA},
- {Mode: tree.FileModeRegular, Name: []byte("plus+name"), ID: blobB},
- {Mode: tree.FileModeRegular, Name: []byte("equal=name"), ID: blobC},
- {Mode: tree.FileModeRegular, Name: []byte("at@name"), ID: blobA},
- {Mode: tree.FileModeRegular, Name: []byte("percent%name"), ID: blobB},
- {Mode: tree.FileModeRegular, Name: []byte("caret^name"), ID: blobC},
- {Mode: tree.FileModeRegular, Name: []byte("comma,name"), ID: blobA},
- {Mode: tree.FileModeRegular, Name: []byte("semi;name"), ID: blobB},
- {Mode: tree.FileModeRegular, Name: []byte("paren(name)"), ID: blobC},
- {Mode: tree.FileModeRegular, Name: []byte("bracket[name]"), ID: blobA},
- {Mode: tree.FileModeExecutable, Name: []byte("exec.sh"), ID: blobB},
- {Mode: tree.FileModeSymlink, Name: []byte("sym.link"), ID: blobC},
- {Mode: tree.FileModeDir, Name: []byte("dir"), ID: subDirA},
- {Mode: tree.FileModeDir, Name: []byte("dir0"), ID: subDirB},
- {Mode: tree.FileModeDir, Name: []byte("dir.space"), ID: subDirC},
- {Mode: tree.FileModeDir, Name: []byte("x"), ID: subDirD},
- }
-}
diff --git a/object/tree/insert.go b/object/tree/insert.go
deleted file mode 100644
index 22bda74f..00000000
--- a/object/tree/insert.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package tree
-
-import (
- "fmt"
- "slices"
-)
-
-// InsertEntry inserts a tree entry while preserving Git ordering.
-//
-// InsertEntry copies newEntry.Name.
-func (tree *Tree) InsertEntry(newEntry TreeEntry) error {
- if tree.entry(newEntry.Name, true) != nil || tree.entry(newEntry.Name, false) != nil {
- return fmt.Errorf("object: tree: entry %q already exists", newEntry.Name)
- }
-
- newEntry.Name = append([]byte(nil), newEntry.Name...)
-
- insertAt, _ := slices.BinarySearchFunc(tree.Entries, newEntry.Name, func(entry TreeEntry, name []byte) int {
- return TreeEntryNameCompare(entry.Name, entry.Mode, name, newEntry.Mode == FileModeDir)
- })
- tree.Entries = slices.Insert(tree.Entries, insertAt, newEntry)
-
- return nil
-}
diff --git a/object/tree/lookup.go b/object/tree/lookup.go
deleted file mode 100644
index 249efd0f..00000000
--- a/object/tree/lookup.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package tree
-
-// Entry looks up a tree entry by name.
-//
-// The returned pointer refers to storage within tree.Entries and must not be
-// retained across InsertEntry or RemoveEntry calls.
-func (tree *Tree) Entry(name []byte) *TreeEntry {
- if len(tree.Entries) == 0 {
- return nil
- }
-
- index, ok := tree.entryIndex(name)
- if !ok {
- return nil
- }
-
- return &tree.Entries[index]
-}
diff --git a/object/tree/mode.go b/object/tree/mode.go
deleted file mode 100644
index b1cbc6bc..00000000
--- a/object/tree/mode.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package tree
-
-// FileMode represents the mode of a file in a Git tree.
-type FileMode uint32
-
-const (
- FileModeDir FileMode = 0o40000
- FileModeRegular FileMode = 0o100644
- FileModeExecutable FileMode = 0o100755
- FileModeSymlink FileMode = 0o120000
- FileModeGitlink FileMode = 0o160000
-)
diff --git a/object/tree/mode_details.go b/object/tree/mode_details.go
deleted file mode 100644
index 9c34fd7c..00000000
--- a/object/tree/mode_details.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package tree
-
-type fileModeDetails struct {
- isBlobLike bool
- isRegularFile bool
-}
-
-func (mode FileMode) details() fileModeDetails {
- return fileModeTable[mode]
-}
diff --git a/object/tree/mode_has_same_type.go b/object/tree/mode_has_same_type.go
deleted file mode 100644
index a058cb9c..00000000
--- a/object/tree/mode_has_same_type.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package tree
-
-// HasSameType reports whether mode and other describe the same tree entry kind.
-//
-// Regular files and executable files have the same type for diff-status purposes.
-func (mode FileMode) HasSameType(other FileMode) bool {
- if mode == other {
- return true
- }
-
- return mode.details().isRegularFile && other.details().isRegularFile
-}
diff --git a/object/tree/mode_is_blob_like.go b/object/tree/mode_is_blob_like.go
deleted file mode 100644
index 3ec3a308..00000000
--- a/object/tree/mode_is_blob_like.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package tree
-
-// IsBlobLike reports whether mode names one blob-like tree entry kind.
-//
-// Blob-like entries store blob object IDs as their targets.
-func (mode FileMode) IsBlobLike() bool {
- return mode.details().isBlobLike
-}
diff --git a/object/tree/mode_is_regular_file.go b/object/tree/mode_is_regular_file.go
deleted file mode 100644
index 115395c0..00000000
--- a/object/tree/mode_is_regular_file.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package tree
-
-// IsRegularFile reports whether mode names one regular-file variant.
-func (mode FileMode) IsRegularFile() bool {
- return mode.details().isRegularFile
-}
diff --git a/object/tree/mode_table.go b/object/tree/mode_table.go
deleted file mode 100644
index 1695f270..00000000
--- a/object/tree/mode_table.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package tree
-
-var fileModeTable = map[FileMode]fileModeDetails{ //nolint:gochecknoglobals
- FileModeDir: {
- isBlobLike: false,
- isRegularFile: false,
- },
- FileModeRegular: {
- isBlobLike: true,
- isRegularFile: true,
- },
- FileModeExecutable: {
- isBlobLike: true,
- isRegularFile: true,
- },
- FileModeSymlink: {
- isBlobLike: true,
- isRegularFile: false,
- },
- FileModeGitlink: {
- isBlobLike: false,
- isRegularFile: false,
- },
-}
diff --git a/object/tree/name.go b/object/tree/name.go
deleted file mode 100644
index 02af3292..00000000
--- a/object/tree/name.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package tree
-
-// TreeEntryNameCompare compares names using Git tree ordering rules.
-func TreeEntryNameCompare(entryName []byte, entryMode FileMode, searchName []byte, searchIsTree bool) int {
- isEntryTree := entryMode == FileModeDir
-
- entryLen := len(entryName)
- if isEntryTree {
- entryLen++
- }
-
- searchLen := len(searchName)
- if searchIsTree {
- searchLen++
- }
-
- n := min(searchLen, entryLen)
-
- for i := range n {
- var ec, sc byte
- if i < len(entryName) {
- ec = entryName[i]
- } else {
- ec = '/'
- }
-
- if i < len(searchName) {
- sc = searchName[i]
- } else {
- sc = '/'
- }
-
- if ec < sc {
- return -1
- }
-
- if ec > sc {
- return 1
- }
- }
-
- if entryLen < searchLen {
- return -1
- }
-
- if entryLen > searchLen {
- return 1
- }
-
- return 0
-}
diff --git a/object/tree/parse.go b/object/tree/parse.go
deleted file mode 100644
index bb874828..00000000
--- a/object/tree/parse.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package tree
-
-import (
- "bytes"
- "fmt"
- "strconv"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
-)
-
-// Parse decodes a tree object body into a fully materialized Tree.
-func Parse(body []byte, algo objectid.Algorithm) (*Tree, error) {
- var entries []TreeEntry
-
- i := 0
- for i < len(body) {
- space := bytes.IndexByte(body[i:], ' ')
- if space < 0 {
- return nil, fmt.Errorf("object: tree: missing mode terminator")
- }
-
- modeBytes := body[i : i+space]
- i += space + 1
-
- nul := bytes.IndexByte(body[i:], 0)
- if nul < 0 {
- return nil, fmt.Errorf("object: tree: missing name terminator")
- }
-
- nameBytes := body[i : i+nul]
- i += nul + 1
-
- idEnd := i + algo.Size()
- if idEnd > len(body) {
- return nil, fmt.Errorf("object: tree: truncated child object id")
- }
-
- id, err := objectid.FromBytes(algo, body[i:idEnd])
- if err != nil {
- return nil, err
- }
-
- i = idEnd
-
- mode, err := strconv.ParseUint(string(modeBytes), 8, 32)
- if err != nil {
- return nil, fmt.Errorf("object: tree: parse mode: %w", err)
- }
-
- entries = append(entries, TreeEntry{
- Mode: FileMode(mode),
- Name: append([]byte(nil), nameBytes...),
- ID: id,
- })
- }
-
- return &Tree{Entries: entries}, nil
-}
diff --git a/object/tree/parse_test.go b/object/tree/parse_test.go
deleted file mode 100644
index 2b98ede7..00000000
--- a/object/tree/parse_test.go
+++ /dev/null
@@ -1,109 +0,0 @@
-package tree_test
-
-import (
- "bytes"
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/tree"
-)
-
-func TestTreeParseFromGit(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- entries := adversarialRootEntries(t, testRepo)
-
- inserted := &tree.Tree{}
- for _, entry := range entries {
- err := inserted.InsertEntry(entry)
- if err != nil {
- t.Fatalf("InsertEntry(%q): %v", entry.Name, err)
- }
- }
-
- treeID := testRepo.Mktree(t, buildGitMktreeInput(inserted.Entries))
-
- rawBody := testRepo.CatFile(t, "tree", treeID)
-
- parsed, err := tree.Parse(rawBody, algo)
- if err != nil {
- t.Fatalf("ParseTree: %v", err)
- }
-
- if len(parsed.Entries) != len(inserted.Entries) {
- t.Fatalf("entry count = %d, want %d", len(parsed.Entries), len(inserted.Entries))
- }
-
- for i := range inserted.Entries {
- got := parsed.Entries[i]
-
- want := inserted.Entries[i]
- if got.Mode != want.Mode || got.ID != want.ID || !bytes.Equal(got.Name, want.Name) {
- t.Fatalf("entry[%d] mismatch: got (%o,%q,%s) want (%o,%q,%s)",
- i, got.Mode, got.Name, got.ID, want.Mode, want.Name, want.ID)
- }
- }
-
- lsNames := gitLsTreeNames(testRepo.RunBytes(t, "ls-tree", "--name-only", "-z", treeID.String()))
- if len(lsNames) != len(parsed.Entries) {
- t.Fatalf("ls-tree names = %d, want %d", len(lsNames), len(parsed.Entries))
- }
-
- for i := range lsNames {
- if !bytes.Equal(lsNames[i], parsed.Entries[i].Name) {
- t.Fatalf("ordering mismatch at %d: git=%q parsed=%q", i, lsNames[i], parsed.Entries[i].Name)
- }
- }
-
- for _, want := range inserted.Entries {
- got := parsed.Entry(want.Name)
-
- if got == nil {
- t.Fatalf("Entry(%q) returned nil", want.Name)
- }
-
- if got.Mode != want.Mode || got.ID != want.ID {
- t.Fatalf("Entry(%q) mismatch", want.Name)
- }
- }
-
- if parsed.Entry([]byte("does-not-exist")) != nil {
- t.Fatalf("Entry on missing name should be nil")
- }
- })
-}
-
-func TestTreeInsertEntryCopiesName(t *testing.T) {
- t.Parallel()
-
- var tr tree.Tree
-
- name := []byte("alpha")
- entry := tree.TreeEntry{
- Mode: tree.FileModeRegular,
- Name: name,
- ID: objectid.ObjectID{},
- }
-
- err := tr.InsertEntry(entry)
- if err != nil {
- t.Fatalf("InsertEntry: %v", err)
- }
-
- name[0] = 'b'
-
- got := tr.Entry([]byte("alpha"))
- if got == nil {
- t.Fatalf("Entry(alpha) returned nil")
- }
-
- if !bytes.Equal(got.Name, []byte("alpha")) {
- t.Fatalf("stored name = %q, want %q", got.Name, []byte("alpha"))
- }
-
- if tr.Entry([]byte("blpha")) != nil {
- t.Fatalf("mutating caller name should not affect stored entry")
- }
-}
diff --git a/object/tree/path_append.go b/object/tree/path_append.go
deleted file mode 100644
index 609d5279..00000000
--- a/object/tree/path_append.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package tree
-
-// AppendPath appends path to dst as one slash-separated byte path.
-func AppendPath(dst []byte, path [][]byte) []byte {
- for i := range path {
- if i > 0 {
- dst = append(dst, '/')
- }
-
- dst = append(dst, path[i]...)
- }
-
- return dst
-}
diff --git a/object/tree/path_clone.go b/object/tree/path_clone.go
deleted file mode 100644
index a4668add..00000000
--- a/object/tree/path_clone.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package tree
-
-import (
- "bytes"
- "slices"
-)
-
-// ClonePath returns one deep copy of path.
-func ClonePath(path [][]byte) [][]byte {
- cloned := slices.Clone(path)
- for i := range cloned {
- cloned[i] = bytes.Clone(cloned[i])
- }
-
- return cloned
-}
diff --git a/object/tree/path_prefix.go b/object/tree/path_prefix.go
deleted file mode 100644
index ed658cee..00000000
--- a/object/tree/path_prefix.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package tree
-
-import (
- "bytes"
- "slices"
-)
-
-// HasPathPrefix reports whether path begins with prefix as whole components.
-func HasPathPrefix(path, prefix [][]byte) bool {
- if len(prefix) == 0 {
- return true
- }
-
- if len(path) < len(prefix) {
- return false
- }
-
- return slices.EqualFunc(path[:len(prefix)], prefix, bytes.Equal)
-}
diff --git a/object/tree/path_split.go b/object/tree/path_split.go
deleted file mode 100644
index c147dd25..00000000
--- a/object/tree/path_split.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package tree
-
-import (
- "bytes"
-)
-
-// SplitPath splits one slash-separated tree path into components.
-func SplitPath(path []byte) [][]byte {
- if len(path) == 0 {
- return nil
- }
-
- parts := bytes.Split(path, []byte{'/'})
- for i := range parts {
- parts[i] = bytes.Clone(parts[i])
- }
-
- return parts
-}
diff --git a/object/tree/remove.go b/object/tree/remove.go
deleted file mode 100644
index 94de88da..00000000
--- a/object/tree/remove.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package tree
-
-import (
- "fmt"
- "slices"
-)
-
-// RemoveEntry removes a tree entry by name.
-func (tree *Tree) RemoveEntry(name []byte) error {
- if len(tree.Entries) == 0 {
- return fmt.Errorf("object: tree: entry %q not found", name)
- }
-
- index, ok := tree.entryIndex(name)
- if !ok {
- return fmt.Errorf("object: tree: entry %q not found", name)
- }
-
- tree.Entries = slices.Delete(tree.Entries, index, index+1)
-
- return nil
-}
diff --git a/object/tree/serialize.go b/object/tree/serialize.go
deleted file mode 100644
index 69deacda..00000000
--- a/object/tree/serialize.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package tree
-
-import (
- "errors"
- "strconv"
-
- objectheader "codeberg.org/lindenii/furgit/object/header"
- objecttype "codeberg.org/lindenii/furgit/object/type"
-)
-
-// SerializeWithoutHeader renders the raw tree body bytes.
-func (tree *Tree) SerializeWithoutHeader() ([]byte, error) {
- var bodyLen int
-
- for _, entry := range tree.Entries {
- mode := strconv.FormatUint(uint64(entry.Mode), 8)
- bodyLen += len(mode) + 1 + len(entry.Name) + 1 + entry.ID.Algorithm().Size()
- }
-
- body := make([]byte, bodyLen)
- pos := 0
-
- for _, entry := range tree.Entries {
- mode := strconv.FormatUint(uint64(entry.Mode), 8)
- pos += copy(body[pos:], mode)
- body[pos] = ' '
- pos++
- pos += copy(body[pos:], entry.Name)
- body[pos] = 0
- pos++
- id := entry.ID.Bytes()
- pos += copy(body[pos:], id)
- }
-
- return body, nil
-}
-
-// SerializeWithHeader renders the raw object (header + body).
-func (tree *Tree) SerializeWithHeader() ([]byte, error) {
- body, err := tree.SerializeWithoutHeader()
- if err != nil {
- return nil, err
- }
-
- header, ok := objectheader.Encode(objecttype.TypeTree, int64(len(body)))
- if !ok {
- return nil, errors.New("object: tree: failed to encode object header")
- }
-
- raw := make([]byte, len(header)+len(body))
- copy(raw, header)
- copy(raw[len(header):], body)
-
- return raw, nil
-}
diff --git a/object/tree/serialize_test.go b/object/tree/serialize_test.go
deleted file mode 100644
index 9c9a2f1c..00000000
--- a/object/tree/serialize_test.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package tree_test
-
-import (
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/tree"
-)
-
-func TestTreeSerialize(t *testing.T) {
- t.Parallel()
- testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
- testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
- entries := adversarialRootEntries(t, testRepo)
- obj := &tree.Tree{}
-
- for i := len(entries) - 1; i >= 0; i-- {
- err := obj.InsertEntry(entries[i])
- if err != nil {
- t.Fatalf("InsertEntry(%q): %v", entries[i].Name, err)
- }
- }
-
- if len(obj.Entries) < 32 {
- t.Fatalf("expected at least 32 entries, got %d", len(obj.Entries))
- }
-
- dup := obj.Entries[0]
-
- err := obj.InsertEntry(dup)
- if err == nil {
- t.Fatalf("duplicate InsertEntry should fail")
- }
-
- removed := obj.Entries[len(obj.Entries)/2]
-
- err = obj.RemoveEntry(removed.Name)
- if err != nil {
- t.Fatalf("RemoveEntry(%q): %v", removed.Name, err)
- }
-
- if obj.Entry(removed.Name) != nil {
- t.Fatalf("Entry(%q) should be nil after remove", removed.Name)
- }
-
- err = obj.RemoveEntry([]byte("no-such-entry"))
- if err == nil {
- t.Fatalf("RemoveEntry missing entry should fail")
- }
-
- err = obj.InsertEntry(removed)
- if err != nil {
- t.Fatalf("re-InsertEntry(%q): %v", removed.Name, err)
- }
-
- if obj.Entry(removed.Name) == nil {
- t.Fatalf("Entry(%q) should exist after reinsert", removed.Name)
- }
-
- wantTreeID := testRepo.Mktree(t, buildGitMktreeInput(obj.Entries))
-
- rawObj, err := obj.SerializeWithHeader()
- if err != nil {
- t.Fatalf("SerializeWithHeader: %v", err)
- }
-
- gotTreeID := algo.Sum(rawObj)
- if gotTreeID != wantTreeID {
- t.Fatalf("tree id mismatch: got %s want %s", gotTreeID, wantTreeID)
- }
- })
-}
diff --git a/object/tree/tree.go b/object/tree/tree.go
deleted file mode 100644
index d0c7f4f0..00000000
--- a/object/tree/tree.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// Package tree provides representations, parsers, and serializers for tree objects.
-package tree
-
-// Tree represents a fully materialized Git tree object.
-//
-// Labels: MT-Unsafe.
-type Tree struct {
- // Entries must be sorted by TreeEntryNameCompare.
- // Use the Tree methods to preserve ordering and copy semantics rather than
- // modifying the slice directly.
- Entries []TreeEntry
-}
diff --git a/object/tree/type.go b/object/tree/type.go
deleted file mode 100644
index 416544af..00000000
--- a/object/tree/type.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package tree
-
-import objecttype "codeberg.org/lindenii/furgit/object/type"
-
-// ObjectType returns TypeTree.
-func (tree *Tree) ObjectType() objecttype.Type {
- _ = tree
-
- return objecttype.TypeTree
-}
diff --git a/object/type/details.go b/object/type/details.go
deleted file mode 100644
index 17bdcfd4..00000000
--- a/object/type/details.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package objecttype
-
-type typeDetails struct {
- name string
- isBaseObject bool
-}
-
-func (ty Type) details() typeDetails {
- return typeTable[ty]
-}
diff --git a/object/type/is_base.go b/object/type/is_base.go
deleted file mode 100644
index cdc11f5b..00000000
--- a/object/type/is_base.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package objecttype
-
-// IsBaseObject reports whether ty is one of the four canonical Git object
-// types encoded directly in pack entries.
-func (ty Type) IsBaseObject() bool {
- return ty.details().isBaseObject
-}
diff --git a/object/type/name.go b/object/type/name.go
deleted file mode 100644
index c95fe90b..00000000
--- a/object/type/name.go
+++ /dev/null
@@ -1,11 +0,0 @@
-package objecttype
-
-// Name returns the canonical Git object type name.
-func (ty Type) Name() (string, bool) {
- details := ty.details()
- if details.name == "" {
- return "", false
- }
-
- return details.name, true
-}
diff --git a/object/type/parse.go b/object/type/parse.go
deleted file mode 100644
index bc5ca736..00000000
--- a/object/type/parse.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package objecttype
-
-// Parse parses a canonical Git object type name.
-func Parse(name string) (Type, bool) {
- ty, ok := typeByName[name]
-
- return ty, ok
-}
diff --git a/object/type/table.go b/object/type/table.go
deleted file mode 100644
index 19cc760d..00000000
--- a/object/type/table.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package objecttype
-
-//nolint:gochecknoglobals
-var typeTable = [...]typeDetails{
- TypeInvalid: {},
- TypeCommit: {name: "commit", isBaseObject: true},
- TypeTree: {name: "tree", isBaseObject: true},
- TypeBlob: {name: "blob", isBaseObject: true},
- TypeTag: {name: "tag", isBaseObject: true},
- TypeFuture: {},
- TypeOfsDelta: {},
- TypeRefDelta: {},
-}
-
-//nolint:gochecknoglobals
-var typeByName = map[string]Type{
- typeTable[TypeCommit].name: TypeCommit,
- typeTable[TypeTree].name: TypeTree,
- typeTable[TypeBlob].name: TypeBlob,
- typeTable[TypeTag].name: TypeTag,
-}
diff --git a/object/type/type.go b/object/type/type.go
deleted file mode 100644
index 18e0ac35..00000000
--- a/object/type/type.go
+++ /dev/null
@@ -1,16 +0,0 @@
-// Package objecttype provides Git object type tags and names.
-package objecttype
-
-// Type mirrors Git object type tags in packfiles.
-type Type uint8
-
-const (
- TypeInvalid Type = 0
- TypeCommit Type = 1
- TypeTree Type = 2
- TypeBlob Type = 3
- TypeTag Type = 4
- TypeFuture Type = 5
- TypeOfsDelta Type = 6
- TypeRefDelta Type = 7
-)