╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │308 const state = State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; │ │309 │ │310 try commitEvents(.xit, repo_opts, state, ctx.io, ctx.allocator, ctx.ref, ctx.events, ctx.first_parent_oids); │ │284 - if (!try consumeInTransaction(.xit, repo_opts, state.readOnly(), &ctx.core.db, &moment, ctx.io, ctx.allocator, ctx.ref)) return error.CancelTransaction;│ │311 + if (!try consumeInTransaction(.xit, repo_opts, state, &ctx.core.db, &moment, ctx.io, ctx.allocator, ctx.ref)) return error.CancelTransaction; │ │312 try xit.undo.writeMessage(repo_opts, state, .{ .custom = "event" }); │ │313 │ │314 // fsync the chunk store, so any chunks written by the │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │458 .name = try arena.allocator().dupe(u8, name), │ │459 .oid = try arena.allocator().dupe(u8, &oid), │ │460 }; │ │461 +} │ │462 + │ │463 +fn commitIdentity(line: []const u8) ![]const u8 { │ │464 + const close = std.mem.indexOfScalar(u8, line, '>') orelse return error.AuthorNotFound;│ │465 + return line[0 .. close + 1]; │ │466 } │ │467 │ │468 // commit each event as a JSON commit message on `ref` through `state`, so a │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │489 const author = try std.fmt.allocPrint(allocator, "{s} <{s}>", .{ event.author.name, event.author.email }); │ │490 defer allocator.free(author); │ │491 │ │460 - // every event gets its own tree, with one entry when it carries a file │ │461 - // and no entries otherwise │ │492 + // every event gets a fresh tree │ │493 var tree = try obj.Tree.init(allocator); │ │494 defer tree.deinit(); │ │464 - if (event.blob) |blob| { │ │465 - var oid_bytes = [_]u8{0} ** hash.byteLen(repo_opts.hash); │ │466 - try obj.writeObject(repo_kind, repo_opts, state, io, blob.reader, .{ .kind = .blob, .size = blob.size }, &oid_bytes); │ │467 - try tree.addBlobEntry(.{ .content = .{ .unix_permission = 0o644, .object_type = .regular_file } }, blob.name, &oid_bytes); │ │495 + for (event.tree_entries) |entry| { │ │496 + switch (entry) { │ │497 + .blob => |blob| { │ │498 + var oid_bytes = [_]u8{0} ** hash.byteLen(repo_opts.hash); │ │499 + try obj.writeObject(repo_kind, repo_opts, state, io, blob.reader, .{ .kind = .blob, .size = blob.size }, &oid_bytes); │ │500 + try tree.addBlobEntry(.{ .content = .{ .unix_permission = 0o644, .object_type = .regular_file } }, blob.name, &oid_bytes);│ │501 + }, │ │502 + .tree => |nested| { │ │503 + var oid_bytes: [hash.byteLen(repo_opts.hash)]u8 = undefined; │ │504 + _ = try std.fmt.hexToBytes(&oid_bytes, nested.oid); │ │505 + if (!std.mem.eql(u8, nested.oid, &std.fmt.bytesToHex(oid_bytes, .lower))) return error.InvalidTreeObject; │ │506 + try tree.addTreeEntry(nested.name, &oid_bytes); │ │507 + }, │ │508 + } │ │509 } │ │510 │ │511 _ = try obj.writeCommit(repo_kind, repo_opts, state, io, allocator, .{ │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │553 // no-op consume must not cancel here, since the push shares the │ │554 // transaction and must commit regardless. │ │555 if (null == try rf.readRecur(.xit, repo_opts, state.readOnly(), ctx.io, .{ .ref = events_ref })) return; │ │515 - _ = try consumeInTransaction(.xit, repo_opts, state.readOnly(), &ctx.core.db, &moment, ctx.io, ctx.allocator, events_ref);│ │556 + _ = try consumeInTransaction(.xit, repo_opts, state, &ctx.core.db, &moment, ctx.io, ctx.allocator, events_ref); │ │557 } │ │558 }; │ │559 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │656 │ │657 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { │ │658 var moment = try DB.HashMap(.read_write).init(cursor.*); │ │618 - var repo_moment = try ctx.repo.core.latestMoment(); │ │619 - const read_state = rp.Repo(repo_kind, repo_opts).State(.read_only){ .core = &ctx.repo.core, .extra = .{ .moment = &repo_moment } }; │ │620 - if (!try consumeInTransaction(repo_kind, repo_opts, read_state, ctx.db, &moment, ctx.io, ctx.allocator, ctx.ref)) return error.CancelTransaction;│ │659 + const state = rp.Repo(repo_kind, repo_opts).State(.read_write){ .core = &ctx.repo.core, .extra = .{} }; │ │660 + if (!try consumeInTransaction(repo_kind, repo_opts, state, ctx.db, &moment, ctx.io, ctx.allocator, ctx.ref)) return error.CancelTransaction; │ │661 } │ │662 }; │ │663 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │677 } │ │678 │ │679 // consume the events on `ref` into the db `moment` writes to, returning false│ │640 -// if there was nothing to do. events are read from `read_state`'s repo, which│ │680 +// if there was nothing to do. events are read from `state`'s repo, which │ │681 // needn't be the repo backing the db — that's how a local (possibly │ │682 // git-backed) repo's events land in a standalone db. │ │683 pub fn consumeInTransaction( │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────╮ │684 comptime repo_kind: rp.RepoKind, │ │685 comptime repo_opts: rp.RepoOpts(repo_kind), │ │646 - read_state: rp.Repo(repo_kind, repo_opts).State(.read_only),│ │686 + state: rp.Repo(repo_kind, repo_opts).State(.read_write), │ │687 db: *EventDB(repo_opts.hash), │ │688 moment: *EventDB(repo_opts.hash).HashMap(.read_write), │ │689 io: std.Io, │ ╰─────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │691 ref: rf.Ref, │ │692 ) !bool { │ │693 const DB = EventDB(repo_opts.hash); │ │694 + const read_state = state.readOnly(); │ │695 │ │696 // the last_object_id represents the object id that was last consumed│ │697 var last_object_id_maybe: ?[hash.byteLen(repo_opts.hash)]u8 = null; │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │894 │ │895 const event_with_id = try EventWithId.fromString(&arena, message.items);│ │896 │ │856 - // get the id of the current event as bytes │ │857 - var current_event_id: [event_id_size]u8 = undefined; │ │858 - _ = try std.fmt.hexToBytes(&current_event_id, &event_with_id.id); │ │897 + const current_event_id = try parseEventId(&event_with_id.id); │ │898 │ │899 // wrap the payload into the record `consume` stores, with its │ │900 // commit-derived fields; on update, `consume` preserves the │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │944 }; │ │945 } else null; │ │946 try Attachment.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, record_maybe, &arena, &repo_event_oid); │ │947 + }, │ │948 + .patchrev => |event_maybe| { │ │949 + const record_maybe: ?PatchRev.Record = if (event_maybe) |event| blk: { │ │950 + const trees = try PatchRev.readTrees(repo_kind, repo_opts, read_state, io, allocator, &commit_object.content.commit.tree);│ │951 + const existing_maybe = try PatchRev.readById(DB, repo_opts.hash, haxy_moment.readOnly(), &arena, &current_event_id); │ │952 + │ │953 + var patch_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; │ │954 + if (existing_maybe) |existing| { │ │955 + if (existing.patch_oid.len != patch_oid.len) return error.InvalidPatch; │ │956 + @memcpy(&patch_oid, existing.patch_oid); │ │957 + } else { │ │958 + var base_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; │ │959 + if (event.base_oid.len != base_oid.len) return error.InvalidPatch; │ │960 + @memcpy(&base_oid, event.base_oid); │ │961 + var base_oid_bytes: [hash.byteLen(repo_opts.hash)]u8 = undefined; │ │962 + _ = try std.fmt.hexToBytes(&base_oid_bytes, &base_oid); │ │963 + if (!std.mem.eql(u8, &base_oid, &std.fmt.bytesToHex(base_oid_bytes, .lower))) return error.InvalidPatch; │ │964 + const derived_parent_oids = [_][hash.hexLen(repo_opts.hash)]u8{base_oid}; │ │965 + const author = try commitIdentity(commit_object.content.commit.metadata.author orelse return error.AuthorNotFound); │ │966 + const committer = try commitIdentity(commit_object.content.commit.metadata.committer orelse author); │ │967 + patch_oid = try obj.writeCommitWithoutRef(repo_kind, repo_opts, state, io, allocator, .{ │ │968 + .author = author, │ │969 + .committer = committer, │ │970 + .message = event.message, │ │971 + .parent_oids = &derived_parent_oids, │ │972 + .timestamp = commit_object.content.commit.metadata.timestamp, │ │973 + }, &trees.head); │ │974 + } │ │975 + │ │976 + break :blk .{ │ │977 + .event = event, │ │978 + .author_email = authorEmail(commit_object.content.commit.metadata.author orelse ""), │ │979 + .created_order = event_order, │ │980 + .base_tree_oid = &trees.base, │ │981 + .head_tree_oid = &trees.head, │ │982 + .patch_oid = &patch_oid, │ │983 + }; │ │984 + } else null; │ │985 + try PatchRev.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, record_maybe, &arena, &repo_event_oid); │ │986 + }, │ │987 + .patch => |event_maybe| { │ │988 + const record_maybe: ?Patch.Record = if (event_maybe) |event| .{ │ │989 + .event = event, │ │990 + .author_email = authorEmail(commit_object.content.commit.metadata.author orelse ""), │ │991 + .created_order = event_order, │ │992 + } else null; │ │993 + try Patch.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, record_maybe, &arena, &repo_event_oid); │ │994 }, │ │995 } │ │996 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯