╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │1048 .home_about => "/", │ │1049 .home_users => |u| blk: { │ │1050 var out: std.Io.Writer.Allocating = .init(arena.allocator());│ │1059 - try out.writer.writeAll("/users"); │ │1051 + try out.writer.writeAll("/" ++ home_segment ++ "/users"); │ │1052 try writeListWindow(&out.writer, u.search.slice(), u.start); │ │1053 break :blk out.written(); │ │1054 }, │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────╮ │1063 - .home_repo_new => "/repo/new", │ │1064 - .home_user_new => "/user/new", │ │1065 - .home_user => "/user", │ │1066 - .not_found => "/not-found", │ │1055 + .home_repo_new => "/" ++ home_segment ++ "/repo/new", │ │1056 + .home_user_new => "/" ++ home_segment ++ "/user/new", │ │1057 + .home_user => "/" ++ home_segment ++ "/user", │ │1058 + .not_found => "/" ++ home_segment ++ "/not-found", │ │1059 .user_repos => |u| blk: { │ │1060 var out: std.Io.Writer.Allocating = .init(arena.allocator()); │ │1069 - try out.writer.print(user_segment ++ "{s}/repos", .{u.name.slice()});│ │1061 + try out.writer.print("/{s}/repos", .{u.name.slice()}); │ │1062 try writeListWindow(&out.writer, u.search.slice(), u.start); │ │1063 break :blk out.written(); │ │1064 }, │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1065 .user_forks => |u| if (u.start == 0) │ │1074 - try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/forks", .{u.name.slice()}) │ │1066 + try std.fmt.allocPrint(arena.allocator(), "/{s}/forks", .{u.name.slice()}) │ │1067 else │ │1076 - try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/forks/" ++ start_seg ++ "{d}", .{ u.name.slice(), u.start }), │ │1077 - .user_repo_new => |name| try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/repo/new", .{name.slice()}), │ │1078 - .user_user_new => |name| try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/user/new", .{name.slice()}), │ │1079 - .user_user => |name| try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/user", .{name.slice()}), │ │1068 + try std.fmt.allocPrint(arena.allocator(), "/{s}/forks/" ++ start_seg ++ "{d}", .{ u.name.slice(), u.start }), │ │1069 + .user_repo_new => |name| try std.fmt.allocPrint(arena.allocator(), "/{s}/repo/new", .{name.slice()}), │ │1070 + .user_user_new => |name| try std.fmt.allocPrint(arena.allocator(), "/{s}/user/new", .{name.slice()}), │ │1071 + .user_user => |name| try std.fmt.allocPrint(arena.allocator(), "/{s}/user", .{name.slice()}), │ │1072 .repo_files => |f| blk: { │ │1073 const prefix = try repoUrlPrefix(arena, f.name.slice()); │ │1074 if (f.ref_kind == null and f.patchrev_id.len == 0 and f.find.len == 0 and f.line == 0) break :blk if (prefix.len == 0) "/" else prefix;│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1240 .repo_user_new => |name| try std.fmt.allocPrint(arena.allocator(), "{s}/user/new", .{try repoUrlPrefix(arena, name.slice())}), │ │1241 .repo_repo => |name| try std.fmt.allocPrint(arena.allocator(), "{s}/repo", .{try repoUrlPrefix(arena, name.slice())}), │ │1242 .repo_user => |name| try std.fmt.allocPrint(arena.allocator(), "{s}/user", .{try repoUrlPrefix(arena, name.slice())}), │ │1251 - .fork_patch => |f| try std.fmt.allocPrint(arena.allocator(), fork_segment ++ "{s}/" ++ patch_seg ++ "{s}", .{ f.name.slice(), f.id.slice() }),│ │1243 + .fork_patch => |f| try std.fmt.allocPrint(arena.allocator(), "/{s}+{s}", .{ f.name.slice(), f.id.slice() }), │ │1244 .fork_diff => |d| blk: { │ │1245 var out: std.Io.Writer.Allocating = .init(arena.allocator()); │ │1254 - try out.writer.print(fork_segment ++ "{s}/" ++ patch_seg ++ "{s}/diff", .{ d.fork.name.slice(), d.fork.id.slice() }); │ │1246 + try out.writer.print("/{s}+{s}/diff", .{ d.fork.name.slice(), d.fork.id.slice() }); │ │1247 if (d.oid.len != 0) try out.writer.print("/object:{s}/" ++ base_seg ++ "{s}", .{ d.oid.slice(), d.base_oid.slice() }); │ │1248 if (d.start != 0) try out.writer.print("/" ++ start_seg ++ "{d}", .{d.start}); │ │1249 if (d.path.len != 0) try out.writer.print("/" ++ path_seg ++ "{s}", .{d.path.slice()}); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1251 }, │ │1252 .fork_files => |f| blk: { │ │1253 var out: std.Io.Writer.Allocating = .init(arena.allocator()); │ │1262 - try out.writer.print(fork_segment ++ "{s}/" ++ patch_seg ++ "{s}/" ++ files_seg, .{ f.fork.name.slice(), f.fork.id.slice() });│ │1254 + try out.writer.print("/{s}+{s}/" ++ files_seg, .{ f.fork.name.slice(), f.fork.id.slice() }); │ │1255 if (f.oid.len != 0) try out.writer.print("/object:{s}", .{f.oid.slice()}); │ │1256 if (f.find.len != 0) try out.writer.print("/" ++ find_seg ++ "{s}", .{f.find.slice()}); │ │1257 if (f.line != 0) try out.writer.print("/" ++ line_seg ++ "{d}", .{f.line}); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1260 }, │ │1261 .fork_commits => |c| blk: { │ │1262 var out: std.Io.Writer.Allocating = .init(arena.allocator()); │ │1271 - try out.writer.print(fork_segment ++ "{s}/" ++ patch_seg ++ "{s}/" ++ commits_seg, .{ c.fork.name.slice(), c.fork.id.slice() });│ │1263 + try out.writer.print("/{s}+{s}/" ++ commits_seg, .{ c.fork.name.slice(), c.fork.id.slice() }); │ │1264 if (c.oid.len != 0) try out.writer.print("/object:{s}", .{c.oid.slice()}); │ │1265 if (c.message) try out.writer.print("/" ++ message_seg, .{}); │ │1266 break :blk out.written(); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1267 }, │ │1276 - .fork_repo_new => |f| try std.fmt.allocPrint(arena.allocator(), fork_segment ++ "{s}/" ++ patch_seg ++ "{s}/repo/new", .{ f.name.slice(), f.id.slice() }),│ │1277 - .fork_user_new => |f| try std.fmt.allocPrint(arena.allocator(), fork_segment ++ "{s}/" ++ patch_seg ++ "{s}/user/new", .{ f.name.slice(), f.id.slice() }),│ │1278 - .fork_user => |f| try std.fmt.allocPrint(arena.allocator(), fork_segment ++ "{s}/" ++ patch_seg ++ "{s}/user", .{ f.name.slice(), f.id.slice() }), │ │1268 + .fork_repo_new => |f| try std.fmt.allocPrint(arena.allocator(), "/{s}+{s}/repo/new", .{ f.name.slice(), f.id.slice() }), │ │1269 + .fork_user_new => |f| try std.fmt.allocPrint(arena.allocator(), "/{s}+{s}/user/new", .{ f.name.slice(), f.id.slice() }), │ │1270 + .fork_user => |f| try std.fmt.allocPrint(arena.allocator(), "/{s}+{s}/user", .{ f.name.slice(), f.id.slice() }), │ │1271 }; │ │1272 } │ │1273 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1276 if (path.len < 2 or path[0] != '/') return null; │ │1277 var segments = std.mem.splitScalar(u8, path[1..], '/'); │ │1278 const first = segments.next() orelse return null; │ │1287 - if (std.mem.eql(u8, first, "users")) { │ │1288 - const params = listParams(&segments, &.{ .start, .search }) orelse return null; │ │1289 - return withEncodedSearch(.{ .home_users = .{ .start = params.start() orelse return null } }, params.values.get(.search) orelse ""); │ │1290 - } │ │1291 - // "user/<name>[/repos[/search:<s>][/start:<n>]|/repo/new|/user[/new]]" │ │1292 - if (std.mem.eql(u8, first, "user")) { │ │1293 - const name = segments.next() orelse return .home_user; │ │1294 - if (name.len == 0) return null; │ │1295 - // "/user/new" is the home page's new user tab, not a user │ │1296 - if (std.mem.eql(u8, name, "new") and segments.peek() == null) return .home_user_new; │ │1297 - const parsed = Array(evt.User.name_max_len).from(name) orelse return null; // name too long │ │1298 - const sub = segments.next() orelse return .{ .user_repos = .{ .name = parsed } }; │ │1299 - if (std.mem.eql(u8, sub, "repos")) { │ │1300 - const params = listParams(&segments, &.{ .start, .search }) orelse return null; │ │1301 - return withEncodedSearch(.{ .user_repos = .{ .name = parsed, .start = params.start() orelse return null } }, params.values.get(.search) orelse ""); │ │1302 - } │ │1303 - if (std.mem.eql(u8, sub, "forks")) return .{ .user_forks = .{ .name = parsed, .start = listStart(&segments) orelse return null } }; │ │1304 - if (std.mem.eql(u8, sub, "repo")) return if (isNewTail(&segments)) .{ .user_repo_new = parsed } else null; │ │1305 - if (std.mem.eql(u8, sub, "user")) return if (segments.peek() == null) .{ .user_user = parsed } else if (isNewTail(&segments)) .{ .user_user_new = parsed } else null;│ │1306 - return null; // unknown sub-path │ │1307 - } │ │1308 - // "fork/<forker>/<reponame>/patch:<id>[/files|/commits]"; the │ │1309 - // patch branch is implicit, while object ids and view-specific params │ │1279 + // "<forker>:<reponame>+<patch id>[/files|/commits]"; the patch │ │1280 + // branch is implicit, while object ids and view-specific params │ │1281 // use the same tails as the repo files and commits routes. │ │1311 - if (std.mem.eql(u8, first, "fork")) { │ │1312 - const rest = segments.rest(); │ │1313 - const owner = segments.next() orelse return null; │ │1314 - const repo_name = segments.next() orelse return null; │ │1315 - if (owner.len == 0 or repo_name.len == 0) return null; │ │1316 - const identity = rest[0 .. owner.len + 1 + repo_name.len]; │ │1317 - const patch = segments.next() orelse return null; │ │1318 - if (!std.mem.startsWith(u8, patch, patch_seg)) return null; │ │1319 - const id = patch[patch_seg.len..]; │ │1282 + if (std.mem.indexOfScalar(u8, first, '+')) |plus| { │ │1283 + const identity = first[0..plus]; │ │1284 + _ = RepoIdentity.parse(identity) orelse return null; │ │1285 + const id = first[plus + 1 ..]; │ │1286 if (id.len != evt.event_id_size * 2) return null; │ │1287 + for (id) |c| if (!std.ascii.isHex(c)) return null; │ │1288 const tab = segments.next() orelse return forkPatchRoute(identity, id); │ │1289 var params = Params{}; │ │1290 if (std.mem.eql(u8, tab, "diff")) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1318 if (std.mem.eql(u8, tab, "user")) return if (segments.peek() == null) forkUserRoute(identity, id) else if (isNewTail(&segments)) forkUserNewRoute(identity, id) else null;│ │1319 return null; │ │1320 } │ │1354 - // "repo/<username>/<reponame>[/<tab tail>]"; the bare pair is the │ │1355 - // files root │ │1356 - if (std.mem.eql(u8, first, "repo")) { │ │1357 - const rest = segments.rest(); // "username/reponame[/...]" │ │1358 - const owner = segments.next() orelse return null; │ │1359 - // "/repo/new" is the home page's new repo tab, not a repo │ │1360 - if (std.mem.eql(u8, owner, "new") and segments.peek() == null) return .home_repo_new; │ │1361 - const repo_name = segments.next() orelse return null; │ │1362 - // reject an empty username or reponame │ │1363 - if (owner.len == 0 or repo_name.len == 0) return null; │ │1364 - const pair = rest[0 .. owner.len + 1 + repo_name.len]; // "username/reponame" │ │1321 + // "<username>:<reponame>[/<tab tail>]"; the bare pair is the files root │ │1322 + if (std.mem.indexOfScalar(u8, first, ':') != null) { │ │1323 + _ = RepoIdentity.parse(first) orelse return null; │ │1324 if (segments.peek() != null) { │ │1366 - return repoSubRoute(pair, segments.rest()); │ │1325 + return repoSubRoute(first, segments.rest()); │ │1326 + } │ │1327 + return repoFilesRoute(first, null, "", "", 0); │ │1328 + } │ │1329 + // "home[/users[/search:<s>][/start:<n>]|/repo/new|/user[/new]|/not-found]" │ │1330 + if (std.mem.eql(u8, first, home_segment)) { │ │1331 + const sub = segments.next() orelse return default; │ │1332 + if (std.mem.eql(u8, sub, "users")) { │ │1333 + const params = listParams(&segments, &.{ .start, .search }) orelse return null; │ │1334 + return withEncodedSearch(.{ .home_users = .{ .start = params.start() orelse return null } }, params.values.get(.search) orelse ""); │ │1335 } │ │1368 - return repoFilesRoute(pair, null, "", "", 0); │ │1336 + if (std.mem.eql(u8, sub, "repo")) return if (isNewTail(&segments)) .home_repo_new else null; │ │1337 + if (std.mem.eql(u8, sub, "user")) return if (segments.peek() == null) .home_user else if (isNewTail(&segments)) .home_user_new else null; │ │1338 + if (std.mem.eql(u8, sub, "not-found")) return if (segments.peek() == null) .not_found else null; │ │1339 + return null; │ │1340 } │ │1370 - return null; │ │1341 + // "<username>[/repos[/search:<s>][/start:<n>]|/forks|/repo/new|/user[/new]]" │ │1342 + evt.User.validateName(first) catch return null; │ │1343 + const parsed = Array(evt.User.name_max_len).from(first) orelse return null; │ │1344 + const sub = segments.next() orelse return .{ .user_repos = .{ .name = parsed } }; │ │1345 + if (std.mem.eql(u8, sub, "repos")) { │ │1346 + const params = listParams(&segments, &.{ .start, .search }) orelse return null; │ │1347 + return withEncodedSearch(.{ .user_repos = .{ .name = parsed, .start = params.start() orelse return null } }, params.values.get(.search) orelse ""); │ │1348 + } │ │1349 + if (std.mem.eql(u8, sub, "forks")) return .{ .user_forks = .{ .name = parsed, .start = listStart(&segments) orelse return null } }; │ │1350 + if (std.mem.eql(u8, sub, "repo")) return if (isNewTail(&segments)) .{ .user_repo_new = parsed } else null; │ │1351 + if (std.mem.eql(u8, sub, "user")) return if (segments.peek() == null) .{ .user_user = parsed } else if (isNewTail(&segments)) .{ .user_user_new = parsed } else null; │ │1352 + return null; // unknown sub-path │ │1353 } │ │1354 │ │1373 - // parse a local-mode url, which elides the "/repo/<owner>/<name>" prefix: │ │1355 + // parse a local-mode url, which elides the "/<owner>:<name>" prefix: │ │1356 // "/" is the files root and the sub-paths follow directly. │ │1357 pub fn fromUrlLocal(path: []const u8) ?RoutablePage { │ │1358 if (std.mem.eql(u8, path, "/")) return repoFilesRoute("", null, "", "", 0); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │1360 return repoSubRoute("", path[1..]); │ │1361 } │ │1362 │ │1381 - // the url prefix for a repo route's identity: "/repo/<identity>", or "" │ │1363 + // the url prefix for a repo route's identity: "/<identity>", or "" │ │1364 // when the identity is elided. │ │1365 fn repoUrlPrefix(arena: *std.heap.ArenaAllocator, identity: []const u8) ![]const u8 {│ │1366 if (identity.len == 0) return ""; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯