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