╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │36 else => return error.UnexpectedRoute, │ │37 }, │ │38 .repo => switch (route) { │ │39 - .repo, .repo_settings, .repo_auth => |name| .{ .repo = try Repo.init(arena, session, name) }, │ │39 + .repo, .repo_commits, .repo_settings, .repo_auth => .{ .repo = try Repo.init(arena, session, route) },│ │40 else => return error.UnexpectedRoute, │ │41 }, │ │42 }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────╮ │63 user_settings: Array(evt.User.name_max_len),│ │64 user_auth: Array(evt.User.name_max_len), │ │65 repo: Array(repo_route_max_len), │ │66 + repo_commits: Array(repo_route_max_len), │ │67 repo_settings: Array(repo_route_max_len), │ │68 repo_auth: Array(repo_route_max_len), │ │69 │ ╰────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────╮ │72 const user_segment = "/user/"; │ │73 const repo_segment = "/repo/"; │ │74 const files_seg = "files"; │ │75 + const commits_seg = "commits"; │ │76 │ │77 // a repo route is "username/reponame", optionally followed by│ │78 // "/files/<dir>" for a directory in the files tab │ ╰──────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │110 var buf: [repo_route_max_len]u8 = undefined; │ │111 const s = std.fmt.bufPrint(&buf, "{s}/" ++ files_seg ++ "/{s}", .{ identity, dir }) catch return null; │ │112 return .{ .repo = Array(repo_route_max_len).from(s) orelse return null }; │ │113 + } │ │114 + │ │115 + // the start oid of a `.repo_commits` route's stored string ("" = page 1, │ │116 + // which the commits tab walks from HEAD). │ │117 + pub fn repoCommitsStart(s: []const u8) []const u8 { │ │118 + const marker = "/" ++ commits_seg ++ "/"; │ │119 + const i = std.mem.indexOf(u8, s, marker) orelse return ""; │ │120 + return s[i + marker.len ..]; │ │121 + } │ │122 + │ │123 + // build a `.repo_commits` route for "owner/name" (identity) at `start_oid` │ │124 + // ("" = the first page). always carries the `commits` marker so page 1 │ │125 + // ("owner/name/commits") doesn't collide with the files root. │ │126 + pub fn repoCommitsRoute(identity: []const u8, start_oid: []const u8) ?RoutablePage { │ │127 + var buf: [repo_route_max_len]u8 = undefined; │ │128 + const s = if (start_oid.len == 0) │ │129 + std.fmt.bufPrint(&buf, "{s}/" ++ commits_seg, .{identity}) catch return null │ │130 + else │ │131 + std.fmt.bufPrint(&buf, "{s}/" ++ commits_seg ++ "/{s}", .{ identity, start_oid }) catch return null;│ │132 + return .{ .repo_commits = Array(repo_route_max_len).from(s) orelse return null }; │ │133 } │ │134 │ │135 // an inline, owned array of data. keeping it in the route (rather than a │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │160 .home_settings => "/settings", │ │161 .home_auth => "/auth", │ │162 .user, .user_settings, .user_auth => @compileError("user routes are dynamic; use urlAlloc"), │ │141 - .repo, .repo_settings, .repo_auth => @compileError("repo routes are dynamic; use urlAlloc"), │ │163 + .repo, .repo_commits, .repo_settings, .repo_auth => @compileError("repo routes are dynamic; use urlAlloc"),│ │164 }; │ │165 } │ │166 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │170 .user_settings => |name| try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/settings", .{name.slice()}),│ │171 .user_auth => |name| try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/auth", .{name.slice()}), │ │172 .repo => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}", .{name.slice()}), │ │173 + // the stored string already carries the "commits[/oid]" suffix. │ │174 + .repo_commits => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}", .{name.slice()}), │ │175 .repo_settings => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/settings", .{name.slice()}),│ │176 .repo_auth => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/auth", .{name.slice()}), │ │177 inline else => |_, tag| url(tag), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │182 return switch (self) { │ │183 .home_users, .home_repos, .home_settings, .home_auth => .home,│ │184 .user, .user_settings, .user_auth => .user, │ │161 - .repo, .repo_settings, .repo_auth => .repo, │ │185 + .repo, .repo_commits, .repo_settings, .repo_auth => .repo, │ │186 }; │ │187 } │ │188 │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │226 if (std.mem.eql(u8, sub, "auth")) return .{ .repo_auth = Array(repo_route_max_len).from(pair) orelse return null };│ │227 if (std.mem.eql(u8, sub, files_seg)) return repoFilesRoute(pair, ""); // trailing /files == root │ │228 if (std.mem.startsWith(u8, sub, files_seg ++ "/")) return repoFilesRoute(pair, sub[files_seg.len + 1 ..]); │ │229 + if (std.mem.eql(u8, sub, commits_seg)) return repoCommitsRoute(pair, ""); // trailing /commits == page 1 │ │230 + if (std.mem.startsWith(u8, sub, commits_seg ++ "/")) return repoCommitsRoute(pair, sub[commits_seg.len + 1 ..]); │ │231 return null; // unknown sub-path │ │232 } │ │233 return repoFilesRoute(pair, ""); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │242 .user_settings => |a_name| std.mem.eql(u8, a_name.slice(), b.user_settings.slice()),│ │243 .user_auth => |a_name| std.mem.eql(u8, a_name.slice(), b.user_auth.slice()), │ │244 .repo => |a_name| std.mem.eql(u8, a_name.slice(), b.repo.slice()), │ │245 + .repo_commits => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_commits.slice()), │ │246 .repo_settings => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_settings.slice()),│ │247 .repo_auth => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_auth.slice()), │ │248 else => true, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │249 }; │ │250 } │ │251 │ │225 - // true when `a` and `b` are both `.repo` files routes pointing at different │ │226 - // directories. a repo directory change is a navigation (its own page); │ │227 - // settings/auth tab switches are not, so they don't satisfy this. │ │228 - pub fn repoDirChanged(a: RoutablePage, b: RoutablePage) bool { │ │229 - return std.meta.activeTag(a) == .repo and std.meta.activeTag(b) == .repo and !a.eql(b); │ │252 + // the content a repo route renders: which repo, plus the files directory and │ │253 + // commits page. tab switches keep this fixed (the page already holds every │ │254 + // tab's data); a files-directory or commits-page change alters it. │ │255 + const RepoContent = struct { identity: []const u8, files_dir: []const u8, commits_start: []const u8 };│ │256 + │ │257 + fn repoContent(r: RoutablePage) ?RepoContent { │ │258 + const name = switch (r) { │ │259 + .repo, .repo_commits, .repo_settings, .repo_auth => |n| n.slice(), │ │260 + else => return null, │ │261 + }; │ │262 + const rf = RepoFiles.parse(name) orelse return null; │ │263 + return .{ │ │264 + .identity = rf.identity, │ │265 + .files_dir = if (std.meta.activeTag(r) == .repo) rf.dir else "", │ │266 + .commits_start = if (std.meta.activeTag(r) == .repo_commits) repoCommitsStart(name) else "", │ │267 + }; │ │268 + } │ │269 + │ │270 + // true when `a` and `b` are both repo routes that render different content │ │271 + pub fn repoPageChanged(a: RoutablePage, b: RoutablePage) bool { │ │272 + const ca = repoContent(a) orelse return false; │ │273 + const cb = repoContent(b) orelse return false; │ │274 + return !std.mem.eql(u8, ca.identity, cb.identity) or │ │275 + !std.mem.eql(u8, ca.files_dir, cb.files_dir) or │ │276 + !std.mem.eql(u8, ca.commits_start, cb.commits_start); │ │277 } │ │278 │ │279 // serialize as { "kind": <tag>, "name"?: <user name> }. the default │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯