╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ event.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭ find file ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ ╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ │ │ │http│ ssh │ │ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯
552 const issue_id_set = try Repo.DB.SortedSet(.read_only).init(issue_id_set_cursor); 553 try std.testing.expectEqual(6, try issue_id_set.count()); 554 555 var found: [6]bool = @splat(false); 556 var iter = try issue_id_set.iteratorFromIndex(0); 557 while (try iter.next()) |id_cursor_val| { 558 var id_cursor = id_cursor_val; 559 const id_kv = try id_cursor.readKeyValuePair(); 560 var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; 561 _ = try id_kv.key_cursor.readBytes(&order_key); 562 const id_hex = std.fmt.bytesToHex(order_key[@sizeOf(u64)..].*, .lower); 563 for (events_to_consume, 0..) |event, i| { 564 if (std.mem.eql(u8, &id_hex, &event.id)) found[i] = true; 565 } 566 for (events_to_consume2, 0..) |event, i| { 567 if (std.mem.eql(u8, &id_hex, &event.id)) found[events_to_consume.len + i] = true; 568 } 569 } 570 for (found) |f| try std.testing.expect(f); 571 } 572 } 573 574 // 575 // insert issues as commits in the repo 576 // 577 578 { 579 var json: std.Io.Writer.Allocating = .init(allocator); 580 defer json.deinit(); 581 582 for (events_to_consume, 0..) |event, i| { 583 json.clearRetainingCapacity(); 584 585 try std.json.Stringify.value(event, .{}, &json.writer); 586 587 // commit the event into a special branch 588 _ = try repo.commitAtRef( 589 io, 590 allocator, 591 // make the first event have no parents so we completely rebase the branch, 592 // undoing the merge that we did above 593 .{ .parent_oids = if (i == 0) &.{} else null, .message = json.written() }, 594 null, 595 evt.events_ref, 596 ); 597 } 598 } 599 600 // 601 // consume events into the database 602 // 603 604 { 605 try evt.consume(.local, .repo, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &.{}); 606 607 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 608 609 // get the map of issues 610 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse return error.NotFound; 611 const event_id_to_issue = try Repo.DB.HashMap(.read_only).init(event_id_to_issue_cursor); 612 613 // make sure the new issue is not there 614 { 615 // get the issue out of the map 616 var first_issue_id: [evt.event_id_size]u8 = undefined; 617 _ = try std.fmt.hexToBytes(&first_issue_id, &events_to_consume2[0].id); 618 const first_issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &first_issue_id)); 619 try std.testing.expect(null == first_issue_cursor); 620 } 621 } 622 623 // 624 // define test events that modify the same issue in conflicting ways 625 // 626 627 const events_to_consume3 = [_]evt.EventWithId{ 628 .{ 629 .id = events_to_consume[0].id, 630 .author = author, 631 .event = .{ 632 .issue = .{ 633 .title = "Login form clears password on validation error", 634 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 635 .labels = "bug priority-low ui", 636 }, 637 }, 638 }, 639 .{ 640 .id = events_to_consume[0].id, 641 .author = author, 642 .event = .{ 643 .issue = .{ 644 .title = "Login form clears password on validation error", 645 .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", 646 .labels = "bug priority-medium ui", 647 }, 648 }, 649 }, 650 }; 651 652 // 653 // insert issues as commits in the repo on different branches 654 // 655 656 var conflicting_oids: [events_to_consume3.len][hash.byteLen(repo_opts.hash)]u8 = undefined; 657 658 { 659 var json: std.Io.Writer.Allocating = .init(allocator); 660 defer json.deinit(); 661 662 for (events_to_consume3, 0..) |event, i| { 663 json.clearRetainingCapacity(); 664 665 try std.json.Stringify.value(event, .{}, &json.writer); 666 667 // commit the event into a special branch 668 const oid = try repo.commitAtRef( 669 io, 670 allocator, 671 .{ .parent_oids = &.{merge_oid}, .message = json.written() }, 672 null, 673 switch (i) { 674 0 => evt.events_ref, 675 1 => other_events_ref, 676 else => unreachable, 677 }, 678 ); 679 _ = try std.fmt.hexToBytes(&conflicting_oids[i], &oid); 680 } 681 } 682 683 // 684 // check out the events branch and merge the other events branch into it 685 // 686 687 const conflicting_merge_oid = blk: { 688 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 689 defer result.deinit(); 690 691 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_events_ref }} } } }, null); 692 defer merge.deinit(); 693 694 try std.testing.expect(.success == merge.result); 695 696 break :blk merge.result.success.oid; 697 }; 698 699 // 700 // consume events into the database 701 // 702 703 var issue_id: [evt.event_id_size]u8 = undefined; 704 _ = try std.fmt.hexToBytes(&issue_id, &events_to_consume[0].id); 705 706 { 707 try evt.consume(.local, .repo, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &.{}); 708 709 // consuming the merge again must be a no-op rather than replaying it 710 try evt.consume(.local, .repo, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &.{}); 711 712 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 713 714 // the first parent is the events branch, so its version stays live 715 const issue = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &issue_id); 716 try std.testing.expectEqualStrings("bug priority-low ui", issue.event.labels); 717 718 // the issue is listed as conflicted, with the merge base recorded 719 const conflicts_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.conflicts_key)) orelse return error.NotFound; 720 const conflicts = try Repo.DB.SortedMap(.read_only).init(conflicts_cursor); 721 try std.testing.expectEqual(1, try conflicts.count()); 722 723 const order_key = evt.orderKeyDesc(issue.created_order, &issue_id); 724 const conflict_cursor = try conflicts.getCursor(&order_key) orelse return error.NotFound; 725 const conflict = try Repo.DB.HashMap(.read_only).init(conflict_cursor); 726 727 const base_cursor = try conflict.getCursor(hash.hashInt(repo_opts.hash, evt.base_record_key)) orelse return error.NotFound; 728 const base = try evt.read(evt.Issue.Record, Repo.DB, repo_opts.hash, &arena, try Repo.DB.HashMap(.read_only).init(base_cursor)); 729 try std.testing.expectEqualStrings("bug priority-high ui", base.event.labels); 730 731 // labels is the one field both sides changed differently 732 const fields_cursor = try conflict.getCursor(hash.hashInt(repo_opts.hash, evt.conflicted_fields_key)) orelse return error.NotFound; 733 const conflicted_fields = try fields_cursor.readBytesAlloc(arena.allocator(), null); 734 try std.testing.expectEqualStrings("labels", conflicted_fields); 735 736 // their whole version is recorded, not just the conflicting field 737 const their_cursor = try conflict.getCursor(hash.hashInt(repo_opts.hash, evt.their_record_key)) orelse return error.NotFound; 738 const theirs = try evt.read(evt.Issue.Record, Repo.DB, repo_opts.hash, &arena, try Repo.DB.HashMap(.read_only).init(their_cursor)); 739 try std.testing.expectEqualStrings("bug priority-medium ui", theirs.event.labels); 740 try std.testing.expectEqualStrings(issue.event.title, theirs.event.title); 741 try std.testing.expectEqualStrings(issue.event.description, theirs.event.description); 742 743 // their value is attributed to the commit that set it on their branch 744 const oids_cursor = try conflict.getCursor(hash.hashInt(repo_opts.hash, evt.their_field_to_oid_key)) orelse return error.NotFound; 745 const oids = try Repo.DB.SortedMap(.read_only).init(oids_cursor); 746 const oid_cursor = try oids.getCursor("labels") orelse return error.NotFound; 747 var their_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 748 _ = try oid_cursor.readBytes(&their_oid); 749 try std.testing.expectEqualSlices(u8, &conflicting_oids[1], &their_oid); 750 751 // ours stays attributed to the commit on the events branch 752 try std.testing.expectEqualSlices(u8, &conflicting_oids[0], &try readOid(Repo.DB, repo_opts.hash, haxy_moment, &issue_id, "labels")); 753 754 // the losing side's label didn't leak into the label index 755 const label_to_issues_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "label+status->issue-id-set")) orelse return error.NotFound; 756 const label_to_issues = try Repo.DB.SortedMap(.read_only).init(label_to_issues_cursor); 757 try std.testing.expect(null != try label_to_issues.getCursor("priority-low open")); 758 try std.testing.expect(null == try label_to_issues.getCursor("priority-medium open")); 759 } 760 761 // 762 // a merge that touches the issue without conflicting leaves the conflict 763 // alone, since only events settle one 764 // 765 766 { 767 const their_issue = events_to_consume3[1].event.issue orelse return error.NotFound; 768 769 var json: std.Io.Writer.Allocating = .init(allocator); 770 defer json.deinit(); 771 772 // the other branch never saw the conflict and rewrites an unrelated 773 // field 774 const event = evt.EventWithId{ .id = events_to_consume3[1].id, .author = author, .event = .{ .issue = .{ 775 .title = their_issue.title, 776 .description = "Rewritten on a branch that never merged.", 777 .labels = their_issue.labels, 778 } } }; 779 try std.json.Stringify.value(event, .{}, &json.writer); 780 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, other_events_ref); 781 782 { 783 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 784 defer result.deinit(); 785 786 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_events_ref }} } } }, null); 787 defer merge.deinit(); 788 789 try std.testing.expect(.success == merge.result); 790 } 791 792 try evt.consume(.local, .repo, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &.{}); 793 794 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 795 796 // their edit came over, so the merge really did touch the issue 797 const issue = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &issue_id); 798 try std.testing.expectEqualStrings("Rewritten on a branch that never merged.", issue.event.description); 799 800 // and the conflict is still waiting to be resolved 801 const conflicts_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.conflicts_key)) orelse return error.NotFound; 802 const conflicts = try Repo.DB.SortedMap(.read_only).init(conflicts_cursor); 803 try std.testing.expectEqual(1, try conflicts.count()); 804 } 805 806 // 807 // any event settles the conflict, even one that changes no conflicting 808 // field 809 // 810 811 { 812 try evt.Issue.update(.local, .xit, repo_opts, io, allocator, &repo, &issue_id, .{ .status = .closed }, author); 813 814 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 815 const conflicts_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.conflicts_key)) orelse return error.NotFound; 816 const conflicts = try Repo.DB.SortedMap(.read_only).init(conflicts_cursor); 817 try std.testing.expectEqual(0, try conflicts.count()); 818 } 819 820 // 821 // edit the issue, so the merge below has a resolution to preserve 822 // 823 824 { 825 try evt.Issue.update(.local, .xit, repo_opts, io, allocator, &repo, &issue_id, .{ .fields = .{ 826 .title = "Login form clears password on validation error", 827 .labels = "bug priority-medium ui", 828 .description = "Submitting an invalid email address resets the password field.", 829 } }, author); 830 } 831 832 // 833 // merging a branch that still holds the settled conflict must not bring it 834 // back 835 // 836 837 { 838 const conflicted_issue = events_to_consume3[0].event.issue orelse return error.NotFound; 839 840 var json: std.Io.Writer.Allocating = .init(allocator); 841 defer json.deinit(); 842 843 // the other branch changes the record on a commit that predates the 844 // resolution, so it still carries the conflict 845 const event = evt.EventWithId{ .id = events_to_consume3[0].id, .author = author, .event = .{ .issue = .{ 846 .title = conflicted_issue.title, 847 .description = conflicted_issue.description, 848 .labels = conflicted_issue.labels, 849 .status = .closed, 850 } } }; 851 try std.json.Stringify.value(event, .{}, &json.writer); 852 _ = try repo.commitAtRef(io, allocator, .{ .parent_oids = &.{conflicting_merge_oid}, .message = json.written() }, null, other_events_ref); 853 854 { 855 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 856 defer result.deinit(); 857 858 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_events_ref }} } } }, null); 859 defer merge.deinit(); 860 861 try std.testing.expect(.success == merge.result); 862 } 863 864 try evt.consume(.local, .repo, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &.{}); 865 866 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 867 868 const conflicts_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.conflicts_key)) orelse return error.NotFound; 869 const conflicts = try Repo.DB.SortedMap(.read_only).init(conflicts_cursor); 870 try std.testing.expectEqual(0, try conflicts.count()); 871 872 // and the resolution stands 873 const issue = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &issue_id); 874 try std.testing.expectEqualStrings("bug priority-medium ui", issue.event.labels); 875 } 876 877 // 878 // diverge again, with each side editing different fields of one issue and 879 // only the other side touching a second issue 880 // 881 882 const second_issue = events_to_consume[1].event.issue orelse return error.NotFound; 883 const third_issue = events_to_consume[2].event.issue orelse return error.NotFound; 884 885 var retitle_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 886 var rewrite_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 887 888 { 889 const base_oid = try repo.readRef(io, evt.events_ref) orelse return error.NotFound; 890 891 var json: std.Io.Writer.Allocating = .init(allocator); 892 defer json.deinit(); 893 894 // the events branch only retitles the second issue 895 { 896 const event = evt.EventWithId{ .id = events_to_consume[1].id, .author = author, .event = .{ .issue = .{ 897 .title = "Search results ignore the archived project filter", 898 .description = second_issue.description, 899 .labels = second_issue.labels, 900 } } }; 901 try std.json.Stringify.value(event, .{}, &json.writer); 902 const oid = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, evt.events_ref); 903 _ = try std.fmt.hexToBytes(&retitle_oid, &oid); 904 } 905 906 // the other branch rewrites the second issue's description and drops one 907 // of its labels, then relabels the third issue 908 { 909 json.clearRetainingCapacity(); 910 const event = evt.EventWithId{ .id = events_to_consume[1].id, .author = author, .event = .{ .issue = .{ 911 .title = second_issue.title, 912 .description = "Archived projects are ranked before the flag is applied.", 913 .labels = "bug search", 914 } } }; 915 try std.json.Stringify.value(event, .{}, &json.writer); 916 const oid = try repo.commitAtRef(io, allocator, .{ .parent_oids = &.{base_oid}, .message = json.written() }, null, other_events_ref); 917 _ = try std.fmt.hexToBytes(&rewrite_oid, &oid); 918 } 919 { 920 json.clearRetainingCapacity(); 921 const event = evt.EventWithId{ .id = events_to_consume[2].id, .author = author, .event = .{ .issue = .{ 922 .title = third_issue.title, 923 .description = third_issue.description, 924 .labels = "enhancement roadmap", 925 } } }; 926 try std.json.Stringify.value(event, .{}, &json.writer); 927 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, other_events_ref); 928 } 929 } 930 931 { 932 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 933 defer result.deinit(); 934 935 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_events_ref }} } } }, null); 936 defer merge.deinit(); 937 938 try std.testing.expect(.success == merge.result); 939 } 940 941 { 942 try evt.consume(.local, .repo, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &.{}); 943 944 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 945 946 var second_id: [evt.event_id_size]u8 = undefined; 947 _ = try std.fmt.hexToBytes(&second_id, &events_to_consume[1].id); 948 949 // each side's edit to a different field survives, without conflicting 950 const merged = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &second_id); 951 try std.testing.expectEqualStrings("Search results ignore the archived project filter", merged.event.title); 952 try std.testing.expectEqualStrings("Archived projects are ranked before the flag is applied.", merged.event.description); 953 try std.testing.expectEqualStrings("bug search", merged.event.labels); 954 955 // each merged field is attributed to the branch it came from 956 try std.testing.expectEqualSlices(u8, &retitle_oid, &try readOid(Repo.DB, repo_opts.hash, haxy_moment, &second_id, "title")); 957 try std.testing.expectEqualSlices(u8, &rewrite_oid, &try readOid(Repo.DB, repo_opts.hash, haxy_moment, &second_id, "description")); 958 959 const conflicts_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.conflicts_key)) orelse return error.NotFound; 960 const conflicts = try Repo.DB.SortedMap(.read_only).init(conflicts_cursor); 961 try std.testing.expectEqual(0, try conflicts.count()); 962 963 // labels the merged-in side dropped don't survive in the label index, 964 // whether we edited that issue or left it alone 965 const label_to_issues_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "label+status->issue-id-set")) orelse return error.NotFound; 966 const label_to_issues = try Repo.DB.SortedMap(.read_only).init(label_to_issues_cursor); 967 968 var third_id: [evt.event_id_size]u8 = undefined; 969 _ = try std.fmt.hexToBytes(&third_id, &events_to_consume[2].id); 970 const relabeled = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &third_id); 971 try std.testing.expectEqualStrings("enhancement roadmap", relabeled.event.labels); 972 973 try std.testing.expect(null == try label_to_issues.getCursor("backend open")); 974 try std.testing.expect(null == try label_to_issues.getCursor("preferences open")); 975 try std.testing.expect(null != try label_to_issues.getCursor("roadmap open")); 976 try std.testing.expect(null != try label_to_issues.getCursor("enhancement open")); 977 } 978 979 // 980 // a removal on the other branch carries over, unless we edited the issue, 981 // in which case the edit wins 982 // 983 984 { 985 const edited_issue = events_to_consume2[1].event.issue orelse return error.NotFound; 986 987 const base_oid = try repo.readRef(io, evt.events_ref) orelse return error.NotFound; 988 989 var json: std.Io.Writer.Allocating = .init(allocator); 990 defer json.deinit(); 991 992 // we retitle one of the two the other branch is about to drop 993 { 994 const event = evt.EventWithId{ .id = events_to_consume2[1].id, .author = author, .event = .{ .issue = .{ 995 .title = "Kept by the edit", 996 .description = edited_issue.description, 997 .labels = edited_issue.labels, 998 } } }; 999 try std.json.Stringify.value(event, .{}, &json.writer); 1000 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, evt.events_ref); 1001 } 1002 1003 // the other branch removes both 1004 { 1005 json.clearRetainingCapacity(); 1006 const event = evt.EventWithId{ .id = events_to_consume2[0].id, .author = author, .event = .{ .issue = null } }; 1007 try std.json.Stringify.value(event, .{}, &json.writer); 1008 _ = try repo.commitAtRef(io, allocator, .{ .parent_oids = &.{base_oid}, .message = json.written() }, null, other_events_ref); 1009 } 1010 { 1011 json.clearRetainingCapacity(); 1012 const event = evt.EventWithId{ .id = events_to_consume2[1].id, .author = author, .event = .{ .issue = null } }; 1013 try std.json.Stringify.value(event, .{}, &json.writer); 1014 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, other_events_ref); 1015 } 1016 1017 { 1018 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 1019 defer result.deinit(); 1020 1021 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_events_ref }} } } }, null); 1022 defer merge.deinit(); 1023 1024 try std.testing.expect(.success == merge.result); 1025 } 1026 1027 try evt.consume(.local, .repo, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &.{}); 1028 1029 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 1030 1031 // the one we left alone is removed, and its labels left the index 1032 var dropped_id: [evt.event_id_size]u8 = undefined; 1033 _ = try std.fmt.hexToBytes(&dropped_id, &events_to_consume2[0].id); 1034 const dropped = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &dropped_id); 1035 try std.testing.expect(dropped.removed); 1036 1037 const label_to_issues_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "label+status->issue-id-set")) orelse return error.NotFound; 1038 const label_to_issues = try Repo.DB.SortedMap(.read_only).init(label_to_issues_cursor); 1039 try std.testing.expect(null == try label_to_issues.getCursor("kanban open")); 1040 1041 // the one we edited survives, with our edit 1042 var kept_id: [evt.event_id_size]u8 = undefined; 1043 _ = try std.fmt.hexToBytes(&kept_id, &events_to_consume2[1].id); 1044 const kept = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &kept_id); 1045 try std.testing.expect(!kept.removed); 1046 try std.testing.expectEqualStrings("Kept by the edit", kept.event.title); 1047 } 1048 } 1049 1050 // the commit that last set `field_name` on the issue `id` names 1051 fn readOid( 1052 comptime DB: type, 1053 comptime hash_kind: hash.HashKind, 1054 haxy_moment: DB.HashMap(.read_only), 1055 id: *const [evt.event_id_size]u8, 1056 comptime field_name: []const u8, 1057 ) ![hash.byteLen(hash_kind)]u8 { 1058 const id_to_field_to_oid_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, evt.Issue.id_to_field_to_oid_key)) orelse return error.NotFound; 1059 const id_to_field_to_oid = try DB.HashMap(.read_only).init(id_to_field_to_oid_cursor); 1060 const field_oids_cursor = try id_to_field_to_oid.getCursor(hash.hashInt(hash_kind, id)) orelse return error.NotFound; 1061 const field_oids = try DB.SortedMap(.read_only).init(field_oids_cursor); 1062 const cursor = try field_oids.getCursor(field_name) orelse return error.NotFound; 1063 var oid: [hash.byteLen(hash_kind)]u8 = undefined; 1064 _ = try cursor.readBytes(&oid); 1065 return oid; 1066 } 1067 1068 // the issue `id` names, read out of `event-id->issue` 1069 fn readIssue( 1070 comptime DB: type, 1071 comptime hash_kind: hash.HashKind, 1072 haxy_moment: DB.HashMap(.read_only), 1073 arena: *std.heap.ArenaAllocator, 1074 id: *const [evt.event_id_size]u8, 1075 ) !evt.Issue.Record { 1076 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "event-id->issue")) orelse return error.NotFound; 1077 const event_id_to_issue = try DB.HashMap(.read_only).init(event_id_to_issue_cursor); 1078 const issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(hash_kind, id)) orelse return error.NotFound; 1079 const issue_map = try DB.HashMap(.read_only).init(issue_cursor); 1080 return try evt.read(evt.Issue.Record, DB, hash_kind, arena, issue_map); 1081 } 1082