╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │31 return rp.Repo(.xit, .{ .hash = hash_kind }).DB; │ │32 } │ │33 │ │34 -// fields derived from the commit rather than carried by the event payload │ │35 -pub fn derivedField(comptime field_name: []const u8) bool { │ │36 - return std.mem.eql(u8, field_name, "created_ts") or std.mem.eql(u8, field_name, "author_email");│ │37 -} │ │38 - │ │34 // the email between a commit author line's angle brackets, or null │ │35 pub fn authorEmail(author_line: []const u8) ?[]const u8 { │ │36 const open_bracket = std.mem.indexOfScalar(u8, author_line, '<') orelse return null; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────╮ │38 return author_line[open_bracket + 1 .. close_bracket]; │ │39 } │ │40 │ │41 +// build a `T` by copying its fields, by name, out of `source` │ │42 +pub fn project(comptime T: type, source: anytype) T { │ │43 + var result: T = undefined; │ │44 + inline for (std.meta.fields(T)) |field| { │ │45 + @field(result, field.name) = @field(source, field.name);│ │46 + } │ │47 + return result; │ │48 +} │ │49 + │ │50 pub const EventKind = enum { │ │51 user, │ │52 repo, │ ╰────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │595 var message: std.ArrayList(u8) = .empty; │ │596 try commit_object.readMessage(arena.allocator(), &message, .limited(max_event_size));│ │597 │ │594 - var event_with_id = try EventWithId.fromString(&arena, message.items); │ │598 + const event_with_id = try EventWithId.fromString(&arena, message.items); │ │599 │ │600 // get the id of the current event as bytes │ │601 var current_event_id: [event_id_size]u8 = undefined; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │602 _ = try std.fmt.hexToBytes(&current_event_id, &event_with_id.id); │ │603 │ │600 - // set the derived fields │ │604 + // wrap the payload into the record `consume` stores, with its │ │605 + // commit-derived fields; on update, `consume` preserves the │ │606 + // existing record's │ │607 + const created_ts = commit_object.content.commit.metadata.timestamp; │ │608 switch (event_with_id.event) { │ │602 - .issue => |*event_maybe| if (event_maybe.*) |*event| { │ │603 - event.created_ts = commit_object.content.commit.metadata.timestamp; │ │604 - event.author_email = authorEmail(commit_object.content.commit.metadata.author orelse ""); │ │609 + .user => |event_maybe| { │ │610 + const record_maybe: ?User.Record = if (event_maybe) |event| .{ .event = event, .created_ts = created_ts } else null;│ │611 + try User.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, record_maybe, &arena, &repo_event_oid); │ │612 }, │ │606 - inline else => |*event_maybe| if (event_maybe.*) |*event| { │ │607 - event.created_ts = commit_object.content.commit.metadata.timestamp; │ │613 + .repo => |event_maybe| { │ │614 + const record_maybe: ?Repo.Record = if (event_maybe) |event| .{ .event = event, .created_ts = created_ts } else null;│ │615 + try Repo.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, record_maybe, &arena, &repo_event_oid); │ │616 }, │ │609 - } │ │610 - │ │611 - switch (event_with_id.event) { │ │612 - inline else => |event_maybe| { │ │613 - const T = @typeInfo(@TypeOf(event_maybe)).optional.child; │ │614 - try T.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, event_maybe, &arena, &repo_event_oid); │ │617 + .issue => |event_maybe| { │ │618 + const record_maybe: ?Issue.Record = if (event_maybe) |event| .{ │ │619 + .event = event, │ │620 + .author_email = authorEmail(commit_object.content.commit.metadata.author orelse ""), │ │621 + .created_ts = created_ts, │ │622 + } else null; │ │623 + try Issue.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, record_maybe, &arena, &repo_event_oid); │ │624 }, │ │625 } │ │626 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │674 var field_oids: ?DB.SortedMap(.read_write) = null; │ │675 │ │676 inline for (std.meta.fields(T)) |field| { │ │668 - if (comptime !derivedField(field.name)) { │ │669 - const changed = if (existing_maybe) |existing| │ │670 - !fieldEqual(field.type, @field(existing, field.name), @field(event, field.name)) │ │671 - else │ │672 - true; │ │673 - if (changed) { │ │674 - if (field_oids == null) { │ │675 - field_oids = try DB.SortedMap(.read_write).init(try id_to_field_to_oid.putCursor(record_key));│ │676 - } │ │677 - if (field_oids) |map| try map.put(field.name, .{ .bytes = oid }); │ │677 + const changed = if (existing_maybe) |existing| │ │678 + !fieldEqual(field.type, @field(existing, field.name), @field(event, field.name)) │ │679 + else │ │680 + true; │ │681 + if (changed) { │ │682 + if (field_oids == null) { │ │683 + field_oids = try DB.SortedMap(.read_write).init(try id_to_field_to_oid.putCursor(record_key)); │ │684 } │ │685 + if (field_oids) |map| try map.put(field.name, .{ .bytes = oid }); │ │686 } │ │687 } │ │688 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────╮ │716 var merged = target; │ │717 │ │718 inline for (std.meta.fields(T), 0..) |field, i| { │ │712 - if (comptime !derivedField(field.name)) { │ │713 - const target_value = @field(target, field.name); │ │714 - const parent_value = @field(parent, field.name); │ │719 + const target_value = @field(target, field.name); │ │720 + const parent_value = @field(parent, field.name); │ │721 │ │716 - if (!fieldEqual(field.type, target_value, parent_value)) { │ │717 - outcome[i] = if (baseline_maybe) |baseline| blk: { │ │718 - const baseline_value = @field(baseline, field.name); │ │719 - if (fieldEqual(field.type, target_value, baseline_value)) { │ │720 - @field(merged, field.name) = parent_value; │ │721 - break :blk .parent; │ │722 - } │ │723 - // only the target changed it │ │724 - if (fieldEqual(field.type, parent_value, baseline_value)) break :blk .kept;│ │725 - break :blk .conflicted; │ │726 - } else .conflicted; │ │727 - } │ │722 + if (!fieldEqual(field.type, target_value, parent_value)) { │ │723 + outcome[i] = if (baseline_maybe) |baseline| blk: { │ │724 + const baseline_value = @field(baseline, field.name); │ │725 + if (fieldEqual(field.type, target_value, baseline_value)) { │ │726 + @field(merged, field.name) = parent_value; │ │727 + break :blk .parent; │ │728 + } │ │729 + // only the target changed it │ │730 + if (fieldEqual(field.type, parent_value, baseline_value)) break :blk .kept; │ │731 + break :blk .conflicted; │ │732 + } else .conflicted; │ │733 } │ │734 } │ │735 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │806 if (!target_record_cursor.slot().eql(baseline_record_cursor.slot())) { │ │807 // every write copies the record's block, so only a content │ │808 // change means the target kept it │ │804 - const target_record = try read(T, DB, hash_kind, &arena, try DB.HashMap(.read_only).init(target_record_cursor)); │ │805 - const baseline_record = try read(T, DB, hash_kind, &arena, try DB.HashMap(.read_only).init(baseline_record_cursor)); │ │806 - if (!fieldsEqual(T, target_record, baseline_record)) continue; │ │809 + const target_record = try read(T.Record, DB, hash_kind, &arena, try DB.HashMap(.read_only).init(target_record_cursor)); │ │810 + const baseline_record = try read(T.Record, DB, hash_kind, &arena, try DB.HashMap(.read_only).init(baseline_record_cursor));│ │811 + if (!fieldsEqual(T, target_record.event, baseline_record.event)) continue; │ │812 } │ │813 │ │814 try T.consume(DB, hash_kind, haxy_moment, &event_id, null, &arena, null); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │831 if (slot.eql(parent_slot)) continue; │ │832 } │ │833 │ │829 - const parent_record = try read(T, DB, hash_kind, &arena, try DB.HashMap(.read_only).init(parent_record_cursor)); │ │834 + const parent_record = try read(T.Record, DB, hash_kind, &arena, try DB.HashMap(.read_only).init(parent_record_cursor));│ │835 │ │836 // re-derived every iteration, since `consume` writes through its own │ │837 // handles │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │847 // both sides share the change │ │848 if (target_record_cursor.slot().eql(parent_slot)) continue; │ │849 │ │845 - const target_record = try read(T, DB, hash_kind, &arena, try DB.HashMap(.read_only).init(target_record_cursor)); │ │850 + const target_record = try read(T.Record, DB, hash_kind, &arena, try DB.HashMap(.read_only).init(target_record_cursor)); │ │851 │ │847 - var baseline_record: ?T = null; │ │852 + var baseline_event: ?T = null; │ │853 if (baseline_records) |map| { │ │854 if (try map.getCursor(record_key)) |baseline_record_cursor| { │ │850 - baseline_record = try read(T, DB, hash_kind, &arena, try DB.HashMap(.read_only).init(baseline_record_cursor)); │ │855 + baseline_event = (try read(T.Record, DB, hash_kind, &arena, try DB.HashMap(.read_only).init(baseline_record_cursor))).event;│ │856 } │ │857 } │ │858 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │859 outcome = @splat(.kept); │ │855 - merged = mergeFields(T, baseline_record, target_record, parent_record, &outcome); │ │860 + merged = target_record; │ │861 + merged.event = mergeFields(T, baseline_event, target_record.event, parent_record.event, &outcome);│ │862 } │ │863 │ │864 // the index is unique, so a key the target gave to a different record │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯