╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ event.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
792 // 793 // edit the issue, so the merge below has a resolution to preserve 794 // 795 796 { 797 try evt.Issue.update(.xit, repo_opts, io, allocator, &repo, &issue_id, .{ .fields = .{ 798 .title = "Login form clears password on validation error", 799 .tags = "bug priority-medium ui", 800 .description = "Submitting an invalid email address resets the password field.", 801 } }, author); 802 } 803 804 // 805 // merging a branch that still holds the settled conflict must not bring it 806 // back 807 // 808 809 { 810 const conflicted_issue = events_to_consume3[0].event.issue orelse return error.NotFound; 811 812 var json: std.Io.Writer.Allocating = .init(allocator); 813 defer json.deinit(); 814 815 // the other branch changes the record on a commit that predates the 816 // resolution, so it still carries the conflict 817 const event = evt.EventWithId{ .id = events_to_consume3[0].id, .author = author, .event = .{ .issue = .{ 818 .title = conflicted_issue.title, 819 .description = conflicted_issue.description, 820 .tags = conflicted_issue.tags, 821 .status = .closed, 822 } } }; 823 try std.json.Stringify.value(event, .{}, &json.writer); 824 _ = try repo.commitAtRef(io, allocator, .{ .parent_oids = &.{conflicting_merge_oid}, .message = json.written() }, null, other_events_ref); 825 826 { 827 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 828 defer result.deinit(); 829 830 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_events_ref }} } } }, null); 831 defer merge.deinit(); 832 833 try std.testing.expect(.success == merge.result); 834 } 835 836 try evt.consume(.repo, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &.{}); 837 838 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 839 840 const conflicts_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.conflicts_key)) orelse return error.NotFound; 841 const conflicts = try Repo.DB.SortedMap(.read_only).init(conflicts_cursor); 842 try std.testing.expectEqual(0, try conflicts.count()); 843 844 // and the resolution stands 845 const issue = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &issue_id); 846 try std.testing.expectEqualStrings("bug priority-medium ui", issue.event.tags); 847 } 848 849 // 850 // diverge again, with each side editing different fields of one issue and 851 // only the other side touching a second issue 852 // 853 854 const second_issue = events_to_consume[1].event.issue orelse return error.NotFound; 855 const third_issue = events_to_consume[2].event.issue orelse return error.NotFound; 856 857 var retitle_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 858 var rewrite_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; 859 860 { 861 const base_oid = try repo.readRef(io, evt.events_ref) orelse return error.NotFound; 862 863 var json: std.Io.Writer.Allocating = .init(allocator); 864 defer json.deinit(); 865 866 // the events branch only retitles the second issue 867 { 868 const event = evt.EventWithId{ .id = events_to_consume[1].id, .author = author, .event = .{ .issue = .{ 869 .title = "Search results ignore the archived project filter", 870 .description = second_issue.description, 871 .tags = second_issue.tags, 872 } } }; 873 try std.json.Stringify.value(event, .{}, &json.writer); 874 const oid = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, evt.events_ref); 875 _ = try std.fmt.hexToBytes(&retitle_oid, &oid); 876 } 877 878 // the other branch rewrites the second issue's description and drops one 879 // of its tags, then retags the third issue 880 { 881 json.clearRetainingCapacity(); 882 const event = evt.EventWithId{ .id = events_to_consume[1].id, .author = author, .event = .{ .issue = .{ 883 .title = second_issue.title, 884 .description = "Archived projects are ranked before the flag is applied.", 885 .tags = "bug search", 886 } } }; 887 try std.json.Stringify.value(event, .{}, &json.writer); 888 const oid = try repo.commitAtRef(io, allocator, .{ .parent_oids = &.{base_oid}, .message = json.written() }, null, other_events_ref); 889 _ = try std.fmt.hexToBytes(&rewrite_oid, &oid); 890 } 891 { 892 json.clearRetainingCapacity(); 893 const event = evt.EventWithId{ .id = events_to_consume[2].id, .author = author, .event = .{ .issue = .{ 894 .title = third_issue.title, 895 .description = third_issue.description, 896 .tags = "enhancement roadmap", 897 } } }; 898 try std.json.Stringify.value(event, .{}, &json.writer); 899 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, other_events_ref); 900 } 901 } 902 903 { 904 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 905 defer result.deinit(); 906 907 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_events_ref }} } } }, null); 908 defer merge.deinit(); 909 910 try std.testing.expect(.success == merge.result); 911 } 912 913 { 914 try evt.consume(.repo, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &.{}); 915 916 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 917 918 var second_id: [evt.event_id_size]u8 = undefined; 919 _ = try std.fmt.hexToBytes(&second_id, &events_to_consume[1].id); 920 921 // each side's edit to a different field survives, without conflicting 922 const merged = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &second_id); 923 try std.testing.expectEqualStrings("Search results ignore the archived project filter", merged.event.title); 924 try std.testing.expectEqualStrings("Archived projects are ranked before the flag is applied.", merged.event.description); 925 try std.testing.expectEqualStrings("bug search", merged.event.tags); 926 927 // each merged field is attributed to the branch it came from 928 try std.testing.expectEqualSlices(u8, &retitle_oid, &try readOid(Repo.DB, repo_opts.hash, haxy_moment, &second_id, "title")); 929 try std.testing.expectEqualSlices(u8, &rewrite_oid, &try readOid(Repo.DB, repo_opts.hash, haxy_moment, &second_id, "description")); 930 931 const conflicts_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.conflicts_key)) orelse return error.NotFound; 932 const conflicts = try Repo.DB.SortedMap(.read_only).init(conflicts_cursor); 933 try std.testing.expectEqual(0, try conflicts.count()); 934 935 // tags the merged-in side dropped don't survive in the tag index, 936 // whether we edited that issue or left it alone 937 const tag_to_issues_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "tag+status->issue-id-set")) orelse return error.NotFound; 938 const tag_to_issues = try Repo.DB.SortedMap(.read_only).init(tag_to_issues_cursor); 939 940 var third_id: [evt.event_id_size]u8 = undefined; 941 _ = try std.fmt.hexToBytes(&third_id, &events_to_consume[2].id); 942 const retagged = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &third_id); 943 try std.testing.expectEqualStrings("enhancement roadmap", retagged.event.tags); 944 945 try std.testing.expect(null == try tag_to_issues.getCursor("backend open")); 946 try std.testing.expect(null == try tag_to_issues.getCursor("preferences open")); 947 try std.testing.expect(null != try tag_to_issues.getCursor("roadmap open")); 948 try std.testing.expect(null != try tag_to_issues.getCursor("enhancement open")); 949 } 950 951 // 952 // a removal on the other branch carries over, unless we edited the issue, 953 // in which case the edit wins 954 // 955 956 { 957 const edited_issue = events_to_consume2[1].event.issue orelse return error.NotFound; 958 959 const base_oid = try repo.readRef(io, evt.events_ref) orelse return error.NotFound; 960 961 var json: std.Io.Writer.Allocating = .init(allocator); 962 defer json.deinit(); 963 964 // we retitle one of the two the other branch is about to drop 965 { 966 const event = evt.EventWithId{ .id = events_to_consume2[1].id, .author = author, .event = .{ .issue = .{ 967 .title = "Kept by the edit", 968 .description = edited_issue.description, 969 .tags = edited_issue.tags, 970 } } }; 971 try std.json.Stringify.value(event, .{}, &json.writer); 972 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, evt.events_ref); 973 } 974 975 // the other branch removes both 976 { 977 json.clearRetainingCapacity(); 978 const event = evt.EventWithId{ .id = events_to_consume2[0].id, .author = author, .event = .{ .issue = null } }; 979 try std.json.Stringify.value(event, .{}, &json.writer); 980 _ = try repo.commitAtRef(io, allocator, .{ .parent_oids = &.{base_oid}, .message = json.written() }, null, other_events_ref); 981 } 982 { 983 json.clearRetainingCapacity(); 984 const event = evt.EventWithId{ .id = events_to_consume2[1].id, .author = author, .event = .{ .issue = null } }; 985 try std.json.Stringify.value(event, .{}, &json.writer); 986 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, other_events_ref); 987 } 988 989 { 990 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 991 defer result.deinit(); 992 993 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_events_ref }} } } }, null); 994 defer merge.deinit(); 995 996 try std.testing.expect(.success == merge.result); 997 } 998 999 try evt.consume(.repo, .xit, repo_opts, io, allocator, &repo, evt.events_ref, &.{}); 1000 1001 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 1002 1003 // the one we left alone is removed, and its tags left the index 1004 var dropped_id: [evt.event_id_size]u8 = undefined; 1005 _ = try std.fmt.hexToBytes(&dropped_id, &events_to_consume2[0].id); 1006 const dropped = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &dropped_id); 1007 try std.testing.expect(dropped.removed); 1008 1009 const tag_to_issues_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "tag+status->issue-id-set")) orelse return error.NotFound; 1010 const tag_to_issues = try Repo.DB.SortedMap(.read_only).init(tag_to_issues_cursor); 1011 try std.testing.expect(null == try tag_to_issues.getCursor("kanban open")); 1012 1013 // the one we edited survives, with our edit 1014 var kept_id: [evt.event_id_size]u8 = undefined; 1015 _ = try std.fmt.hexToBytes(&kept_id, &events_to_consume2[1].id); 1016 const kept = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &kept_id); 1017 try std.testing.expect(!kept.removed); 1018 try std.testing.expectEqualStrings("Kept by the edit", kept.event.title); 1019 } 1020 } 1021 1022 // the commit that last set `field_name` on the issue `id` names 1023 fn readOid( 1024 comptime DB: type, 1025 comptime hash_kind: hash.HashKind, 1026 haxy_moment: DB.HashMap(.read_only), 1027 id: *const [evt.event_id_size]u8, 1028 comptime field_name: []const u8, 1029 ) ![hash.byteLen(hash_kind)]u8 { 1030 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; 1031 const id_to_field_to_oid = try DB.HashMap(.read_only).init(id_to_field_to_oid_cursor); 1032 const field_oids_cursor = try id_to_field_to_oid.getCursor(hash.hashInt(hash_kind, id)) orelse return error.NotFound; 1033 const field_oids = try DB.SortedMap(.read_only).init(field_oids_cursor); 1034 const cursor = try field_oids.getCursor(field_name) orelse return error.NotFound; 1035 var oid: [hash.byteLen(hash_kind)]u8 = undefined; 1036 _ = try cursor.readBytes(&oid); 1037 return oid; 1038 } 1039 1040 // the issue `id` names, read out of `event-id->issue` 1041 fn readIssue( 1042 comptime DB: type, 1043 comptime hash_kind: hash.HashKind, 1044 haxy_moment: DB.HashMap(.read_only), 1045 arena: *std.heap.ArenaAllocator, 1046 id: *const [evt.event_id_size]u8, 1047 ) !evt.Issue.Record { 1048 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "event-id->issue")) orelse return error.NotFound; 1049 const event_id_to_issue = try DB.HashMap(.read_only).init(event_id_to_issue_cursor); 1050 const issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(hash_kind, id)) orelse return error.NotFound; 1051 const issue_map = try DB.HashMap(.read_only).init(issue_cursor); 1052 return try evt.read(evt.Issue.Record, DB, hash_kind, arena, issue_map); 1053 } 1054