╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │42 else => return error.UnexpectedRoute, │ │43 }, │ │44 .repo => switch (route) { │ │45 - .repo, .repo_commits, .repo_settings, .repo_auth => .{ .repo = try Repo.init(arena, session, route) }, │ │45 + .repo, .repo_commits, .repo_refs, .repo_settings, .repo_auth => .{ .repo = try Repo.init(arena, session, route) },│ │46 else => return error.UnexpectedRoute, │ │47 }, │ │48 }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │70 user_auth: Array(evt.User.name_max_len), │ │71 repo: Array(repo_route_max_len), │ │72 repo_commits: struct { name: Array(repo_route_max_len), after: usize = 0 }, │ │73 + repo_refs: struct { name: Array(repo_route_max_len), kind: RefKind = .branch, after: usize = 0 },│ │74 repo_settings: Array(repo_route_max_len), │ │75 repo_auth: Array(repo_route_max_len), │ │76 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────╮ │77 pub const default: RoutablePage = .{ .home_users = 0 };│ │78 + │ │79 + pub const RefKind = enum { branch, tag }; │ │80 │ │81 const user_segment = "/user/"; │ │82 const repo_segment = "/repo/"; │ ╰───────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │141 return .{ .repo_commits = .{ .name = Array(repo_route_max_len).from(s) orelse return null, .after = after } }; │ │142 } │ │143 │ │144 + // build a `.repo_refs` route for "owner/name" (identity) paginating `kind`'s │ │145 + // column to `after` (0 = the first window, the other column always at 0). │ │146 + pub fn repoRefsRoute(identity: []const u8, kind: RefKind, after: usize) ?RoutablePage { │ │147 + return .{ .repo_refs = .{ .name = Array(repo_route_max_len).from(identity) orelse return null, .kind = kind, .after = after } };│ │148 + } │ │149 + │ │150 // an inline, owned array of data. keeping it in the route (rather than a │ │151 // borrowed slice) makes RoutablePage a plain value: it can be copied, stored │ │152 // in history, and serialized without any arena tracking. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │175 .home_settings => "/settings", │ │176 .home_auth => "/auth", │ │177 .user, .user_settings, .user_auth => @compileError("user routes are dynamic; use urlAlloc"), │ │169 - .repo, .repo_commits, .repo_settings, .repo_auth => @compileError("repo routes are dynamic; use urlAlloc"), │ │178 + .repo, .repo_commits, .repo_refs, .repo_settings, .repo_auth => @compileError("repo routes are dynamic; use urlAlloc"),│ │179 }; │ │180 } │ │181 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │191 try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}", .{c.name.slice()}) │ │192 else │ │193 try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}?after={d}", .{ c.name.slice(), c.after }), │ │194 + .repo_refs => |r| if (r.after == 0) │ │195 + try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/refs", .{r.name.slice()}) │ │196 + else │ │197 + try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/refs?kind={s}&after={d}", .{ r.name.slice(), @tagName(r.kind), r.after }),│ │198 .repo_settings => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/settings", .{name.slice()}), │ │199 .repo_auth => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/auth", .{name.slice()}), │ │200 inline else => |_, tag| url(tag), │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │205 return switch (self) { │ │206 .home_users, .home_repos, .home_settings, .home_auth => .home, │ │207 .user, .user_settings, .user_auth => .user, │ │195 - .repo, .repo_commits, .repo_settings, .repo_auth => .repo, │ │208 + .repo, .repo_commits, .repo_refs, .repo_settings, .repo_auth => .repo,│ │209 }; │ │210 } │ │211 │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │247 const pair = rest[0 .. user_slash + 1 + repo_name.len]; // "username/reponame" │ │248 if (repo_slash) |s| { │ │249 const sub = after_user[s + 1 ..]; │ │250 + if (std.mem.eql(u8, sub, "refs")) return .{ .repo_refs = .{ │ │251 + .name = Array(repo_route_max_len).from(pair) orelse return null, │ │252 + .kind = refsKind(query), │ │253 + .after = after, │ │254 + } }; │ │255 if (std.mem.eql(u8, sub, "settings")) return .{ .repo_settings = Array(repo_route_max_len).from(pair) orelse return null };│ │256 if (std.mem.eql(u8, sub, "auth")) return .{ .repo_auth = Array(repo_route_max_len).from(pair) orelse return null }; │ │257 if (std.mem.eql(u8, sub, files_seg)) return repoFilesRoute(pair, ""); // trailing /files == root │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │263 return repoFilesRoute(pair, ""); │ │264 } │ │265 return null; │ │266 + } │ │267 + │ │268 + // the refs column a "kind=" query param names; absent or unrecognized reads │ │269 + // as the branches column. │ │270 + fn refsKind(query: ?[]const u8) RefKind { │ │271 + const qs = query orelse return .branch; │ │272 + var it = std.mem.splitScalar(u8, qs, '&'); │ │273 + while (it.next()) |pair| { │ │274 + const eq = std.mem.indexOfScalar(u8, pair, '=') orelse continue; │ │275 + if (std.mem.eql(u8, pair[0..eq], "kind") and std.mem.eql(u8, pair[eq + 1 ..], "tag")) return .tag;│ │276 + } │ │277 + return .branch; │ │278 } │ │279 │ │280 // read a "key=<uint>" value from a raw "a=1&b=2" query string. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │299 .user_auth => |a_name| std.mem.eql(u8, a_name.slice(), b.user_auth.slice()), │ │300 .repo => |a_name| std.mem.eql(u8, a_name.slice(), b.repo.slice()), │ │301 .repo_commits => |a_c| std.mem.eql(u8, a_c.name.slice(), b.repo_commits.name.slice()) and a_c.after == b.repo_commits.after, │ │302 + .repo_refs => |a_r| std.mem.eql(u8, a_r.name.slice(), b.repo_refs.name.slice()) and a_r.kind == b.repo_refs.kind and a_r.after == b.repo_refs.after,│ │303 .repo_settings => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_settings.slice()), │ │304 .repo_auth => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_auth.slice()), │ │305 else => true, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯