╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │533 }; │ │534 } │ │535 }; │ │536 + │ │537 +// a `RefOrOid` resolved against an on-disk repo to the commit oid it points at │ │538 +pub const ResolvedRefOrOid = struct { │ │539 + // the hex-oid length for the default repo options the on-disk repos use │ │540 + pub const hex_len = hash.hexLen((rp.RepoOpts(.xit){}).hash); │ │541 │ │542 + // the concrete ref/oid (a null request resolves to the default branch) │ │543 + ref_or_oid: RoutablePage.RefOrOid, │ │544 + // its branch/tag name or oid string, duped into the allocator passed in │ │545 + value: []const u8, │ │546 + // the commit oid it points at │ │547 + oid: [hex_len]u8, │ │548 + │ │549 + // resolve a requested ref/oid (null = the repo's default branch) to a │ │550 + // concrete ref/oid plus the commit oid it points at. null when it doesn't │ │551 + // resolve (an unknown branch/tag, or a malformed/unknown oid). │ │552 + pub fn init( │ │553 + repo: *rp.Repo(.xit, .{}), │ │554 + io: std.Io, │ │555 + aa: std.mem.Allocator, │ │556 + requested_ref_or_oid: ?RoutablePage.RefOrOid, │ │557 + requested_value: []const u8, │ │558 + ) !?ResolvedRefOrOid { │ │559 + var ref_or_oid: RoutablePage.RefOrOid = requested_ref_or_oid orelse .branch; │ │560 + var value: []const u8 = requested_value; │ │561 + // no ref named: fall back to HEAD's branch (or its oid when detached). │ │562 + if (requested_ref_or_oid == null) { │ │563 + var head_buf: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; │ │564 + if (repo.head(io, &head_buf)) |head| switch (head) { │ │565 + .ref => |r| { │ │566 + ref_or_oid = .branch; │ │567 + value = r.name; │ │568 + }, │ │569 + .oid => |o| { │ │570 + ref_or_oid = .object; │ │571 + value = o; │ │572 + }, │ │573 + } else |_| {} │ │574 + } │ │575 + │ │576 + var oid: [hex_len]u8 = undefined; │ │577 + switch (ref_or_oid) { │ │578 + .object => { │ │579 + if (value.len != hex_len) return null; │ │580 + @memcpy(&oid, value); │ │581 + }, │ │582 + .branch, .tag => { │ │583 + const ref_kind: rf.RefKind = if (ref_or_oid == .branch) .head else .tag; │ │584 + oid = (repo.readRef(io, .{ .kind = ref_kind, .name = value }) catch null) orelse return null;│ │585 + }, │ │586 + } │ │587 + // dupe so the value outlives the stack buffer head() may have filled. │ │588 + return .{ .ref_or_oid = ref_or_oid, .value = try aa.dupe(u8, value), .oid = oid }; │ │589 + } │ │590 +}; │ │591 + │ │592 // per-connection mutable state. each SSH session / web session / local TUI │ │593 // run gets its own. │ │594 pub const Session = struct { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │47 const hash_kind = evt.admin_repo_opts.hash; │ │48 const haxy_moment = session.haxy_moment orelse return error.NoMoment; │ │49 │ │50 - // every repo route's stored string starts with "owner/name"; the files │ │51 - // route additionally encodes a directory, the commits route a start oid. │ │50 + // every repo route's stored string starts with "owner/name"; the files and│ │51 + // commits routes additionally encode a ref/oid (files also a directory). │ │52 const name_str: []const u8 = switch (route) { │ │53 .repo, .repo_settings, .repo_auth => |n| n.slice(), │ │54 .repo_commits => |c| c.name.slice(), │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │64 const files_ref_kind = if (tag == .repo) rf.ref_kind else null; │ │65 const files_ref_value = if (tag == .repo) rf.ref_value else ""; │ │66 const files_dir = if (tag == .repo) rf.dir else ""; │ │67 - const commits_start = if (tag == .repo_commits) ui.RoutablePage.repoCommitsStart(name_str) else ""; │ │67 + // a null commits ref means the commits tab opens at the default branch │ │68 + // (Commits.init resolves it). │ │69 + const commits_ref = if (tag == .repo_commits) ui.RoutablePage.repoCommitsRef(name_str) else ui.RoutablePage.CommitsRef{ .ref_or_oid = null, .value = "" };│ │70 // how many diff hunks the commits view's selected commit shows ("load more"). │ │71 const commits_after: usize = switch (route) { │ │72 .repo_commits => |c| c.after, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │89 // resolve the creating user so the header can show their name to the left │ │90 // of the repo title. │ │91 const owner = (try evt.User.readById(DB, hash_kind, haxy_moment, arena, repo.user_id)) orelse return error.NotFound; │ │92 + │ │93 + // build commits first so its resolved ref (the default branch when the route │ │94 + // named none) can canonicalize the commits-tab mirror url. │ │95 + const commits = try Commits.init(arena, session, &found.event_id, rf.identity, commits_ref.ref_or_oid, commits_ref.value, commits_after);│ │96 │ │97 // each tab mirror carries this page's route for that tab; tabs not targeted │ │98 // by the incoming route fall back to their root/first-page route. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 99 const route_name = (ui.RoutablePage.repoFilesRoute(rf.identity, files_ref_kind, files_ref_value, files_dir) orelse return error.NotFound).repo; │ │ 94 - const commits_route_name = if (tag == .repo_commits) route.repo_commits.name else (ui.RoutablePage.repoCommitsRoute(rf.identity, "", 0) orelse return error.NotFound).repo_commits.name;│ │100 + 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; │ │101 │ │102 return .{ │ │103 .header = try Header.init(arena, repo.name, owner.name), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │104 .repo = repo, │ │105 // the repo's event id is the on-disk repo directory name │ │106 .files = try Files.init(arena, session, &found.event_id, rf.identity, files_ref_kind, files_ref_value, files_dir),│ │101 - .commits = try Commits.init(arena, session, &found.event_id, rf.identity, commits_start, commits_after), │ │107 + .commits = commits, │ │108 .refs = try Refs.init(arena, session, &found.event_id, rf.identity, refs_kind, refs_after), │ │109 .settings = Settings.init(), │ │110 .auth = Auth.init(), │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │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. │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯