╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ try.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
623 .title = "Crash when resizing the window during the splash animation", 624 .description = title_issue.description, 625 .tags = "bug crash ui priority-high", 626 } }, 627 }, .{ 628 .id = issue_events[issue_data.len - 3].id, 629 .timestamp = 101, 630 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 631 .event = .{ .issue = .{ 632 .title = sync_issue.title, 633 .description = "The sync engine's coverage is unit tests only, with every transport mocked out." ++ "\n\n" ++ 634 sync_p2 ++ "\n\n" ++ sync_p3, 635 .tags = sync_issue.tags, 636 } }, 637 }, .{ 638 .id = issue_events[issue_data.len - 2].id, 639 .timestamp = 102, 640 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 641 .event = .{ .issue = .{ 642 .title = desc_issue.title, 643 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ tz_p4 ++ "\n\n" ++ 644 "The confusion is worst for teams spread across timezones, who each read a different wall-clock time from the same feed.", 645 .tags = desc_issue.tags, 646 } }, 647 } }; 648 const theirs = [_]evt.EventWithId{ .{ 649 .id = issue_events[issue_data.len - 4].id, 650 .timestamp = 103, 651 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 652 .event = .{ .issue = .{ 653 .title = "Segfault on early window resize", 654 .description = title_issue.description, 655 .tags = "bug crash rendering", 656 } }, 657 }, .{ 658 .id = issue_events[issue_data.len - 3].id, 659 .timestamp = 104, 660 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 661 .event = .{ .issue = .{ 662 .title = sync_issue.title, 663 .description = sync_p1 ++ "\n\n" ++ sync_p2 ++ "\n\n" ++ 664 "Add end-to-end tests that drive two live instances against a local server on every CI run.", 665 .tags = sync_issue.tags, 666 } }, 667 }, .{ 668 .id = issue_events[issue_data.len - 2].id, 669 .timestamp = 105, 670 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 671 .event = .{ .issue = .{ 672 .title = desc_issue.title, 673 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ 674 "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" ++ 675 "Log exports inherit the same UTC rendering, so downstream tooling has to guess the source timezone.", 676 .tags = desc_issue.tags, 677 } }, 678 } }; 679 680 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &ours); 681 682 // consume can't root a new branch, so theirs commits by hand 683 var json: std.Io.Writer.Allocating = .init(allocator); 684 defer json.deinit(); 685 for (theirs, 0..) |event, i| { 686 json.clearRetainingCapacity(); 687 try std.json.Stringify.value(event, .{}, &json.writer); 688 const author = try std.fmt.allocPrint(allocator, "{s} <{s}>", .{ event.author.name, event.author.email }); 689 defer allocator.free(author); 690 _ = try template_repo.commitAtRef(io, allocator, .{ 691 .author = author, 692 .message = json.written(), 693 .timestamp = event.timestamp, 694 // root the branch at the shared seed tip 695 .parent_oids = if (i == 0) &.{seed_tip} else null, 696 }, null, other_ref); 697 } 698 699 { 700 var to_events = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 701 defer to_events.deinit(); 702 } 703 { 704 var merge = try template_repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, null); 705 defer merge.deinit(); 706 if (merge.result != .success) return error.MergeFailed; 707 } 708 { 709 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 710 defer to_master.deinit(); 711 } 712 try template_repo.removeBranch(io, .{ .name = other_ref.name }); 713 } 714 715 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &.{}); 716 717 // seed a small comment tree on the newest issue 718 var comment_ids: [5][evt.event_id_size]u8 = undefined; 719 for (&comment_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 720 721 const comment_bodies = [_][]const u8{ 722 "I can take this. The compatibility scanner already reports which repositories still use the legacy format.", 723 "Can we keep loading legacy plugins for one release after the warning lands? That would give downstream maintainers time to migrate.", 724 "That sounds good. I'll include the scanner output in the warning so each affected plugin is named.", 725 "Please include the replacement manifest path too. That should make the warning actionable without opening the migration guide.", 726 "I'll add release-note text once the warning format is settled.", 727 }; 728 729 var comment_events: [comment_ids.len]evt.EventWithId = undefined; 730 for (&comment_events, 0..) |*event, i| { 731 event.* = .{ 732 .id = std.fmt.bytesToHex(comment_ids[i], .lower), 733 .timestamp = @intCast(200 + i), 734 .author = .{ .name = user_data[(i + 1) % user_data.len].name, .email = user_data[(i + 1) % user_data.len].email }, 735 .event = .{ .comment = .{ 736 .thread_id = issue_events[issue_events.len - 1].id, 737 .parent_id = switch (i) { 738 0, 1 => issue_events[issue_events.len - 1].id, 739 2, 4 => std.fmt.bytesToHex(comment_ids[0], .lower), 740 3 => std.fmt.bytesToHex(comment_ids[2], .lower), 741 else => unreachable, 742 }, 743 .body = comment_bodies[i], 744 } }, 745 }; 746 } 747 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &comment_events); 748 749 const discussion_data = [_]struct { 750 title: []const u8, 751 description: []const u8, 752 tags: []const u8, 753 }{ 754 .{ 755 .title = "How should plugins declare capabilities?", 756 .description = "I'd like the manifest to make privileged capabilities explicit without making simple plugins verbose.", 757 .tags = "plugins design", 758 }, 759 .{ 760 .title = "Ideas for making large repositories faster", 761 .description = "This is a place to collect profiling results and discuss which indexing work is worth pursuing first.", 762 .tags = "performance indexing", 763 }, 764 .{ 765 .title = "What should the next release focus on?", 766 .description = "Let's compare the most important reliability fixes with the larger features already in progress.", 767 .tags = "release planning", 768 }, 769 .{ 770 .title = "Improving keyboard navigation", 771 .description = "Share workflows that still require a mouse and suggestions for making their focus behavior predictable.", 772 .tags = "ui keyboard accessibility", 773 }, 774 .{ 775 .title = "Configuration format discussion", 776 .description = "Should the next configuration format favor strict validation or accept common conveniences such as trailing commas?", 777 .tags = "config design", 778 }, 779 }; 780 781 var discussion_events: [discussion_data.len]evt.EventWithId = undefined; 782 for (discussion_data, 0..) |discussion, i| { 783 discussion_events[i] = .{ 784 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 785 .timestamp = @intCast(300 + i), 786 .author = .{ .name = user_data[i % user_data.len].name, .email = user_data[i % user_data.len].email }, 787 .event = .{ .discuss = .{ 788 .title = discussion.title, 789 .description = discussion.description, 790 .tags = discussion.tags, 791 } }, 792 }; 793 } 794 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &discussion_events); 795 796 var discussion_comment_ids: [discussion_data.len][3][evt.event_id_size]u8 = undefined; 797 for (&discussion_comment_ids) |*ids| { 798 for (ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 799 } 800 var discussion_comment_events: [discussion_data.len * 3]evt.EventWithId = undefined; 801 for (&discussion_comment_events, 0..) |*event, i| { 802 const discussion_index = i / 3; 803 const comment_index = i % 3; 804 event.* = .{ 805 .id = std.fmt.bytesToHex(discussion_comment_ids[discussion_index][comment_index], .lower), 806 .timestamp = @intCast(400 + i), 807 .author = .{ .name = user_data[(i + 1) % user_data.len].name, .email = user_data[(i + 1) % user_data.len].email }, 808 .event = .{ .comment = .{ 809 .thread_id = discussion_events[discussion_index].id, 810 .parent_id = if (comment_index < 2) 811 discussion_events[discussion_index].id 812 else 813 std.fmt.bytesToHex(discussion_comment_ids[discussion_index][0], .lower), 814 .body = switch (comment_index) { 815 0 => "My preference is to start with the smallest useful version and expand it after we have real usage to learn from.", 816 1 => "It would help to write down the tradeoffs we are accepting so the decision is easy to revisit later.", 817 2 => "Agreed. A narrow first version should also make it easier to keep the implementation understandable.", 818 else => unreachable, 819 }, 820 } }, 821 }; 822 } 823 try evt.consume(.repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &discussion_comment_events); 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, .{ .iterate = true }); 831 defer template_dir.close(io); 832 833 for (repo_event_ids, 0..) |id_bytes, repo_index| { 834 const repo_id = std.fmt.bytesToHex(id_bytes, .lower); 835 const repo_path = try std.fs.path.join(arena.allocator(), &.{ server_path, "repos", &repo_id }); 836 { 837 var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); 838 defer dest_dir.close(io); 839 try copyDir(io, template_dir, dest_dir); 840 } 841 842 if (repo_index + 1 == repo_data.len) { 843 var target_repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = repo_path, .require_repo_root = true }); 844 defer target_repo.deinit(io, allocator); 845 try seedPatches(io, allocator, server_path, &repo, &target_repo, &repo_event_ids[repo_index], &admin_user_id, prng.random()); 846 } 847 } 848 } 849 850 break :blk try ui.Session.init(&session_arena, &repo, .{ .user_id = &admin_user_id }); 851 }; 852 session.is_terminal = true; 853 session.data.git_ssh_prefix = git_ssh_prefix; 854 855 // start the server 856 857 // let the native TUI's page builders open the on-disk repos for the file tree 858 session.io = io; 859 session.repos_dir = try std.fs.path.join(session_arena.allocator(), &.{ server_path, "repos" }); 860 861 // leave a one-shot session for the first browser to hit the web ui, so it 862 // starts logged in as admin 863 { 864 var data_dir = try cwd.createDirPathOpen(io, server_path, .{}); 865 defer data_dir.close(io); 866 const store = try hx.web.SessionStore.init(io, data_dir); 867 defer store.deinit(); 868 const token = try store.create(&admin_user_id); 869 try store.offerAutoLogin(&token); 870 } 871 872 if (cli) { 873 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 874 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 875 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; 876 877 const Runnable = struct { 878 io: std.Io, 879 key_path: []const u8, 880 881 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port_maybe: ?u16) !void { 882 _ = web_port; 883 _ = http_port; 884 const ssh_port = ssh_port_maybe orelse return error.SshDisabled; 885 std.debug.print( 886 \\ 887 \\connect to the TUI with: 888 \\ ssh -p {d} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost 889 \\ 890 \\create a git repo and push over SSH: 891 \\ mkdir -p temp-try/client/test 892 \\ cd temp-try/client/test 893 \\ git init 894 \\ echo "hello" > hello.txt 895 \\ git add hello.txt 896 \\ git commit -m "let there be light" 897 \\ 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 898 \\ 899 \\to quit, press enter. 900 \\ 901 , .{ ssh_port, ssh_port, self.key_path }); 902 // portable stdin read via std.Io — equivalent to the 903 // std.posix.read(STDIN_FILENO, ...) that doesn't compile 904 // on windows. 905 var buf: [1]u8 = undefined; 906 var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); 907 _ = stdin_reader.interface.takeByte() catch {}; 908 } 909 }; 910 911 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .key_path = key_path }); 912 } else { 913 var null_writer = std.Io.Writer.Discarding.init(&.{}); 914 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; 915 916 const Runnable = struct { 917 io: std.Io, 918 allocator: std.mem.Allocator, 919 session: *ui.Session, 920 repo: *Repo, 921 922 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port: ?u16) !void { 923 // launch the TUI; the footer points at whatever port was bound 924 self.session.web_port = web_port; 925 self.session.data.git_http_port = http_port; 926 self.session.data.git_ssh_port = ssh_port; 927 try hx.ui.run(self.io, self.allocator, self.session, self.repo); 928 } 929 }; 930 931 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .allocator = allocator, .session = &session, .repo = &repo }); 932 } 933 } 934 935 // words cycled through to pad generated lines into prose-like content 936 const scatter_words = [_][]const u8{ 937 "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", 938 "quantum", "vector", "matrix", "buffer", "kernel", "socket", 939 "falcon", "otter", "badger", "walrus", "ferret", "marmot", 940 "scatter", "gather", "encode", "decode", "render", "commit", 941 }; 942 943 // write `path` as 40 lines of mostly-stable filler. revision `c` and file 944 // index `fi` shift which lines change and the words used, so each (c, fi) 945 // yields a distinct multi-hunk diff against the previous revision. untouched 946 // lines stay byte-identical across revisions, so a bump in `c` is a scatter of 947 // small hunks rather than a full rewrite. 948 fn writeScatterFile(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir, path: []const u8, fi: usize, c: usize) !void { 949 const file = try repo_dir.createFile(io, path, .{}); 950 defer file.close(io); 951 var writer = std.Io.Writer.Allocating.init(allocator); 952 defer writer.deinit(); 953 for (0..40) |line| { 954 const start = writer.written().len; 955 if ((line + fi + c) % 8 == 0) { 956 const tag = scatter_words[(line + fi + c * 3) % scatter_words.len]; 957 try writer.writer.print("rev {d} {s}", .{ c, tag }); 958 } 959 const target = 40 + (line * 17 + fi * 23) % 70; 960 var w = line + fi; 961 while (true) : (w += 1) { 962 const word = scatter_words[w % scatter_words.len]; 963 const len = writer.written().len - start; 964 if (len >= target or len + 1 + word.len > 120) break; 965 if (len == 0) 966 try writer.writer.print("{s}", .{word}) 967 else 968 try writer.writer.print(" {s}", .{word}); 969 } 970 try writer.writer.writeByte('\n'); 971 } 972 try file.writeStreamingAll(io, writer.written()); 973 } 974 975 // tag the current HEAD as the next sequential version: v1, v2, and so on 976 fn addNextTag(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void { 977 var buf: [16]u8 = undefined; 978 const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); 979 _ = try repo.addTag(io, allocator, .{ .name = name }); 980 n.* += 1; 981 } 982 983 const PatchRevisionFixture = struct { 984 id: [evt.event_id_size]u8, 985 oid: [hash.hexLen(.sha1)]u8, 986 tree_oid: [hash.hexLen(.sha1)]u8, 987 timestamp: u64, 988 989 fn event(self: *const PatchRevisionFixture, title: []const u8) evt.PatchRev { 990 return .{ 991 .base_oid = &self.oid, 992 .source_oid = &self.oid, 993 .target_ref = "refs/heads/master", 994 .message = title, 995 }; 996 } 997 }; 998 999 fn commitTree( 1000 io: std.Io, 1001 allocator: std.mem.Allocator, 1002 repo: *rp.Repo(.xit, .{}), 1003 oid: *const [hash.hexLen(.sha1)]u8, 1004 ) ![hash.hexLen(.sha1)]u8 { 1005 var moment = try repo.core.latestMoment(); 1006 const state = rp.Repo(.xit, .{}).State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; 1007 var object = try obj.Object(.xit, .{}).init(state, io, allocator, oid); 1008 defer object.deinit(); 1009 return switch (object.content) { 1010 .commit => |commit| commit.tree, 1011 else => error.InvalidObject, 1012 }; 1013 } 1014 1015 fn seedPatchRevision( 1016 io: std.Io, 1017 allocator: std.mem.Allocator, 1018 repos_path: []const u8, 1019 patch_id: *const [evt.event_id_size]u8, 1020 title: []const u8, 1021 timestamp: u64, 1022 random: std.Random, 1023 ) !PatchRevisionFixture { 1024 const patch_hex = std.fmt.bytesToHex(patch_id.*, .lower); 1025 const fork_path = try fork.forkPath(allocator, repos_path, &patch_hex); 1026 defer allocator.free(fork_path); 1027 var fork_repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }); 1028 defer fork_repo.deinit(io, allocator); 1029 1030 const source_oid = (try fork_repo.readRef(io, fork.ref)) orelse return error.NotFound; 1031 var fixture = PatchRevisionFixture{ 1032 .id = evt.EventWithId.randomId(random), 1033 .oid = source_oid, 1034 .tree_oid = try commitTree(io, allocator, &fork_repo, &source_oid), 1035 .timestamp = timestamp, 1036 }; 1037 const revision = fixture.event(title); 1038 const tree_entries = [_]evt.EventTreeEntry{ 1039 .{ .tree = .{ .name = "base", .oid = &fixture.tree_oid } }, 1040 .{ .tree = .{ .name = "head", .oid = &fixture.tree_oid } }, 1041 }; 1042 try evt.consume(.fork, .xit, .{}, io, allocator, &fork_repo, evt.events_ref, &.{.{ 1043 .id = std.fmt.bytesToHex(fixture.id, .lower), 1044 .timestamp = timestamp, 1045 .author = .{ .name = "admin", .email = "admin@example.test" }, 1046 .tree_entries = &tree_entries, 1047 .event = .{ .patchrev = revision }, 1048 }}); 1049 1050 var arena = std.heap.ArenaAllocator.init(allocator); 1051 defer arena.deinit(); 1052 const moment = try evt.currentMoment(.{}, &fork_repo); 1053 const revision_record = (try evt.PatchRev.readById(evt.EventDB(.sha1), .sha1, moment, &arena, &fixture.id)) orelse return error.NotFound; 1054 const patch_record = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &arena, patch_id)) orelse return error.NotFound; 1055 var patch = patch_record.event; 1056 patch.revision = evt.Patch.Revision.fromRecord(fixture.id, revision_record); 1057 try evt.consume(.fork, .xit, .{}, io, allocator, &fork_repo, evt.events_ref, &.{.{ 1058 .id = patch_hex, 1059 .timestamp = timestamp + 1, 1060 .author = .{ .name = "admin", .email = "admin@example.test" }, 1061 .event = .{ .patch = patch }, 1062 }}); 1063 return fixture; 1064 } 1065 1066 fn seedPatches( 1067 io: std.Io, 1068 allocator: std.mem.Allocator, 1069 server_path: []const u8, 1070 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), 1071 target_repo: *rp.Repo(.xit, .{}), 1072 repo_id: *const [evt.event_id_size]u8, 1073 user_id: *const [evt.event_id_size]u8, 1074 random: std.Random, 1075 ) !void { 1076 const patch_data = [_]struct { 1077 title: []const u8, 1078 description: []const u8, 1079 tags: []const u8, 1080 status: ?evt.Patch.Status, 1081 }{ 1082 .{ 1083 .title = "Draft a faster dependency scanner", 1084 .description = "Rework dependency discovery so a large workspace can be scanned without repeatedly opening the same manifests.", 1085 .tags = "performance build", 1086 .status = null, 1087 }, 1088 .{ 1089 .title = "Remove the legacy configuration loader", 1090 .description = "Delete the compatibility loader now that the replacement format has shipped and the migration warning has been available for a full release.", 1091 .tags = "cleanup config", 1092 .status = .merged, 1093 }, 1094 .{ 1095 .title = "Cache parsed manifests between commands", 1096 .description = "Keep parsed manifests in the command context so consecutive operations do not repeat identical filesystem and parsing work.", 1097 .tags = "performance cache", 1098 .status = .closed, 1099 }, 1100 .{ 1101 .title = "Add structured output to the inspect command", 1102 .description = "Add a stable JSON representation of inspect results for scripts and editor integrations.", 1103 .tags = "enhancement cli", 1104 .status = .open, 1105 }, 1106 .{ 1107 .title = "Preserve file permissions during export", 1108 .description = "Carry executable bits through archive exports so unpacked command-line tools remain runnable without a manual chmod step.", 1109 .tags = "bug export permissions", 1110 .status = .open, 1111 }, 1112 }; 1113 1114 const repos_path = try std.fs.path.join(allocator, &.{ server_path, "repos" }); 1115 defer allocator.free(repos_path); 1116 var patch_ids: [patch_data.len][evt.event_id_size]u8 = undefined; 1117 for (patch_data, 0..) |patch, i| { 1118 patch_ids[i] = evt.EventWithId.randomId(random); 1119 const patch_hex = std.fmt.bytesToHex(patch_ids[i], .lower); 1120 const path = try fork.create(.{}, io, allocator, repos_path, admin_repo, .{ 1121 .id = patch_hex, 1122 .user_id = user_id.*, 1123 .repo_id = repo_id.*, 1124 .title = patch.title, 1125 .description = patch.description, 1126 .tags = patch.tags, 1127 .author = .{ .name = "admin", .email = "admin@example.test" }, 1128 .timestamp = @intCast(500 + i * 10), 1129 }); 1130 allocator.free(path); 1131 const status = patch.status orelse continue; 1132 1133 const revision = try seedPatchRevision(io, allocator, repos_path, &patch_ids[i], patch.title, @intCast(502 + i * 10), random); 1134 const fork_path = try fork.forkPath(allocator, repos_path, &patch_hex); 1135 defer allocator.free(fork_path); 1136 try pch.post(.{}, io, allocator, admin_repo, target_repo, fork_path, .{ 1137 .id = patch_hex, 1138 .user_id = user_id.*, 1139 .repo_id = repo_id.*, 1140 .author = .{ .name = "admin", .email = "admin@example.test" }, 1141 .timestamp = @intCast(505 + i * 10), 1142 }); 1143 1144 if (status == .merged) { 1145 const event = revision.event(patch.title); 1146 const tree_entries = [_]evt.EventTreeEntry{ 1147 .{ .tree = .{ .name = "base", .oid = &revision.tree_oid } }, 1148 .{ .tree = .{ .name = "head", .oid = &revision.tree_oid } }, 1149 }; 1150 try evt.consume(.repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &.{.{ 1151 .id = std.fmt.bytesToHex(revision.id, .lower), 1152 .timestamp = revision.timestamp, 1153 .author = .{ .name = "admin", .email = "admin@example.test" }, 1154 .tree_entries = &tree_entries, 1155 .event = .{ .patchrev = event }, 1156 }}); 1157 } 1158 if (status != .open) { 1159 try evt.Patch.update(.xit, .{}, io, allocator, target_repo, &patch_ids[i], .{ .status = status }, .{ 1160 .name = "admin", 1161 .email = "admin@example.test", 1162 }); 1163 } 1164 } 1165 1166 // seed a small comment tree on the newest patch 1167 var comment_ids: [5][evt.event_id_size]u8 = undefined; 1168 for (&comment_ids) |*id| id.* = evt.EventWithId.randomId(random); 1169 const comment_bodies = [_][]const u8{ 1170 "This also needs to preserve the executable bit for files nested inside generated directories.", 1171 "I tested the archive path and the direct-copy path; both currently lose the mode.", 1172 "The archive writer is the right place to centralize it, since every exporter already goes through that layer.", 1173 "Please include a regression fixture with a mix of executable and ordinary files.", 1174 "I'll add that fixture and cover both supported archive formats.", 1175 }; 1176 var comments: [comment_ids.len]evt.EventWithId = undefined; 1177 for (&comments, 0..) |*comment, i| { 1178 comment.* = .{ 1179 .id = std.fmt.bytesToHex(comment_ids[i], .lower), 1180 .timestamp = @intCast(700 + i), 1181 .author = .{ .name = if (i % 2 == 0) "alice" else "bob", .email = if (i % 2 == 0) "alice@example.test" else "bob@example.test" }, 1182 .event = .{ .comment = .{ 1183 .thread_id = std.fmt.bytesToHex(patch_ids[patch_ids.len - 1], .lower), 1184 .parent_id = switch (i) { 1185 0, 1 => std.fmt.bytesToHex(patch_ids[patch_ids.len - 1], .lower), 1186 2, 4 => std.fmt.bytesToHex(comment_ids[0], .lower), 1187 3 => std.fmt.bytesToHex(comment_ids[2], .lower), 1188 else => unreachable, 1189 }, 1190 .body = comment_bodies[i], 1191 } }, 1192 }; 1193 } 1194 try evt.consume(.repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &comments); 1195 1196 // create divergent metadata edits on the next three patches 1197 var patch_arena = std.heap.ArenaAllocator.init(allocator); 1198 defer patch_arena.deinit(); 1199 const moment = try evt.currentMoment(.{}, target_repo); 1200 var values: [3]evt.Patch = undefined; 1201 for (1..4) |i| { 1202 const record = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &patch_arena, &patch_ids[i])) orelse return error.NotFound; 1203 values[i - 1] = record.event; 1204 } 1205 var ours_values = values; 1206 ours_values[0].title = "Drop the legacy configuration loader"; 1207 ours_values[0].tags = "cleanup config breaking"; 1208 ours_values[1].description = "Cache parsed manifests for the lifetime of a command invocation and invalidate entries when their files change."; 1209 ours_values[2].description = "Add JSON output to inspect with versioned field names and deterministic object ordering."; 1210 var theirs_values = values; 1211 theirs_values[0].title = "Delete compatibility configuration support"; 1212 theirs_values[0].tags = "config maintenance"; 1213 theirs_values[1].description = "Keep a process-wide manifest cache shared by every command and refresh it after writes."; 1214 theirs_values[2].description = "Expose inspect results as newline-delimited JSON so callers can stream large repositories."; 1215 1216 const seed_tip = (try target_repo.readRef(io, evt.events_ref)) orelse return error.NotFound; 1217 const other_ref = xit.ref.Ref{ .kind = .head, .name = "haxy/patch-other" }; 1218 var ours = [_]evt.EventWithId{ 1219 .{ 1220 .id = std.fmt.bytesToHex(patch_ids[1], .lower), 1221 .timestamp = 800, 1222 .author = .{ .name = "alice", .email = "alice@example.test" }, 1223 .event = .{ .patch = ours_values[0] }, 1224 }, 1225 .{ 1226 .id = std.fmt.bytesToHex(patch_ids[2], .lower), 1227 .timestamp = 801, 1228 .author = .{ .name = "alice", .email = "alice@example.test" }, 1229 .event = .{ .patch = ours_values[1] }, 1230 }, 1231 .{ 1232 .id = std.fmt.bytesToHex(patch_ids[3], .lower), 1233 .timestamp = 802, 1234 .author = .{ .name = "alice", .email = "alice@example.test" }, 1235 .event = .{ .patch = ours_values[2] }, 1236 }, 1237 }; 1238 1239 var theirs = ours; 1240 theirs[0].timestamp = 810; 1241 theirs[0].author = .{ .name = "bob", .email = "bob@example.test" }; 1242 theirs[0].event.patch = theirs_values[0]; 1243 theirs[1].timestamp = 811; 1244 theirs[1].author = .{ .name = "bob", .email = "bob@example.test" }; 1245 theirs[1].event.patch = theirs_values[1]; 1246 theirs[2].timestamp = 812; 1247 theirs[2].author = .{ .name = "bob", .email = "bob@example.test" }; 1248 theirs[2].event.patch = theirs_values[2]; 1249 1250 try evt.consume(.repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &ours); 1251 var json: std.Io.Writer.Allocating = .init(allocator); 1252 defer json.deinit(); 1253 for (theirs, 0..) |event, i| { 1254 json.clearRetainingCapacity(); 1255 try std.json.Stringify.value(event, .{}, &json.writer); 1256 const author = try std.fmt.allocPrint(allocator, "{s} <{s}>", .{ event.author.name, event.author.email }); 1257 defer allocator.free(author); 1258 _ = try target_repo.commitAtRef(io, allocator, .{ 1259 .author = author, 1260 .message = json.written(), 1261 .timestamp = event.timestamp, 1262 .parent_oids = if (i == 0) &.{seed_tip} else null, 1263 }, null, other_ref); 1264 } 1265 { 1266 var to_events = try target_repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 1267 defer to_events.deinit(); 1268 } 1269 { 1270 var merge = try target_repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, null); 1271 defer merge.deinit(); 1272 if (merge.result != .success) return error.MergeFailed; 1273 } 1274 { 1275 var to_master = try target_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 1276 defer to_master.deinit(); 1277 } 1278 try target_repo.removeBranch(io, .{ .name = other_ref.name }); 1279 try evt.consume(.repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &.{}); 1280 } 1281 1282 // recursively copy the contents of src_dir into dest_dir 1283 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 1284 var iter = src_dir.iterate(); 1285 while (try iter.next(io)) |entry| { 1286 switch (entry.kind) { 1287 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 1288 .directory => { 1289 try dest_dir.createDirPath(io, entry.name); 1290 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1291 defer dest_entry_dir.close(io); 1292 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1293 defer src_entry_dir.close(io); 1294 try copyDir(io, src_entry_dir, dest_entry_dir); 1295 }, 1296 else => {}, 1297 } 1298 } 1299 } 1300