╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ try.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
621 const ours = [_]evt.EventWithId{ .{ 622 .id = issue_events[issue_data.len - 4].id, 623 .timestamp = 100, 624 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 625 .event = .{ .issue = .{ 626 .title = "Crash when resizing the window during the splash animation", 627 .description = title_issue.description, 628 .tags = "bug crash ui priority-high", 629 } }, 630 }, .{ 631 .id = issue_events[issue_data.len - 3].id, 632 .timestamp = 101, 633 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 634 .event = .{ .issue = .{ 635 .title = sync_issue.title, 636 .description = "The sync engine's coverage is unit tests only, with every transport mocked out." ++ "\n\n" ++ 637 sync_p2 ++ "\n\n" ++ sync_p3, 638 .tags = sync_issue.tags, 639 } }, 640 }, .{ 641 .id = issue_events[issue_data.len - 2].id, 642 .timestamp = 102, 643 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 644 .event = .{ .issue = .{ 645 .title = desc_issue.title, 646 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ tz_p4 ++ "\n\n" ++ 647 "The confusion is worst for teams spread across timezones, who each read a different wall-clock time from the same feed.", 648 .tags = desc_issue.tags, 649 } }, 650 } }; 651 const theirs = [_]evt.EventWithId{ .{ 652 .id = issue_events[issue_data.len - 4].id, 653 .timestamp = 103, 654 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 655 .event = .{ .issue = .{ 656 .title = "Segfault on early window resize", 657 .description = title_issue.description, 658 .tags = "bug crash rendering", 659 } }, 660 }, .{ 661 .id = issue_events[issue_data.len - 3].id, 662 .timestamp = 104, 663 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 664 .event = .{ .issue = .{ 665 .title = sync_issue.title, 666 .description = sync_p1 ++ "\n\n" ++ sync_p2 ++ "\n\n" ++ 667 "Add end-to-end tests that drive two live instances against a local server on every CI run.", 668 .tags = sync_issue.tags, 669 } }, 670 }, .{ 671 .id = issue_events[issue_data.len - 2].id, 672 .timestamp = 105, 673 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 674 .event = .{ .issue = .{ 675 .title = desc_issue.title, 676 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ 677 "Most other clients already render local time by default, which makes our UTC output stand out as a bug." ++ "\n\n" ++ tz_p4 ++ "\n\n" ++ 678 "Log exports inherit the same UTC rendering, so downstream tooling has to guess the source timezone.", 679 .tags = desc_issue.tags, 680 } }, 681 } }; 682 683 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &ours); 684 685 try commitEventsAtRef(io, allocator, &template_repo, other_ref, &theirs, seed_tip); 686 687 { 688 var to_events = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 689 defer to_events.deinit(); 690 } 691 { 692 var merge = try template_repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, null); 693 defer merge.deinit(); 694 if (merge.result != .success) return error.MergeFailed; 695 } 696 { 697 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 698 defer to_master.deinit(); 699 } 700 try template_repo.removeBranch(io, .{ .name = other_ref.name }); 701 } 702 703 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &.{}); 704 705 // seed a small comment tree on the newest issue 706 var comment_ids: [5][evt.event_id_size]u8 = undefined; 707 for (&comment_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 708 709 const comment_bodies = [_][]const u8{ 710 "I can take this. The compatibility scanner already reports which repositories still use the legacy format.", 711 "Can we keep loading legacy plugins for one release after the warning lands? That would give downstream maintainers time to migrate.", 712 "That sounds good. I'll include the scanner output in the warning so each affected plugin is named.", 713 "Please include the replacement manifest path too. That should make the warning actionable without opening the migration guide.", 714 "I'll add release-note text once the warning format is settled.", 715 }; 716 717 var comment_events: [comment_ids.len]evt.EventWithId = undefined; 718 for (&comment_events, 0..) |*event, i| { 719 event.* = .{ 720 .id = std.fmt.bytesToHex(comment_ids[i], .lower), 721 .timestamp = @intCast(200 + i), 722 .author = .{ .name = user_data[(i + 1) % user_data.len].name, .email = user_data[(i + 1) % user_data.len].email }, 723 .event = .{ .comment = .{ 724 .thread_id = issue_events[issue_events.len - 1].id, 725 .parent_id = switch (i) { 726 0, 1 => issue_events[issue_events.len - 1].id, 727 2, 4 => std.fmt.bytesToHex(comment_ids[0], .lower), 728 3 => std.fmt.bytesToHex(comment_ids[2], .lower), 729 else => unreachable, 730 }, 731 .body = comment_bodies[i], 732 } }, 733 }; 734 } 735 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &comment_events); 736 737 const discussion_data = [_]struct { 738 title: []const u8, 739 description: []const u8, 740 tags: []const u8, 741 }{ 742 .{ 743 .title = "How should plugins declare capabilities?", 744 .description = "I'd like the manifest to make privileged capabilities explicit without making simple plugins verbose.", 745 .tags = "plugins design", 746 }, 747 .{ 748 .title = "Ideas for making large repositories faster", 749 .description = "This is a place to collect profiling results and discuss which indexing work is worth pursuing first.", 750 .tags = "performance indexing", 751 }, 752 .{ 753 .title = "What should the next release focus on?", 754 .description = "Let's compare the most important reliability fixes with the larger features already in progress.", 755 .tags = "release planning", 756 }, 757 .{ 758 .title = "Improving keyboard navigation", 759 .description = "Share workflows that still require a mouse and suggestions for making their focus behavior predictable.", 760 .tags = "ui keyboard accessibility", 761 }, 762 .{ 763 .title = "Configuration format discussion", 764 .description = "Should the next configuration format favor strict validation or accept common conveniences such as trailing commas?", 765 .tags = "config design", 766 }, 767 }; 768 769 var discussion_events: [discussion_data.len]evt.EventWithId = undefined; 770 for (discussion_data, 0..) |discussion, i| { 771 discussion_events[i] = .{ 772 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 773 .timestamp = @intCast(300 + i), 774 .author = .{ .name = user_data[i % user_data.len].name, .email = user_data[i % user_data.len].email }, 775 .event = .{ .discuss = .{ 776 .title = discussion.title, 777 .description = discussion.description, 778 .tags = discussion.tags, 779 } }, 780 }; 781 } 782 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &discussion_events); 783 784 var discussion_comment_ids: [discussion_data.len][3][evt.event_id_size]u8 = undefined; 785 for (&discussion_comment_ids) |*ids| { 786 for (ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 787 } 788 var discussion_comment_events: [discussion_data.len * 3]evt.EventWithId = undefined; 789 for (&discussion_comment_events, 0..) |*event, i| { 790 const discussion_index = i / 3; 791 const comment_index = i % 3; 792 event.* = .{ 793 .id = std.fmt.bytesToHex(discussion_comment_ids[discussion_index][comment_index], .lower), 794 .timestamp = @intCast(400 + i), 795 .author = .{ .name = user_data[(i + 1) % user_data.len].name, .email = user_data[(i + 1) % user_data.len].email }, 796 .event = .{ .comment = .{ 797 .thread_id = discussion_events[discussion_index].id, 798 .parent_id = if (comment_index < 2) 799 discussion_events[discussion_index].id 800 else 801 std.fmt.bytesToHex(discussion_comment_ids[discussion_index][0], .lower), 802 .body = switch (comment_index) { 803 0 => "My preference is to start with the smallest useful version and expand it after we have real usage to learn from.", 804 1 => "It would help to write down the tradeoffs we are accepting so the decision is easy to revisit later.", 805 2 => "Agreed. A narrow first version should also make it easier to keep the implementation understandable.", 806 else => unreachable, 807 }, 808 } }, 809 }; 810 } 811 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &discussion_comment_events); 812 try template_repo.patchAll(io, allocator, null); 813 try template_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "true" }); 814 // fixtures commit directly, so index the ref tips by hand 815 try find.refresh(.{}, io, allocator, &template_repo); 816 try srch_cmmt.refresh(.{}, io, allocator, &template_repo, null); 817 // clear history only after indexing has written its transactions. 818 { 819 const moment = try evt.currentMoment(.{}, &template_repo); 820 const roots = try evt.PatchRev.gcRoots(rp.Repo(.xit, .{}).DB, .sha1, allocator, moment); 821 defer allocator.free(roots); 822 _ = try template_repo.garbageCollect(io, allocator, .{ .extra_roots = roots }); 823 } 824 } 825 826 // copy the template to each repo's on-disk location, named by its 827 // hex-encoded event id. the template repo is deinitialized above, so its 828 // db file is fully written before we copy it. 829 { 830 var template_dir = try cwd.openDir(io, template_path, .{}); 831 defer template_dir.close(io); 832 var template_repo_dir = try template_dir.openDir(io, ".xit", .{ .iterate = true }); 833 defer template_repo_dir.close(io); 834 835 for (repo_event_ids, 0..) |id_bytes, repo_index| { 836 const repo_id = std.fmt.bytesToHex(id_bytes, .lower); 837 const repo_path = try std.fs.path.join(arena.allocator(), &.{ server_path, "repos", &repo_id }); 838 { 839 var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); 840 defer dest_dir.close(io); 841 var dest_repo_dir = try dest_dir.createDirPathOpen(io, ".xit", .{}); 842 defer dest_repo_dir.close(io); 843 try copyDir(io, template_repo_dir, dest_repo_dir); 844 } 845 846 if (repo_index + 1 == repo_data.len) { 847 var target_repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = repo_path, .require_repo_root = true }); 848 defer target_repo.deinit(io, allocator); 849 try seedPatches(io, allocator, server_path, &repo, &target_repo, &repo_event_ids[repo_index], &admin_user_id, prng.random()); 850 } 851 } 852 } 853 854 break :blk try ui.Session.init(&session_arena, &repo, .{ .user_id = &admin_user_id }); 855 }; 856 session.is_terminal = true; 857 session.data.git_ssh_prefix = git_ssh_prefix; 858 859 // start the server 860 861 // let the native TUI's page builders open the on-disk repos for the file tree 862 session.io = io; 863 session.repos_dir = try std.fs.path.join(session_arena.allocator(), &.{ server_path, "repos" }); 864 865 // leave a one-shot session for the first browser to hit the web ui, so it 866 // starts logged in as admin 867 { 868 var data_dir = try cwd.createDirPathOpen(io, server_path, .{}); 869 defer data_dir.close(io); 870 const store = try hx.web.SessionStore.init(io, data_dir); 871 defer store.deinit(); 872 const token = try store.create(&admin_user_id); 873 try store.offerAutoLogin(&token); 874 } 875 876 if (cli) { 877 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 878 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 879 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; 880 881 const Runnable = struct { 882 io: std.Io, 883 key_path: []const u8, 884 885 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port_maybe: ?u16) !void { 886 _ = web_port; 887 _ = http_port; 888 const ssh_port = ssh_port_maybe orelse return error.SshDisabled; 889 std.debug.print( 890 \\ 891 \\connect to the TUI with: 892 \\ ssh -p {d} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost 893 \\ 894 \\create a git repo and push over SSH: 895 \\ mkdir -p temp-try/client/test 896 \\ cd temp-try/client/test 897 \\ git init 898 \\ echo "hello" > hello.txt 899 \\ git add hello.txt 900 \\ git commit -m "let there be light" 901 \\ GIT_SSH_COMMAND='ssh -p {d} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -i {s}' git push localhost:admin/test HEAD:master 902 \\ 903 \\to quit, press enter. 904 \\ 905 , .{ ssh_port, ssh_port, self.key_path }); 906 // portable stdin read via std.Io — equivalent to the 907 // std.posix.read(STDIN_FILENO, ...) that doesn't compile 908 // on windows. 909 var buf: [1]u8 = undefined; 910 var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); 911 _ = stdin_reader.interface.takeByte() catch {}; 912 } 913 }; 914 915 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .key_path = key_path }); 916 } else { 917 var null_writer = std.Io.Writer.Discarding.init(&.{}); 918 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; 919 920 const Runnable = struct { 921 io: std.Io, 922 allocator: std.mem.Allocator, 923 session: *ui.Session, 924 repo: *Repo, 925 926 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port: ?u16) !void { 927 // launch the TUI; the footer points at whatever port was bound 928 self.session.web_port = web_port; 929 self.session.data.git_http_port = http_port; 930 self.session.data.git_ssh_port = ssh_port; 931 try hx.ui.run(self.io, self.allocator, self.session, self.repo); 932 } 933 }; 934 935 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .allocator = allocator, .session = &session, .repo = &repo }); 936 } 937 } 938 939 // words cycled through to pad generated lines into prose-like content 940 const scatter_words = [_][]const u8{ 941 "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", 942 "quantum", "vector", "matrix", "buffer", "kernel", "socket", 943 "falcon", "otter", "badger", "walrus", "ferret", "marmot", 944 "scatter", "gather", "encode", "decode", "render", "commit", 945 }; 946 947 fn longDescription(allocator: std.mem.Allocator, initial: []const u8) ![]u8 { 948 var writer = std.Io.Writer.Allocating.init(allocator); 949 defer writer.deinit(); 950 try writer.writer.print("{s}", .{initial}); 951 for (0..ui.detail_preview_lines) |line| 952 try writer.writer.print("\n{d} {s}", .{ line, scatter_words[line % scatter_words.len] }); 953 return writer.toOwnedSlice(); 954 } 955 956 // write `path` as 40 lines of mostly-stable filler. revision `c` and file 957 // index `fi` shift which lines change and the words used, so each (c, fi) 958 // yields a distinct multi-hunk diff against the previous revision. untouched 959 // lines stay byte-identical across revisions, so a bump in `c` is a scatter of 960 // small hunks rather than a full rewrite. 961 fn writeScatterFile(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir, path: []const u8, fi: usize, c: usize) !void { 962 const content = try scatterContent(allocator, fi, c, null); 963 defer allocator.free(content); 964 const file = try repo_dir.createFile(io, path, .{}); 965 defer file.close(io); 966 try file.writeStreamingAll(io, content); 967 } 968 969 const ScatterEdit = struct { line: usize, text: []const u8 }; 970 971 fn scatterContent(allocator: std.mem.Allocator, fi: usize, c: usize, replacement: ?ScatterEdit) ![]u8 { 972 var writer = std.Io.Writer.Allocating.init(allocator); 973 defer writer.deinit(); 974 for (0..40) |line| { 975 if (replacement) |edit| if (line == edit.line) { 976 try writer.writer.print("{s}\n", .{edit.text}); 977 continue; 978 }; 979 const start = writer.written().len; 980 if ((line + fi + c) % 8 == 0) { 981 const tag = scatter_words[(line + fi + c * 3) % scatter_words.len]; 982 try writer.writer.print("rev {d} {s}", .{ c, tag }); 983 } 984 const target = 40 + (line * 17 + fi * 23) % 70; 985 var w = line + fi; 986 while (true) : (w += 1) { 987 const word = scatter_words[w % scatter_words.len]; 988 const len = writer.written().len - start; 989 if (len >= target or len + 1 + word.len > 120) break; 990 if (len == 0) 991 try writer.writer.print("{s}", .{word}) 992 else 993 try writer.writer.print(" {s}", .{word}); 994 } 995 try writer.writer.writeByte('\n'); 996 } 997 return writer.toOwnedSlice(); 998 } 999 1000 // tag the current HEAD as the next sequential version: v1, v2, and so on 1001 fn addNextTag(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void { 1002 var buf: [16]u8 = undefined; 1003 const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); 1004 _ = try repo.addTag(io, allocator, .{ .name = name }); 1005 n.* += 1; 1006 } 1007 1008 fn commitTree( 1009 io: std.Io, 1010 allocator: std.mem.Allocator, 1011 repo: *rp.Repo(.xit, .{}), 1012 oid: *const [hash.hexLen(.sha1)]u8, 1013 ) ![hash.hexLen(.sha1)]u8 { 1014 var moment = try repo.core.latestMoment(); 1015 const state = rp.Repo(.xit, .{}).State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; 1016 var object = try obj.Object(.xit, .{}).init(state, io, allocator, oid); 1017 defer object.deinit(); 1018 return switch (object.content) { 1019 .commit => |commit| commit.tree, 1020 else => error.InvalidObject, 1021 }; 1022 } 1023 1024 fn seedPatchRevision( 1025 io: std.Io, 1026 allocator: std.mem.Allocator, 1027 repos_path: []const u8, 1028 patch_id: *const [evt.event_id_size]u8, 1029 title: []const u8, 1030 author: evt.CommitAuthor, 1031 timestamp: u64, 1032 random: std.Random, 1033 base_oid: [hash.hexLen(.sha1)]u8, 1034 path: []const u8, 1035 contents: []const []const u8, 1036 ) !void { 1037 const patch_hex = std.fmt.bytesToHex(patch_id.*, .lower); 1038 const fork_path = try fork.forkPath(allocator, repos_path, &patch_hex); 1039 defer allocator.free(fork_path); 1040 var fork_repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }); 1041 defer fork_repo.deinit(io, allocator); 1042 1043 try fork_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "false" }); 1044 { 1045 var reset = try fork_repo.reset(io, allocator, .{ .target = .{ .oid = &base_oid } }); 1046 defer reset.deinit(); 1047 } 1048 const base_tree_oid = try commitTree(io, allocator, &fork_repo, &base_oid); 1049 var source_oid = base_oid; 1050 var fork_dir = try std.Io.Dir.cwd().openDir(io, fork_path, .{}); 1051 defer fork_dir.close(io); 1052 var writer = std.Io.Writer.Allocating.init(allocator); 1053 defer writer.deinit(); 1054 if (std.fs.path.dirname(path)) |dir| try fork_dir.createDirPath(io, dir); 1055 for (contents, 0..) |content, i| { 1056 { 1057 const file = try fork_dir.createFile(io, path, .{}); 1058 defer file.close(io); 1059 try file.writeStreamingAll(io, content); 1060 } 1061 try fork_repo.add(io, allocator, &.{path}); 1062 writer.clearRetainingCapacity(); 1063 try writer.writer.print("{s} ({d}/{d})", .{ title, i + 1, contents.len }); 1064 source_oid = try fork_repo.commit(io, allocator, .{ .message = writer.written(), .timestamp = timestamp + i }); 1065 } 1066 try fork_repo.patchAll(io, allocator, null); 1067 try fork_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "true" }); 1068 // the commits above skipped the push path, so index the patch branch by hand 1069 try find.refresh(.{}, io, allocator, &fork_repo); 1070 _ = try fork_repo.garbageCollect(io, allocator, .{}); 1071 try fork_dir.deleteFile(io, path); 1072 const revision_id = evt.EventWithId.randomId(random); 1073 const head_tree_oid = try commitTree(io, allocator, &fork_repo, &source_oid); 1074 const revision: evt.PatchRev = .{ 1075 .base_oid = &base_oid, 1076 .source_oid = &source_oid, 1077 .message = title, 1078 }; 1079 const revision_timestamp = timestamp + 3; 1080 const tree_entries = [_]evt.EventTreeEntry{ 1081 .{ .tree = .{ .name = "base", .oid = &base_tree_oid } }, 1082 .{ .tree = .{ .name = "head", .oid = &head_tree_oid } }, 1083 }; 1084 try evt.consume(.server, .fork, .xit, .{}, io, allocator, &fork_repo, evt.events_ref, &.{.{ 1085 .id = std.fmt.bytesToHex(revision_id, .lower), 1086 .timestamp = revision_timestamp, 1087 .author = author, 1088 .tree_entries = &tree_entries, 1089 .event = .{ .patchrev = revision }, 1090 }}); 1091 1092 var arena = std.heap.ArenaAllocator.init(allocator); 1093 defer arena.deinit(); 1094 const moment = try evt.currentMoment(.{}, &fork_repo); 1095 const revision_record = (try evt.PatchRev.readById(evt.EventDB(.sha1), .sha1, moment, &arena, &revision_id)) orelse return error.NotFound; 1096 const patch_record = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &arena, patch_id)) orelse return error.NotFound; 1097 var patch = patch_record.event; 1098 patch.revision = evt.Patch.Revision.fromRecord(revision_id, revision_record); 1099 try evt.consume(.server, .fork, .xit, .{}, io, allocator, &fork_repo, evt.events_ref, &.{.{ 1100 .id = patch_hex, 1101 .timestamp = revision_timestamp + 1, 1102 .author = author, 1103 .event = .{ .patch = patch }, 1104 }}); 1105 } 1106 1107 fn seedPatches( 1108 io: std.Io, 1109 allocator: std.mem.Allocator, 1110 server_path: []const u8, 1111 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), 1112 target_repo: *rp.Repo(.xit, .{}), 1113 repo_id: *const [evt.event_id_size]u8, 1114 user_id: *const [evt.event_id_size]u8, 1115 random: std.Random, 1116 ) !void { 1117 const patch_data = [_]struct { 1118 title: []const u8, 1119 description: []const u8, 1120 tags: []const u8, 1121 status: ?evt.Patch.StatusKind, 1122 alpha_edit: ?ScatterEdit = null, 1123 }{ 1124 .{ 1125 .title = "Draft a faster dependency scanner", 1126 .description = "Rework dependency discovery so a large workspace can be scanned without repeatedly opening the same manifests.", 1127 .tags = "performance build", 1128 .status = null, 1129 }, 1130 .{ 1131 .title = "Remove the legacy configuration loader", 1132 .description = "Delete the compatibility loader now that the replacement format has shipped and the migration warning has been available for a full release.", 1133 .tags = "cleanup config", 1134 .status = .merged, 1135 }, 1136 .{ 1137 .title = "Cache parsed manifests between commands", 1138 .description = "Keep parsed manifests in the command context so consecutive operations do not repeat identical filesystem and parsing work.", 1139 .tags = "performance cache", 1140 .status = .closed, 1141 }, 1142 .{ 1143 .title = "Add structured output to the inspect command", 1144 .description = "Add a stable JSON representation of inspect results for scripts and editor integrations.", 1145 .tags = "enhancement cli", 1146 .status = .open, 1147 }, 1148 .{ 1149 .title = "Edit an adjacent line in alpha.txt", 1150 .description = "This patch can be cleanly merged by haxy, while git throws a merge conflict!", 1151 .tags = "merge", 1152 .status = .open, 1153 .alpha_edit = .{ .line = 2, .text = "adjust alpha beside the latest scatter edit" }, 1154 }, 1155 .{ 1156 .title = "Edit a conflicting line in alpha.txt", 1157 .description = "Both source and squash merges conflict with a different edit to the same line on master.", 1158 .tags = "merge", 1159 .status = .open, 1160 .alpha_edit = .{ .line = 3, .text = "replace alpha with a conflicting edit" }, 1161 }, 1162 }; 1163 1164 const repos_path = try std.fs.path.join(allocator, &.{ server_path, "repos" }); 1165 defer allocator.free(repos_path); 1166 const patch_author = evt.CommitAuthor{ .name = "admin", .email = "admin@example.test" }; 1167 var patch_ids: [patch_data.len][evt.event_id_size]u8 = undefined; 1168 for (patch_data, 0..) |patch, i| { 1169 const timestamp: u64 = @intCast(500 + i * 10); 1170 patch_ids[i] = evt.EventWithId.randomId(random); 1171 const patch_hex = std.fmt.bytesToHex(patch_ids[i], .lower); 1172 const path = try fork.create(.{}, io, allocator, repos_path, admin_repo, .{ 1173 .id = patch_hex, 1174 .user_id = user_id.*, 1175 .repo_id = repo_id.*, 1176 .title = patch.title, 1177 .description = patch.description, 1178 .tags = patch.tags, 1179 .target_branch = "master", 1180 .author = patch_author, 1181 .timestamp = timestamp, 1182 }); 1183 allocator.free(path); 1184 1185 // prepare the patch's base and file contents, then seed its commits and revision 1186 { 1187 var arena = std.heap.ArenaAllocator.init(allocator); 1188 defer arena.deinit(); 1189 const aa = arena.allocator(); 1190 var base_oid = (try target_repo.readRef(io, .{ .kind = .head, .name = "master" })) orelse return error.NotFound; 1191 var contents: [3][]const u8 = undefined; 1192 const file_path: []const u8, const count: usize = if (patch.alpha_edit) |edit| blk: { 1193 // v30 precedes the last scatter commit, even after the sample merge 1194 const tag_oid = (try target_repo.readRef(io, .{ .kind = .tag, .name = "v30" })) orelse return error.NotFound; 1195 var moment = try target_repo.core.latestMoment(); 1196 const state = rp.Repo(.xit, .{}).State(.read_only){ .core = &target_repo.core, .extra = .{ .moment = &moment } }; 1197 var tag = try obj.Object(.xit, .{}).init(state, io, allocator, &tag_oid); 1198 defer tag.deinit(); 1199 base_oid = switch (tag.content) { 1200 .tag => |value| value.target, 1201 else => return error.InvalidObject, 1202 }; 1203 // rev 29 first changes line 4; edit that line or the one above it 1204 contents[0] = try scatterContent(aa, 0, 28, edit); 1205 break :blk .{ "src/alpha.txt", 1 }; 1206 } else blk: { 1207 var writer = std.Io.Writer.Allocating.init(allocator); 1208 defer writer.deinit(); 1209 for (&contents, 0..) |*content, part| { 1210 try writer.writer.print("{s}: part {d}\n", .{ patch.title, part + 1 }); 1211 content.* = try aa.dupe(u8, writer.written()); 1212 } 1213 break :blk .{ "patch.txt", contents.len }; 1214 }; 1215 try seedPatchRevision(io, allocator, repos_path, &patch_ids[i], patch.title, patch_author, timestamp + 1, random, base_oid, file_path, contents[0..count]); 1216 } 1217 const status = patch.status orelse continue; 1218 const fork_path = try fork.forkPath(allocator, repos_path, &patch_hex); 1219 defer allocator.free(fork_path); 1220 try pch.publish(.{}, io, allocator, admin_repo, target_repo, fork_path, .{ 1221 .id = patch_hex, 1222 .user_id = user_id.*, 1223 .repo_id = repo_id.*, 1224 .author = patch_author, 1225 .timestamp = timestamp + 6, 1226 }); 1227 1228 if (status == .merged) { 1229 try pch.mergeAndRemoveFork(.{}, io, allocator, repos_path, admin_repo, target_repo, .{ 1230 .id = patch_hex, 1231 .revision = .source, 1232 .author = patch_author, 1233 .timestamp = timestamp + 7, 1234 }); 1235 } else if (status != .open) { 1236 try evt.Patch.update(.server, .xit, .{}, io, allocator, target_repo, &patch_ids[i], .{ .status = status }, patch_author); 1237 } 1238 } 1239 1240 // seed a small comment tree on the newest patch 1241 var comment_ids: [5][evt.event_id_size]u8 = undefined; 1242 for (&comment_ids) |*id| id.* = evt.EventWithId.randomId(random); 1243 const comment_bodies = [_][]const u8{ 1244 "This also needs to preserve the executable bit for files nested inside generated directories.", 1245 "I tested the archive path and the direct-copy path; both currently lose the mode.", 1246 "The archive writer is the right place to centralize it, since every exporter already goes through that layer.", 1247 "Please include a regression fixture with a mix of executable and ordinary files.", 1248 "I'll add that fixture and cover both supported archive formats.", 1249 }; 1250 var comments: [comment_ids.len]evt.EventWithId = undefined; 1251 for (&comments, 0..) |*comment, i| { 1252 comment.* = .{ 1253 .id = std.fmt.bytesToHex(comment_ids[i], .lower), 1254 .timestamp = @intCast(700 + i), 1255 .author = .{ .name = if (i % 2 == 0) "alice" else "bob", .email = if (i % 2 == 0) "alice@example.test" else "bob@example.test" }, 1256 .event = .{ .comment = .{ 1257 .thread_id = std.fmt.bytesToHex(patch_ids[patch_ids.len - 1], .lower), 1258 .parent_id = switch (i) { 1259 0, 1 => std.fmt.bytesToHex(patch_ids[patch_ids.len - 1], .lower), 1260 2, 4 => std.fmt.bytesToHex(comment_ids[0], .lower), 1261 3 => std.fmt.bytesToHex(comment_ids[2], .lower), 1262 else => unreachable, 1263 }, 1264 .body = comment_bodies[i], 1265 } }, 1266 }; 1267 } 1268 try evt.consume(.server, .repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &comments); 1269 1270 // create divergent metadata edits on the next three patches 1271 var patch_arena = std.heap.ArenaAllocator.init(allocator); 1272 defer patch_arena.deinit(); 1273 const moment = try evt.currentMoment(.{}, target_repo); 1274 var values: [3]evt.Patch = undefined; 1275 for (1..4) |i| { 1276 const record = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &patch_arena, &patch_ids[i])) orelse return error.NotFound; 1277 values[i - 1] = record.event; 1278 } 1279 var ours_values = values; 1280 ours_values[0].title = "Drop the legacy configuration loader"; 1281 ours_values[0].tags = "cleanup config breaking"; 1282 const closed_description = try longDescription( 1283 patch_arena.allocator(), 1284 "Cache parsed manifests for the lifetime of a command invocation and invalidate entries when their files change.", 1285 ); 1286 ours_values[1].description = closed_description; 1287 ours_values[2].description = "Add JSON output to inspect with versioned field names and deterministic object ordering."; 1288 var theirs_values = values; 1289 theirs_values[0].title = "Delete compatibility configuration support"; 1290 theirs_values[0].tags = "config maintenance"; 1291 theirs_values[1].description = "Keep a process-wide manifest cache shared by every command and refresh it after writes."; 1292 theirs_values[2].description = "Expose inspect results as newline-delimited JSON so callers can stream large repositories."; 1293 1294 const seed_tip = (try target_repo.readRef(io, evt.events_ref)) orelse return error.NotFound; 1295 const other_ref = xit.ref.Ref{ .kind = .head, .name = "haxy/patch-other" }; 1296 var ours = [_]evt.EventWithId{ 1297 .{ 1298 .id = std.fmt.bytesToHex(patch_ids[1], .lower), 1299 .timestamp = 800, 1300 .author = .{ .name = "alice", .email = "alice@example.test" }, 1301 .event = .{ .patch = ours_values[0] }, 1302 }, 1303 .{ 1304 .id = std.fmt.bytesToHex(patch_ids[2], .lower), 1305 .timestamp = 801, 1306 .author = .{ .name = "alice", .email = "alice@example.test" }, 1307 .event = .{ .patch = ours_values[1] }, 1308 }, 1309 .{ 1310 .id = std.fmt.bytesToHex(patch_ids[3], .lower), 1311 .timestamp = 802, 1312 .author = .{ .name = "alice", .email = "alice@example.test" }, 1313 .event = .{ .patch = ours_values[2] }, 1314 }, 1315 }; 1316 1317 var theirs = ours; 1318 theirs[0].timestamp = 810; 1319 theirs[0].author = .{ .name = "bob", .email = "bob@example.test" }; 1320 theirs[0].event.patch = theirs_values[0]; 1321 theirs[1].timestamp = 811; 1322 theirs[1].author = .{ .name = "bob", .email = "bob@example.test" }; 1323 theirs[1].event.patch = theirs_values[1]; 1324 theirs[2].timestamp = 812; 1325 theirs[2].author = .{ .name = "bob", .email = "bob@example.test" }; 1326 theirs[2].event.patch = theirs_values[2]; 1327 1328 try evt.consume(.server, .repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &ours); 1329 try commitEventsAtRef(io, allocator, target_repo, other_ref, &theirs, seed_tip); 1330 { 1331 var merge = try target_repo.mergeAtRef(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, evt.events_ref, null); 1332 defer merge.deinit(); 1333 if (merge.result != .success) return error.MergeFailed; 1334 } 1335 try target_repo.removeBranch(io, .{ .name = other_ref.name }); 1336 try evt.consume(.server, .repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &.{}); 1337 try pch.writeBranchPatch(.server, .xit, .{}, io, allocator, target_repo, std.fmt.bytesToHex(evt.EventWithId.randomId(random), .lower), .{ 1338 .title = "Merge the existing feature branch", 1339 .description = "This patch tracks the feature branch in this repo without creating a fork.", 1340 .tags = "feature", 1341 .source_branch = "feature", 1342 .target_branch = "master", 1343 }, null, patch_author); 1344 1345 // a push onto the branch that patch tracks, so the undo tab shows the 1346 // action and the patch refresh it sets off 1347 try seedPush(io, allocator, target_repo, patch_author); 1348 try pch.refreshBranches(.server, .xit, .{}, io, allocator, target_repo, null, null); 1349 } 1350 1351 // stand in for a client push, which the fixture has no server to receive: one 1352 // transaction moves the branch, refreshes the patches tracking it and records 1353 // the action, the way receive-pack does 1354 fn seedPush(io: std.Io, allocator: std.mem.Allocator, repo: *rp.Repo(.xit, .{}), author: evt.CommitAuthor) !void { 1355 const Repo = rp.Repo(.xit, .{}); 1356 const DB = Repo.DB; 1357 const Ctx = struct { 1358 core: *Repo.Core, 1359 io: std.Io, 1360 allocator: std.mem.Allocator, 1361 author: evt.CommitAuthor, 1362 1363 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 1364 const opts: rp.RepoOpts(.xit) = .{}; 1365 var moment = try DB.HashMap(.read_write).init(cursor.*); 1366 const state = Repo.State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 1367 var arena = std.heap.ArenaAllocator.init(ctx.allocator); 1368 defer arena.deinit(); 1369 const aa = arena.allocator(); 1370 1371 // a null tree keeps the parent's content, so the branch advances 1372 // without the fixture having to build one 1373 const author_line = try std.fmt.allocPrint(aa, "{s} <{s}>", .{ ctx.author.name, ctx.author.email }); 1374 const feature_old = (try rf.readRecur(.xit, opts, state.readOnly(), ctx.io, .{ .ref = .{ .kind = .head, .name = "feature" } })) orelse return error.NotFound; 1375 const feature_new = try obj.writeCommit(.xit, opts, state, ctx.io, ctx.allocator, .{ 1376 .message = "Revise the feature branch", 1377 .author = author_line, 1378 .timestamp = 820, 1379 }, null, .{ .kind = .head, .name = "feature" }); 1380 1381 // a branch the push creates has no old tip, so its range is open 1382 try bch.add(.xit, opts, state, ctx.io, .{ .name = "docs" }); 1383 const docs_new = (try rf.readRecur(.xit, opts, state.readOnly(), ctx.io, .{ .ref = .{ .kind = .head, .name = "docs" } })) orelse return error.NotFound; 1384 1385 const updates = [_]xit.net_server_receive_pack.AppliedRefUpdate{ 1386 .{ 1387 .ref_name = try aa.dupe(u8, "refs/heads/feature"), 1388 .old_oid = try aa.dupe(u8, &feature_old), 1389 .new_oid = try aa.dupe(u8, &feature_new), 1390 }, 1391 .{ 1392 .ref_name = try aa.dupe(u8, "refs/heads/docs"), 1393 .old_oid = try aa.dupe(u8, "0" ** hash.hexLen(opts.hash)), 1394 .new_oid = try aa.dupe(u8, &docs_new), 1395 }, 1396 }; 1397 1398 _ = try pch.refreshBranchesInTransaction(.server, opts, state, &moment, ctx.io, ctx.allocator, &updates, null, null); 1399 try push.writeUndo(opts, state, ctx.io, ctx.allocator, ctx.author, &updates); 1400 } 1401 }; 1402 1403 try transaction(io, repo, Ctx{ .core = &repo.core, .io = io, .allocator = allocator, .author = author }); 1404 } 1405 1406 // commit `events` onto `ref`, rooted at `parent`, as one transaction. 1407 // consume always roots a new branch at nothing, so the divergent fixtures 1408 // write their side branch here instead. nothing is consumed, so it records 1409 // xit's own commit action rather than one of haxy's event actions 1410 fn commitEventsAtRef( 1411 io: std.Io, 1412 allocator: std.mem.Allocator, 1413 repo: *rp.Repo(.xit, .{}), 1414 ref: xit.ref.Ref, 1415 events: []const evt.EventWithId, 1416 parent: [hash.hexLen(.sha1)]u8, 1417 ) !void { 1418 const Repo = rp.Repo(.xit, .{}); 1419 const DB = Repo.DB; 1420 const Ctx = struct { 1421 core: *Repo.Core, 1422 io: std.Io, 1423 allocator: std.mem.Allocator, 1424 ref: xit.ref.Ref, 1425 events: []const evt.EventWithId, 1426 parent: [hash.hexLen(.sha1)]u8, 1427 1428 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 1429 const opts: rp.RepoOpts(.xit) = .{}; 1430 var moment = try DB.HashMap(.read_write).init(cursor.*); 1431 const state = Repo.State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 1432 try evt.commitEvents(.xit, opts, state, ctx.io, ctx.allocator, ctx.ref, ctx.events, .{ctx.parent}); 1433 const tip = (try xit.ref.readRecur(.xit, opts, state.readOnly(), ctx.io, .{ .ref = ctx.ref })) orelse return error.NotFound; 1434 try xit.undo.write(opts, state, std.Io.Timestamp.now(ctx.io, .real).toSeconds(), .{ 1435 .commit = try .init("seed conflicting events", tip, opts.max_read_size), 1436 }); 1437 } 1438 }; 1439 1440 try transaction(io, repo, Ctx{ 1441 .core = &repo.core, 1442 .io = io, 1443 .allocator = allocator, 1444 .ref = ref, 1445 .events = events, 1446 .parent = parent, 1447 }); 1448 } 1449 1450 // append `ctx` to the repo's history as one transaction. the fixtures write a 1451 // few states the normal paths have no way to produce 1452 fn transaction(io: std.Io, repo: *rp.Repo(.xit, .{}), ctx: anytype) !void { 1453 const DB = rp.Repo(.xit, .{}).DB; 1454 try repo.core.db_file.lock(io, .exclusive); 1455 defer repo.core.db_file.unlock(io); 1456 1457 const history = try DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); 1458 try history.appendContext(.{ .slot = try history.getSlot(-1) }, ctx); 1459 } 1460 1461 // recursively copy the contents of src_dir into dest_dir 1462 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 1463 var iter = src_dir.iterate(); 1464 while (try iter.next(io)) |entry| { 1465 switch (entry.kind) { 1466 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 1467 .directory => { 1468 try dest_dir.createDirPath(io, entry.name); 1469 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1470 defer dest_entry_dir.close(io); 1471 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1472 defer src_entry_dir.close(io); 1473 try copyDir(io, src_entry_dir, dest_entry_dir); 1474 }, 1475 else => {}, 1476 } 1477 } 1478 } 1479