Skip to content
zit Artifact [3915583b18]
Login

Artifact [3915583b18]

Artifact 3915583b18358853fad592b3ee1fbbfc994836b02704874d870c2ec86f1694f3:


//! 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 (#167) — 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,
    /// #63/L1 — 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,
        };
        // #65 — 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);
            items.appendAssumeCapacity(.{ .hash = digests.items[digests.items.len - 1][0..raw.len], .bytes = node.bytes, .base = parentHint(self, node.bytes) });
        }
        self.log.putArtifactBatch(items.items) catch |err| switch (err) {
            error.OutOfMemory => return error.OutOfMemory,
            else => return error.StorageFailure,
        };
        // #65 — 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 {};
        }
    }
};

/// zit#169: a manifest's primary parent, already in the log, is the delta base
/// that pays best — the same hint `zit commit` gives. Anything without a P
/// card (a sub-manifest, a blob) 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;
}