aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md3
-rw-r--r--hash.go98
-rw-r--r--hash_sha1_test.go12
-rw-r--r--hash_sha256_test.go12
-rw-r--r--hash_test.go21
-rw-r--r--loose.go52
-rw-r--r--obj.go29
-rw-r--r--obj_blob.go13
-rw-r--r--obj_commit.go28
-rw-r--r--obj_tag.go22
-rw-r--r--obj_tree.go36
-rw-r--r--objects_test.go43
-rw-r--r--pack_idx.go41
-rw-r--r--pack_midx.go44
-rw-r--r--pack_pack.go32
-rw-r--r--pack_test.go18
-rw-r--r--refs.go33
-rw-r--r--repo.go35
-rw-r--r--repo_test.go75
19 files changed, 363 insertions, 284 deletions
diff --git a/README.md b/README.md
index c0defc35..9f93c9fa 100644
--- a/README.md
+++ b/README.md
@@ -73,7 +73,8 @@ optimizations thereof.
## Hash algorithm
-Furgit supports both SHA-256 and SHA-1.
+Furgit supports both SHA-256 and SHA-1 wit ha generics API. But it's broken and
+being refactored.
The default tests run with SHA-256. To run tests with SHA-1, use the `sha1`
build tag.
diff --git a/hash.go b/hash.go
index 336d5322..53dff11b 100644
--- a/hash.go
+++ b/hash.go
@@ -5,52 +5,80 @@ import (
"crypto/sha256"
"encoding/hex"
"fmt"
+ "unsafe"
)
-const maxHashSize = 32
+// HashType is a constraint that enumerates the supported hash sizes.
+type HashType interface {
+ [sha1.Size]byte | [sha256.Size]byte
+}
-// Hash represents a Git object identifier.
-type Hash [maxHashSize]byte
+type (
+ // SHA1Hash represents a SHA-1 hash.
+ SHA1Hash = [sha1.Size]byte
-// hashFunc is a function that computes a hash from input data.
-type hashFunc func([]byte) [maxHashSize]byte
+ // SHA256Hash represents a SHA-256 hash.
+ SHA256Hash = [sha256.Size]byte
+)
-// hashFuncs maps hash size to hash function.
-var hashFuncs = map[int]hashFunc{
- sha1.Size: func(data []byte) [maxHashSize]byte {
- var result [maxHashSize]byte
- sum := sha1.Sum(data)
- copy(result[:], sum[:])
- return result
- },
- sha256.Size: func(data []byte) [maxHashSize]byte {
- var result [maxHashSize]byte
- sum := sha256.Sum256(data)
- copy(result[:], sum[:])
- return result
- },
+// Hash represents a Git object identifier with type-level hash size.
+type Hash[T HashType] struct {
+ v T
}
-// ParseHashWithSize converts a hex string into a Hash for a given hash size.
-func ParseHashWithSize(s string, hashSize int) (Hash, error) {
- var id Hash
- if len(s) != hashSize*2 {
- return id, fmt.Errorf("furgit: invalid hash length %d", len(s))
+// hashLen returns the hash size for a given hash type.
+func hashLen[T HashType]() int {
+ var zero T
+ return len(zero)
+}
+
+// String returns the hash as a hex string.
+func (h Hash[T]) String() string {
+ return hex.EncodeToString(unsafe.Slice((*byte)(unsafe.Pointer(&h.v)), hashLen[T]()))
+}
+
+// Bytes returns a mutable copy of the underlying bytes.
+func (h Hash[T]) Bytes() []byte {
+ s := unsafe.Slice((*byte)(unsafe.Pointer(&h.v)), hashLen[T]())
+ return append([]byte(nil), s...)
+}
+
+// Slice returns a read-only slice view of the underlying bytes.
+func (h *Hash[T]) Slice() []byte {
+ return unsafe.Slice((*byte)(unsafe.Pointer(&h.v)), hashLen[T]())
+}
+
+// ParseHash converts a hex string into a Hash for the given hash type.
+func ParseHash[T HashType](s string) (Hash[T], error) {
+ var out Hash[T]
+ wantHex := hashLen[T]() * 2
+
+ if len(s) != wantHex {
+ return out, fmt.Errorf("furgit: invalid hash length %d, want %d", len(s), wantHex)
}
- data, err := hex.DecodeString(s)
+ raw, err := hex.DecodeString(s)
if err != nil {
- return id, fmt.Errorf("furgit: decode hash: %w", err)
+ return out, fmt.Errorf("furgit: decode hash: %w", err)
}
- copy(id[:], data)
- return id, nil
+ slice := unsafe.Slice((*byte)(unsafe.Pointer(&out.v)), hashLen[T]())
+ copy(slice, raw)
+ return out, nil
}
-// StringWithSize returns the ID as hex for a given hash size.
-func (id Hash) StringWithSize(hashSize int) string {
- return hex.EncodeToString(id[:hashSize])
-}
+// computeRawHash computes a hash from data.
+func computeRawHash[T HashType](data []byte) Hash[T] {
+ var out Hash[T]
+ slice := unsafe.Slice((*byte)(unsafe.Pointer(&out.v)), hashLen[T]())
-// BytesWithSize returns a mutable copy of the underlying bytes for a given hash size.
-func (id Hash) BytesWithSize(hashSize int) []byte {
- return append([]byte(nil), id[:hashSize]...)
+ switch hashLen[T]() {
+ case sha1.Size:
+ sum := sha1.Sum(data)
+ copy(slice, sum[:])
+ case sha256.Size:
+ sum := sha256.Sum256(data)
+ copy(slice, sum[:])
+ default:
+ panic("furgit: unsupported hash length")
+ }
+ return out
}
diff --git a/hash_sha1_test.go b/hash_sha1_test.go
index 9f3137b9..8d5b4e9c 100644
--- a/hash_sha1_test.go
+++ b/hash_sha1_test.go
@@ -7,3 +7,15 @@ import (
)
const testHashSize = sha1.Size
+
+type (
+ testHashType = [sha1.Size]byte
+ TestHash = Hash[testHashType]
+ TestRepository = Repository[testHashType]
+ TestBlob = Blob[testHashType]
+ TestTree = Tree[testHashType]
+ TestTreeEntry = TreeEntry[testHashType]
+ TestCommit = Commit[testHashType]
+ TestTag = Tag[testHashType]
+ TestObject = Object[testHashType]
+)
diff --git a/hash_sha256_test.go b/hash_sha256_test.go
index 0b735f0a..ce436ad7 100644
--- a/hash_sha256_test.go
+++ b/hash_sha256_test.go
@@ -7,3 +7,15 @@ import (
)
const testHashSize = sha256.Size
+
+type (
+ testHashType = [sha256.Size]byte
+ TestHash = Hash[testHashType]
+ TestRepository = Repository[testHashType]
+ TestBlob = Blob[testHashType]
+ TestTree = Tree[testHashType]
+ TestTreeEntry = TreeEntry[testHashType]
+ TestCommit = Commit[testHashType]
+ TestTag = Tag[testHashType]
+ TestObject = Object[testHashType]
+)
diff --git a/hash_test.go b/hash_test.go
index 4b359c4a..a6215a36 100644
--- a/hash_test.go
+++ b/hash_test.go
@@ -10,33 +10,34 @@ func TestParseHashValidAndInvalid(t *testing.T) {
repeats := (testHashSize*2 + len(pattern) - 1) / len(pattern)
hexStr := strings.Repeat(pattern, repeats)[:testHashSize*2]
- id, err := ParseHashWithSize(hexStr, testHashSize)
+ id, err := ParseHash[testHashType](hexStr)
if err != nil {
t.Fatalf("ParseHash returned error: %v", err)
}
- if got := id.StringWithSize(testHashSize); got != hexStr {
+ if got := id.String(); got != hexStr {
t.Fatalf("unexpected String result: %q", got)
}
- if _, err := ParseHashWithSize("abcd", testHashSize); err == nil {
+ if _, err := ParseHash[testHashType]("abcd"); err == nil {
t.Fatal("expected error for short hash")
}
badHex := strings.Repeat("z", testHashSize*2)
- if _, err := ParseHashWithSize(badHex, testHashSize); err == nil {
+ if _, err := ParseHash[testHashType](badHex); err == nil {
t.Fatal("expected error for non-hex input")
}
}
-func TestHashBytesCopiesUnderlyingData(t *testing.T) {
- var id Hash
- for i := range id {
- id[i] = byte(i)
+func TestHashTypeCopiesUnderlyingData(t *testing.T) {
+ var id TestHash
+ idSlice := id.Slice()
+ for i := range idSlice {
+ idSlice[i] = byte(i)
}
- orig := id.BytesWithSize(testHashSize)
+ orig := id.Bytes()
orig[0] ^= 0xff
- if id[0] == orig[0] {
+ if idSlice[0] == orig[0] {
t.Fatal("Bytes should return a copy")
}
}
diff --git a/loose.go b/loose.go
index c1371991..8ed9d173 100644
--- a/loose.go
+++ b/loose.go
@@ -12,21 +12,21 @@ import (
const looseHeaderLimit = 4096
-func loosePath(id Hash, hashSize int) string {
- hex := id.StringWithSize(hashSize)
+func loosePath[T HashType](id Hash[T]) string {
+ hex := id.String()
return filepath.Join("objects", hex[:2], hex[2:])
}
-func (repo *Repository) looseRead(id Hash) (Object, error) {
+func (repo *Repository[T]) looseRead(id Hash[T]) (Object[T], error) {
ty, body, err := repo.looseReadTyped(id)
if err != nil {
return nil, err
}
- return parseObjectBody(ty, id, body, repo.HashSize)
+ return parseObjectBody[T](ty, id, body)
}
-func (repo *Repository) looseReadTyped(id Hash) (ObjType, []byte, error) {
- path := repo.repoPath(loosePath(id, repo.HashSize))
+func (repo *Repository[T]) looseReadTyped(id Hash[T]) (ObjType, []byte, error) {
+ path := repo.repoPath(loosePath(id))
f, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
@@ -62,7 +62,7 @@ func (repo *Repository) looseReadTyped(id Hash) (ObjType, []byte, error) {
if declaredSize != int64(len(body)) {
return ObjInvalid, nil, ErrInvalidObject
}
- if !verifyRawObject(raw, id, repo.HashSize) {
+ if !verifyRawObject[T](raw, id) {
return ObjInvalid, nil, ErrInvalidObject
}
@@ -70,8 +70,8 @@ func (repo *Repository) looseReadTyped(id Hash) (ObjType, []byte, error) {
return ty, out, nil
}
-func (repo *Repository) looseTypeSize(id Hash) (ObjType, int64, error) {
- path := repo.repoPath(loosePath(id, repo.HashSize))
+func (repo *Repository[T]) looseTypeSize(id Hash[T]) (ObjType, int64, error) {
+ path := repo.repoPath(loosePath(id))
// #nosec G304
f, err := os.Open(path)
if err != nil {
@@ -155,46 +155,46 @@ func objTypeFromName(name string) (ObjType, error) {
}
// WriteLooseObject writes an object to the repository as a loose object.
-func (repo *Repository) WriteLooseObject(obj Object) (Hash, error) {
+func (repo *Repository[T]) WriteLooseObject(obj Object[T]) (Hash[T], error) {
var raw []byte
var err error
switch o := obj.(type) {
- case *Blob:
- raw, err = o.Serialize(repo.HashSize)
- case *Tree:
- raw, err = o.Serialize(repo.HashSize)
- case *Commit:
- raw, err = o.Serialize(repo.HashSize)
- case *Tag:
- raw, err = o.Serialize(repo.HashSize)
+ case *Blob[T]:
+ raw, err = o.Serialize()
+ case *Tree[T]:
+ raw, err = o.Serialize()
+ case *Commit[T]:
+ raw, err = o.Serialize()
+ case *Tag[T]:
+ raw, err = o.Serialize()
default:
- return Hash{}, fmt.Errorf("furgit: unsupported object type for writing: %T", obj)
+ return Hash[T]{}, fmt.Errorf("furgit: unsupported object type for writing: %T", obj)
}
// TODO: Consider adding serialize to the interface?
if err != nil {
- return Hash{}, err
+ return Hash[T]{}, err
}
- id := computeRawHash(raw, repo.HashSize)
- path := repo.repoPath(loosePath(id, repo.HashSize))
+ id := computeRawHash[T](raw)
+ path := repo.repoPath(loosePath(id))
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
- return Hash{}, err
+ return Hash[T]{}, err
}
var buf bytes.Buffer
zw := zlib.NewWriter(&buf)
if _, err := zw.Write(raw); err != nil {
- return Hash{}, err
+ return Hash[T]{}, err
}
if err := zw.Close(); err != nil {
- return Hash{}, err
+ return Hash[T]{}, err
}
if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil {
- return Hash{}, err
+ return Hash[T]{}, err
}
return id, nil
diff --git a/obj.go b/obj.go
index ce3d0258..07b76c17 100644
--- a/obj.go
+++ b/obj.go
@@ -29,15 +29,10 @@ const (
)
// Object describes any Git object variant.
-type Object interface {
+type Object[T HashType] interface {
ObjType() ObjType
}
-func computeRawHash(data []byte, hashSize int) Hash {
- hashFunc := hashFuncs[hashSize]
- return hashFunc(data)
-}
-
func headerForType(ty ObjType, body []byte) ([]byte, error) {
var tyStr string
switch ty {
@@ -64,11 +59,11 @@ func headerForType(ty ObjType, body []byte) ([]byte, error) {
return buf.Bytes(), nil
}
-func verifyRawObject(buf []byte, want Hash, hashSize int) bool {
- return computeRawHash(buf, hashSize) == want
+func verifyRawObject[T HashType](buf []byte, want Hash[T]) bool {
+ return computeRawHash[T](buf) == want
}
-func verifyTypedObject(ty ObjType, body []byte, want Hash, hashSize int) bool {
+func verifyTypedObject[T HashType](ty ObjType, body []byte, want Hash[T]) bool {
header, err := headerForType(ty, body)
if err != nil {
return false
@@ -76,19 +71,19 @@ func verifyTypedObject(ty ObjType, body []byte, want Hash, hashSize int) bool {
raw := make([]byte, len(header)+len(body))
copy(raw, header)
copy(raw[len(header):], body)
- return computeRawHash(raw, hashSize) == want
+ return computeRawHash[T](raw) == want
}
-func parseObjectBody(ty ObjType, id Hash, body []byte, hashSize int) (Object, error) {
+func parseObjectBody[T HashType](ty ObjType, id Hash[T], body []byte) (Object[T], error) {
switch ty {
case ObjBlob:
- return parseBlob(id, body)
+ return parseBlob[T](id, body)
case ObjTree:
- return parseTree(id, body, hashSize)
+ return parseTree[T](id, body)
case ObjCommit:
- return parseCommit(id, body, hashSize)
+ return parseCommit[T](id, body)
case ObjTag:
- return parseTag(id, body, hashSize)
+ return parseTag[T](id, body)
case ObjInvalid, ObjFuture, ObjOfsDelta, ObjRefDelta:
return nil, fmt.Errorf("furgit: object: unsupported type %d", ty)
default:
@@ -97,7 +92,7 @@ func parseObjectBody(ty ObjType, id Hash, body []byte, hashSize int) (Object, er
}
// ReadObject resolves an ID by consulting loose then packed storage.
-func (repo *Repository) ReadObject(id Hash) (Object, error) {
+func (repo *Repository[T]) ReadObject(id Hash[T]) (Object[T], error) {
obj, err := repo.looseRead(id)
if err == nil {
return obj, nil
@@ -113,7 +108,7 @@ func (repo *Repository) ReadObject(id Hash) (Object, error) {
}
// ReadObjectTypeSize reports the object type and size without inflating the body.
-func (repo *Repository) ReadObjectTypeSize(id Hash) (ObjType, int64, error) {
+func (repo *Repository[T]) ReadObjectTypeSize(id Hash[T]) (ObjType, int64, error) {
ty, size, err := repo.looseTypeSize(id)
if err == nil {
return ty, size, nil
diff --git a/obj_blob.go b/obj_blob.go
index 9edad0a9..1f74464e 100644
--- a/obj_blob.go
+++ b/obj_blob.go
@@ -1,27 +1,26 @@
package furgit
// Blob represents the contents of a Git blob.
-type Blob struct {
- Hash Hash
-
+type Blob[T HashType] struct {
+ Hash Hash[T]
Data []byte
}
// ObjType allows Blob to satisfy the Object interface.
-func (*Blob) ObjType() ObjType {
+func (*Blob[T]) ObjType() ObjType {
return ObjBlob
}
-func parseBlob(id Hash, body []byte) (*Blob, error) {
+func parseBlob[T HashType](id Hash[T], body []byte) (*Blob[T], error) {
data := append([]byte(nil), body...)
- return &Blob{
+ return &Blob[T]{
Hash: id,
Data: data,
}, nil
}
// Serialize renders the full "blob size\\0body" representation.
-func (b *Blob) Serialize(hashSize int) ([]byte, error) {
+func (b *Blob[T]) Serialize() ([]byte, error) {
header, err := headerForType(ObjBlob, b.Data)
if err != nil {
return nil, err
diff --git a/obj_commit.go b/obj_commit.go
index 84de2c41..f733a56f 100644
--- a/obj_commit.go
+++ b/obj_commit.go
@@ -7,10 +7,10 @@ import (
)
// Commit mirrors the structure of a Git commit object.
-type Commit struct {
- Hash Hash
- Tree Hash
- Parents []Hash
+type Commit[T HashType] struct {
+ Hash Hash[T]
+ Tree Hash[T]
+ Parents []Hash[T]
Author Ident
Committer Ident
Message []byte
@@ -18,12 +18,12 @@ type Commit struct {
}
// ObjType allows Commit to satisfy the Object interface.
-func (*Commit) ObjType() ObjType {
+func (*Commit[T]) ObjType() ObjType {
return ObjCommit
}
-func parseCommit(id Hash, body []byte, hashSize int) (*Commit, error) {
- c := new(Commit)
+func parseCommit[T HashType](id Hash[T], body []byte) (*Commit[T], error) {
+ c := new(Commit[T])
c.Hash = id
i := 0
for i < len(body) {
@@ -39,13 +39,13 @@ func parseCommit(id Hash, body []byte, hashSize int) (*Commit, error) {
switch {
case bytes.HasPrefix(line, []byte("tree ")):
- treeID, err := ParseHashWithSize(string(line[5:]), hashSize)
+ treeID, err := ParseHash[T](string(line[5:]))
if err != nil {
return nil, fmt.Errorf("furgit: commit: tree: %w", err)
}
c.Tree = treeID
case bytes.HasPrefix(line, []byte("parent ")):
- parent, err := ParseHashWithSize(string(line[7:]), hashSize)
+ parent, err := ParseHash[T](string(line[7:]))
if err != nil {
return nil, fmt.Errorf("furgit: commit: parent: %w", err)
}
@@ -91,11 +91,11 @@ func parseCommit(id Hash, body []byte, hashSize int) (*Commit, error) {
return c, nil
}
-func commitBody(c *Commit, hashSize int) []byte {
+func commitBody[T HashType](c *Commit[T]) []byte {
var buf bytes.Buffer
- fmt.Fprintf(&buf, "tree %s\n", c.Tree.StringWithSize(hashSize))
+ fmt.Fprintf(&buf, "tree %s\n", c.Tree.String())
for _, p := range c.Parents {
- fmt.Fprintf(&buf, "parent %s\n", p.StringWithSize(hashSize))
+ fmt.Fprintf(&buf, "parent %s\n", p.String())
}
buf.WriteString("author ")
buf.Write(c.Author.Serialize())
@@ -110,8 +110,8 @@ func commitBody(c *Commit, hashSize int) []byte {
}
// Serialize renders a Commit into canonical Git format.
-func (c *Commit) Serialize(hashSize int) ([]byte, error) {
- body := commitBody(c, hashSize)
+func (c *Commit[T]) Serialize() ([]byte, error) {
+ body := commitBody(c)
header, err := headerForType(ObjCommit, body)
if err != nil {
return nil, err
diff --git a/obj_tag.go b/obj_tag.go
index bf9ee97d..ce42e41b 100644
--- a/obj_tag.go
+++ b/obj_tag.go
@@ -7,9 +7,9 @@ import (
)
// Tag models an annotated Git tag object.
-type Tag struct {
- Hash Hash
- Target Hash
+type Tag[T HashType] struct {
+ Hash Hash[T]
+ Target Hash[T]
TargetType ObjType
Name []byte
Tagger *Ident
@@ -17,13 +17,13 @@ type Tag struct {
}
// ObjType allows Tag to satisfy the Object interface.
-func (*Tag) ObjType() ObjType {
+func (*Tag[T]) ObjType() ObjType {
return ObjTag
}
// parseTag parses a tag object body.
-func parseTag(id Hash, body []byte, hashSize int) (*Tag, error) {
- t := new(Tag)
+func parseTag[T HashType](id Hash[T], body []byte) (*Tag[T], error) {
+ t := new(Tag[T])
t.Hash = id
i := 0
var haveTarget, haveType bool
@@ -41,7 +41,7 @@ func parseTag(id Hash, body []byte, hashSize int) (*Tag, error) {
switch {
case bytes.HasPrefix(line, []byte("object ")):
- hash, err := ParseHashWithSize(string(line[7:]), hashSize)
+ hash, err := ParseHash[T](string(line[7:]))
if err != nil {
return nil, fmt.Errorf("furgit: tag: object: %w", err)
}
@@ -94,9 +94,9 @@ func parseTag(id Hash, body []byte, hashSize int) (*Tag, error) {
return t, nil
}
-func tagBody(t *Tag, hashSize int) ([]byte, error) {
+func tagBody[T HashType](t *Tag[T]) ([]byte, error) {
var buf bytes.Buffer
- fmt.Fprintf(&buf, "object %s\n", t.Target.StringWithSize(hashSize))
+ fmt.Fprintf(&buf, "object %s\n", t.Target.String())
buf.WriteString("type ")
switch t.TargetType {
case ObjCommit:
@@ -128,8 +128,8 @@ func tagBody(t *Tag, hashSize int) ([]byte, error) {
}
// Serialize renders a Tag into canonical Git format.
-func (t *Tag) Serialize(hashSize int) ([]byte, error) {
- body, err := tagBody(t, hashSize)
+func (t *Tag[T]) Serialize() ([]byte, error) {
+ body, err := tagBody(t)
if err != nil {
return nil, err
}
diff --git a/obj_tree.go b/obj_tree.go
index 55a27a08..c025dfa3 100644
--- a/obj_tree.go
+++ b/obj_tree.go
@@ -8,26 +8,27 @@ import (
)
// Tree represents a Git tree object.
-type Tree struct {
- Hash Hash
- Entries []TreeEntry
+type Tree[T HashType] struct {
+ Hash Hash[T]
+ Entries []TreeEntry[T]
}
// TreeEntry represents a single entry in a Git tree.
-type TreeEntry struct {
+type TreeEntry[T HashType] struct {
Mode uint32
Name []byte
- ID Hash
+ ID Hash[T]
}
// ObjType allows Tree to satisfy the Object interface.
-func (*Tree) ObjType() ObjType {
+func (*Tree[T]) ObjType() ObjType {
return ObjTree
}
// parseTree decodes a tree body.
-func parseTree(id Hash, body []byte, hashSize int) (*Tree, error) {
- var entries []TreeEntry
+func parseTree[T HashType](id Hash[T], body []byte) (*Tree[T], error) {
+ var entries []TreeEntry[T]
+ hashSize := hashLen[T]()
i := 0
for i < len(body) {
space := bytes.IndexByte(body[i:], ' ')
@@ -47,8 +48,8 @@ func parseTree(id Hash, body []byte, hashSize int) (*Tree, error) {
if i+hashSize > len(body) {
return nil, errors.New("furgit: tree: truncated child hash")
}
- var child Hash
- copy(child[:], body[i:i+hashSize])
+ var child Hash[T]
+ copy(child.Slice(), body[i:i+hashSize])
i += hashSize
mode, err := strconv.ParseUint(string(modeBytes), 8, 32)
@@ -56,7 +57,7 @@ func parseTree(id Hash, body []byte, hashSize int) (*Tree, error) {
return nil, fmt.Errorf("furgit: tree: parse mode: %w", err)
}
- entry := TreeEntry{
+ entry := TreeEntry[T]{
Mode: uint32(mode),
Name: append([]byte(nil), nameBytes...),
ID: child,
@@ -64,14 +65,15 @@ func parseTree(id Hash, body []byte, hashSize int) (*Tree, error) {
entries = append(entries, entry)
}
- return &Tree{
+ return &Tree[T]{
Hash: id,
Entries: entries,
}, nil
}
// treeBody builds the entry list for a tree without the Git header.
-func treeBody(t *Tree, hashSize int) []byte {
+func treeBody[T HashType](t *Tree[T]) []byte {
+ hashSize := hashLen[T]()
var bodyLen int
for _, e := range t.Entries {
mode := strconv.FormatUint(uint64(e.Mode), 8)
@@ -88,15 +90,15 @@ func treeBody(t *Tree, hashSize int) []byte {
pos += copy(body[pos:], e.Name)
body[pos] = 0
pos++
- pos += copy(body[pos:], e.ID[:hashSize])
+ pos += copy(body[pos:], e.ID.Slice()[:hashSize])
}
return body
}
// Serialize renders a Tree into canonical Git format.
-func (t *Tree) Serialize(hashSize int) ([]byte, error) {
- body := treeBody(t, hashSize)
+func (t *Tree[T]) Serialize() ([]byte, error) {
+ body := treeBody(t)
header, err := headerForType(ObjTree, body)
if err != nil {
return nil, err
@@ -109,7 +111,7 @@ func (t *Tree) Serialize(hashSize int) ([]byte, error) {
}
// Entry looks up a tree entry by name.
-func (t *Tree) Entry(name []byte) *TreeEntry {
+func (t *Tree[T]) Entry(name []byte) *TreeEntry[T] {
low, high := 0, len(t.Entries)-1
for low <= high {
mid := (low + high) / 2
diff --git a/objects_test.go b/objects_test.go
index 3a3e641f..cf9d3da7 100644
--- a/objects_test.go
+++ b/objects_test.go
@@ -8,18 +8,19 @@ import (
"testing"
)
-func mustHash(t *testing.T, hex string) Hash {
- id, err := ParseHashWithSize(hex, testHashSize)
+func mustHash(t *testing.T, hex string) TestHash {
+ id, err := ParseHash[testHashType](hex)
if err != nil {
t.Fatalf("ParseHash failed: %v", err)
}
return id
}
-func hashWithByte(fill byte) Hash {
- var h Hash
+func hashWithByte(fill byte) TestHash {
+ var h TestHash
+ s := h.Slice()
for i := 0; i < testHashSize; i++ {
- h[i] = fill
+ s[i] = fill
fill++
}
return h
@@ -31,7 +32,7 @@ func TestLoosePathUsesExpectedLayout(t *testing.T) {
hexStr := strings.Repeat(pattern, repeats)[:testHashSize*2]
id := mustHash(t, hexStr)
expect := filepath.Join("objects", hexStr[:2], hexStr[2:])
- if got := loosePath(id, testHashSize); got != expect {
+ if got := loosePath(id); got != expect {
t.Fatalf("unexpected loose path: %q", got)
}
}
@@ -49,7 +50,7 @@ func TestParseBlobAndSerialize(t *testing.T) {
if blob.Hash != id {
t.Fatalf("blob hash mismatch: %v", blob.Hash)
}
- raw, err := blob.Serialize(testHashSize)
+ raw, err := blob.Serialize()
if err != nil {
t.Fatalf("Serialize error: %v", err)
}
@@ -64,13 +65,13 @@ func TestParseBlobAndSerialize(t *testing.T) {
}
func TestParseTreeAndSerialize(t *testing.T) {
- entries := []TreeEntry{
+ entries := []TestTreeEntry{
{Mode: 0100644, Name: []byte("file.txt"), ID: hashWithByte(0x20)},
{Mode: 040000, Name: []byte("subdir"), ID: hashWithByte(0x30)},
}
- body := treeBody(&Tree{Entries: entries}, testHashSize)
+ body := treeBody(&TestTree{Entries: entries})
id := hashWithByte(0x40)
- tree, err := parseTree(id, body, testHashSize)
+ tree, err := parseTree(id, body)
if err != nil {
t.Fatalf("parseTree error: %v", err)
}
@@ -82,7 +83,7 @@ func TestParseTreeAndSerialize(t *testing.T) {
t.Fatalf("entry %d mismatch", i)
}
}
- serialized, err := (&Tree{Entries: entries}).Serialize(testHashSize)
+ serialized, err := (&TestTree{Entries: entries}).Serialize()
if err != nil {
t.Fatalf("Serialize error: %v", err)
}
@@ -103,8 +104,8 @@ func TestParseCommitWithExtraHeader(t *testing.T) {
OffsetMinutes: -420,
}
var buf bytes.Buffer
- fmt.Fprintf(&buf, "tree %s\n", treeID.StringWithSize(testHashSize))
- fmt.Fprintf(&buf, "parent %s\n", parent.StringWithSize(testHashSize))
+ fmt.Fprintf(&buf, "tree %s\n", treeID.String())
+ fmt.Fprintf(&buf, "parent %s\n", parent.String())
buf.WriteString("author ")
buf.Write(ident.Serialize())
buf.WriteByte('\n')
@@ -112,7 +113,7 @@ func TestParseCommitWithExtraHeader(t *testing.T) {
buf.Write(ident.Serialize())
buf.WriteByte('\n')
buf.WriteString("extra data\n\nMessage body\n")
- commit, err := parseCommit(hashWithByte(0x70), buf.Bytes(), testHashSize)
+ commit, err := parseCommit(hashWithByte(0x70), buf.Bytes())
if err != nil {
t.Fatalf("parseCommit error: %v", err)
}
@@ -129,18 +130,18 @@ func TestParseCommitWithExtraHeader(t *testing.T) {
t.Fatalf("extra headers mismatch: %+v", commit.ExtraHeaders)
}
- roundTrip := &Commit{
+ roundTrip := &TestCommit{
Tree: treeID,
- Parents: []Hash{parent},
+ Parents: []TestHash{parent},
Author: ident,
Committer: ident,
Message: []byte("Message body\n"),
}
- raw, err := roundTrip.Serialize(testHashSize)
+ raw, err := roundTrip.Serialize()
if err != nil {
t.Fatalf("Serialize error: %v", err)
}
- if !strings.Contains(string(raw), "tree "+treeID.StringWithSize(testHashSize)) {
+ if !strings.Contains(string(raw), "tree "+treeID.String()) {
t.Fatalf("serialized commit missing tree header")
}
}
@@ -155,7 +156,7 @@ func TestParseTagAndSerialize(t *testing.T) {
}
var buf bytes.Buffer
buf.WriteString("object ")
- buf.WriteString(target.StringWithSize(testHashSize))
+ buf.WriteString(target.String())
buf.WriteByte('\n')
buf.WriteString("type commit\n")
buf.WriteString("tag v1.0\n")
@@ -163,7 +164,7 @@ func TestParseTagAndSerialize(t *testing.T) {
buf.Write(tagger.Serialize())
buf.WriteString("\n\nannotated tag\n")
body := append([]byte(nil), buf.Bytes()...)
- tag, err := parseTag(hashWithByte(0x90), body, testHashSize)
+ tag, err := parseTag(hashWithByte(0x90), body)
if err != nil {
t.Fatalf("parseTag error: %v", err)
}
@@ -179,7 +180,7 @@ func TestParseTagAndSerialize(t *testing.T) {
if string(tag.Name) != "v1.0" {
t.Fatalf("tag name mismatch: %q", tag.Name)
}
- serialized, err := tag.Serialize(testHashSize)
+ serialized, err := tag.Serialize()
if err != nil {
t.Fatalf("Serialize error: %v", err)
}
diff --git a/pack_idx.go b/pack_idx.go
index 9ca28193..772c99cc 100644
--- a/pack_idx.go
+++ b/pack_idx.go
@@ -15,8 +15,8 @@ const (
idxVersion2 = 2
)
-type packIndex struct {
- repo *Repository
+type packIndex[T HashType] struct {
+ repo *Repository[T]
idxRel string
packPath string
@@ -34,7 +34,7 @@ type packIndex struct {
closeOnce sync.Once
}
-func (pi *packIndex) Close() error {
+func (pi *packIndex[T]) Close() error {
if pi == nil {
return nil
}
@@ -56,14 +56,14 @@ func (pi *packIndex) Close() error {
return closeErr
}
-func (pi *packIndex) ensureLoaded() error {
+func (pi *packIndex[T]) ensureLoaded() error {
pi.loadOnce.Do(func() {
pi.loadErr = pi.load()
})
return pi.loadErr
}
-func (pi *packIndex) load() error {
+func (pi *packIndex[T]) load() error {
if pi.repo == nil {
return ErrInvalidObject
}
@@ -105,14 +105,14 @@ func (pi *packIndex) load() error {
return nil
}
-func (r *Repository) packIndexes() ([]*packIndex, error) {
+func (r *Repository[T]) packIndexes() ([]*packIndex[T], error) {
r.packIdxOnce.Do(func() {
r.packIdx, r.packIdxErr = r.loadPackIndexes()
})
return r.packIdx, r.packIdxErr
}
-func (repo *Repository) loadPackIndexes() ([]*packIndex, error) {
+func (repo *Repository[T]) loadPackIndexes() ([]*packIndex[T], error) {
dir := filepath.Join(repo.rootPath, "objects", "pack")
entries, err := os.ReadDir(dir)
if err != nil {
@@ -122,14 +122,14 @@ func (repo *Repository) loadPackIndexes() ([]*packIndex, error) {
return nil, err
}
- idxs := make([]*packIndex, 0, len(entries))
+ idxs := make([]*packIndex[T], 0, len(entries))
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".idx") {
continue
}
rel := filepath.Join("objects", "pack", entry.Name())
packRel := strings.TrimSuffix(rel, ".idx") + ".pack"
- idxs = append(idxs, &packIndex{
+ idxs = append(idxs, &packIndex[T]{
repo: repo,
idxRel: rel,
packPath: packRel,
@@ -141,7 +141,7 @@ func (repo *Repository) loadPackIndexes() ([]*packIndex, error) {
return idxs, nil
}
-func (pi *packIndex) parse(buf []byte) error {
+func (pi *packIndex[T]) parse(buf []byte) error {
if len(buf) < 8+256*4 {
return ErrInvalidObject
}
@@ -161,8 +161,9 @@ func (pi *packIndex) parse(buf []byte) error {
pi.fanout = buf[fanoutStart:fanoutEnd]
nobj := int(readBE32(pi.fanout[len(pi.fanout)-4:]))
+ hashSize := pi.repo.hashSize()
namesStart := fanoutEnd
- namesEnd := namesStart + nobj*pi.repo.HashSize
+ namesEnd := namesStart + nobj*hashSize
if namesEnd > len(buf) {
return ErrInvalidObject
}
@@ -182,7 +183,7 @@ func (pi *packIndex) parse(buf []byte) error {
pi.offset32 = buf[off32Start:off32End]
off64Start := off32End
- trailerStart := len(buf) - 2*pi.repo.HashSize
+ trailerStart := len(buf) - 2*hashSize
if trailerStart < off64Start {
return ErrInvalidObject
}
@@ -211,7 +212,7 @@ func readBE64(b []byte) uint64 {
(uint64(b[6]) << 8) | uint64(b[7])
}
-func (pi *packIndex) fanoutEntry(i int) uint32 {
+func (pi *packIndex[T]) fanoutEntry(i int) uint32 {
if len(pi.fanout) == 0 {
return 0
}
@@ -223,7 +224,7 @@ func (pi *packIndex) fanoutEntry(i int) uint32 {
return readBE32(pi.fanout[start : start+4])
}
-func (pi *packIndex) offset(idx int) (uint64, error) {
+func (pi *packIndex[T]) offset(idx int) (uint64, error) {
start := idx * 4
word := readBE32(pi.offset32[start : start+4])
if word&0x80000000 == 0 {
@@ -238,18 +239,20 @@ func (pi *packIndex) offset(idx int) (uint64, error) {
return readBE64(pi.offset64[base : base+8]), nil
}
-func (pi *packIndex) lookup(id Hash) (packlocation, error) {
+func (pi *packIndex[T]) lookup(id Hash[T]) (packlocation, error) {
err := pi.ensureLoaded()
if err != nil {
return packlocation{}, err
}
- first := int(id[0])
+ idSlice := id.Slice()
+ first := int(idSlice[0])
var lo int
if first > 0 {
lo = int(pi.fanoutEntry(first - 1))
}
hi := int(pi.fanoutEntry(first))
- idx, found := bsearchHash(pi.names, pi.repo.HashSize, lo, hi, id)
+ stride := hashLen[T]()
+ idx, found := bsearchHash(pi.names, stride, lo, hi, idSlice)
if !found {
return packlocation{}, ErrNotFound
}
@@ -263,10 +266,10 @@ func (pi *packIndex) lookup(id Hash) (packlocation, error) {
}, nil
}
-func bsearchHash(names []byte, stride, lo, hi int, want Hash) (int, bool) {
+func bsearchHash(names []byte, stride, lo, hi int, want []byte) (int, bool) {
for lo < hi {
mid := lo + (hi-lo)/2
- cmp := compareHash(names, stride, mid, want[:stride])
+ cmp := compareHash(names, stride, mid, want)
if cmp == 0 {
return mid, true
}
diff --git a/pack_midx.go b/pack_midx.go
index 7f31e565..e91d5d43 100644
--- a/pack_midx.go
+++ b/pack_midx.go
@@ -1,6 +1,8 @@
package furgit
import (
+ "crypto/sha1"
+ "crypto/sha256"
"os"
"path/filepath"
"strings"
@@ -22,8 +24,8 @@ const (
chunkLOFF = 0x4c4f4646 // LOFF
)
-type multiPackIndex struct {
- repo *Repository
+type multiPackIndex[T HashType] struct {
+ repo *Repository[T]
loadOnce sync.Once
loadErr error
@@ -40,7 +42,7 @@ type multiPackIndex struct {
closeOnce sync.Once
}
-func (midx *multiPackIndex) Close() error {
+func (midx *multiPackIndex[T]) Close() error {
if midx == nil {
return nil
}
@@ -63,14 +65,14 @@ func (midx *multiPackIndex) Close() error {
return closeErr
}
-func (midx *multiPackIndex) ensureLoaded() error {
+func (midx *multiPackIndex[T]) ensureLoaded() error {
midx.loadOnce.Do(func() {
midx.loadErr = midx.load()
})
return midx.loadErr
}
-func (midx *multiPackIndex) load() error {
+func (midx *multiPackIndex[T]) load() error {
if midx.repo == nil {
return ErrInvalidObject
}
@@ -113,7 +115,18 @@ func (midx *multiPackIndex) load() error {
return nil
}
-func (midx *multiPackIndex) parse(buf []byte) error {
+func oidVersionFor[T HashType]() byte {
+ switch hashLen[T]() {
+ case sha1.Size:
+ return midxOIDVersionSHA1
+ case sha256.Size:
+ return midxOIDVersionSHA256
+ default:
+ panic("furgit: unsupported hash len")
+ }
+}
+
+func (midx *multiPackIndex[T]) parse(buf []byte) error {
if len(buf) < 12 {
return ErrInvalidObject
}
@@ -125,7 +138,7 @@ func (midx *multiPackIndex) parse(buf []byte) error {
return ErrInvalidObject
}
oidVersion := buf[5]
- if oidVersion != midxOIDVersionSHA1 && oidVersion != midxOIDVersionSHA256 {
+ if oidVersion != oidVersionFor[T]() {
return ErrInvalidObject
}
numChunks := int(buf[6])
@@ -197,7 +210,8 @@ func (midx *multiPackIndex) parse(buf []byte) error {
if !ok {
return ErrInvalidObject
}
- oidlSize := int64(numObjects) * int64(midx.repo.HashSize)
+ hashSize := midx.repo.hashSize()
+ oidlSize := int64(numObjects) * int64(hashSize)
if oidlOffset < 0 || oidlOffset+oidlSize > int64(len(buf)) {
return ErrInvalidObject
}
@@ -237,7 +251,7 @@ func (midx *multiPackIndex) parse(buf []byte) error {
return nil
}
-func (midx *multiPackIndex) lookup(id Hash) (packlocation, error) {
+func (midx *multiPackIndex[T]) lookup(id Hash[T]) (packlocation, error) {
if len(midx.data) == 0 {
err := midx.ensureLoaded()
if err != nil {
@@ -245,14 +259,16 @@ func (midx *multiPackIndex) lookup(id Hash) (packlocation, error) {
}
}
- first := int(id[0])
+ idSlice := id.Slice()
+ first := int(idSlice[0])
var lo int
if first > 0 {
lo = int(readBE32(midx.fanout[(first-1)*4 : first*4]))
}
hi := int(readBE32(midx.fanout[first*4 : (first+1)*4]))
- idx, found := bsearchHash(midx.oids, midx.repo.HashSize, lo, hi, id)
+ stride := hashLen[T]()
+ idx, found := bsearchHash(midx.oids, stride, lo, hi, idSlice)
if !found {
return packlocation{}, ErrNotFound
}
@@ -288,15 +304,15 @@ func (midx *multiPackIndex) lookup(id Hash) (packlocation, error) {
}, nil
}
-func (repo *Repository) multiPackIndex() (*multiPackIndex, error) {
+func (repo *Repository[T]) multiPackIndex() (*multiPackIndex[T], error) {
repo.midxOnce.Do(func() {
repo.midx, repo.midxErr = repo.loadMultiPackIndex()
})
return repo.midx, repo.midxErr
}
-func (repo *Repository) loadMultiPackIndex() (*multiPackIndex, error) {
- midx := &multiPackIndex{repo: repo}
+func (repo *Repository[T]) loadMultiPackIndex() (*multiPackIndex[T], error) {
+ midx := &multiPackIndex[T]{repo: repo}
err := midx.ensureLoaded()
if err != nil {
if os.IsNotExist(err) {
diff --git a/pack_pack.go b/pack_pack.go
index 757c5c02..9643ba99 100644
--- a/pack_pack.go
+++ b/pack_pack.go
@@ -24,7 +24,7 @@ type packlocation struct {
Offset uint64
}
-func (repo *Repository) packRead(id Hash) (Object, error) {
+func (repo *Repository[T]) packRead(id Hash[T]) (Object[T], error) {
loc, err := repo.packIndexFind(id)
if err != nil {
return nil, err
@@ -32,7 +32,7 @@ func (repo *Repository) packRead(id Hash) (Object, error) {
return repo.packReadAt(loc, id)
}
-func (repo *Repository) packIndexFind(id Hash) (packlocation, error) {
+func (repo *Repository[T]) packIndexFind(id Hash[T]) (packlocation, error) {
midx, err := repo.multiPackIndex()
if err == nil {
loc, err := midx.lookup(id)
@@ -63,22 +63,22 @@ func (repo *Repository) packIndexFind(id Hash) (packlocation, error) {
return packlocation{}, ErrNotFound
}
-func (repo *Repository) packReadAt(loc packlocation, want Hash) (Object, error) {
+func (repo *Repository[T]) packReadAt(loc packlocation, want Hash[T]) (Object[T], error) {
ty, body, err := repo.packBodyResolveAtLocation(loc)
if err != nil {
return nil, err
}
data := body.Bytes()
- if !verifyTypedObject(ty, data, want, repo.HashSize) {
+ if !verifyTypedObject[T](ty, data, want) {
body.Release()
return nil, ErrInvalidObject
}
- obj, err := parseObjectBody(ty, want, data, repo.HashSize)
+ obj, err := parseObjectBody[T](ty, want, data)
body.Release()
return obj, err
}
-func (repo *Repository) packBodyResolveAtLocation(loc packlocation) (ObjType, borrowedBody, error) {
+func (repo *Repository[T]) packBodyResolveAtLocation(loc packlocation) (ObjType, borrowedBody, error) {
pf, err := repo.packFile(loc.PackPath)
if err != nil {
return ObjInvalid, borrowedBody{}, err
@@ -86,7 +86,7 @@ func (repo *Repository) packBodyResolveAtLocation(loc packlocation) (ObjType, bo
return repo.packBodyResolveWithin(pf, loc.Offset)
}
-func (repo *Repository) packTypeSizeAtLocation(loc packlocation, seen map[packKey]struct{}) (ObjType, int64, error) {
+func (repo *Repository[T]) packTypeSizeAtLocation(loc packlocation, seen map[packKey]struct{}) (ObjType, int64, error) {
pf, err := repo.packFile(loc.PackPath)
if err != nil {
return ObjInvalid, 0, err
@@ -94,7 +94,7 @@ func (repo *Repository) packTypeSizeAtLocation(loc packlocation, seen map[packKe
return repo.packTypeSizeWithin(pf, loc.Offset, seen)
}
-func (repo *Repository) packTypeSizeByID(id Hash, seen map[packKey]struct{}) (ObjType, int64, error) {
+func (repo *Repository[T]) packTypeSizeByID(id Hash[T], seen map[packKey]struct{}) (ObjType, int64, error) {
loc, err := repo.packIndexFind(id)
if err == nil {
return repo.packTypeSizeAtLocation(loc, seen)
@@ -172,7 +172,7 @@ func packSectionInflate(r io.Reader, sizeHint int) (borrowedBody, error) {
}
}
-func (repo *Repository) packDeltaResolveOfs(pf *packFile, deltaOffset uint64, r io.Reader) (ObjType, borrowedBody, error) {
+func (repo *Repository[T]) packDeltaResolveOfs(pf *packFile, deltaOffset uint64, r io.Reader) (ObjType, borrowedBody, error) {
dist, err := packDeltaReadOfsDistance(r)
if err != nil {
return ObjInvalid, borrowedBody{}, err
@@ -220,7 +220,7 @@ func packDeltaReadOfsDistance(r io.Reader) (uint64, error) {
return dist, nil
}
-func (repo *Repository) packBodyResolveByID(id Hash) (ObjType, borrowedBody, error) {
+func (repo *Repository[T]) packBodyResolveByID(id Hash[T]) (ObjType, borrowedBody, error) {
loc, err := repo.packIndexFind(id)
if err == nil {
return repo.packBodyResolveAtLocation(loc)
@@ -240,7 +240,7 @@ type packKey struct {
ofs uint64
}
-func (repo *Repository) packTypeSizeWithin(pf *packFile, ofs uint64, seen map[packKey]struct{}) (ObjType, int64, error) {
+func (repo *Repository[T]) packTypeSizeWithin(pf *packFile, ofs uint64, seen map[packKey]struct{}) (ObjType, int64, error) {
if pf == nil {
return ObjInvalid, 0, ErrInvalidObject
}
@@ -268,8 +268,8 @@ func (repo *Repository) packTypeSizeWithin(pf *packFile, ofs uint64, seen map[pa
case ObjCommit, ObjTree, ObjBlob, ObjTag:
return ty, declaredSize, nil
case ObjRefDelta:
- var base Hash
- _, err := io.ReadFull(r, base[:])
+ var base Hash[T]
+ _, err := io.ReadFull(r, base.Slice())
if err != nil {
return ObjInvalid, 0, err
}
@@ -299,7 +299,7 @@ func (repo *Repository) packTypeSizeWithin(pf *packFile, ofs uint64, seen map[pa
}
}
-func (repo *Repository) packBodyResolveWithin(pf *packFile, ofs uint64) (ObjType, borrowedBody, error) {
+func (repo *Repository[T]) packBodyResolveWithin(pf *packFile, ofs uint64) (ObjType, borrowedBody, error) {
r, err := pf.cursor(ofs)
if err != nil {
return ObjInvalid, borrowedBody{}, err
@@ -314,8 +314,8 @@ func (repo *Repository) packBodyResolveWithin(pf *packFile, ofs uint64) (ObjType
body, err := packSectionInflate(r, size)
return ty, body, err
case ObjRefDelta:
- var base Hash
- _, err := io.ReadFull(r, base[:])
+ var base Hash[T]
+ _, err := io.ReadFull(r, base.Slice())
if err != nil {
return ObjInvalid, borrowedBody{}, err
}
diff --git a/pack_test.go b/pack_test.go
index 48f4d604..89b521fb 100644
--- a/pack_test.go
+++ b/pack_test.go
@@ -153,20 +153,22 @@ func TestPackDeltaReadOfsDistance(t *testing.T) {
func TestBsearchHash(t *testing.T) {
h1 := hashWithByte(0x01)
h2 := hashWithByte(0x03)
- names := append(append([]byte(nil), h1[:testHashSize]...), h2[:testHashSize]...)
- idx, found := bsearchHash(names, testHashSize, 0, 2, h2)
+ names := append(append([]byte(nil), h1.Slice()[:testHashSize]...), h2.Slice()[:testHashSize]...)
+ idx, found := bsearchHash(names, testHashSize, 0, 2, h2.Slice())
if !found || idx != 1 {
t.Fatalf("expected to find second hash, idx=%d found=%v", idx, found)
}
- _, found = bsearchHash(names, testHashSize, 0, 2, hashWithByte(0x05))
+ h3 := hashWithByte(0x05)
+ _, found = bsearchHash(names, testHashSize, 0, 2, h3.Slice())
if found {
t.Fatalf("did not expect to find unknown hash")
}
}
-func buildTestPackIndexBuffer(hash Hash, offset uint32) []byte {
+func buildTestPackIndexBuffer(hash TestHash, offset uint32) []byte {
fanout := make([]byte, 256*4)
- first := int(hash[0])
+ hashSlice := hash.Slice()
+ first := int(hashSlice[0])
for i := 0; i < 256; i++ {
var val uint32
if i >= first {
@@ -178,7 +180,7 @@ func buildTestPackIndexBuffer(hash Hash, offset uint32) []byte {
_ = binary.Write(&buf, binary.BigEndian, uint32(idxMagic))
_ = binary.Write(&buf, binary.BigEndian, uint32(idxVersion2))
buf.Write(fanout)
- buf.Write(hash[:testHashSize])
+ buf.Write(hashSlice[:testHashSize])
buf.Write(make([]byte, 4))
off32 := make([]byte, 4)
binary.BigEndian.PutUint32(off32, offset)
@@ -190,7 +192,7 @@ func buildTestPackIndexBuffer(hash Hash, offset uint32) []byte {
func TestPackIndexParse(t *testing.T) {
h := hashWithByte(0x11)
data := buildTestPackIndexBuffer(h, 0x12345678)
- pi := &packIndex{repo: &Repository{HashSize: testHashSize}}
+ pi := &packIndex[testHashType]{repo: &TestRepository{}}
if err := pi.parse(data); err != nil {
t.Fatalf("parse error: %v", err)
}
@@ -203,7 +205,7 @@ func TestPackIndexParse(t *testing.T) {
}
func TestPackIndexOffset64(t *testing.T) {
- pi := &packIndex{}
+ pi := &packIndex[testHashType]{}
pi.offset32 = make([]byte, 4)
binary.BigEndian.PutUint32(pi.offset32, 0x80000000)
pi.offset64 = make([]byte, 8)
diff --git a/refs.go b/refs.go
index b543a842..92837454 100644
--- a/refs.go
+++ b/refs.go
@@ -9,44 +9,45 @@ import (
)
// ResolveRef resolves a fully qualified ref name to its object ID.
-func (repo *Repository) ResolveRef(refname string) (Hash, error) {
+func (repo *Repository[T]) ResolveRef(refname string) (Hash[T], error) {
id, err := repo.resolveLooseRef(refname)
if err == nil {
return id, nil
} else if !errors.Is(err, ErrNotFound) {
- return Hash{}, err
+ return Hash[T]{}, err
}
return repo.resolvePackedRef(refname)
}
-func (repo *Repository) resolveLooseRef(refname string) (Hash, error) {
+func (repo *Repository[T]) resolveLooseRef(refname string) (Hash[T], error) {
data, err := os.ReadFile(repo.repoPath(refname))
if err != nil {
if os.IsNotExist(err) {
- return Hash{}, ErrNotFound
+ return Hash[T]{}, ErrNotFound
}
- return Hash{}, err
+ return Hash[T]{}, err
}
line := strings.TrimSpace(string(data))
- id, err := ParseHashWithSize(line, repo.HashSize)
+ id, err := ParseHash[T](line)
if err != nil {
- return Hash{}, err
+ return Hash[T]{}, err
}
return id, nil
}
-func (repo *Repository) resolvePackedRef(refname string) (Hash, error) {
+func (repo *Repository[T]) resolvePackedRef(refname string) (Hash[T], error) {
path := repo.repoPath("packed-refs")
f, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
- return Hash{}, ErrInvalidObject
+ return Hash[T]{}, ErrInvalidObject
}
- return Hash{}, err
+ return Hash[T]{}, err
}
defer func() { _ = f.Close() }()
+ hashSize := repo.hashSize()
want := []byte(refname)
scanner := bufio.NewScanner(f)
for scanner.Scan() {
@@ -55,28 +56,28 @@ func (repo *Repository) resolvePackedRef(refname string) (Hash, error) {
continue
}
sp := bytes.IndexByte(line, ' ')
- if sp != repo.HashSize*2 {
+ if sp != hashSize*2 {
continue
}
name := line[sp+1:]
if bytes.Equal(name, want) {
hex := string(line[:sp])
- id, err := ParseHashWithSize(hex, repo.HashSize)
+ id, err := ParseHash[T](hex)
if err != nil {
- return Hash{}, err
+ return Hash[T]{}, err
}
return id, nil
}
}
scanErr := scanner.Err()
if scanErr != nil {
- return Hash{}, scanErr
+ return Hash[T]{}, scanErr
}
- return Hash{}, ErrInvalidObject
+ return Hash[T]{}, ErrInvalidObject
}
// ResolveHEAD reads HEAD and returns the ref that HEAD points to.
-func (repo *Repository) ResolveHEAD() (string, error) {
+func (repo *Repository[T]) ResolveHEAD() (string, error) {
data, err := os.ReadFile(repo.repoPath("HEAD"))
if err != nil {
return "", err
diff --git a/repo.go b/repo.go
index fb835edb..50bbf44c 100644
--- a/repo.go
+++ b/repo.go
@@ -1,33 +1,31 @@
package furgit
import (
- "fmt"
"os"
"path/filepath"
"sync"
)
// Repository represents the root of a Git repository.
-type Repository struct {
+type Repository[T HashType] struct {
rootPath string
- HashSize int
packIdxOnce sync.Once
- packIdx []*packIndex
+ packIdx []*packIndex[T]
packIdxErr error
midxOnce sync.Once
- midx *multiPackIndex
+ midx *multiPackIndex[T]
midxErr error
packFiles sync.Map // string, *packFile
closeOnce sync.Once
}
-// OpenRepository opens the repository at the provided path with the specified hash size.
+// OpenRepository opens the repository at the provided path with the specified hash type.
// This will be replaced later with a function that auto-detects the hash size based
// on the git configuration.
-func OpenRepository(path string, hashSize int) (*Repository, error) {
+func OpenRepository[T HashType](path string) (*Repository[T], error) {
fi, err := os.Stat(path)
if err != nil {
return nil, err
@@ -35,13 +33,20 @@ func OpenRepository(path string, hashSize int) (*Repository, error) {
if !fi.IsDir() {
return nil, ErrInvalidObject
}
- if _, ok := hashFuncs[hashSize]; !ok {
- return nil, fmt.Errorf("furgit: unsupported hash size %d", hashSize)
- }
- return &Repository{rootPath: path, HashSize: hashSize}, nil
+ return &Repository[T]{rootPath: path}, nil
+}
+
+// hashSize returns the hash size for this repository.
+func (r *Repository[T]) hashSize() int {
+ return hashLen[T]()
+}
+
+// ParseHash is a convenience method for parsing hashes in the context of this repository.
+func (r *Repository[T]) ParseHash(s string) (Hash[T], error) {
+ return ParseHash[T](s)
}
-func (r *Repository) Close() error {
+func (r *Repository[T]) Close() error {
var closeErr error
r.closeOnce.Do(func() {
r.packFiles.Range(func(keya any, pfa any) bool {
@@ -73,16 +78,16 @@ func (r *Repository) Close() error {
}
// Root returns the repository root path.
-func (r *Repository) Root() string {
+func (r *Repository[T]) Root() string {
return r.rootPath
}
// repoPath joins the root with a relative path.
-func (r *Repository) repoPath(rel string) string {
+func (r *Repository[T]) repoPath(rel string) string {
return filepath.Join(r.rootPath, rel)
}
-func (r *Repository) packFile(rel string) (*packFile, error) {
+func (r *Repository[T]) packFile(rel string) (*packFile, error) {
if pf, ok := r.packFiles.Load(rel); ok {
return pf.(*packFile), nil
}
diff --git a/repo_test.go b/repo_test.go
index d09d2642..f8e24926 100644
--- a/repo_test.go
+++ b/repo_test.go
@@ -12,8 +12,8 @@ import (
"testing"
)
-func writeLooseBlob(t *testing.T, repo *Repository, data []byte) Hash {
- blob := &Blob{Data: data}
+func writeLooseBlob(t *testing.T, repo *TestRepository, data []byte) TestHash {
+ blob := &TestBlob{Data: data}
id, err := repo.WriteLooseObject(blob)
if err != nil {
t.Fatalf("WriteLooseObject: %v", err)
@@ -23,7 +23,7 @@ func writeLooseBlob(t *testing.T, repo *Repository, data []byte) Hash {
func TestOpenRepositoryAndLooseRead(t *testing.T) {
root := t.TempDir()
- repo, err := OpenRepository(root, testHashSize)
+ repo, err := OpenRepository[testHashType](root)
if err != nil {
t.Fatalf("OpenRepository error: %v", err)
}
@@ -34,7 +34,7 @@ func TestOpenRepositoryAndLooseRead(t *testing.T) {
if err != nil {
t.Fatalf("looseRead error: %v", err)
}
- blob, ok := obj.(*Blob)
+ blob, ok := obj.(*TestBlob)
if !ok {
t.Fatalf("expected Blob, got %T", obj)
}
@@ -45,7 +45,7 @@ func TestOpenRepositoryAndLooseRead(t *testing.T) {
func TestResolveRefLooseAndPacked(t *testing.T) {
root := t.TempDir()
- repo, err := OpenRepository(root, testHashSize)
+ repo, err := OpenRepository[testHashType](root)
if err != nil {
t.Fatalf("OpenRepository error: %v", err)
}
@@ -56,7 +56,7 @@ func TestResolveRefLooseAndPacked(t *testing.T) {
if err := os.MkdirAll(loosePath, 0o755); err != nil {
t.Fatalf("mkdir refs: %v", err)
}
- if err := os.WriteFile(filepath.Join(loosePath, "master"), []byte(looseID.StringWithSize(testHashSize)+"\n"), 0o644); err != nil {
+ if err := os.WriteFile(filepath.Join(loosePath, "master"), []byte(looseID.String()+"\n"), 0o644); err != nil {
t.Fatalf("write ref: %v", err)
}
id, err := repo.ResolveRef("refs/heads/master")
@@ -65,7 +65,7 @@ func TestResolveRefLooseAndPacked(t *testing.T) {
}
packedID := hashWithByte(0xb0)
- packed := fmt.Sprintf("%s refs/tags/v1\n", packedID.StringWithSize(testHashSize))
+ packed := fmt.Sprintf("%s refs/tags/v1\n", packedID.String())
if err := os.WriteFile(filepath.Join(root, "packed-refs"), []byte(packed), 0o644); err != nil {
t.Fatalf("write packed refs: %v", err)
}
@@ -85,7 +85,7 @@ func TestResolveRefLooseAndPacked(t *testing.T) {
func TestResolveHEAD(t *testing.T) {
root := t.TempDir()
- repo, err := OpenRepository(root, testHashSize)
+ repo, err := OpenRepository[testHashType](root)
if err != nil {
t.Fatalf("OpenRepository error: %v", err)
}
@@ -110,7 +110,7 @@ func TestResolveHEAD(t *testing.T) {
func TestReadObjectTypeSizeLoose(t *testing.T) {
t.Parallel()
root := t.TempDir()
- repo, err := OpenRepository(root, testHashSize)
+ repo, err := OpenRepository[testHashType](root)
if err != nil {
t.Fatalf("OpenRepository error: %v", err)
}
@@ -142,7 +142,7 @@ func TestReadObjectTypeSizePackedObjects(t *testing.T) {
}
ids := writeTestPack(t, root, "pack-basic", objs)
- repo, err := OpenRepository(root, testHashSize)
+ repo, err := OpenRepository[testHashType](root)
if err != nil {
t.Fatalf("OpenRepository error: %v", err)
}
@@ -169,7 +169,7 @@ func TestReadObjectTypeSizePackRefDeltaLooseBase(t *testing.T) {
t.Parallel()
root := t.TempDir()
- repo, err := OpenRepository(root, testHashSize)
+ repo, err := OpenRepository[testHashType](root)
if err != nil {
t.Fatalf("OpenRepository error: %v", err)
}
@@ -200,14 +200,14 @@ func TestReadObjectTypeSizePackRefDeltaLooseBase(t *testing.T) {
func TestWriteLooseObjectAllTypes(t *testing.T) {
root := t.TempDir()
- repo, err := OpenRepository(root, testHashSize)
+ repo, err := OpenRepository[testHashType](root)
if err != nil {
t.Fatalf("OpenRepository error: %v", err)
}
t.Cleanup(func() { _ = repo.Close() })
// Blob
- blob := &Blob{Data: []byte("test blob data")}
+ blob := &TestBlob{Data: []byte("test blob data")}
blobID, err := repo.WriteLooseObject(blob)
if err != nil {
t.Fatalf("WriteLooseObject Blob error: %v", err)
@@ -216,15 +216,15 @@ func TestWriteLooseObjectAllTypes(t *testing.T) {
if err != nil {
t.Fatalf("ReadObject Blob error: %v", err)
}
- if rb, ok := readBlob.(*Blob); !ok {
+ if rb, ok := readBlob.(*TestBlob); !ok {
t.Fatalf("expected Blob, got %T", readBlob)
} else if string(rb.Data) != "test blob data" {
t.Fatalf("blob data mismatch: %q", rb.Data)
}
// Tree
- tree := &Tree{
- Entries: []TreeEntry{
+ tree := &TestTree{
+ Entries: []TestTreeEntry{
{Mode: 0100644, Name: []byte("file.txt"), ID: blobID},
},
}
@@ -236,14 +236,14 @@ func TestWriteLooseObjectAllTypes(t *testing.T) {
if err != nil {
t.Fatalf("ReadObject Tree error: %v", err)
}
- if rt, ok := readTree.(*Tree); !ok {
+ if rt, ok := readTree.(*TestTree); !ok {
t.Fatalf("expected Tree, got %T", readTree)
} else if len(rt.Entries) != 1 {
t.Fatalf("tree entries mismatch: %d", len(rt.Entries))
}
// Commit
- commit := &Commit{
+ commit := &TestCommit{
Tree: treeID,
Author: Ident{
Name: []byte("Test Author"),
@@ -267,14 +267,14 @@ func TestWriteLooseObjectAllTypes(t *testing.T) {
if err != nil {
t.Fatalf("ReadObject Commit error: %v", err)
}
- if rc, ok := readCommit.(*Commit); !ok {
+ if rc, ok := readCommit.(*TestCommit); !ok {
t.Fatalf("expected Commit, got %T", readCommit)
} else if rc.Tree != treeID {
t.Fatalf("commit tree mismatch")
}
// Tag
- tag := &Tag{
+ tag := &TestTag{
Target: commitID,
TargetType: ObjCommit,
Name: []byte("v1.0.0"),
@@ -294,7 +294,7 @@ func TestWriteLooseObjectAllTypes(t *testing.T) {
if err != nil {
t.Fatalf("ReadObject Tag error: %v", err)
}
- if rtag, ok := readTag.(*Tag); !ok {
+ if rtag, ok := readTag.(*TestTag); !ok {
t.Fatalf("expected Tag, got %T", readTag)
} else if rtag.Target != commitID {
t.Fatalf("tag target mismatch")
@@ -314,11 +314,11 @@ type testPackObject struct {
body []byte
encoding packObjectEncoding
baseIndex int
- baseHash Hash
+ baseHash TestHash
baseBody []byte
}
-func writeTestPack(t *testing.T, root, name string, objs []testPackObject) []Hash {
+func writeTestPack(t *testing.T, root, name string, objs []testPackObject) []TestHash {
t.Helper()
packDir := filepath.Join(root, "objects", "pack")
err := os.MkdirAll(packDir, 0o750)
@@ -343,7 +343,7 @@ func writeTestPack(t *testing.T, root, name string, objs []testPackObject) []Has
}
offsets := make([]uint64, len(objs))
- ids := make([]Hash, len(objs))
+ ids := make([]TestHash, len(objs))
for i, obj := range objs {
offset := buf.Len()
@@ -358,7 +358,7 @@ func writeTestPack(t *testing.T, root, name string, objs []testPackObject) []Has
raw := make([]byte, len(header)+len(obj.body))
copy(raw, header)
copy(raw[len(header):], obj.body)
- ids[i] = computeRawHash(raw, testHashSize)
+ ids[i] = computeRawHash[testHashType](raw)
switch obj.encoding {
case packEncodingFull:
@@ -375,7 +375,7 @@ func writeTestPack(t *testing.T, root, name string, objs []testPackObject) []Has
delta := buildInsertOnlyDelta(len(baseBody), obj.body)
buf.Write(compressBytes(t, delta))
case packEncodingRefDelta:
- if obj.baseHash == (Hash{}) {
+ if obj.baseHash == (TestHash{}) {
t.Fatalf("ref delta %d missing base hash", i)
}
baseBody := obj.baseBody
@@ -383,7 +383,7 @@ func writeTestPack(t *testing.T, root, name string, objs []testPackObject) []Has
t.Fatalf("ref delta %d missing base body", i)
}
buf.Write(encodePackHeader(ObjRefDelta, len(obj.body)))
- buf.Write(obj.baseHash[:])
+ buf.Write(obj.baseHash.Slice())
delta := buildInsertOnlyDelta(len(baseBody), obj.body)
buf.Write(compressBytes(t, delta))
default:
@@ -392,8 +392,8 @@ func writeTestPack(t *testing.T, root, name string, objs []testPackObject) []Has
}
packContent := append([]byte(nil), buf.Bytes()...)
- packChecksum := computeRawHash(packContent, testHashSize)
- buf.Write(packChecksum[:testHashSize])
+ packChecksum := computeRawHash[testHashType](packContent)
+ buf.Write(packChecksum.Slice()[:testHashSize])
packBytes := buf.Bytes()
packPath := filepath.Join(packDir, name+".pack")
@@ -406,10 +406,10 @@ func writeTestPack(t *testing.T, root, name string, objs []testPackObject) []Has
return ids
}
-func writeTestPackIndex(t *testing.T, packDir, name string, ids []Hash, offsets []uint64, packChecksum Hash) {
+func writeTestPackIndex(t *testing.T, packDir, name string, ids []TestHash, offsets []uint64, packChecksum TestHash) {
t.Helper()
type idxEntry struct {
- id Hash
+ id TestHash
offset uint64
}
entries := make([]idxEntry, len(ids))
@@ -417,7 +417,7 @@ func writeTestPackIndex(t *testing.T, packDir, name string, ids []Hash, offsets
entries[i] = idxEntry{id: ids[i], offset: offsets[i]}
}
sort.Slice(entries, func(i, j int) bool {
- return bytes.Compare(entries[i].id[:], entries[j].id[:]) < 0
+ return bytes.Compare(entries[i].id.Slice(), entries[j].id.Slice()) < 0
})
var buf bytes.Buffer
@@ -432,7 +432,8 @@ func writeTestPackIndex(t *testing.T, packDir, name string, ids []Hash, offsets
var fanout [256]uint32
for _, entry := range entries {
- first := int(entry.id[0])
+ entrySlice := entry.id.Slice()
+ first := int(entrySlice[0])
for i := first; i < len(fanout); i++ {
fanout[i]++
}
@@ -445,7 +446,7 @@ func writeTestPackIndex(t *testing.T, packDir, name string, ids []Hash, offsets
}
for _, entry := range entries {
- buf.Write(entry.id[:testHashSize])
+ buf.Write(entry.id.Slice()[:testHashSize])
}
buf.Write(make([]byte, len(entries)*4))
@@ -460,9 +461,9 @@ func writeTestPackIndex(t *testing.T, packDir, name string, ids []Hash, offsets
}
idxData := append([]byte(nil), buf.Bytes()...)
- idxChecksum := computeRawHash(idxData, testHashSize)
- buf.Write(packChecksum[:testHashSize])
- buf.Write(idxChecksum[:testHashSize])
+ idxChecksum := computeRawHash[testHashType](idxData)
+ buf.Write(packChecksum.Slice()[:testHashSize])
+ buf.Write(idxChecksum.Slice()[:testHashSize])
idxPath := filepath.Join(packDir, name+".idx")
err = os.WriteFile(idxPath, buf.Bytes(), 0o600)