╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │12 const Grid = xitui.grid.Grid; │ │13 const Focus = xitui.focus.Focus; │ │14 │ │15 +const RefOrOid = ui.RoutablePage.RefOrOid; │ │16 +const hex_len = ui.ResolvedRefOrOid.hex_len; │ │17 + │ │18 // how many commits a page shows before a "next" link appears. │ │19 const page_size = 20; │ │20 // how many diff hunks one window of a commit's diff shows; "next"/"previous"│ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │40 has_more: bool, │ │41 }; │ │42 │ │40 -// "owner/name", so the view can build /repo/owner/name/commits/<oid> links. │ │43 +// "owner/name", so the view can build /repo/owner/name/commits/... links. │ │44 identity: []const u8, │ │45 +// the resolved ref/oid this log walks from (the default branch when the route│ │46 +// didn't name one), so the page can canonicalize its url to it. │ │47 +ref_or_oid: RefOrOid, │ │48 +ref_or_oid_value: []const u8, │ │49 commits: []const Commit, │ │50 // the first oid of the next page, or null when this is the last page. │ │51 next_start: ?[]const u8, │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │57 session: *ui.Session, │ │58 event_id: *const [evt.event_id_size]u8, │ │59 identity: []const u8, │ │53 - start_oid: []const u8, │ │54 - // the hunk index the selected commit's (the first one, == start_oid) diff │ │60 + requested_ref_or_oid: ?RefOrOid, │ │61 + requested_value: []const u8, │ │62 + // the hunk index the selected commit's (the first one, the walk root) diff│ │63 // window starts at; 0 is the first window. moved by "next"/"previous". │ │64 after: usize, │ │65 ) !Self { │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │66 const aa = arena.allocator(); │ │67 const hex = std.fmt.bytesToHex(event_id.*, .lower); │ │60 - const empty: Self = .{ │ │61 - .identity = try aa.dupe(u8, identity), │ │62 - .commits = &.{}, │ │63 - .next_start = null, │ │64 - }; │ │68 │ │69 // no filesystem (wasm) or nowhere to look: empty listing. the wasm path│ │70 // never calls init anyway — it rebuilds from the serialized snapshot. │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯