╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ patch.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
430 try evt.consume(.repo, .xit, repo_opts, io, allocator, &target, status_side_ref, &.{.{ 431 .id = std.fmt.bytesToHex(child_id, .lower), 432 .timestamp = 15, 433 .author = author, 434 .event = .{ .patch = merged_child }, 435 }}); 436 try evt.mergeEvents(.xit, repo_opts, io, allocator, &target, status_side_ref); 437 try evt.consume(.repo, .xit, repo_opts, io, allocator, &target, evt.events_ref, &.{}); 438 439 _ = arena.reset(.retain_capacity); 440 const status_moment = try evt.currentMoment(repo_opts, &target); 441 const merged_child_record = (try evt.Patch.readById(Repo.DB, repo_opts.hash, status_moment, &arena, &child_id)) orelse return error.NotFound; 442 try std.testing.expectEqual(.merged, merged_child_record.event.status); 443 try std.testing.expectEqual(0, try patchStatusCount(status_moment, .open)); 444 try std.testing.expectEqual(1, try patchStatusCount(status_moment, .closed)); 445 try std.testing.expectEqual(1, try patchStatusCount(status_moment, .merged)); 446 try std.testing.expectEqual(1, try patchTagStatusCount(status_moment, "enhancement", .merged)); 447 const status_conflicts_cursor = try status_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.conflicts_key)) orelse return error.NotFound; 448 const status_conflicts = try Repo.DB.SortedMap(.read_only).init(status_conflicts_cursor); 449 const child_conflict = try status_conflicts.getCursor(&evt.orderKeyDesc(merged_child_record.created_order, &child_id)); 450 try std.testing.expectEqual(null, child_conflict); 451 452 var reopened_child = merged_child_record.event; 453 reopened_child.status = .open; 454 try std.testing.expectError(error.PatchAlreadyMerged, evt.consume(.repo, .xit, repo_opts, io, allocator, &target, evt.events_ref, &.{.{ 455 .id = std.fmt.bytesToHex(child_id, .lower), 456 .timestamp = 16, 457 .author = author, 458 .event = .{ .patch = reopened_child }, 459 }})); 460 461 const invalid_merged_id = evt.EventWithId.randomId(prng.random()); 462 try std.testing.expectError(error.InvalidPatch, evt.consume(.repo, .xit, repo_opts, io, allocator, &target, evt.events_ref, &.{.{ 463 .id = std.fmt.bytesToHex(invalid_merged_id, .lower), 464 .timestamp = 16, 465 .author = author, 466 .event = .{ .patch = .{ 467 .title = "invalid merged patch", 468 .description = "has no revision", 469 .tags = "enhancement", 470 .status = .merged, 471 } }, 472 }})); 473 474 // 475 // retain the ref-free commits during garbage collection 476 // 477 478 { 479 const roots = try evt.PatchRev.gcRoots(Repo.DB, repo_opts.hash, allocator, stacked_moment); 480 defer allocator.free(roots); 481 try std.testing.expectEqual(4, roots.len); 482 _ = try target.garbageCollect(io, allocator, roots); 483 484 var after_gc_moment = try target.core.latestMoment(); 485 const after_gc_state = Repo.State(.read_only){ .core = &target.core, .extra = .{ .moment = &after_gc_moment } }; 486 var retained = try obj.Object(.xit, repo_opts).init(after_gc_state, io, allocator, &roots[0]); 487 retained.deinit(); 488 } 489 } 490 491 test "patch lifecycle" { 492 try patchLifecycle("temp-patch-lifecycle-squash", .squash); 493 try patchLifecycle("temp-patch-lifecycle-source", .source); 494 } 495 496 fn patchLifecycle(temp_dir_name: []const u8, merge_revision: pch.MergeRevision) !void { 497 const io = std.testing.io; 498 const allocator = std.testing.allocator; 499 const cwd = std.Io.Dir.cwd(); 500 501 if (cwd.openDir(io, temp_dir_name, .{})) |dir_value| { 502 var dir = dir_value; 503 dir.close(io); 504 try cwd.deleteTree(io, temp_dir_name); 505 } else |_| {} 506 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 507 defer cwd.deleteTree(io, temp_dir_name) catch {}; 508 defer temp_dir.close(io); 509 510 const cwd_path = try std.process.currentPathAlloc(io, allocator); 511 defer allocator.free(cwd_path); 512 const root = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 513 defer allocator.free(root); 514 const repos_dir = try std.fs.path.join(allocator, &.{ root, "repos" }); 515 defer allocator.free(repos_dir); 516 const admin_path = try std.fs.path.join(allocator, &.{ root, "admin" }); 517 defer allocator.free(admin_path); 518 const upstream_path = try std.fs.path.join(allocator, &.{ root, "upstream" }); 519 defer allocator.free(upstream_path); 520 const source_path = try std.fs.path.join(allocator, &.{ root, "source" }); 521 defer allocator.free(source_path); 522 523 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 524 const user_id = evt.EventWithId.randomId(prng.random()); 525 const repo_id = evt.EventWithId.randomId(prng.random()); 526 const repo_id_hex = std.fmt.bytesToHex(repo_id, .lower); 527 const target_path = try std.fs.path.join(allocator, &.{ repos_dir, &repo_id_hex }); 528 defer allocator.free(target_path); 529 530 var admin = try rp.Repo(.xit, evt.admin_repo_opts).init(io, allocator, .{ .path = admin_path }); 531 defer admin.deinit(io, allocator); 532 try evt.consume(.admin, .xit, evt.admin_repo_opts, io, allocator, &admin, evt.events_ref, &.{ 533 .{ 534 .id = std.fmt.bytesToHex(user_id, .lower), 535 .timestamp = 1, 536 .author = author, 537 .event = .{ .user = .{ 538 .name = author.name, 539 .email = author.email, 540 .password_hash = "", 541 } }, 542 }, 543 .{ 544 .id = repo_id_hex, 545 .timestamp = 1, 546 .author = author, 547 .event = .{ .repo = .{ 548 .user_id = &user_id, 549 .name = "repo", 550 .description = "", 551 } }, 552 }, 553 }); 554 555 // 556 // create the target repo 557 // 558 559 { 560 var upstream = try rp.Repo(.git, .{ .is_test = true }).init(io, allocator, .{ .path = upstream_path }); 561 defer upstream.deinit(io, allocator); 562 const file = try upstream.core.work_dir.createFile(io, "main.zig", .{ .truncate = true }); 563 defer file.close(io); 564 try file.writeStreamingAll(io, "pub fn main() void {}\n"); 565 try upstream.add(io, allocator, &.{"main.zig"}); 566 _ = try upstream.commit(io, allocator, .{ .author = "alice <alice@example.test>", .message = "initial code", .timestamp = 1 }); 567 } 568 var target = try Repo.clone(io, allocator, upstream_path, target_path, target_path, null, .{}); 569 defer target.deinit(io, allocator); 570 571 // 572 // create the patch draft in the fork 573 // 574 575 const patch_id = evt.EventWithId.randomId(prng.random()); 576 const patch_id_hex = std.fmt.bytesToHex(patch_id, .lower); 577 const draft_path = try fork.create(repo_opts, io, allocator, repos_dir, &admin, .{ 578 .id = patch_id_hex, 579 .user_id = user_id, 580 .repo_id = repo_id, 581 .title = "add the answer", 582 .tags = "enhancement", 583 .description = "adds a reusable answer constant", 584 .author = author, 585 .timestamp = 2, 586 }); 587 defer allocator.free(draft_path); 588 589 // 590 // create and record the proposed revision in the fork 591 // 592 593 const base_oid = (try target.readRef(io, .{ .kind = .head, .name = "master" })) orelse return error.NotFound; 594 const base_tree_oid = try commitTree(io, allocator, &target, &base_oid); 595 var source = try Repo.clone(io, allocator, upstream_path, source_path, source_path, null, .{}); 596 defer source.deinit(io, allocator); 597 const source_oid = blk: { 598 const file = try source.core.work_dir.createFile(io, "feature.zig", .{ .truncate = true }); 599 defer file.close(io); 600 try file.writeStreamingAll(io, "pub const answer = 42;\n"); 601 try source.add(io, allocator, &.{"feature.zig"}); 602 break :blk try source.commit(io, allocator, .{ .author = "alice <alice@example.test>", .message = "add feature", .timestamp = 2 }); 603 }; 604 const head_tree_oid = try commitTree(io, allocator, &source, &source_oid); 605 const first_patchrev_id = evt.EventWithId.randomId(prng.random()); 606 const first_patchrev_hex = std.fmt.bytesToHex(first_patchrev_id, .lower); 607 const first_entries = [_]evt.EventTreeEntry{ 608 .{ .tree = .{ .name = "base", .oid = &base_tree_oid } }, 609 .{ .tree = .{ .name = "head", .oid = &head_tree_oid } }, 610 }; 611 { 612 var draft = try Repo.open(io, allocator, .{ .path = draft_path }); 613 defer draft.deinit(io, allocator); 614 var arena = std.heap.ArenaAllocator.init(allocator); 615 defer arena.deinit(); 616 617 const initial_moment = try evt.currentMoment(repo_opts, &draft); 618 const initial_patch = (try evt.Patch.readById(Repo.DB, repo_opts.hash, initial_moment, &arena, &patch_id)) orelse return error.NotFound; 619 try std.testing.expect(!initial_patch.removed); 620 try std.testing.expectEqualStrings("add the answer", initial_patch.event.title); 621 try std.testing.expectEqual(null, initial_patch.event.revision); 622 try std.testing.expectEqual(null, try evt.PatchRev.readNewest(Repo.DB, repo_opts.hash, initial_moment, &arena)); 623 624 var source_moment = try source.core.latestMoment(); 625 const source_state = Repo.State(.read_only){ .core = &source.core, .extra = .{ .moment = &source_moment } }; 626 var objects = try obj.ObjectIterator(.xit, repo_opts).init(source_state, io, allocator, .{ .kind = .all }); 627 defer objects.deinit(); 628 try objects.include(&source_oid); 629 try draft.copyObjects(.xit, repo_opts, &objects, io, null); 630 try consumePatchWithRevision(.fork, io, allocator, &draft, evt.events_ref, first_patchrev_id, .{ 631 .base_oid = &base_oid, 632 .source_oid = &source_oid, 633 .target_ref = "refs/heads/master", 634 .message = "add the answer", 635 }, &first_entries, 3, patch_id, .{ 636 .title = "add the answer", 637 .tags = "enhancement", 638 .description = "adds a reusable answer constant", 639 }, 3); 640 641 _ = arena.reset(.retain_capacity); 642 const moment = try evt.currentMoment(repo_opts, &draft); 643 const patch = (try evt.Patch.readById(Repo.DB, repo_opts.hash, moment, &arena, &patch_id)) orelse return error.NotFound; 644 try std.testing.expect(!patch.removed); 645 try std.testing.expectEqualStrings(&first_patchrev_hex, &(patch.event.revision orelse return error.NotFound).id); 646 try std.testing.expect(null != try evt.PatchRev.readById(Repo.DB, repo_opts.hash, moment, &arena, &first_patchrev_id)); 647 } 648 try std.testing.expectError(error.NotFound, evt.currentMoment(repo_opts, &target)); 649 650 // 651 // post the metadata and revision pointer 652 // 653 654 try pch.post(repo_opts, io, allocator, &admin, &target, draft_path, .{ 655 .id = patch_id_hex, 656 .user_id = user_id, 657 .repo_id = repo_id, 658 .author = author, 659 .timestamp = 3, 660 }); 661 try pch.post(repo_opts, io, allocator, &admin, &target, draft_path, .{ 662 .id = patch_id_hex, 663 .user_id = user_id, 664 .repo_id = repo_id, 665 .author = author, 666 .timestamp = 4, 667 }); 668 669 var arena = std.heap.ArenaAllocator.init(allocator); 670 defer arena.deinit(); 671 const moment = try evt.currentMoment(repo_opts, &target); 672 const patch = (try evt.Patch.readById(Repo.DB, repo_opts.hash, moment, &arena, &patch_id)) orelse return error.NotFound; 673 try std.testing.expectEqualStrings(&first_patchrev_hex, &(patch.event.revision orelse return error.NotFound).id); 674 try std.testing.expectEqual(.open, patch.event.status); 675 try std.testing.expectEqual(null, try evt.PatchRev.readById(Repo.DB, repo_opts.hash, moment, &arena, &first_patchrev_id)); 676 677 _ = arena.reset(.retain_capacity); 678 const admin_moment = try evt.currentMoment(evt.admin_repo_opts, &admin); 679 const posted_fork = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &arena, &patch_id)) orelse return error.NotFound; 680 try std.testing.expect(!posted_fork.removed); 681 try std.testing.expectEqual(.posted, posted_fork.event.stage); 682 const draft_key = evt.Fork.draftKey(&repo_id, &user_id); 683 try std.testing.expectEqual(0, try indexedForkCount(admin_moment, evt.Fork.repo_user_to_draft_id_set_key, &draft_key)); 684 try std.testing.expectEqual(1, try indexedForkCount(admin_moment, evt.Fork.user_id_to_fork_id_set_key, &user_id)); 685 var squash_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 686 { 687 var draft = try Repo.open(io, allocator, .{ .path = draft_path }); 688 defer draft.deinit(io, allocator); 689 _ = arena.reset(.retain_capacity); 690 const draft_moment = try evt.currentMoment(repo_opts, &draft); 691 const removed = (try evt.Patch.readById(Repo.DB, repo_opts.hash, draft_moment, &arena, &patch_id)) orelse return error.NotFound; 692 try std.testing.expect(removed.removed); 693 const revision = (try evt.PatchRev.readById(Repo.DB, repo_opts.hash, draft_moment, &arena, &first_patchrev_id)) orelse return error.NotFound; 694 @memcpy(&squash_oid, revision.patch_oid); 695 try std.testing.expectEqualStrings("refs/heads/master", revision.event.target_ref); 696 try std.testing.expectEqual(hash.hexLen(repo_opts.hash), revision.event_oid.len); 697 var event_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 698 @memcpy(&event_oid, revision.event_oid); 699 var repo_moment = try draft.core.latestMoment(); 700 const state = Repo.State(.read_only){ .core = &draft.core, .extra = .{ .moment = &repo_moment } }; 701 var event_object = try obj.Object(.xit, repo_opts).initCommit(state, io, allocator, &event_oid); 702 defer event_object.deinit(); 703 const trees = try evt.PatchRev.readTrees(.xit, repo_opts, state, io, allocator, &event_object.content.commit.tree); 704 try std.testing.expectEqualStrings(revision.base_tree_oid, &trees.base); 705 try std.testing.expectEqualStrings(revision.head_tree_oid, &trees.head); 706 } 707 708 // 709 // merge the selected revision into the target 710 // 711 712 const selected_oid = switch (merge_revision) { 713 .squash => squash_oid, 714 .source => source_oid, 715 }; 716 try pch.merge(repo_opts, io, allocator, repos_dir, &target, .{ 717 .id = patch_id_hex, 718 .revision = merge_revision, 719 .author = author, 720 .timestamp = 5, 721 }); 722 const target_oid = (try target.readRef(io, .{ .kind = .head, .name = "master" })) orelse return error.NotFound; 723 try std.testing.expectEqualStrings(&selected_oid, &target_oid); 724 725 _ = arena.reset(.retain_capacity); 726 const merged_moment = try evt.currentMoment(repo_opts, &target); 727 const merged = (try evt.Patch.readById(Repo.DB, repo_opts.hash, merged_moment, &arena, &patch_id)) orelse return error.NotFound; 728 try std.testing.expectEqual(.merged, merged.event.status); 729 try std.testing.expect(null != try evt.PatchRev.readById(Repo.DB, repo_opts.hash, merged_moment, &arena, &first_patchrev_id)); 730 } 731 732 fn indexedForkCount( 733 moment: evt.AdminDB.HashMap(.read_only), 734 index_key: []const u8, 735 parent_id: []const u8, 736 ) !u64 { 737 const index_cursor = try moment.getCursor(hash.hashInt(evt.admin_repo_opts.hash, index_key)) orelse return 0; 738 const index = try evt.AdminDB.HashMap(.read_only).init(index_cursor); 739 const ids_cursor = try index.getCursor(hash.hashInt(evt.admin_repo_opts.hash, parent_id)) orelse return 0; 740 const ids = try evt.AdminDB.SortedSet(.read_only).init(ids_cursor); 741 return try ids.count(); 742 } 743 744 fn patchStatusCount(moment: Repo.DB.HashMap(.read_only), status: evt.Patch.Status) !u64 { 745 const statuses_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.status_to_id_set_key)) orelse return 0; 746 const statuses = try Repo.DB.SortedMap(.read_only).init(statuses_cursor); 747 const ids_cursor = try statuses.getCursor(@tagName(status)) orelse return 0; 748 const ids = try Repo.DB.SortedSet(.read_only).init(ids_cursor); 749 return try ids.count(); 750 } 751 752 fn patchTagStatusCount(moment: Repo.DB.HashMap(.read_only), tag: []const u8, status: evt.Patch.Status) !u64 { 753 const tags_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.tag_status_to_id_set_key)) orelse return 0; 754 const tags = try Repo.DB.SortedMap(.read_only).init(tags_cursor); 755 var key_buffer: evt.Patch.TagStatusKey = undefined; 756 const ids_cursor = try tags.getCursor(try evt.Patch.tagStatusKey(&key_buffer, tag, status)) orelse return 0; 757 const ids = try Repo.DB.SortedSet(.read_only).init(ids_cursor); 758 return try ids.count(); 759 } 760