╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Patches.zig ──────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
571 } 572 return .{ 573 .items = items.items, 574 .prev_id = prev_id, 575 .next_id = next_id, 576 .count = @intCast(try set.count()), 577 }; 578 } 579 580 pub const View = thread.View(.patch, Self); 581 582 pub const Header = thread.Header; 583 584 pub fn appendDraftDetails(self: *const Self, allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), session: *ui.Session, entry: Entry) !void { 585 const aa = session.page_arena.allocator(); 586 var fields: [2]ui.widget.CopyableText.Choice = undefined; 587 var field_count: usize = 0; 588 589 if (entry.target_branch.len != 0) if (session.data.git_ssh_port) |port| { 590 const url = try std.fmt.allocPrint(aa, "ssh://localhost:{d}/repo/{s}/patch:{s}/branch:{s}", .{ port, self.identity, entry.id, entry.target_branch }); 591 const push_command = try std.fmt.allocPrint(aa, "git push {s} HEAD:patch", .{url}); 592 const clone_name = try cloneDirectoryName(aa, entry.record.event.title); 593 const clone_command = if (clone_name.len == 0) 594 try std.fmt.allocPrint(aa, "git clone {s}", .{url}) 595 else 596 try std.fmt.allocPrint(aa, "git clone {s} {s}", .{ url, clone_name }); 597 const choices: [2]ui.widget.CopyableText.Choice = .{ 598 .{ 599 .selector = "push", 600 .text = push_command, 601 .copyable_text = try std.fmt.allocPrint(aa, "{s}{s}", .{ session.data.git_ssh_prefix, push_command }), 602 .label = " push to this patch from existing repo ", 603 }, 604 .{ 605 .selector = "clone", 606 .text = clone_command, 607 .copyable_text = try std.fmt.allocPrint(aa, "{s}{s}", .{ session.data.git_ssh_prefix, clone_command }), 608 .label = " clone this patch ", 609 }, 610 }; 611 var copyable_text = try ui.widget.CopyableText.init(allocator, session, &choices); 612 errdefer copyable_text.deinit(allocator); 613 try box.children.put(allocator, copyable_text.getFocus().id, .{ .widget = .{ .copyable_text = copyable_text }, .rect = null, .min_size = null }); 614 }; 615 if (entry.fork_oid.len != 0) { 616 fields[field_count] = .{ 617 .selector = "from", 618 .text = entry.fork_oid, 619 .label = if (entry.no_changes) " object id of this patch (no changes) " else " object id of this patch ", 620 }; 621 field_count += 1; 622 } 623 if (entry.target_branch.len != 0) { 624 fields[field_count] = .{ 625 .selector = "to", 626 .text = entry.target_branch, 627 .label = " target branch this patch will go to ", 628 .bottom_label = " (set by the url you push to above) ", 629 }; 630 field_count += 1; 631 } 632 633 if (field_count > 0) { 634 var copyable_text = try ui.widget.CopyableText.init(allocator, session, fields[0..field_count]); 635 errdefer copyable_text.deinit(allocator); 636 try box.children.put(allocator, copyable_text.getFocus().id, .{ .widget = .{ .copyable_text = copyable_text }, .rect = null, .min_size = null }); 637 638 var spacer = try ui.widget.Spacer.init(allocator); 639 errdefer spacer.deinit(allocator); 640 try box.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .spacer = spacer }, .rect = null, .min_size = .{ .width = null, .height = 1 }, .max_size = .{ .width = null, .height = 1 } }); 641 } 642 } 643 644 fn cloneDirectoryName(allocator: std.mem.Allocator, title: []const u8) ![]const u8 { 645 var name: [64]u8 = undefined; 646 var len: usize = 0; 647 var pending: ?u8 = null; 648 649 for (title) |char| { 650 if (std.ascii.isAlphanumeric(char)) { 651 const needed: usize = if (pending == null) 1 else 2; 652 if (len + needed > name.len) break; 653 if (pending) |value| { 654 name[len] = value; 655 len += 1; 656 } 657 name[len] = std.ascii.toLower(char); 658 len += 1; 659 pending = null; 660 } else if (char == ' ') { 661 if (len != 0) pending = '-'; 662 } else if (char == '.') { 663 if (len != 0 and pending != '-') pending = '.'; 664 } 665 } 666 if (len == 0) return ""; 667 return allocator.dupe(u8, name[0..len]); 668 } 669 670 // tabs switching between the patches page's views 671 pub fn initHeader(allocator: std.mem.Allocator, session: *ui.Session, data: *const Self) !Header { 672 var header = try Header.init(allocator); 673 errdefer header.deinit(allocator); 674 const aa = session.page_arena.allocator(); 675 const selected_index = View.viewIndex(data.view); 676 const page_selected = std.meta.activeTag(session.data.current_page) == .repo_patches; 677 678 // a list tab per status, labeled with its listing's patch count 679 for ([_]evt.Patch.Status{ .open, .closed, .merged }, 0..) |status, index| { 680 const route = ui.RoutablePage.repoPatchesRoute(data.identity, status, data.tag, "") orelse return error.RouteTooLong; 681 const link = try ui.inPageTabLink(session, route, page_selected and selected_index == index); 682 var label_buf: [64]u8 = undefined; 683 const label = try std.fmt.bufPrint(&label_buf, "{s} ({d})", .{ @tagName(status), data.window(status).count }); 684 try header.addTab(allocator, label, link, index); 685 } 686 687 // tags tab, labeled with the active tag filter 688 { 689 const tags_route = ui.RoutablePage.repoThreadTagsRoute(.patch, data.identity, data.tag) orelse return error.RouteTooLong; 690 const tags_link = try ui.inPageTabLink(session, tags_route, page_selected and selected_index == View.viewIndex(.tags)); 691 const label = if (data.tag.len == 0) "tags" else blk: { 692 const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, data.tag)); 693 break :blk try std.fmt.allocPrint(aa, "tags ({s})", .{decoded}); 694 }; 695 try header.addTab(allocator, label, tags_link, View.viewIndex(.tags)); 696 } 697 698 // new-patch tab; an edit or resolve url shows its tab in this place 699 if (session.local == null) { 700 const route = switch (data.view) { 701 .edit => ui.RoutablePage.repoThreadEditRoute(.patch, data.identity, data.selected_id) orelse return error.RouteTooLong, 702 .new_comment => ui.RoutablePage.repoThreadCommentNewRoute(.patch, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, 703 .edit_comment => ui.RoutablePage.repoThreadCommentEditRoute(.patch, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, 704 .remove => ui.RoutablePage.repoThreadRemoveRoute(.patch, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, 705 .resolve => ui.RoutablePage.repoPatchesResolveRoute(data.identity, data.selected_id, data.theirs_picks) orelse return error.RouteTooLong, 706 else => ui.RoutablePage.repoThreadNewRoute(.patch, data.identity) orelse return error.RouteTooLong, 707 }; 708 const link = try ui.inPageTabLink(session, route, page_selected and selected_index == View.viewIndex(.new)); 709 const label: []const u8 = switch (data.view) { 710 .edit => "edit", 711 .new_comment => "reply", 712 .edit_comment => "edit", 713 .remove => "remove", 714 .resolve => "resolve", 715 else => "new", 716 }; 717 try header.addTab(allocator, label, link, View.viewIndex(.new)); 718 } 719 720 // drafts belong to the logged-in user and are not available locally 721 if (session.local == null) { 722 const route = ui.RoutablePage.repoPatchesDraftsRoute(data.identity) orelse return error.RouteTooLong; 723 const link = try ui.inPageTabLink(session, route, page_selected and selected_index == View.viewIndex(.drafts)); 724 var label_buf: [64]u8 = undefined; 725 const label = try std.fmt.bufPrint(&label_buf, "drafts ({d})", .{data.drafts.count}); 726 try header.addTab(allocator, label, link, View.viewIndex(.drafts)); 727 } 728 729 // conflicts tab, labeled with the conflict count 730 if (data.conflicts.count > 0) { 731 const route = ui.RoutablePage.repoPatchesConflictsRoute(data.identity, "") orelse return error.RouteTooLong; 732 const link = try ui.inPageTabLink(session, route, page_selected and selected_index == View.viewIndex(.conflicts)); 733 var label_buf: [64]u8 = undefined; 734 const label = try std.fmt.bufPrint(&label_buf, "conflicts ({d})", .{data.conflicts.count}); 735 try header.addTab(allocator, label, link, View.viewIndex(.conflicts)); 736 } 737 738 header.select(View.viewIndex(data.view)); 739 return header; 740 } 741