╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────╮ │1063 for (std.meta.fields(T)) |field| len += field.name.len + 1;│ │1064 return len; │ │1065 } │ │1066 + │ │1067 +pub const MergePolicy = enum { │ │1068 + field_conflicts, │ │1069 + target_wins, │ │1070 +}; │ │1071 │ │1072 // what a three-way merge did with a field │ │1073 const FieldMerge = enum { │ ╰─────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1584 @field(event, field.name) = std.meta.stringToEnum(field.type, bytes) orelse return error.InvalidEnumTag; │ │1585 }, │ │1586 .optional => |optional_info| { │ │1496 - if (optional_info.child != []const u8) @compileError("unsupported read field type: " ++ @typeName(field.type)); │ │1587 // a missing key is null │ │1498 - @field(event, field.name) = if (try map.getCursor(hash.hashInt(hash_kind, field.name))) |cursor| │ │1499 - try cursor.readBytesAlloc(arena.allocator(), null) │ │1500 - else │ │1501 - null; │ │1588 + if (try map.getCursor(hash.hashInt(hash_kind, field.name))) |cursor| { │ │1589 + if (optional_info.child == []const u8) { │ │1590 + @field(event, field.name) = try cursor.readBytesAlloc(arena.allocator(), null); │ │1591 + } else switch (@typeInfo(optional_info.child)) { │ │1592 + .array => |array_info| { │ │1593 + if (array_info.child != u8) @compileError("unsupported read field type: " ++ @typeName(field.type));│ │1594 + var bytes: optional_info.child = undefined; │ │1595 + const value = try cursor.readBytes(&bytes); │ │1596 + if (value.len != bytes.len) return error.InvalidByteArrayLength; │ │1597 + @field(event, field.name) = bytes; │ │1598 + }, │ │1599 + else => @compileError("unsupported read field type: " ++ @typeName(field.type)), │ │1600 + } │ │1601 + } else { │ │1602 + @field(event, field.name) = null; │ │1603 + } │ │1604 }, │ │1605 // a struct field's own fields are read from the same map │ │1606 .@"struct" => @field(event, field.name) = try read(field.type, DB, hash_kind, arena, map), │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │1645 } │ │1646 }, │ │1647 .bool, .int, .@"enum" => return a == b, │ │1648 + .optional => |optional_info| { │ │1649 + if (a) |a_value| { │ │1650 + if (b) |b_value| return fieldEqual(optional_info.child, a_value, b_value);│ │1651 + return false; │ │1652 + } │ │1653 + return b == null; │ │1654 + }, │ │1655 else => @compileError("unsupported field type: " ++ @typeName(Field)), │ │1656 } │ │1657 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1786 }, │ │1787 .@"enum" => try upsertBytes(DB, hash_kind, map, key, @tagName(value)), │ │1788 .optional => |optional_info| { │ │1680 - if (optional_info.child != []const u8) @compileError("unsupported upsert field type: " ++ @typeName(Field)); │ │1789 // a missing key is null │ │1682 - if (value) |bytes| { │ │1683 - try upsertBytes(DB, hash_kind, map, key, bytes); │ │1790 + if (value) |child| { │ │1791 + if (optional_info.child == []const u8) { │ │1792 + try upsertBytes(DB, hash_kind, map, key, child); │ │1793 + } else switch (@typeInfo(optional_info.child)) { │ │1794 + .array => |array_info| { │ │1795 + if (array_info.child != u8) @compileError("unsupported upsert field type: " ++ @typeName(Field));│ │1796 + try upsertBytes(DB, hash_kind, map, key, &child); │ │1797 + }, │ │1798 + else => @compileError("unsupported upsert field type: " ++ @typeName(Field)), │ │1799 + } │ │1800 } else { │ │1801 _ = try map.remove(key); │ │1802 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/event/Attachment.zig│ ╰────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │84 break :blk record; │ │85 }; │ │86 │ │87 - // references use the same lower-case hex form as event ids in json │ │88 - var id_bytes: [evt.event_id_size]u8 = undefined; │ │89 - _ = try std.fmt.hexToBytes(&id_bytes, &record_to_write.event.parent_id); │ │90 - if (!std.mem.eql(u8, &record_to_write.event.parent_id, &std.fmt.bytesToHex(id_bytes, .lower))) return error.InvalidEventId;│ │87 + const id_bytes = try evt.parseEventId(&record_to_write.event.parent_id); │ │88 if (!try parentExists(DB, hash_kind, haxy_moment, &id_bytes)) return error.ParentNotFound; │ │89 if (!nameValid(record_to_write.name)) return error.InvalidAttachment; │ │90 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │132 if (!nameValid(blob.name)) return error.InvalidFields; │ │133 if (blob.size == 0 or blob.size > max_size) return error.InvalidFields; │ │134 │ │138 - var parent_id_bytes: [evt.event_id_size]u8 = undefined; │ │139 - _ = std.fmt.hexToBytes(&parent_id_bytes, parent_id) catch return error.InvalidFields; │ │140 - if (!std.mem.eql(u8, parent_id, &std.fmt.bytesToHex(parent_id_bytes, .lower))) return error.InvalidFields;│ │135 + const parent_id_bytes = evt.parseEventId(parent_id) catch return error.InvalidFields; │ │136 │ │137 { │ │138 const DB = evt.EventDB(repo_opts.hash); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │154 .id = event_id, │ │155 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()),│ │156 .author = author, │ │162 - .blob = blob, │ │157 + .tree_entries = &.{.{ .blob = blob }}, │ │158 .event = .{ .attach = .{ .parent_id = parent_id.* } }, │ │159 }}); │ │160 return event_id; │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────────╮ │src/event/Comment.zig│ ╰─────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │66 break :blk record; │ │67 }; │ │68 │ │69 - // references use the same lower-case hex form as event ids in json │ │70 - var thread_id: [evt.event_id_size]u8 = undefined; │ │71 - _ = try std.fmt.hexToBytes(&thread_id, &record_to_write.event.thread_id); │ │72 - if (!std.mem.eql(u8, &record_to_write.event.thread_id, &std.fmt.bytesToHex(thread_id, .lower))) return error.InvalidEventId;│ │73 - var parent_id: [evt.event_id_size]u8 = undefined; │ │74 - _ = try std.fmt.hexToBytes(&parent_id, &record_to_write.event.parent_id); │ │75 - if (!std.mem.eql(u8, &record_to_write.event.parent_id, &std.fmt.bytesToHex(parent_id, .lower))) return error.InvalidEventId;│ │69 + const thread_id = try evt.parseEventId(&record_to_write.event.thread_id); │ │70 + _ = try evt.parseEventId(&record_to_write.event.parent_id); │ │71 │ │72 if (existing_record_maybe) |existing_record| { │ │73 // updates preserve the original creation order and author │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ added ────────────╮ │src/event/Patch.zig│ ╰───────────────────╯ ╭ added ───────────────╮ │src/event/PatchRev.zig│ ╰──────────────────────╯ ╭──────╮ │next →│ ╰──────╯