╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │68 - const io = session.io orelse return empty; │ │69 - const repos_dir = session.repos_dir orelse return empty; │ │71 + const io = session.io orelse return emptyResult(aa, identity, requested_ref_or_oid orelse .branch, requested_value); │ │72 + const repos_dir = session.repos_dir orelse return emptyResult(aa, identity, requested_ref_or_oid orelse .branch, requested_value);│ │73 │ │74 const repo_path = try std.fs.path.join(aa, &.{ repos_dir, &hex }); │ │75 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │76 // walk the log with the arena's backing allocator (transient; the commits │ │77 // we keep are duped into the page arena so they outlive it). │ │78 const gpa = arena.child_allocator; │ │76 - var repo = rp.Repo(.xit, .{}).open(io, gpa, .{ .path = repo_path }) catch return empty; │ │79 + var repo = rp.Repo(.xit, .{}).open(io, gpa, .{ .path = repo_path }) catch return emptyResult(aa, identity, requested_ref_or_oid orelse .branch, requested_value);│ │80 defer repo.deinit(io, gpa); │ │81 │ │79 - // page 1 walks from HEAD (null); a later page starts at its first oid. │ │80 - var start_arr: [1][xit.hash.hexLen(.sha1)]u8 = undefined; │ │81 - const start_oids: ?[]const [xit.hash.hexLen(.sha1)]u8 = if (start_oid.len == 0) null else blk: { │ │82 - if (start_oid.len != start_arr[0].len) return empty; // malformed oid │ │83 - @memcpy(&start_arr[0], start_oid); │ │84 - break :blk start_arr[0..1]; │ │82 + // resolve the requested ref (or the default branch) to the commit oid to │ │83 + // walk from. an explicitly named ref that doesn't resolve is a bad url │ │84 + // (NotFound -> 404); the default-branch path falls through to empty. │ │85 + const resolved = (try ui.ResolvedRefOrOid.init(&repo, io, aa, requested_ref_or_oid, requested_value)) orelse { │ │86 + if (requested_ref_or_oid != null) return error.NotFound; │ │87 + return emptyResult(aa, identity, .branch, requested_value); │ │88 }; │ │89 + var start_arr = [1][hex_len]u8{resolved.oid}; │ │90 + const start_oids: []const [hex_len]u8 = start_arr[0..1]; │ │91 │ │92 // collect this page's commit metadata, plus a peek at the one after it (its │ │93 // oid is the next page's start). the diff for each is rendered afterward, so │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │94 // the log iterator is closed before opening per-commit diff iterators.│ │95 var buf: [page_size]Commit = undefined; │ │91 - var oids: [page_size][xit.hash.hexLen(.sha1)]u8 = undefined; │ │96 + var oids: [page_size][hex_len]u8 = undefined; │ │97 var count: usize = 0; │ │98 var next_start: ?[]const u8 = null; │ │99 { │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 95 - var iter = repo.log(io, gpa, start_oids) catch return empty; │ │100 + var iter = repo.log(io, gpa, start_oids) catch return emptyResult(aa, identity, resolved.ref_or_oid, resolved.value);│ │101 defer iter.deinit(); │ │102 while (try iter.next(gpa)) |commit_object| { │ │103 defer commit_object.deinit(); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────╮ │138 } │ │139 │ │140 return .{ │ │136 - .identity = empty.identity, │ │141 + .identity = try aa.dupe(u8, identity), │ │142 + .ref_or_oid = resolved.ref_or_oid, │ │143 + .ref_or_oid_value = resolved.value, │ │144 .commits = try aa.dupe(Commit, buf[0..count]),│ │145 .next_start = next_start, │ │146 }; │ ╰───────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │147 } │ │148 │ │149 +// an empty listing pinned to a ref, for the wasm / no-repo / unresolved paths. │ │150 +fn emptyResult(aa: std.mem.Allocator, identity: []const u8, ref_or_oid: RefOrOid, value: []const u8) !Self {│ │151 + return .{ │ │152 + .identity = try aa.dupe(u8, identity), │ │153 + .ref_or_oid = ref_or_oid, │ │154 + .ref_or_oid_value = try aa.dupe(u8, value), │ │155 + .commits = &.{}, │ │156 + .next_start = null, │ │157 + }; │ │158 +} │ │159 + │ │160 const RenderedDiff = struct { │ │161 hunks: []const Hunk, │ │162 has_prev: bool, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │457 if (self.selectedCommitIndex()) |sel| { │ │458 // mirror the commit's current diff window so the url stays │ │459 // linkable (0 for any commit but the windowed start one). │ │442 - if (ui.RoutablePage.repoCommitsRoute(self.data.identity, self.data.commits[sel].oid, self.data.commits[sel].window_start)) |route| │ │460 + if (ui.RoutablePage.repoCommitsRoute(self.data.identity, .object, self.data.commits[sel].oid, self.data.commits[sel].window_start)) |route|│ │461 self.session.data.current_page = route; │ │462 } │ │463 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │559 .enter => if (self.selectedCommitIndex() != null) │ │560 try self.focusDiff(root_focus) │ │561 else if (self.data.next_start) |next| { │ │544 - if (ui.RoutablePage.repoCommitsRoute(self.data.identity, next, 0)) |route| │ │562 + if (ui.RoutablePage.repoCommitsRoute(self.data.identity, .object, next, 0)) |route|│ │563 try self.session.navigate(route); │ │564 }, │ │565 .arrow_right => try self.focusDiff(root_focus), │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │781 } │ │782 }; │ │783 │ │766 -// the "a:" navigation link for the commits page starting at `start_oid` within │ │784 +// the "a:" navigation link for the commits page walking from commit `oid` within │ │785 // `identity`, showing the selected commit's first `after` hunks (0 = default). │ │768 -fn commitsLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, start_oid: []const u8, after: usize) ![]const u8 {│ │769 - const route = ui.RoutablePage.repoCommitsRoute(identity, start_oid, after) orelse return error.RouteTooLong; │ │786 +fn commitsLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, oid: []const u8, after: usize) ![]const u8 { │ │787 + const route = ui.RoutablePage.repoCommitsRoute(identity, .object, oid, after) orelse return error.RouteTooLong; │ │788 const url = try route.urlAlloc(page_arena); │ │789 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); │ │790 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │793 // points at the same route as commitsLink but the "ai:" prefix keeps wasm clicks │ │794 // in-page (crossPageLink ignores it); the href is only followed with js off. │ │795 fn commitRowLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, oid: []const u8) ![]const u8 { │ │778 - const route = ui.RoutablePage.repoCommitsRoute(identity, oid, 0) orelse return error.RouteTooLong; │ │796 + const route = ui.RoutablePage.repoCommitsRoute(identity, .object, oid, 0) orelse return error.RouteTooLong;│ │797 const url = try route.urlAlloc(page_arena); │ │798 return std.fmt.allocPrint(page_arena.allocator(), "ai:{s}", .{url}); │ │799 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯