╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │281 .identity = rf.identity, │ │282 .files_dir = if (std.meta.activeTag(r) == .repo) rf.dir else "", │ │283 .commits_start = if (std.meta.activeTag(r) == .repo_commits) repoCommitsStart(name) else "",│ │284 + .commits_after = switch (r) { │ │285 + .repo_commits => |c| c.after, │ │286 + else => 0, │ │287 + }, │ │288 }; │ │289 } │ │290 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │294 const cb = repoContent(b) orelse return false; │ │295 return !std.mem.eql(u8, ca.identity, cb.identity) or │ │296 !std.mem.eql(u8, ca.files_dir, cb.files_dir) or │ │276 - !std.mem.eql(u8, ca.commits_start, cb.commits_start); │ │297 + !std.mem.eql(u8, ca.commits_start, cb.commits_start) or │ │298 + ca.commits_after != cb.commits_after; │ │299 } │ │300 │ │279 - // serialize as { "kind": <tag>, "name"?: <user name> }. the default │ │301 + // serialize as { "kind": <tag>, "name"?: <name>, "after"?: <n> }. the default│ │302 // union(enum) codec doesn't round-trip here: Stringify emits a void │ │303 // variant as a bare tag string, but the parser expects an object — so the │ │304 // server->wasm Snapshot JSON would fail to parse. │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │311 try jw.objectField("name"); │ │312 try jw.write(name.slice()); │ │313 }, │ │292 - .repo, .repo_commits, .repo_settings, .repo_auth => |name| {│ │314 + .repo, .repo_settings, .repo_auth => |name| { │ │315 try jw.objectField("name"); │ │316 try jw.write(name.slice()); │ │317 + }, │ │318 + .repo_commits => |c| { │ │319 + try jw.objectField("name"); │ │320 + try jw.write(c.name.slice()); │ │321 + try jw.objectField("after"); │ │322 + try jw.write(c.after); │ │323 }, │ │324 else => {}, │ │325 } │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │327 } │ │328 │ │329 pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !RoutablePage {│ │302 - const Helper = struct { kind: std.meta.Tag(RoutablePage), name: ?[]const u8 = null }; │ │330 + const Helper = struct { kind: std.meta.Tag(RoutablePage), name: ?[]const u8 = null, after: usize = 0 }; │ │331 const helper = try std.json.innerParse(Helper, allocator, source, options); │ │332 const parseName = struct { │ │333 // errors must stay within std.json's ParseError set; ValueTooLong │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │345 .user_settings => .{ .user_settings = try parseName(evt.User.name_max_len, helper.name) }, │ │346 .user_auth => .{ .user_auth = try parseName(evt.User.name_max_len, helper.name) }, │ │347 .repo => .{ .repo = try parseName(repo_route_max_len, helper.name) }, │ │320 - .repo_commits => .{ .repo_commits = try parseName(repo_route_max_len, helper.name) }, │ │348 + .repo_commits => .{ .repo_commits = .{ .name = try parseName(repo_route_max_len, helper.name), .after = helper.after } },│ │349 .repo_settings => .{ .repo_settings = try parseName(repo_route_max_len, helper.name) }, │ │350 .repo_auth => .{ .repo_auth = try parseName(repo_route_max_len, helper.name) }, │ │351 }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │591 }; │ │592 const a_prefix = "a:"; │ │593 if (!std.mem.startsWith(u8, custom, a_prefix)) return null; │ │566 - const route = RoutablePage.fromUrl(custom[a_prefix.len..]) orelse return null; │ │594 + const url = custom[a_prefix.len..]; │ │595 + const q = std.mem.indexOfScalar(u8, url, '?'); │ │596 + const route = RoutablePage.fromUrl(if (q) |i| url[0..i] else url, if (q) |i| url[i + 1 ..] else null) orelse return null;│ │597 // a link to a different parent page always navigates; within the repo page, │ │598 // a files-directory link navigates while settings/auth tab links stay │ │599 // in-page (header-handled). │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │47 │ │48 // every repo route's stored string starts with "owner/name"; the files │ │49 // route additionally encodes a directory, the commits route a start oid. │ │50 - const name = switch (route) { │ │51 - .repo, .repo_commits, .repo_settings, .repo_auth => |n| n, │ │50 + const name_str: []const u8 = switch (route) { │ │51 + .repo, .repo_settings, .repo_auth => |n| n.slice(), │ │52 + .repo_commits => |c| c.name.slice(), │ │53 else => return error.UnexpectedRoute, │ │54 }; │ │54 - const rf = ui.RoutablePage.RepoFiles.parse(name.slice()) orelse return error.NotFound;│ │55 + const rf = ui.RoutablePage.RepoFiles.parse(name_str) orelse return error.NotFound; │ │56 const tag = std.meta.activeTag(route); │ │57 │ │58 // page state lives in the route only for the tab it targets; the other tabs │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │59 // open at their root/first page. │ │60 const files_dir = if (tag == .repo) rf.dir else ""; │ │60 - const commits_start = if (tag == .repo_commits) ui.RoutablePage.repoCommitsStart(name.slice()) else ""; │ │61 + const commits_start = if (tag == .repo_commits) ui.RoutablePage.repoCommitsStart(name_str) else ""; │ │62 + // how many diff hunks the commits view's selected commit shows ("load more"). │ │63 + const commits_after: usize = switch (route) { │ │64 + .repo_commits => |c| c.after, │ │65 + else => 0, │ │66 + }; │ │67 │ │68 const found = (try evt.Repo.readByOwnerAndName(DB, hash_kind, haxy_moment, arena, rf.owner, rf.name)) orelse return error.NotFound;│ │69 const repo = found.repo; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │74 │ │75 // each tab mirror carries this page's route for that tab; tabs not targeted │ │76 // by the incoming route fall back to their root/first-page route. │ │71 - const route_name = if (tag == .repo) name else (ui.RoutablePage.repoFilesRoute(rf.identity, "") orelse return error.NotFound).repo; │ │72 - const commits_route_name = if (tag == .repo_commits) name else (ui.RoutablePage.repoCommitsRoute(rf.identity, "") orelse return error.NotFound).repo_commits; │ │77 + const route_name = (ui.RoutablePage.repoFilesRoute(rf.identity, files_dir) orelse return error.NotFound).repo; │ │78 + 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;│ │79 │ │80 return .{ │ │81 .header = try Header.init(arena, repo.name, owner.name), │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │82 .repo = repo, │ │83 // the repo's event id is the on-disk repo directory name │ │84 .files = try Files.init(arena, session, &found.event_id, rf.identity, files_dir), │ │79 - .commits = try Commits.init(arena, session, &found.event_id, rf.identity, commits_start), │ │85 + .commits = try Commits.init(arena, session, &found.event_id, rf.identity, commits_start, commits_after),│ │86 .settings = Settings.init(), │ │87 .auth = Auth.init(), │ │88 .quit = Quit.init(), │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯