╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ event.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
102 // we call the item haxy_moments. it is a map of object id to haxy moment. 103 // in other words, it maps each object id to a hash map containing the 104 // state that the database was in when that object id was consumed. 105 var haxy_moments_cursor = try haxy.appendCursor(); 106 // use the previous haxy_moments as the basis for this one if it exists 107 if (try haxy.getCursor(-2)) |last_haxy_moments_cursor| { 108 try haxy_moments_cursor.write(.{ .slot = last_haxy_moments_cursor.slot() }); 109 } 110 var haxy_moments = try DB.HashMap(.read_write).init(haxy_moments_cursor); 111 112 // for each event we want to consume... 113 for (ctx.repo_events) |repo_event| { 114 // if this object id has already been consumed, skip it 115 if (null != try haxy_moments.getCursor(hash.bytesToInt(hash_kind, &repo_event.oid))) { 116 continue; 117 } 118 119 // compare last_object_id to the event's parent. this is important 120 // in situations where the branch was rebased, because in that case 121 // the last_object_id may no longer be valid. 122 if (last_object_id_maybe) |*last_object_id| { 123 if (repo_event.parent_oid) |*parent_oid| { 124 if (!std.mem.eql(u8, last_object_id, parent_oid)) { 125 // the last_object_id does not match the current event's parent id. 126 // this means that a rebase occurred. we just need to revert the 127 // haxy list to the state it was in what parent_oid was consumed, 128 // and then create a new haxy_moments map to work with. 129 130 const old_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(hash_kind, parent_oid)) orelse return error.CursorNotFound; 131 const old_moment = try DB.HashMap(.read_only).init(old_moment_cursor); 132 133 const old_moment_index_cursor = try old_moment.getCursor(hash.hashInt(hash_kind, "moment-index")) orelse return error.CursorNotFound; 134 const old_moment_index = try old_moment_index_cursor.readUint(); 135 136 // resize the haxy list so we truncate all the moments that were 137 // created after the parent_oid was consumed 138 try haxy.slice(old_moment_index + 1); 139 140 // make a new haxy moment and set its initial value to the last haxy moment 141 haxy_moments_cursor = try haxy.appendCursor(); 142 const old_haxy_moments_cursor = try haxy.getCursor(old_moment_index) orelse return error.CursorNotFound; 143 try haxy_moments_cursor.write(.{ .slot = old_haxy_moments_cursor.slot() }); 144 haxy_moments = try DB.HashMap(.read_write).init(haxy_moments_cursor); 145 146 last_object_id_maybe = parent_oid.*; 147 } 148 } else { 149 // the branch was rebased all the way to the very beginning. 150 // we have a repo event with no parent, which means it is now 151 // the very first event. all we need to do is set the haxy list 152 // to be empty and make a new haxy_moments map to work with. 153 154 try haxy.slice(0); 155 haxy_moments_cursor = try haxy.appendCursor(); 156 haxy_moments = try DB.HashMap(.read_write).init(haxy_moments_cursor); 157 158 last_object_id_maybe = null; 159 } 160 } else { 161 if (repo_event.parent_oid) |_| { 162 // there is no last_object_id, but this event has a parent. 163 // this is an invalid state. if the event has a parent, that 164 // implies that an event has already been processed, but 165 // if that's true then there would be a last_object_id. 166 return error.UnexpectedParent; 167 } 168 } 169 170 // create a moment for this object id 171 var haxy_moment_cursor = try haxy_moments.putCursor(hash.bytesToInt(hash_kind, &repo_event.oid)); 172 173 // if there was a previous object id, make this haxy moment's initial value to it. 174 // this efficiently "clones" the map so we make further modifications based on it. 175 if (last_object_id_maybe) |*last_object_id| { 176 if (try haxy_moments.getCursor(hash.bytesToInt(hash_kind, last_object_id))) |last_haxy_moment_cursor| { 177 try haxy_moment_cursor.write(.{ .slot = last_haxy_moment_cursor.slot() }); 178 } 179 } 180 181 const haxy_moment = try DB.HashMap(.read_write).init(haxy_moment_cursor); 182 183 // associate this moment with the index it will first appear at in the haxy list. 184 // this will be important later so we can truncate that list if the user ever 185 // rebases starting at this object id. 186 try haxy_moment.put(hash.hashInt(hash_kind, "moment-index"), .{ .uint = try haxy.count() - 1 }); 187 188 // consume the event into the views map 189 { 190 const event = repo_event.event; 191 192 // get the id of the current event as bytes 193 var current_event_id: [event_id_size]u8 = undefined; 194 _ = try std.fmt.hexToBytes(&current_event_id, &event.id); 195 196 switch (event.data) { 197 .issue => |data| { 198 const event_id_to_issue_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "event-id->issue")); 199 const event_id_to_issue = try DB.HashMap(.read_write).init(event_id_to_issue_cursor); 200 201 const issue_cursor = try event_id_to_issue.putCursor(hash.hashInt(hash_kind, &current_event_id)); 202 const issue = try DB.HashMap(.read_write).init(issue_cursor); 203 204 try upsert(DB, hash_kind, issue, @TypeOf(data), data); 205 }, 206 } 207 } 208 209 // the current object id is now the last one 210 last_object_id_maybe = repo_event.oid; 211 212 // prevent any of the data created above from being mutated by future iterations of this loop 213 try cursor.db.freeze(); 214 } 215 216 if (last_object_id_maybe) |*last_object_id| { 217 try moment.put(hash.hashInt(hash_kind, "haxy-last-object-id"), .{ .bytes = last_object_id }); 218 } 219 } 220 }; 221 222 try db_file.lock(io, .exclusive); 223 defer db_file.unlock(io); 224 225 // create a new transaction in the database that runs the above-defined Ctx function 226 const history = try DB.ArrayList(.read_write).init(db.rootCursor()); 227 try history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ .repo_events = repo_events }); 228 } 229 230 fn upsert( 231 comptime DB: type, 232 comptime hash_kind: hash.HashKind, 233 map: DB.HashMap(.read_write), 234 comptime Data: type, 235 data: Data, 236 ) !void { 237 switch (@typeInfo(Data)) { 238 .@"struct" => |struct_info| { 239 inline for (struct_info.fields) |field| { 240 try upsertField(DB, hash_kind, map, field.name, field.type, @field(data, field.name)); 241 } 242 }, 243 else => @compileError("upsert expects a struct"), 244 } 245 } 246 247 fn upsertField( 248 comptime DB: type, 249 comptime hash_kind: hash.HashKind, 250 map: DB.HashMap(.read_write), 251 comptime field_name: []const u8, 252 comptime Field: type, 253 value: Field, 254 ) !void { 255 const key = hash.hashInt(hash_kind, field_name); 256 257 switch (@typeInfo(Field)) { 258 .pointer => |pointer_info| { 259 if (pointer_info.size == .slice and pointer_info.child == u8) { 260 try upsertBytes(DB, hash_kind, map, key, value); 261 } else { 262 @compileError("unsupported upsert field type: " ++ @typeName(Field)); 263 } 264 }, 265 .array => |array_info| { 266 if (array_info.child == u8) { 267 try upsertBytes(DB, hash_kind, map, key, &value); 268 } else { 269 @compileError("unsupported upsert field type: " ++ @typeName(Field)); 270 } 271 }, 272 .int => |int_info| switch (int_info.signedness) { 273 .unsigned => { 274 if (try map.getCursor(key)) |value_cursor| { 275 if (try value_cursor.readUint() == value) { 276 return; 277 } 278 } 279 280 try map.put(key, .{ .uint = value }); 281 }, 282 .signed => { 283 if (try map.getCursor(key)) |value_cursor| { 284 if (try value_cursor.readInt() == value) { 285 return; 286 } 287 } 288 289 try map.put(key, .{ .int = value }); 290 }, 291 }, 292 else => @compileError("unsupported upsert field type: " ++ @typeName(Field)), 293 } 294 } 295 296 fn upsertBytes( 297 comptime DB: type, 298 comptime hash_kind: hash.HashKind, 299 map: DB.HashMap(.read_write), 300 key: hash.HashInt(hash_kind), 301 value: []const u8, 302 ) !void { 303 var existing_cursor_maybe = try map.getCursor(key); 304 if (existing_cursor_maybe) |*existing_cursor| { 305 if (try bytesEqual(DB, existing_cursor, value)) { 306 return; 307 } 308 } 309 310 var value_cursor = try map.putCursor(key); 311 var write_buffer: [1024]u8 = undefined; 312 var writer = try value_cursor.writer(&write_buffer); 313 try writer.interface.writeAll(value); 314 try writer.finish(); 315 } 316 317 fn bytesEqual( 318 comptime DB: type, 319 cursor: *DB.Cursor(.read_only), 320 value: []const u8, 321 ) !bool { 322 var read_buffer: [1024]u8 = undefined; 323 var reader = try cursor.reader(&read_buffer); 324 if (reader.size != value.len) { 325 return false; 326 } 327 328 var chunk_buffer: [1024]u8 = undefined; 329 var offset: usize = 0; 330 while (offset < value.len) { 331 const chunk_len = @min(chunk_buffer.len, value.len - offset); 332 try reader.interface.readSliceAll(chunk_buffer[0..chunk_len]); 333 if (!std.mem.eql(u8, chunk_buffer[0..chunk_len], value[offset .. offset + chunk_len])) { 334 return false; 335 } 336 offset += chunk_len; 337 } 338 339 return true; 340 } 341