╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────╮ │400 const ca = repoContent(a) orelse return false; │ │401 const cb = repoContent(b) orelse return false; │ │402 return !std.mem.eql(u8, ca.identity, cb.identity) or │ │403 + ca.files_ref_kind != cb.files_ref_kind or │ │404 + !std.mem.eql(u8, ca.files_ref_value, cb.files_ref_value) or│ │405 !std.mem.eql(u8, ca.files_dir, cb.files_dir) or │ │406 !std.mem.eql(u8, ca.commits_start, cb.commits_start) or │ │407 ca.commits_after != cb.commits_after or │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │59 const tag = std.meta.activeTag(route); │ │60 │ │61 // page state lives in the route only for the tab it targets; the other tabs │ │62 - // open at their root/first page. │ │62 + // open at their root/first page. a null ref kind means the files tab opens at │ │63 + // the default branch (Files.init resolves it). │ │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 "";│ │68 // how many diff hunks the commits view's selected commit shows ("load more"). │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │90 │ │91 // each tab mirror carries this page's route for that tab; tabs not targeted │ │92 // by the incoming route fall back to their root/first-page route. │ │90 - const route_name = (ui.RoutablePage.repoFilesRoute(rf.identity, files_dir) orelse return error.NotFound).repo; │ │93 + 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;│ │95 │ │96 return .{ │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 97 .header = try Header.init(arena, repo.name, owner.name), │ │ 98 .repo = repo, │ │ 99 // the repo's event id is the on-disk repo directory name │ │ 97 - .files = try Files.init(arena, session, &found.event_id, rf.identity, files_dir), │ │100 + .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), │ │102 .refs = try Refs.init(arena, session, &found.event_id, rf.identity, refs_kind, refs_after), │ │103 .settings = Settings.init(), │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭───────────────────────────────────────╮ │ 3 const ui = @import("../../ui.zig");│ │ 4 const xit = @import("xit"); │ │ 5 const rp = xit.repo; │ │ 6 +const tr = xit.tree; │ │ 7 +const rf = xit.ref; │ │ 8 +const hash = xit.hash; │ │ 9 const xitui = xit.xitui; │ │10 const wgt = xitui.widget; │ │11 const layout = xitui.layout; │ ╰───────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │12 const inp = xitui.input; │ │13 const Grid = xitui.grid.Grid; │ │14 const Focus = xitui.focus.Focus; │ │15 + │ │16 +const RefKindOrOid = ui.RoutablePage.RefKindOrOid; │ │17 +// the hex-oid length for the default repo options the on-disk repos use.│ │18 +const hex_len = hash.hexLen((rp.RepoOpts(.xit){}).hash); │ │19 │ │20 // one entry in the directory currently being viewed. │ │21 pub const Entry = struct { │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │25 │ │26 // "owner/name", so the view can build /repo/owner/name/files/... links. │ │27 identity: []const u8, │ │28 +// the resolved ref this listing is read at (the default branch when the route│ │29 +// didn't name one), so the view's directory links stay pinned to it. │ │30 +ref_kind: RefKindOrOid, │ │31 +ref_value: []const u8, │ │32 // the directory being viewed, relative to the repo root ("" at the root). │ │33 dir: []const u8, │ │34 entries: []const Entry, │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────╮ │40 session: *ui.Session, │ │41 event_id: *const [evt.event_id_size]u8,│ │42 identity: []const u8, │ │43 + req_kind: ?RefKindOrOid, │ │44 + req_value: []const u8, │ │45 dir: []const u8, │ │46 ) !Self { │ │47 const aa = arena.allocator(); │ ╰───────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │35 - const empty: Self = .{ .identity = try aa.dupe(u8, identity), .dir = try aa.dupe(u8, dir), .entries = &.{} }; │ │48 │ │37 - // no filesystem (wasm) or nowhere to look: empty listing. the wasm path │ │38 - // never calls init anyway — it rebuilds from the serialized snapshot. │ │39 - const io = session.io orelse return empty; │ │40 - const repos_dir = session.repos_dir orelse return empty; │ │49 + // no filesystem (wasm) or nowhere to look: empty listing pinned to whatever │ │50 + // ref the route asked for. the wasm path never calls init anyway — it │ │51 + // rebuilds from the serialized snapshot. │ │52 + const io = session.io orelse return emptyResult(aa, identity, req_kind orelse .branch, req_value, dir); │ │53 + const repos_dir = session.repos_dir orelse return emptyResult(aa, identity, req_kind orelse .branch, req_value, dir);│ │54 │ │55 // the repo's working copy lives at <repos_dir>/<hex event id>. │ │56 const hex = std.fmt.bytesToHex(event_id.*, .lower); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 60 // (transient; freed before init returns); the listing is built into the │ │ 61 // page arena so it outlives them. │ │ 62 const gpa = arena.child_allocator; │ │ 50 - var repo = rp.Repo(.xit, .{}).open(io, gpa, .{ .path = repo_path }) catch return empty; │ │ 63 + var repo = rp.Repo(.xit, .{}).open(io, gpa, .{ .path = repo_path }) catch return emptyResult(aa, identity, req_kind orelse .branch, req_value, dir);│ │ 64 defer repo.deinit(io, gpa); │ │ 52 - var status = repo.status(io, gpa) catch return empty; │ │ 53 - defer status.deinit(gpa); │ │ 65 + │ │ 66 + // resolve the effective ref: the one named in the route, else HEAD's branch. │ │ 67 + // ref_value is duped immediately so it outlives the stack buffer head() fills. │ │ 68 + var eff_kind: RefKindOrOid = req_kind orelse .branch; │ │ 69 + var eff_value: []const u8 = req_value; │ │ 70 + if (req_kind == null) { │ │ 71 + var head_buf: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; │ │ 72 + if (repo.head(io, &head_buf)) |head| switch (head) { │ │ 73 + .ref => |ref| { │ │ 74 + eff_kind = .branch; │ │ 75 + eff_value = try aa.dupe(u8, ref.name); │ │ 76 + }, │ │ 77 + .oid => |oid| { │ │ 78 + eff_kind = .oid; │ │ 79 + eff_value = try aa.dupe(u8, oid); │ │ 80 + }, │ │ 81 + } else |_| {} │ │ 82 + } │ │ 83 + │ │ 84 + // resolve the ref to the commit oid whose tree we list. a ref the route │ │ 85 + // named explicitly that doesn't resolve is a bad url (NotFound -> 404); the │ │ 86 + // default-branch path instead falls through to an empty listing below. │ │ 87 + const explicit = req_kind != null; │ │ 88 + var oid: [hex_len]u8 = undefined; │ │ 89 + switch (eff_kind) { │ │ 90 + .oid => { │ │ 91 + if (eff_value.len != hex_len) return unresolved(aa, identity, explicit, eff_kind, eff_value, dir); │ │ 92 + @memcpy(&oid, eff_value); │ │ 93 + }, │ │ 94 + .branch, .tag => { │ │ 95 + const ref_kind: rf.RefKind = if (eff_kind == .branch) .head else .tag; │ │ 96 + oid = (repo.readRef(io, .{ .kind = ref_kind, .name = eff_value }) catch null) orelse │ │ 97 + return unresolved(aa, identity, explicit, eff_kind, eff_value, dir); │ │ 98 + }, │ │ 99 + } │ │100 + │ │101 + // read the tree at that commit. building the read-only state mirrors what │ │102 + // repo.status does internally, but for an arbitrary commit rather than HEAD. │ │103 + var moment = repo.core.latestMoment() catch return emptyResult(aa, identity, eff_kind, eff_value, dir); │ │104 + const state = rp.Repo(.xit, .{}).State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; │ │105 + var tree = tr.Tree(.xit, .{}).init(state, io, gpa, &oid) catch return emptyResult(aa, identity, eff_kind, eff_value, dir); │ │106 + defer tree.deinit(); │ │107 │ │108 // collect the immediate children of `dir`. each committed path is a full │ │109 // file path; a child is a directory when more path follows its first │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯