╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────╮ │ 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 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │111 var children: std.StringArrayHashMapUnmanaged(bool) = .empty; // name -> is_dir │ │112 defer children.deinit(gpa); │ │113 const prefix_len = if (dir.len == 0) 0 else dir.len + 1; // skip "dir/" │ │ 61 - for (status.head_tree.entries.keys()) |path| { │ │114 + for (tree.entries.keys()) |path| { │ │115 if (dir.len != 0) { │ │116 if (!std.mem.startsWith(u8, path, dir) or path.len <= dir.len or path[dir.len] != '/') continue;│ │117 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮ │123 if (!gop.found_existing) gop.value_ptr.* = is_dir else if (is_dir) gop.value_ptr.* = true;│ │124 } │ │125 │ │126 + // trees hold no empty directories, so a non-root `dir` with no children │ │127 + // doesn't exist in this ref — a bad url (404). │ │128 + if (dir.len != 0 and children.count() == 0) return error.NotFound; │ │129 + │ │130 // committed paths come out sorted (tree objects are written in sorted │ │131 // order), so the deduped children are already in name order. just group │ │132 // directories before files, keeping that order within each group, and dupe │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │141 } │ │142 } │ │143 } │ │144 + │ │145 + return .{ │ │146 + .identity = try aa.dupe(u8, identity), │ │147 + .ref_kind = eff_kind, │ │148 + .ref_value = try aa.dupe(u8, eff_value), │ │149 + .dir = try aa.dupe(u8, dir), │ │150 + .entries = entries, │ │151 + }; │ │152 +} │ │153 │ │ 88 - return .{ .identity = empty.identity, .dir = empty.dir, .entries = entries }; │ │154 +// a ref that didn't resolve: a 404 when the route named it explicitly, else an │ │155 +// empty listing (e.g. resolving an empty repo's default branch). │ │156 +fn unresolved(aa: std.mem.Allocator, identity: []const u8, explicit: bool, ref_kind: RefKindOrOid, ref_value: []const u8, dir: []const u8) !Self {│ │157 + if (explicit) return error.NotFound; │ │158 + return emptyResult(aa, identity, ref_kind, ref_value, dir); │ │159 +} │ │160 + │ │161 +// an empty listing pinned to a ref, for the wasm / no-repo / unresolved paths. │ │162 +fn emptyResult(aa: std.mem.Allocator, identity: []const u8, ref_kind: RefKindOrOid, ref_value: []const u8, dir: []const u8) !Self { │ │163 + return .{ │ │164 + .identity = try aa.dupe(u8, identity), │ │165 + .ref_kind = ref_kind, │ │166 + .ref_value = try aa.dupe(u8, ref_value), │ │167 + .dir = try aa.dupe(u8, dir), │ │168 + .entries = &.{}, │ │169 + }; │ │170 } │ │171 │ │172 pub const View = struct { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │185 // page arena (as long as this page's widget tree). │ │186 const aa = session.page_arena.allocator(); │ │187 if (data.dir.len != 0) { │ │107 - try addRow(allocator, &box, "..", try dirLink(session.page_arena, data.identity, parentDir(data.dir)));│ │188 + try addRow(allocator, &box, "..", try dirLink(session.page_arena, data, parentDir(data.dir))); │ │189 } │ │190 for (data.entries) |entry| { │ │191 const label = if (entry.is_dir) try std.fmt.allocPrint(aa, "{s}/", .{entry.name}) else entry.name; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯