aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Runxi Yu2025-11-15 00:00:00 +0000
committerGravatar Runxi Yu2025-11-15 00:00:00 +0000
commitb0e7f570b7b0932044ef44d0aba6c49ce8060b08 (patch)
treeeec077c63fbca890348859ec180cadd5495f3add
parentREADME: Fix typo (diff)
signature
Loose object writing draft
-rw-r--r--README.md6
-rw-r--r--loose.go46
-rw-r--r--repo_test.go142
3 files changed, 163 insertions, 31 deletions
diff --git a/README.md b/README.md
index df86cd85..95d93532 100644
--- a/README.md
+++ b/README.md
@@ -16,9 +16,9 @@ used as a library.
## Features
-Currently, furgit is very basic; it supports reading objects from loose objects
-and packfiles. There is some infrastructure for writing loose objects and
-packfiles in the tests but they need to be refactored.
+Currently, furgit is very basic; it supports reading and writing loose objects
+and reading from packfiles. There is some infrastructure for writing packfiles
+in the tests but they need to be refactored.
We intend for repository objects to be freely usable across goroutines, which
may enable long-running applications such as forges to keep a pool of recently
diff --git a/loose.go b/loose.go
index c32311f5..0951bc5f 100644
--- a/loose.go
+++ b/loose.go
@@ -153,3 +153,49 @@ func objTypeFromName(name string) (ObjType, error) {
return ObjInvalid, ErrInvalidObject
}
}
+
+// WriteLooseObject writes an object to the repository as a loose object.
+func (repo *Repository) WriteLooseObject(obj Object) (Hash, error) {
+ var raw []byte
+ var err error
+
+ switch o := obj.(type) {
+ case *Blob:
+ raw, err = o.Serialize()
+ case *Tree:
+ raw, err = o.Serialize()
+ case *Commit:
+ raw, err = o.Serialize()
+ case *Tag:
+ raw, err = o.Serialize()
+ default:
+ return Hash{}, fmt.Errorf("furgit: unsupported object type for writing: %T", obj)
+ }
+ // TODO: Consider adding serialize to the interface?
+
+ if err != nil {
+ return Hash{}, err
+ }
+
+ id := computeRawHash(raw)
+ path := repo.repoPath(loosePath(id))
+
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ return Hash{}, err
+ }
+
+ var buf bytes.Buffer
+ zw := zlib.NewWriter(&buf)
+ if _, err := zw.Write(raw); err != nil {
+ return Hash{}, err
+ }
+ if err := zw.Close(); err != nil {
+ return Hash{}, err
+ }
+
+ if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil {
+ return Hash{}, err
+ }
+
+ return id, nil
+}
diff --git a/repo_test.go b/repo_test.go
index 344f817f..4b46d49a 100644
--- a/repo_test.go
+++ b/repo_test.go
@@ -2,7 +2,6 @@ package furgit
import (
"bytes"
- "compress/zlib"
"encoding/binary"
"errors"
"fmt"
@@ -13,27 +12,11 @@ import (
"testing"
)
-func writeLooseBlob(t *testing.T, root string, data []byte) Hash {
- header, err := headerForType(ObjBlob, 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("headerForType: %v", err)
- }
- raw := append(append([]byte(nil), header...), data...)
- id := computeRawHash(raw)
- path := filepath.Join(root, loosePath(id))
- if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
- t.Fatalf("mkdir for loose object: %v", err)
- }
- var buf bytes.Buffer
- zw := zlib.NewWriter(&buf)
- if _, err := zw.Write(raw); err != nil {
- t.Fatalf("compress: %v", err)
- }
- if err := zw.Close(); err != nil {
- t.Fatalf("close zlib: %v", err)
- }
- if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil {
- t.Fatalf("write loose object: %v", err)
+ t.Fatalf("WriteLooseObject: %v", err)
}
return id
}
@@ -46,7 +29,7 @@ func TestOpenRepositoryAndLooseRead(t *testing.T) {
}
t.Cleanup(func() { _ = repo.Close() })
- id := writeLooseBlob(t, root, []byte("loose blob payload"))
+ id := writeLooseBlob(t, repo, []byte("loose blob payload"))
obj, err := repo.looseRead(id)
if err != nil {
t.Fatalf("looseRead error: %v", err)
@@ -134,7 +117,7 @@ func TestReadObjectTypeSizeLoose(t *testing.T) {
t.Cleanup(func() { _ = repo.Close() })
data := []byte("header-only read")
- id := writeLooseBlob(t, root, data)
+ id := writeLooseBlob(t, repo, data)
ty, size, err := repo.ReadObjectTypeSize(id)
if err != nil {
t.Fatalf("ReadObjectTypeSize loose error: %v", err)
@@ -186,8 +169,14 @@ func TestReadObjectTypeSizePackRefDeltaLooseBase(t *testing.T) {
t.Parallel()
root := t.TempDir()
+ repo, err := OpenRepository(root)
+ if err != nil {
+ t.Fatalf("OpenRepository error: %v", err)
+ }
+ t.Cleanup(func() { _ = repo.Close() })
+
looseBody := []byte("loose base for ref delta")
- baseID := writeLooseBlob(t, root, looseBody)
+ baseID := writeLooseBlob(t, repo, looseBody)
objs := []testPackObject{
{
@@ -200,18 +189,115 @@ func TestReadObjectTypeSizePackRefDeltaLooseBase(t *testing.T) {
}
ids := writeTestPack(t, root, "pack-ref", objs)
+ ty, size, err := repo.ReadObjectTypeSize(ids[0])
+ if err != nil {
+ t.Fatalf("ReadObjectTypeSize ref delta error: %v", err)
+ }
+ if ty != ObjBlob || size != int64(len(objs[0].body)) {
+ t.Fatalf("unexpected ref delta metadata ty=%d size=%d", ty, size)
+ }
+}
+
+func TestWriteLooseObjectAllTypes(t *testing.T) {
+ root := t.TempDir()
repo, err := OpenRepository(root)
if err != nil {
t.Fatalf("OpenRepository error: %v", err)
}
t.Cleanup(func() { _ = repo.Close() })
- ty, size, err := repo.ReadObjectTypeSize(ids[0])
+ // Blob
+ blob := &Blob{Data: []byte("test blob data")}
+ blobID, err := repo.WriteLooseObject(blob)
if err != nil {
- t.Fatalf("ReadObjectTypeSize ref delta error: %v", err)
+ t.Fatalf("WriteLooseObject Blob error: %v", err)
}
- if ty != ObjBlob || size != int64(len(objs[0].body)) {
- t.Fatalf("unexpected ref delta metadata ty=%d size=%d", ty, size)
+ readBlob, err := repo.ReadObject(blobID)
+ if err != nil {
+ t.Fatalf("ReadObject Blob error: %v", err)
+ }
+ 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 := &Tree{
+ Entries: []TreeEntry{
+ {Mode: 0100644, Name: []byte("file.txt"), ID: blobID},
+ },
+ }
+ treeID, err := repo.WriteLooseObject(tree)
+ if err != nil {
+ t.Fatalf("WriteLooseObject Tree error: %v", err)
+ }
+ readTree, err := repo.ReadObject(treeID)
+ if err != nil {
+ t.Fatalf("ReadObject Tree error: %v", err)
+ }
+ 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 := &Commit{
+ Tree: treeID,
+ Author: Ident{
+ Name: []byte("Test Author"),
+ Email: []byte("test@example.com"),
+ WhenUnix: 1700000000,
+ OffsetMinutes: 0,
+ },
+ Committer: Ident{
+ Name: []byte("Test Author"),
+ Email: []byte("test@example.com"),
+ WhenUnix: 1700000000,
+ OffsetMinutes: 0,
+ },
+ Message: []byte("Test commit message\n"),
+ }
+ commitID, err := repo.WriteLooseObject(commit)
+ if err != nil {
+ t.Fatalf("WriteLooseObject Commit error: %v", err)
+ }
+ readCommit, err := repo.ReadObject(commitID)
+ if err != nil {
+ t.Fatalf("ReadObject Commit error: %v", err)
+ }
+ 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 := &Tag{
+ Target: commitID,
+ TargetType: ObjCommit,
+ Name: []byte("v1.0.0"),
+ Tagger: &Ident{
+ Name: []byte("Test Tagger"),
+ Email: []byte("tagger@example.com"),
+ WhenUnix: 1700000000,
+ OffsetMinutes: 0,
+ },
+ Message: []byte("Test tag message\n"),
+ }
+ tagID, err := repo.WriteLooseObject(tag)
+ if err != nil {
+ t.Fatalf("WriteLooseObject Tag error: %v", err)
+ }
+ readTag, err := repo.ReadObject(tagID)
+ if err != nil {
+ t.Fatalf("ReadObject Tag error: %v", err)
+ }
+ if rtag, ok := readTag.(*Tag); !ok {
+ t.Fatalf("expected Tag, got %T", readTag)
+ } else if rtag.Target != commitID {
+ t.Fatalf("tag target mismatch")
}
}