╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ event.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
309 // branch. it lives inside the repo dir but is not part of the repo's own data, 310 // so viewing a repo never mutates it. 311 pub fn LocalEventDB(comptime hash_kind: hash.HashKind) type { 312 return struct { 313 file: std.Io.File, 314 buffer: *std.Io.Writer.Allocating, 315 db: *EventDB(hash_kind), 316 317 const db_name = "haxy"; 318 const Self = @This(); 319 320 // open the db in `repo_dir` for writing, creating it if it doesn't exist 321 pub fn open(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir) !Self { 322 const file = try repo_dir.createFile(io, db_name, .{ .truncate = false, .read = true, .lock = .exclusive }); 323 return fromFile(io, allocator, file); 324 } 325 326 // open the db in `repo_dir` for reading, or null if it doesn't exist 327 pub fn openReadOnly(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir) !?Self { 328 const file = repo_dir.openFile(io, db_name, .{ .mode = .read_write, .lock = .shared }) catch |err| switch (err) { 329 error.FileNotFound => return null, 330 else => |e| return e, 331 }; 332 return try fromFile(io, allocator, file); 333 } 334 335 // remove the db from `repo_dir`, so a repo whose events branch 336 // disappeared doesn't keep showing stale issues 337 pub fn delete(io: std.Io, repo_dir: std.Io.Dir) !void { 338 repo_dir.deleteFile(io, db_name) catch |err| switch (err) { 339 error.FileNotFound => {}, 340 else => |e| return e, 341 }; 342 } 343 344 fn fromFile(io: std.Io, allocator: std.mem.Allocator, file: std.Io.File) !Self { 345 errdefer file.close(io); 346 347 const buffer = try allocator.create(std.Io.Writer.Allocating); 348 errdefer allocator.destroy(buffer); 349 buffer.* = std.Io.Writer.Allocating.init(allocator); 350 errdefer buffer.deinit(); 351 352 const db = try allocator.create(EventDB(hash_kind)); 353 errdefer allocator.destroy(db); 354 db.* = try EventDB(hash_kind).init(.{ .io = io, .file = file, .buffer = buffer }); 355 356 return .{ .file = file, .buffer = buffer, .db = db }; 357 } 358 359 pub fn deinit(self: *Self, io: std.Io, allocator: std.mem.Allocator) void { 360 self.file.close(io); 361 self.buffer.deinit(); 362 allocator.destroy(self.buffer); 363 allocator.destroy(self.db); 364 } 365 366 // consume the events on `repo`'s `ref` into this db. the db must come 367 // from `open`, whose handle holds the exclusive lock guarding the 368 // transaction. 369 pub fn consume( 370 self: *Self, 371 comptime repo_kind: rp.RepoKind, 372 comptime repo_opts: rp.RepoOpts(repo_kind), 373 io: std.Io, 374 allocator: std.mem.Allocator, 375 repo: *rp.Repo(repo_kind, repo_opts), 376 ref: rf.Ref, 377 ) !void { 378 comptime if (repo_opts.hash != hash_kind) @compileError("the repo must use the db's hash kind"); 379 const DB = EventDB(hash_kind); 380 381 const Ctx = struct { 382 repo: *rp.Repo(repo_kind, repo_opts), 383 db: *DB, 384 io: std.Io, 385 allocator: std.mem.Allocator, 386 ref: rf.Ref, 387 388 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 389 var moment = try DB.HashMap(.read_write).init(cursor.*); 390 var repo_moment = try ctx.repo.core.latestMoment(); 391 const read_state = rp.Repo(repo_kind, repo_opts).State(.read_only){ .core = &ctx.repo.core, .extra = .{ .moment = &repo_moment } }; 392 if (!try consumeInTransaction(repo_kind, repo_opts, read_state, ctx.db, &moment, ctx.io, ctx.allocator, ctx.ref)) return error.CancelTransaction; 393 } 394 }; 395 396 const history = try DB.ArrayList(.read_write).init(self.db.rootCursor()); 397 history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ 398 .repo = repo, 399 .db = self.db, 400 .io = io, 401 .allocator = allocator, 402 .ref = ref, 403 }) catch |err| switch (err) { 404 error.CancelTransaction => {}, 405 else => |e| return e, 406 }; 407 } 408 }; 409 } 410 411 // consume the events on `ref` into the db `moment` writes to, returning false 412 // if there was nothing to do. events are read from `read_state`'s repo, which 413 // needn't be the repo backing the db — that's how a local (possibly 414 // git-backed) repo's events land in a standalone db. 415 pub fn consumeInTransaction( 416 comptime repo_kind: rp.RepoKind, 417 comptime repo_opts: rp.RepoOpts(repo_kind), 418 read_state: rp.Repo(repo_kind, repo_opts).State(.read_only), 419 db: *EventDB(repo_opts.hash), 420 moment: *EventDB(repo_opts.hash).HashMap(.read_write), 421 io: std.Io, 422 allocator: std.mem.Allocator, 423 ref: rf.Ref, 424 ) !bool { 425 const DB = EventDB(repo_opts.hash); 426 427 // the last_object_id represents the object id that was last consumed 428 var last_object_id_maybe: ?[hash.byteLen(repo_opts.hash)]u8 = null; 429 if (try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id"))) |last_object_id_cursor| { 430 var last_object_id_buffer: [hash.byteLen(repo_opts.hash)]u8 = undefined; 431 _ = try last_object_id_cursor.readBytes(&last_object_id_buffer); 432 last_object_id_maybe = last_object_id_buffer; 433 } 434 435 // the tip was already consumed, so there is nothing to do 436 if (last_object_id_maybe) |*last_object_id| { 437 const tip_hex = (try rf.readRecur(repo_kind, repo_opts, read_state, io, .{ .ref = ref })) orelse return error.OidNotFound; 438 var tip: [hash.byteLen(repo_opts.hash)]u8 = undefined; 439 _ = try std.fmt.hexToBytes(&tip, &tip_hex); 440 if (std.mem.eql(u8, last_object_id, &tip)) return false; 441 } 442 443 // the list with all of haxy's state, including materialized views. 444 // the reason it is a list is so we can keep every previous haxy 445 // state, making it easy to revert to an older state if the user 446 // rebases some of the past commits. 447 const haxy_cursor = try moment.putCursor(hash.hashInt(repo_opts.hash, "haxy")); 448 const haxy = try DB.ArrayList(.read_write).init(haxy_cursor); 449 450 // add a new item to the haxy list created above. 451 // we call the item haxy_moments. it is a map of object id to haxy moment. 452 // in other words, it maps each object id to a hash map containing the 453 // state that the database was in when that object id was consumed. 454 var haxy_moments_cursor = try haxy.appendCursor(); 455 // use the previous haxy_moments as the basis for this one if it exists 456 if (try haxy.getCursor(-2)) |last_haxy_moments_cursor| { 457 try haxy_moments_cursor.write(.{ .slot = last_haxy_moments_cursor.slot() }); 458 } 459 var haxy_moments = try DB.HashMap(.read_write).init(haxy_moments_cursor); 460 461 var commit_iter = try CommitIterator(repo_kind, repo_opts).init(read_state, io, allocator, haxy_moments.readOnly(), ref); 462 defer commit_iter.deinit(io, allocator); 463 464 // if there are no events to process, look at the oid at HEAD and update 465 // the last_object_id to point to it. this is important in situations 466 // where we do a merge and then force-push to remove the merge. see the 467 // merge test for an example. 468 if (commit_iter.newest_object_id == null) { 469 const head_oid_hex = (try rf.readRecur(repo_kind, repo_opts, read_state, io, .{ .ref = ref })) orelse return error.OidNotFound; 470 var head_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 471 _ = try std.fmt.hexToBytes(&head_oid, &head_oid_hex); 472 473 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &head_oid)) orelse return error.CursorNotFound; 474 const haxy_moment = try DB.HashMap(.read_only).init(haxy_moment_cursor); 475 476 const moment_index_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "moment-index")) orelse return error.CursorNotFound; 477 const moment_index = try moment_index_cursor.readUint(); 478 479 try haxy.slice(moment_index + 1); 480 try moment.put(hash.hashInt(repo_opts.hash, "haxy-last-object-id"), .{ .bytes = &head_oid }); 481 return true; 482 } 483 484 // if this branch was rebased and force pushed, we need to detect that and 485 // properly revert the haxy state to the last valid state. we detect this 486 // by simply asking if the last event is a descendent of the last event 487 // we consumed. if it isn't, then we know the haxy state needs to be reverted. 488 if (last_object_id_maybe) |*last_object_id| { 489 var is_rebased = false; 490 const newest_object_id = commit_iter.newest_object_id orelse return error.OidNotFound; 491 492 _ = mrg.getDescendent( 493 repo_kind, 494 repo_opts, 495 read_state, 496 io, 497 allocator, 498 &std.fmt.bytesToHex(last_object_id, .lower), 499 &std.fmt.bytesToHex(&newest_object_id, .lower), 500 ) catch |err| switch (err) { 501 error.DescendentNotFound => is_rebased = true, 502 else => |e| return e, 503 }; 504 505 if (is_rebased) { 506 const oldest_object_id = commit_iter.oldest_object_id orelse return error.OidNotFound; 507 var oldest_object = try obj.Object(repo_kind, repo_opts).init(read_state, io, allocator, &std.fmt.bytesToHex(oldest_object_id, .lower)); 508 defer oldest_object.deinit(); 509 510 const parent_oids = oldest_object.content.commit.metadata.parent_oids orelse return error.ParentOidsNotFound; 511 switch (parent_oids.len) { 512 0 => { 513 // the branch was rebased all the way to the very beginning. 514 // we have a repo event with no parent, which means it is now 515 // the very first event. all we need to do is set the haxy list 516 // to be empty and make a new haxy_moments map to work with. 517 518 try haxy.slice(0); 519 haxy_moments_cursor = try haxy.appendCursor(); 520 haxy_moments = try DB.HashMap(.read_write).init(haxy_moments_cursor); 521 522 last_object_id_maybe = null; 523 }, 524 1 => { 525 var oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 526 _ = try std.fmt.hexToBytes(&oid, &parent_oids[0]); 527 528 const old_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &oid)) orelse return error.CursorNotFound; 529 const old_moment = try DB.HashMap(.read_only).init(old_moment_cursor); 530 531 const old_moment_index_cursor = try old_moment.getCursor(hash.hashInt(repo_opts.hash, "moment-index")) orelse return error.CursorNotFound; 532 const old_moment_index = try old_moment_index_cursor.readUint(); 533 534 // resize the haxy list so we truncate all the moments that were 535 // created after the parent_oid was consumed 536 try haxy.slice(old_moment_index + 1); 537 538 // make a new haxy moment and set its initial value to the last haxy moment 539 haxy_moments_cursor = try haxy.appendCursor(); 540 const old_haxy_moments_cursor = try haxy.getCursor(old_moment_index) orelse return error.CursorNotFound; 541 try haxy_moments_cursor.write(.{ .slot = old_haxy_moments_cursor.slot() }); 542 haxy_moments = try DB.HashMap(.read_write).init(haxy_moments_cursor); 543 544 last_object_id_maybe = oid; 545 }, 546 else => return error.UnexpectedParentCount, 547 } 548 } 549 } 550 551 while (try commit_iter.next()) |repo_event_oid| { 552 var commit_object = try obj.Object(repo_kind, repo_opts).init(read_state, io, allocator, &std.fmt.bytesToHex(repo_event_oid, .lower)); 553 defer commit_object.deinit(); 554 555 const parent_oids = commit_object.content.commit.metadata.parent_oids orelse return error.ParentOidsNotFound; 556 557 // create a moment for this object id 558 var haxy_moment_cursor = try haxy_moments.putCursor(hash.bytesToInt(repo_opts.hash, &repo_event_oid)); 559 560 // if there was a previous object id, set this haxy moment's initial value to it. 561 // this efficiently "clones" the map so we make further modifications based on it. 562 if (parent_oids.len > 0) { 563 var first_parent_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 564 _ = try std.fmt.hexToBytes(&first_parent_oid, &parent_oids[0]); 565 566 const first_parent_haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &first_parent_oid)) orelse return error.CursorNotFound; 567 try haxy_moment_cursor.write(.{ .slot = first_parent_haxy_moment_cursor.slot() }); 568 } 569 570 const haxy_moment = try DB.HashMap(.read_write).init(haxy_moment_cursor); 571 572 // merge changes from every parent after the first parent. the common 573 // ancestor is the baseline, so a later parent only contributes records 574 // it changed relative to the merge base. only the record maps are 575 // merged; every other view is derived from them, and each kind's 576 // `consume` re-derives its own as the winning records are written. 577 if (parent_oids.len > 1) { 578 for (parent_oids[1..]) |*parent_oid| { 579 var oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 580 _ = try std.fmt.hexToBytes(&oid, parent_oid); 581 582 const parent_haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &oid)) orelse return error.CursorNotFound; 583 const parent_haxy_moment = try DB.HashMap(.read_only).init(parent_haxy_moment_cursor); 584 585 const baseline_oid_hex = try mrg.commonAncestor(repo_kind, repo_opts, read_state, io, allocator, &parent_oids[0], parent_oid); 586 var baseline_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 587 _ = try std.fmt.hexToBytes(&baseline_oid, &baseline_oid_hex); 588 const baseline_haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &baseline_oid)) orelse return error.CursorNotFound; 589 const baseline_haxy_moment = try DB.HashMap(.read_only).init(baseline_haxy_moment_cursor); 590 591 inline for (std.meta.fields(Event)) |field| { 592 const T = @typeInfo(field.type).optional.child; 593 try merge(T, DB, repo_opts.hash, allocator, haxy_moment, parent_haxy_moment, baseline_haxy_moment); 594 } 595 } 596 } 597 598 // associate this moment with the index it will first appear at in the haxy list. 599 // this will be important later so we can truncate that list if the user ever 600 // rebases starting at this object id. 601 try haxy_moment.put(hash.hashInt(repo_opts.hash, "moment-index"), .{ .uint = try haxy.count() - 1 }); 602 603 // consume the event unless it's a merge commit 604 if (parent_oids.len <= 1) { 605 var arena = std.heap.ArenaAllocator.init(allocator); 606 defer arena.deinit(); 607 608 // read the message from the commit 609 var message: std.ArrayList(u8) = .empty; 610 try commit_object.readMessage(arena.allocator(), &message, .limited(max_event_size)); 611 612 const event_with_id = try EventWithId.fromString(&arena, message.items); 613 614 // get the id of the current event as bytes 615 var current_event_id: [event_id_size]u8 = undefined; 616 _ = try std.fmt.hexToBytes(&current_event_id, &event_with_id.id); 617 618 // wrap the payload into the record `consume` stores, with its 619 // commit-derived fields; on update, `consume` preserves the 620 // existing record's 621 const created_ts = commit_object.content.commit.metadata.timestamp; 622 switch (event_with_id.event) { 623 .user => |event_maybe| { 624 const record_maybe: ?User.Record = if (event_maybe) |event| .{ .event = event, .created_ts = created_ts } else null; 625 try User.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, record_maybe, &arena, &repo_event_oid); 626 }, 627 .repo => |event_maybe| { 628 const record_maybe: ?Repo.Record = if (event_maybe) |event| .{ .event = event, .created_ts = created_ts } else null; 629 try Repo.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, record_maybe, &arena, &repo_event_oid); 630 }, 631 .issue => |event_maybe| { 632 const record_maybe: ?Issue.Record = if (event_maybe) |event| .{ 633 .event = event, 634 .author_email = authorEmail(commit_object.content.commit.metadata.author orelse ""), 635 .created_ts = created_ts, 636 } else null; 637 try Issue.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, record_maybe, &arena, &repo_event_oid); 638 }, 639 .comment => |event_maybe| { 640 const record_maybe: ?Comment.Record = if (event_maybe) |event| .{ 641 .event = event, 642 .author_email = authorEmail(commit_object.content.commit.metadata.author orelse ""), 643 .created_ts = created_ts, 644 } else null; 645 try Comment.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, record_maybe, &arena, &repo_event_oid); 646 }, 647 } 648 } 649 650 // the current object id is now the last one 651 last_object_id_maybe = repo_event_oid; 652 653 // prevent any of the data created above from being mutated by future iterations of this loop 654 try db.freeze(); 655 } 656 657 if (last_object_id_maybe) |*last_object_id| { 658 try moment.put(hash.hashInt(repo_opts.hash, "haxy-last-object-id"), .{ .bytes = last_object_id }); 659 } 660 661 return true; 662 } 663 664 // the conflicted records of one kind, keyed by the same order key its id set 665 // uses, so this doubles as the index the conflicts view lists and counts. every 666 // value under an entry is a slot reference to a structure that already exists: 667 // 668 // <order key> -> conflict entry 669 // conflicted-fields the conflicting field names, space separated 670 // base-record the merge base's whole record 671 // their-record the merged-in side's whole record 672 // their-field->oid that side's field name to commit map 673 // 674 // our own value is the live record and our commit is in the kind's oid map, so 675 // neither is repeated here. 676 pub const conflicted_fields_key = "conflicted-fields"; 677 pub const base_record_key = "base-record"; 678 pub const their_record_key = "their-record"; 679 pub const their_field_to_oid_key = "their-field->oid"; 680 681 // record the commit against every field this event changes. a merge passes no 682 // oid, since each field's winner may come from a different side. 683 pub fn writeOid( 684 comptime T: type, 685 comptime DB: type, 686 comptime hash_kind: hash.HashKind, 687 id_to_field_to_oid: DB.HashMap(.read_write), 688 record_key: hash.HashInt(hash_kind), 689 existing_maybe: ?T, 690 event: T, 691 event_oid: ?[]const u8, 692 ) !void { 693 const oid = event_oid orelse return; 694 695 // created only once a field actually changes 696 var field_oids: ?DB.SortedMap(.read_write) = null; 697 698 inline for (std.meta.fields(T)) |field| { 699 const changed = if (existing_maybe) |existing| 700 !fieldEqual(field.type, @field(existing, field.name), @field(event, field.name)) 701 else 702 true; 703 if (changed) { 704 if (field_oids == null) { 705 field_oids = try DB.SortedMap(.read_write).init(try id_to_field_to_oid.putCursor(record_key)); 706 } 707 if (field_oids) |map| try map.put(field.name, .{ .bytes = oid }); 708 } 709 } 710 } 711 712 // the whole field list, space separated, is the longest it can get 713 fn conflictedFieldsMaxLen(comptime T: type) usize { 714 var len: usize = 0; 715 for (std.meta.fields(T)) |field| len += field.name.len + 1; 716 return len; 717 } 718 719 // what a three-way merge did with a field 720 const FieldMerge = enum { 721 // the target's value stands, because neither side changed it, both made the 722 // same change, or only the target changed it 723 kept, 724 parent, 725 conflicted, 726 }; 727 728 // three-way merge each field. one only the parent changed takes the parent's 729 // value; one both sides changed differently keeps the target's. a missing 730 // baseline makes every differing field a conflict. 731 fn mergeFields( 732 comptime T: type, 733 baseline_maybe: ?T, 734 target: T, 735 parent: T, 736 outcome: *[std.meta.fields(T).len]FieldMerge, 737 ) T { 738 var merged = target; 739 740 inline for (std.meta.fields(T), 0..) |field, i| { 741 const target_value = @field(target, field.name); 742 const parent_value = @field(parent, field.name); 743 744 if (!fieldEqual(field.type, target_value, parent_value)) { 745 outcome[i] = if (baseline_maybe) |baseline| blk: { 746 const baseline_value = @field(baseline, field.name); 747 if (fieldEqual(field.type, target_value, baseline_value)) { 748 @field(merged, field.name) = parent_value; 749 break :blk .parent; 750 } 751 // only the target changed it 752 if (fieldEqual(field.type, parent_value, baseline_value)) break :blk .kept; 753 break :blk .conflicted; 754 } else .conflicted; 755 } 756 } 757 758 return merged; 759 } 760 761 // three-way merge of one kind's records from a merge parent. kinds that expose 762 // conflicts merge field by field; the others take one whole record. the result 763 // goes through the kind's `consume`, which re-derives the indexes from it. a 764 // deletion carries over unless the other side changed the record, so a merge 765 // never fails. 766 pub fn merge( 767 comptime T: type, 768 comptime DB: type, 769 comptime hash_kind: hash.HashKind, 770 allocator: std.mem.Allocator, 771 haxy_moment: DB.HashMap(.read_write), 772 parent_moment: DB.HashMap(.read_only), 773 baseline_moment: DB.HashMap(.read_only), 774 ) !void { 775 const parent_set_cursor = try parent_moment.getCursor(hash.hashInt(hash_kind, T.id_set_key)) orelse return; 776 const parent_set = try DB.SortedSet(.read_only).init(parent_set_cursor); 777 778 const parent_records_cursor = try parent_moment.getCursor(hash.hashInt(hash_kind, T.record_map_key)) orelse return; 779 const parent_records = try DB.HashMap(.read_only).init(parent_records_cursor); 780 781 var baseline_records: ?DB.HashMap(.read_only) = null; 782 if (try baseline_moment.getCursor(hash.hashInt(hash_kind, T.record_map_key))) |cursor| { 783 baseline_records = try DB.HashMap(.read_only).init(cursor); 784 } 785 786 var parent_conflicts: ?DB.SortedMap(.read_only) = null; 787 var baseline_conflicts: ?DB.SortedMap(.read_only) = null; 788 var parent_id_to_field_to_oid: ?DB.HashMap(.read_only) = null; 789 switch (comptime T.merge_policy) { 790 .field_conflicts => { 791 if (try parent_moment.getCursor(hash.hashInt(hash_kind, T.conflicts_key))) |cursor| { 792 parent_conflicts = try DB.SortedMap(.read_only).init(cursor); 793 } 794 if (try baseline_moment.getCursor(hash.hashInt(hash_kind, T.conflicts_key))) |cursor| { 795 baseline_conflicts = try DB.SortedMap(.read_only).init(cursor); 796 } 797 if (try parent_moment.getCursor(hash.hashInt(hash_kind, T.id_to_field_to_oid_key))) |cursor| { 798 parent_id_to_field_to_oid = try DB.HashMap(.read_only).init(cursor); 799 } 800 }, 801 .target_wins => {}, 802 } 803 804 var arena = std.heap.ArenaAllocator.init(allocator); 805 defer arena.deinit(); 806 807 var iter = try parent_set.iteratorFromIndex(0); 808 while (try iter.next()) |kv_pair_cursor| { 809 const event_id = try readOrderKeyId(DB, kv_pair_cursor); 810 const record_key = hash.hashInt(hash_kind, &event_id); 811 812 _ = arena.reset(.retain_capacity); 813 814 const parent_record_cursor = try parent_records.getCursor(record_key) orelse continue; 815 const parent_slot = parent_record_cursor.slot(); 816 const baseline_slot = if (baseline_records) |map| try map.getSlot(record_key) else null; 817 818 // the parent left this record alone 819 if (baseline_slot) |slot| { 820 if (slot.eql(parent_slot)) continue; 821 } 822 823 const parent_record = try read(T.Record, DB, hash_kind, &arena, try DB.HashMap(.read_only).init(parent_record_cursor)); 824 825 // re-derived every iteration, since `consume` writes through its own 826 // handles 827 const target_records_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, T.record_map_key)); 828 const target_records = try DB.HashMap(.read_write).init(target_records_cursor); 829 830 // a record the target doesn't have arrives wholesale from the parent 831 var outcome: [std.meta.fields(T).len]FieldMerge = @splat(.parent); 832 var merged = parent_record; 833 834 if (try target_records.getCursor(record_key)) |target_record_cursor| { 835 // both sides share the change 836 if (target_record_cursor.slot().eql(parent_slot)) continue; 837 838 const target_record = try read(T.Record, DB, hash_kind, &arena, try DB.HashMap(.read_only).init(target_record_cursor)); 839 840 var baseline_record: ?T.Record = null; 841 if (baseline_records) |map| { 842 if (try map.getCursor(record_key)) |baseline_record_cursor| { 843 baseline_record = try read(T.Record, DB, hash_kind, &arena, try DB.HashMap(.read_only).init(baseline_record_cursor)); 844 } 845 } 846 847 switch (comptime T.merge_policy) { 848 .field_conflicts => { 849 outcome = @splat(.kept); 850 merged = target_record; 851 merged.event = mergeFields(T, if (baseline_record) |baseline| baseline.event else null, target_record.event, parent_record.event, &outcome); 852 853 // merge the tombstone separately from serialized fields. a deletion 854 // carries over an unchanged record, while an edit or restoration 855 // keeps it active. 856 merged.deleted = if (target_record.deleted == parent_record.deleted) 857 target_record.deleted 858 else if (baseline_record) |baseline| blk: { 859 const active = if (!target_record.deleted) target_record else parent_record; 860 if (active.deleted != baseline.deleted) break :blk false; 861 if (!fieldsEqual(T, active.event, baseline.event)) break :blk false; 862 break :blk true; 863 } else false; 864 }, 865 .target_wins => { 866 // an uncontested parent change carries over. if both sides 867 // changed the record, the target wins as a whole, except that 868 // an edit or restoration wins over a deletion. 869 const target_changed = if (baseline_slot) |slot| 870 !target_record_cursor.slot().eql(slot) 871 else 872 true; 873 merged = if (!target_changed) 874 parent_record 875 else if (target_record.deleted != parent_record.deleted) 876 if (target_record.deleted) parent_record else target_record 877 else 878 target_record; 879 }, 880 } 881 } 882 883 // the index is unique, so a key the target gave to a different record 884 // leaves this one out rather than stranding that one behind a name it 885 // no longer owns 886 if (!merged.deleted and @hasDecl(T, "name_index_key")) { 887 const index_key = try merged.indexKey(arena.allocator()); 888 const name_index_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, T.name_index_key)); 889 const name_index = try DB.HashMap(.read_write).init(name_index_cursor); 890 if (try name_index.getCursor(hash.hashInt(hash_kind, index_key))) |owner_cursor| { 891 var owner_id: [event_id_size]u8 = undefined; 892 _ = try owner_cursor.readBytes(&owner_id); 893 if (!std.mem.eql(u8, &owner_id, &event_id)) continue; 894 } 895 } 896 897 // `consume` sets no oids, since each field's winner may come from 898 // a different side 899 try T.consume(DB, hash_kind, haxy_moment, &event_id, merged, &arena, null); 900 901 switch (comptime T.merge_policy) { 902 .field_conflicts => {}, 903 .target_wins => continue, 904 } 905 906 // tombstones do not carry field conflicts 907 if (merged.deleted) continue; 908 909 const conflicts_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, T.conflicts_key)); 910 const conflicts = try DB.SortedMap(.read_write).init(conflicts_cursor); 911 const id_to_field_to_oid_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, T.id_to_field_to_oid_key)); 912 const id_to_field_to_oid = try DB.HashMap(.read_write).init(id_to_field_to_oid_cursor); 913 const merged_key = orderKeyDesc(merged.created_ts, &event_id); 914 915 var parent_field_oids: ?DB.SortedMap(.read_only) = null; 916 if (parent_id_to_field_to_oid) |map| { 917 if (try map.getCursor(record_key)) |field_oids_cursor| { 918 parent_field_oids = try DB.SortedMap(.read_only).init(field_oids_cursor); 919 } 920 } 921 // created only once a field actually comes from the parent 922 var field_oids: ?DB.SortedMap(.read_write) = null; 923 924 // the fields both sides changed differently, space separated 925 var conflicted_fields: [conflictedFieldsMaxLen(T)]u8 = undefined; 926 var conflicted_len: usize = 0; 927 928 inline for (std.meta.fields(T), 0..) |field, i| { 929 switch (outcome[i]) { 930 // the target's value stands, so its oid does too 931 .kept => {}, 932 // the parent set this value, so its commit takes the field. a 933 // parent with no oid took its own value from a merge, leaving 934 // the field unattributed rather than crediting the target's 935 // commit for a value it no longer holds. 936 .parent => { 937 if (field_oids == null) { 938 field_oids = try DB.SortedMap(.read_write).init(try id_to_field_to_oid.putCursor(record_key)); 939 } 940 const map = field_oids orelse unreachable; 941 const parent_oid = if (parent_field_oids) |parent_map| try parent_map.getCursor(field.name) else null; 942 if (parent_oid) |oid_cursor| { 943 try map.put(field.name, .{ .slot = oid_cursor.slot() }); 944 } else { 945 _ = try map.remove(field.name); 946 } 947 }, 948 .conflicted => { 949 if (conflicted_len > 0) { 950 conflicted_fields[conflicted_len] = ' '; 951 conflicted_len += 1; 952 } 953 @memcpy(conflicted_fields[conflicted_len..][0..field.name.len], field.name); 954 conflicted_len += field.name.len; 955 }, 956 } 957 } 958 959 if (conflicted_len > 0) { 960 // each side's whole record is stored by reference, so the view can 961 // show any of them or take theirs wholesale 962 _ = try conflicts.remove(&merged_key); 963 const conflict = try DB.HashMap(.read_write).init(try conflicts.putCursor(&merged_key)); 964 try conflict.put(hash.hashInt(hash_kind, conflicted_fields_key), .{ .bytes = conflicted_fields[0..conflicted_len] }); 965 try conflict.put(hash.hashInt(hash_kind, base_record_key), .{ .slot = baseline_slot }); 966 try conflict.put(hash.hashInt(hash_kind, their_record_key), .{ .slot = parent_slot }); 967 if (parent_field_oids) |map| { 968 try conflict.put(hash.hashInt(hash_kind, their_field_to_oid_key), .{ .slot = map.slot() }); 969 } 970 continue; 971 } 972 973 // created_ts never changes, so the parent keys this record's conflict 974 // identically, and it carries over when the target has none, unless the 975 // parent's entry is unchanged from the baseline, meaning the target 976 // inherited it too and resolving it removed it here. 977 if (parent_conflicts) |map| { 978 if (null == try conflicts.getCursor(&merged_key)) { 979 if (try map.getSlot(&merged_key)) |conflict_slot| { 980 const baseline_conflict_slot = if (baseline_conflicts) |baseline_map| try baseline_map.getSlot(&merged_key) else null; 981 const resolved = if (baseline_conflict_slot) |slot| slot.eql(conflict_slot) else false; 982 if (!resolved) try conflicts.put(&merged_key, .{ .slot = conflict_slot }); 983 } 984 } 985 } 986 } 987 } 988 989 pub fn currentMoment( 990 comptime repo_opts: rp.RepoOpts(.xit), 991 repo: *rp.Repo(.xit, repo_opts), 992 ) !rp.Repo(.xit, repo_opts).DB.HashMap(.read_only) { 993 return currentMomentFromDb(repo_opts.hash, &repo.core.db); 994 } 995 996 // the oid of the last event commit consumed into `db`, or null before any 997 // consume completed 998 fn lastConsumedOid( 999 comptime hash_kind: hash.HashKind, 1000 db: *EventDB(hash_kind), 1001 ) !?[hash.byteLen(hash_kind)]u8 { 1002 const DB = EventDB(hash_kind); 1003 const history = try DB.ArrayList(.read_only).init(db.rootCursor().readOnly()); 1004 const moment_cursor = try history.getCursor(-1) orelse return null; 1005 const moment = try DB.HashMap(.read_only).init(moment_cursor); 1006 const last_object_id_cursor = try moment.getCursor(hash.hashInt(hash_kind, "haxy-last-object-id")) orelse return null; 1007 var last_object_id: [hash.byteLen(hash_kind)]u8 = undefined; 1008 _ = try last_object_id_cursor.readBytes(&last_object_id); 1009 return last_object_id; 1010 } 1011 1012 pub fn currentMomentFromDb( 1013 comptime hash_kind: hash.HashKind, 1014 db: *EventDB(hash_kind), 1015 ) !EventDB(hash_kind).HashMap(.read_only) { 1016 const DB = EventDB(hash_kind); 1017 const last_object_id = (try lastConsumedOid(hash_kind, db)) orelse return error.NotFound; 1018 const history = try DB.ArrayList(.read_only).init(db.rootCursor().readOnly()); 1019 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 1020 const moment = try DB.HashMap(.read_only).init(moment_cursor); 1021 const haxy_cursor = try moment.getCursor(hash.hashInt(hash_kind, "haxy")) orelse return error.NotFound; 1022 const haxy = try DB.ArrayList(.read_only).init(haxy_cursor); 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