╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ───────────────╮ │src/test/event/patch.zig│ ╰────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │316 const conflict_cursor = try conflicts.getCursor(&evt.orderKeyDesc(merged.created_order, &patch_id)) orelse return error.NotFound; │ │317 const conflict = try Repo.DB.HashMap(.read_only).init(conflict_cursor); │ │318 const fields_cursor = try conflict.getCursor(hash.hashInt(repo_opts.hash, evt.conflicted_fields_key)) orelse return error.NotFound; │ │317 - try std.testing.expectEqualStrings("patchrev_id", try fields_cursor.readBytesAlloc(arena.allocator(), null)); │ │319 + try std.testing.expectEqualStrings("patchrev_id status", try fields_cursor.readBytesAlloc(arena.allocator(), null)); │ │320 const their_cursor = try conflict.getCursor(hash.hashInt(repo_opts.hash, evt.their_record_key)) orelse return error.NotFound; │ │321 const theirs = try evt.read(evt.Patch.Record, Repo.DB, repo_opts.hash, &arena, try Repo.DB.HashMap(.read_only).init(their_cursor)); │ │322 try std.testing.expectEqualStrings(&std.fmt.bytesToHex(patchrev_b_id, .lower), &(theirs.event.patchrev_id orelse return error.NotFound));│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │387 try std.testing.expectEqual(1, try children.count()); │ │388 │ │389 // │ │390 + // close and merge the same revision concurrently │ │391 + // │ │392 + │ │393 + const status_side_ref: rf.Ref = .{ .kind = .head, .name = "haxy/patch-status-side" }; │ │394 + { │ │395 + var result = try target.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); │ │396 + defer result.deinit(); │ │397 + try target.addBranch(io, .{ .name = status_side_ref.name }); │ │398 + } │ │399 + var closed_child = child.event; │ │400 + closed_child.status = .closed; │ │401 + try evt.consume(.repo, .xit, repo_opts, io, allocator, &target, evt.events_ref, &.{.{ │ │402 + .id = std.fmt.bytesToHex(child_id, .lower), │ │403 + .timestamp = 14, │ │404 + .author = author, │ │405 + .event = .{ .patch = closed_child }, │ │406 + }}); │ │407 + var merged_child = child.event; │ │408 + merged_child.status = .merged; │ │409 + try evt.consume(.repo, .xit, repo_opts, io, allocator, &target, status_side_ref, &.{.{ │ │410 + .id = std.fmt.bytesToHex(child_id, .lower), │ │411 + .timestamp = 15, │ │412 + .author = author, │ │413 + .event = .{ .patch = merged_child }, │ │414 + }}); │ │415 + try evt.mergeEvents(.xit, repo_opts, io, allocator, &target, status_side_ref); │ │416 + try evt.consume(.repo, .xit, repo_opts, io, allocator, &target, evt.events_ref, &.{}); │ │417 + │ │418 + _ = arena.reset(.retain_capacity); │ │419 + const status_moment = try evt.currentMoment(repo_opts, &target); │ │420 + const merged_child_record = (try evt.Patch.readById(Repo.DB, repo_opts.hash, status_moment, &arena, &child_id)) orelse return error.NotFound; │ │421 + try std.testing.expectEqual(.merged, merged_child_record.event.status); │ │422 + try std.testing.expectEqual(0, try patchStatusCount(status_moment, .open)); │ │423 + try std.testing.expectEqual(1, try patchStatusCount(status_moment, .closed)); │ │424 + try std.testing.expectEqual(1, try patchStatusCount(status_moment, .merged)); │ │425 + try std.testing.expectEqual(1, try patchTagStatusCount(status_moment, "enhancement", .merged)); │ │426 + const status_conflicts_cursor = try status_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.conflicts_key)) orelse return error.NotFound;│ │427 + const status_conflicts = try Repo.DB.SortedMap(.read_only).init(status_conflicts_cursor); │ │428 + const child_conflict = try status_conflicts.getCursor(&evt.orderKeyDesc(merged_child_record.created_order, &child_id)); │ │429 + try std.testing.expectEqual(null, child_conflict); │ │430 + │ │431 + var reopened_child = merged_child_record.event; │ │432 + reopened_child.status = .open; │ │433 + try std.testing.expectError(error.PatchAlreadyMerged, evt.consume(.repo, .xit, repo_opts, io, allocator, &target, evt.events_ref, &.{.{ │ │434 + .id = std.fmt.bytesToHex(child_id, .lower), │ │435 + .timestamp = 16, │ │436 + .author = author, │ │437 + .event = .{ .patch = reopened_child }, │ │438 + }})); │ │439 + │ │440 + const invalid_merged_id = evt.EventWithId.randomId(prng.random()); │ │441 + try std.testing.expectError(error.InvalidPatch, evt.consume(.repo, .xit, repo_opts, io, allocator, &target, evt.events_ref, &.{.{ │ │442 + .id = std.fmt.bytesToHex(invalid_merged_id, .lower), │ │443 + .timestamp = 16, │ │444 + .author = author, │ │445 + .event = .{ .patch = .{ │ │446 + .title = "invalid merged patch", │ │447 + .description = "has no revision", │ │448 + .tags = "enhancement", │ │449 + .status = .merged, │ │450 + } }, │ │451 + }})); │ │452 + │ │453 + // │ │454 // retain the ref-free commits during garbage collection │ │455 // │ │456 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │648 .author = author, │ │649 .timestamp = 3, │ │650 }); │ │651 + try fork.publish(repo_opts, io, allocator, &admin, &target, draft_path, .{│ │652 + .id = patch_id_hex, │ │653 + .user_id = user_id, │ │654 + .repo_id = repo_id, │ │655 + .author = author, │ │656 + .timestamp = 4, │ │657 + }); │ │658 │ │659 var arena = std.heap.ArenaAllocator.init(allocator); │ │660 defer arena.deinit(); │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │661 const moment = try evt.currentMoment(repo_opts, &target); │ │662 const patch = (try evt.Patch.readById(Repo.DB, repo_opts.hash, moment, &arena, &patch_id)) orelse return error.NotFound; │ │663 try std.testing.expectEqualStrings(&first_patchrev_hex, &(patch.event.patchrev_id orelse return error.NotFound)); │ │664 + try std.testing.expectEqual(.open, patch.event.status); │ │665 try std.testing.expectEqual(null, try evt.PatchRev.readById(Repo.DB, repo_opts.hash, moment, &arena, &first_patchrev_id));│ │666 │ │667 _ = arena.reset(.retain_capacity); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │668 const admin_moment = try evt.currentMoment(evt.admin_repo_opts, &admin); │ │669 const published_fork = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &arena, &patch_id)) orelse return error.NotFound;│ │670 try std.testing.expect(!published_fork.removed); │ │671 + try std.testing.expectEqual(.posted, published_fork.event.stage); │ │672 + const draft_key = evt.Fork.draftKey(&repo_id, &user_id); │ │673 + try std.testing.expectEqual(0, try indexedForkCount(admin_moment, evt.Fork.repo_user_to_draft_id_set_key, &draft_key)); │ │674 + try std.testing.expectEqual(1, try indexedForkCount(admin_moment, evt.Fork.user_id_to_fork_id_set_key, &user_id)); │ │675 { │ │676 var draft = try Repo.open(io, allocator, .{ .path = draft_path }); │ │677 defer draft.deinit(io, allocator); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │692 try std.testing.expectEqualStrings(revision.base_tree_oid, &trees.base); │ │693 try std.testing.expectEqualStrings(revision.head_tree_oid, &trees.head); │ │694 } │ │695 +} │ │696 + │ │697 +fn indexedForkCount( │ │698 + moment: evt.AdminDB.HashMap(.read_only), │ │699 + index_key: []const u8, │ │700 + parent_id: []const u8, │ │701 +) !u64 { │ │702 + const index_cursor = try moment.getCursor(hash.hashInt(evt.admin_repo_opts.hash, index_key)) orelse return 0; │ │703 + const index = try evt.AdminDB.HashMap(.read_only).init(index_cursor); │ │704 + const ids_cursor = try index.getCursor(hash.hashInt(evt.admin_repo_opts.hash, parent_id)) orelse return 0; │ │705 + const ids = try evt.AdminDB.SortedSet(.read_only).init(ids_cursor); │ │706 + return try ids.count(); │ │707 } │ │708 │ │709 +fn patchStatusCount(moment: Repo.DB.HashMap(.read_only), status: evt.Patch.Status) !u64 { │ │710 + const statuses_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.status_to_id_set_key)) orelse return 0;│ │711 + const statuses = try Repo.DB.SortedMap(.read_only).init(statuses_cursor); │ │712 + const ids_cursor = try statuses.getCursor(@tagName(status)) orelse return 0; │ │713 + const ids = try Repo.DB.SortedSet(.read_only).init(ids_cursor); │ │714 + return try ids.count(); │ │715 +} │ │716 + │ │717 +fn patchTagStatusCount(moment: Repo.DB.HashMap(.read_only), tag: []const u8, status: evt.Patch.Status) !u64 { │ │718 + const tags_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.tag_status_to_id_set_key)) orelse return 0;│ │719 + const tags = try Repo.DB.SortedMap(.read_only).init(tags_cursor); │ │720 + var key_buffer: evt.Patch.TagStatusKey = undefined; │ │721 + const ids_cursor = try tags.getCursor(try evt.Patch.tagStatusKey(&key_buffer, tag, status)) orelse return 0; │ │722 + const ids = try Repo.DB.SortedSet(.read_only).init(ids_cursor); │ │723 + return try ids.count(); │ │724 +} │ │725 + │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯