╭ Patch.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("../event.zig"); 3 const xit = @import("xit"); 4 const hash = xit.hash; 5 6 title: []const u8, 7 description: []const u8, 8 tags: []const u8, // space-separated 9 target_patch_id: ?[evt.event_id_size * 2]u8 = null, 10 revision: ?Revision = null, 11 status: Status = .open, 12 13 pub const Revision = struct { 14 id: [evt.event_id_size * 2]u8, 15 squash_oid: []const u8, 16 source_oid: []const u8, 17 target_ref: []const u8, 18 19 pub fn fromRecord(id: [evt.event_id_size]u8, record: evt.PatchRev.Record) Revision { 20 return .{ 21 .id = std.fmt.bytesToHex(id, .lower), 22 .squash_oid = record.patch_oid, 23 .source_oid = record.event.source_oid, 24 .target_ref = record.event.target_ref, 25 }; 26 } 27 28 pub fn matches(self: Revision, record: evt.PatchRev.Record) bool { 29 return !record.removed and 30 std.mem.eql(u8, record.patch_oid, self.squash_oid) and 31 std.mem.eql(u8, record.event.source_oid, self.source_oid) and 32 std.mem.eql(u8, record.event.target_ref, self.target_ref); 33 } 34 }; 35 36 pub const Record = struct { 37 event: Self, 38 removed: bool = false, 39 author_email: ?[]const u8 = null, 40 created_order: u64 = 0, 41 }; 42 43 const Self = @This(); 44 45 pub const Status = enum { 46 open, 47 closed, 48 merged, 49 50 const longest_len = blk: { 51 var len: usize = 0; 52 for (@typeInfo(Status).@"enum".fields) |field| len = @max(len, field.name.len); 53 break :blk len; 54 }; 55 }; 56 57 pub const tag_max_len = 64; 58 pub const merge_policy: evt.MergePolicy = .field_conflicts; 59 pub const record_map_key = "event-id->patch"; 60 pub const id_set_key = "patch-id-set"; 61 pub const conflicts_key = "conflicted-patch-id->conflict"; 62 pub const id_to_field_to_oid_key = "patch-id->field->oid"; 63 pub const target_patch_id_to_patch_id_set_key = "target-patch-id->patch-id-set"; 64 pub const status_to_id_set_key = "status->patch-id-set"; 65 pub const tag_status_to_id_set_key = "tag+status->patch-id-set"; 66 pub const revision_to_id_set_key = "target-ref+oid->patch-id-set"; 67 68 pub const TagStatusKey = [tag_max_len + 1 + Status.longest_len]u8; 69 70 pub fn tagStatusKey(buffer: *TagStatusKey, tag: []const u8, status: Status) ![]const u8 { 71 return std.fmt.bufPrint(buffer, "{s} {s}", .{ tag, @tagName(status) }); 72 } 73 74 pub fn tagIterator(tags: []const u8) std.mem.SplitIterator(u8, .scalar) { 75 return std.mem.splitScalar(u8, tags, ' '); 76 } 77 78 pub fn fieldsValid(title: []const u8, tags: []const u8) bool { 79 if (title.len == 0) return false; 80 var tag_iter = tagIterator(tags); 81 while (tag_iter.next()) |tag| { 82 if (tag.len > tag_max_len) return false; 83 } 84 return true; 85 } 86 87 pub fn consume( 88 comptime DB: type, 89 comptime hash_kind: hash.HashKind, 90 haxy_moment: DB.HashMap(.read_write), 91 event_id: *const [evt.event_id_size]u8, 92 record_maybe: ?Record, 93 arena: *std.heap.ArenaAllocator, 94 event_oid: ?[]const u8, 95 ) !void { 96 const record_key = hash.hashInt(hash_kind, event_id); 97 const records = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, record_map_key))); 98 const statuses = try DB.SortedMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, status_to_id_set_key))); 99 const tag_statuses = try DB.SortedMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, tag_status_to_id_set_key))); 100 const conflicts = try DB.SortedMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, conflicts_key))); 101 const field_oid_map = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, id_to_field_to_oid_key))); 102 103 var existing_maybe: ?Record = null; 104 const existing_cursor_maybe = try records.getCursor(record_key); 105 if (existing_cursor_maybe) |cursor| { 106 existing_maybe = try evt.read(Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(cursor)); 107 } 108 109 var record = if (record_maybe) |value| 110 value 111 else blk: { 112 var value = existing_maybe orelse return error.EventNotFound; 113 value.removed = true; 114 break :blk value; 115 }; 116 117 if (!fieldsValid(record.event.title, record.event.tags)) return error.InvalidPatch; 118 const revision_id = if (record.event.revision) |*revision| blk: { 119 try evt.PatchRev.validateOid(hash_kind, revision.squash_oid); 120 try evt.PatchRev.validateOid(hash_kind, revision.source_oid); 121 try evt.PatchRev.validateTarget(revision.target_ref); 122 break :blk try evt.parseEventId(&revision.id); 123 } else null; 124 const target_patch_id = if (record.event.target_patch_id) |*id| try evt.parseEventId(id) else null; 125 if (target_patch_id) |*id| { 126 if (std.mem.eql(u8, id, event_id)) return error.InvalidPatch; 127 } 128 129 if (existing_maybe) |existing| { 130 record.created_order = existing.created_order; 131 record.author_email = existing.author_email; 132 if (existing.event.status == .merged and record.event.status != .merged) return error.PatchAlreadyMerged; 133 if (event_oid != null) { 134 if (!std.meta.eql(existing.event.target_patch_id, record.event.target_patch_id)) { 135 return error.TargetPatchChanged; 136 } 137 } 138 } 139 140 if (record.event.status == .merged) { 141 const id = revision_id orelse return error.InvalidPatch; 142 const selected = record.event.revision orelse return error.InvalidPatch; 143 const revision = (try evt.PatchRev.readById(DB, hash_kind, haxy_moment.readOnly(), arena, &id)) orelse return error.InvalidPatch; 144 if (!selected.matches(revision)) return error.InvalidPatch; 145 } 146 147 const order_key = evt.orderKeyDesc(record.created_order, event_id); 148 if (existing_maybe) |existing| { 149 if (event_oid != null or record.removed) _ = try conflicts.remove(&order_key); 150 if (!existing.removed) { 151 const old_status = try statusSet(DB, statuses, existing.event.status); 152 _ = try old_status.remove(&order_key); 153 try removeFromTagSets(DB, tag_statuses, existing.event.tags, existing.event.status, &order_key); 154 if (existing.event.status != .merged) { 155 if (existing.event.revision) |revision| { 156 const revisions = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, revision_to_id_set_key))); 157 for ([_][]const u8{ revision.squash_oid, revision.source_oid }, 0..) |oid, i| { 158 if (i != 0 and std.mem.eql(u8, revision.squash_oid, revision.source_oid)) continue; 159 const key_hash = hash.hashInt(hash_kind, try revisionKey(arena.allocator(), revision.target_ref, oid)); 160 const ids = try DB.CountedHashSet(.read_write).init(try revisions.putCursor(key_hash)); 161 _ = try ids.remove(record_key); 162 if (try ids.count() == 0) _ = try revisions.remove(key_hash); 163 } 164 } 165 } 166 } 167 } 168 169 try evt.writeOid(Self, DB, hash_kind, field_oid_map, record_key, if (existing_maybe) |existing| existing.event else null, record.event, event_oid); 170 171 const cursor = try records.putCursor(record_key); 172 try evt.upsert(Record, DB, hash_kind, try DB.HashMap(.read_write).init(cursor), record); 173 try evt.indexEvent(DB, hash_kind, haxy_moment, event_id, .patch, record.created_order, record.removed); 174 175 if (existing_cursor_maybe == null) { 176 const ids = try DB.SortedSet(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, id_set_key))); 177 try ids.put(&order_key); 178 if (target_patch_id) |*target_id| { 179 const targets = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, target_patch_id_to_patch_id_set_key))); 180 const children = try DB.SortedSet(.read_write).init(try targets.putCursor(hash.hashInt(hash_kind, target_id))); 181 try children.put(&order_key); 182 } 183 } 184 185 if (!record.removed) { 186 const status = try statusSet(DB, statuses, record.event.status); 187 try status.put(&order_key); 188 189 var tag_iter = tagIterator(record.event.tags); 190 while (tag_iter.next()) |tag| { 191 if (tag.len == 0 or tag.len > tag_max_len) continue; 192 var key_buffer: TagStatusKey = undefined; 193 const set = try DB.SortedSet(.read_write).init(try tag_statuses.putCursor(try tagStatusKey(&key_buffer, tag, record.event.status))); 194 try set.put(&order_key); 195 } 196 197 if (record.event.status != .merged) { 198 if (record.event.revision) |revision| { 199 const revisions = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, revision_to_id_set_key))); 200 for ([_][]const u8{ revision.squash_oid, revision.source_oid }, 0..) |oid, i| { 201 if (i != 0 and std.mem.eql(u8, revision.squash_oid, revision.source_oid)) continue; 202 const key = try revisionKey(arena.allocator(), revision.target_ref, oid); 203 const ids = try DB.CountedHashSet(.read_write).init(try revisions.putCursor(hash.hashInt(hash_kind, key))); 204 try ids.put(record_key, .{ .bytes = event_id }); 205 } 206 } 207 } 208 } 209 } 210 211 pub fn resolveMerge( 212 target: Self, 213 parent: Self, 214 merged: *Self, 215 outcome: *[std.meta.fields(Self).len]evt.FieldMerge, 216 ) void { 217 const status_index = std.meta.fieldIndex(Self, "status") orelse @compileError("Patch.status not found"); 218 const revision_index = std.meta.fieldIndex(Self, "revision") orelse @compileError("Patch.revision not found"); 219 if (outcome[status_index] != .conflicted or outcome[revision_index] == .conflicted) return; 220 221 if (target.status == .merged and evt.fieldEqual(?Revision, target.revision, merged.revision)) { 222 merged.status = .merged; 223 outcome[status_index] = .kept; 224 } else if (parent.status == .merged and evt.fieldEqual(?Revision, parent.revision, merged.revision)) { 225 merged.status = .merged; 226 outcome[status_index] = .parent; 227 } 228 } 229 230 fn revisionKey(allocator: std.mem.Allocator, target_ref: []const u8, oid: []const u8) ![]u8 { 231 return std.mem.concat(allocator, u8, &.{ target_ref, "\x00", oid }); 232 } 233 234 fn statusSet( 235 comptime DB: type, 236 statuses: DB.SortedMap(.read_write), 237 status: Status, 238 ) !DB.SortedSet(.read_write) { 239 return DB.SortedSet(.read_write).init(try statuses.putCursor(@tagName(status))); 240 } 241 242 fn removeFromTagSets( 243 comptime DB: type, 244 tag_statuses: DB.SortedMap(.read_write), 245 tags: []const u8, 246 status: Status, 247 order_key: []const u8, 248 ) !void { 249 var tag_iter = tagIterator(tags); 250 while (tag_iter.next()) |tag| { 251 if (tag.len == 0 or tag.len > tag_max_len) continue; 252 var key_buffer: TagStatusKey = undefined; 253 const key = try tagStatusKey(&key_buffer, tag, status); 254 const set = try DB.SortedSet(.read_write).init(try tag_statuses.putCursor(key)); 255 _ = try set.remove(order_key); 256 if (0 == try set.count()) _ = try tag_statuses.remove(key); 257 } 258 } 259 260 pub fn readById( 261 comptime DB: type, 262 comptime hash_kind: hash.HashKind, 263 haxy_moment: DB.HashMap(.read_only), 264 arena: *std.heap.ArenaAllocator, 265 id: *const [evt.event_id_size]u8, 266 ) !?Record { 267 const records_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, record_map_key)) orelse return null; 268 const records = try DB.HashMap(.read_only).init(records_cursor); 269 const record_cursor = try records.getCursor(hash.hashInt(hash_kind, id)) orelse return null; 270 return try evt.read(Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(record_cursor)); 271 } 272