╭ event.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const xit = @import("xit"); 3 const rp = xit.repo; 4 const hash = xit.hash; 5 6 const event_id_size: usize = 32; 7 8 pub const EventKind = enum { 9 issue, 10 }; 11 12 pub const EventData = union(EventKind) { 13 issue: struct { 14 title: []const u8, 15 description: []const u8, 16 tags: []const u8, 17 }, 18 19 pub fn read( 20 comptime DB: type, 21 comptime hash_kind: hash.HashKind, 22 allocator: std.mem.Allocator, 23 map: DB.HashMap(.read_only), 24 kind: EventKind, 25 ) !EventData { 26 return switch (kind) { 27 .issue => .{ 28 .issue = .{ 29 .title = try readBytes(DB, hash_kind, allocator, map, "title"), 30 .description = try readBytes(DB, hash_kind, allocator, map, "description"), 31 .tags = try readBytes(DB, hash_kind, allocator, map, "tags"), 32 }, 33 }, 34 }; 35 } 36 37 fn readBytes( 38 comptime DB: type, 39 comptime hash_kind: hash.HashKind, 40 allocator: std.mem.Allocator, 41 map: DB.HashMap(.read_only), 42 field_name: []const u8, 43 ) ![]const u8 { 44 const cursor = try map.getCursor(hash.hashInt(hash_kind, field_name)) orelse return error.NotFound; 45 return try cursor.readBytesAlloc(allocator, null); 46 } 47 }; 48 49 pub const Event = struct { 50 id: [event_id_size * 2]u8, 51 data: EventData, 52 }; 53 54 /// associates an Event with a git object id. 55 /// we save this oid in the database so we can reliably re-consume 56 /// events if their oid changed. this can happen if the branch 57 /// is rebased and force-pushed. 58 pub fn RepoEvent(comptime hash_kind: hash.HashKind) type { 59 return struct { 60 parent_oid: ?[hash.byteLen(hash_kind)]u8, 61 oid: [hash.byteLen(hash_kind)]u8, 62 event: Event, 63 }; 64 } 65 66 pub fn randomId(random: std.Random) [event_id_size]u8 { 67 var id_bytes: [event_id_size]u8 = undefined; 68 random.bytes(&id_bytes); 69 return id_bytes; 70 } 71 72 pub fn consume( 73 comptime DB: type, 74 comptime hash_kind: hash.HashKind, 75 io: std.Io, 76 db: *DB, 77 db_file: std.Io.File, 78 repo_events: []RepoEvent(hash_kind), 79 ) !void { 80 const Ctx = struct { 81 repo_events: []RepoEvent(hash_kind), 82 history_count: u64, 83 84 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 85 const moment = try DB.HashMap(.read_write).init(cursor.*); 86 87 // the map with all of haxy's state and materialized views 88 const haxy_cursor = try moment.putCursor(hash.hashInt(hash_kind, "haxy")); 89 const haxy = try DB.HashMap(.read_write).init(haxy_cursor); 90 91 // try reading the last object id that was consumed 92 var last_object_id_maybe: ?[hash.byteLen(hash_kind)]u8 = null; 93 if (try haxy.getCursor(hash.hashInt(hash_kind, "last-object-id"))) |last_object_id_cursor| { 94 var last_object_id_buffer: [hash.byteLen(hash_kind)]u8 = undefined; 95 _ = try last_object_id_cursor.readBytes(&last_object_id_buffer); 96 last_object_id_maybe = last_object_id_buffer; 97 } 98 99 // for each event we want to consume... 100 for (ctx.repo_events) |repo_event| { 101 // map that associates this oid with the current transaction id. 102 // this will be important for reverting things when a rebase occurrs. 103 var object_id_to_tx_id_cursor = try haxy.putCursor(hash.hashInt(hash_kind, "object-id->tx-id")); 104 105 // map with the views as they appeared when each event was consumed. 106 // we can use this to see (and revert) the views to any previous state. 107 var object_id_to_views_cursor = try haxy.putCursor(hash.hashInt(hash_kind, "object-id->views")); 108 109 // if this object id has already been consumed, skip it 110 { 111 const object_id_to_views = try DB.HashMap(.read_only).init(object_id_to_views_cursor.readOnly()); 112 if (null != try object_id_to_views.getCursor(hash.bytesToInt(hash_kind, &repo_event.oid))) { 113 continue; 114 } 115 } 116 117 // compare last-object-id to the event's parent. this is important 118 // in situations where the branch was rebased, because in that case 119 // the last-object-id may no longer be valid. 120 if (last_object_id_maybe) |*last_object_id| { 121 if (repo_event.parent_oid) |*parent_oid| { 122 if (!std.mem.eql(u8, last_object_id, parent_oid)) { 123 // the last-object-id does not match the current event's parent id. 124 // this means that a rebase occurred. what we need to do is just 125 // revert the two object-id maps to the state they were in when 126 // parent-oid was consumed. 127 128 const object_id_to_tx_id = try DB.HashMap(.read_only).init(object_id_to_tx_id_cursor.readOnly()); 129 const tx_id_cursor = try object_id_to_tx_id.getCursor(hash.bytesToInt(hash_kind, parent_oid)) orelse return error.ObjectNotFound; 130 const tx_id = try tx_id_cursor.readUint(); 131 const history = try DB.ArrayList(.read_only).init(cursor.db.rootCursor().readOnly()); 132 133 const old_moment_cursor = try history.getCursor(tx_id) orelse return error.TransactionNotFound; 134 const old_moment = try DB.HashMap(.read_only).init(old_moment_cursor); 135 136 const old_haxy_cursor = try old_moment.getCursor(hash.hashInt(hash_kind, "haxy")) orelse return error.CursorNotFound; 137 const old_haxy = try DB.HashMap(.read_only).init(old_haxy_cursor); 138 139 const old_object_id_to_tx_id_cursor = try old_haxy.getCursor(hash.hashInt(hash_kind, "object-id->tx-id")) orelse return error.CursorNotFound; 140 const old_object_id_to_views_cursor = try old_haxy.getCursor(hash.hashInt(hash_kind, "object-id->views")) orelse return error.CursorNotFound; 141 142 try object_id_to_tx_id_cursor.write(.{ .slot = old_object_id_to_tx_id_cursor.slot() }); 143 try object_id_to_views_cursor.write(.{ .slot = old_object_id_to_views_cursor.slot() }); 144 145 last_object_id_maybe = parent_oid.*; 146 } 147 } else { 148 // the branch was rebased all the way to the very beginning. 149 // we have a repo event with no parent, which means it is now 150 // the very first event. all we need to do is set the 151 // object-id->views map to be empty so we can rebuild it. 152 try object_id_to_views_cursor.write(.{ .slot = null }); 153 } 154 } else { 155 if (repo_event.parent_oid) |_| { 156 // there is no last-object-id, but this event has a parent. 157 // this is an invalid state. if the event has a parent, that 158 // implies that an event has already been processed, but 159 // if that's true then there would be a last-object-id. 160 return error.UnexpectedParent; 161 } 162 } 163 164 // associate this object id with this transaction id 165 const object_id_to_tx_id = try DB.HashMap(.read_write).init(object_id_to_tx_id_cursor); 166 try object_id_to_tx_id.put(hash.bytesToInt(hash_kind, &repo_event.oid), .{ .uint = ctx.history_count }); 167 168 // init the object-id->views map 169 const object_id_to_views = try DB.HashMap(.read_write).init(object_id_to_views_cursor); 170 171 // create a new views map for the current event we are consuming 172 var views_cursor = try object_id_to_views.putCursor(hash.bytesToInt(hash_kind, &repo_event.oid)); 173 174 // if there was a previous event, set the views map to have the same value as it. 175 // this efficiently "clones" the map so we make further modifications based on it. 176 if (last_object_id_maybe) |*last_object_id| { 177 if (try object_id_to_views.getCursor(hash.bytesToInt(hash_kind, last_object_id))) |last_view_cursor| { 178 try views_cursor.write(.{ .slot = last_view_cursor.slot() }); 179 } 180 } 181 182 // consume the event into the views map 183 { 184 const event = repo_event.event; 185 186 // get the id of the current event as bytes 187 var current_event_id: [event_id_size]u8 = undefined; 188 _ = try std.fmt.hexToBytes(&current_event_id, &event.id); 189 190 const views = try DB.HashMap(.read_write).init(views_cursor); 191 192 switch (event.data) { 193 .issue => |data| { 194 const event_id_to_issue_cursor = try views.putCursor(hash.hashInt(hash_kind, "event-id->issue")); 195 const event_id_to_issue = try DB.HashMap(.read_write).init(event_id_to_issue_cursor); 196 197 const issue_cursor = try event_id_to_issue.putCursor(hash.hashInt(hash_kind, &current_event_id)); 198 const issue = try DB.HashMap(.read_write).init(issue_cursor); 199 200 try upsert(DB, hash_kind, issue, @TypeOf(data), data); 201 }, 202 } 203 } 204 205 // the current object id is now the last one 206 last_object_id_maybe = repo_event.oid; 207 208 // prevent any of the data created above from being mutated by future iterations of this loop 209 try cursor.db.freeze(); 210 } 211 212 if (last_object_id_maybe) |*last_object_id| { 213 try haxy.put(hash.hashInt(hash_kind, "last-object-id"), .{ .bytes = last_object_id }); 214 } 215 } 216 }; 217 218 try db_file.lock(io, .exclusive); 219 defer db_file.unlock(io); 220 221 // create a new transaction in the database that runs the above-defined Ctx function 222 const history = try DB.ArrayList(.read_write).init(db.rootCursor()); 223 try history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ 224 .repo_events = repo_events, 225 .history_count = try history.count(), 226 }); 227 } 228 229 fn upsert( 230 comptime DB: type, 231 comptime hash_kind: hash.HashKind, 232 map: DB.HashMap(.read_write), 233 comptime Data: type, 234 data: Data, 235 ) !void { 236 switch (@typeInfo(Data)) { 237 .@"struct" => |struct_info| { 238 inline for (struct_info.fields) |field| { 239 try upsertField(DB, hash_kind, map, field.name, field.type, @field(data, field.name)); 240 } 241 }, 242 else => @compileError("upsert expects a struct"), 243 } 244 } 245 246 fn upsertField( 247 comptime DB: type, 248 comptime hash_kind: hash.HashKind, 249 map: DB.HashMap(.read_write), 250 comptime field_name: []const u8, 251 comptime Field: type, 252 value: Field, 253 ) !void { 254 const key = hash.hashInt(hash_kind, field_name); 255 256 switch (@typeInfo(Field)) { 257 .pointer => |pointer_info| { 258 if (pointer_info.size == .slice and pointer_info.child == u8) { 259 try upsertBytes(DB, hash_kind, map, key, value); 260 } else { 261 @compileError("unsupported upsert field type: " ++ @typeName(Field)); 262 } 263 }, 264 .array => |array_info| { 265 if (array_info.child == u8) { 266 try upsertBytes(DB, hash_kind, map, key, &value); 267 } else { 268 @compileError("unsupported upsert field type: " ++ @typeName(Field)); 269 } 270 }, 271 .int => |int_info| switch (int_info.signedness) { 272 .unsigned => { 273 if (try map.getCursor(key)) |value_cursor| { 274 if (try value_cursor.readUint() == value) { 275 return; 276 } 277 } 278 279 try map.put(key, .{ .uint = value }); 280 }, 281 .signed => { 282 if (try map.getCursor(key)) |value_cursor| { 283 if (try value_cursor.readInt() == value) { 284 return; 285 } 286 } 287 288 try map.put(key, .{ .int = value }); 289 }, 290 }, 291 else => @compileError("unsupported upsert field type: " ++ @typeName(Field)), 292 } 293 } 294 295 fn upsertBytes( 296 comptime DB: type, 297 comptime hash_kind: hash.HashKind, 298 map: DB.HashMap(.read_write), 299 key: hash.HashInt(hash_kind), 300 value: []const u8, 301 ) !void { 302 var existing_cursor_maybe = try map.getCursor(key); 303 if (existing_cursor_maybe) |*existing_cursor| { 304 if (try bytesEqual(DB, existing_cursor, value)) { 305 return; 306 } 307 } 308 309 var value_cursor = try map.putCursor(key); 310 var write_buffer: [1024]u8 = undefined; 311 var writer = try value_cursor.writer(&write_buffer); 312 try writer.interface.writeAll(value); 313 try writer.finish(); 314 } 315 316 fn bytesEqual( 317 comptime DB: type, 318 cursor: *DB.Cursor(.read_only), 319 value: []const u8, 320 ) !bool { 321 var read_buffer: [1024]u8 = undefined; 322 var reader = try cursor.reader(&read_buffer); 323 if (reader.size != value.len) { 324 return false; 325 } 326 327 var chunk_buffer: [1024]u8 = undefined; 328 var offset: usize = 0; 329 while (offset < value.len) { 330 const chunk_len = @min(chunk_buffer.len, value.len - offset); 331 try reader.interface.readSliceAll(chunk_buffer[0..chunk_len]); 332 if (!std.mem.eql(u8, chunk_buffer[0..chunk_len], value[offset .. offset + chunk_len])) { 333 return false; 334 } 335 offset += chunk_len; 336 } 337 338 return true; 339 } 340