╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │599 } │ │600 │ │601 // percent-encode a ref name for use as a single url path segment. │ │602 - fn urlEncode(aa: std.mem.Allocator, raw: []const u8) ![]const u8 { │ │602 + pub fn urlEncode(aa: std.mem.Allocator, raw: []const u8) ![]const u8 { │ │603 var out: std.Io.Writer.Allocating = .init(aa); │ │604 try std.Uri.Component.percentEncode(&out.writer, raw, isUnreserved);│ │605 return out.written(); │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │59 const rf = ui.RoutablePage.RepoFiles.parse(name_str) orelse return error.NotFound; │ │60 const tag = std.meta.activeTag(route); │ │61 │ │62 - // page state lives in the route only for the tab it targets; the other tabs │ │63 - // open at their root/first page. a null ref kind means the files tab opens at │ │64 - // the default branch (Files.init resolves it). │ │65 - const files_ref_kind = if (tag == .repo) rf.ref_kind else null; │ │66 - const files_ref_value = if (tag == .repo) rf.ref_value else ""; │ │67 - const files_dir = if (tag == .repo) rf.dir else ""; │ │68 - // a null commits ref means the commits tab opens at the default branch │ │69 - // (Commits.init resolves it). │ │62 + // the files and commits tabs share one ref/oid: whichever the incoming route │ │63 + // names (it rides on the route's target tab), or the default branch when │ │64 + // neither tab is targeted. building both views at it keeps switching tabs │ │65 + // (even by key, without a reload) on the same ref. a null ref means the │ │66 + // default branch (Files/Commits.init resolve it). the directory and the diff │ │67 + // window only apply to their own tab. │ │68 const commits_ref = if (tag == .repo_commits) ui.RoutablePage.repoCommitsRef(name_str) else ui.RoutablePage.CommitsRef{ .ref_or_oid = null, .value = "" };│ │69 + const requested_ref_or_oid: ?ui.RoutablePage.RefOrOid = switch (tag) { │ │70 + .repo => rf.ref_kind, │ │71 + .repo_commits => commits_ref.ref_or_oid, │ │72 + else => null, │ │73 + }; │ │74 + const requested_ref_value: []const u8 = switch (tag) { │ │75 + .repo => rf.ref_value, │ │76 + .repo_commits => commits_ref.value, │ │77 + else => "", │ │78 + }; │ │79 + const files_dir = if (tag == .repo) rf.dir else ""; │ │80 // how many diff hunks the commits view's selected commit shows ("load more"). │ │81 const commits_after: usize = switch (route) { │ │82 .repo_commits => |c| c.after, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │100 // of the repo title. │ │101 const owner = (try evt.User.readById(DB, hash_kind, haxy_moment, arena, repo.user_id)) orelse return error.NotFound; │ │102 │ │ 94 - // build files and commits first so their resolved refs (the default branch │ │103 + // build files and commits first so their resolved ref (the default branch │ │104 // when the route named none) can canonicalize each tab's mirror url to the │ │ 96 - // explicit ref it's viewing rather than leaving it bare. │ │ 97 - const files = try Files.init(arena, session, &found.event_id, rf.identity, files_ref_kind, files_ref_value, files_dir); │ │ 98 - const commits = try Commits.init(arena, session, &found.event_id, rf.identity, commits_ref.ref_or_oid, commits_ref.value, commits_after);│ │105 + // explicit ref it's viewing rather than leaving it bare. both resolve the │ │106 + // same requested ref, so they end up viewing the same one. │ │107 + const files = try Files.init(arena, session, &found.event_id, rf.identity, requested_ref_or_oid, requested_ref_value, files_dir); │ │108 + const commits = try Commits.init(arena, session, &found.event_id, rf.identity, requested_ref_or_oid, requested_ref_value, commits_after);│ │109 │ │110 // each tab mirror carries this page's route for that tab; tabs not targeted │ │111 // by the incoming route fall back to their root/first-page route. │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │113 const commits_route_name = (ui.RoutablePage.repoCommitsRoute(rf.identity, commits.ref_or_oid, commits.ref_or_oid_value, commits_after) orelse return error.NotFound).repo_commits.name;│ │114 │ │115 return .{ │ │106 - .header = try Header.init(arena, repo.name, owner.name), │ │116 + // files and commits resolve the same ref, so either's serves the header, │ │117 + // which points both tabs at it. │ │118 + .header = try Header.init(arena, repo.name, owner.name, files.ref_or_oid, files.ref_or_oid_value), │ │119 .repo = repo, │ │120 .files = files, │ │121 .commits = commits, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │429 try box.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │430 } │ │431 │ │432 + // a focusable row for the top of the diff pane linking to the files tab at │ │433 + // this commit's tree, so its files are always viewable as of this object. │ │434 + fn addViewFilesLink(self: *View, allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), oid: []const u8) !void { │ │435 + const link = try filesObjectLink(self.session.page_arena, self.data.identity, oid); │ │436 + var tb = try wgt.TextBox(ui.Widget).init(allocator, "view files at this commit", .{ .border_style = .hidden, .rounded_corners = true, .wrap_kind = .none });│ │437 + errdefer tb.deinit(allocator); │ │438 + tb.getFocus().focusable = true; │ │439 + tb.getFocus().kind = .{ .custom = link }; │ │440 + try box.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │441 + } │ │442 + │ │443 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { │ │444 self.box.deinit(allocator); │ │445 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │564 inner.children.clearAndFree(allocator); │ │565 inner.getFocus().child_id = null; │ │566 │ │556 - // a "previous" row at the top and a "next" row at the bottom reload the │ │557 - // page on the adjacent diff window. │ │567 + // the first row always links to viewing the repo's files at this commit. │ │568 + try self.addViewFilesLink(allocator, inner, commit.oid); │ │569 + │ │570 + // a "previous" row and a "next" row at the bottom reload the page on the │ │571 + // adjacent diff window. │ │572 if (commit.has_prev) try self.addNavLink(allocator, inner, "← previous", commit.oid, commit.window_start -| diff_page);│ │573 for (commit.hunks) |hunk| { │ │574 if (hunk.path) |path| try self.addPathBox(allocator, inner, path); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │832 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); │ │833 } │ │834 │ │835 +// the "a:" link to the files tab at commit `oid` (an object ref), at its root │ │836 +// directory, within `identity` ("owner/name"). │ │837 +fn filesObjectLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, oid: []const u8) ![]const u8 { │ │838 + const route = ui.RoutablePage.repoFilesRoute(identity, .object, oid, "") orelse return error.RouteTooLong;│ │839 + const url = try route.urlAlloc(page_arena); │ │840 + return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); │ │841 +} │ │842 + │ │843 // the in-page "ai:" anchor for selecting `oid` in `identity`'s commit list. it │ │844 // points at the same route as commitsLink but the "ai:" prefix keeps wasm clicks │ │845 // in-page (crossPageLink ignores it); the href is only followed with js off. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Header.zig│ ╰──────────────────────╯ ╭────────────────────────────────────────────────────╮ │ 9 const Focus = xitui.focus.Focus; │ │10 │ │11 pub const AuthTab = @import("./../AuthTab.zig");│ │12 + │ │13 +const RefOrOid = ui.RoutablePage.RefOrOid; │ │14 │ │15 name: []const u8, │ │16 owner_name: []const u8, │ ╰────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │17 title: ui.Title, │ │18 auth_tab: AuthTab, │ │19 +// the ref/oid this page is viewing, so the files and commits tabs both link to│ │20 +// it (switching tabs keeps the same ref). the value is url-encoded. │ │21 +ref_or_oid: RefOrOid, │ │22 +ref_or_oid_value: []const u8, │ │23 │ │24 const Self = @This(); │ │25 │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │20 -pub fn init(arena: *std.heap.ArenaAllocator, name: []const u8, owner_name: []const u8) !Self { │ │26 +pub fn init(arena: *std.heap.ArenaAllocator, name: []const u8, owner_name: []const u8, ref_or_oid: RefOrOid, ref_or_oid_value: []const u8) !Self {│ │27 return .{ │ │28 .name = name, │ │29 .owner_name = owner_name, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯