//! A `TreeStore` over a `LogStore`: the seam through which the sub-manifest
//! editor reads and writes tree nodes in an append-only log. Shared by the
//! Git importer and the fossil converter — both build every check-in's
//! tree incrementally from its parent's, and both want the same write-through
//! plaintext memo over the nodes that consecutive commits share.
const std = @import("std");
const artifact = @import("artifact.zig");
const logstore = @import("logstore.zig");
const editor = @import("submanifest_editor.zig");
pub const LogTreeStore = struct {
gpa: std.mem.Allocator,
log: *logstore.LogStore,
/// Plaintext memo over the staging log's tree nodes. Every
/// commit's tree build AND flatten re-load the same content-addressed,
/// immutable nodes the previous commit just loaded; without a memo each
/// load is a frame read, an inflate, and a delta-chain assemble (the
/// profile put those, plus their worker-thread readv time, at over half
/// the import). `memoize_undeltaed` because staging nodes are routinely
/// raw/zlib top-level frames, exactly the class the delta-base-only
/// default declines to retain.
cache: logstore.PlaintextCache = .{ .memoize_undeltaed = true },
pub fn deinitCache(self: *LogTreeStore) void {
self.cache.deinit(self.gpa);
}
pub fn treeStore(self: *LogTreeStore) editor.TreeStore {
return .{ .ctx = self, .load_fn = load, .put_fn = put, .put_batch_fn = putBatch };
}
fn load(context: *anyopaque, allocator: std.mem.Allocator, name: []const u8) editor.StoreError!editor.LoadedBytes {
const self: *LogTreeStore = @ptrCast(@alignCast(context));
var digest: [32]u8 = undefined;
const raw = rawHashName(name, &digest) orelse return error.MissingNode;
const rid = self.log.lookup(raw) orelse return error.MissingNode;
// Through the memo, on the STORE's allocator: the cache's resident
// copies must outlive any per-call arena the editor hands in, and
// `LoadedBytes` frees by its recorded allocator, so returning
// `self.gpa`-owned bytes keeps the contract whichever allocator the
// caller offered.
_ = allocator;
const bytes = self.log.contentCached(self.gpa, rid, &self.cache) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.StorageFailure,
};
return .{ .bytes = bytes orelse return error.MissingNode, .allocator = self.gpa };
}
fn put(context: *anyopaque, name: []const u8, bytes: []const u8) editor.StoreError!void {
const self: *LogTreeStore = @ptrCast(@alignCast(context));
// Algorithm-agnostic: `verify` picks the digest from the name width.
if (!artifact.verify(name, bytes)) return error.StorageFailure;
var digest: [32]u8 = undefined;
const raw = rawHashName(name, &digest) orelse return error.StorageFailure;
const rid = self.log.putArtifactPreferring(raw, bytes, parentHint(self, bytes)) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.StorageFailure,
};
// Write-through: the very next flatten reads this node straight
// back; priming turns that read-after-write miss (a delta-chain
// reassembly) into a memcpy.
self.cache.put(self.gpa, self.log.offsetOf(rid), bytes) catch {};
}
fn putBatch(context: *anyopaque, nodes: []const editor.StoreBatchNode) editor.StoreError!void {
const self: *LogTreeStore = @ptrCast(@alignCast(context));
var digests: std.ArrayList([32]u8) = .empty;
defer digests.deinit(self.gpa);
var items: std.ArrayList(logstore.LogStore.ArtifactBatchItem) = .empty;
defer items.deinit(self.gpa);
digests.ensureTotalCapacity(self.gpa, nodes.len) catch return error.OutOfMemory;
items.ensureTotalCapacity(self.gpa, nodes.len) catch return error.OutOfMemory;
for (nodes) |node| {
// Algorithm-agnostic: `verify` picks the digest from the name width.
if (!artifact.verify(node.name, node.bytes)) return error.StorageFailure;
var digest: [32]u8 = undefined;
const raw = rawHashName(node.name, &digest) orelse return error.StorageFailure;
digests.appendAssumeCapacity(digest);
const hint = if (node.base) |base| nameHint(self, base) orelse parentHint(self, node.bytes) else parentHint(self, node.bytes);
items.appendAssumeCapacity(.{ .hash = digests.items[digests.items.len - 1][0..raw.len], .bytes = node.bytes, .base = hint });
}
self.log.putArtifactBatch(items.items) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return error.StorageFailure,
};
// Write-through for the batch path too; see `put`.
for (nodes) |node| {
var digest: [32]u8 = undefined;
const raw = rawHashName(node.name, &digest) orelse continue;
const rid = self.log.lookup(raw) orelse continue;
self.cache.put(self.gpa, self.log.offsetOf(rid), node.bytes) catch {};
}
}
};
/// The rid of the node a writer named as the one this node replaces, when
/// the log holds it.
fn nameHint(self: *LogTreeStore, name: []const u8) ?logstore.Rid {
var digest: [32]u8 = undefined;
const raw = rawHashName(name, &digest) orelse return null;
return self.log.lookup(raw);
}
/// A manifest's primary parent, already in the log, is the delta base
/// that pays best — the same hint `zit commit` gives. A sub-manifest gets
/// the node it replaces when its writer names one (`StoreBatchNode.base`);
/// anything else falls back to the recent window.
fn parentHint(self: *LogTreeStore, bytes: []const u8) ?logstore.Rid {
var lines = std.mem.splitScalar(u8, bytes, '\n');
while (lines.next()) |line| {
if (line.len < 2 or line[0] != 'P' or line[1] != ' ') continue;
var toks = std.mem.tokenizeScalar(u8, line[2..], ' ');
const first = toks.next() orelse return null;
var digest: [32]u8 = undefined;
const raw = rawHashName(first, &digest) orelse return null;
return self.log.lookup(raw);
}
return null;
}
pub fn rawHashName(name: []const u8, buffer: *[32]u8) ?[]const u8 {
if (name.len != 40 and name.len != 64) return null;
const raw = buffer[0 .. name.len / 2];
_ = std.fmt.hexToBytes(raw, name) catch return null;
return raw;
}