╭ changed ──────────╮ │src/event/Patch.zig│ ╰───────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────╮ │ 8 tags: []const u8, // space-separated │ │ 9 target_patch_id: ?[evt.event_id_size * 2]u8 = null,│ │10 patchrev_id: ?[evt.event_id_size * 2]u8 = null, │ │11 +status: Status = .open, │ │12 │ │13 pub const Record = struct { │ │14 event: Self, │ ╰───────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────╮ │18 }; │ │19 │ │20 const Self = @This(); │ │21 + │ │22 +pub const Status = enum { │ │23 + open, │ │24 + closed, │ │25 + merged, │ │26 + │ │27 + const longest_len = blk: { │ │28 + var len: usize = 0; │ │29 + for (@typeInfo(Status).@"enum".fields) |field| len = @max(len, field.name.len);│ │30 + break :blk len; │ │31 + }; │ │32 +}; │ │33 │ │34 pub const tag_max_len = 64; │ │35 pub const merge_policy: evt.MergePolicy = .field_conflicts; │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │38 pub const conflicts_key = "conflicted-patch-id->conflict"; │ │39 pub const id_to_field_to_oid_key = "patch-id->field->oid"; │ │40 pub const target_patch_id_to_patch_id_set_key = "target-patch-id->patch-id-set"; │ │41 +pub const status_to_id_set_key = "status->patch-id-set"; │ │42 +pub const tag_status_to_id_set_key = "tag+status->patch-id-set"; │ │43 + │ │44 +pub const TagStatusKey = [tag_max_len + 1 + Status.longest_len]u8; │ │45 + │ │46 +pub fn tagStatusKey(buffer: *TagStatusKey, tag: []const u8, status: Status) ![]const u8 {│ │47 + return std.fmt.bufPrint(buffer, "{s} {s}", .{ tag, @tagName(status) }); │ │48 +} │ │49 + │ │50 +pub fn tagIterator(tags: []const u8) std.mem.SplitIterator(u8, .scalar) { │ │51 + return std.mem.splitScalar(u8, tags, ' '); │ │52 +} │ │53 │ │54 pub fn fieldsValid(title: []const u8, tags: []const u8) bool { │ │55 if (title.len == 0) return false; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────╮ │31 - var tag_iter = std.mem.splitScalar(u8, tags, ' ');│ │56 + var tag_iter = tagIterator(tags); │ │57 while (tag_iter.next()) |tag| { │ │58 if (tag.len > tag_max_len) return false; │ │59 } │ ╰──────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │71 ) !void { │ │72 const record_key = hash.hashInt(hash_kind, event_id); │ │73 const records = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, record_map_key))); │ │74 + const statuses = try DB.SortedMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, status_to_id_set_key))); │ │75 + const tag_statuses = try DB.SortedMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, tag_status_to_id_set_key)));│ │76 const conflicts = try DB.SortedMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, conflicts_key))); │ │77 const field_oid_map = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, id_to_field_to_oid_key))); │ │78 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │91 }; │ │92 │ │93 if (!fieldsValid(record.event.title, record.event.tags)) return error.InvalidPatch; │ │67 - if (record.event.patchrev_id) |*id| _ = try evt.parseEventId(id); │ │94 + const patchrev_id = if (record.event.patchrev_id) |*id| try evt.parseEventId(id) else null; │ │95 const target_patch_id = if (record.event.target_patch_id) |*id| try evt.parseEventId(id) else null;│ │96 if (target_patch_id) |*id| { │ │97 if (std.mem.eql(u8, id, event_id)) return error.InvalidPatch; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │100 if (existing_maybe) |existing| { │ │101 record.created_order = existing.created_order; │ │102 record.author_email = existing.author_email; │ │103 + if (existing.event.status == .merged and record.event.status != .merged) return error.PatchAlreadyMerged;│ │104 if (event_oid != null) { │ │105 if (!std.meta.eql(existing.event.target_patch_id, record.event.target_patch_id)) { │ │106 return error.TargetPatchChanged; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │107 } │ │108 } │ │ 81 - if (event_oid != null or record.removed) { │ │ 82 - _ = try conflicts.remove(&evt.orderKeyDesc(existing.created_order, event_id)); │ │109 + } │ │110 + │ │111 + if (record.event.status == .merged) { │ │112 + const revision_id = patchrev_id orelse return error.InvalidPatch; │ │113 + const revision = (try evt.PatchRev.readById(DB, hash_kind, haxy_moment.readOnly(), arena, &revision_id)) orelse return error.InvalidPatch;│ │114 + if (revision.removed) return error.InvalidPatch; │ │115 + } │ │116 + │ │117 + const order_key = evt.orderKeyDesc(record.created_order, event_id); │ │118 + if (existing_maybe) |existing| { │ │119 + if (event_oid != null or record.removed) _ = try conflicts.remove(&order_key); │ │120 + if (!existing.removed) { │ │121 + const old_status = try statusSet(DB, statuses, existing.event.status); │ │122 + _ = try old_status.remove(&order_key); │ │123 + try removeFromTagSets(DB, tag_statuses, existing.event.tags, existing.event.status, &order_key); │ │124 } │ │125 } │ │126 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │132 │ │133 if (existing_cursor_maybe == null) { │ │134 const ids = try DB.SortedSet(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, id_set_key))); │ │ 94 - try ids.put(&evt.orderKeyDesc(record.created_order, event_id)); │ │135 + try ids.put(&order_key); │ │136 if (target_patch_id) |*target_id| { │ │137 const targets = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, target_patch_id_to_patch_id_set_key)));│ │138 const children = try DB.SortedSet(.read_write).init(try targets.putCursor(hash.hashInt(hash_kind, target_id))); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 98 - try children.put(&evt.orderKeyDesc(record.created_order, event_id)); │ │139 + try children.put(&order_key); │ │140 + } │ │141 + } │ │142 + │ │143 + if (!record.removed) { │ │144 + const status = try statusSet(DB, statuses, record.event.status); │ │145 + try status.put(&order_key); │ │146 + │ │147 + var tag_iter = tagIterator(record.event.tags); │ │148 + while (tag_iter.next()) |tag| { │ │149 + if (tag.len == 0 or tag.len > tag_max_len) continue; │ │150 + var key_buffer: TagStatusKey = undefined; │ │151 + const set = try DB.SortedSet(.read_write).init(try tag_statuses.putCursor(try tagStatusKey(&key_buffer, tag, record.event.status)));│ │152 + try set.put(&order_key); │ │153 } │ │154 } │ │155 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯