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