diff options
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | hash.go | 98 | ||||
| -rw-r--r-- | hash_sha1_test.go | 12 | ||||
| -rw-r--r-- | hash_sha256_test.go | 12 | ||||
| -rw-r--r-- | hash_test.go | 21 | ||||
| -rw-r--r-- | loose.go | 52 | ||||
| -rw-r--r-- | obj.go | 29 | ||||
| -rw-r--r-- | obj_blob.go | 13 | ||||
| -rw-r--r-- | obj_commit.go | 28 | ||||
| -rw-r--r-- | obj_tag.go | 22 | ||||
| -rw-r--r-- | obj_tree.go | 36 | ||||
| -rw-r--r-- | objects_test.go | 43 | ||||
| -rw-r--r-- | pack_idx.go | 41 | ||||
| -rw-r--r-- | pack_midx.go | 44 | ||||
| -rw-r--r-- | pack_pack.go | 32 | ||||
| -rw-r--r-- | pack_test.go | 18 | ||||
| -rw-r--r-- | refs.go | 33 | ||||
| -rw-r--r-- | repo.go | 35 | ||||
| -rw-r--r-- | repo_test.go | 75 |
19 files changed, 284 insertions, 363 deletions
@@ -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. @@ -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") } } @@ -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 @@ -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 @@ -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) @@ -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 @@ -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) |
