╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 ""; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │1385 - return std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}", .{identity});│ │1367 + return std.fmt.allocPrint(arena.allocator(), "/{s}", .{identity}); │ │1368 } │ │1369 │ │1370 // true when the page builds at its content height on the web, letting the │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │1377 }; │ │1378 } │ │1379 │ │1398 - // the "owner/name" a repo route carries, null for any other route. the│ │1380 + // the "owner:name" a repo route carries, null for any other route. the│ │1381 // result borrows the route, so it must outlive the slice. │ │1382 pub fn repoIdentity(self: *const RoutablePage) ?[]const u8 { │ │1383 return switch (self.*) { │ ╰─────────────────────────────────────────────────────────────────────────────────╯