╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ try.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
615 .event = .{ .issue = .{ 616 .title = sync_issue.title, 617 .description = "The sync engine's coverage is unit tests only, with every transport mocked out." ++ "\n\n" ++ 618 sync_p2 ++ "\n\n" ++ sync_p3, 619 .tags = sync_issue.tags, 620 } }, 621 }, .{ 622 .id = issue_events[issue_data.len - 2].id, 623 .timestamp = 102, 624 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 625 .event = .{ .issue = .{ 626 .title = desc_issue.title, 627 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ tz_p4 ++ "\n\n" ++ 628 "The confusion is worst for teams spread across timezones, who each read a different wall-clock time from the same feed.", 629 .tags = desc_issue.tags, 630 } }, 631 } }; 632 const theirs = [_]evt.EventWithId{ .{ 633 .id = issue_events[issue_data.len - 4].id, 634 .timestamp = 103, 635 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 636 .event = .{ .issue = .{ 637 .title = "Segfault on early window resize", 638 .description = title_issue.description, 639 .tags = "bug crash rendering", 640 } }, 641 }, .{ 642 .id = issue_events[issue_data.len - 3].id, 643 .timestamp = 104, 644 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 645 .event = .{ .issue = .{ 646 .title = sync_issue.title, 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 734 // copy the template to each repo's on-disk location, named by its 735 // hex-encoded event id. the template repo is deinitialized above, so its 736 // db file is fully written before we copy it. 737 { 738 var template_dir = try cwd.openDir(io, template_path, .{ .iterate = true }); 739 defer template_dir.close(io); 740 741 for (repo_event_ids) |id_bytes| { 742 const repo_id = std.fmt.bytesToHex(id_bytes, .lower); 743 const repo_path = try std.fs.path.join(arena.allocator(), &.{ server_path, "repos", &repo_id }); 744 var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); 745 defer dest_dir.close(io); 746 try copyDir(io, template_dir, dest_dir); 747 } 748 } 749 750 break :blk try ui.Session.init(&session_arena, &repo, .{ .user_id = &admin_user_id }); 751 }; 752 session.is_terminal = true; 753 754 // start the server 755 756 // let the native TUI's page builders open the on-disk repos for the file tree 757 session.io = io; 758 session.repos_dir = try std.fs.path.join(session_arena.allocator(), &.{ server_path, "repos" }); 759 760 // leave a one-shot session for the first browser to hit the web ui, so it 761 // starts logged in as admin 762 { 763 var data_dir = try cwd.createDirPathOpen(io, server_path, .{}); 764 defer data_dir.close(io); 765 const store = try hx.web.SessionStore.init(io, data_dir); 766 defer store.deinit(); 767 const token = try store.create(&admin_user_id); 768 try store.offerAutoLogin(&token); 769 } 770 771 if (cli) { 772 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 773 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 774 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; 775 776 const key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 777 defer allocator.free(key_path); 778 779 const Runnable = struct { 780 io: std.Io, 781 key_path: []const u8, 782 783 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port_maybe: ?u16) !void { 784 _ = web_port; 785 _ = http_port; 786 const ssh_port = ssh_port_maybe orelse return error.SshDisabled; 787 std.debug.print( 788 \\ 789 \\connect to the TUI with: 790 \\ ssh -p {d} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost 791 \\ 792 \\create a git repo and push over SSH: 793 \\ mkdir -p temp-try/client/test 794 \\ cd temp-try/client/test 795 \\ git init 796 \\ echo "hello" > hello.txt 797 \\ git add hello.txt 798 \\ git commit -m "let there be light" 799 \\ 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 800 \\ 801 \\to quit, press enter. 802 \\ 803 , .{ ssh_port, ssh_port, self.key_path }); 804 // portable stdin read via std.Io — equivalent to the 805 // std.posix.read(STDIN_FILENO, ...) that doesn't compile 806 // on windows. 807 var buf: [1]u8 = undefined; 808 var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); 809 _ = stdin_reader.interface.takeByte() catch {}; 810 } 811 }; 812 813 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .key_path = key_path }); 814 } else { 815 var null_writer = std.Io.Writer.Discarding.init(&.{}); 816 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; 817 818 const Runnable = struct { 819 io: std.Io, 820 allocator: std.mem.Allocator, 821 session: *ui.Session, 822 repo: *Repo, 823 824 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port: ?u16) !void { 825 // launch the TUI; the footer points at whatever port was bound 826 self.session.web_port = web_port; 827 self.session.data.clone_http_port = http_port; 828 self.session.data.clone_ssh_port = ssh_port; 829 try hx.ui.run(self.io, self.allocator, self.session, self.repo); 830 } 831 }; 832 833 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .allocator = allocator, .session = &session, .repo = &repo }); 834 } 835 } 836 837 // words cycled through to pad generated lines into prose-like content 838 const scatter_words = [_][]const u8{ 839 "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", 840 "quantum", "vector", "matrix", "buffer", "kernel", "socket", 841 "falcon", "otter", "badger", "walrus", "ferret", "marmot", 842 "scatter", "gather", "encode", "decode", "render", "commit", 843 }; 844 845 // write `path` as 40 lines of mostly-stable filler. revision `c` and file 846 // index `fi` shift which lines change and the words used, so each (c, fi) 847 // yields a distinct multi-hunk diff against the previous revision. untouched 848 // lines stay byte-identical across revisions, so a bump in `c` is a scatter of 849 // small hunks rather than a full rewrite. 850 fn writeScatterFile(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir, path: []const u8, fi: usize, c: usize) !void { 851 const file = try repo_dir.createFile(io, path, .{}); 852 defer file.close(io); 853 var writer = std.Io.Writer.Allocating.init(allocator); 854 defer writer.deinit(); 855 for (0..40) |line| { 856 const start = writer.written().len; 857 if ((line + fi + c) % 8 == 0) { 858 const tag = scatter_words[(line + fi + c * 3) % scatter_words.len]; 859 try writer.writer.print("rev {d} {s}", .{ c, tag }); 860 } 861 const target = 40 + (line * 17 + fi * 23) % 70; 862 var w = line + fi; 863 while (true) : (w += 1) { 864 const word = scatter_words[w % scatter_words.len]; 865 const len = writer.written().len - start; 866 if (len >= target or len + 1 + word.len > 120) break; 867 if (len == 0) 868 try writer.writer.print("{s}", .{word}) 869 else 870 try writer.writer.print(" {s}", .{word}); 871 } 872 try writer.writer.writeByte('\n'); 873 } 874 try file.writeStreamingAll(io, writer.written()); 875 } 876 877 // tag the current HEAD as the next sequential version: v1, v2, and so on 878 fn addNextTag(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void { 879 var buf: [16]u8 = undefined; 880 const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); 881 _ = try repo.addTag(io, allocator, .{ .name = name }); 882 n.* += 1; 883 } 884 885 // recursively copy the contents of src_dir into dest_dir 886 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 887 var iter = src_dir.iterate(); 888 while (try iter.next(io)) |entry| { 889 switch (entry.kind) { 890 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 891 .directory => { 892 try dest_dir.createDirPath(io, entry.name); 893 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 894 defer dest_entry_dir.close(io); 895 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 896 defer src_entry_dir.close(io); 897 try copyDir(io, src_entry_dir, dest_entry_dir); 898 }, 899 else => {}, 900 } 901 } 902 } 903