╭ changed ──────────╮ │src/event/Patch.zig│ ╰───────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │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; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │217 } │ │218 } │ │219 │ │164 - if (record.event.status == .merged) { │ │165 - const id = revision_id orelse return error.InvalidPatch; │ │166 - const selected = record.event.revision orelse return error.InvalidPatch; │ │167 - const revision = (try evt.PatchRev.readById(DB, hash_kind, haxy_moment.readOnly(), arena, &id)) orelse return error.InvalidPatch; │ │168 - if (!selected.matches(revision)) return error.InvalidPatch; │ │220 + switch (record.event.status) { │ │221 + .open, .closed => {}, │ │222 + .merged => |merged_oid| { │ │223 + try evt.PatchRev.validateOid(hash_kind, merged_oid); │ │224 + const id = revision_id orelse return error.InvalidPatch; │ │225 + const selected = record.event.revision orelse return error.InvalidPatch; │ │226 + if (!selected.includesOid(merged_oid)) return error.InvalidPatch; │ │227 + const revision = (try evt.PatchRev.readById(DB, hash_kind, haxy_moment.readOnly(), arena, &id)) orelse return error.InvalidPatch;│ │228 + if (!selected.matches(revision)) return error.InvalidPatch; │ │229 + }, │ │230 } │ │231 │ │232 const order_key = evt.orderKeyDesc(record.created_order, event_id); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │233 if (existing_maybe) |existing| { │ │234 if (event_oid != null or record.removed) _ = try conflicts.remove(&order_key); │ │235 if (!existing.removed) { │ │175 - const old_status = try statusSet(DB, statuses, existing.event.status); │ │236 + const existing_status_kind = existing.event.status.kind(); │ │237 + const old_status = try statusSet(DB, statuses, existing_status_kind); │ │238 _ = try old_status.remove(&order_key); │ │177 - try removeFromTagSets(DB, tag_statuses, existing.event.tags, existing.event.status, &order_key); │ │178 - if (existing.event.status != .merged) { │ │239 + try removeFromTagSets(DB, tag_statuses, existing.event.tags, existing_status_kind, &order_key); │ │240 + if (existing_status_kind != .merged) { │ │241 if (existing.event.revision) |revision| { │ │242 const revisions = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, revision_to_id_set_key)));│ │243 for ([_][]const u8{ revision.squash_oid, revision.source_oid }, 0..) |oid, i| { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │269 } │ │270 │ │271 if (!record.removed) { │ │210 - const status = try statusSet(DB, statuses, record.event.status);│ │272 + const status = try statusSet(DB, statuses, status_kind); │ │273 try status.put(&order_key); │ │274 │ │275 var tag_iter = tagIterator(record.event.tags); │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯