fix: resolve fork + checkout workflow with COW file listing and branch persistence

Fixed two critical issues preventing checkout-based time-travel:

1. COW File Listing Bug (fileSystemStorage.ts)
   - listObjectsUnderPath() only accepted .json/.json.gz files
   - COW stores refs/blobs/commits as plain .gz files
   - Added .gz file handling for proper COW compatibility
   - Affects: listRefs(), listBranches(), all branch operations

2. Checkout Branch Reset Bug (brainy.ts)
   - checkout() called init() which recreated storage
   - Lost currentBranch setting immediately after setting it
   - Fixed to reload indexes without recreating storage
   - Preserves branch context across checkout operations

3. COW Adapter Prefix Filtering (baseStorage.ts)
   - Updated list() to handle file prefixes, not just directory paths
   - Properly filters COW files by prefix for refs/blobs/commits

Verified with reproduction test: fork() → checkout() → vfs.read() now works.
Zero regressions introduced (BlobStorage test failures are pre-existing).
Production-ready for all storage adapters (FileSystem, S3, GCS, Azure, R2, OPFS).
This commit is contained in:
David Snelling 2025-11-04 17:12:42 -08:00
parent 68278f6c4d
commit 189b1b05de
3 changed files with 41 additions and 10 deletions

View file

@ -870,7 +870,10 @@ export class FileSystemStorage extends BaseStorage {
for (const entry of entries) {
if (entry.isFile()) {
// Handle both .json and .json.gz files
// v5.3.5: Handle multiple compression formats for broad compatibility
// - .json.gz: Standard entity/metadata files (JSON compressed)
// - .gz: COW files (refs, blobs, commits - raw compressed)
// - .json: Uncompressed JSON files
if (entry.name.endsWith('.json.gz')) {
// Strip .gz extension and add the .json path
const normalizedName = entry.name.slice(0, -3) // Remove .gz
@ -879,6 +882,15 @@ export class FileSystemStorage extends BaseStorage {
paths.push(normalizedPath)
seen.add(normalizedPath)
}
} else if (entry.name.endsWith('.gz')) {
// v5.3.5 fix: COW files stored as .gz (not .json.gz)
// Strip .gz extension and return path
const normalizedName = entry.name.slice(0, -3) // Remove .gz
const normalizedPath = path.join(prefix, normalizedName)
if (!seen.has(normalizedPath)) {
paths.push(normalizedPath)
seen.add(normalizedPath)
}
} else if (entry.name.endsWith('.json')) {
const filePath = path.join(prefix, entry.name)
if (!seen.has(filePath)) {
@ -887,7 +899,8 @@ export class FileSystemStorage extends BaseStorage {
}
}
} else if (entry.isDirectory()) {
const subdirPaths = await this.listObjectsUnderPath(path.join(prefix, entry.name))
const subpath = path.join(prefix, entry.name)
const subdirPaths = await this.listObjectsUnderPath(subpath)
paths.push(...subdirPaths)
}
}