╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────╮ │248 │ │249 const user_segment = "/user/"; │ │250 const repo_segment = "/repo/"; │ │251 + const fork_segment = "/fork/"; │ │252 const files_seg = "files"; │ │253 const commits_seg = "commits"; │ │254 // ends a commits route naming its commit's full message│ ╰─────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │351 view: EventsView = .active, │ │352 kind: ?evt.EventKind = null, │ │353 selected: Array(evt.event_id_size * 2) = .{}, │ │354 + }; │ │355 + │ │356 + pub const ForkRoute = struct { │ │357 + name: Array(repo_identity_max_len), │ │358 + id: Array(evt.event_id_size * 2), │ │359 + }; │ │360 + │ │361 + pub const ForkFilesRoute = struct { │ │362 + fork: ForkRoute, │ │363 + oid: Array(ref_route_max_len) = .{}, │ │364 + path: Array(repo_route_max_len) = .{}, │ │365 + line: usize = 0, │ │366 + }; │ │367 + │ │368 + pub const ForkCommitsRoute = struct { │ │369 + fork: ForkRoute, │ │370 + oid: Array(ref_route_max_len) = .{}, │ │371 + content: RepoCommitsRoute.Content = .{ .diff = .{} }, │ │372 }; │ │373 + │ │374 + // where the shared files and commits views are mounted. fork routes always │ │375 + // read their implicit `patch` branch unless they name an object directly. │ │376 + pub const RepoLocation = union(enum) { │ │377 + repo: []const u8, │ │378 + fork: struct { │ │379 + identity: []const u8, │ │380 + id: []const u8, │ │381 + target_branch: []const u8, │ │382 + }, │ │383 + │ │384 + pub fn dupe(self: RepoLocation, allocator: std.mem.Allocator) !RepoLocation { │ │385 + return switch (self) { │ │386 + .repo => |identity_value| .{ .repo = try allocator.dupe(u8, identity_value) }, │ │387 + .fork => |f| .{ .fork = .{ │ │388 + .identity = try allocator.dupe(u8, f.identity), │ │389 + .id = try allocator.dupe(u8, f.id), │ │390 + .target_branch = try allocator.dupe(u8, f.target_branch), │ │391 + } }, │ │392 + }; │ │393 + } │ │394 │ │395 + pub fn filesRoute(self: RepoLocation, ref_or_oid: RefOrOid, value: []const u8, path: []const u8, line: usize) ?RoutablePage { │ │396 + return switch (self) { │ │397 + .repo => |repo_identity| repoFilesRoute(repo_identity, ref_or_oid, value, path, line), │ │398 + .fork => |f| if (ref_or_oid == .branch and std.mem.eql(u8, value, "patch")) │ │399 + forkFilesRoute(f.identity, f.id, "", path, line) │ │400 + else if (ref_or_oid == .object) │ │401 + forkFilesRoute(f.identity, f.id, value, path, line) │ │402 + else │ │403 + null, │ │404 + }; │ │405 + } │ │406 + │ │407 + pub fn commitsRoute(self: RepoLocation, ref_or_oid: ?RefOrOid, value: []const u8, start: usize, path: []const u8) ?RoutablePage {│ │408 + return switch (self) { │ │409 + .repo => |repo_identity| repoCommitsRoute(repo_identity, ref_or_oid, value, start, path), │ │410 + .fork => |f| if (ref_or_oid == null or (ref_or_oid == .branch and std.mem.eql(u8, value, "patch"))) │ │411 + forkCommitsRoute(f.identity, f.id, "", start, path) │ │412 + else if (ref_or_oid == .object) │ │413 + forkCommitsRoute(f.identity, f.id, value, start, path) │ │414 + else │ │415 + null, │ │416 + }; │ │417 + } │ │418 + │ │419 + pub fn commitMessageRoute(self: RepoLocation, ref_or_oid: RefOrOid, value: []const u8) ?RoutablePage { │ │420 + return switch (self) { │ │421 + .repo => |repo_identity| repoCommitMessageRoute(repo_identity, ref_or_oid, value), │ │422 + .fork => |f| if (ref_or_oid == .object) forkCommitMessageRoute(f.identity, f.id, value) else null, │ │423 + }; │ │424 + } │ │425 + }; │ │426 + │ │427 + fn initForkRoute(identity: []const u8, id: []const u8) ?ForkRoute { │ │428 + if (id.len != evt.event_id_size * 2) return null; │ │429 + return .{ │ │430 + .name = Array(repo_identity_max_len).from(identity) orelse return null, │ │431 + .id = Array(evt.event_id_size * 2).from(id) orelse return null, │ │432 + }; │ │433 + } │ │434 + │ │435 + pub fn forkPatchRoute(identity: []const u8, id: []const u8) ?RoutablePage { │ │436 + return .{ .fork_patch = initForkRoute(identity, id) orelse return null }; │ │437 + } │ │438 + │ │439 + pub fn forkFilesRoute(identity: []const u8, id: []const u8, oid: []const u8, path: []const u8, line: usize) ?RoutablePage { │ │440 + return .{ .fork_files = .{ │ │441 + .fork = initForkRoute(identity, id) orelse return null, │ │442 + .oid = Array(ref_route_max_len).from(oid) orelse return null, │ │443 + .path = Array(repo_route_max_len).from(path) orelse return null, │ │444 + .line = line, │ │445 + } }; │ │446 + } │ │447 + │ │448 + pub fn forkCommitsRoute(identity: []const u8, id: []const u8, oid: []const u8, start: usize, path: []const u8) ?RoutablePage { │ │449 + return .{ .fork_commits = .{ │ │450 + .fork = initForkRoute(identity, id) orelse return null, │ │451 + .oid = Array(ref_route_max_len).from(oid) orelse return null, │ │452 + .content = .{ .diff = .{ │ │453 + .start = start, │ │454 + .path = Array(repo_route_max_len).from(path) orelse return null, │ │455 + } }, │ │456 + } }; │ │457 + } │ │458 + │ │459 + pub fn forkCommitMessageRoute(identity: []const u8, id: []const u8, oid: []const u8) ?RoutablePage { │ │460 + if (oid.len == 0) return null; │ │461 + return .{ .fork_commits = .{ │ │462 + .fork = initForkRoute(identity, id) orelse return null, │ │463 + .oid = Array(ref_route_max_len).from(oid) orelse return null, │ │464 + .content = .message, │ │465 + } }; │ │466 + } │ │467 + │ │468 + pub fn forkSettingsRoute(identity: []const u8, id: []const u8) ?RoutablePage { │ │469 + return .{ .fork_settings = initForkRoute(identity, id) orelse return null }; │ │470 + } │ │471 + │ │472 + pub fn forkAuthRoute(identity: []const u8, id: []const u8) ?RoutablePage { │ │473 + return .{ .fork_auth = initForkRoute(identity, id) orelse return null }; │ │474 + } │ │475 + │ │476 // build a `.repo_files` route (a null ref_kind = the bare default-branch │ │477 // root). null if the result doesn't fit the inline name. │ │478 pub fn repoFilesRoute(identity: []const u8, ref_kind: ?RefOrOid, ref_value: []const u8, dir: []const u8, line: usize) ?RoutablePage {│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │939 }, │ │940 .repo_settings => |name| try std.fmt.allocPrint(arena.allocator(), "{s}/settings", .{try repoUrlPrefix(arena, name.slice())}), │ │941 .repo_auth => |name| try std.fmt.allocPrint(arena.allocator(), "{s}/auth", .{try repoUrlPrefix(arena, name.slice())}), │ │942 + .fork_patch => |f| try std.fmt.allocPrint(arena.allocator(), fork_segment ++ "{s}/" ++ patch_seg ++ "{s}", .{ f.name.slice(), f.id.slice() }), │ │943 + .fork_files => |f| blk: { │ │944 + var out: std.Io.Writer.Allocating = .init(arena.allocator()); │ │945 + try out.writer.print(fork_segment ++ "{s}/" ++ patch_seg ++ "{s}/" ++ files_seg, .{ f.fork.name.slice(), f.fork.id.slice() }); │ │946 + if (f.oid.len != 0) try out.writer.print("/object:{s}", .{f.oid.slice()}); │ │947 + if (f.line != 0) try out.writer.print("/" ++ line_seg ++ "{d}", .{f.line}); │ │948 + if (f.path.len != 0) try out.writer.print("/" ++ path_seg ++ "{s}", .{f.path.slice()}); │ │949 + break :blk out.written(); │ │950 + }, │ │951 + .fork_commits => |c| blk: { │ │952 + var out: std.Io.Writer.Allocating = .init(arena.allocator()); │ │953 + try out.writer.print(fork_segment ++ "{s}/" ++ patch_seg ++ "{s}/" ++ commits_seg, .{ c.fork.name.slice(), c.fork.id.slice() }); │ │954 + if (c.oid.len != 0) try out.writer.print("/object:{s}", .{c.oid.slice()}); │ │955 + switch (c.content) { │ │956 + .diff => |d| { │ │957 + if (d.start != 0) try out.writer.print("/" ++ start_seg ++ "{d}", .{d.start}); │ │958 + if (d.path.len != 0) try out.writer.print("/" ++ path_seg ++ "{s}", .{d.path.slice()}); │ │959 + }, │ │960 + .message => try out.writer.print("/" ++ message_seg, .{}), │ │961 + } │ │962 + break :blk out.written(); │ │963 + }, │ │964 + .fork_settings => |f| try std.fmt.allocPrint(arena.allocator(), fork_segment ++ "{s}/" ++ patch_seg ++ "{s}/settings", .{ f.name.slice(), f.id.slice() }),│ │965 + .fork_auth => |f| try std.fmt.allocPrint(arena.allocator(), fork_segment ++ "{s}/" ++ patch_seg ++ "{s}/auth", .{ f.name.slice(), f.id.slice() }), │ │966 }; │ │967 } │ │968 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 986 if (std.mem.eql(u8, sub, "settings")) return if (segments.next() == null) .{ .user_settings = parsed } else null; │ │ 987 if (std.mem.eql(u8, sub, "auth")) return if (segments.next() == null) .{ .user_auth = parsed } else null; │ │ 988 return null; // unknown sub-path │ │ 989 + } │ │ 990 + // "fork/<username>/<reponame>/patch:<id>[/files|/commits]"; the │ │ 991 + // patch branch is implicit, while object ids and view-specific params │ │ 992 + // use the same tails as the repo files and commits routes. │ │ 993 + if (std.mem.eql(u8, first, "fork")) { │ │ 994 + const rest = segments.rest(); │ │ 995 + const owner = segments.next() orelse return null; │ │ 996 + const repo_name = segments.next() orelse return null; │ │ 997 + if (owner.len == 0 or repo_name.len == 0) return null; │ │ 998 + const identity = rest[0 .. owner.len + 1 + repo_name.len]; │ │ 999 + const patch = segments.next() orelse return null; │ │1000 + if (!std.mem.startsWith(u8, patch, patch_seg)) return null; │ │1001 + const id = patch[patch_seg.len..]; │ │1002 + if (id.len != evt.event_id_size * 2) return null; │ │1003 + const tab = segments.next() orelse return forkPatchRoute(identity, id); │ │1004 + var params = Params{}; │ │1005 + if (std.mem.eql(u8, tab, files_seg)) { │ │1006 + params.scanPairs(&segments) catch return null; │ │1007 + if (!params.only(&.{ .line, .object })) return null; │ │1008 + const line = params.line() orelse return null; │ │1009 + const ref = params.ref() catch return null; │ │1010 + const oid = if (ref) |value| if (value.kind == .object) value.value else return null else ""; │ │1011 + const file_path = pathValue(segments.rest()) orelse return null; │ │1012 + return forkFilesRoute(identity, id, oid, file_path, line); │ │1013 + } │ │1014 + if (std.mem.eql(u8, tab, commits_seg)) { │ │1015 + params.scanPairs(&segments) catch return null; │ │1016 + if (!params.only(&.{ .start, .object })) return null; │ │1017 + const start = params.start() orelse return null; │ │1018 + const ref = params.ref() catch return null; │ │1019 + const oid = if (ref) |value| if (value.kind == .object) value.value else return null else ""; │ │1020 + if (std.mem.eql(u8, segments.rest(), message_seg)) │ │1021 + return if (start == 0 and oid.len != 0) forkCommitMessageRoute(identity, id, oid) else null; │ │1022 + const file_path = pathValue(segments.rest()) orelse return null; │ │1023 + return forkCommitsRoute(identity, id, oid, start, file_path); │ │1024 + } │ │1025 + if (std.mem.eql(u8, tab, "settings")) return if (segments.next() == null) forkSettingsRoute(identity, id) else null;│ │1026 + if (std.mem.eql(u8, tab, "auth")) return if (segments.next() == null) forkAuthRoute(identity, id) else null; │ │1027 + return null; │ │1028 } │ │1029 // "repo/<username>/<reponame>[/<tab tail>]"; the bare pair is the │ │1030 // files root │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │1084 }; │ │1085 } │ │1086 │ │1087 + pub fn forkRoute(self: *const RoutablePage) ?*const ForkRoute { │ │1088 + return switch (self.*) { │ │1089 + .fork_patch => |*f| f, │ │1090 + .fork_files => |*f| &f.fork, │ │1091 + .fork_commits => |*f| &f.fork, │ │1092 + .fork_settings => |*f| f, │ │1093 + .fork_auth => |*f| f, │ │1094 + else => null, │ │1095 + }; │ │1096 + } │ │1097 + │ │1098 // the route this route's page lands on, with no tab or params: the users│ │1099 // list, a user's repos, or a repo's files root. │ │1100 pub fn pageRoot(self: RoutablePage) RoutablePage { │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1108 }, │ │1109 // every repo route stores the same identity, so it always fits │ │1110 .repo => repoFilesRoute(self.repoIdentity() orelse return self, null, "", "", 0) orelse self,│ │1111 + .fork => blk: { │ │1112 + const f = self.forkRoute() orelse break :blk self; │ │1113 + break :blk forkPatchRoute(f.name.slice(), f.id.slice()) orelse self; │ │1114 + }, │ │1115 }; │ │1116 } │ │1117 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1120 .home_users, .home_repos, .home_settings, .home_auth => .home, │ │1121 .user_repos, .user_forks, .user_settings, .user_auth => .user, │ │1122 .repo_files, .repo_commits, .repo_refs, .repo_issues, .repo_patches, .repo_discussions, .repo_events, .repo_settings, .repo_auth => .repo,│ │1123 + .fork_patch, .fork_files, .fork_commits, .fork_settings, .fork_auth => .fork, │ │1124 }; │ │1125 } │ │1126 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1392 std.mem.eql(u8, a_e.selected.slice(), b.repo_events.selected.slice()), │ │1393 .repo_settings => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_settings.slice()), │ │1394 .repo_auth => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_auth.slice()), │ │1395 + .fork_patch => |a_f| std.mem.eql(u8, a_f.name.slice(), b.fork_patch.name.slice()) and std.mem.eql(u8, a_f.id.slice(), b.fork_patch.id.slice()), │ │1396 + .fork_files => |a_f| std.mem.eql(u8, a_f.fork.name.slice(), b.fork_files.fork.name.slice()) and │ │1397 + std.mem.eql(u8, a_f.fork.id.slice(), b.fork_files.fork.id.slice()) and │ │1398 + std.mem.eql(u8, a_f.oid.slice(), b.fork_files.oid.slice()) and │ │1399 + std.mem.eql(u8, a_f.path.slice(), b.fork_files.path.slice()) and │ │1400 + a_f.line == b.fork_files.line, │ │1401 + .fork_commits => |a_c| std.mem.eql(u8, a_c.fork.name.slice(), b.fork_commits.fork.name.slice()) and │ │1402 + std.mem.eql(u8, a_c.fork.id.slice(), b.fork_commits.fork.id.slice()) and │ │1403 + std.mem.eql(u8, a_c.oid.slice(), b.fork_commits.oid.slice()) and │ │1404 + switch (a_c.content) { │ │1405 + .diff => |a_d| switch (b.fork_commits.content) { │ │1406 + .diff => |b_d| a_d.start == b_d.start and std.mem.eql(u8, a_d.path.slice(), b_d.path.slice()), │ │1407 + .message => false, │ │1408 + }, │ │1409 + .message => std.meta.activeTag(b.fork_commits.content) == .message, │ │1410 + }, │ │1411 + .fork_settings => |a_f| std.mem.eql(u8, a_f.name.slice(), b.fork_settings.name.slice()) and std.mem.eql(u8, a_f.id.slice(), b.fork_settings.id.slice()),│ │1412 + .fork_auth => |a_f| std.mem.eql(u8, a_f.name.slice(), b.fork_auth.name.slice()) and std.mem.eql(u8, a_f.id.slice(), b.fork_auth.id.slice()), │ │1413 else => true, │ │1414 }; │ │1415 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │1422 a.repo_refs.from.len == 0 and b.repo_refs.from.len == 0) │ │1423 return !std.mem.eql(u8, a.repo_refs.name.slice(), b.repo_refs.name.slice());│ │1424 return !a.eql(b); │ │1425 + } │ │1426 + │ │1427 + pub fn forkPageChanged(a: RoutablePage, b: RoutablePage) bool { │ │1428 + return a.parent() == .fork and b.parent() == .fork and !a.eql(b); │ │1429 } │ │1430 │ │1431 // true when `a` and `b` are the same user paginated to a different repos │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2014 // a link to a different parent page always navigates; within a page, a │ │2015 // files-directory / commits-page / list-window change navigates while tab │ │2016 // links stay in-page. │ │1783 - if (route.parent() != current.parent() or RoutablePage.repoPageChanged(route, current) or RoutablePage.homePageChanged(route, current) or RoutablePage.userPageChanged(route, current)) return route; │ │2017 + if (route.parent() != current.parent() or RoutablePage.repoPageChanged(route, current) or RoutablePage.forkPageChanged(route, current) or RoutablePage.homePageChanged(route, current) or RoutablePage.userPageChanged(route, current)) return route;│ │2018 return null; │ │2019 } │ │2020 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯