╭ changed ──────╮ │src/ui/Fork.zig│ ╰───────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │269 .down => switch (child.*) { │ │270 .fork_header => { │ │271 if (stack.getSelected()) |selected| switch (selected.*) { │ │272 + .repo_patches => |*view| if (view.focusDetail(root_focus)) return, │ │273 .repo_files => |*view| if (view.focusCloneUrl(root_focus)) return, │ │274 .repo_commits => |*view| if (view.focusCloneUrl(root_focus)) return,│ │275 else => {}, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Patches.zig│ ╰───────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │497 var closed_window = try thread.loadWindow(Self, repo_opts.hash, arena, admin_moment, haxy_moment, event_id_to_patch, closed_set, closed_root, conflict_set, empty.selected_id, thread_comments_start); │ │498 var merged_window = try thread.loadWindow(Self, repo_opts.hash, arena, admin_moment, haxy_moment, event_id_to_patch, merged_set, merged_root, conflict_set, empty.selected_id, thread_comments_start); │ │499 var conflicts_window = try thread.loadWindow(Self, repo_opts.hash, arena, admin_moment, haxy_moment, event_id_to_patch, conflict_set, conflicts_root, conflict_set, empty.selected_id, thread_comments_start);│ │500 - try setForkAvailability(arena, admin_moment, &open_window); │ │501 - try setForkAvailability(arena, admin_moment, &closed_window); │ │502 - try setForkAvailability(arena, admin_moment, &merged_window); │ │503 - try setForkAvailability(arena, admin_moment, &conflicts_window); │ │500 + try setForkDetails(repo_kind, repo_opts, io, arena, admin_moment, repo, &open_window); │ │501 + try setForkDetails(repo_kind, repo_opts, io, arena, admin_moment, repo, &closed_window); │ │502 + try setForkDetails(repo_kind, repo_opts, io, arena, admin_moment, repo, &merged_window); │ │503 + try setForkDetails(repo_kind, repo_opts, io, arena, admin_moment, repo, &conflicts_window); │ │504 if (view == .conflicts and conflicts_window.count > 0) resolved_view = .conflicts; │ │505 │ │506 const comment_page = if (empty.comment_id.len == 0) │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │530 }; │ │531 } │ │532 │ │533 -fn setForkAvailability(arena: *std.heap.ArenaAllocator, admin_moment: ?evt.AdminDB.HashMap(.read_only), target: *Window) !void {│ │534 - const moment = admin_moment orelse return; │ │533 +fn setForkDetails( │ │534 + comptime repo_kind: rp.RepoKind, │ │535 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │536 + io: std.Io, │ │537 + arena: *std.heap.ArenaAllocator, │ │538 + admin_moment: ?evt.AdminDB.HashMap(.read_only), │ │539 + repo: *rp.Repo(repo_kind, repo_opts), │ │540 + target: *Window, │ │541 +) !void { │ │542 if (target.items.len == 0) return; │ │543 const items = try arena.allocator().dupe(PatchWithId, target.items); │ │544 for (items) |*item| { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │538 - const id = try evt.parseEventId(item.id); │ │539 - const record = try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, &id); │ │540 - item.fork_exists = if (record) |value| !value.removed else false; │ │545 + if (admin_moment) |moment| { │ │546 + const id = try evt.parseEventId(item.id); │ │547 + const record = try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, &id);│ │548 + item.fork_exists = if (record) |value| !value.removed else false; │ │549 + } │ │550 + const revision = item.record.event.revision orelse continue; │ │551 + const target_branch = revision.targetBranch() orelse continue; │ │552 + item.fork_oid = revision.source_oid; │ │553 + item.target_branch = target_branch; │ │554 + if (item.record.event.status == .merged) continue; │ │555 + const target_oid = try repo.readRef(io, .{ .kind = .head, .name = target_branch }); │ │556 + item.no_changes = if (target_oid) |oid| std.mem.eql(u8, &oid, revision.source_oid) else false; │ │557 } │ │558 target.items = items; │ │559 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │675 │ │676 pub const Header = thread.Header; │ │677 │ │662 -pub fn appendDraftDetails(self: *const Self, allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), session: *ui.Session, entry: Entry) !void {│ │678 +pub fn appendDetails(self: *const Self, allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), session: *ui.Session, entry: Entry) !void { │ │679 const aa = session.page_arena.allocator(); │ │680 var fields: [2]ui.widget.CopyableText.Choice = undefined; │ │681 var field_count: usize = 0; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │682 │ │667 - if (entry.target_branch.len != 0) if (session.data.git_ssh_port) |port| { │ │683 + if (entry.record.event.status != .merged and entry.fork_exists and entry.target_branch.len != 0) if (session.data.git_ssh_port) |port| { │ │684 const url = try std.fmt.allocPrint(aa, "ssh://localhost:{d}/repo/{s}/patch:{s}/branch:{s}", .{ port, self.identity, entry.id, entry.target_branch });│ │685 const push_command = try std.fmt.allocPrint(aa, "git push {s} HEAD:patch", .{url}); │ │686 const clone_name = try cloneDirectoryName(aa, entry.record.event.title); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │718 fields[field_count] = .{ │ │719 .selector = "to", │ │720 .text = entry.target_branch, │ │705 - .label = " target branch this patch will go to ", │ │706 - .bottom_label = " (set by the url you push to above) ", │ │721 + .label = if (entry.record.event.status == .merged) " target branch this patch was merged into " else " target branch this patch will go to ",│ │722 + .bottom_label = if (entry.record.event.status == .merged) "" else " (set by the url you push to above) ", │ │723 }; │ │724 field_count += 1; │ │725 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │728 var copyable_text = try ui.widget.CopyableText.init(allocator, session, fields[0..field_count]); │ │729 errdefer copyable_text.deinit(allocator); │ │730 try box.children.put(allocator, copyable_text.getFocus().id, .{ .widget = .{ .copyable_text = copyable_text }, .rect = null, .min_size = null }); │ │715 - │ │716 - var spacer = try ui.widget.Spacer.init(allocator); │ │717 - errdefer spacer.deinit(allocator); │ │718 - 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 } });│ │731 } │ │732 } │ │733 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/ui/widget/thread.zig│ ╰────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1363 } │ │1364 } │ │1365 │ │1366 - if (supports_drafts and entryDraft(entry) and !description_page and comment_page == null) {│ │1367 - try self.data.appendDraftDetails(allocator, inner, self.session, entry); │ │1366 + if (supports_drafts and !description_page and comment_page == null) { │ │1367 + try self.data.appendDetails(allocator, inner, self.session, entry); │ │1368 } │ │1369 │ │1370 if (comment_page) |page| { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1467 break :blk tb.getFocus().id; │ │1468 }; │ │1469 │ │1470 - if (supports_drafts and entryDraft(entry)) { │ │1471 - inner.getFocus().child_id = inner.children.keys()[tool_row_index + 1]; │ │1472 - const sc = self.detailScroll(index); │ │1473 - sc.x = 0; │ │1474 - sc.y = 0; │ │1475 - sc.getFocus().version +%= 1; │ │1476 - return; │ │1477 - } │ │1470 + if (!supports_drafts or !entryDraft(entry)) { │ │1471 + const parent_route = ui.RoutablePage.repoThreadCommentsRoute(kind, self.data.identity, entry.id, 0) orelse return error.RouteTooLong; │ │1472 + try Attachment.appendRows(allocator, inner, self.session, self.data.identity, try parent_route.toUrl(self.session.page_arena), entry.attachments); │ │1473 │ │1479 - const parent_route = ui.RoutablePage.repoThreadCommentsRoute(kind, self.data.identity, entry.id, 0) orelse return error.RouteTooLong; │ │1480 - try Attachment.appendRows(allocator, inner, self.session, self.data.identity, try parent_route.toUrl(self.session.page_arena), entry.attachments); │ │1474 + if (!description_page) { │ │1475 + var reply = try Comment.linkBox(allocator, self.session, "new comment", ui.RoutablePage.repoThreadCommentNewRoute(kind, self.data.identity, entry.id, "") orelse return error.RouteTooLong);│ │1476 + errdefer reply.deinit(allocator); │ │1477 + try inner.children.put(allocator, reply.getFocus().id, .{ .widget = .{ .text_box = reply }, .rect = null, .min_size = null }); │ │1478 │ │1482 - if (!description_page) { │ │1483 - var reply = try Comment.linkBox(allocator, self.session, "new comment", ui.RoutablePage.repoThreadCommentNewRoute(kind, self.data.identity, entry.id, "") orelse return error.RouteTooLong); │ │1484 - errdefer reply.deinit(allocator); │ │1485 - try inner.children.put(allocator, reply.getFocus().id, .{ .widget = .{ .text_box = reply }, .rect = null, .min_size = null }); │ │1486 - │ │1487 - try Comment.appendCount(allocator, inner, entry.comments.count, "comment", "comments"); │ │1488 - for (entry.comments.comments) |comment| try Comment.appendComment(allocator, inner, self.session, self.data.identity, kind, comment); │ │1489 - try Comment.appendWindowNav(allocator, inner, self.session, self.data.identity, kind, entry.id, null, entry.comments); │ │1479 + try Comment.appendCount(allocator, inner, entry.comments.count, "comment", "comments"); │ │1480 + for (entry.comments.comments) |comment| try Comment.appendComment(allocator, inner, self.session, self.data.identity, kind, comment); │ │1481 + try Comment.appendWindowNav(allocator, inner, self.session, self.data.identity, kind, entry.id, null, entry.comments); │ │1482 │ │1491 - var spacer = try Spacer.init(allocator); │ │1492 - errdefer spacer.deinit(allocator); │ │1493 - try inner.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .spacer = spacer }, .rect = null, .min_size = null }); │ │1483 + var spacer = try Spacer.init(allocator); │ │1484 + errdefer spacer.deinit(allocator); │ │1485 + try inner.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .spacer = spacer }, .rect = null, .min_size = null }); │ │1486 + } │ │1487 } │ │1488 │ │1496 - // select the title by default │ │1497 - if (self.title_id[index]) |id| inner.getFocus().child_id = id; │ │1489 + // select the first focusable widget below the tool row │ │1490 + for (tool_row_index + 1..inner.children.count()) |child_index| { │ │1491 + if (!self.detailChildFocusable(index, child_index)) continue; │ │1492 + inner.getFocus().child_id = inner.children.keys()[child_index]; │ │1493 + break; │ │1494 + } │ │1495 │ │1496 // reset the scroll to the top for the newly-shown thread: directly on the │ │1497 // terminal (the wasm offset), and via a version bump on the web (so the │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯