╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Patches.zig ──────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
636 } 637 } 638 639 var prev_id: ?[]const u8 = null; 640 var iter = if (root_key) |root| blk: { 641 const rank = try set.rank(root); 642 if (rank > 0 and rank <= page_size) { 643 prev_id = ""; 644 } else if (rank > page_size) { 645 const pair = try set.getIndexKeyValuePair(@intCast(rank - page_size)) orelse return error.NotFound; 646 var previous: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; 647 _ = try pair.key_cursor.readBytes(&previous); 648 const hex = std.fmt.bytesToHex(previous[@sizeOf(u64)..].*, .lower); 649 prev_id = try aa.dupe(u8, &hex); 650 } 651 break :blk try set.iteratorFrom(root); 652 } else try set.iteratorFromIndex(0); 653 654 var items: std.ArrayList(PatchWithId) = .empty; 655 var next_id: ?[]const u8 = null; 656 while (try iter.next()) |cursor| { 657 const id = try evt.readOrderKeyId(evt.AdminDB, cursor); 658 const id_hex = std.fmt.bytesToHex(id, .lower); 659 if (items.items.len == page_size) { 660 next_id = try aa.dupe(u8, &id_hex); 661 break; 662 } 663 const path = try fork.forkPath(aa, repos_dir, &id_hex); 664 var fork_repo = rp.Repo(.xit, .{}).open(io, arena.child_allocator, .{ .path = path, .require_repo_root = true }) catch continue; 665 defer fork_repo.deinit(io, arena.child_allocator); 666 const entry = (try loadDraftEntry(repo_kind, repo_opts, arena, io, admin_moment, &fork_repo, target_repo, id, default_target_branch)) orelse continue; 667 try items.append(aa, entry); 668 } 669 return .{ 670 .items = items.items, 671 .prev_id = prev_id, 672 .next_id = next_id, 673 .count = @intCast(try set.count()), 674 }; 675 } 676 677 pub fn loadDraftEntry( 678 comptime repo_kind: rp.RepoKind, 679 comptime repo_opts: rp.RepoOpts(repo_kind), 680 arena: *std.heap.ArenaAllocator, 681 io: std.Io, 682 admin_moment: ?evt.AdminDB.HashMap(.read_only), 683 fork_repo: *rp.Repo(.xit, .{}), 684 target_repo: *rp.Repo(repo_kind, repo_opts), 685 id: [evt.event_id_size]u8, 686 default_target_branch: []const u8, 687 ) !?PatchWithId { 688 const aa = arena.allocator(); 689 const id_hex = std.fmt.bytesToHex(id, .lower); 690 const fork_oid = (try fork_repo.readRef(io, fork.ref)) orelse return null; 691 const moment = evt.currentMoment(.{}, fork_repo) catch return null; 692 const patch = (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, arena, &id)) orelse return null; 693 var target_branch = default_target_branch; 694 if (patch.event.revision) |revision| { 695 target_branch = revision.targetBranch() orelse return null; 696 } 697 const target_oid = if (target_branch.len != 0) 698 try target_repo.readRef(io, .{ .kind = .head, .name = target_branch }) 699 else 700 null; 701 return .{ 702 .id = try aa.dupe(u8, &id_hex), 703 .record = patch, 704 .author = try ui.Author.initFromEmail(admin_moment, arena, patch.author_email), 705 .draft = true, 706 .fork_oid = try aa.dupe(u8, &fork_oid), 707 .target_branch = try aa.dupe(u8, target_branch), 708 .no_changes = if (target_oid) |oid| std.mem.eql(u8, &oid, &fork_oid) else false, 709 .fork_exists = true, 710 }; 711 } 712 713 pub const View = thread.View(.patch, Self); 714 pub const Detail = thread.Detail(.patch, Self); 715 pub const detail_widget_name = "repo_patch_detail"; 716 717 pub const Header = thread.Header; 718 719 pub fn appendDetails(self: *const Self, allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), session: *ui.Session, entry: Entry) !void { 720 const aa = session.page_arena.allocator(); 721 const status_kind = entry.record.event.status.kind(); 722 var fields: [2]ui.widget.CopyableText.Choice = undefined; 723 var field_count: usize = 0; 724 725 if (status_kind != .merged and entry.fork_exists and entry.target_branch.len != 0) if (session.data.git_ssh_port) |port| { 726 const url = try std.fmt.allocPrint(aa, "ssh://localhost:{d}/repo/{s}/patch:{s}/branch:{s}", .{ port, self.identity, entry.id, entry.target_branch }); 727 const push_command = try std.fmt.allocPrint(aa, "git push {s} HEAD:patch", .{url}); 728 const clone_name = try cloneDirectoryName(aa, entry.record.event.title); 729 const clone_command = if (clone_name.len == 0) 730 try std.fmt.allocPrint(aa, "git clone {s}", .{url}) 731 else 732 try std.fmt.allocPrint(aa, "git clone {s} {s}", .{ url, clone_name }); 733 const choices: [2]ui.widget.CopyableText.Choice = .{ 734 .{ 735 .selector = "push", 736 .text = push_command, 737 .copyable_text = try std.fmt.allocPrint(aa, "{s}{s}", .{ session.data.git_ssh_prefix, push_command }), 738 .label = " push to this patch from existing repo ", 739 }, 740 .{ 741 .selector = "clone", 742 .text = clone_command, 743 .copyable_text = try std.fmt.allocPrint(aa, "{s}{s}", .{ session.data.git_ssh_prefix, clone_command }), 744 .label = " clone this patch ", 745 }, 746 }; 747 var copyable_text = try ui.widget.CopyableText.init(allocator, session, &choices); 748 errdefer copyable_text.deinit(allocator); 749 try box.children.put(allocator, copyable_text.getFocus().id, .{ .widget = .{ .copyable_text = copyable_text }, .rect = null, .min_size = null }); 750 }; 751 if (entry.fork_oid.len != 0) { 752 fields[field_count] = .{ 753 .selector = "from", 754 .text = entry.fork_oid, 755 .label = if (entry.no_changes) " object id of this patch (no changes) " else " object id of this patch ", 756 }; 757 field_count += 1; 758 } 759 if (entry.target_branch.len != 0) { 760 fields[field_count] = .{ 761 .selector = "to", 762 .text = entry.target_branch, 763 .label = if (status_kind == .merged) " target branch this patch was merged into " else " target branch this patch will go to ", 764 .bottom_label = if (status_kind == .merged) "" else " (set by the url you push to above) ", 765 }; 766 field_count += 1; 767 } 768 769 if (field_count > 0) { 770 var copyable_text = try ui.widget.CopyableText.init(allocator, session, fields[0..field_count]); 771 errdefer copyable_text.deinit(allocator); 772 try box.children.put(allocator, copyable_text.getFocus().id, .{ .widget = .{ .copyable_text = copyable_text }, .rect = null, .min_size = null }); 773 } 774 } 775 776 fn cloneDirectoryName(allocator: std.mem.Allocator, title: []const u8) ![]const u8 { 777 var name: [64]u8 = undefined; 778 var len: usize = 0; 779 var pending: ?u8 = null; 780 781 for (title) |char| { 782 if (std.ascii.isAlphanumeric(char)) { 783 const needed: usize = if (pending == null) 1 else 2; 784 if (len + needed > name.len) break; 785 if (pending) |value| { 786 name[len] = value; 787 len += 1; 788 } 789 name[len] = std.ascii.toLower(char); 790 len += 1; 791 pending = null; 792 } else if (char == ' ') { 793 if (len != 0) pending = '-'; 794 } else if (char == '.') { 795 if (len != 0 and pending != '-') pending = '.'; 796 } 797 } 798 if (len == 0) return ""; 799 return allocator.dupe(u8, name[0..len]); 800 } 801 802 // tabs switching between the patches page's views 803 pub fn initHeader(allocator: std.mem.Allocator, session: *ui.Session, data: *const Self) !Header { 804 var header = try Header.init(allocator); 805 errdefer header.deinit(allocator); 806 const aa = session.page_arena.allocator(); 807 const selected_index = View.viewIndex(data.view); 808 const page_selected = std.meta.activeTag(session.data.current_page) == .repo_patches; 809 810 // a list tab per status, labeled with its listing's patch count 811 for ([_]Status{ .open, .closed, .merged }, 0..) |status, index| { 812 const route = ui.RoutablePage.repoPatchesRoute(data.identity, status, data.tag, "") orelse return error.RouteTooLong; 813 const link = try ui.inPageTabLink(session, route, page_selected and selected_index == index); 814 var label_buf: [64]u8 = undefined; 815 const label = try std.fmt.bufPrint(&label_buf, "{s} ({d})", .{ @tagName(status), data.window(status).count }); 816 try header.addTab(allocator, label, link, index); 817 } 818 819 // tags tab, labeled with the active tag filter 820 { 821 const tags_route = ui.RoutablePage.repoThreadTagsRoute(.patch, data.identity, data.tag) orelse return error.RouteTooLong; 822 const tags_link = try ui.inPageTabLink(session, tags_route, page_selected and selected_index == View.viewIndex(.tags)); 823 const label = if (data.tag.len == 0) "tags" else blk: { 824 const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, data.tag)); 825 break :blk try std.fmt.allocPrint(aa, "tags ({s})", .{decoded}); 826 }; 827 try header.addTab(allocator, label, tags_link, View.viewIndex(.tags)); 828 } 829 830 // new-patch tab; an edit or resolve url shows its tab in this place 831 if (session.local == null) { 832 const route = switch (data.view) { 833 .edit => ui.RoutablePage.repoThreadEditRoute(.patch, data.identity, data.selected_id) orelse return error.RouteTooLong, 834 .publish => ui.RoutablePage.repoPatchPublishRoute(data.identity, data.selected_id) orelse return error.RouteTooLong, 835 .merge => ui.RoutablePage.repoPatchMergeRoute(data.identity, data.selected_id) orelse return error.RouteTooLong, 836 .new_comment => ui.RoutablePage.repoThreadCommentNewRoute(.patch, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, 837 .edit_comment => ui.RoutablePage.repoThreadCommentEditRoute(.patch, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, 838 .remove => ui.RoutablePage.repoThreadRemoveRoute(.patch, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, 839 .resolve => ui.RoutablePage.repoPatchesResolveRoute(data.identity, data.selected_id, data.theirs_picks) orelse return error.RouteTooLong, 840 else => ui.RoutablePage.repoThreadNewRoute(.patch, data.identity) orelse return error.RouteTooLong, 841 }; 842 const link = try ui.inPageTabLink(session, route, page_selected and selected_index == View.viewIndex(.new)); 843 const label: []const u8 = switch (data.view) { 844 .edit => "edit", 845 .publish => "publish", 846 .merge => "merge", 847 .new_comment => "reply", 848 .edit_comment => "edit", 849 .remove => "remove", 850 .resolve => "resolve", 851 else => "new", 852 }; 853 try header.addTab(allocator, label, link, View.viewIndex(.new)); 854 } 855 856 // drafts belong to the logged-in user and are not available locally 857 if (session.local == null) { 858 const route = ui.RoutablePage.repoPatchesDraftsRoute(data.identity) orelse return error.RouteTooLong; 859 const link = try ui.inPageTabLink(session, route, page_selected and selected_index == View.viewIndex(.drafts)); 860 var label_buf: [64]u8 = undefined; 861 const label = try std.fmt.bufPrint(&label_buf, "drafts ({d})", .{data.drafts.count}); 862 try header.addTab(allocator, label, link, View.viewIndex(.drafts)); 863 } 864 865 // conflicts tab, labeled with the conflict count 866 if (data.conflicts.count > 0) { 867 const route = ui.RoutablePage.repoPatchesConflictsRoute(data.identity, "") orelse return error.RouteTooLong; 868 const link = try ui.inPageTabLink(session, route, page_selected and selected_index == View.viewIndex(.conflicts)); 869 var label_buf: [64]u8 = undefined; 870 const label = try std.fmt.bufPrint(&label_buf, "conflicts ({d})", .{data.conflicts.count}); 871 try header.addTab(allocator, label, link, View.viewIndex(.conflicts)); 872 } 873 874 header.select(View.viewIndex(data.view)); 875 return header; 876 } 877