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