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