╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ try.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
563 .tags = "bug crash ui", 564 }, 565 .{ 566 .title = "Add integration tests for the sync engine", 567 .description = sync_p1 ++ "\n\n" ++ sync_p2 ++ "\n\n" ++ sync_p3, 568 .tags = "testing sync", 569 }, 570 .{ 571 .title = "Timestamps display in UTC instead of local time", 572 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ tz_p3 ++ "\n\n" ++ tz_p4, 573 .tags = "bug i18n time", 574 }, 575 .{ 576 .title = "Deprecate the legacy plugin format", 577 .description = "Both plugin formats are currently loaded, doubling the maintenance surface. Warn on legacy plugins this release and drop support in the next.", 578 .tags = "maintenance plugins", 579 }, 580 }; 581 582 var issue_events: [issue_data.len]evt.EventWithId = undefined; 583 for (issue_data, 0..) |issue, i| { 584 // the fifth-newest issue's description runs past what the detail 585 // pane shows, so it shows the truncated description and its 586 // link. 587 const description = if (i == issue_data.len - 5) 588 try longDescription(arena.allocator(), issue.description) 589 else 590 issue.description; 591 issue_events[i] = .{ 592 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 593 // stepped timestamps so the issues list in a stable order 594 .timestamp = @intCast(i + 1), 595 .author = .{ .name = user_data[i % user_data.len].name, .email = user_data[i % user_data.len].email }, 596 .event = .{ 597 .issue = .{ 598 .title = issue.title, 599 .description = description, 600 .tags = issue.tags, 601 .status = issue.status, 602 }, 603 }, 604 }; 605 } 606 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &issue_events); 607 // the tip is the branch point for the conflicting edits below 608 const seed_tip = (try template_repo.readRef(io, evt.events_ref)) orelse unreachable; 609 610 // two divergent edits per conflicted issue: ours on the events 611 // branch, theirs on a temp branch rooted at the seed tip, then a 612 // merge. the 4th-newest issue conflicts on title and tags; the 613 // 3rd-newest on its description with every hunk auto-resolving 614 // (each side reworks a different paragraph); the 2nd-newest on 615 // its description with a removal conflict (ours removes a 616 // paragraph theirs rewords) and an insertion conflict (each side 617 // appends a different closing paragraph). 618 { 619 const other_ref: xit.ref.Ref = .{ .kind = .head, .name = "haxy/other" }; 620 const title_issue = issue_data[issue_data.len - 4]; 621 const sync_issue = issue_data[issue_data.len - 3]; 622 const desc_issue = issue_data[issue_data.len - 2]; 623 624 const ours = [_]evt.EventWithId{ .{ 625 .id = issue_events[issue_data.len - 4].id, 626 .timestamp = 100, 627 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 628 .event = .{ .issue = .{ 629 .title = "Crash when resizing the window during the splash animation", 630 .description = title_issue.description, 631 .tags = "bug crash ui priority-high", 632 } }, 633 }, .{ 634 .id = issue_events[issue_data.len - 3].id, 635 .timestamp = 101, 636 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 637 .event = .{ .issue = .{ 638 .title = sync_issue.title, 639 .description = "The sync engine's coverage is unit tests only, with every transport mocked out." ++ "\n\n" ++ 640 sync_p2 ++ "\n\n" ++ sync_p3, 641 .tags = sync_issue.tags, 642 } }, 643 }, .{ 644 .id = issue_events[issue_data.len - 2].id, 645 .timestamp = 102, 646 .author = .{ .name = user_data[1].name, .email = user_data[1].email }, 647 .event = .{ .issue = .{ 648 .title = desc_issue.title, 649 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ tz_p4 ++ "\n\n" ++ 650 "The confusion is worst for teams spread across timezones, who each read a different wall-clock time from the same feed.", 651 .tags = desc_issue.tags, 652 } }, 653 } }; 654 const theirs = [_]evt.EventWithId{ .{ 655 .id = issue_events[issue_data.len - 4].id, 656 .timestamp = 103, 657 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 658 .event = .{ .issue = .{ 659 .title = "Segfault on early window resize", 660 .description = title_issue.description, 661 .tags = "bug crash rendering", 662 } }, 663 }, .{ 664 .id = issue_events[issue_data.len - 3].id, 665 .timestamp = 104, 666 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 667 .event = .{ .issue = .{ 668 .title = sync_issue.title, 669 .description = sync_p1 ++ "\n\n" ++ sync_p2 ++ "\n\n" ++ 670 "Add end-to-end tests that drive two live instances against a local server on every CI run.", 671 .tags = sync_issue.tags, 672 } }, 673 }, .{ 674 .id = issue_events[issue_data.len - 2].id, 675 .timestamp = 105, 676 .author = .{ .name = user_data[2].name, .email = user_data[2].email }, 677 .event = .{ .issue = .{ 678 .title = desc_issue.title, 679 .description = tz_p1 ++ "\n\n" ++ tz_p2 ++ "\n\n" ++ 680 "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" ++ 681 "Log exports inherit the same UTC rendering, so downstream tooling has to guess the source timezone.", 682 .tags = desc_issue.tags, 683 } }, 684 } }; 685 686 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &ours); 687 688 try commitEventsAtRef(io, allocator, &template_repo, other_ref, &theirs, seed_tip); 689 690 { 691 var to_events = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 692 defer to_events.deinit(); 693 } 694 { 695 var merge = try template_repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, null); 696 defer merge.deinit(); 697 if (merge.result != .success) return error.MergeFailed; 698 } 699 { 700 var to_master = try template_repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "master" } } }); 701 defer to_master.deinit(); 702 } 703 try template_repo.removeBranch(io, .{ .name = other_ref.name }); 704 } 705 706 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &.{}); 707 708 // seed a small comment tree on the newest issue 709 var comment_ids: [5][evt.event_id_size]u8 = undefined; 710 for (&comment_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 711 712 const comment_bodies = [_][]const u8{ 713 "I can take this. The compatibility scanner already reports which repositories still use the legacy format.", 714 "Can we keep loading legacy plugins for one release after the warning lands? That would give downstream maintainers time to migrate.", 715 "That sounds good. I'll include the scanner output in the warning so each affected plugin is named.", 716 "Please include the replacement manifest path too. That should make the warning actionable without opening the migration guide.", 717 "I'll add release-note text once the warning format is settled.", 718 }; 719 720 var comment_events: [comment_ids.len]evt.EventWithId = undefined; 721 for (&comment_events, 0..) |*event, i| { 722 event.* = .{ 723 .id = std.fmt.bytesToHex(comment_ids[i], .lower), 724 .timestamp = @intCast(200 + i), 725 .author = .{ .name = user_data[(i + 1) % user_data.len].name, .email = user_data[(i + 1) % user_data.len].email }, 726 .event = .{ .comment = .{ 727 .thread_id = issue_events[issue_events.len - 1].id, 728 .parent_id = switch (i) { 729 0, 1 => issue_events[issue_events.len - 1].id, 730 2, 4 => std.fmt.bytesToHex(comment_ids[0], .lower), 731 3 => std.fmt.bytesToHex(comment_ids[2], .lower), 732 else => unreachable, 733 }, 734 .body = comment_bodies[i], 735 } }, 736 }; 737 } 738 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &comment_events); 739 740 const discussion_data = [_]struct { 741 title: []const u8, 742 description: []const u8, 743 tags: []const u8, 744 }{ 745 .{ 746 .title = "How should plugins declare capabilities?", 747 .description = "I'd like the manifest to make privileged capabilities explicit without making simple plugins verbose.", 748 .tags = "plugins design", 749 }, 750 .{ 751 .title = "Ideas for making large repositories faster", 752 .description = "This is a place to collect profiling results and discuss which indexing work is worth pursuing first.", 753 .tags = "performance indexing", 754 }, 755 .{ 756 .title = "What should the next release focus on?", 757 .description = "Let's compare the most important reliability fixes with the larger features already in progress.", 758 .tags = "release planning", 759 }, 760 .{ 761 .title = "Improving keyboard navigation", 762 .description = "Share workflows that still require a mouse and suggestions for making their focus behavior predictable.", 763 .tags = "ui keyboard accessibility", 764 }, 765 .{ 766 .title = "Configuration format discussion", 767 .description = "Should the next configuration format favor strict validation or accept common conveniences such as trailing commas?", 768 .tags = "config design", 769 }, 770 }; 771 772 var discussion_events: [discussion_data.len]evt.EventWithId = undefined; 773 for (discussion_data, 0..) |discussion, i| { 774 discussion_events[i] = .{ 775 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), 776 .timestamp = @intCast(300 + i), 777 .author = .{ .name = user_data[i % user_data.len].name, .email = user_data[i % user_data.len].email }, 778 .event = .{ .discuss = .{ 779 .title = discussion.title, 780 .description = discussion.description, 781 .tags = discussion.tags, 782 } }, 783 }; 784 } 785 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &discussion_events); 786 787 var discussion_comment_ids: [discussion_data.len][3][evt.event_id_size]u8 = undefined; 788 for (&discussion_comment_ids) |*ids| { 789 for (ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 790 } 791 var discussion_comment_events: [discussion_data.len * 3]evt.EventWithId = undefined; 792 for (&discussion_comment_events, 0..) |*event, i| { 793 const discussion_index = i / 3; 794 const comment_index = i % 3; 795 event.* = .{ 796 .id = std.fmt.bytesToHex(discussion_comment_ids[discussion_index][comment_index], .lower), 797 .timestamp = @intCast(400 + i), 798 .author = .{ .name = user_data[(i + 1) % user_data.len].name, .email = user_data[(i + 1) % user_data.len].email }, 799 .event = .{ .comment = .{ 800 .thread_id = discussion_events[discussion_index].id, 801 .parent_id = if (comment_index < 2) 802 discussion_events[discussion_index].id 803 else 804 std.fmt.bytesToHex(discussion_comment_ids[discussion_index][0], .lower), 805 .body = switch (comment_index) { 806 0 => "My preference is to start with the smallest useful version and expand it after we have real usage to learn from.", 807 1 => "It would help to write down the tradeoffs we are accepting so the decision is easy to revisit later.", 808 2 => "Agreed. A narrow first version should also make it easier to keep the implementation understandable.", 809 else => unreachable, 810 }, 811 } }, 812 }; 813 } 814 try evt.consume(.local, .repo, .xit, .{}, io, allocator, &template_repo, evt.events_ref, &discussion_comment_events); 815 try template_repo.patchAll(io, allocator, null); 816 try template_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "true" }); 817 // fixtures commit directly, so index the ref tips by hand 818 try find.refresh(.{}, io, allocator, &template_repo); 819 try srch_cmmt.refresh(.{}, io, allocator, &template_repo, null); 820 // clear history only after indexing has written its transactions. 821 { 822 const moment = try evt.currentMoment(.{}, &template_repo); 823 const roots = try evt.PatchRev.gcRoots(rp.Repo(.xit, .{}).DB, .sha1, allocator, moment); 824 defer allocator.free(roots); 825 _ = try template_repo.garbageCollect(io, allocator, .{ .extra_roots = roots }); 826 } 827 } 828 829 // copy the template to each repo's on-disk location, named by its 830 // hex-encoded event id. the template repo is deinitialized above, so its 831 // db file is fully written before we copy it. 832 { 833 var template_dir = try cwd.openDir(io, template_path, .{}); 834 defer template_dir.close(io); 835 var template_repo_dir = try template_dir.openDir(io, ".xit", .{ .iterate = true }); 836 defer template_repo_dir.close(io); 837 838 for (repo_event_ids, 0..) |id_bytes, repo_index| { 839 const repo_id = std.fmt.bytesToHex(id_bytes, .lower); 840 const repo_path = try std.fs.path.join(arena.allocator(), &.{ server_path, "repos", &repo_id }); 841 { 842 var dest_dir = try cwd.createDirPathOpen(io, repo_path, .{}); 843 defer dest_dir.close(io); 844 var dest_repo_dir = try dest_dir.createDirPathOpen(io, ".xit", .{}); 845 defer dest_repo_dir.close(io); 846 try copyDir(io, template_repo_dir, dest_repo_dir); 847 } 848 849 if (repo_index + 1 == repo_data.len) { 850 var target_repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = repo_path, .require_repo_root = true }); 851 defer target_repo.deinit(io, allocator); 852 try seedPatches(io, allocator, server_path, &repo, &target_repo, &repo_event_ids[repo_index], &admin_user_id, prng.random()); 853 } 854 } 855 } 856 857 break :blk try ui.Session.init(&session_arena, &repo, .{ .user_id = &admin_user_id }); 858 }; 859 session.is_terminal = true; 860 session.data.git_ssh_prefix = git_ssh_prefix; 861 862 // start the server 863 864 // let the native TUI's page builders open the on-disk repos for the file tree 865 session.io = io; 866 session.repos_dir = try std.fs.path.join(session_arena.allocator(), &.{ server_path, "repos" }); 867 868 // leave a one-shot session for the first browser to hit the web ui, so it 869 // starts logged in as admin 870 { 871 var data_dir = try cwd.createDirPathOpen(io, server_path, .{}); 872 defer data_dir.close(io); 873 const store = try hx.web.SessionStore.init(io, data_dir); 874 defer store.deinit(); 875 const token = try store.create(&admin_user_id); 876 try store.offerAutoLogin(&token); 877 } 878 879 if (cli) { 880 var stdout_writer = std.Io.File.stdout().writer(io, &.{}); 881 var stderr_writer = std.Io.File.stderr().writer(io, &.{}); 882 const run_opts = hx.main.RunOpts{ .out = &stdout_writer.interface, .err = &stderr_writer.interface }; 883 884 const Runnable = struct { 885 io: std.Io, 886 key_path: []const u8, 887 888 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port_maybe: ?u16) !void { 889 _ = web_port; 890 _ = http_port; 891 const ssh_port = ssh_port_maybe orelse return error.SshDisabled; 892 std.debug.print( 893 \\ 894 \\connect to the TUI with: 895 \\ ssh -p {d} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null localhost 896 \\ 897 \\create a git repo and push over SSH: 898 \\ mkdir -p temp-try/client/test 899 \\ cd temp-try/client/test 900 \\ git init 901 \\ echo "hello" > hello.txt 902 \\ git add hello.txt 903 \\ git commit -m "let there be light" 904 \\ 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 905 \\ 906 \\to quit, press enter. 907 \\ 908 , .{ ssh_port, ssh_port, self.key_path }); 909 // portable stdin read via std.Io — equivalent to the 910 // std.posix.read(STDIN_FILENO, ...) that doesn't compile 911 // on windows. 912 var buf: [1]u8 = undefined; 913 var stdin_reader = std.Io.File.stdin().reader(self.io, &buf); 914 _ = stdin_reader.interface.takeByte() catch {}; 915 } 916 }; 917 918 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .key_path = key_path }); 919 } else { 920 var null_writer = std.Io.Writer.Discarding.init(&.{}); 921 const run_opts = hx.main.RunOpts{ .out = &null_writer.writer, .err = &null_writer.writer }; 922 923 const Runnable = struct { 924 io: std.Io, 925 allocator: std.mem.Allocator, 926 session: *ui.Session, 927 repo: *Repo, 928 929 pub fn run(self: @This(), web_port: u16, http_port: ?u16, ssh_port: ?u16) !void { 930 // launch the TUI; the footer points at whatever port was bound 931 self.session.web_port = web_port; 932 self.session.data.git_http_port = http_port; 933 self.session.data.git_ssh_port = ssh_port; 934 try hx.ui.run(self.io, self.allocator, self.session, self.repo); 935 } 936 }; 937 938 try srv.run(.xit, .{}, io, allocator, cwd_path, serve_options, run_opts.err, Runnable{ .io = io, .allocator = allocator, .session = &session, .repo = &repo }); 939 } 940 } 941 942 // words cycled through to pad generated lines into prose-like content 943 const scatter_words = [_][]const u8{ 944 "lorem", "ipsum", "dolor", "sit", "amet", "consectetur", 945 "quantum", "vector", "matrix", "buffer", "kernel", "socket", 946 "falcon", "otter", "badger", "walrus", "ferret", "marmot", 947 "scatter", "gather", "encode", "decode", "render", "commit", 948 }; 949 950 fn longDescription(allocator: std.mem.Allocator, initial: []const u8) ![]u8 { 951 var writer = std.Io.Writer.Allocating.init(allocator); 952 defer writer.deinit(); 953 try writer.writer.print("{s}", .{initial}); 954 for (0..ui.detail_preview_lines) |line| 955 try writer.writer.print("\n{d} {s}", .{ line, scatter_words[line % scatter_words.len] }); 956 return writer.toOwnedSlice(); 957 } 958 959 // write `path` as 40 lines of mostly-stable filler. revision `c` and file 960 // index `fi` shift which lines change and the words used, so each (c, fi) 961 // yields a distinct multi-hunk diff against the previous revision. untouched 962 // lines stay byte-identical across revisions, so a bump in `c` is a scatter of 963 // small hunks rather than a full rewrite. 964 fn writeScatterFile(io: std.Io, allocator: std.mem.Allocator, repo_dir: std.Io.Dir, path: []const u8, fi: usize, c: usize) !void { 965 const content = try scatterContent(allocator, fi, c, null); 966 defer allocator.free(content); 967 const file = try repo_dir.createFile(io, path, .{}); 968 defer file.close(io); 969 try file.writeStreamingAll(io, content); 970 } 971 972 const ScatterEdit = struct { line: usize, text: []const u8 }; 973 974 fn scatterContent(allocator: std.mem.Allocator, fi: usize, c: usize, replacement: ?ScatterEdit) ![]u8 { 975 var writer = std.Io.Writer.Allocating.init(allocator); 976 defer writer.deinit(); 977 for (0..40) |line| { 978 if (replacement) |edit| if (line == edit.line) { 979 try writer.writer.print("{s}\n", .{edit.text}); 980 continue; 981 }; 982 const start = writer.written().len; 983 if ((line + fi + c) % 8 == 0) { 984 const tag = scatter_words[(line + fi + c * 3) % scatter_words.len]; 985 try writer.writer.print("rev {d} {s}", .{ c, tag }); 986 } 987 const target = 40 + (line * 17 + fi * 23) % 70; 988 var w = line + fi; 989 while (true) : (w += 1) { 990 const word = scatter_words[w % scatter_words.len]; 991 const len = writer.written().len - start; 992 if (len >= target or len + 1 + word.len > 120) break; 993 if (len == 0) 994 try writer.writer.print("{s}", .{word}) 995 else 996 try writer.writer.print(" {s}", .{word}); 997 } 998 try writer.writer.writeByte('\n'); 999 } 1000 return writer.toOwnedSlice(); 1001 } 1002 1003 // tag the current HEAD with the next three sequential versions, so the refs 1004 // tab has enough tags to page through 1005 fn addNextTags(repo: *rp.Repo(.xit, .{}), io: std.Io, allocator: std.mem.Allocator, n: *usize) !void { 1006 for (0..3) |_| { 1007 var buf: [16]u8 = undefined; 1008 const name = try std.fmt.bufPrint(&buf, "v{d}", .{n.*}); 1009 _ = try repo.addTag(io, allocator, .{ .name = name }); 1010 n.* += 1; 1011 } 1012 } 1013 1014 fn commitTree( 1015 io: std.Io, 1016 allocator: std.mem.Allocator, 1017 repo: *rp.Repo(.xit, .{}), 1018 oid: *const [hash.hexLen(.sha1)]u8, 1019 ) ![hash.hexLen(.sha1)]u8 { 1020 var moment = try repo.core.latestMoment(); 1021 const state = rp.Repo(.xit, .{}).State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; 1022 var object = try obj.Object(.xit, .{}).init(state, io, allocator, oid); 1023 defer object.deinit(); 1024 return switch (object.content) { 1025 .commit => |commit| commit.tree, 1026 else => error.InvalidObject, 1027 }; 1028 } 1029 1030 fn seedPatchRevision( 1031 io: std.Io, 1032 allocator: std.mem.Allocator, 1033 repos_path: []const u8, 1034 patch_id: *const [evt.event_id_size]u8, 1035 title: []const u8, 1036 author: evt.CommitAuthor, 1037 timestamp: u64, 1038 random: std.Random, 1039 base_oid: [hash.hexLen(.sha1)]u8, 1040 path: []const u8, 1041 contents: []const []const u8, 1042 ) !void { 1043 const patch_hex = std.fmt.bytesToHex(patch_id.*, .lower); 1044 const fork_path = try fork.forkPath(allocator, repos_path, &patch_hex); 1045 defer allocator.free(fork_path); 1046 var fork_repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }); 1047 defer fork_repo.deinit(io, allocator); 1048 1049 try fork_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "false" }); 1050 { 1051 var reset = try fork_repo.reset(io, allocator, .{ .target = .{ .oid = &base_oid } }); 1052 defer reset.deinit(); 1053 } 1054 const base_tree_oid = try commitTree(io, allocator, &fork_repo, &base_oid); 1055 var source_oid = base_oid; 1056 var fork_dir = try std.Io.Dir.cwd().openDir(io, fork_path, .{}); 1057 defer fork_dir.close(io); 1058 var writer = std.Io.Writer.Allocating.init(allocator); 1059 defer writer.deinit(); 1060 if (std.fs.path.dirname(path)) |dir| try fork_dir.createDirPath(io, dir); 1061 for (contents, 0..) |content, i| { 1062 { 1063 const file = try fork_dir.createFile(io, path, .{}); 1064 defer file.close(io); 1065 try file.writeStreamingAll(io, content); 1066 } 1067 try fork_repo.add(io, allocator, &.{path}); 1068 writer.clearRetainingCapacity(); 1069 try writer.writer.print("{s} ({d}/{d})", .{ title, i + 1, contents.len }); 1070 source_oid = try fork_repo.commit(io, allocator, .{ .message = writer.written(), .timestamp = timestamp + i }); 1071 } 1072 try fork_repo.patchAll(io, allocator, null); 1073 try fork_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "true" }); 1074 // the commits above skipped the push path, so index the patch branch by hand 1075 try find.refresh(.{}, io, allocator, &fork_repo); 1076 _ = try fork_repo.garbageCollect(io, allocator, .{}); 1077 try fork_dir.deleteFile(io, path); 1078 const revision_id = evt.EventWithId.randomId(random); 1079 const head_tree_oid = try commitTree(io, allocator, &fork_repo, &source_oid); 1080 const revision: evt.PatchRev = .{ 1081 .base_oid = &base_oid, 1082 .source_oid = &source_oid, 1083 .message = title, 1084 }; 1085 const revision_timestamp = timestamp + 3; 1086 const tree_entries = [_]evt.EventTreeEntry{ 1087 .{ .tree = .{ .name = "base", .oid = &base_tree_oid } }, 1088 .{ .tree = .{ .name = "head", .oid = &head_tree_oid } }, 1089 }; 1090 try evt.consume(.server, .fork, .xit, .{}, io, allocator, &fork_repo, evt.events_ref, &.{.{ 1091 .id = std.fmt.bytesToHex(revision_id, .lower), 1092 .timestamp = revision_timestamp, 1093 .author = author, 1094 .tree_entries = &tree_entries, 1095 .event = .{ .patchrev = revision }, 1096 }}); 1097 1098 var arena = std.heap.ArenaAllocator.init(allocator); 1099 defer arena.deinit(); 1100 const moment = try evt.currentMoment(.{}, &fork_repo); 1101 const revision_record = (try evt.PatchRev.readById(evt.EventDB(.sha1), .sha1, moment, &arena, &revision_id)) orelse return error.NotFound; 1102 const patch_record = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &arena, patch_id)) orelse return error.NotFound; 1103 var patch = patch_record.event; 1104 patch.revision = evt.Patch.Revision.fromRecord(revision_id, revision_record); 1105 try evt.consume(.server, .fork, .xit, .{}, io, allocator, &fork_repo, evt.events_ref, &.{.{ 1106 .id = patch_hex, 1107 .timestamp = revision_timestamp + 1, 1108 .author = author, 1109 .event = .{ .patch = patch }, 1110 }}); 1111 } 1112 1113 fn seedPatches( 1114 io: std.Io, 1115 allocator: std.mem.Allocator, 1116 server_path: []const u8, 1117 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), 1118 target_repo: *rp.Repo(.xit, .{}), 1119 repo_id: *const [evt.event_id_size]u8, 1120 user_id: *const [evt.event_id_size]u8, 1121 random: std.Random, 1122 ) !void { 1123 const patch_data = [_]struct { 1124 title: []const u8, 1125 description: []const u8, 1126 tags: []const u8, 1127 status: ?evt.Patch.StatusKind, 1128 alpha_edit: ?ScatterEdit = null, 1129 // based behind master, so merging it makes a merge commit 1130 behind_master: bool = false, 1131 }{ 1132 .{ 1133 .title = "Draft a faster dependency scanner", 1134 .description = "Rework dependency discovery so a large workspace can be scanned without repeatedly opening the same manifests.", 1135 .tags = "performance build", 1136 .status = null, 1137 }, 1138 .{ 1139 .title = "Remove the legacy configuration loader", 1140 .description = "Delete the compatibility loader now that the replacement format has shipped and the migration warning has been available for a full release.", 1141 .tags = "cleanup config", 1142 .status = .merged, 1143 .behind_master = true, 1144 }, 1145 .{ 1146 .title = "Cache parsed manifests between commands", 1147 .description = "Keep parsed manifests in the command context so consecutive operations do not repeat identical filesystem and parsing work.", 1148 .tags = "performance cache", 1149 .status = .closed, 1150 }, 1151 .{ 1152 .title = "Add structured output to the inspect command", 1153 .description = "Add a stable JSON representation of inspect results for scripts and editor integrations.", 1154 .tags = "enhancement cli", 1155 .status = .open, 1156 }, 1157 .{ 1158 .title = "Edit an adjacent line in alpha.txt", 1159 .description = "This patch can be cleanly merged by haxy, while git throws a merge conflict!", 1160 .tags = "merge", 1161 .status = .open, 1162 .alpha_edit = .{ .line = 2, .text = "adjust alpha beside the latest scatter edit" }, 1163 }, 1164 .{ 1165 .title = "Edit a conflicting line in alpha.txt", 1166 .description = "Both source and squash merges conflict with a different edit to the same line on master.", 1167 .tags = "merge", 1168 .status = .open, 1169 .alpha_edit = .{ .line = 3, .text = "replace alpha with a conflicting edit" }, 1170 }, 1171 }; 1172 1173 const repos_path = try std.fs.path.join(allocator, &.{ server_path, "repos" }); 1174 defer allocator.free(repos_path); 1175 const patch_author = evt.CommitAuthor{ .name = "admin", .email = "admin@example.test" }; 1176 var patch_ids: [patch_data.len][evt.event_id_size]u8 = undefined; 1177 for (patch_data, 0..) |patch, i| { 1178 const timestamp: u64 = @intCast(500 + i * 10); 1179 patch_ids[i] = evt.EventWithId.randomId(random); 1180 const patch_hex = std.fmt.bytesToHex(patch_ids[i], .lower); 1181 const path = try fork.create(.{}, io, allocator, repos_path, admin_repo, .{ 1182 .id = patch_hex, 1183 .user_id = user_id.*, 1184 .repo_id = repo_id.*, 1185 .title = patch.title, 1186 .description = patch.description, 1187 .tags = patch.tags, 1188 .target_branch = "master", 1189 .author = patch_author, 1190 .timestamp = timestamp, 1191 }); 1192 allocator.free(path); 1193 1194 // prepare the patch's base and file contents, then seed its commits and revision 1195 { 1196 var arena = std.heap.ArenaAllocator.init(allocator); 1197 defer arena.deinit(); 1198 const aa = arena.allocator(); 1199 var base_oid = (try target_repo.readRef(io, .{ .kind = .head, .name = "master" })) orelse return error.NotFound; 1200 if (patch.alpha_edit != null or patch.behind_master) { 1201 // v30 precedes the last scatter commit, even after the sample merge 1202 const tag_oid = (try target_repo.readRef(io, .{ .kind = .tag, .name = "v30" })) orelse return error.NotFound; 1203 var moment = try target_repo.core.latestMoment(); 1204 const state = rp.Repo(.xit, .{}).State(.read_only){ .core = &target_repo.core, .extra = .{ .moment = &moment } }; 1205 var tag = try obj.Object(.xit, .{}).init(state, io, allocator, &tag_oid); 1206 defer tag.deinit(); 1207 base_oid = switch (tag.content) { 1208 .tag => |value| value.target, 1209 else => return error.InvalidObject, 1210 }; 1211 } 1212 var contents: [3][]const u8 = undefined; 1213 const file_path: []const u8, const count: usize = if (patch.alpha_edit) |edit| blk: { 1214 // rev 29 first changes line 4; edit that line or the one above it 1215 contents[0] = try scatterContent(aa, 0, 28, edit); 1216 break :blk .{ "src/alpha.txt", 1 }; 1217 } else blk: { 1218 var writer = std.Io.Writer.Allocating.init(allocator); 1219 defer writer.deinit(); 1220 for (&contents, 0..) |*content, part| { 1221 try writer.writer.print("{s}: part {d}\n", .{ patch.title, part + 1 }); 1222 content.* = try aa.dupe(u8, writer.written()); 1223 } 1224 break :blk .{ "patch.txt", contents.len }; 1225 }; 1226 try seedPatchRevision(io, allocator, repos_path, &patch_ids[i], patch.title, patch_author, timestamp + 1, random, base_oid, file_path, contents[0..count]); 1227 } 1228 const status = patch.status orelse continue; 1229 const fork_path = try fork.forkPath(allocator, repos_path, &patch_hex); 1230 defer allocator.free(fork_path); 1231 try pch.publish(.{}, io, allocator, admin_repo, target_repo, fork_path, .{ 1232 .id = patch_hex, 1233 .user_id = user_id.*, 1234 .repo_id = repo_id.*, 1235 .author = patch_author, 1236 .timestamp = timestamp + 6, 1237 }); 1238 1239 if (status == .merged) { 1240 try pch.mergeAndRemoveFork(.{}, io, allocator, repos_path, admin_repo, target_repo, .{ 1241 .id = patch_hex, 1242 .revision = .source, 1243 .author = patch_author, 1244 .timestamp = timestamp + 7, 1245 }); 1246 } else if (status != .open) { 1247 try evt.Patch.update(.server, .xit, .{}, io, allocator, target_repo, &patch_ids[i], .{ .status = status }, patch_author); 1248 } 1249 } 1250 1251 // seed a small comment tree on the newest patch 1252 var comment_ids: [5][evt.event_id_size]u8 = undefined; 1253 for (&comment_ids) |*id| id.* = evt.EventWithId.randomId(random); 1254 const comment_bodies = [_][]const u8{ 1255 "This also needs to preserve the executable bit for files nested inside generated directories.", 1256 "I tested the archive path and the direct-copy path; both currently lose the mode.", 1257 "The archive writer is the right place to centralize it, since every exporter already goes through that layer.", 1258 "Please include a regression fixture with a mix of executable and ordinary files.", 1259 "I'll add that fixture and cover both supported archive formats.", 1260 }; 1261 var comments: [comment_ids.len]evt.EventWithId = undefined; 1262 for (&comments, 0..) |*comment, i| { 1263 comment.* = .{ 1264 .id = std.fmt.bytesToHex(comment_ids[i], .lower), 1265 .timestamp = @intCast(700 + i), 1266 .author = .{ .name = if (i % 2 == 0) "alice" else "bob", .email = if (i % 2 == 0) "alice@example.test" else "bob@example.test" }, 1267 .event = .{ .comment = .{ 1268 .thread_id = std.fmt.bytesToHex(patch_ids[patch_ids.len - 1], .lower), 1269 .parent_id = switch (i) { 1270 0, 1 => std.fmt.bytesToHex(patch_ids[patch_ids.len - 1], .lower), 1271 2, 4 => std.fmt.bytesToHex(comment_ids[0], .lower), 1272 3 => std.fmt.bytesToHex(comment_ids[2], .lower), 1273 else => unreachable, 1274 }, 1275 .body = comment_bodies[i], 1276 } }, 1277 }; 1278 } 1279 try evt.consume(.server, .repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &comments); 1280 1281 // create divergent metadata edits on the next three patches 1282 var patch_arena = std.heap.ArenaAllocator.init(allocator); 1283 defer patch_arena.deinit(); 1284 const moment = try evt.currentMoment(.{}, target_repo); 1285 var values: [3]evt.Patch = undefined; 1286 for (1..4) |i| { 1287 const record = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, &patch_arena, &patch_ids[i])) orelse return error.NotFound; 1288 values[i - 1] = record.event; 1289 } 1290 var ours_values = values; 1291 ours_values[0].title = "Drop the legacy configuration loader"; 1292 ours_values[0].tags = "cleanup config breaking"; 1293 const closed_description = try longDescription( 1294 patch_arena.allocator(), 1295 "Cache parsed manifests for the lifetime of a command invocation and invalidate entries when their files change.", 1296 ); 1297 ours_values[1].description = closed_description; 1298 ours_values[2].description = "Add JSON output to inspect with versioned field names and deterministic object ordering."; 1299 var theirs_values = values; 1300 theirs_values[0].title = "Delete compatibility configuration support"; 1301 theirs_values[0].tags = "config maintenance"; 1302 theirs_values[1].description = "Keep a process-wide manifest cache shared by every command and refresh it after writes."; 1303 theirs_values[2].description = "Expose inspect results as newline-delimited JSON so callers can stream large repositories."; 1304 1305 const seed_tip = (try target_repo.readRef(io, evt.events_ref)) orelse return error.NotFound; 1306 const other_ref = xit.ref.Ref{ .kind = .head, .name = "haxy/patch-other" }; 1307 var ours = [_]evt.EventWithId{ 1308 .{ 1309 .id = std.fmt.bytesToHex(patch_ids[1], .lower), 1310 .timestamp = 800, 1311 .author = .{ .name = "alice", .email = "alice@example.test" }, 1312 .event = .{ .patch = ours_values[0] }, 1313 }, 1314 .{ 1315 .id = std.fmt.bytesToHex(patch_ids[2], .lower), 1316 .timestamp = 801, 1317 .author = .{ .name = "alice", .email = "alice@example.test" }, 1318 .event = .{ .patch = ours_values[1] }, 1319 }, 1320 .{ 1321 .id = std.fmt.bytesToHex(patch_ids[3], .lower), 1322 .timestamp = 802, 1323 .author = .{ .name = "alice", .email = "alice@example.test" }, 1324 .event = .{ .patch = ours_values[2] }, 1325 }, 1326 }; 1327 1328 var theirs = ours; 1329 theirs[0].timestamp = 810; 1330 theirs[0].author = .{ .name = "bob", .email = "bob@example.test" }; 1331 theirs[0].event.patch = theirs_values[0]; 1332 theirs[1].timestamp = 811; 1333 theirs[1].author = .{ .name = "bob", .email = "bob@example.test" }; 1334 theirs[1].event.patch = theirs_values[1]; 1335 theirs[2].timestamp = 812; 1336 theirs[2].author = .{ .name = "bob", .email = "bob@example.test" }; 1337 theirs[2].event.patch = theirs_values[2]; 1338 1339 try evt.consume(.server, .repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &ours); 1340 try commitEventsAtRef(io, allocator, target_repo, other_ref, &theirs, seed_tip); 1341 { 1342 var merge = try target_repo.mergeAtRef(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_ref }} } } }, evt.events_ref, null); 1343 defer merge.deinit(); 1344 if (merge.result != .success) return error.MergeFailed; 1345 } 1346 try target_repo.removeBranch(io, .{ .name = other_ref.name }); 1347 try evt.consume(.server, .repo, .xit, .{}, io, allocator, target_repo, evt.events_ref, &.{}); 1348 try pch.writeBranchPatch(.server, .xit, .{}, io, allocator, target_repo, std.fmt.bytesToHex(evt.EventWithId.randomId(random), .lower), .{ 1349 .title = "Merge the existing feature branch", 1350 .description = "This patch tracks the feature branch in this repo without creating a fork.", 1351 .tags = "feature", 1352 .source_branch = "feature", 1353 .target_branch = "master", 1354 }, null, patch_author); 1355 1356 // a push onto the branch that patch tracks, so the undo tab shows the 1357 // action and the patch refresh it sets off 1358 try seedPush(io, allocator, target_repo, patch_author); 1359 try pch.refreshBranches(.server, .xit, .{}, io, allocator, target_repo, null, null); 1360 } 1361 1362 // stand in for a client push, which the fixture has no server to receive: one 1363 // transaction moves the branch, refreshes the patches tracking it and records 1364 // the action, the way receive-pack does 1365 fn seedPush(io: std.Io, allocator: std.mem.Allocator, repo: *rp.Repo(.xit, .{}), author: evt.CommitAuthor) !void { 1366 const Repo = rp.Repo(.xit, .{}); 1367 const DB = Repo.DB; 1368 const Ctx = struct { 1369 core: *Repo.Core, 1370 io: std.Io, 1371 allocator: std.mem.Allocator, 1372 author: evt.CommitAuthor, 1373 1374 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 1375 const opts: rp.RepoOpts(.xit) = .{}; 1376 var moment = try DB.HashMap(.read_write).init(cursor.*); 1377 const state = Repo.State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 1378 var arena = std.heap.ArenaAllocator.init(ctx.allocator); 1379 defer arena.deinit(); 1380 const aa = arena.allocator(); 1381 1382 // a null tree keeps the parent's content, so the branch advances 1383 // without the fixture having to build one 1384 const author_line = try std.fmt.allocPrint(aa, "{s} <{s}>", .{ ctx.author.name, ctx.author.email }); 1385 const feature_old = (try rf.readRecur(.xit, opts, state.readOnly(), ctx.io, .{ .ref = .{ .kind = .head, .name = "feature" } })) orelse return error.NotFound; 1386 const feature_new = try obj.writeCommit(.xit, opts, state, ctx.io, ctx.allocator, .{ 1387 .message = "Revise the feature branch", 1388 .author = author_line, 1389 .timestamp = 820, 1390 }, null, .{ .kind = .head, .name = "feature" }); 1391 1392 // a branch the push creates has no old tip, so its range is open 1393 try bch.add(.xit, opts, state, ctx.io, .{ .name = "docs" }); 1394 const docs_new = (try rf.readRecur(.xit, opts, state.readOnly(), ctx.io, .{ .ref = .{ .kind = .head, .name = "docs" } })) orelse return error.NotFound; 1395 1396 const updates = [_]xit.net_server_receive_pack.AppliedRefUpdate{ 1397 .{ 1398 .ref_name = try aa.dupe(u8, "refs/heads/feature"), 1399 .old_oid = try aa.dupe(u8, &feature_old), 1400 .new_oid = try aa.dupe(u8, &feature_new), 1401 }, 1402 .{ 1403 .ref_name = try aa.dupe(u8, "refs/heads/docs"), 1404 .old_oid = try aa.dupe(u8, "0" ** hash.hexLen(opts.hash)), 1405 .new_oid = try aa.dupe(u8, &docs_new), 1406 }, 1407 }; 1408 1409 _ = try pch.refreshBranchesInTransaction(.server, opts, state, &moment, ctx.io, ctx.allocator, &updates, null, null); 1410 try push.writeUndo(opts, state, ctx.io, ctx.allocator, ctx.author, &updates); 1411 } 1412 }; 1413 1414 try transaction(io, repo, Ctx{ .core = &repo.core, .io = io, .allocator = allocator, .author = author }); 1415 } 1416 1417 // commit `events` onto `ref`, rooted at `parent`, as one transaction. 1418 // consume always roots a new branch at nothing, so the divergent fixtures 1419 // write their side branch here instead. nothing is consumed, so it records 1420 // xit's own commit action rather than one of haxy's event actions 1421 fn commitEventsAtRef( 1422 io: std.Io, 1423 allocator: std.mem.Allocator, 1424 repo: *rp.Repo(.xit, .{}), 1425 ref: xit.ref.Ref, 1426 events: []const evt.EventWithId, 1427 parent: [hash.hexLen(.sha1)]u8, 1428 ) !void { 1429 const Repo = rp.Repo(.xit, .{}); 1430 const DB = Repo.DB; 1431 const Ctx = struct { 1432 core: *Repo.Core, 1433 io: std.Io, 1434 allocator: std.mem.Allocator, 1435 ref: xit.ref.Ref, 1436 events: []const evt.EventWithId, 1437 parent: [hash.hexLen(.sha1)]u8, 1438 1439 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 1440 const opts: rp.RepoOpts(.xit) = .{}; 1441 var moment = try DB.HashMap(.read_write).init(cursor.*); 1442 const state = Repo.State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 1443 try evt.commitEvents(.xit, opts, state, ctx.io, ctx.allocator, ctx.ref, ctx.events, .{ctx.parent}); 1444 const tip = (try xit.ref.readRecur(.xit, opts, state.readOnly(), ctx.io, .{ .ref = ctx.ref })) orelse return error.NotFound; 1445 try xit.undo.write(opts, state, std.Io.Timestamp.now(ctx.io, .real).toSeconds(), .{ 1446 .commit = try .init("seed conflicting events", tip, opts.max_read_size), 1447 }); 1448 } 1449 }; 1450 1451 try transaction(io, repo, Ctx{ 1452 .core = &repo.core, 1453 .io = io, 1454 .allocator = allocator, 1455 .ref = ref, 1456 .events = events, 1457 .parent = parent, 1458 }); 1459 } 1460 1461 // append `ctx` to the repo's history as one transaction. the fixtures write a 1462 // few states the normal paths have no way to produce 1463 fn transaction(io: std.Io, repo: *rp.Repo(.xit, .{}), ctx: anytype) !void { 1464 const DB = rp.Repo(.xit, .{}).DB; 1465 try repo.core.db_file.lock(io, .exclusive); 1466 defer repo.core.db_file.unlock(io); 1467 1468 const history = try DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); 1469 try history.appendContext(.{ .slot = try history.getSlot(-1) }, ctx); 1470 } 1471 1472 // recursively copy the contents of src_dir into dest_dir 1473 fn copyDir(io: std.Io, src_dir: std.Io.Dir, dest_dir: std.Io.Dir) !void { 1474 var iter = src_dir.iterate(); 1475 while (try iter.next(io)) |entry| { 1476 switch (entry.kind) { 1477 .file => try src_dir.copyFile(entry.name, dest_dir, entry.name, io, .{}), 1478 .directory => { 1479 try dest_dir.createDirPath(io, entry.name); 1480 var dest_entry_dir = try dest_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1481 defer dest_entry_dir.close(io); 1482 var src_entry_dir = try src_dir.openDir(io, entry.name, .{ .access_sub_paths = true, .iterate = true, .follow_symlinks = false }); 1483 defer src_entry_dir.close(io); 1484 try copyDir(io, src_entry_dir, dest_entry_dir); 1485 }, 1486 else => {}, 1487 } 1488 } 1489 } 1490