╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ try.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
611 // appends a different closing paragraph). 612 { 613 const other_ref: xit.ref.Ref = .{ .kind = .head, .name = "haxy/other" }; 614 const title_issue = issue_data[issue_data.len - 4]; 615 const sync_issue = issue_data[issue_data.len - 3]; 616 const desc_issue = issue_data[issue_data.len - 2]; 617 618 const ours = [_]evt.EventWithId{ .{ 619 .id = issue_events[issue_data.len - 4].id, 620 .timestamp = 100, 621 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 622 .event = .{ .issue = .{ 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(.local, .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(.local, .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(.local, .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(.local, .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(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &discussion_comment_events); 824 try template_repo.patchAll(io, allocator, null); 825 try template_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "true" }); 826 // fixtures commit directly, so index the ref tips by hand 827 try find.refresh(.{}, io, allocator, &template_repo); 828 try cms.refresh(.{}, io, allocator, &template_repo, null); 829 } 830 831 // copy the template to each repo's on-disk location, named by its 832 // hex-encoded event id. the template repo is deinitialized above, so its 833 // db file is fully written before we copy it. 834 { 835 var template_dir = try cwd.openDir(io, template_path, .{}); 836 defer template_dir.close(io); 837 var template_repo_dir = try template_dir.openDir(io, ".xit", .{ .iterate = true }); 838 defer template_repo_dir.close(io); 839 840 for (repo_event_ids, 0..) |id_bytes, repo_index| { 841 const repo_id = std.fmt.bytesToHex(id_bytes, .lower); 842 const repo_path = try std.fs.path.join(arena.allocator(), &.{ server_path, "repos", &repo_id }); 843 { 844 var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); 845 defer dest_dir.close(io); 846 var dest_repo_dir = try dest_dir.createDirPathOpen(io, ".xit", .{}); 847 defer dest_repo_dir.close(io); 848 try copyDir(io, template_repo_dir, dest_repo_dir); 849 } 850 851 if (repo_index + 1 == repo_data.len) { 852 var target_repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = repo_path, .require_repo_root = true }); 853 defer target_repo.deinit(io, allocator); 854 try seedPatches(io, allocator, server_path, &repo, &target_repo, &repo_event_ids[repo_index], &admin_user_id, prng.random()); 855 } 856 } 857 } 858 859 break :blk try ui.Session.init(&session_arena, &repo, .{ .user_id = &admin_user_id }); 860 }; 861 session.is_terminal = true; 862 session.data.git_ssh_prefix = git_ssh_prefix; 863 864 // start the server 865 866 // let the native TUI's page builders open the on-disk repos for the file tree 867 session.io = io; 868 session.repos_dir = try std.fs.path.join(session_arena.allocator(), &.{ server_path, "repos" }); 869 870 // leave a one-shot session for the first browser to hit the web ui, so it 871 // starts logged in as admin 872 { 873 var data_dir = try cwd.createDirPathOpen(io, server_path, .{}); 874 defer data_dir.close(io); 875 const store = try hx.web.SessionStore.init(io, data_dir); 876 defer store.deinit(); 877 const token = try store.create(&admin_user_id); 878 try store.offerAutoLogin(&token); 879 } 880 881 if (cli) { 882 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 883 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 884 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; 885 886 const Runnable = struct { 887 io: std.Io, 888 key_path: []const u8, 889 890 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port_maybe: ?u16) !void { 891 _ = web_port; 892 _ = http_port; 893 const ssh_port = ssh_port_maybe orelse return error.SshDisabled; 894 std.debug.print( 895 \\ 896 \\connect to the TUI with: 897 \\ ssh -p {d} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost 898 \\ 899 \\create a git repo and push over SSH: 900 \\ mkdir -p temp-try/client/test 901 \\ cd temp-try/client/test 902 \\ git init 903 \\ echo "hello" > hello.txt 904 \\ git add hello.txt 905 \\ git commit -m "let there be light" 906 \\ 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 907 \\ 908 \\to quit, press enter. 909 \\ 910 , .{ ssh_port, ssh_port, self.key_path }); 911 // portable stdin read via std.Io — equivalent to the 912 // std.posix.read(STDIN_FILENO, ...) that doesn't compile 913 // on windows. 914 var buf: [1]u8 = undefined; 915 var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); 916 _ = stdin_reader.interface.takeByte() catch {}; 917 } 918 }; 919 920 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .key_path = key_path }); 921 } else { 922 var null_writer = std.Io.Writer.Discarding.init(&.{}); 923 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; 924 925 const Runnable = struct { 926 io: std.Io, 927 allocator: std.mem.Allocator, 928 session: *ui.Session, 929 repo: *Repo, 930 931 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port: ?u16) !void { 932 // launch the TUI; the footer points at whatever port was bound 933 self.session.web_port = web_port; 934 self.session.data.git_http_port = http_port; 935 self.session.data.git_ssh_port = ssh_port; 936 try hx.ui.run(self.io, self.allocator, self.session, self.repo); 937 } 938 }; 939 940 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .allocator = allocator, .session = &session, .repo = &repo }); 941 } 942 } 943 944 // words cycled through to pad generated lines into prose-like content 945 const scatter_words = [_][]const u8{ 946 "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", 947 "quantum", "vector", "matrix", "buffer", "kernel", "socket", 948 "falcon", "otter", "badger", "walrus", "ferret", "marmot", 949 "scatter", "gather", "encode", "decode", "render", "commit", 950 }; 951 952 fn longDescription(allocator: std.mem.Allocator, initial: []const u8) ![]u8 { 953 var writer = std.Io.Writer.Allocating.init(allocator); 954 defer writer.deinit(); 955 try writer.writer.print("{s}", .{initial}); 956 for (0..ui.detail_preview_lines) |line| 957 try writer.writer.print("\n{d} {s}", .{ line, scatter_words[line % scatter_words.len] }); 958 return writer.toOwnedSlice(); 959 } 960 961 // write `path` as 40 lines of mostly-stable filler. revision `c` and file 962 // index `fi` shift which lines change and the words used, so each (c, fi) 963 // yields a distinct multi-hunk diff against the previous revision. untouched 964 // lines stay byte-identical across revisions, so a bump in `c` is a scatter of 965 // small hunks rather than a full rewrite. 966 fn writeScatterFile(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir, path: []const u8, fi: usize, c: usize) !void { 967 const content = try scatterContent(allocator, fi, c, null); 968 defer allocator.free(content); 969 const file = try repo_dir.createFile(io, path, .{}); 970 defer file.close(io); 971 try file.writeStreamingAll(io, content); 972 } 973 974 const ScatterEdit = struct { line: usize, text: []const u8 }; 975 976 fn scatterContent(allocator: std.mem.Allocator, fi: usize, c: usize, replacement: ?ScatterEdit) ![]u8 { 977 var writer = std.Io.Writer.Allocating.init(allocator); 978 defer writer.deinit(); 979 for (0..40) |line| { 980 if (replacement) |edit| if (line == edit.line) { 981 try writer.writer.print("{s}\n", .{edit.text}); 982 continue; 983 }; 984 const start = writer.written().len; 985 if ((line + fi + c) % 8 == 0) { 986 const tag = scatter_words[(line + fi + c * 3) % scatter_words.len]; 987 try writer.writer.print("rev {d} {s}", .{ c, tag }); 988 } 989 const target = 40 + (line * 17 + fi * 23) % 70; 990 var w = line + fi; 991 while (true) : (w += 1) { 992 const word = scatter_words[w % scatter_words.len]; 993 const len = writer.written().len - start; 994 if (len >= target or len + 1 + word.len > 120) break; 995 if (len == 0) 996 try writer.writer.print("{s}", .{word}) 997 else 998 try writer.writer.print(" {s}", .{word}); 999 } 1000 try writer.writer.writeByte('\n'); 1001 } 1002 return writer.toOwnedSlice(); 1003 } 1004 1005 // tag the current HEAD as the next sequential version: v1, v2, and so on 1006 fn addNextTag(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void { 1007 var buf: [16]u8 = undefined; 1008 const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); 1009 _ = try repo.addTag(io, allocator, .{ .name = name }); 1010 n.* += 1; 1011 } 1012 1013 fn commitTree( 1014 io: std.Io, 1015 allocator: std.mem.Allocator, 1016 repo: *rp.Repo(.xit, .{}), 1017 oid: *const [hash.hexLen(.sha1)]u8, 1018 ) ![hash.hexLen(.sha1)]u8 { 1019 var moment = try repo.core.latestMoment(); 1020 const state = rp.Repo(.xit, .{}).State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; 1021 var object = try obj.Object(.xit, .{}).init(state, io, allocator, oid); 1022 defer object.deinit(); 1023 return switch (object.content) { 1024 .commit => |commit| commit.tree, 1025 else => error.InvalidObject, 1026 }; 1027 } 1028 1029 fn seedPatchRevision( 1030 io: std.Io, 1031 allocator: std.mem.Allocator, 1032 repos_path: []const u8, 1033 patch_id: *const [evt.event_id_size]u8, 1034 title: []const u8, 1035 author: evt.CommitAuthor, 1036 timestamp: u64, 1037 random: std.Random, 1038 base_oid: [hash.hexLen(.sha1)]u8, 1039 path: []const u8, 1040 contents: []const []const u8, 1041 ) !void { 1042 const patch_hex = std.fmt.bytesToHex(patch_id.*, .lower); 1043 const fork_path = try fork.forkPath(allocator, repos_path, &patch_hex); 1044 defer allocator.free(fork_path); 1045 var fork_repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }); 1046 defer fork_repo.deinit(io, allocator); 1047 1048 try fork_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "false" }); 1049 { 1050 var reset = try fork_repo.reset(io, allocator, .{ .target = .{ .oid = &base_oid } }); 1051 defer reset.deinit(); 1052 } 1053 const base_tree_oid = try commitTree(io, allocator, &fork_repo, &base_oid); 1054 var source_oid = base_oid; 1055 var fork_dir = try std.Io.Dir.cwd().openDir(io, fork_path, .{}); 1056 defer fork_dir.close(io); 1057 var writer = std.Io.Writer.Allocating.init(allocator); 1058 defer writer.deinit(); 1059 if (std.fs.path.dirname(path)) |dir| try fork_dir.createDirPath(io, dir); 1060 for (contents, 0..) |content, i| { 1061 { 1062 const file = try fork_dir.createFile(io, path, .{}); 1063 defer file.close(io); 1064 try file.writeStreamingAll(io, content); 1065 } 1066 try fork_repo.add(io, allocator, &.{path}); 1067 writer.clearRetainingCapacity(); 1068 try writer.writer.print("{s} ({d}/{d})", .{ title, i + 1, contents.len }); 1069 source_oid = try fork_repo.commit(io, allocator, .{ .message = writer.written(), .timestamp = timestamp + i }); 1070 } 1071 try fork_repo.patchAll(io, allocator, null); 1072 try fork_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "true" }); 1073 // the commits above skipped the push path, so index the patch branch by hand 1074 try find.refresh(.{}, io, allocator, &fork_repo); 1075 try fork_dir.deleteFile(io, path); 1076 const revision_id = evt.EventWithId.randomId(random); 1077 const head_tree_oid = try commitTree(io, allocator, &fork_repo, &source_oid); 1078 const revision: evt.PatchRev = .{ 1079 .base_oid = &base_oid, 1080 .source_oid = &source_oid, 1081 .message = title, 1082 }; 1083 const revision_timestamp = timestamp + 3; 1084 const tree_entries = [_]evt.EventTreeEntry{ 1085 .{ .tree = .{ .name = "base", .oid = &base_tree_oid } }, 1086 .{ .tree = .{ .name = "head", .oid = &head_tree_oid } }, 1087 }; 1088 try evt.consume(.server, .fork, .xit, .{}, io, allocator, &fork_repo, evt.events_ref, &.{.{ 1089 .id = std.fmt.bytesToHex(revision_id, .lower), 1090 .timestamp = revision_timestamp, 1091 .author = author, 1092 .tree_entries = &tree_entries, 1093 .event = .{ .patchrev = revision }, 1094 }}); 1095 1096 var arena = std.heap.ArenaAllocator.init(allocator); 1097 defer arena.deinit(); 1098 const moment = try evt.currentMoment(.{}, &fork_repo); 1099 const revision_record = (try evt.PatchRev.readById(evt.EventDB(.sha1), .sha1, moment, &arena, &revision_id)) orelse return error.NotFound; 1100 const patch_record = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &arena, patch_id)) orelse return error.NotFound; 1101 var patch = patch_record.event; 1102 patch.revision = evt.Patch.Revision.fromRecord(revision_id, revision_record); 1103 try evt.consume(.server, .fork, .xit, .{}, io, allocator, &fork_repo, evt.events_ref, &.{.{ 1104 .id = patch_hex, 1105 .timestamp = revision_timestamp + 1, 1106 .author = author, 1107 .event = .{ .patch = patch }, 1108 }}); 1109 } 1110 1111 fn seedPatches( 1112 io: std.Io, 1113 allocator: std.mem.Allocator, 1114 server_path: []const u8, 1115 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), 1116 target_repo: *rp.Repo(.xit, .{}), 1117 repo_id: *const [evt.event_id_size]u8, 1118 user_id: *const [evt.event_id_size]u8, 1119 random: std.Random, 1120 ) !void { 1121 const patch_data = [_]struct { 1122 title: []const u8, 1123 description: []const u8, 1124 tags: []const u8, 1125 status: ?evt.Patch.StatusKind, 1126 alpha_edit: ?ScatterEdit = null, 1127 }{ 1128 .{ 1129 .title = "Draft a faster dependency scanner", 1130 .description = "Rework dependency discovery so a large workspace can be scanned without repeatedly opening the same manifests.", 1131 .tags = "performance build", 1132 .status = null, 1133 }, 1134 .{ 1135 .title = "Remove the legacy configuration loader", 1136 .description = "Delete the compatibility loader now that the replacement format has shipped and the migration warning has been available for a full release.", 1137 .tags = "cleanup config", 1138 .status = .merged, 1139 }, 1140 .{ 1141 .title = "Cache parsed manifests between commands", 1142 .description = "Keep parsed manifests in the command context so consecutive operations do not repeat identical filesystem and parsing work.", 1143 .tags = "performance cache", 1144 .status = .closed, 1145 }, 1146 .{ 1147 .title = "Add structured output to the inspect command", 1148 .description = "Add a stable JSON representation of inspect results for scripts and editor integrations.", 1149 .tags = "enhancement cli", 1150 .status = .open, 1151 }, 1152 .{ 1153 .title = "Edit an adjacent line in alpha.txt", 1154 .description = "This patch can be cleanly merged by haxy, while git throws a merge conflict!", 1155 .tags = "merge", 1156 .status = .open, 1157 .alpha_edit = .{ .line = 2, .text = "adjust alpha beside the latest scatter edit" }, 1158 }, 1159 .{ 1160 .title = "Edit a conflicting line in alpha.txt", 1161 .description = "Both source and squash merges conflict with a different edit to the same line on master.", 1162 .tags = "merge", 1163 .status = .open, 1164 .alpha_edit = .{ .line = 3, .text = "replace alpha with a conflicting edit" }, 1165 }, 1166 }; 1167 1168 const repos_path = try std.fs.path.join(allocator, &.{ server_path, "repos" }); 1169 defer allocator.free(repos_path); 1170 const patch_author = evt.CommitAuthor{ .name = "admin", .email = "admin@example.test" }; 1171 var patch_ids: [patch_data.len][evt.event_id_size]u8 = undefined; 1172 for (patch_data, 0..) |patch, i| { 1173 const timestamp: u64 = @intCast(500 + i * 10); 1174 patch_ids[i] = evt.EventWithId.randomId(random); 1175 const patch_hex = std.fmt.bytesToHex(patch_ids[i], .lower); 1176 const path = try fork.create(.{}, io, allocator, repos_path, admin_repo, .{ 1177 .id = patch_hex, 1178 .user_id = user_id.*, 1179 .repo_id = repo_id.*, 1180 .title = patch.title, 1181 .description = patch.description, 1182 .tags = patch.tags, 1183 .target_branch = "master", 1184 .author = patch_author, 1185 .timestamp = timestamp, 1186 }); 1187 allocator.free(path); 1188 1189 // prepare the patch's base and file contents, then seed its commits and revision 1190 { 1191 var arena = std.heap.ArenaAllocator.init(allocator); 1192 defer arena.deinit(); 1193 const aa = arena.allocator(); 1194 var base_oid = (try target_repo.readRef(io, .{ .kind = .head, .name = "master" })) orelse return error.NotFound; 1195 var contents: [3][]const u8 = undefined; 1196 const file_path: []const u8, const count: usize = if (patch.alpha_edit) |edit| blk: { 1197 // v30 precedes the last scatter commit, even after the sample merge 1198 const tag_oid = (try target_repo.readRef(io, .{ .kind = .tag, .name = "v30" })) orelse return error.NotFound; 1199 var moment = try target_repo.core.latestMoment(); 1200 const state = rp.Repo(.xit, .{}).State(.read_only){ .core = &target_repo.core, .extra = .{ .moment = &moment } }; 1201 var tag = try obj.Object(.xit, .{}).init(state, io, allocator, &tag_oid); 1202 defer tag.deinit(); 1203 base_oid = switch (tag.content) { 1204 .tag => |value| value.target, 1205 else => return error.InvalidObject, 1206 }; 1207 // rev 29 first changes line 4; edit that line or the one above it 1208 contents[0] = try scatterContent(aa, 0, 28, edit); 1209 break :blk .{ "src/alpha.txt", 1 }; 1210 } else blk: { 1211 var writer = std.Io.Writer.Allocating.init(allocator); 1212 defer writer.deinit(); 1213 for (&contents, 0..) |*content, part| { 1214 try writer.writer.print("{s}: part {d}\n", .{ patch.title, part + 1 }); 1215 content.* = try aa.dupe(u8, writer.written()); 1216 } 1217 break :blk .{ "patch.txt", contents.len }; 1218 }; 1219 try seedPatchRevision(io, allocator, repos_path, &patch_ids[i], patch.title, patch_author, timestamp + 1, random, base_oid, file_path, contents[0..count]); 1220 } 1221 const status = patch.status orelse continue; 1222 const fork_path = try fork.forkPath(allocator, repos_path, &patch_hex); 1223 defer allocator.free(fork_path); 1224 try pch.publish(.{}, io, allocator, admin_repo, target_repo, fork_path, .{ 1225 .id = patch_hex, 1226 .user_id = user_id.*, 1227 .repo_id = repo_id.*, 1228 .author = patch_author, 1229 .timestamp = timestamp + 6, 1230 }); 1231 1232 if (status == .merged) { 1233 try pch.mergeAndRemoveFork(.{}, io, allocator, repos_path, admin_repo, target_repo, .{ 1234 .id = patch_hex, 1235 .revision = .source, 1236 .author = patch_author, 1237 .timestamp = timestamp + 7, 1238 }); 1239 } else if (status != .open) { 1240 try evt.Patch.update(.server, .xit, .{}, io, allocator, target_repo, &patch_ids[i], .{ .status = status }, patch_author); 1241 } 1242 } 1243 1244 // seed a small comment tree on the newest patch 1245 var comment_ids: [5][evt.event_id_size]u8 = undefined; 1246 for (&comment_ids) |*id| id.* = evt.EventWithId.randomId(random); 1247 const comment_bodies = [_][]const u8{ 1248 "This also needs to preserve the executable bit for files nested inside generated directories.", 1249 "I tested the archive path and the direct-copy path; both currently lose the mode.", 1250 "The archive writer is the right place to centralize it, since every exporter already goes through that layer.", 1251 "Please include a regression fixture with a mix of executable and ordinary files.", 1252 "I'll add that fixture and cover both supported archive formats.", 1253 }; 1254 var comments: [comment_ids.len]evt.EventWithId = undefined; 1255 for (&comments, 0..) |*comment, i| { 1256 comment.* = .{ 1257 .id = std.fmt.bytesToHex(comment_ids[i], .lower), 1258 .timestamp = @intCast(700 + i), 1259 .author = .{ .name = if (i % 2 == 0) "alice" else "bob", .email = if (i % 2 == 0) "alice@example.test" else "bob@example.test" }, 1260 .event = .{ .comment = .{ 1261 .thread_id = std.fmt.bytesToHex(patch_ids[patch_ids.len - 1], .lower), 1262 .parent_id = switch (i) { 1263 0, 1 => std.fmt.bytesToHex(patch_ids[patch_ids.len - 1], .lower), 1264 2, 4 => std.fmt.bytesToHex(comment_ids[0], .lower), 1265 3 => std.fmt.bytesToHex(comment_ids[2], .lower), 1266 else => unreachable, 1267 }, 1268 .body = comment_bodies[i], 1269 } }, 1270 }; 1271 } 1272 try evt.consume(.server, .repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &comments); 1273 1274 // create divergent metadata edits on the next three patches 1275 var patch_arena = std.heap.ArenaAllocator.init(allocator); 1276 defer patch_arena.deinit(); 1277 const moment = try evt.currentMoment(.{}, target_repo); 1278 var values: [3]evt.Patch = undefined; 1279 for (1..4) |i| { 1280 const record = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &patch_arena, &patch_ids[i])) orelse return error.NotFound; 1281 values[i - 1] = record.event; 1282 } 1283 var ours_values = values; 1284 ours_values[0].title = "Drop the legacy configuration loader"; 1285 ours_values[0].tags = "cleanup config breaking"; 1286 const closed_description = try longDescription( 1287 patch_arena.allocator(), 1288 "Cache parsed manifests for the lifetime of a command invocation and invalidate entries when their files change.", 1289 ); 1290 ours_values[1].description = closed_description; 1291 ours_values[2].description = "Add JSON output to inspect with versioned field names and deterministic object ordering."; 1292 var theirs_values = values; 1293 theirs_values[0].title = "Delete compatibility configuration support"; 1294 theirs_values[0].tags = "config maintenance"; 1295 theirs_values[1].description = "Keep a process-wide manifest cache shared by every command and refresh it after writes."; 1296 theirs_values[2].description = "Expose inspect results as newline-delimited JSON so callers can stream large repositories."; 1297 1298 const seed_tip = (try target_repo.readRef(io, evt.events_ref)) orelse return error.NotFound; 1299 const other_ref = xit.ref.Ref{ .kind = .head, .name = "haxy/patch-other" }; 1300 var ours = [_]evt.EventWithId{ 1301 .{ 1302 .id = std.fmt.bytesToHex(patch_ids[1], .lower), 1303 .timestamp = 800, 1304 .author = .{ .name = "alice", .email = "alice@example.test" }, 1305 .event = .{ .patch = ours_values[0] }, 1306 }, 1307 .{ 1308 .id = std.fmt.bytesToHex(patch_ids[2], .lower), 1309 .timestamp = 801, 1310 .author = .{ .name = "alice", .email = "alice@example.test" }, 1311 .event = .{ .patch = ours_values[1] }, 1312 }, 1313 .{ 1314 .id = std.fmt.bytesToHex(patch_ids[3], .lower), 1315 .timestamp = 802, 1316 .author = .{ .name = "alice", .email = "alice@example.test" }, 1317 .event = .{ .patch = ours_values[2] }, 1318 }, 1319 }; 1320 1321 var theirs = ours; 1322 theirs[0].timestamp = 810; 1323 theirs[0].author = .{ .name = "bob", .email = "bob@example.test" }; 1324 theirs[0].event.patch = theirs_values[0]; 1325 theirs[1].timestamp = 811; 1326 theirs[1].author = .{ .name = "bob", .email = "bob@example.test" }; 1327 theirs[1].event.patch = theirs_values[1]; 1328 theirs[2].timestamp = 812; 1329 theirs[2].author = .{ .name = "bob", .email = "bob@example.test" }; 1330 theirs[2].event.patch = theirs_values[2]; 1331 1332 try evt.consume(.server, .repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &ours); 1333 var json: std.Io.Writer.Allocating = .init(allocator); 1334 defer json.deinit(); 1335 for (theirs, 0..) |event, i| { 1336 json.clearRetainingCapacity(); 1337 try std.json.Stringify.value(event, .{}, &json.writer); 1338 const author = try std.fmt.allocPrint(allocator, "{s} <{s}>", .{ event.author.name, event.author.email }); 1339 defer allocator.free(author); 1340 _ = try target_repo.commitAtRef(io, allocator, .{ 1341 .author = author, 1342 .message = json.written(), 1343 .timestamp = event.timestamp, 1344 .parent_oids = if (i == 0) &.{seed_tip} else null, 1345 }, null, other_ref); 1346 } 1347 { 1348 var merge = try target_repo.mergeAtRef(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, evt.events_ref, null); 1349 defer merge.deinit(); 1350 if (merge.result != .success) return error.MergeFailed; 1351 } 1352 try target_repo.removeBranch(io, .{ .name = other_ref.name }); 1353 try evt.consume(.server, .repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &.{}); 1354 try pch.writeBranchPatch(.server, .xit, .{}, io, allocator, target_repo, std.fmt.bytesToHex(evt.EventWithId.randomId(random), .lower), .{ 1355 .title = "Merge the existing feature branch", 1356 .description = "This patch tracks the feature branch in this repo without creating a fork.", 1357 .tags = "feature", 1358 .source_branch = "feature", 1359 .target_branch = "master", 1360 }, null, patch_author); 1361 } 1362 1363 // recursively copy the contents of src_dir into dest_dir 1364 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 1365 var iter = src_dir.iterate(); 1366 while (try iter.next(io)) |entry| { 1367 switch (entry.kind) { 1368 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 1369 .directory => { 1370 try dest_dir.createDirPath(io, entry.name); 1371 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1372 defer dest_entry_dir.close(io); 1373 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1374 defer src_entry_dir.close(io); 1375 try copyDir(io, src_entry_dir, dest_entry_dir); 1376 }, 1377 else => {}, 1378 } 1379 } 1380 } 1381