╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ try.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
554 .event = .{ 555 .issue = .{ 556 .title = issue.title, 557 .description = description, 558 .tags = issue.tags, 559 .status = issue.status, 560 }, 561 }, 562 }; 563 } 564 try evt.consume(.xit, .{}, io, allocator, &template_repo, evt.events_ref, &issue_events); 565 // the tip is the branch point for the conflicting edits below 566 const seed_tip = (try template_repo.readRef(io, evt.events_ref)) orelse unreachable; 567 568 // two divergent edits per conflicted issue: ours on the events 569 // branch, theirs on a temp branch rooted at the seed tip, then a 570 // merge. the 4th-newest issue conflicts on title and tags; the 571 // 3rd-newest on its description with every hunk auto-resolving 572 // (each side reworks a different paragraph); the 2nd-newest on 573 // its description with a deletion conflict (ours removes a 574 // paragraph theirs rewords) and an insertion conflict (each side 575 // appends a different closing paragraph). 576 { 577 const other_ref: xit.ref.Ref = .{ .kind = .head, .name = "haxy/other" }; 578 const title_issue = issue_data[issue_data.len - 4]; 579 const sync_issue = issue_data[issue_data.len - 3]; 580 const desc_issue = issue_data[issue_data.len - 2]; 581 582 const ours = [_]evt.EventWithId{ .{ 583 .id = issue_events[issue_data.len - 4].id, 584 .timestamp = 100, 585 .author_email = user_data[1].email, 586 .event = .{ .issue = .{ 587 .title = "Crash when resizing the window during the splash animation", 588 .description = title_issue.description, 589 .tags = "bug crash ui priority-high", 590 } }, 591 }, .{ 592 .id = issue_events[issue_data.len - 3].id, 593 .timestamp = 101, 594 .author_email = user_data[1].email, 595 .event = .{ .issue = .{ 596 .title = sync_issue.title, 597 .description = "The sync engine's coverage is unit tests only, with every transport mocked out." ++ "\n\n" ++ 598 sync_p2 ++ "\n\n" ++ sync_p3, 599 .tags = sync_issue.tags, 600 } }, 601 }, .{ 602 .id = issue_events[issue_data.len - 2].id, 603 .timestamp = 102, 604 .author_email = user_data[1].email, 605 .event = .{ .issue = .{ 606 .title = desc_issue.title, 607 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ tz_p4 ++ "\n\n" ++ 608 "The confusion is worst for teams spread across timezones, who each read a different wall-clock time from the same feed.", 609 .tags = desc_issue.tags, 610 } }, 611 } }; 612 const theirs = [_]evt.EventWithId{ .{ 613 .id = issue_events[issue_data.len - 4].id, 614 .timestamp = 103, 615 .author_email = user_data[2].email, 616 .event = .{ .issue = .{ 617 .title = "Segfault on early window resize", 618 .description = title_issue.description, 619 .tags = "bug crash rendering", 620 } }, 621 }, .{ 622 .id = issue_events[issue_data.len - 3].id, 623 .timestamp = 104, 624 .author_email = user_data[2].email, 625 .event = .{ .issue = .{ 626 .title = sync_issue.title, 627 .description = sync_p1 ++ "\n\n" ++ sync_p2 ++ "\n\n" ++ 628 "Add end-to-end tests that drive two live instances against a local server on every CI run.", 629 .tags = sync_issue.tags, 630 } }, 631 }, .{ 632 .id = issue_events[issue_data.len - 2].id, 633 .timestamp = 105, 634 .author_email = user_data[2].email, 635 .event = .{ .issue = .{ 636 .title = desc_issue.title, 637 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ 638 "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" ++ 639 "Log exports inherit the same UTC rendering, so downstream tooling has to guess the source timezone.", 640 .tags = desc_issue.tags, 641 } }, 642 } }; 643 644 try evt.consume(.xit, .{}, io, allocator, &template_repo, evt.events_ref, &ours); 645 646 // consume can't root a new branch, so theirs commits by hand 647 var json: std.Io.Writer.Allocating = .init(allocator); 648 defer json.deinit(); 649 for (theirs, 0..) |event, i| { 650 json.clearRetainingCapacity(); 651 try std.json.Stringify.value(event, .{}, &json.writer); 652 const author = try std.fmt.allocPrint(allocator, "haxy <{s}>", .{event.author_email}); 653 defer allocator.free(author); 654 _ = try template_repo.commitAtRef(io, allocator, .{ 655 .author = author, 656 .message = json.written(), 657 .timestamp = event.timestamp, 658 // root the branch at the shared seed tip 659 .parent_oids = if (i == 0) &.{seed_tip} else null, 660 }, null, other_ref); 661 } 662 663 { 664 var to_events = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 665 defer to_events.deinit(); 666 } 667 { 668 var merge = try template_repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, null); 669 defer merge.deinit(); 670 if (merge.result != .success) return error.MergeFailed; 671 } 672 { 673 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 674 defer to_master.deinit(); 675 } 676 try template_repo.removeBranch(io, .{ .name = other_ref.name }); 677 } 678 679 try evt.consume(.xit, .{}, io, allocator, &template_repo, evt.events_ref, &.{}); 680 681 // seed a small comment tree on the second-newest issue 682 var comment_ids: [5][evt.event_id_size]u8 = undefined; 683 for (&comment_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 684 685 const comment_bodies = [_][]const u8{ 686 "I can take this. The compatibility scanner already reports which repositories still use the legacy format.", 687 "Can we keep loading legacy plugins for one release after the warning lands? That would give downstream maintainers time to migrate.", 688 "That sounds good. I'll include the scanner output in the warning so each affected plugin is named.", 689 "Please include the replacement manifest path too. That should make the warning actionable without opening the migration guide.", 690 "I'll add release-note text once the warning format is settled.", 691 }; 692 693 var comment_events: [comment_ids.len]evt.EventWithId = undefined; 694 for (&comment_events, 0..) |*event, i| { 695 event.* = .{ 696 .id = std.fmt.bytesToHex(comment_ids[i], .lower), 697 .timestamp = @intCast(200 + i), 698 .author_email = user_data[(i + 1) % user_data.len].email, 699 .event = .{ .comment = .{ 700 .thread_id = issue_events[issue_events.len - 2].id, 701 .parent_id = switch (i) { 702 0, 1 => issue_events[issue_events.len - 2].id, 703 2, 4 => std.fmt.bytesToHex(comment_ids[0], .lower), 704 3 => std.fmt.bytesToHex(comment_ids[2], .lower), 705 else => unreachable, 706 }, 707 .body = comment_bodies[i], 708 } }, 709 }; 710 } 711 try evt.consume(.xit, .{}, io, allocator, &template_repo, evt.events_ref, &comment_events); 712 } 713 714 // copy the template to each repo's on-disk location, named by its 715 // hex-encoded event id. the template repo is deinitialized above, so its 716 // db file is fully written before we copy it. 717 { 718 var template_dir = try cwd.openDir(io, template_path, .{ .iterate = true }); 719 defer template_dir.close(io); 720 721 for (repo_event_ids) |id_bytes| { 722 const repo_id = std.fmt.bytesToHex(id_bytes, .lower); 723 const repo_path = try std.fs.path.join(arena.allocator(), &.{ cwd_path, temp_dir_name, "server", "repos", &repo_id }); 724 var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); 725 defer dest_dir.close(io); 726 try copyDir(io, template_dir, dest_dir); 727 } 728 } 729 730 break :blk try ui.Session.init(&session_arena, &repo, .{ .user_id = &admin_user_id }); 731 }; 732 session.is_terminal = true; 733 734 // start the server 735 736 var cli = false; 737 738 var arg_it = try init.minimal.args.iterateAllocator(allocator); 739 defer arg_it.deinit(); 740 _ = arg_it.skip(); 741 while (arg_it.next()) |arg| { 742 if (std.mem.eql(u8, "--cli", arg)) { 743 cli = true; 744 } 745 } 746 747 const server_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "server" }); 748 defer allocator.free(server_path); 749 750 // let the native TUI's page builders open the on-disk repos for the file tree 751 session.io = io; 752 session.repos_dir = try std.fs.path.join(session_arena.allocator(), &.{ server_path, "repos" }); 753 754 // leave a one-shot session for the first browser to hit the web ui, so it 755 // starts logged in as admin 756 { 757 var data_dir = try cwd.createDirPathOpen(io, server_path, .{}); 758 defer data_dir.close(io); 759 const store = try hx.web.SessionStore.init(io, data_dir); 760 defer store.deinit(); 761 const token = try store.create(&admin_user_id); 762 try store.offerAutoLogin(&token); 763 } 764 765 if (cli) { 766 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 767 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 768 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; 769 770 const key_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name, "key" }); 771 defer allocator.free(key_path); 772 773 const Runnable = struct { 774 io: std.Io, 775 key_path: []const u8, 776 777 pub fn run(self: @This(), web_port: u16, ssh_port: u16) !void { 778 _ = web_port; 779 std.debug.print( 780 \\ 781 \\connect to the TUI with: 782 \\ ssh -p {d} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost 783 \\ 784 \\create a git repo and push over SSH: 785 \\ mkdir -p temp-try/client/test 786 \\ cd temp-try/client/test 787 \\ git init 788 \\ echo "hello" > hello.txt 789 \\ git add hello.txt 790 \\ git commit -m "let there be light" 791 \\ 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 792 \\ 793 \\to quit, press enter. 794 \\ 795 , .{ ssh_port, ssh_port, self.key_path }); 796 // portable stdin read via std.Io — equivalent to the 797 // std.posix.read(STDIN_FILENO, ...) that doesn't compile 798 // on windows. 799 var buf: [1]u8 = undefined; 800 var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); 801 _ = stdin_reader.interface.takeByte() catch {}; 802 } 803 }; 804 805 try srv.run(.xit, .{}, io, allocator, cwd_path, .{ 806 .data_dir = server_path, 807 }, run_opts.err, Runnable{ .io = io, .key_path = key_path }); 808 } else { 809 var null_writer = std.Io.Writer.Discarding.init(&.{}); 810 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; 811 812 const Runnable = struct { 813 io: std.Io, 814 allocator: std.mem.Allocator, 815 session: *ui.Session, 816 repo: *Repo, 817 818 pub fn run(self: @This(), web_port: u16, ssh_port: u16) !void { 819 _ = ssh_port; 820 // launch the TUI; the footer points at whatever port was bound 821 self.session.web_port = web_port; 822 try hx.ui.run(self.io, self.allocator, self.session, self.repo); 823 } 824 }; 825 826 try srv.run(.xit, .{}, io, allocator, cwd_path, .{ 827 .data_dir = server_path, 828 }, run_opts.err, Runnable{ .io = io, .allocator = allocator, .session = &session, .repo = &repo }); 829 } 830 } 831 832 // words cycled through to pad generated lines into prose-like content 833 const scatter_words = [_][]const u8{ 834 "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", 835 "quantum", "vector", "matrix", "buffer", "kernel", "socket", 836 "falcon", "otter", "badger", "walrus", "ferret", "marmot", 837 "scatter", "gather", "encode", "decode", "render", "commit", 838 }; 839 840 // write `path` as 40 lines of mostly-stable filler. revision `c` and file 841 // index `fi` shift which lines change and the words used, so each (c, fi) 842 // yields a distinct multi-hunk diff against the previous revision. untouched 843 // lines stay byte-identical across revisions, so a bump in `c` is a scatter of 844 // small hunks rather than a full rewrite. 845 fn writeScatterFile(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir, path: []const u8, fi: usize, c: usize) !void { 846 const file = try repo_dir.createFile(io, path, .{}); 847 defer file.close(io); 848 var writer = std.Io.Writer.Allocating.init(allocator); 849 defer writer.deinit(); 850 for (0..40) |line| { 851 const start = writer.written().len; 852 if ((line + fi + c) % 8 == 0) { 853 const tag = scatter_words[(line + fi + c * 3) % scatter_words.len]; 854 try writer.writer.print("rev {d} {s}", .{ c, tag }); 855 } 856 const target = 40 + (line * 17 + fi * 23) % 70; 857 var w = line + fi; 858 while (true) : (w += 1) { 859 const word = scatter_words[w % scatter_words.len]; 860 const len = writer.written().len - start; 861 if (len >= target or len + 1 + word.len > 120) break; 862 if (len == 0) 863 try writer.writer.print("{s}", .{word}) 864 else 865 try writer.writer.print(" {s}", .{word}); 866 } 867 try writer.writer.writeByte('\n'); 868 } 869 try file.writeStreamingAll(io, writer.written()); 870 } 871 872 // tag the current HEAD as the next sequential version: v1, v2, and so on 873 fn addNextTag(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void { 874 var buf: [16]u8 = undefined; 875 const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); 876 _ = try repo.addTag(io, allocator, .{ .name = name }); 877 n.* += 1; 878 } 879 880 // recursively copy the contents of src_dir into dest_dir 881 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 882 var iter = src_dir.iterate(); 883 while (try iter.next(io)) |entry| { 884 switch (entry.kind) { 885 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 886 .directory => { 887 try dest_dir.createDirPath(io, entry.name); 888 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 889 defer dest_entry_dir.close(io); 890 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 891 defer src_entry_dir.close(io); 892 try copyDir(io, src_entry_dir, dest_entry_dir); 893 }, 894 else => {}, 895 } 896 } 897 } 898