╭ 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 + │ │39 +// the email between a commit author line's angle brackets, or null │ │40 +pub fn authorEmail(author_line: []const u8) ?[]const u8 { │ │41 + const open_bracket = std.mem.indexOfScalar(u8, author_line, '<') orelse return null; │ │42 + const close_bracket = std.mem.indexOfScalarPos(u8, author_line, open_bracket + 1, '>') orelse return null;│ │43 + return author_line[open_bracket + 1 .. close_bracket]; │ │44 +} │ │45 + │ │46 pub const EventKind = enum { │ │47 user, │ │48 repo, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │591 var message: std.ArrayList(u8) = .empty; │ │592 try commit_object.readMessage(arena.allocator(), &message, .limited(max_event_size));│ │593 │ │582 - const event_with_id = try EventWithId.fromString(&arena, message.items); │ │594 + var event_with_id = try EventWithId.fromString(&arena, message.items); │ │595 │ │596 // get the id of the current event as bytes │ │597 var current_event_id: [event_id_size]u8 = undefined; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │598 _ = try std.fmt.hexToBytes(&current_event_id, &event_with_id.id); │ │599 │ │588 - const created_ts = commit_object.content.commit.metadata.timestamp; │ │600 + // set the derived fields │ │601 + 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 ""); │ │605 + }, │ │606 + inline else => |*event_maybe| if (event_maybe.*) |*event| { │ │607 + event.created_ts = commit_object.content.commit.metadata.timestamp; │ │608 + }, │ │609 + } │ │610 │ │611 switch (event_with_id.event) { │ │591 - .user => |event_maybe| try User.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, event_maybe, &arena, created_ts, &repo_event_oid), │ │592 - .repo => |event_maybe| try Repo.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, event_maybe, &arena, created_ts, &repo_event_oid), │ │593 - .issue => |event_maybe| try Issue.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, event_maybe, &arena, created_ts, &repo_event_oid),│ │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); │ │615 + }, │ │616 } │ │617 } │ │618 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │665 var field_oids: ?DB.SortedMap(.read_write) = null; │ │666 │ │667 inline for (std.meta.fields(T)) |field| { │ │646 - if (comptime !std.mem.eql(u8, field.name, "created_ts")) { │ │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 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────╮ │709 var merged = target; │ │710 │ │711 inline for (std.meta.fields(T), 0..) |field, i| { │ │690 - if (comptime !std.mem.eql(u8, field.name, "created_ts")) {│ │712 + if (comptime !derivedField(field.name)) { │ │713 const target_value = @field(target, field.name); │ │714 const parent_value = @field(parent, field.name); │ │715 │ ╰───────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │806 if (!fieldsEqual(T, target_record, baseline_record)) continue; │ │807 } │ │808 │ │787 - try T.consume(DB, hash_kind, haxy_moment, &event_id, null, &arena, 0, null);│ │809 + try T.consume(DB, hash_kind, haxy_moment, &event_id, null, &arena, null); │ │810 } │ │811 } │ │812 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │871 │ │872 // `consume` sets no oids, since each field's winner may come from │ │873 // a different side │ │852 - try T.consume(DB, hash_kind, haxy_moment, &event_id, merged, &arena, merged.created_ts, null);│ │874 + try T.consume(DB, hash_kind, haxy_moment, &event_id, merged, &arena, null); │ │875 │ │876 const conflicts_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, T.conflicts_key)); │ │877 const conflicts = try DB.SortedMap(.read_write).init(conflicts_cursor); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │893 var conflicted_len: usize = 0; │ │894 │ │895 inline for (std.meta.fields(T), 0..) |field, i| { │ │874 - if (comptime !std.mem.eql(u8, field.name, "created_ts")) { │ │875 - switch (outcome[i]) { │ │876 - // the target's value stands, so its oid does too │ │877 - .kept => {}, │ │878 - .parent => if (parent_field_oids) |parent_map| { │ │879 - if (try parent_map.getCursor(field.name)) |oid_cursor| { │ │880 - if (field_oids == null) { │ │881 - field_oids = try DB.SortedMap(.read_write).init(try id_to_field_to_oid.putCursor(record_key));│ │882 - } │ │883 - if (field_oids) |map| try map.put(field.name, .{ .slot = oid_cursor.slot() }); │ │896 + switch (outcome[i]) { │ │897 + // the target's value stands, so its oid does too │ │898 + .kept => {}, │ │899 + .parent => if (parent_field_oids) |parent_map| { │ │900 + if (try parent_map.getCursor(field.name)) |oid_cursor| { │ │901 + if (field_oids == null) { │ │902 + field_oids = try DB.SortedMap(.read_write).init(try id_to_field_to_oid.putCursor(record_key)); │ │903 } │ │885 - }, │ │886 - .conflicted => { │ │887 - if (conflicted_len > 0) { │ │888 - conflicted_fields[conflicted_len] = ' '; │ │889 - conflicted_len += 1; │ │890 - } │ │891 - @memcpy(conflicted_fields[conflicted_len..][0..field.name.len], field.name); │ │892 - conflicted_len += field.name.len; │ │893 - }, │ │894 - } │ │904 + if (field_oids) |map| try map.put(field.name, .{ .slot = oid_cursor.slot() }); │ │905 + } │ │906 + }, │ │907 + .conflicted => { │ │908 + if (conflicted_len > 0) { │ │909 + conflicted_fields[conflicted_len] = ' '; │ │910 + conflicted_len += 1; │ │911 + } │ │912 + @memcpy(conflicted_fields[conflicted_len..][0..field.name.len], field.name); │ │913 + conflicted_len += field.name.len; │ │914 + }, │ │915 } │ │916 } │ │917 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1119 const bytes = try readBytes(DB, hash_kind, arena.allocator(), map, field.name); │ │1120 @field(event, field.name) = std.meta.stringToEnum(field.type, bytes) orelse return error.InvalidEnumTag; │ │1121 }, │ │1122 + .optional => |optional_info| { │ │1123 + if (optional_info.child != []const u8) @compileError("unsupported read field type: " ++ @typeName(field.type));│ │1124 + // a missing key is null │ │1125 + @field(event, field.name) = if (try map.getCursor(hash.hashInt(hash_kind, field.name))) |cursor| │ │1126 + try cursor.readBytesAlloc(arena.allocator(), null) │ │1127 + else │ │1128 + null; │ │1129 + }, │ │1130 else => @compileError("unsupported read field type: " ++ @typeName(field.type)), │ │1131 } │ │1132 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │1137 return event; │ │1138 } │ │1139 │ │1112 -// whether two events carry the same data. `created_ts` comes from the commit│ │1113 -// rather than the payload, so it isn't compared. │ │1140 +// whether two events carry the same payload data (commit-derived fields │ │1141 +// aren't compared) │ │1142 fn fieldsEqual(comptime T: type, a: T, b: T) bool { │ │1143 switch (@typeInfo(T)) { │ │1144 .@"struct" => |struct_info| { │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯