aboutsummaryrefslogtreecommitdiff
path: root/format/commitgraph/read
diff options
context:
space:
mode:
Diffstat (limited to 'format/commitgraph/read')
-rw-r--r--format/commitgraph/read/bloom.go117
-rw-r--r--format/commitgraph/read/close.go20
-rw-r--r--format/commitgraph/read/commitat.go85
-rw-r--r--format/commitgraph/read/commits.go20
-rw-r--r--format/commitgraph/read/doc.go2
-rw-r--r--format/commitgraph/read/edges.go48
-rw-r--r--format/commitgraph/read/errors.go58
-rw-r--r--format/commitgraph/read/generation.go43
-rw-r--r--format/commitgraph/read/hash.go79
-rw-r--r--format/commitgraph/read/iterators.go45
-rw-r--r--format/commitgraph/read/layer.go28
-rw-r--r--format/commitgraph/read/layer_close.go33
-rw-r--r--format/commitgraph/read/layer_lookup.go53
-rw-r--r--format/commitgraph/read/layer_open.go81
-rw-r--r--format/commitgraph/read/layer_parse.go276
-rw-r--r--format/commitgraph/read/layer_pos.go21
-rw-r--r--format/commitgraph/read/layerinfo.go23
-rw-r--r--format/commitgraph/read/lookup.go29
-rw-r--r--format/commitgraph/read/mode.go11
-rw-r--r--format/commitgraph/read/oidat.go36
-rw-r--r--format/commitgraph/read/open.go26
-rw-r--r--format/commitgraph/read/open_chain.go133
-rw-r--r--format/commitgraph/read/open_single.go32
-rw-r--r--format/commitgraph/read/parents.go67
-rw-r--r--format/commitgraph/read/position.go38
-rw-r--r--format/commitgraph/read/read_test.go322
-rw-r--r--format/commitgraph/read/reader.go16
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/HEAD1
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/config4
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/commit-graph-chain2
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/graph-bf985c21612a52070d8b008e6ef51edf8b609401.graphbin0 -> 4810 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/graph-dd7578d5216ca76c25b19631ba90f7498aeabbe7.graphbin0 -> 7088 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/packs2
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.bitmapbin0 -> 8234 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.idxbin0 -> 13252 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.packbin0 -> 34730 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.revbin0 -> 1792 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/refs/heads/master1
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/HEAD1
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/config4
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/info/commit-graphbin0 -> 9068 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/info/packs2
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.bitmapbin0 -> 7780 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.idxbin0 -> 11152 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.packbin0 -> 28664 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.revbin0 -> 1492 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/refs/heads/main1
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/HEAD1
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/config4
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/info/commit-graphbin0 -> 5912 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/info/packs2
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.bitmapbin0 -> 5452 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.idxbin0 -> 7792 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.packbin0 -> 18969 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.revbin0 -> 1012 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/refs/heads/master1
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/HEAD1
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/config6
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/commit-graph-chain2
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/graph-505cab61f8ddfa614301e8f97943112739236c6bcd19ed4d1f7c6b830cab4f62.graphbin0 -> 9260 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/graph-77c47bd6ca2ce17208c9361717a5823c0cb4b5ee336a14959678e060d674ffb6.graphbin0 -> 6154 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/packs2
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.bitmapbin0 -> 8234 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.idxbin0 -> 18496 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.packbin0 -> 41482 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.revbin0 -> 1816 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/refs/heads/master1
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/HEAD1
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/config6
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/info/commit-graphbin0 -> 11960 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/info/packs2
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.bitmapbin0 -> 7804 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.idxbin0 -> 15496 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.packbin0 -> 34252 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.revbin0 -> 1516 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/refs/heads/main1
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/HEAD1
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/config6
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/info/commit-graphbin0 -> 7844 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/info/packs2
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.bitmapbin0 -> 5476 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.idxbin0 -> 10696 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.packbin0 -> 22569 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.revbin0 -> 1036 bytes
-rw-r--r--format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/refs/heads/master1
85 files changed, 1800 insertions, 0 deletions
diff --git a/format/commitgraph/read/bloom.go b/format/commitgraph/read/bloom.go
new file mode 100644
index 00000000..12dd6db3
--- /dev/null
+++ b/format/commitgraph/read/bloom.go
@@ -0,0 +1,117 @@
+package read
+
+import (
+ "encoding/binary"
+
+ "codeberg.org/lindenii/furgit/format/commitgraph/bloom"
+ "codeberg.org/lindenii/furgit/internal/intconv"
+)
+
+// HasBloom reports whether any layer has changed-path Bloom data.
+func (reader *Reader) HasBloom() bool {
+ for i := range reader.layers {
+ layer := &reader.layers[i]
+ if layer.chunkBloomIndex != nil && layer.chunkBloomData != nil && layer.bloomSettings != nil {
+ return true
+ }
+ }
+
+ return false
+}
+
+// BloomVersion returns the changed-path Bloom hash version, or 0 if absent.
+func (reader *Reader) BloomVersion() uint8 {
+ for i := len(reader.layers) - 1; i >= 0; i-- {
+ layer := &reader.layers[i]
+ if layer.bloomSettings != nil {
+ version, err := intconv.Uint32ToUint8(layer.bloomSettings.HashVersion)
+ if err != nil {
+ return 0
+ }
+
+ return version
+ }
+ }
+
+ return 0
+}
+
+// BloomFilterAt returns one commit's changed-path Bloom filter.
+//
+// The returned filter borrows reader-owned mapped commit-graph data and is
+// only valid until the reader is closed.
+//
+// Returns BloomUnavailableError when this commit graph has no Bloom data.
+func (reader *Reader) BloomFilterAt(pos Position) (bloom.Filter, error) {
+ layer, err := reader.layerByPosition(pos)
+ if err != nil {
+ return bloom.Filter{}, err
+ }
+
+ if layer.chunkBloomIndex == nil || layer.chunkBloomData == nil || layer.bloomSettings == nil {
+ return bloom.Filter{}, &BloomUnavailableError{Pos: pos}
+ }
+
+ start, end, err := bloomRange(layer, pos.Index)
+ if err != nil {
+ return bloom.Filter{}, err
+ }
+
+ filter := bloom.NewFilter(
+ layer.chunkBloomData[bloom.DataHeaderSize+start:bloom.DataHeaderSize+end],
+ *layer.bloomSettings,
+ )
+
+ return filter, nil
+}
+
+func bloomRange(layer *layer, commitIndex uint32) (int, int, error) {
+ off64 := uint64(commitIndex) * 4
+
+ off, err := intconv.Uint64ToInt(off64)
+ if err != nil {
+ return 0, 0, err
+ }
+
+ end := binary.BigEndian.Uint32(layer.chunkBloomIndex[off : off+4])
+
+ var start uint32
+
+ if commitIndex > 0 {
+ prevOff64 := uint64(commitIndex-1) * 4
+
+ prevOff, err := intconv.Uint64ToInt(prevOff64)
+ if err != nil {
+ return 0, 0, err
+ }
+
+ start = binary.BigEndian.Uint32(layer.chunkBloomIndex[prevOff : prevOff+4])
+ }
+
+ if end < start {
+ return 0, 0, &MalformedError{Path: layer.path, Reason: "invalid BIDX range"}
+ }
+
+ bdatLen := len(layer.chunkBloomData) - bloom.DataHeaderSize
+
+ bdatLenU32, err := intconv.IntToUint32(bdatLen)
+ if err != nil {
+ return 0, 0, err
+ }
+
+ if end > bdatLenU32 {
+ return 0, 0, &MalformedError{Path: layer.path, Reason: "BIDX range out of BDAT bounds"}
+ }
+
+ startInt, err := intconv.Uint64ToInt(uint64(start))
+ if err != nil {
+ return 0, 0, err
+ }
+
+ endInt, err := intconv.Uint64ToInt(uint64(end))
+ if err != nil {
+ return 0, 0, err
+ }
+
+ return startInt, endInt, nil
+}
diff --git a/format/commitgraph/read/close.go b/format/commitgraph/read/close.go
new file mode 100644
index 00000000..f8b6141a
--- /dev/null
+++ b/format/commitgraph/read/close.go
@@ -0,0 +1,20 @@
+package read
+
+// Close releases all mapped commit-graph files.
+//
+// Repeated calls to Close are undefined behavior.
+func (reader *Reader) Close() error {
+ var closeErr error
+
+ for i := len(reader.layers) - 1; i >= 0; i-- {
+ err := reader.layers[i].close()
+ if err != nil && closeErr == nil {
+ closeErr = err
+ }
+ }
+
+ reader.layers = nil
+ reader.total = 0
+
+ return closeErr
+}
diff --git a/format/commitgraph/read/commitat.go b/format/commitgraph/read/commitat.go
new file mode 100644
index 00000000..a39c5ccd
--- /dev/null
+++ b/format/commitgraph/read/commitat.go
@@ -0,0 +1,85 @@
+package read
+
+import (
+ "encoding/binary"
+
+ "codeberg.org/lindenii/furgit/internal/intconv"
+ objectid "codeberg.org/lindenii/furgit/object/id"
+)
+
+// CommitAt returns decoded commit-graph metadata at one position.
+func (reader *Reader) CommitAt(pos Position) (Commit, error) {
+ layer, err := reader.layerByPosition(pos)
+ if err != nil {
+ return Commit{}, err
+ }
+
+ hashSize := reader.algo.Size()
+ stride := hashSize + 16
+
+ strideU64, err := intconv.IntToUint64(stride)
+ if err != nil {
+ return Commit{}, err
+ }
+
+ start64 := uint64(pos.Index) * strideU64
+ end64 := start64 + strideU64
+
+ start, err := intconv.Uint64ToInt(start64)
+ if err != nil {
+ return Commit{}, err
+ }
+
+ end, err := intconv.Uint64ToInt(end64)
+ if err != nil {
+ return Commit{}, err
+ }
+
+ record := layer.chunkCommit[start:end]
+
+ treeOID, err := objectid.FromBytes(reader.algo, record[:hashSize])
+ if err != nil {
+ return Commit{}, err
+ }
+
+ oid, err := reader.OIDAt(pos)
+ if err != nil {
+ return Commit{}, err
+ }
+
+ p1 := binary.BigEndian.Uint32(record[hashSize : hashSize+4])
+ p2 := binary.BigEndian.Uint32(record[hashSize+4 : hashSize+8])
+ genAndTimeHi := binary.BigEndian.Uint32(record[hashSize+8 : hashSize+12])
+ timeLow := binary.BigEndian.Uint32(record[hashSize+12 : hashSize+16])
+
+ timeHigh := uint64(genAndTimeHi & 0x3)
+ commitTimeU64 := (timeHigh << 32) | uint64(timeLow)
+
+ commitTime, err := intconv.Uint64ToInt64(commitTimeU64)
+ if err != nil {
+ return Commit{}, err
+ }
+
+ generationV1 := genAndTimeHi >> 2
+
+ generationV2, err := reader.readGenerationV2(layer, pos.Index, commitTimeU64)
+ if err != nil {
+ return Commit{}, err
+ }
+
+ parent1, parent2, extra, err := reader.decodeParents(layer, p1, p2)
+ if err != nil {
+ return Commit{}, err
+ }
+
+ return Commit{
+ OID: oid,
+ TreeOID: treeOID,
+ Parent1: parent1,
+ Parent2: parent2,
+ ExtraParents: extra,
+ CommitTimeUnix: commitTime,
+ GenerationV1: generationV1,
+ GenerationV2: generationV2,
+ }, nil
+}
diff --git a/format/commitgraph/read/commits.go b/format/commitgraph/read/commits.go
new file mode 100644
index 00000000..48984ecb
--- /dev/null
+++ b/format/commitgraph/read/commits.go
@@ -0,0 +1,20 @@
+package read
+
+import objectid "codeberg.org/lindenii/furgit/object/id"
+
+// Commit stores decoded commit-graph record data.
+type Commit struct {
+ OID objectid.ObjectID
+ TreeOID objectid.ObjectID
+ Parent1 ParentRef
+ Parent2 ParentRef
+ ExtraParents []Position
+ CommitTimeUnix int64
+ GenerationV1 uint32
+ GenerationV2 uint64
+}
+
+// NumCommits returns total commits across loaded layers.
+func (reader *Reader) NumCommits() uint32 {
+ return reader.total
+}
diff --git a/format/commitgraph/read/doc.go b/format/commitgraph/read/doc.go
new file mode 100644
index 00000000..573ddc19
--- /dev/null
+++ b/format/commitgraph/read/doc.go
@@ -0,0 +1,2 @@
+// Package read provides routines for reading commit graphs.
+package read
diff --git a/format/commitgraph/read/edges.go b/format/commitgraph/read/edges.go
new file mode 100644
index 00000000..96ffeb6d
--- /dev/null
+++ b/format/commitgraph/read/edges.go
@@ -0,0 +1,48 @@
+package read
+
+import (
+ "encoding/binary"
+
+ "codeberg.org/lindenii/furgit/format/commitgraph"
+ "codeberg.org/lindenii/furgit/internal/intconv"
+)
+
+func (reader *Reader) decodeExtraEdgeList(layer *layer, edgeStart uint32) ([]Position, error) {
+ if len(layer.chunkExtraEdges) == 0 {
+ return nil, &MalformedError{Path: layer.path, Reason: "missing EDGE chunk"}
+ }
+
+ out := make([]Position, 0)
+
+ cur := edgeStart
+ for {
+ off64 := uint64(cur) * 4
+
+ off, err := intconv.Uint64ToInt(off64)
+ if err != nil {
+ return nil, err
+ }
+
+ if off+4 > len(layer.chunkExtraEdges) {
+ return nil, &MalformedError{Path: layer.path, Reason: "EDGE index out of range"}
+ }
+
+ word := binary.BigEndian.Uint32(layer.chunkExtraEdges[off : off+4])
+ parentGlobal := word & commitgraph.ParentLastMask
+
+ parentPos, err := reader.globalToPosition(parentGlobal)
+ if err != nil {
+ return nil, err
+ }
+
+ out = append(out, parentPos)
+
+ if word&commitgraph.ParentExtraMask != 0 {
+ break
+ }
+
+ cur++
+ }
+
+ return out, nil
+}
diff --git a/format/commitgraph/read/errors.go b/format/commitgraph/read/errors.go
new file mode 100644
index 00000000..0a32a368
--- /dev/null
+++ b/format/commitgraph/read/errors.go
@@ -0,0 +1,58 @@
+package read
+
+import (
+ "fmt"
+
+ objectid "codeberg.org/lindenii/furgit/object/id"
+)
+
+// NotFoundError reports a missing commit graph entry by object ID.
+type NotFoundError struct {
+ OID objectid.ObjectID
+}
+
+// Error implements error.
+func (err *NotFoundError) Error() string {
+ return fmt.Sprintf("commitgraph: object not found: %s", err.OID)
+}
+
+// PositionOutOfRangeError reports an invalid graph position.
+type PositionOutOfRangeError struct {
+ Pos Position
+}
+
+// Error implements error.
+func (err *PositionOutOfRangeError) Error() string {
+ return fmt.Sprintf("commitgraph: position out of range: graph=%d index=%d", err.Pos.Graph, err.Pos.Index)
+}
+
+// MalformedError reports malformed commit-graph data.
+type MalformedError struct {
+ Path string
+ Reason string
+}
+
+// Error implements error.
+func (err *MalformedError) Error() string {
+ return fmt.Sprintf("commitgraph: malformed %q: %s", err.Path, err.Reason)
+}
+
+// UnsupportedVersionError reports unsupported commit-graph version.
+type UnsupportedVersionError struct {
+ Version uint8
+}
+
+// Error implements error.
+func (err *UnsupportedVersionError) Error() string {
+ return fmt.Sprintf("commitgraph: unsupported version %d", err.Version)
+}
+
+// BloomUnavailableError reports missing changed-path bloom data at one position.
+type BloomUnavailableError struct {
+ Pos Position
+}
+
+// Error implements error.
+func (err *BloomUnavailableError) Error() string {
+ return fmt.Sprintf("commitgraph: bloom unavailable at position graph=%d index=%d", err.Pos.Graph, err.Pos.Index)
+}
diff --git a/format/commitgraph/read/generation.go b/format/commitgraph/read/generation.go
new file mode 100644
index 00000000..62e47996
--- /dev/null
+++ b/format/commitgraph/read/generation.go
@@ -0,0 +1,43 @@
+package read
+
+import (
+ "encoding/binary"
+
+ "codeberg.org/lindenii/furgit/format/commitgraph"
+ "codeberg.org/lindenii/furgit/internal/intconv"
+)
+
+func (reader *Reader) readGenerationV2(layer *layer, index uint32, commitTime uint64) (uint64, error) {
+ if len(layer.chunkGeneration) == 0 {
+ return 0, nil
+ }
+
+ off64 := uint64(index) * 4
+
+ off, err := intconv.Uint64ToInt(off64)
+ if err != nil {
+ return 0, err
+ }
+
+ value := binary.BigEndian.Uint32(layer.chunkGeneration[off : off+4])
+
+ if value&commitgraph.GenerationOverflow == 0 {
+ return commitTime + uint64(value), nil
+ }
+
+ gdo2Index := value ^ commitgraph.GenerationOverflow
+ gdo2Off64 := uint64(gdo2Index) * 8
+
+ gdo2Off, err := intconv.Uint64ToInt(gdo2Off64)
+ if err != nil {
+ return 0, err
+ }
+
+ if gdo2Off+8 > len(layer.chunkGenerationOv) {
+ return 0, &MalformedError{Path: layer.path, Reason: "GDO2 index out of range"}
+ }
+
+ overflow := binary.BigEndian.Uint64(layer.chunkGenerationOv[gdo2Off : gdo2Off+8])
+
+ return commitTime + overflow, nil
+}
diff --git a/format/commitgraph/read/hash.go b/format/commitgraph/read/hash.go
new file mode 100644
index 00000000..3a525afe
--- /dev/null
+++ b/format/commitgraph/read/hash.go
@@ -0,0 +1,79 @@
+package read
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+
+ objectid "codeberg.org/lindenii/furgit/object/id"
+)
+
+// HashVersion returns the commit-graph hash version.
+func (reader *Reader) HashVersion() uint8 {
+ return reader.hashVersion
+}
+
+func validateChainBaseHashes(algo objectid.Algorithm, chain []string, idx int, graph *layer) error {
+ if idx == 0 {
+ if len(graph.chunkBaseGraphs) != 0 {
+ return &MalformedError{Path: graph.path, Reason: "unexpected BASE chunk in first graph"}
+ }
+
+ return nil
+ }
+
+ hashSize := algo.Size()
+
+ expectedLen := idx * hashSize
+ if len(graph.chunkBaseGraphs) != expectedLen {
+ return &MalformedError{
+ Path: graph.path,
+ Reason: fmt.Sprintf("BASE chunk length %d does not match expected %d", len(graph.chunkBaseGraphs), expectedLen),
+ }
+ }
+
+ for i := range idx {
+ start := i * hashSize
+ end := start + hashSize
+
+ baseHash, err := objectid.FromBytes(algo, graph.chunkBaseGraphs[start:end])
+ if err != nil {
+ return err
+ }
+
+ if baseHash.String() != chain[i] {
+ return &MalformedError{
+ Path: graph.path,
+ Reason: fmt.Sprintf("BASE chunk mismatch at index %d", i),
+ }
+ }
+ }
+
+ return nil
+}
+
+func verifyTrailerHash(data []byte, algo objectid.Algorithm, path string) error {
+ hashSize := algo.Size()
+ if len(data) < hashSize {
+ return &MalformedError{Path: path, Reason: "file too short for trailer"}
+ }
+
+ hashImpl, err := algo.New()
+ if err != nil {
+ return err
+ }
+
+ _, err = io.Copy(hashImpl, bytes.NewReader(data[:len(data)-hashSize]))
+ if err != nil {
+ return err
+ }
+
+ got := hashImpl.Sum(nil)
+
+ want := data[len(data)-hashSize:]
+ if !bytes.Equal(got, want) {
+ return &MalformedError{Path: path, Reason: "trailer hash mismatch"}
+ }
+
+ return nil
+}
diff --git a/format/commitgraph/read/iterators.go b/format/commitgraph/read/iterators.go
new file mode 100644
index 00000000..85c56ff1
--- /dev/null
+++ b/format/commitgraph/read/iterators.go
@@ -0,0 +1,45 @@
+package read
+
+import (
+ "iter"
+
+ "codeberg.org/lindenii/furgit/internal/intconv"
+ objectid "codeberg.org/lindenii/furgit/object/id"
+)
+
+// AllPositions iterates all commit positions in native layer order.
+func (reader *Reader) AllPositions() iter.Seq[Position] {
+ return func(yield func(Position) bool) {
+ for layerIdx := range reader.layers {
+ layer := &reader.layers[layerIdx]
+
+ graph, err := intconv.IntToUint32(layerIdx)
+ if err != nil {
+ return
+ }
+
+ for idx := range layer.numCommits {
+ if !yield(Position{Graph: graph, Index: idx}) {
+ return
+ }
+ }
+ }
+ }
+}
+
+// AllOIDs iterates all commit object IDs in native layer order.
+func (reader *Reader) AllOIDs() iter.Seq[objectid.ObjectID] {
+ return func(yield func(objectid.ObjectID) bool) {
+ positions := reader.AllPositions()
+ for pos := range positions {
+ oid, err := reader.OIDAt(pos)
+ if err != nil {
+ return
+ }
+
+ if !yield(oid) {
+ return
+ }
+ }
+ }
+}
diff --git a/format/commitgraph/read/layer.go b/format/commitgraph/read/layer.go
new file mode 100644
index 00000000..53ab1663
--- /dev/null
+++ b/format/commitgraph/read/layer.go
@@ -0,0 +1,28 @@
+package read
+
+import (
+ "os"
+
+ "codeberg.org/lindenii/furgit/format/commitgraph/bloom"
+)
+
+type layer struct {
+ path string
+ file *os.File
+ data []byte
+ numCommits uint32
+ baseCount uint32
+ globalFrom uint32
+
+ chunkOIDFanout []byte
+ chunkOIDLookup []byte
+ chunkCommit []byte
+ chunkGeneration []byte
+ chunkGenerationOv []byte
+ chunkExtraEdges []byte
+ chunkBloomIndex []byte
+ chunkBloomData []byte
+ chunkBaseGraphs []byte
+
+ bloomSettings *bloom.Settings
+}
diff --git a/format/commitgraph/read/layer_close.go b/format/commitgraph/read/layer_close.go
new file mode 100644
index 00000000..03dc91d5
--- /dev/null
+++ b/format/commitgraph/read/layer_close.go
@@ -0,0 +1,33 @@
+package read
+
+import "syscall"
+
+func closeLayers(layers []layer) {
+ for i := len(layers) - 1; i >= 0; i-- {
+ _ = layers[i].close()
+ }
+}
+
+func (layer *layer) close() error {
+ var closeErr error
+
+ if layer.data != nil {
+ err := syscall.Munmap(layer.data)
+ if err != nil {
+ closeErr = err
+ }
+
+ layer.data = nil
+ }
+
+ if layer.file != nil {
+ err := layer.file.Close()
+ if err != nil && closeErr == nil {
+ closeErr = err
+ }
+
+ layer.file = nil
+ }
+
+ return closeErr
+}
diff --git a/format/commitgraph/read/layer_lookup.go b/format/commitgraph/read/layer_lookup.go
new file mode 100644
index 00000000..84095788
--- /dev/null
+++ b/format/commitgraph/read/layer_lookup.go
@@ -0,0 +1,53 @@
+package read
+
+import (
+ "bytes"
+ "encoding/binary"
+
+ "codeberg.org/lindenii/furgit/internal/intconv"
+ objectid "codeberg.org/lindenii/furgit/object/id"
+)
+
+func layerLookup(layer *layer, oid objectid.ObjectID) (uint32, bool) {
+ hashSize := oid.Size()
+ first := int(oid.RawBytes()[0])
+
+ var lo uint32
+ if first > 0 {
+ lo = binary.BigEndian.Uint32(layer.chunkOIDFanout[(first-1)*4 : first*4])
+ }
+
+ hi := binary.BigEndian.Uint32(layer.chunkOIDFanout[first*4 : (first+1)*4])
+ if hi == 0 || lo >= hi {
+ return 0, false
+ }
+
+ target := oid.RawBytes()
+ left := int(lo)
+
+ right := int(hi) - 1
+ for left <= right {
+ mid := left + (right-left)/2
+ start := mid * hashSize
+ end := start + hashSize
+
+ current := layer.chunkOIDLookup[start:end]
+
+ cmp := bytes.Compare(current, target)
+ switch {
+ case cmp == 0:
+ pos, err := intconv.IntToUint32(mid)
+ if err != nil {
+ return 0, false
+ }
+
+ return pos, true
+ case cmp < 0:
+ left = mid + 1
+ default:
+ right = mid - 1
+ }
+ }
+
+ return 0, false
+}
diff --git a/format/commitgraph/read/layer_open.go b/format/commitgraph/read/layer_open.go
new file mode 100644
index 00000000..21a97644
--- /dev/null
+++ b/format/commitgraph/read/layer_open.go
@@ -0,0 +1,81 @@
+package read
+
+import (
+ "os"
+ "syscall"
+
+ "codeberg.org/lindenii/furgit/format/commitgraph"
+ "codeberg.org/lindenii/furgit/internal/intconv"
+ objectid "codeberg.org/lindenii/furgit/object/id"
+)
+
+func openLayer(root *os.Root, relPath string, algo objectid.Algorithm) (*layer, error) {
+ file, err := root.Open(relPath)
+ if err != nil {
+ return nil, err
+ }
+
+ info, err := file.Stat()
+ if err != nil {
+ _ = file.Close()
+
+ return nil, err
+ }
+
+ size := info.Size()
+ if size < int64(commitgraph.HeaderSize+commitgraph.FanoutSize+algo.Size()) {
+ _ = file.Close()
+
+ return nil, &MalformedError{Path: relPath, Reason: "file too short"}
+ }
+
+ mapLen, err := intconv.Int64ToUint64(size)
+ if err != nil {
+ _ = file.Close()
+
+ return nil, err
+ }
+
+ mapLenInt, err := intconv.Uint64ToInt(mapLen)
+ if err != nil {
+ _ = file.Close()
+
+ return nil, err
+ }
+
+ fd, err := intconv.UintptrToInt(file.Fd())
+ if err != nil {
+ _ = file.Close()
+
+ return nil, err
+ }
+
+ data, err := syscall.Mmap(fd, 0, mapLenInt, syscall.PROT_READ, syscall.MAP_PRIVATE)
+ if err != nil {
+ _ = file.Close()
+
+ return nil, err
+ }
+
+ out := &layer{
+ path: relPath,
+ file: file,
+ data: data,
+ }
+
+ parseErr := parseLayer(out, algo)
+ if parseErr != nil {
+ _ = out.close()
+
+ return nil, parseErr
+ }
+
+ verifyErr := verifyTrailerHash(out.data, algo, relPath)
+ if verifyErr != nil {
+ _ = out.close()
+
+ return nil, verifyErr
+ }
+
+ return out, nil
+}
diff --git a/format/commitgraph/read/layer_parse.go b/format/commitgraph/read/layer_parse.go
new file mode 100644
index 00000000..13e36c0a
--- /dev/null
+++ b/format/commitgraph/read/layer_parse.go
@@ -0,0 +1,276 @@
+package read
+
+import (
+ "encoding/binary"
+
+ "codeberg.org/lindenii/furgit/format/commitgraph"
+ "codeberg.org/lindenii/furgit/format/commitgraph/bloom"
+ "codeberg.org/lindenii/furgit/internal/intconv"
+ objectid "codeberg.org/lindenii/furgit/object/id"
+)
+
+func parseLayer(layer *layer, algo objectid.Algorithm) error { //nolint:maintidx
+ if len(layer.data) < commitgraph.HeaderSize {
+ return &MalformedError{Path: layer.path, Reason: "file too short"}
+ }
+
+ header := layer.data[:commitgraph.HeaderSize]
+
+ signature := binary.BigEndian.Uint32(header[:4])
+ if signature != commitgraph.FileSignature {
+ return &MalformedError{Path: layer.path, Reason: "invalid signature"}
+ }
+
+ version := header[4]
+ if version != commitgraph.FileVersion {
+ return &UnsupportedVersionError{Version: version}
+ }
+
+ expectedHashVersion, err := intconv.Uint32ToUint8(algo.PackHashID())
+ if err != nil {
+ return err
+ }
+
+ hashVersion := header[5]
+ if hashVersion != expectedHashVersion {
+ return &MalformedError{Path: layer.path, Reason: "hash version does not match object format"}
+ }
+
+ numChunks := int(header[6])
+ baseCount := uint32(header[7])
+
+ tocLen := (numChunks + 1) * commitgraph.ChunkEntrySize
+ tocStart := commitgraph.HeaderSize
+
+ tocEnd := tocStart + tocLen
+ if tocEnd > len(layer.data) {
+ return &MalformedError{Path: layer.path, Reason: "truncated chunk table"}
+ }
+
+ type tocEntry struct {
+ id uint32
+ offset uint64
+ }
+
+ entries := make([]tocEntry, 0, numChunks+1)
+ for i := range numChunks + 1 {
+ entryOff := tocStart + i*commitgraph.ChunkEntrySize
+ entryData := layer.data[entryOff : entryOff+commitgraph.ChunkEntrySize]
+
+ entry := tocEntry{
+ id: binary.BigEndian.Uint32(entryData[:4]),
+ offset: binary.BigEndian.Uint64(entryData[4:]),
+ }
+ entries = append(entries, entry)
+ }
+
+ if entries[len(entries)-1].id != 0 {
+ return &MalformedError{Path: layer.path, Reason: "missing chunk table terminator"}
+ }
+
+ trailerStart := len(layer.data) - algo.Size()
+
+ chunks := make(map[uint32][]byte, numChunks)
+ for i := range numChunks {
+ entry := entries[i]
+ if entry.id == 0 {
+ return &MalformedError{Path: layer.path, Reason: "early chunk table terminator"}
+ }
+
+ next := entries[i+1]
+
+ start, err := intconv.Uint64ToInt(entry.offset)
+ if err != nil {
+ return err
+ }
+
+ end, err := intconv.Uint64ToInt(next.offset)
+ if err != nil {
+ return err
+ }
+
+ if start < tocEnd || end < start || end > trailerStart {
+ return &MalformedError{Path: layer.path, Reason: "invalid chunk offsets"}
+ }
+
+ if _, exists := chunks[entry.id]; exists {
+ return &MalformedError{Path: layer.path, Reason: "duplicate chunk id"}
+ }
+
+ chunks[entry.id] = layer.data[start:end]
+ }
+
+ oidf := chunks[commitgraph.ChunkOIDF]
+ if len(oidf) != commitgraph.FanoutSize {
+ return &MalformedError{Path: layer.path, Reason: "invalid OIDF length"}
+ }
+
+ layer.chunkOIDFanout = oidf
+ layer.numCommits = binary.BigEndian.Uint32(oidf[commitgraph.FanoutSize-4:])
+
+ for i := range 255 {
+ cur := binary.BigEndian.Uint32(oidf[i*4 : (i+1)*4])
+
+ next := binary.BigEndian.Uint32(oidf[(i+1)*4 : (i+2)*4])
+ if cur > next {
+ return &MalformedError{Path: layer.path, Reason: "non-monotonic OIDF fanout"}
+ }
+ }
+
+ hashSize := algo.Size()
+
+ hashSizeU64, err := intconv.IntToUint64(hashSize)
+ if err != nil {
+ return err
+ }
+
+ oidl := chunks[commitgraph.ChunkOIDL]
+ oidlWantLen64 := uint64(layer.numCommits) * hashSizeU64
+
+ oidlWantLen, err := intconv.Uint64ToInt(oidlWantLen64)
+ if err != nil {
+ return err
+ }
+
+ if len(oidl) != oidlWantLen {
+ return &MalformedError{Path: layer.path, Reason: "invalid OIDL length"}
+ }
+
+ layer.chunkOIDLookup = oidl
+
+ stride := hashSize + 16
+
+ strideU64, err := intconv.IntToUint64(stride)
+ if err != nil {
+ return err
+ }
+
+ cdat := chunks[commitgraph.ChunkCDAT]
+ cdatWantLen64 := uint64(layer.numCommits) * strideU64
+
+ cdatWantLen, err := intconv.Uint64ToInt(cdatWantLen64)
+ if err != nil {
+ return err
+ }
+
+ if len(cdat) != cdatWantLen {
+ return &MalformedError{Path: layer.path, Reason: "invalid CDAT length"}
+ }
+
+ layer.chunkCommit = cdat
+
+ gda2 := chunks[commitgraph.ChunkGDA2]
+ if len(gda2) != 0 {
+ wantLen64 := uint64(layer.numCommits) * 4
+
+ wantLen, err := intconv.Uint64ToInt(wantLen64)
+ if err != nil {
+ return err
+ }
+
+ if len(gda2) != wantLen {
+ return &MalformedError{Path: layer.path, Reason: "invalid GDA2 length"}
+ }
+
+ layer.chunkGeneration = gda2
+ }
+
+ gdo2 := chunks[commitgraph.ChunkGDO2]
+ if len(gdo2) != 0 {
+ if len(gdo2)%8 != 0 {
+ return &MalformedError{Path: layer.path, Reason: "invalid GDO2 length"}
+ }
+
+ layer.chunkGenerationOv = gdo2
+ }
+
+ edge := chunks[commitgraph.ChunkEDGE]
+ if len(edge) != 0 {
+ if len(edge)%4 != 0 {
+ return &MalformedError{Path: layer.path, Reason: "invalid EDGE length"}
+ }
+
+ layer.chunkExtraEdges = edge
+ }
+
+ base := chunks[commitgraph.ChunkBASE]
+ if baseCount == 0 {
+ if len(base) != 0 {
+ return &MalformedError{Path: layer.path, Reason: "unexpected BASE chunk"}
+ }
+ } else {
+ wantLen64 := uint64(baseCount) * hashSizeU64
+
+ wantLen, err := intconv.Uint64ToInt(wantLen64)
+ if err != nil {
+ return err
+ }
+
+ if len(base) != wantLen {
+ return &MalformedError{Path: layer.path, Reason: "invalid BASE length"}
+ }
+
+ layer.chunkBaseGraphs = base
+ }
+
+ layer.baseCount = baseCount
+
+ bidx := chunks[commitgraph.ChunkBIDX]
+
+ bdat := chunks[commitgraph.ChunkBDAT]
+ if len(bidx) != 0 || len(bdat) != 0 { //nolint:nestif
+ if len(bidx) == 0 || len(bdat) == 0 {
+ return &MalformedError{Path: layer.path, Reason: "BIDX/BDAT must both be present"}
+ }
+
+ bidxWantLen64 := uint64(layer.numCommits) * 4
+
+ bidxWantLen, err := intconv.Uint64ToInt(bidxWantLen64)
+ if err != nil {
+ return err
+ }
+
+ if len(bidx) != bidxWantLen {
+ return &MalformedError{Path: layer.path, Reason: "invalid BIDX length"}
+ }
+
+ if len(bdat) < bloom.DataHeaderSize {
+ return &MalformedError{Path: layer.path, Reason: "invalid BDAT length"}
+ }
+
+ settings, err := bloom.ParseSettings(bdat)
+ if err != nil {
+ return err
+ }
+
+ prev := uint32(0)
+
+ for i := range layer.numCommits {
+ off := int(i) * 4
+
+ cur := binary.BigEndian.Uint32(bidx[off : off+4])
+ if i > 0 && cur < prev {
+ return &MalformedError{Path: layer.path, Reason: "non-monotonic BIDX"}
+ }
+
+ bdatDataLen := len(bdat) - bloom.DataHeaderSize
+
+ bdatDataLenU32, err := intconv.IntToUint32(bdatDataLen)
+ if err != nil {
+ return err
+ }
+
+ if cur > bdatDataLenU32 {
+ return &MalformedError{Path: layer.path, Reason: "BIDX offset out of range"}
+ }
+
+ prev = cur
+ }
+
+ layer.chunkBloomIndex = bidx
+ layer.chunkBloomData = bdat
+ layer.bloomSettings = settings
+ }
+
+ return nil
+}
diff --git a/format/commitgraph/read/layer_pos.go b/format/commitgraph/read/layer_pos.go
new file mode 100644
index 00000000..7b87b381
--- /dev/null
+++ b/format/commitgraph/read/layer_pos.go
@@ -0,0 +1,21 @@
+package read
+
+import "codeberg.org/lindenii/furgit/internal/intconv"
+
+func (reader *Reader) layerByPosition(pos Position) (*layer, error) {
+ graphIdx, err := intconv.Uint64ToInt(uint64(pos.Graph))
+ if err != nil {
+ return nil, err
+ }
+
+ if graphIdx < 0 || graphIdx >= len(reader.layers) {
+ return nil, &PositionOutOfRangeError{Pos: pos}
+ }
+
+ layer := &reader.layers[graphIdx]
+ if pos.Index >= layer.numCommits {
+ return nil, &PositionOutOfRangeError{Pos: pos}
+ }
+
+ return layer, nil
+}
diff --git a/format/commitgraph/read/layerinfo.go b/format/commitgraph/read/layerinfo.go
new file mode 100644
index 00000000..83c4407d
--- /dev/null
+++ b/format/commitgraph/read/layerinfo.go
@@ -0,0 +1,23 @@
+package read
+
+// LayerInfo describes one loaded commit-graph layer.
+type LayerInfo struct {
+ Path string
+ BaseCount uint32
+ Commits uint32
+}
+
+// Layers returns loaded layer metadata in native chain order.
+func (reader *Reader) Layers() []LayerInfo {
+ out := make([]LayerInfo, 0, len(reader.layers))
+ for i := range reader.layers {
+ layer := reader.layers[i]
+ out = append(out, LayerInfo{
+ Path: layer.path,
+ BaseCount: layer.baseCount,
+ Commits: layer.numCommits,
+ })
+ }
+
+ return out
+}
diff --git a/format/commitgraph/read/lookup.go b/format/commitgraph/read/lookup.go
new file mode 100644
index 00000000..5f1b08f6
--- /dev/null
+++ b/format/commitgraph/read/lookup.go
@@ -0,0 +1,29 @@
+package read
+
+import (
+ "codeberg.org/lindenii/furgit/internal/intconv"
+ objectid "codeberg.org/lindenii/furgit/object/id"
+)
+
+// Lookup resolves one object ID to one graph position.
+func (reader *Reader) Lookup(oid objectid.ObjectID) (Position, error) {
+ if oid.Algorithm() != reader.algo {
+ return Position{}, &NotFoundError{OID: oid}
+ }
+
+ for layerIdx := len(reader.layers) - 1; layerIdx >= 0; layerIdx-- {
+ layer := &reader.layers[layerIdx]
+
+ found, ok := layerLookup(layer, oid)
+ if ok {
+ idxU32, err := intconv.IntToUint32(layerIdx)
+ if err != nil {
+ return Position{}, err
+ }
+
+ return Position{Graph: idxU32, Index: found}, nil
+ }
+ }
+
+ return Position{}, &NotFoundError{OID: oid}
+}
diff --git a/format/commitgraph/read/mode.go b/format/commitgraph/read/mode.go
new file mode 100644
index 00000000..76afa21f
--- /dev/null
+++ b/format/commitgraph/read/mode.go
@@ -0,0 +1,11 @@
+package read
+
+// OpenMode controls which commit-graph layout Open loads.
+type OpenMode uint8
+
+const (
+ // OpenSingle opens one commit-graph file at info/commit-graph.
+ OpenSingle OpenMode = iota
+ // OpenChain opens chained commit-graphs from info/commit-graphs.
+ OpenChain
+)
diff --git a/format/commitgraph/read/oidat.go b/format/commitgraph/read/oidat.go
new file mode 100644
index 00000000..908cbc1c
--- /dev/null
+++ b/format/commitgraph/read/oidat.go
@@ -0,0 +1,36 @@
+package read
+
+import (
+ "codeberg.org/lindenii/furgit/internal/intconv"
+ objectid "codeberg.org/lindenii/furgit/object/id"
+)
+
+// OIDAt returns object ID at one position.
+func (reader *Reader) OIDAt(pos Position) (objectid.ObjectID, error) {
+ layer, err := reader.layerByPosition(pos)
+ if err != nil {
+ return objectid.ObjectID{}, err
+ }
+
+ hashSize := reader.algo.Size()
+
+ hashSizeU64, err := intconv.IntToUint64(hashSize)
+ if err != nil {
+ return objectid.ObjectID{}, err
+ }
+
+ start64 := uint64(pos.Index) * hashSizeU64
+ end64 := start64 + hashSizeU64
+
+ start, err := intconv.Uint64ToInt(start64)
+ if err != nil {
+ return objectid.ObjectID{}, err
+ }
+
+ end, err := intconv.Uint64ToInt(end64)
+ if err != nil {
+ return objectid.ObjectID{}, err
+ }
+
+ return objectid.FromBytes(reader.algo, layer.chunkOIDLookup[start:end])
+}
diff --git a/format/commitgraph/read/open.go b/format/commitgraph/read/open.go
new file mode 100644
index 00000000..9c708b49
--- /dev/null
+++ b/format/commitgraph/read/open.go
@@ -0,0 +1,26 @@
+package read
+
+import (
+ "fmt"
+ "os"
+
+ objectid "codeberg.org/lindenii/furgit/object/id"
+)
+
+// Open opens commit-graph data from one objects root.
+//
+// Open borrows root during construction and does not close it.
+func Open(root *os.Root, algo objectid.Algorithm, mode OpenMode) (*Reader, error) {
+ if algo.Size() == 0 {
+ return nil, objectid.ErrInvalidAlgorithm
+ }
+
+ switch mode {
+ case OpenSingle:
+ return openSingle(root, algo)
+ case OpenChain:
+ return openChain(root, algo)
+ default:
+ return nil, fmt.Errorf("commitgraph: invalid open mode %d", mode)
+ }
+}
diff --git a/format/commitgraph/read/open_chain.go b/format/commitgraph/read/open_chain.go
new file mode 100644
index 00000000..b55f3e57
--- /dev/null
+++ b/format/commitgraph/read/open_chain.go
@@ -0,0 +1,133 @@
+package read
+
+import (
+ "bufio"
+ "errors"
+ "fmt"
+ "os"
+ "strings"
+
+ "codeberg.org/lindenii/furgit/internal/intconv"
+ objectid "codeberg.org/lindenii/furgit/object/id"
+)
+
+func openChain(root *os.Root, algo objectid.Algorithm) (*Reader, error) {
+ chainPath := "info/commit-graphs/commit-graph-chain"
+
+ file, err := root.Open(chainPath)
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ return nil, &MalformedError{Path: chainPath, Reason: "missing commit-graph-chain"}
+ }
+
+ return nil, err
+ }
+
+ scanner := bufio.NewScanner(file)
+ hashes := make([]string, 0)
+
+ for scanner.Scan() {
+ line := strings.TrimSpace(scanner.Text())
+ if line == "" {
+ continue
+ }
+
+ hashes = append(hashes, line)
+ }
+
+ scanErr := scanner.Err()
+ closeErr := file.Close()
+
+ if scanErr != nil {
+ return nil, scanErr
+ }
+
+ if closeErr != nil {
+ return nil, closeErr
+ }
+
+ if len(hashes) == 0 {
+ return nil, &MalformedError{Path: chainPath, Reason: "empty chain"}
+ }
+
+ layers := make([]layer, 0, len(hashes))
+
+ var total uint32
+
+ hashVersion, err := intconv.Uint32ToUint8(algo.PackHashID())
+ if err != nil {
+ return nil, err
+ }
+
+ for i, hashHex := range hashes {
+ expectedBaseCount, err := intconv.IntToUint32(i)
+ if err != nil {
+ closeLayers(layers)
+
+ return nil, err
+ }
+
+ if len(hashHex) != algo.HexLen() {
+ closeLayers(layers)
+
+ return nil, &MalformedError{
+ Path: chainPath,
+ Reason: fmt.Sprintf("invalid graph hash length at line %d", i+1),
+ }
+ }
+
+ relPath := fmt.Sprintf("info/commit-graphs/graph-%s.graph", hashHex)
+
+ loaded, loadErr := openLayer(root, relPath, algo)
+ if loadErr != nil {
+ closeLayers(layers)
+
+ return nil, loadErr
+ }
+
+ if loaded.baseCount != expectedBaseCount {
+ _ = loaded.close()
+
+ closeLayers(layers)
+
+ return nil, &MalformedError{
+ Path: relPath,
+ Reason: fmt.Sprintf("BASE count %d does not match chain depth %d", loaded.baseCount, i),
+ }
+ }
+
+ validateErr := validateChainBaseHashes(algo, hashes, i, loaded)
+ if validateErr != nil {
+ _ = loaded.close()
+
+ closeLayers(layers)
+
+ return nil, validateErr
+ }
+
+ loaded.globalFrom = total
+ loaded.baseCount = expectedBaseCount
+
+ totalNext := total + loaded.numCommits
+ if totalNext < total {
+ _ = loaded.close()
+
+ closeLayers(layers)
+
+ return nil, &MalformedError{Path: relPath, Reason: "total commit count overflow"}
+ }
+
+ total = totalNext
+
+ layers = append(layers, *loaded)
+ }
+
+ out := &Reader{
+ algo: algo,
+ hashVersion: hashVersion,
+ layers: layers,
+ total: total,
+ }
+
+ return out, nil
+}
diff --git a/format/commitgraph/read/open_single.go b/format/commitgraph/read/open_single.go
new file mode 100644
index 00000000..9ad6607f
--- /dev/null
+++ b/format/commitgraph/read/open_single.go
@@ -0,0 +1,32 @@
+package read
+
+import (
+ "os"
+
+ "codeberg.org/lindenii/furgit/internal/intconv"
+ objectid "codeberg.org/lindenii/furgit/object/id"
+)
+
+func openSingle(root *os.Root, algo objectid.Algorithm) (*Reader, error) {
+ graph, err := openLayer(root, "info/commit-graph", algo)
+ if err != nil {
+ return nil, err
+ }
+
+ graph.baseCount = 0
+ graph.globalFrom = 0
+
+ hashVersion, err := intconv.Uint32ToUint8(algo.PackHashID())
+ if err != nil {
+ return nil, err
+ }
+
+ out := &Reader{
+ algo: algo,
+ hashVersion: hashVersion,
+ layers: []layer{*graph},
+ total: graph.numCommits,
+ }
+
+ return out, nil
+}
diff --git a/format/commitgraph/read/parents.go b/format/commitgraph/read/parents.go
new file mode 100644
index 00000000..fcaad8b6
--- /dev/null
+++ b/format/commitgraph/read/parents.go
@@ -0,0 +1,67 @@
+package read
+
+import "codeberg.org/lindenii/furgit/format/commitgraph"
+
+// ParentRef references one parent position.
+type ParentRef struct {
+ Valid bool
+ Pos Position
+}
+
+func (reader *Reader) decodeParents(layer *layer, p1, p2 uint32) (ParentRef, ParentRef, []Position, error) {
+ parent1, err := reader.decodeSingleParent(p1)
+ if err != nil {
+ return ParentRef{}, ParentRef{}, nil, err
+ }
+
+ if p2 == commitgraph.ParentNone {
+ return parent1, ParentRef{}, nil, nil
+ }
+
+ if p2&commitgraph.ParentExtraMask == 0 {
+ parent2, err := reader.decodeSingleParent(p2)
+ if err != nil {
+ return ParentRef{}, ParentRef{}, nil, err
+ }
+
+ return parent1, parent2, nil, nil
+ }
+
+ edgeStart := p2 & commitgraph.ParentLastMask
+
+ parents, err := reader.decodeExtraEdgeList(layer, edgeStart)
+ if err != nil {
+ return ParentRef{}, ParentRef{}, nil, err
+ }
+
+ if len(parents) == 0 {
+ return ParentRef{}, ParentRef{}, nil, &MalformedError{Path: layer.path, Reason: "empty EDGE list"}
+ }
+
+ parent2 := ParentRef{Valid: true, Pos: parents[0]}
+ if len(parents) == 1 {
+ return parent1, parent2, nil, nil
+ }
+
+ return parent1, parent2, parents[1:], nil
+}
+
+func (reader *Reader) decodeSingleParent(raw uint32) (ParentRef, error) {
+ if raw == commitgraph.ParentNone {
+ return ParentRef{}, nil
+ }
+
+ if raw&commitgraph.ParentExtraMask != 0 {
+ return ParentRef{}, &MalformedError{
+ Path: "commit-graph",
+ Reason: "unexpected EDGE marker in single-parent slot",
+ }
+ }
+
+ pos, err := reader.globalToPosition(raw)
+ if err != nil {
+ return ParentRef{}, err
+ }
+
+ return ParentRef{Valid: true, Pos: pos}, nil
+}
diff --git a/format/commitgraph/read/position.go b/format/commitgraph/read/position.go
new file mode 100644
index 00000000..b2e1138b
--- /dev/null
+++ b/format/commitgraph/read/position.go
@@ -0,0 +1,38 @@
+package read
+
+import (
+ "fmt"
+
+ "codeberg.org/lindenii/furgit/internal/intconv"
+)
+
+// Position identifies one commit record by layer and row index.
+type Position struct {
+ Graph uint32
+ Index uint32
+}
+
+func (reader *Reader) globalToPosition(global uint32) (Position, error) {
+ for i := range reader.layers {
+ layer := &reader.layers[i]
+ from := layer.globalFrom
+
+ to := from + layer.numCommits
+ if global >= from && global < to {
+ graph, err := intconv.IntToUint32(i)
+ if err != nil {
+ return Position{}, err
+ }
+
+ return Position{
+ Graph: graph,
+ Index: global - from,
+ }, nil
+ }
+ }
+
+ return Position{}, &MalformedError{
+ Path: "commit-graph",
+ Reason: fmt.Sprintf("parent global position out of range: %d", global),
+ }
+}
diff --git a/format/commitgraph/read/read_test.go b/format/commitgraph/read/read_test.go
new file mode 100644
index 00000000..c65b183e
--- /dev/null
+++ b/format/commitgraph/read/read_test.go
@@ -0,0 +1,322 @@
+package read_test
+
+import (
+ "errors"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "testing"
+
+ "codeberg.org/lindenii/furgit/format/commitgraph/bloom"
+ "codeberg.org/lindenii/furgit/format/commitgraph/read"
+ "codeberg.org/lindenii/furgit/internal/intconv"
+ "codeberg.org/lindenii/furgit/internal/testgit"
+ objectid "codeberg.org/lindenii/furgit/object/id"
+)
+
+func fixtureRepoPath(t *testing.T, algo objectid.Algorithm, name string) string {
+ t.Helper()
+
+ return filepath.Join("testdata", "fixtures", algo.String(), name, "repo.git")
+}
+
+func fixtureRepo(t *testing.T, algo objectid.Algorithm, name string) *testgit.TestRepo {
+ t.Helper()
+
+ return testgit.NewRepoFromFixture(t, algo, fixtureRepoPath(t, algo, name))
+}
+
+func TestReadSingleMatchesGit(t *testing.T) {
+ t.Parallel()
+
+ testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
+ testRepo := fixtureRepo(t, algo, "single_changed")
+
+ reader := openReader(t, testRepo, read.OpenSingle)
+
+ defer func() { _ = reader.Close() }()
+
+ allIDs := testRepo.RevList(t, "--all")
+ if len(allIDs) == 0 {
+ t.Fatal("git rev-list --all returned no commits")
+ }
+
+ wantCommitCount, err := intconv.IntToUint32(len(allIDs))
+ if err != nil {
+ t.Fatalf("len(allIDs) convert: %v", err)
+ }
+
+ if got := reader.NumCommits(); got != wantCommitCount {
+ t.Fatalf("NumCommits() = %d, want %d", got, len(allIDs))
+ }
+
+ if !reader.HasBloom() {
+ t.Fatal("HasBloom() = false, want true")
+ }
+
+ bloomVersion := reader.BloomVersion()
+ if bloomVersion == 0 {
+ t.Fatal("BloomVersion() = 0, want non-zero when HasBloom() is true")
+ }
+
+ for _, id := range allIDs {
+ pos, err := reader.Lookup(id)
+ if err != nil {
+ t.Fatalf("Lookup(%s): %v", id, err)
+ }
+
+ gotID, err := reader.OIDAt(pos)
+ if err != nil {
+ t.Fatalf("OIDAt(%+v): %v", pos, err)
+ }
+
+ if gotID != id {
+ t.Fatalf("OIDAt(Lookup(%s)) = %s, want %s", id, gotID, id)
+ }
+ }
+
+ step := max(len(allIDs)/24, 1)
+
+ for i, id := range allIDs {
+ if i%step != 0 && i != len(allIDs)-1 {
+ continue
+ }
+
+ verifyCommitAgainstGit(t, testRepo, reader, id)
+ }
+ })
+}
+
+func TestReadChainMatchesGit(t *testing.T) {
+ t.Parallel()
+
+ testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
+ testRepo := fixtureRepo(t, algo, "chain_changed")
+
+ reader := openReader(t, testRepo, read.OpenChain)
+
+ defer func() { _ = reader.Close() }()
+
+ layers := reader.Layers()
+ if len(layers) < 2 {
+ t.Fatalf("Layers len = %d, want >= 2", len(layers))
+ }
+
+ allIDs := testRepo.RevList(t, "--all")
+
+ wantCommitCount, err := intconv.IntToUint32(len(allIDs))
+ if err != nil {
+ t.Fatalf("len(allIDs) convert: %v", err)
+ }
+
+ if got := reader.NumCommits(); got != wantCommitCount {
+ t.Fatalf("NumCommits() = %d, want %d", got, len(allIDs))
+ }
+
+ step := max(len(allIDs)/20, 1)
+
+ for i, id := range allIDs {
+ pos, err := reader.Lookup(id)
+ if err != nil {
+ t.Fatalf("Lookup(%s): %v", id, err)
+ }
+
+ if i%step != 0 && i != len(allIDs)-1 {
+ continue
+ }
+
+ gotID, err := reader.OIDAt(pos)
+ if err != nil {
+ t.Fatalf("OIDAt(%+v): %v", pos, err)
+ }
+
+ if gotID != id {
+ t.Fatalf("OIDAt(Lookup(%s)) = %s, want %s", id, gotID, id)
+ }
+ }
+ })
+}
+
+func TestBloomUnavailableWithoutChangedPaths(t *testing.T) {
+ t.Parallel()
+
+ testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
+ testRepo := fixtureRepo(t, algo, "single_nochanged")
+
+ reader := openReader(t, testRepo, read.OpenSingle)
+
+ defer func() { _ = reader.Close() }()
+
+ head := testRepo.RevParse(t, "HEAD")
+
+ pos, err := reader.Lookup(head)
+ if err != nil {
+ t.Fatalf("Lookup(%s): %v", head, err)
+ }
+
+ _, err = reader.BloomFilterAt(pos)
+ if err == nil {
+ t.Fatal("BloomFilterAt() error = nil, want BloomUnavailableError")
+ }
+
+ unavailable, ok := errors.AsType[*read.BloomUnavailableError](err)
+ if !ok {
+ t.Fatalf("BloomFilterAt() error type = %T, want *BloomUnavailableError", err)
+ }
+
+ if unavailable.Pos != pos {
+ t.Fatalf("BloomUnavailableError.Pos = %+v, want %+v", unavailable.Pos, pos)
+ }
+ })
+}
+
+func openReader(tb testing.TB, testRepo *testgit.TestRepo, mode read.OpenMode) *read.Reader {
+ tb.Helper()
+
+ root := testRepo.OpenObjectsRoot(tb)
+
+ reader, err := read.Open(root, testRepo.Algorithm(), mode)
+ if err != nil {
+ tb.Fatalf("read.Open(objects): %v", err)
+ }
+
+ return reader
+}
+
+func verifyCommitAgainstGit(tb testing.TB, testRepo *testgit.TestRepo, reader *read.Reader, id objectid.ObjectID) {
+ tb.Helper()
+
+ pos, err := reader.Lookup(id)
+ if err != nil {
+ tb.Fatalf("Lookup(%s): %v", id, err)
+ }
+
+ commit, err := reader.CommitAt(pos)
+ if err != nil {
+ tb.Fatalf("CommitAt(%+v): %v", pos, err)
+ }
+
+ if commit.OID != id {
+ tb.Fatalf("CommitAt(%+v).OID = %s, want %s", pos, commit.OID, id)
+ }
+
+ treeHex := testRepo.Run(tb, "show", "-s", "--format=%T", id.String())
+
+ wantTree, err := objectid.ParseHex(testRepo.Algorithm(), treeHex)
+ if err != nil {
+ tb.Fatalf("parse tree id %q: %v", treeHex, err)
+ }
+
+ if commit.TreeOID != wantTree {
+ tb.Fatalf("CommitAt(%+v).TreeOID = %s, want %s", pos, commit.TreeOID, wantTree)
+ }
+
+ wantParents := parseOIDLine(tb, testRepo.Algorithm(), testRepo.Run(tb, "show", "-s", "--format=%P", id.String()))
+
+ gotParents := commitParents(tb, reader, commit)
+ if len(gotParents) != len(wantParents) {
+ tb.Fatalf("parent count for %s = %d, want %d", id, len(gotParents), len(wantParents))
+ }
+
+ for i := range gotParents {
+ if gotParents[i] != wantParents[i] {
+ tb.Fatalf("parent %d for %s = %s, want %s", i, id, gotParents[i], wantParents[i])
+ }
+ }
+
+ commitTimeRaw := testRepo.Run(tb, "show", "-s", "--format=%ct", id.String())
+
+ wantCommitTime, err := strconv.ParseInt(strings.TrimSpace(commitTimeRaw), 10, 64)
+ if err != nil {
+ tb.Fatalf("parse commit time %q: %v", commitTimeRaw, err)
+ }
+
+ if commit.CommitTimeUnix != wantCommitTime {
+ tb.Fatalf("CommitAt(%+v).CommitTimeUnix = %d, want %d", pos, commit.CommitTimeUnix, wantCommitTime)
+ }
+
+ filter, err := reader.BloomFilterAt(pos)
+ if err != nil {
+ tb.Fatalf("BloomFilterAt(%+v): %v", pos, err)
+ }
+
+ if filter.HashVersion != uint32(reader.BloomVersion()) {
+ tb.Fatalf("filter.HashVersion = %d, want %d", filter.HashVersion, reader.BloomVersion())
+ }
+
+ assertChangedPathsBloomPositive(tb, testRepo, filter, id)
+}
+
+func commitParents(tb testing.TB, reader *read.Reader, commit read.Commit) []objectid.ObjectID {
+ tb.Helper()
+
+ out := make([]objectid.ObjectID, 0, 2+len(commit.ExtraParents))
+
+ if commit.Parent1.Valid {
+ id, err := reader.OIDAt(commit.Parent1.Pos)
+ if err != nil {
+ tb.Fatalf("OIDAt(parent1 %+v): %v", commit.Parent1.Pos, err)
+ }
+
+ out = append(out, id)
+ }
+
+ if commit.Parent2.Valid {
+ id, err := reader.OIDAt(commit.Parent2.Pos)
+ if err != nil {
+ tb.Fatalf("OIDAt(parent2 %+v): %v", commit.Parent2.Pos, err)
+ }
+
+ out = append(out, id)
+ }
+
+ for _, parentPos := range commit.ExtraParents {
+ id, err := reader.OIDAt(parentPos)
+ if err != nil {
+ tb.Fatalf("OIDAt(extra parent %+v): %v", parentPos, err)
+ }
+
+ out = append(out, id)
+ }
+
+ return out
+}
+
+func assertChangedPathsBloomPositive(tb testing.TB, testRepo *testgit.TestRepo, filter bloom.Filter, commitID objectid.ObjectID) {
+ tb.Helper()
+
+ changedPaths := testRepo.Run(tb, "diff-tree", "--no-commit-id", "--name-only", "-r", "--root", commitID.String())
+ for line := range strings.SplitSeq(strings.TrimSpace(changedPaths), "\n") {
+ path := strings.TrimSpace(line)
+ if path == "" {
+ continue
+ }
+
+ mightContain, err := filter.MightContain([]byte(path))
+ if err != nil {
+ tb.Fatalf("MightContain(%q): %v", path, err)
+ }
+
+ if !mightContain {
+ tb.Fatalf("Bloom filter false negative for commit %s path %q", commitID, path)
+ }
+ }
+}
+
+func parseOIDLine(tb testing.TB, algo objectid.Algorithm, line string) []objectid.ObjectID {
+ tb.Helper()
+
+ toks := strings.Fields(line)
+
+ out := make([]objectid.ObjectID, 0, len(toks))
+ for _, tok := range toks {
+ id, err := objectid.ParseHex(algo, tok)
+ if err != nil {
+ tb.Fatalf("parse object id %q: %v", tok, err)
+ }
+
+ out = append(out, id)
+ }
+
+ return out
+}
diff --git a/format/commitgraph/read/reader.go b/format/commitgraph/read/reader.go
new file mode 100644
index 00000000..d5c84a70
--- /dev/null
+++ b/format/commitgraph/read/reader.go
@@ -0,0 +1,16 @@
+package read
+
+import objectid "codeberg.org/lindenii/furgit/object/id"
+
+// Reader provides read-only access to one mmap-backed commit-graph snapshot.
+//
+// It is safe for concurrent read-only queries.
+// Values returned by Reader methods are only valid until the reader is closed
+// when explicitly documented on that method.
+type Reader struct {
+ algo objectid.Algorithm
+ hashVersion uint8
+
+ layers []layer
+ total uint32
+}
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/HEAD b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/HEAD
new file mode 100644
index 00000000..cb089cd8
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/config b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/config
new file mode 100644
index 00000000..07d359d0
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/config
@@ -0,0 +1,4 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/commit-graph-chain b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/commit-graph-chain
new file mode 100644
index 00000000..74c46b64
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/commit-graph-chain
@@ -0,0 +1,2 @@
+dd7578d5216ca76c25b19631ba90f7498aeabbe7
+bf985c21612a52070d8b008e6ef51edf8b609401
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/graph-bf985c21612a52070d8b008e6ef51edf8b609401.graph b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/graph-bf985c21612a52070d8b008e6ef51edf8b609401.graph
new file mode 100644
index 00000000..c31869c1
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/graph-bf985c21612a52070d8b008e6ef51edf8b609401.graph
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/graph-dd7578d5216ca76c25b19631ba90f7498aeabbe7.graph b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/graph-dd7578d5216ca76c25b19631ba90f7498aeabbe7.graph
new file mode 100644
index 00000000..241eb3cc
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/commit-graphs/graph-dd7578d5216ca76c25b19631ba90f7498aeabbe7.graph
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/packs b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/packs
new file mode 100644
index 00000000..61decf9b
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/info/packs
@@ -0,0 +1,2 @@
+P pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.pack
+
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.bitmap b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.bitmap
new file mode 100644
index 00000000..1508cf18
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.bitmap
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.idx b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.idx
new file mode 100644
index 00000000..00ee2646
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.idx
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.pack b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.pack
new file mode 100644
index 00000000..c65ae27f
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.pack
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.rev b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.rev
new file mode 100644
index 00000000..d0689f72
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/objects/pack/pack-15b064d6a8ef8cff520565f6db8c006b2e6f7f2f.rev
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/refs/heads/master b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/refs/heads/master
new file mode 100644
index 00000000..8942d437
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/chain_changed/repo.git/refs/heads/master
@@ -0,0 +1 @@
+46ca641fd65e566b8ecfa567a1f01766289192f8
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/HEAD b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/HEAD
new file mode 100644
index 00000000..b870d826
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/main
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/config b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/config
new file mode 100644
index 00000000..07d359d0
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/config
@@ -0,0 +1,4 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/info/commit-graph b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/info/commit-graph
new file mode 100644
index 00000000..56b59a54
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/info/commit-graph
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/info/packs b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/info/packs
new file mode 100644
index 00000000..ecf5d272
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/info/packs
@@ -0,0 +1,2 @@
+P pack-34e9e132566989e2abfe8821731236c77f9bcbe9.pack
+
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.bitmap b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.bitmap
new file mode 100644
index 00000000..9fec7b16
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.bitmap
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.idx b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.idx
new file mode 100644
index 00000000..e30cbb5a
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.idx
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.pack b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.pack
new file mode 100644
index 00000000..8da45eab
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.pack
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.rev b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.rev
new file mode 100644
index 00000000..3bcd2e2c
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/objects/pack/pack-34e9e132566989e2abfe8821731236c77f9bcbe9.rev
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/refs/heads/main b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/refs/heads/main
new file mode 100644
index 00000000..090ca933
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/single_changed/repo.git/refs/heads/main
@@ -0,0 +1 @@
+d02a8dbd1a8fbaac8ab7f7f1533cc312ab2c9eec
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/HEAD b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/HEAD
new file mode 100644
index 00000000..cb089cd8
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/config b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/config
new file mode 100644
index 00000000..07d359d0
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/config
@@ -0,0 +1,4 @@
+[core]
+ repositoryformatversion = 0
+ filemode = true
+ bare = true
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/info/commit-graph b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/info/commit-graph
new file mode 100644
index 00000000..28f7d06a
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/info/commit-graph
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/info/packs b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/info/packs
new file mode 100644
index 00000000..8434a002
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/info/packs
@@ -0,0 +1,2 @@
+P pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.pack
+
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.bitmap b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.bitmap
new file mode 100644
index 00000000..64a36c71
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.bitmap
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.idx b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.idx
new file mode 100644
index 00000000..f5e16674
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.idx
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.pack b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.pack
new file mode 100644
index 00000000..8f82b451
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.pack
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.rev b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.rev
new file mode 100644
index 00000000..64771f70
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/objects/pack/pack-a3da595034c94bb16b6829d757a66b7d259b9ffc.rev
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/refs/heads/master b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/refs/heads/master
new file mode 100644
index 00000000..475cb2c1
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha1/single_nochanged/repo.git/refs/heads/master
@@ -0,0 +1 @@
+dda8217252bdf3e01fdf31309d0e5c3051b00945
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/HEAD b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/HEAD
new file mode 100644
index 00000000..cb089cd8
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/config b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/config
new file mode 100644
index 00000000..7d1c0006
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/config
@@ -0,0 +1,6 @@
+[extensions]
+ objectformat = sha256
+[core]
+ repositoryformatversion = 1
+ filemode = true
+ bare = true
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/commit-graph-chain b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/commit-graph-chain
new file mode 100644
index 00000000..4e7d76fe
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/commit-graph-chain
@@ -0,0 +1,2 @@
+505cab61f8ddfa614301e8f97943112739236c6bcd19ed4d1f7c6b830cab4f62
+77c47bd6ca2ce17208c9361717a5823c0cb4b5ee336a14959678e060d674ffb6
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/graph-505cab61f8ddfa614301e8f97943112739236c6bcd19ed4d1f7c6b830cab4f62.graph b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/graph-505cab61f8ddfa614301e8f97943112739236c6bcd19ed4d1f7c6b830cab4f62.graph
new file mode 100644
index 00000000..4a93de94
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/graph-505cab61f8ddfa614301e8f97943112739236c6bcd19ed4d1f7c6b830cab4f62.graph
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/graph-77c47bd6ca2ce17208c9361717a5823c0cb4b5ee336a14959678e060d674ffb6.graph b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/graph-77c47bd6ca2ce17208c9361717a5823c0cb4b5ee336a14959678e060d674ffb6.graph
new file mode 100644
index 00000000..7807351d
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/commit-graphs/graph-77c47bd6ca2ce17208c9361717a5823c0cb4b5ee336a14959678e060d674ffb6.graph
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/packs b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/packs
new file mode 100644
index 00000000..3b1241c4
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/info/packs
@@ -0,0 +1,2 @@
+P pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.pack
+
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.bitmap b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.bitmap
new file mode 100644
index 00000000..007fcd0e
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.bitmap
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.idx b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.idx
new file mode 100644
index 00000000..248cf8fc
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.idx
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.pack b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.pack
new file mode 100644
index 00000000..92cea7fb
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.pack
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.rev b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.rev
new file mode 100644
index 00000000..569862ce
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/objects/pack/pack-04168d0884c910f505cb9fbcf045957e44ccee06d812b5e531ae666014a26ed1.rev
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/refs/heads/master b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/refs/heads/master
new file mode 100644
index 00000000..29d83be8
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/chain_changed/repo.git/refs/heads/master
@@ -0,0 +1 @@
+10d2943dc7ad88011cae3b776d9565d6451a350ce1d16949bc8546a5fe6c0a53
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/HEAD b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/HEAD
new file mode 100644
index 00000000..b870d826
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/main
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/config b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/config
new file mode 100644
index 00000000..7d1c0006
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/config
@@ -0,0 +1,6 @@
+[extensions]
+ objectformat = sha256
+[core]
+ repositoryformatversion = 1
+ filemode = true
+ bare = true
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/info/commit-graph b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/info/commit-graph
new file mode 100644
index 00000000..f4dd0e0c
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/info/commit-graph
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/info/packs b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/info/packs
new file mode 100644
index 00000000..0f39ed89
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/info/packs
@@ -0,0 +1,2 @@
+P pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.pack
+
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.bitmap b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.bitmap
new file mode 100644
index 00000000..b5c5055c
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.bitmap
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.idx b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.idx
new file mode 100644
index 00000000..144778cd
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.idx
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.pack b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.pack
new file mode 100644
index 00000000..599ccae0
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.pack
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.rev b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.rev
new file mode 100644
index 00000000..3c093f31
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/objects/pack/pack-316dbc67dac12d131591640da0c55b76387cbf1fd2a117ab3d7ca0d854a031c9.rev
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/refs/heads/main b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/refs/heads/main
new file mode 100644
index 00000000..4ba32358
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/single_changed/repo.git/refs/heads/main
@@ -0,0 +1 @@
+a9ff114900e6be139ec66a2a61c930973d8c4bc6fd3b899405ee7ab8740bdbd3
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/HEAD b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/HEAD
new file mode 100644
index 00000000..cb089cd8
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/HEAD
@@ -0,0 +1 @@
+ref: refs/heads/master
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/config b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/config
new file mode 100644
index 00000000..7d1c0006
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/config
@@ -0,0 +1,6 @@
+[extensions]
+ objectformat = sha256
+[core]
+ repositoryformatversion = 1
+ filemode = true
+ bare = true
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/info/commit-graph b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/info/commit-graph
new file mode 100644
index 00000000..f98ca4a1
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/info/commit-graph
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/info/packs b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/info/packs
new file mode 100644
index 00000000..65184c9a
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/info/packs
@@ -0,0 +1,2 @@
+P pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.pack
+
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.bitmap b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.bitmap
new file mode 100644
index 00000000..53530f4c
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.bitmap
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.idx b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.idx
new file mode 100644
index 00000000..b3a417a8
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.idx
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.pack b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.pack
new file mode 100644
index 00000000..d8dcedbf
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.pack
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.rev b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.rev
new file mode 100644
index 00000000..e50d1a81
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/objects/pack/pack-d335453f760b064e36459d780ec9bf0e5dd596c0ee1ac6310136067c4f13438b.rev
Binary files differ
diff --git a/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/refs/heads/master b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/refs/heads/master
new file mode 100644
index 00000000..a4e184b4
--- /dev/null
+++ b/format/commitgraph/read/testdata/fixtures/sha256/single_nochanged/repo.git/refs/heads/master
@@ -0,0 +1 @@
+7e396bf648e3b045c293d9fbdc533d4377d4e801d5d1fb57b84d22dd054a5860