Add Garden accessor methods for manifest and blob store access.
Expose GetManifest, BlobExists, ReadBlob, WriteBlob, and ReplaceManifest on *Garden to support future gRPC and higher-level operations without breaking encapsulation. Includes 5 unit tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -98,6 +98,35 @@ func (g *Garden) SetClock(c clockwork.Clock) {
|
|||||||
g.clock = c
|
g.clock = c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetManifest returns the current manifest.
|
||||||
|
func (g *Garden) GetManifest() *manifest.Manifest {
|
||||||
|
return g.manifest
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlobExists reports whether a blob with the given hash exists in the store.
|
||||||
|
func (g *Garden) BlobExists(hash string) bool {
|
||||||
|
return g.store.Exists(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadBlob returns the contents of the blob with the given hash.
|
||||||
|
func (g *Garden) ReadBlob(hash string) ([]byte, error) {
|
||||||
|
return g.store.Read(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteBlob writes data to the blob store and returns the hash.
|
||||||
|
func (g *Garden) WriteBlob(data []byte) (string, error) {
|
||||||
|
return g.store.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceManifest atomically replaces the current manifest.
|
||||||
|
func (g *Garden) ReplaceManifest(m *manifest.Manifest) error {
|
||||||
|
if err := m.Save(g.manifestPath); err != nil {
|
||||||
|
return fmt.Errorf("saving manifest: %w", err)
|
||||||
|
}
|
||||||
|
g.manifest = m
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Add tracks new files, directories, or symlinks. Each path is resolved
|
// Add tracks new files, directories, or symlinks. Each path is resolved
|
||||||
// to an absolute path, inspected for its type, and added to the manifest.
|
// to an absolute path, inspected for its type, and added to the manifest.
|
||||||
// Regular files are hashed and stored in the blob store.
|
// Regular files are hashed and stored in the blob store.
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package garden
|
package garden
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/kisom/sgard/manifest"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestInitCreatesStructure(t *testing.T) {
|
func TestInitCreatesStructure(t *testing.T) {
|
||||||
@@ -619,6 +623,161 @@ func TestRestoreConfirmSkips(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetManifest(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
repoDir := filepath.Join(root, "repo")
|
||||||
|
|
||||||
|
g, err := Init(repoDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testFile := filepath.Join(root, "testfile")
|
||||||
|
if err := os.WriteFile(testFile, []byte("hello"), 0o644); err != nil {
|
||||||
|
t.Fatalf("writing test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.Add([]string{testFile}); err != nil {
|
||||||
|
t.Fatalf("Add: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := g.GetManifest()
|
||||||
|
if m == nil {
|
||||||
|
t.Fatal("GetManifest returned nil")
|
||||||
|
}
|
||||||
|
if len(m.Files) != 1 {
|
||||||
|
t.Errorf("expected 1 entry, got %d", len(m.Files))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlobExists(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
repoDir := filepath.Join(root, "repo")
|
||||||
|
|
||||||
|
g, err := Init(repoDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testFile := filepath.Join(root, "testfile")
|
||||||
|
if err := os.WriteFile(testFile, []byte("blob exists test"), 0o644); err != nil {
|
||||||
|
t.Fatalf("writing test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.Add([]string{testFile}); err != nil {
|
||||||
|
t.Fatalf("Add: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := g.GetManifest().Files[0].Hash
|
||||||
|
if !g.BlobExists(hash) {
|
||||||
|
t.Error("BlobExists returned false for a stored blob")
|
||||||
|
}
|
||||||
|
if g.BlobExists("0000000000000000000000000000000000000000000000000000000000000000") {
|
||||||
|
t.Error("BlobExists returned true for a fake hash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadBlob(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
repoDir := filepath.Join(root, "repo")
|
||||||
|
|
||||||
|
g, err := Init(repoDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
content := []byte("read blob test content")
|
||||||
|
testFile := filepath.Join(root, "testfile")
|
||||||
|
if err := os.WriteFile(testFile, content, 0o644); err != nil {
|
||||||
|
t.Fatalf("writing test file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.Add([]string{testFile}); err != nil {
|
||||||
|
t.Fatalf("Add: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := g.GetManifest().Files[0].Hash
|
||||||
|
got, err := g.ReadBlob(hash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadBlob: %v", err)
|
||||||
|
}
|
||||||
|
if string(got) != string(content) {
|
||||||
|
t.Errorf("ReadBlob content = %q, want %q", got, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteBlob(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
repoDir := filepath.Join(root, "repo")
|
||||||
|
|
||||||
|
g, err := Init(repoDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := []byte("write blob test data")
|
||||||
|
hash, err := g.WriteBlob(data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("WriteBlob: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the hash is correct SHA-256.
|
||||||
|
sum := sha256.Sum256(data)
|
||||||
|
wantHash := hex.EncodeToString(sum[:])
|
||||||
|
if hash != wantHash {
|
||||||
|
t.Errorf("WriteBlob hash = %s, want %s", hash, wantHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !g.BlobExists(hash) {
|
||||||
|
t.Error("BlobExists returned false after WriteBlob")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplaceManifest(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
repoDir := filepath.Join(root, "repo")
|
||||||
|
|
||||||
|
g, err := Init(repoDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Init: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new manifest with a custom entry.
|
||||||
|
newManifest := manifest.New()
|
||||||
|
newManifest.Files = append(newManifest.Files, manifest.Entry{
|
||||||
|
Path: "~/replaced-file",
|
||||||
|
Type: "file",
|
||||||
|
Hash: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
Mode: "0644",
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := g.ReplaceManifest(newManifest); err != nil {
|
||||||
|
t.Fatalf("ReplaceManifest: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify in-memory manifest was updated.
|
||||||
|
m := g.GetManifest()
|
||||||
|
if len(m.Files) != 1 {
|
||||||
|
t.Fatalf("expected 1 entry, got %d", len(m.Files))
|
||||||
|
}
|
||||||
|
if m.Files[0].Path != "~/replaced-file" {
|
||||||
|
t.Errorf("expected path ~/replaced-file, got %s", m.Files[0].Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify persistence by re-opening.
|
||||||
|
g2, err := Open(repoDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("re-Open: %v", err)
|
||||||
|
}
|
||||||
|
m2 := g2.GetManifest()
|
||||||
|
if len(m2.Files) != 1 {
|
||||||
|
t.Fatalf("persisted manifest has %d entries, want 1", len(m2.Files))
|
||||||
|
}
|
||||||
|
if m2.Files[0].Path != "~/replaced-file" {
|
||||||
|
t.Errorf("persisted entry path = %s, want ~/replaced-file", m2.Files[0].Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExpandTildePath(t *testing.T) {
|
func TestExpandTildePath(t *testing.T) {
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user