╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1633 const bytes = try readBytes(DB, hash_kind, arena.allocator(), map, field.name); │ │1634 @field(event, field.name) = std.meta.stringToEnum(field.type, bytes) orelse return error.InvalidEnumTag; │ │1635 }, │ │1636 + .@"union" => { │ │1637 + if (!@hasDecl(field.type, "decode")) @compileError("unsupported read field type: " ++ @typeName(field.type)); │ │1638 + @field(event, field.name) = try field.type.decode(try readBytes(DB, hash_kind, arena.allocator(), map, field.name));│ │1639 + }, │ │1640 .optional => |optional_info| { │ │1641 // a missing key is null │ │1642 if (try map.getCursor(hash.hashInt(hash_kind, field.name))) |cursor| { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1713 @compileError("unsupported field type: " ++ @typeName(Field)); │ │1714 } │ │1715 }, │ │1716 + .void => return true, │ │1717 .bool, .int, .@"enum" => return a == b, │ │1718 + .@"union" => { │ │1719 + if (std.meta.activeTag(a) != std.meta.activeTag(b)) return false; │ │1720 + return switch (a) { │ │1721 + inline else => |value, tag| fieldEqual(@TypeOf(value), value, @field(b, @tagName(tag))),│ │1722 + }; │ │1723 + }, │ │1724 .@"struct" => return fieldsEqual(Field, a, b), │ │1725 .optional => |optional_info| { │ │1726 if (a) |a_value| { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │1861 try map.put(key, .{ .bytes_object = .{ .value = bytes, .format_tag = "bl".* } });│ │1862 }, │ │1863 .@"enum" => try upsertBytes(DB, hash_kind, map, key, @tagName(value)), │ │1864 + .@"union" => { │ │1865 + if (!@hasDecl(Field, "encode") or !@hasDecl(Field, "max_encoded_len")) { │ │1866 + @compileError("unsupported upsert field type: " ++ @typeName(Field)); │ │1867 + } │ │1868 + var buffer: [Field.max_encoded_len]u8 = undefined; │ │1869 + try upsertBytes(DB, hash_kind, map, key, try value.encode(&buffer)); │ │1870 + }, │ │1871 .optional => |optional_info| { │ │1872 // a missing key is null │ │1873 if (value) |child| { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────╮ │src/event/Patch.zig│ ╰───────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │33 std.mem.eql(u8, record.patch_oid, self.squash_oid) and │ │34 std.mem.eql(u8, record.event.source_oid, self.source_oid) and │ │35 std.mem.eql(u8, record.event.target_ref, self.target_ref); │ │36 + } │ │37 + │ │38 + pub fn includesOid(self: Revision, oid: []const u8) bool { │ │39 + return std.mem.eql(u8, oid, self.source_oid) or std.mem.eql(u8, oid, self.squash_oid);│ │40 } │ │41 │ │42 pub fn targetBranch(self: Revision) ?[]const u8 { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────╮ │57 │ │58 const Self = @This(); │ │59 │ │56 -pub const Status = enum { │ │60 +pub const StatusKind = enum {│ │61 open, │ │62 closed, │ │63 merged, │ ╰─────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 64 │ │ 65 const longest_len = blk: { │ │ 66 var len: usize = 0; │ │ 63 - for (@typeInfo(Status).@"enum".fields) |field| len = @max(len, field.name.len); │ │ 67 + for (@typeInfo(StatusKind).@"enum".fields) |field| len = @max(len, field.name.len); │ │ 68 + break :blk len; │ │ 69 + }; │ │ 70 +}; │ │ 71 + │ │ 72 +pub const Status = union(StatusKind) { │ │ 73 + open, │ │ 74 + closed, │ │ 75 + merged: []const u8, │ │ 76 + │ │ 77 + pub const max_encoded_len = "merged ".len + blk: { │ │ 78 + var len: usize = 0; │ │ 79 + for (std.enums.values(hash.HashKind)) |hash_kind| len = @max(len, hash.hexLen(hash_kind)); │ │ 80 break :blk len; │ │ 81 }; │ │ 82 + │ │ 83 + pub fn kind(self: Status) StatusKind { │ │ 84 + return std.meta.activeTag(self); │ │ 85 + } │ │ 86 + │ │ 87 + pub fn encode(self: Status, buffer: []u8) ![]const u8 { │ │ 88 + return switch (self) { │ │ 89 + .open => "open", │ │ 90 + .closed => "closed", │ │ 91 + .merged => |oid| try std.fmt.bufPrint(buffer, "merged {s}", .{oid}), │ │ 92 + }; │ │ 93 + } │ │ 94 + │ │ 95 + pub fn decode(encoded: []const u8) !Status { │ │ 96 + if (std.mem.eql(u8, encoded, "open")) return .open; │ │ 97 + if (std.mem.eql(u8, encoded, "closed")) return .closed; │ │ 98 + const prefix = "merged "; │ │ 99 + if (std.mem.startsWith(u8, encoded, prefix) and encoded.len != prefix.len) { │ │100 + return .{ .merged = encoded[prefix.len..] }; │ │101 + } │ │102 + return error.InvalidEnumTag; │ │103 + } │ │104 + │ │105 + pub fn jsonStringify(self: Status, jw: anytype) !void { │ │106 + var buffer: [max_encoded_len]u8 = undefined; │ │107 + try jw.write(self.encode(&buffer) catch return error.WriteFailed); │ │108 + } │ │109 + │ │110 + pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !Status { │ │111 + return decode(try std.json.innerParse([]const u8, allocator, source, options)); │ │112 + } │ │113 + │ │114 + pub fn jsonParseFromValue(_: std.mem.Allocator, source: std.json.Value, _: std.json.ParseOptions) !Status {│ │115 + return switch (source) { │ │116 + .string => |encoded| decode(encoded), │ │117 + else => error.UnexpectedToken, │ │118 + }; │ │119 + } │ │120 }; │ │121 │ │122 pub const Resolve = struct { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │127 }; │ │128 │ │129 pub const Update = union(enum) { │ │ 76 - status: Status, │ │130 + status: StatusKind, │ │131 fields: struct { title: []const u8, tags: []const u8, description: []const u8 },│ │132 resolve: Resolve, │ │133 }; │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │143 pub const tag_status_to_id_set_key = "tag+status->patch-id-set"; │ │144 pub const revision_to_id_set_key = "target-ref+oid->patch-id-set"; │ │145 │ │ 92 -pub const TagStatusKey = [tag_max_len + 1 + Status.longest_len]u8; │ │146 +pub const TagStatusKey = [tag_max_len + 1 + StatusKind.longest_len]u8; │ │147 │ │ 94 -pub fn tagStatusKey(buffer: *TagStatusKey, tag: []const u8, status: Status) ![]const u8 { │ │148 +pub fn tagStatusKey(buffer: *TagStatusKey, tag: []const u8, status: StatusKind) ![]const u8 {│ │149 return std.fmt.bufPrint(buffer, "{s} {s}", .{ tag, @tagName(status) }); │ │150 } │ │151 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────╮ │203 if (target_patch_id) |*id| { │ │204 if (std.mem.eql(u8, id, event_id)) return error.InvalidPatch;│ │205 } │ │206 + const status_kind = record.event.status.kind(); │ │207 │ │208 if (existing_maybe) |existing| { │ │209 record.created_order = existing.created_order; │ ╰──────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │210 record.author_email = existing.author_email; │ │156 - if (existing.event.status == .merged and record.event.status != .merged) return error.PatchAlreadyMerged; │ │211 + const existing_status_kind = existing.event.status.kind(); │ │212 + if (existing_status_kind == .merged and !evt.fieldEqual(Status, existing.event.status, record.event.status)) return error.PatchAlreadyMerged;│ │213 if (event_oid != null) { │ │214 if (!std.meta.eql(existing.event.target_patch_id, record.event.target_patch_id)) { │ │215 return error.TargetPatchChanged; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯