╭ 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 patchrev_id: ?[evt.event_id_size * 2]u8 = null, 11 status: Status = .open, 12 13 pub const Record = struct { 14 event: Self, 15 removed: bool = false, 16 author_email: ?[]const u8 = null, 17 created_order: u64 = 0, 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; 36 pub const record_map_key = "event-id->patch"; 37 pub const id_set_key = "patch-id-set"; 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; 56 var tag_iter = tagIterator(tags); 57 while (tag_iter.next()) |tag| { 58 if (tag.len > tag_max_len) return false; 59 } 60 return true; 61 } 62 63 pub fn consume( 64 comptime DB: type, 65 comptime hash_kind: hash.HashKind, 66 haxy_moment: DB.HashMap(.read_write), 67 event_id: *const [evt.event_id_size]u8, 68 record_maybe: ?Record, 69 arena: *std.heap.ArenaAllocator, 70 event_oid: ?[]const u8, 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 79 var existing_maybe: ?Record = null; 80 const existing_cursor_maybe = try records.getCursor(record_key); 81 if (existing_cursor_maybe) |cursor| { 82 existing_maybe = try evt.read(Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(cursor)); 83 } 84 85 var record = if (record_maybe) |value| 86 value 87 else blk: { 88 var value = existing_maybe orelse return error.EventNotFound; 89 value.removed = true; 90 break :blk value; 91 }; 92 93 if (!fieldsValid(record.event.title, record.event.tags)) return error.InvalidPatch; 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; 98 } 99 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 } 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 127 try evt.writeOid(Self, DB, hash_kind, field_oid_map, record_key, if (existing_maybe) |existing| existing.event else null, record.event, event_oid); 128 129 const cursor = try records.putCursor(record_key); 130 try evt.upsert(Record, DB, hash_kind, try DB.HashMap(.read_write).init(cursor), record); 131 try evt.indexEvent(DB, hash_kind, haxy_moment, event_id, .patch, record.created_order, record.removed); 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))); 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))); 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 } 156 157 pub fn resolveMerge( 158 target: Self, 159 parent: Self, 160 merged: *Self, 161 outcome: *[std.meta.fields(Self).len]evt.FieldMerge, 162 ) void { 163 const status_index = std.meta.fieldIndex(Self, "status") orelse @compileError("Patch.status not found"); 164 const patchrev_index = std.meta.fieldIndex(Self, "patchrev_id") orelse @compileError("Patch.patchrev_id not found"); 165 if (outcome[status_index] != .conflicted or outcome[patchrev_index] == .conflicted) return; 166 167 if (target.status == .merged and std.meta.eql(target.patchrev_id, merged.patchrev_id)) { 168 merged.status = .merged; 169 outcome[status_index] = .kept; 170 } else if (parent.status == .merged and std.meta.eql(parent.patchrev_id, merged.patchrev_id)) { 171 merged.status = .merged; 172 outcome[status_index] = .parent; 173 } 174 } 175 176 fn statusSet( 177 comptime DB: type, 178 statuses: DB.SortedMap(.read_write), 179 status: Status, 180 ) !DB.SortedSet(.read_write) { 181 return DB.SortedSet(.read_write).init(try statuses.putCursor(@tagName(status))); 182 } 183 184 fn removeFromTagSets( 185 comptime DB: type, 186 tag_statuses: DB.SortedMap(.read_write), 187 tags: []const u8, 188 status: Status, 189 order_key: []const u8, 190 ) !void { 191 var tag_iter = tagIterator(tags); 192 while (tag_iter.next()) |tag| { 193 if (tag.len == 0 or tag.len > tag_max_len) continue; 194 var key_buffer: TagStatusKey = undefined; 195 const key = try tagStatusKey(&key_buffer, tag, status); 196 const set = try DB.SortedSet(.read_write).init(try tag_statuses.putCursor(key)); 197 _ = try set.remove(order_key); 198 if (0 == try set.count()) _ = try tag_statuses.remove(key); 199 } 200 } 201 202 pub fn readById( 203 comptime DB: type, 204 comptime hash_kind: hash.HashKind, 205 haxy_moment: DB.HashMap(.read_only), 206 arena: *std.heap.ArenaAllocator, 207 id: *const [evt.event_id_size]u8, 208 ) !?Record { 209 const records_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, record_map_key)) orelse return null; 210 const records = try DB.HashMap(.read_only).init(records_cursor); 211 const record_cursor = try records.getCursor(hash.hashInt(hash_kind, id)) orelse return null; 212 return try evt.read(Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(record_cursor)); 213 } 214