╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │510 }; │ │511 │ │512 // a `RefOrOid` resolved against an on-disk repo to the commit oid it points at │ │513 -pub const ResolvedRefOrOid = struct { │ │514 - // the hex-oid length for the default repo options the on-disk repos use │ │515 - pub const hex_len = hash.hexLen((rp.RepoOpts(.xit){}).hash); │ │513 +fn isUnreserved(c: u8) bool { │ │514 + return std.ascii.isAlphanumeric(c) or c == '-' or c == '.' or c == '_' or c == '~'; │ │515 +} │ │516 │ │517 - // the concrete ref/oid (a null request resolves to the default branch) │ │518 - ref_or_oid: RoutablePage.RefOrOid, │ │519 - // its branch/tag name or oid, url-encoded (ref names may contain '/', so the │ │520 - // route layer — which splits on '/' — stores them encoded). duped into `aa`. │ │521 - value: []const u8, │ │522 - // the commit oid it points at │ │523 - oid: [hex_len]u8, │ │517 +// percent-encode a ref name for use as a single url path segment. │ │518 +pub fn urlEncodeRef(aa: std.mem.Allocator, raw: []const u8) ![]const u8 { │ │519 + var out: std.Io.Writer.Allocating = .init(aa); │ │520 + try std.Uri.Component.percentEncode(&out.writer, raw, isUnreserved); │ │521 + return out.written(); │ │522 +} │ │523 + │ │524 +// a resolved ref/oid for a repo opened with `repo_opts` │ │525 +pub fn ResolvedRefOrOid(comptime repo_opts: rp.RepoOpts(.xit)) type { │ │526 + return struct { │ │527 + pub const hex_len = hash.hexLen(repo_opts.hash); │ │528 + │ │529 + // the concrete ref/oid (a null request resolves to the default branch) │ │530 + ref_or_oid: RoutablePage.RefOrOid, │ │531 + // its branch/tag name or oid, url-encoded (ref names may contain '/', so │ │532 + // the route layer — which splits on '/' — stores them encoded). duped │ │533 + // into `aa`. │ │534 + value: []const u8, │ │535 + // the commit oid it points at │ │536 + oid: [hex_len]u8, │ │537 + │ │538 + const Self = @This(); │ │539 + │ │540 + // resolve a requested ref/oid (null = the repo's default branch) to a │ │541 + // concrete ref/oid plus the commit oid it points at. `requested_value` is │ │542 + // the url-encoded form (as it appears in the route). null when it doesn't │ │543 + // resolve (an unknown branch/tag, or a malformed/unknown oid). │ │544 + pub fn init( │ │545 + repo: *rp.Repo(.xit, repo_opts), │ │546 + io: std.Io, │ │547 + aa: std.mem.Allocator, │ │548 + requested_ref_or_oid: ?RoutablePage.RefOrOid, │ │549 + requested_value: []const u8, │ │550 + ) !?Self { │ │551 + var ref_or_oid: RoutablePage.RefOrOid = requested_ref_or_oid orelse .branch; │ │552 + // the decoded ref name / oid to look up. a named ref arrives url-encoded. │ │553 + var value: []const u8 = if (requested_ref_or_oid == null) │ │554 + requested_value │ │555 + else │ │556 + std.Uri.percentDecodeInPlace(try aa.dupe(u8, requested_value)); │ │557 + // no ref named: fall back to HEAD's branch (or its oid when detached). │ │558 + if (requested_ref_or_oid == null) { │ │559 + var head_buf: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; │ │560 + if (repo.head(io, &head_buf)) |head| switch (head) { │ │561 + .ref => |r| { │ │562 + ref_or_oid = .branch; │ │563 + value = r.name; │ │564 + }, │ │565 + .oid => |o| { │ │566 + ref_or_oid = .object; │ │567 + value = o; │ │568 + }, │ │569 + } else |_| {} │ │570 + } │ │571 │ │525 - // resolve a requested ref/oid (null = the repo's default branch) to a │ │526 - // concrete ref/oid plus the commit oid it points at. `requested_value` is the │ │527 - // url-encoded form (as it appears in the route). null when it doesn't resolve │ │528 - // (an unknown branch/tag, or a malformed/unknown oid). │ │529 - pub fn init( │ │530 - repo: *rp.Repo(.xit, .{}), │ │531 - io: std.Io, │ │532 - aa: std.mem.Allocator, │ │533 - requested_ref_or_oid: ?RoutablePage.RefOrOid, │ │534 - requested_value: []const u8, │ │535 - ) !?ResolvedRefOrOid { │ │536 - var ref_or_oid: RoutablePage.RefOrOid = requested_ref_or_oid orelse .branch; │ │537 - // the decoded ref name / oid to look up. a named ref arrives url-encoded. │ │538 - var value: []const u8 = if (requested_ref_or_oid == null) │ │539 - requested_value │ │540 - else │ │541 - std.Uri.percentDecodeInPlace(try aa.dupe(u8, requested_value)); │ │542 - // no ref named: fall back to HEAD's branch (or its oid when detached). │ │543 - if (requested_ref_or_oid == null) { │ │544 - var head_buf: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; │ │545 - if (repo.head(io, &head_buf)) |head| switch (head) { │ │546 - .ref => |r| { │ │547 - ref_or_oid = .branch; │ │548 - value = r.name; │ │572 + var oid: [hex_len]u8 = undefined; │ │573 + switch (ref_or_oid) { │ │574 + .object => { │ │575 + if (value.len != hex_len) return null; │ │576 + @memcpy(&oid, value); │ │577 }, │ │550 - .oid => |o| { │ │551 - ref_or_oid = .object; │ │552 - value = o; │ │578 + .branch, .tag => { │ │579 + const ref_kind: rf.RefKind = if (ref_or_oid == .branch) .head else .tag; │ │580 + oid = (repo.readRef(io, .{ .kind = ref_kind, .name = value }) catch null) orelse return null;│ │581 }, │ │554 - } else |_| {} │ │582 + } │ │583 + // store url-encoded so the route layer can hold the value verbatim. │ │584 + return .{ .ref_or_oid = ref_or_oid, .value = try urlEncodeRef(aa, value), .oid = oid }; │ │585 } │ │586 + }; │ │587 +} │ │588 │ │557 - var oid: [hex_len]u8 = undefined; │ │558 - switch (ref_or_oid) { │ │559 - .object => { │ │560 - if (value.len != hex_len) return null; │ │561 - @memcpy(&oid, value); │ │562 - }, │ │563 - .branch, .tag => { │ │564 - const ref_kind: rf.RefKind = if (ref_or_oid == .branch) .head else .tag; │ │565 - oid = (repo.readRef(io, .{ .kind = ref_kind, .name = value }) catch null) orelse return null; │ │566 - }, │ │567 - } │ │568 - // store url-encoded so the route layer can hold the value verbatim. │ │569 - return .{ .ref_or_oid = ref_or_oid, .value = try urlEncode(aa, value), .oid = oid }; │ │570 - } │ │571 - │ │572 - fn isUnreserved(c: u8) bool { │ │573 - return std.ascii.isAlphanumeric(c) or c == '-' or c == '.' or c == '_' or c == '~'; │ │574 - } │ │575 - │ │576 - // percent-encode a ref name for use as a single url path segment. │ │577 - pub fn urlEncode(aa: std.mem.Allocator, raw: []const u8) ![]const u8 { │ │578 - var out: std.Io.Writer.Allocating = .init(aa); │ │579 - try std.Uri.Component.percentEncode(&out.writer, raw, isUnreserved); │ │580 - return out.written(); │ │581 - } │ │582 -}; │ │583 - │ │589 // per-connection mutable state. each SSH session / web session / local TUI │ │590 // run gets its own. │ │591 pub const Session = struct { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭──────────────────────────────────────────────────────────────────╮ │13 const Focus = xitui.focus.Focus; │ │14 │ │15 const SubHeader = @import("SubHeader.zig"); │ │16 - │ │17 -const RefOrOid = ui.RoutablePage.RefOrOid; │ │18 -const hex_len = ui.ResolvedRefOrOid.hex_len; │ │16 │ │17 // how many commits a page shows before a "next" link appears.│ │18 const page_size = 20; │ ╰──────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │43 identity: []const u8, │ │44 // the resolved ref/oid this log walks from (the default branch when the route│ │45 // didn't name one), so the page can canonicalize its url to it. │ │49 -ref_or_oid: RefOrOid, │ │46 +ref_or_oid: ui.RoutablePage.RefOrOid, │ │47 ref_or_oid_value: []const u8, │ │48 commits: []const Commit, │ │49 // the first oid of the next page, or null when this is the last page. │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │58 session: *ui.Session, │ │59 event_id: *const [evt.event_id_size]u8, │ │60 identity: []const u8, │ │64 - requested_ref_or_oid: ?RefOrOid, │ │61 + requested_ref_or_oid: ?ui.RoutablePage.RefOrOid, │ │62 requested_value: []const u8, │ │63 // the hunk index the selected commit's (the first one, the walk root) diff│ │64 // window starts at; 0 is the first window. moved by "next"/"previous". │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 75 const repo_path = try std.fs.path.join(aa, &.{ repos_dir, &hex }); │ │ 76 │ │ 77 // walk the log with the arena's backing allocator (transient; the commits │ │ 81 - // we keep are duped into the page arena so they outlive it). │ │ 78 + // we keep are duped into the page arena so they outlive it). AnyRepo opens │ │ 79 + // both sha1 and sha256 repos. │ │ 80 const gpa = arena.child_allocator; │ │ 83 - var repo = rp.Repo(.xit, .{}).open(io, gpa, .{ .path = repo_path }) catch return emptyResult(aa, identity, requested_ref_or_oid orelse .branch, requested_value); │ │ 84 - defer repo.deinit(io, gpa); │ │ 81 + var any_repo = rp.AnyRepo(.xit, .{}).open(io, gpa, .{ .path = repo_path }) catch return emptyResult(aa, identity, requested_ref_or_oid orelse .branch, requested_value);│ │ 82 + defer any_repo.deinit(io, gpa); │ │ 83 + │ │ 84 + return switch (any_repo) { │ │ 85 + inline else => |*repo| collect(repo.self_repo_opts, arena, repo, io, gpa, identity, requested_ref_or_oid, requested_value, after), │ │ 86 + }; │ │ 87 +} │ │ 88 + │ │ 89 +// walk the log for an opened repo. generic over the repo's hash kind so the │ │ 90 +// oid buffers and diff types it threads through match the repo's opts. │ │ 91 +fn collect( │ │ 92 + comptime repo_opts: rp.RepoOpts(.xit), │ │ 93 + arena: *std.heap.ArenaAllocator, │ │ 94 + repo: *rp.Repo(.xit, repo_opts), │ │ 95 + io: std.Io, │ │ 96 + gpa: std.mem.Allocator, │ │ 97 + identity: []const u8, │ │ 98 + requested_ref_or_oid: ?ui.RoutablePage.RefOrOid, │ │ 99 + requested_value: []const u8, │ │100 + after: usize, │ │101 +) !Self { │ │102 + const aa = arena.allocator(); │ │103 + const hex_len = ui.ResolvedRefOrOid(repo_opts).hex_len; │ │104 │ │105 // resolve the requested ref (or the default branch) to the commit oid to │ │106 // walk from. an explicitly named ref that doesn't resolve is a bad url │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │107 // (NotFound -> 404); the default-branch path falls through to empty. │ │ 89 - const resolved = (try ui.ResolvedRefOrOid.init(&repo, io, aa, requested_ref_or_oid, requested_value)) orelse { │ │108 + const resolved = (try ui.ResolvedRefOrOid(repo_opts).init(repo, io, aa, requested_ref_or_oid, requested_value)) orelse {│ │109 if (requested_ref_or_oid != null) return error.NotFound; │ │110 return emptyResult(aa, identity, .branch, requested_value); │ │111 }; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │151 // the selected commit (the first, == start_oid) shows the window the │ │152 // url asks for; the rest show their first window. │ │153 const start = if (i == 0) after else 0; │ │135 - const rendered = renderCommitDiff(io, gpa, aa, &repo, oid, start, diff_page) catch │ │154 + const rendered = renderCommitDiff(repo_opts, io, gpa, aa, repo, oid, start, diff_page) catch│ │155 RenderedDiff{ .hunks = &.{}, .has_prev = false, .has_more = false }; │ │156 commit.hunks = rendered.hunks; │ │157 commit.window_start = start; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │171 } │ │172 │ │173 // an empty listing pinned to a ref, for the wasm / no-repo / unresolved paths. │ │155 -fn emptyResult(aa: std.mem.Allocator, identity: []const u8, ref_or_oid: RefOrOid, value: []const u8) !Self { │ │174 +fn emptyResult(aa: std.mem.Allocator, identity: []const u8, ref_or_oid: ui.RoutablePage.RefOrOid, value: []const u8) !Self {│ │175 return .{ │ │176 .identity = try aa.dupe(u8, identity), │ │177 .ref_or_oid = ref_or_oid, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │191 // render the window [start, start+len) of a commit's diff against its first│ │192 // parent into `arena`-owned hunks. has_prev/has_more flag adjacent windows.│ │193 fn renderCommitDiff( │ │194 + comptime repo_opts: rp.RepoOpts(.xit), │ │195 io: std.Io, │ │196 gpa: std.mem.Allocator, │ │197 arena: std.mem.Allocator, │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────╮ │178 - repo: *rp.Repo(.xit, .{}), │ │179 - oid: [xit.hash.hexLen(.sha1)]u8, │ │198 + repo: *rp.Repo(.xit, repo_opts), │ │199 + oid: [xit.hash.hexLen(repo_opts.hash)]u8,│ │200 start: usize, │ │201 len: usize, │ │202 ) !RenderedDiff { │ ╰──────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯