╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │111 - const link = if (entry.is_dir) try dirLink(session.page_arena, data.identity, try childDir(aa, data.dir, entry.name)) else "";│ │192 + const link = if (entry.is_dir) try dirLink(session.page_arena, data, try childDir(aa, data.dir, entry.name)) else ""; │ │193 try addRow(allocator, &box, label, link); │ │194 } │ │195 if (box.children.count() > 0) box.getFocus().child_id = box.children.keys()[0]; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │278 } │ │279 }; │ │280 │ │200 -// the "a:" link for the files route at `dir` within `identity` ("owner/name"). │ │201 -fn dirLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, dir: []const u8) ![]const u8 { │ │202 - const route = ui.RoutablePage.repoFilesRoute(identity, dir) orelse return error.RouteTooLong; │ │281 +// the "a:" link for the files route at `dir`, pinned to the listing's ref. │ │282 +fn dirLink(page_arena: *std.heap.ArenaAllocator, data: *const Self, dir: []const u8) ![]const u8 { │ │283 + const route = ui.RoutablePage.repoFilesRoute(data.identity, data.ref_kind, data.ref_value, dir) orelse return error.RouteTooLong;│ │284 const url = try route.urlAlloc(page_arena); │ │285 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); │ │286 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Header.zig│ ╰──────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │88 // the global settings or auth pages. the files tab routes through the │ │89 // shared helper so the /repo/.../files url format lives in one place. │ │90 const identity = try std.fmt.allocPrint(aa, "{s}/{s}", .{ data.owner_name, data.name }); │ │91 - const files_route = ui.RoutablePage.repoFilesRoute(identity, "") orelse return error.RouteTooLong; │ │91 + const files_route = ui.RoutablePage.repoFilesRoute(identity, null, "", "") orelse return error.RouteTooLong;│ │92 const files_link = try std.fmt.allocPrint(aa, "a:{s}", .{try files_route.urlAlloc(session.page_arena)}); │ │93 const commits_route = ui.RoutablePage.repoCommitsRoute(identity, "", 0) orelse return error.RouteTooLong; │ │94 const commits_link = try std.fmt.allocPrint(aa, "a:{s}", .{try commits_route.urlAlloc(session.page_arena)});│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯