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