╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ event.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1023 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 1024 const haxy_moments = try DB.HashMap(.read_only).init(haxy_moments_cursor); 1025 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(hash_kind, &last_object_id)) orelse return error.NotFound; 1026 return try DB.HashMap(.read_only).init(haxy_moment_cursor); 1027 } 1028 1029 // build the key for SortedSets sorted by timestamp. the big-endian timestamp 1030 // makes byte order match creation order; the event id breaks ties and keeps 1031 // keys unique within the same timestamp. 1032 pub fn orderKey(timestamp: u64, event_id: *const [event_id_size]u8) [@sizeOf(u64) + event_id_size]u8 { 1033 var key: [@sizeOf(u64) + event_id_size]u8 = undefined; 1034 std.mem.writeInt(u64, key[0..@sizeOf(u64)], timestamp, .big); 1035 @memcpy(key[@sizeOf(u64)..], event_id); 1036 return key; 1037 } 1038 1039 pub fn orderKeyDesc(timestamp: u64, event_id: *const [event_id_size]u8) [@sizeOf(u64) + event_id_size]u8 { 1040 return orderKey(std.math.maxInt(u64) - timestamp, event_id); 1041 } 1042 1043 // the event id embedded in a set entry's order key 1044 pub fn readOrderKeyId(comptime DB: type, kv_pair_cursor: DB.Cursor(.read_only)) ![event_id_size]u8 { 1045 var cursor = kv_pair_cursor; 1046 const kv_pair = try cursor.readKeyValuePair(); 1047 var order_key: [@sizeOf(u64) + event_id_size]u8 = undefined; 1048 _ = try kv_pair.key_cursor.readBytes(&order_key); 1049 return order_key[@sizeOf(u64)..][0..event_id_size].*; 1050 } 1051 1052 // split a pushed "<owner>/<repo>" path into its two components, or null if it 1053 // isn't exactly two non-empty segments. 1054 pub fn parseOwnerRepoPath(path: []const u8) ?struct { owner: []const u8, name: []const u8 } { 1055 var it = std.mem.splitScalar(u8, path, '/'); 1056 const owner = it.next() orelse return null; 1057 const name = it.next() orelse return null; 1058 if (it.next() != null) return null; 1059 if (owner.len == 0 or name.len == 0) return null; 1060 1061 return .{ .owner = owner, .name = name }; 1062 } 1063 1064 // resolve a pushed `<owner>/<repo>` to the hex event id that names its on-disk 1065 // directory under the repos dir 1066 pub fn resolveOrCreateRepo( 1067 io: std.Io, 1068 allocator: std.mem.Allocator, 1069 admin_repo_path: []const u8, 1070 owner_name: []const u8, 1071 repo_name: []const u8, 1072 create_if_missing: bool, 1073 ) !?[event_id_size * 2]u8 { 1074 var repo = rp.Repo(.xit, admin_repo_opts).open(io, allocator, .{ .path = admin_repo_path }) catch |err| switch (err) { 1075 error.RepoNotFound => return null, 1076 else => |e| return e, 1077 }; 1078 defer repo.deinit(io, allocator); 1079 1080 var arena = std.heap.ArenaAllocator.init(allocator); 1081 defer arena.deinit(); 1082 1083 const moment = try currentMoment(admin_repo_opts, &repo); 1084 1085 // an already-registered repo reuses its event id, so a re-push (or a clone) 1086 // lands in the same repo 1087 if (try Repo.readByOwnerAndName(AdminDB, admin_repo_opts.hash, moment, &arena, owner_name, repo_name)) |found| { 1088 return std.fmt.bytesToHex(found.event_id, .lower); 1089 } 1090 1091 if (!create_if_missing) return null; 1092 1093 // the new repo is owned by the named user, read from the name index; an 1094 // unknown owner can't own a repo, so the push is rejected 1095 const name_to_user_id_cursor = try moment.getCursor(hash.hashInt(admin_repo_opts.hash, "name->user-id")) orelse return null; 1096 const name_to_user_id = try AdminDB.HashMap(.read_only).init(name_to_user_id_cursor); 1097 const owner_id_cursor = try name_to_user_id.getCursor(hash.hashInt(admin_repo_opts.hash, owner_name)) orelse return null; 1098 var owner_user_id: [event_id_size]u8 = undefined; 1099 _ = try owner_id_cursor.readBytes(&owner_user_id); 1100 1101 const owner = (try User.readById(AdminDB, admin_repo_opts.hash, moment, &arena, &owner_user_id)) orelse unreachable; 1102 1103 var id_bytes: [event_id_size]u8 = undefined; 1104 io.random(&id_bytes); 1105 const event_id_hex = std.fmt.bytesToHex(id_bytes, .lower); 1106 1107 try consume(.xit, admin_repo_opts, io, allocator, &repo, events_ref, &[_]EventWithId{.{ 1108 .id = event_id_hex, 1109 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), 1110 .author_email = owner.event.email, 1111 .event = .{ .repo = .{ 1112 .user_id = &owner_user_id, 1113 .name = repo_name, 1114 .description = "", 1115 } }, 1116 }}); 1117 1118 return event_id_hex; 1119 } 1120 1121 // 1122 // reading from xitdb 1123 // 1124 1125 // a struct field's fields share its container's map, so their names must not 1126 // collide with the container's or two fields would silently share a key 1127 fn validateFlattenedFields(comptime T: type) void { 1128 comptime { 1129 for (std.meta.fields(T)) |field| { 1130 if (@typeInfo(field.type) != .@"struct") continue; 1131 for (std.meta.fields(field.type)) |nested| { 1132 if (@hasField(T, nested.name)) 1133 @compileError("flattened field name collision in " ++ @typeName(T) ++ ": " ++ nested.name); 1134 } 1135 } 1136 } 1137 } 1138 1139 pub fn read( 1140 comptime T: type, 1141 comptime DB: type, 1142 comptime hash_kind: hash.HashKind, 1143 arena: *std.heap.ArenaAllocator, 1144 map: DB.HashMap(.read_only), 1145 ) !T { 1146 var event: T = undefined; 1147 1148 switch (@typeInfo(T)) { 1149 .@"struct" => |struct_info| { 1150 comptime validateFlattenedFields(T); 1151 inline for (struct_info.fields) |field| { 1152 switch (@typeInfo(field.type)) { 1153 .pointer => |pointer_info| { 1154 if (pointer_info.size == .slice and pointer_info.child == u8 and pointer_info.is_const) { 1155 @field(event, field.name) = try readBytes(DB, hash_kind, arena.allocator(), map, field.name); 1156 } else { 1157 @compileError("unsupported read field type: " ++ @typeName(field.type)); 1158 } 1159 }, 1160 .array => |array_info| { 1161 if (array_info.child == u8) { 1162 const bytes = try readBytes(DB, hash_kind, arena.allocator(), map, field.name); 1163 if (bytes.len != array_info.len) return error.InvalidByteArrayLength; 1164 @memcpy(@field(event, field.name)[0..], bytes); 1165 } else { 1166 @compileError("unsupported read field type: " ++ @typeName(field.type)); 1167 } 1168 }, 1169 .bool => @field(event, field.name) = try readBool(DB, hash_kind, map, field.name), 1170 .int => |int_info| switch (int_info.signedness) { 1171 .unsigned => @field(event, field.name) = @intCast(try readUint(DB, hash_kind, map, field.name)), 1172 .signed => @field(event, field.name) = @intCast(try readInt(DB, hash_kind, map, field.name)), 1173 }, 1174 .@"enum" => { 1175 const bytes = try readBytes(DB, hash_kind, arena.allocator(), map, field.name); 1176 @field(event, field.name) = std.meta.stringToEnum(field.type, bytes) orelse return error.InvalidEnumTag; 1177 }, 1178 .optional => |optional_info| { 1179 if (optional_info.child != []const u8) @compileError("unsupported read field type: " ++ @typeName(field.type)); 1180 // a missing key is null 1181 @field(event, field.name) = if (try map.getCursor(hash.hashInt(hash_kind, field.name))) |cursor| 1182 try cursor.readBytesAlloc(arena.allocator(), null) 1183 else 1184 null; 1185 }, 1186 // a struct field's own fields are read from the same map 1187 .@"struct" => @field(event, field.name) = try read(field.type, DB, hash_kind, arena, map), 1188 else => @compileError("unsupported read field type: " ++ @typeName(field.type)), 1189 } 1190 } 1191 }, 1192 else => @compileError("read expects a struct"), 1193 } 1194 1195 return event; 1196 } 1197 1198 // whether two events carry the same data 1199 fn fieldsEqual(comptime T: type, a: T, b: T) bool { 1200 switch (@typeInfo(T)) { 1201 .@"struct" => |struct_info| { 1202 inline for (struct_info.fields) |field| { 1203 if (!fieldEqual(field.type, @field(a, field.name), @field(b, field.name))) return false; 1204 } 1205 }, 1206 else => @compileError("fieldsEqual expects a struct"), 1207 } 1208 1209 return true; 1210 } 1211 1212 pub fn fieldEqual(comptime Field: type, a: Field, b: Field) bool { 1213 switch (@typeInfo(Field)) { 1214 .pointer => |pointer_info| { 1215 if (pointer_info.size == .slice and pointer_info.child == u8) { 1216 return std.mem.eql(u8, a, b); 1217 } else { 1218 @compileError("unsupported field type: " ++ @typeName(Field)); 1219 } 1220 }, 1221 .array => |array_info| { 1222 if (array_info.child == u8) { 1223 return std.mem.eql(u8, &a, &b); 1224 } else { 1225 @compileError("unsupported field type: " ++ @typeName(Field)); 1226 } 1227 }, 1228 .bool, .int, .@"enum" => return a == b, 1229 else => @compileError("unsupported field type: " ++ @typeName(Field)), 1230 } 1231 } 1232 1233 fn readBytes( 1234 comptime DB: type, 1235 comptime hash_kind: hash.HashKind, 1236 allocator: std.mem.Allocator, 1237 map: DB.HashMap(.read_only), 1238 field_name: []const u8, 1239 ) ![]const u8 { 1240 const cursor = try map.getCursor(hash.hashInt(hash_kind, field_name)) orelse return error.NotFound; 1241 return try cursor.readBytesAlloc(allocator, null); 1242 } 1243 1244 fn readUint( 1245 comptime DB: type, 1246 comptime hash_kind: hash.HashKind, 1247 map: DB.HashMap(.read_only), 1248 field_name: []const u8, 1249 ) !u64 { 1250 const cursor = try map.getCursor(hash.hashInt(hash_kind, field_name)) orelse return error.NotFound; 1251 return try cursor.readUint(); 1252 } 1253 1254 fn readInt( 1255 comptime DB: type, 1256 comptime hash_kind: hash.HashKind, 1257 map: DB.HashMap(.read_only), 1258 field_name: []const u8, 1259 ) !i64 { 1260 const cursor = try map.getCursor(hash.hashInt(hash_kind, field_name)) orelse return error.NotFound; 1261 return try cursor.readInt(); 1262 } 1263 1264 fn readBool( 1265 comptime DB: type, 1266 comptime hash_kind: hash.HashKind, 1267 map: DB.HashMap(.read_only), 1268 field_name: []const u8, 1269 ) !bool { 1270 const cursor = try map.getCursor(hash.hashInt(hash_kind, field_name)) orelse return error.NotFound; 1271 var buffer: [5]u8 = undefined; 1272 const bytes = try cursor.readBytesObject(&buffer); 1273 const format_tag = bytes.format_tag orelse return error.InvalidFormatTag; 1274 if (!std.mem.eql(u8, &format_tag, "bl")) return error.InvalidFormatTag; 1275 if (std.mem.eql(u8, bytes.value, "true")) return true; 1276 if (std.mem.eql(u8, bytes.value, "false")) return false; 1277 return error.InvalidBool; 1278 } 1279 1280 // 1281 // writing to xitdb 1282 // 1283 1284 pub fn upsert( 1285 comptime T: type, 1286 comptime DB: type, 1287 comptime hash_kind: hash.HashKind, 1288 map: DB.HashMap(.read_write), 1289 event: T, 1290 ) !void { 1291 switch (@typeInfo(T)) { 1292 .@"struct" => |struct_info| { 1293 comptime validateFlattenedFields(T); 1294 inline for (struct_info.fields) |field| { 1295 try upsertField(DB, hash_kind, map, field.name, field.type, @field(event, field.name)); 1296 } 1297 }, 1298 else => @compileError("upsert expects a struct"), 1299 } 1300 } 1301 1302 fn upsertField( 1303 comptime DB: type, 1304 comptime hash_kind: hash.HashKind, 1305 map: DB.HashMap(.read_write), 1306 comptime field_name: []const u8, 1307 comptime Field: type, 1308 value: Field, 1309 ) !void { 1310 const key = hash.hashInt(hash_kind, field_name); 1311 1312 switch (@typeInfo(Field)) { 1313 .pointer => |pointer_info| { 1314 if (pointer_info.size == .slice and pointer_info.child == u8) { 1315 try upsertBytes(DB, hash_kind, map, key, value); 1316 } else { 1317 @compileError("unsupported upsert field type: " ++ @typeName(Field)); 1318 } 1319 }, 1320 .array => |array_info| { 1321 if (array_info.child == u8) { 1322 try upsertBytes(DB, hash_kind, map, key, &value); 1323 } else { 1324 @compileError("unsupported upsert field type: " ++ @typeName(Field)); 1325 } 1326 }, 1327 .int => |int_info| switch (int_info.signedness) { 1328 .unsigned => { 1329 if (try map.getCursor(key)) |value_cursor| { 1330 if (try value_cursor.readUint() == value) { 1331 return; 1332 } 1333 } 1334 1335 try map.put(key, .{ .uint = value }); 1336 }, 1337 .signed => { 1338 if (try map.getCursor(key)) |value_cursor| { 1339 if (try value_cursor.readInt() == value) { 1340 return; 1341 } 1342 } 1343 1344 try map.put(key, .{ .int = value }); 1345 }, 1346 }, 1347 .bool => { 1348 const bytes = if (value) "true" else "false"; 1349 if (try map.getCursor(key)) |value_cursor| { 1350 var buffer: [5]u8 = undefined; 1351 const existing = try value_cursor.readBytesObject(&buffer); 1352 if (existing.format_tag) |format_tag| { 1353 if (std.mem.eql(u8, &format_tag, "bl") and std.mem.eql(u8, existing.value, bytes)) { 1354 return; 1355 } 1356 } 1357 } 1358 1359 try map.put(key, .{ .bytes_object = .{ .value = bytes, .format_tag = "bl".* } }); 1360 }, 1361 .@"enum" => try upsertBytes(DB, hash_kind, map, key, @tagName(value)), 1362 .optional => |optional_info| { 1363 if (optional_info.child != []const u8) @compileError("unsupported upsert field type: " ++ @typeName(Field)); 1364 // a missing key is null 1365 if (value) |bytes| { 1366 try upsertBytes(DB, hash_kind, map, key, bytes); 1367 } else { 1368 _ = try map.remove(key); 1369 } 1370 }, 1371 // a struct field's own fields are stored in the same map 1372 .@"struct" => try upsert(Field, DB, hash_kind, map, value), 1373 else => @compileError("unsupported upsert field type: " ++ @typeName(Field)), 1374 } 1375 } 1376 1377 fn upsertBytes( 1378 comptime DB: type, 1379 comptime hash_kind: hash.HashKind, 1380 map: DB.HashMap(.read_write), 1381 key: hash.HashInt(hash_kind), 1382 value: []const u8, 1383 ) !void { 1384 var existing_cursor_maybe = try map.getCursor(key); 1385 if (existing_cursor_maybe) |*existing_cursor| { 1386 if (try bytesEqual(DB, existing_cursor, value)) { 1387 return; 1388 } 1389 } 1390 1391 var value_cursor = try map.putCursor(key); 1392 var write_buffer: [1024]u8 = undefined; 1393 var writer = try value_cursor.writer(&write_buffer); 1394 try writer.interface.writeAll(value); 1395 try writer.finish(); 1396 } 1397 1398 fn bytesEqual( 1399 comptime DB: type, 1400 cursor: *DB.Cursor(.read_only), 1401 value: []const u8, 1402 ) !bool { 1403 var read_buffer: [1024]u8 = undefined; 1404 var reader = try cursor.reader(&read_buffer); 1405 if (reader.size != value.len) { 1406 return false; 1407 } 1408 1409 var chunk_buffer: [1024]u8 = undefined; 1410 var offset: usize = 0; 1411 while (offset < value.len) { 1412 const chunk_len = @min(chunk_buffer.len, value.len - offset); 1413 try reader.interface.readSliceAll(chunk_buffer[0..chunk_len]); 1414 if (!std.mem.eql(u8, chunk_buffer[0..chunk_len], value[offset .. offset + chunk_len])) { 1415 return false; 1416 } 1417 offset += chunk_len; 1418 } 1419 1420 return true; 1421 } 1422 1423 // 1424 // CommitIterator 1425 // 1426 // differs from xit's ObjectIterator in a few ways: 1427 // 1428 // 1. iterates parents first, rather than children first 1429 // 2. `next` only returns the object id, not the full Object 1430 // 3. stores its temporary state in a file on disk to avoid OOMs 1431 // 1432 1433 pub fn CommitIterator(comptime repo_kind: rp.RepoKind, comptime repo_opts: rp.RepoOpts(repo_kind)) type { 1434 return struct { 1435 const DB = EventDB(repo_opts.hash); 1436 const db_name = "haxy-repo-events.db"; 1437 1438 repo_dir: std.Io.Dir, 1439 db_file: std.Io.File, 1440 db: *DB, 1441 // map of each object id to its children 1442 parent_to_children: DB.HashMap(.read_write), 1443 // tracking pending parents is necessary because a child can only 1444 // be returned after all of its unconsumed parents have been returned 1445 child_to_pending_parents: DB.HashMap(.read_write), 1446 // queue of object ids that are ready to be returned from `next` 1447 ready_queue: DB.ArrayList(.read_write), 1448 ready_queue_index: u64, 1449 // the object id at the tip of this branch 1450 newest_object_id: ?[hash.byteLen(repo_opts.hash)]u8, 1451 // the object id furthest back in the history that hasn't been consumed 1452 oldest_object_id: ?[hash.byteLen(repo_opts.hash)]u8, 1453 1454 pub fn init( 1455 state: rp.Repo(repo_kind, repo_opts).State(.read_only), 1456 io: std.Io, 1457 allocator: std.mem.Allocator, 1458 consumed_object_ids: DB.HashMap(.read_only), 1459 ref: rf.Ref, 1460 ) !CommitIterator(repo_kind, repo_opts) { 1461 const db_file = try state.core.repo_dir.createFile(io, db_name, .{ .truncate = true, .lock = .exclusive, .read = true }); 1462 errdefer { 1463 db_file.close(io); 1464 state.core.repo_dir.deleteFile(io, db_name) catch {}; 1465 } 1466 1467 const buffer_ptr = try allocator.create(std.Io.Writer.Allocating); 1468 errdefer allocator.destroy(buffer_ptr); 1469 1470 buffer_ptr.* = std.Io.Writer.Allocating.init(allocator); 1471 errdefer buffer_ptr.deinit(); 1472 1473 const db_ptr = try allocator.create(DB); 1474 errdefer allocator.destroy(db_ptr); 1475 // the db is scratch state that gets deleted after iteration, so 1476 // there is nothing worth fsyncing 1477 db_ptr.* = try DB.init(.{ .io = io, .file = db_file, .buffer = buffer_ptr, .fsync = false }); 1478 1479 const map = try DB.HashMap(.read_write).init(db_ptr.rootCursor()); 1480 1481 const parent_to_children_cursor = try map.putCursor(hash.hashInt(repo_opts.hash, "parent->children")); 1482 const parent_to_children = try DB.HashMap(.read_write).init(parent_to_children_cursor); 1483 1484 const child_to_pending_parents_cursor = try map.putCursor(hash.hashInt(repo_opts.hash, "child->pending-parents")); 1485 const child_to_pending_parents = try DB.HashMap(.read_write).init(child_to_pending_parents_cursor); 1486 1487 const ready_queue_cursor = try map.putCursor(hash.hashInt(repo_opts.hash, "ready-queue")); 1488 const ready_queue = try DB.ArrayList(.read_write).init(ready_queue_cursor); 1489 1490 var self = CommitIterator(repo_kind, repo_opts){ 1491 .repo_dir = state.core.repo_dir, 1492 .db_file = db_file, 1493 .db = db_ptr, 1494 .parent_to_children = parent_to_children, 1495 .child_to_pending_parents = child_to_pending_parents, 1496 .ready_queue = ready_queue, 1497 .ready_queue_index = 0, 1498 .newest_object_id = null, 1499 .oldest_object_id = null, 1500 }; 1501 errdefer self.deinit(io, allocator); 1502 1503 try self.collect(state, io, allocator, consumed_object_ids, ref); 1504 1505 return self; 1506 } 1507 1508 pub fn deinit(self: *CommitIterator(repo_kind, repo_opts), io: std.Io, allocator: std.mem.Allocator) void { 1509 self.db_file.close(io); 1510 self.db.core.memory.buffer.deinit(); 1511 allocator.destroy(self.db.core.memory.buffer); 1512 self.repo_dir.deleteFile(io, db_name) catch {}; 1513 allocator.destroy(self.db); 1514 } 1515 1516 pub fn next(self: *CommitIterator(repo_kind, repo_opts)) !?[hash.byteLen(repo_opts.hash)]u8 { 1517 if (self.ready_queue_index >= try self.ready_queue.count()) return null; 1518 1519 const oid = try readOidFromList(self.ready_queue.readOnly(), self.ready_queue_index); 1520 self.ready_queue_index += 1; 1521 1522 try self.enqueueChildren(&oid); 1523 return oid; 1524 } 1525 1526 fn collect( 1527 self: *CommitIterator(repo_kind, repo_opts), 1528 state: rp.Repo(repo_kind, repo_opts).State(.read_only), 1529 io: std.Io, 1530 allocator: std.mem.Allocator, 1531 consumed_object_ids: DB.HashMap(.read_only), 1532 ref: rf.Ref, 1533 ) !void { 1534 const map = try DB.HashMap(.read_write).init(self.db.rootCursor()); 1535 1536 const walk_queue_cursor = try map.putCursor(hash.hashInt(repo_opts.hash, "walk-queue")); 1537 const walk_queue = try DB.ArrayList(.read_write).init(walk_queue_cursor); 1538 var walk_queue_index: u64 = 0; 1539 1540 const seen_object_ids_cursor = try map.putCursor(hash.hashInt(repo_opts.hash, "seen-object-ids")); 1541 const seen_object_ids = try DB.HashSet(.read_write).init(seen_object_ids_cursor); 1542 1543 const head_oid_hex = (try rf.readRecur(repo_kind, repo_opts, state, io, .{ .ref = ref })) orelse return error.OidNotFound; 1544 var head_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 1545 _ = try std.fmt.hexToBytes(&head_oid, &head_oid_hex); 1546 try walk_queue.append(.{ .bytes = &head_oid }); 1547 1548 while (walk_queue_index < try walk_queue.count()) { 1549 const oid = try readOidFromList(walk_queue.readOnly(), walk_queue_index); 1550 const oid_int = hash.bytesToInt(repo_opts.hash, &oid); 1551 1552 walk_queue_index += 1; 1553 1554 // if we've seen this object id, skip it 1555 if (null != try seen_object_ids.getCursor(oid_int)) { 1556 continue; 1557 } 1558 1559 try seen_object_ids.put(oid_int, .{ .bytes = &oid }); 1560 1561 var commit_object = try obj.Object(repo_kind, repo_opts).init(state, io, allocator, &std.fmt.bytesToHex(oid, .lower)); 1562 defer commit_object.deinit(); 1563 1564 // if this object id has already been consumed, skip it 1565 if (null != try consumed_object_ids.getCursor(oid_int)) { 1566 continue; 1567 } 1568 1569 if (self.newest_object_id == null) self.newest_object_id = oid; 1570 self.oldest_object_id = oid; 1571 1572 const parent_oids = commit_object.content.commit.metadata.parent_oids orelse return error.ParentOidsNotFound; 1573 var pending_parent_count: u64 = 0; 1574 for (parent_oids) |*parent_oid| { 1575 var parent_oid_bytes: [hash.byteLen(repo_opts.hash)]u8 = undefined; 1576 _ = try std.fmt.hexToBytes(&parent_oid_bytes, parent_oid); 1577 const parent_oid_int = hash.bytesToInt(repo_opts.hash, &parent_oid_bytes); 1578 1579 // if this object id has already been consumed, skip it 1580 if (null != try consumed_object_ids.getCursor(parent_oid_int)) { 1581 continue; 1582 } 1583 1584 // if this object id hasn't already been seen, add it to the walk queue 1585 if (null == try seen_object_ids.getCursor(parent_oid_int)) { 1586 try walk_queue.append(.{ .bytes = &parent_oid_bytes }); 1587 } 1588 1589 const children_cursor = try self.parent_to_children.putCursor(parent_oid_int); 1590 const children = try DB.HashSet(.read_write).init(children_cursor); 1591 try children.put(oid_int, .{ .bytes = &oid }); 1592 pending_parent_count += 1; 1593 } 1594 1595 if (pending_parent_count == 0) { 1596 try self.ready_queue.append(.{ .bytes = &oid }); 1597 } else { 1598 try self.child_to_pending_parents.put(oid_int, .{ .uint = pending_parent_count }); 1599 } 1600 } 1601 } 1602 1603 fn enqueueChildren(self: *CommitIterator(repo_kind, repo_opts), oid: *const [hash.byteLen(repo_opts.hash)]u8) !void { 1604 const oid_int = hash.bytesToInt(repo_opts.hash, oid); 1605 const children_cursor = try self.parent_to_children.getCursor(oid_int) orelse return; 1606 const children = try DB.HashSet(.read_only).init(children_cursor); 1607 var children_iter = try children.iterator(); 1608 1609 while (try children_iter.next()) |*child_cursor| { 1610 const kv_pair = try child_cursor.readKeyValuePair(); 1611 const child_oid_int = kv_pair.hash; 1612 const pending_parent_cursor = try self.child_to_pending_parents.getCursor(child_oid_int) orelse return error.CursorNotFound; 1613 const pending_parent_count = try pending_parent_cursor.readUint(); 1614 1615 if (pending_parent_count <= 1) { 1616 _ = try self.child_to_pending_parents.remove(child_oid_int); 1617 const child_oid = hash.intToBytes(hash.HashInt(repo_opts.hash), child_oid_int); 1618 try self.ready_queue.append(.{ .bytes = &child_oid }); 1619 } else { 1620 try self.child_to_pending_parents.put(child_oid_int, .{ .uint = pending_parent_count - 1 }); 1621 } 1622 } 1623 } 1624 1625 fn readOidFromList(list: DB.ArrayList(.read_only), index: u64) ![hash.byteLen(repo_opts.hash)]u8 { 1626 const oid_cursor = try list.getCursor(index) orelse return error.CursorNotFound; 1627 var oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 1628 _ = try oid_cursor.readBytes(&oid); 1629 return oid; 1630 } 1631 }; 1632 } 1633