╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ try.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
647 .description = sync_p1 ++ "\n\n" ++ sync_p2 ++ "\n\n" ++ 648 "Add end-to-end tests that drive two live instances against a local server on every CI run.", 649 .tags = sync_issue.tags, 650 } }, 651 }, .{ 652 .id = issue_events[issue_data.len - 2].id, 653 .timestamp = 105, 654 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 655 .event = .{ .issue = .{ 656 .title = desc_issue.title, 657 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ 658 "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" ++ 659 "Log exports inherit the same UTC rendering, so downstream tooling has to guess the source timezone.", 660 .tags = desc_issue.tags, 661 } }, 662 } }; 663 664 try evt.consume(.xit, .{}, io, allocator, &template_repo, evt.events_ref, &ours); 665 666 // consume can't root a new branch, so theirs commits by hand 667 var json: std.Io.Writer.Allocating = .init(allocator); 668 defer json.deinit(); 669 for (theirs, 0..) |event, i| { 670 json.clearRetainingCapacity(); 671 try std.json.Stringify.value(event, .{}, &json.writer); 672 const author = try std.fmt.allocPrint(allocator, "{s} <{s}>", .{ event.author.name, event.author.email }); 673 defer allocator.free(author); 674 _ = try template_repo.commitAtRef(io, allocator, .{ 675 .author = author, 676 .message = json.written(), 677 .timestamp = event.timestamp, 678 // root the branch at the shared seed tip 679 .parent_oids = if (i == 0) &.{seed_tip} else null, 680 }, null, other_ref); 681 } 682 683 { 684 var to_events = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 685 defer to_events.deinit(); 686 } 687 { 688 var merge = try template_repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, null); 689 defer merge.deinit(); 690 if (merge.result != .success) return error.MergeFailed; 691 } 692 { 693 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 694 defer to_master.deinit(); 695 } 696 try template_repo.removeBranch(io, .{ .name = other_ref.name }); 697 } 698 699 try evt.consume(.xit, .{}, io, allocator, &template_repo, evt.events_ref, &.{}); 700 701 // seed a small comment tree on the newest issue 702 var comment_ids: [5][evt.event_id_size]u8 = undefined; 703 for (&comment_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 704 705 const comment_bodies = [_][]const u8{ 706 "I can take this. The compatibility scanner already reports which repositories still use the legacy format.", 707 "Can we keep loading legacy plugins for one release after the warning lands? That would give downstream maintainers time to migrate.", 708 "That sounds good. I'll include the scanner output in the warning so each affected plugin is named.", 709 "Please include the replacement manifest path too. That should make the warning actionable without opening the migration guide.", 710 "I'll add release-note text once the warning format is settled.", 711 }; 712 713 var comment_events: [comment_ids.len]evt.EventWithId = undefined; 714 for (&comment_events, 0..) |*event, i| { 715 event.* = .{ 716 .id = std.fmt.bytesToHex(comment_ids[i], .lower), 717 .timestamp = @intCast(200 + i), 718 .author = .{ .name = user_data[(i + 1) % user_data.len].name, .email = user_data[(i + 1) % user_data.len].email }, 719 .event = .{ .comment = .{ 720 .thread_id = issue_events[issue_events.len - 1].id, 721 .parent_id = switch (i) { 722 0, 1 => issue_events[issue_events.len - 1].id, 723 2, 4 => std.fmt.bytesToHex(comment_ids[0], .lower), 724 3 => std.fmt.bytesToHex(comment_ids[2], .lower), 725 else => unreachable, 726 }, 727 .body = comment_bodies[i], 728 } }, 729 }; 730 } 731 try evt.consume(.xit, .{}, io, allocator, &template_repo, evt.events_ref, &comment_events); 732 733 const discussion_data = [_]struct { 734 title: []const u8, 735 description: []const u8, 736 tags: []const u8, 737 }{ 738 .{ 739 .title = "How should plugins declare capabilities?", 740 .description = "I'd like the manifest to make privileged capabilities explicit without making simple plugins verbose.", 741 .tags = "plugins design", 742 }, 743 .{ 744 .title = "Ideas for making large repositories faster", 745 .description = "This is a place to collect profiling results and discuss which indexing work is worth pursuing first.", 746 .tags = "performance indexing", 747 }, 748 .{ 749 .title = "What should the next release focus on?", 750 .description = "Let's compare the most important reliability fixes with the larger features already in progress.", 751 .tags = "release planning", 752 }, 753 .{ 754 .title = "Improving keyboard navigation", 755 .description = "Share workflows that still require a mouse and suggestions for making their focus behavior predictable.", 756 .tags = "ui keyboard accessibility", 757 }, 758 .{ 759 .title = "Configuration format discussion", 760 .description = "Should the next configuration format favor strict validation or accept common conveniences such as trailing commas?", 761 .tags = "config design", 762 }, 763 }; 764 765 var discussion_events: [discussion_data.len]evt.EventWithId = undefined; 766 for (discussion_data, 0..) |discussion, i| { 767 discussion_events[i] = .{ 768 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 769 .timestamp = @intCast(300 + i), 770 .author = .{ .name = user_data[i % user_data.len].name, .email = user_data[i % user_data.len].email }, 771 .event = .{ .discussion = .{ 772 .title = discussion.title, 773 .description = discussion.description, 774 .tags = discussion.tags, 775 } }, 776 }; 777 } 778 try evt.consume(.xit, .{}, io, allocator, &template_repo, evt.events_ref, &discussion_events); 779 780 var discussion_comment_ids: [discussion_data.len][3][evt.event_id_size]u8 = undefined; 781 for (&discussion_comment_ids) |*ids| { 782 for (ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 783 } 784 var discussion_comment_events: [discussion_data.len * 3]evt.EventWithId = undefined; 785 for (&discussion_comment_events, 0..) |*event, i| { 786 const discussion_index = i / 3; 787 const comment_index = i % 3; 788 event.* = .{ 789 .id = std.fmt.bytesToHex(discussion_comment_ids[discussion_index][comment_index], .lower), 790 .timestamp = @intCast(400 + i), 791 .author = .{ .name = user_data[(i + 1) % user_data.len].name, .email = user_data[(i + 1) % user_data.len].email }, 792 .event = .{ .comment = .{ 793 .thread_id = discussion_events[discussion_index].id, 794 .parent_id = if (comment_index < 2) 795 discussion_events[discussion_index].id 796 else 797 std.fmt.bytesToHex(discussion_comment_ids[discussion_index][0], .lower), 798 .body = switch (comment_index) { 799 0 => "My preference is to start with the smallest useful version and expand it after we have real usage to learn from.", 800 1 => "It would help to write down the tradeoffs we are accepting so the decision is easy to revisit later.", 801 2 => "Agreed. A narrow first version should also make it easier to keep the implementation understandable.", 802 else => unreachable, 803 }, 804 } }, 805 }; 806 } 807 try evt.consume(.xit, .{}, io, allocator, &template_repo, evt.events_ref, &discussion_comment_events); 808 } 809 810 // copy the template to each repo's on-disk location, named by its 811 // hex-encoded event id. the template repo is deinitialized above, so its 812 // db file is fully written before we copy it. 813 { 814 var template_dir = try cwd.openDir(io, template_path, .{ .iterate = true }); 815 defer template_dir.close(io); 816 817 for (repo_event_ids) |id_bytes| { 818 const repo_id = std.fmt.bytesToHex(id_bytes, .lower); 819 const repo_path = try std.fs.path.join(arena.allocator(), &.{ server_path, "repos", &repo_id }); 820 var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); 821 defer dest_dir.close(io); 822 try copyDir(io, template_dir, dest_dir); 823 } 824 } 825 826 break :blk try ui.Session.init(&session_arena, &repo, .{ .user_id = &admin_user_id }); 827 }; 828 session.is_terminal = true; 829 830 // start the server 831 832 // let the native TUI's page builders open the on-disk repos for the file tree 833 session.io = io; 834 session.repos_dir = try std.fs.path.join(session_arena.allocator(), &.{ server_path, "repos" }); 835 836 // leave a one-shot session for the first browser to hit the web ui, so it 837 // starts logged in as admin 838 { 839 var data_dir = try cwd.createDirPathOpen(io, server_path, .{}); 840 defer data_dir.close(io); 841 const store = try hx.web.SessionStore.init(io, data_dir); 842 defer store.deinit(); 843 const token = try store.create(&admin_user_id); 844 try store.offerAutoLogin(&token); 845 } 846 847 if (cli) { 848 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 849 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 850 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; 851 852 const key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 853 defer allocator.free(key_path); 854 855 const Runnable = struct { 856 io: std.Io, 857 key_path: []const u8, 858 859 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port_maybe: ?u16) !void { 860 _ = web_port; 861 _ = http_port; 862 const ssh_port = ssh_port_maybe orelse return error.SshDisabled; 863 std.debug.print( 864 \\ 865 \\connect to the TUI with: 866 \\ ssh -p {d} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost 867 \\ 868 \\create a git repo and push over SSH: 869 \\ mkdir -p temp-try/client/test 870 \\ cd temp-try/client/test 871 \\ git init 872 \\ echo "hello" > hello.txt 873 \\ git add hello.txt 874 \\ git commit -m "let there be light" 875 \\ 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 876 \\ 877 \\to quit, press enter. 878 \\ 879 , .{ ssh_port, ssh_port, self.key_path }); 880 // portable stdin read via std.Io — equivalent to the 881 // std.posix.read(STDIN_FILENO, ...) that doesn't compile 882 // on windows. 883 var buf: [1]u8 = undefined; 884 var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); 885 _ = stdin_reader.interface.takeByte() catch {}; 886 } 887 }; 888 889 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .key_path = key_path }); 890 } else { 891 var null_writer = std.Io.Writer.Discarding.init(&.{}); 892 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; 893 894 const Runnable = struct { 895 io: std.Io, 896 allocator: std.mem.Allocator, 897 session: *ui.Session, 898 repo: *Repo, 899 900 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port: ?u16) !void { 901 // launch the TUI; the footer points at whatever port was bound 902 self.session.web_port = web_port; 903 self.session.data.clone_http_port = http_port; 904 self.session.data.clone_ssh_port = ssh_port; 905 try hx.ui.run(self.io, self.allocator, self.session, self.repo); 906 } 907 }; 908 909 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .allocator = allocator, .session = &session, .repo = &repo }); 910 } 911 } 912 913 // words cycled through to pad generated lines into prose-like content 914 const scatter_words = [_][]const u8{ 915 "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", 916 "quantum", "vector", "matrix", "buffer", "kernel", "socket", 917 "falcon", "otter", "badger", "walrus", "ferret", "marmot", 918 "scatter", "gather", "encode", "decode", "render", "commit", 919 }; 920 921 // write `path` as 40 lines of mostly-stable filler. revision `c` and file 922 // index `fi` shift which lines change and the words used, so each (c, fi) 923 // yields a distinct multi-hunk diff against the previous revision. untouched 924 // lines stay byte-identical across revisions, so a bump in `c` is a scatter of 925 // small hunks rather than a full rewrite. 926 fn writeScatterFile(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir, path: []const u8, fi: usize, c: usize) !void { 927 const file = try repo_dir.createFile(io, path, .{}); 928 defer file.close(io); 929 var writer = std.Io.Writer.Allocating.init(allocator); 930 defer writer.deinit(); 931 for (0..40) |line| { 932 const start = writer.written().len; 933 if ((line + fi + c) % 8 == 0) { 934 const tag = scatter_words[(line + fi + c * 3) % scatter_words.len]; 935 try writer.writer.print("rev {d} {s}", .{ c, tag }); 936 } 937 const target = 40 + (line * 17 + fi * 23) % 70; 938 var w = line + fi; 939 while (true) : (w += 1) { 940 const word = scatter_words[w % scatter_words.len]; 941 const len = writer.written().len - start; 942 if (len >= target or len + 1 + word.len > 120) break; 943 if (len == 0) 944 try writer.writer.print("{s}", .{word}) 945 else 946 try writer.writer.print(" {s}", .{word}); 947 } 948 try writer.writer.writeByte('\n'); 949 } 950 try file.writeStreamingAll(io, writer.written()); 951 } 952 953 // tag the current HEAD as the next sequential version: v1, v2, and so on 954 fn addNextTag(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void { 955 var buf: [16]u8 = undefined; 956 const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); 957 _ = try repo.addTag(io, allocator, .{ .name = name }); 958 n.* += 1; 959 } 960 961 // recursively copy the contents of src_dir into dest_dir 962 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 963 var iter = src_dir.iterate(); 964 while (try iter.next(io)) |entry| { 965 switch (entry.kind) { 966 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 967 .directory => { 968 try dest_dir.createDirPath(io, entry.name); 969 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 970 defer dest_entry_dir.close(io); 971 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 972 defer src_entry_dir.close(io); 973 try copyDir(io, src_entry_dir, dest_entry_dir); 974 }, 975 else => {}, 976 } 977 } 978 } 979