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