╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1938 } │ │1939 if (session.data.git_ssh_port) |port| { │ │1940 ssh_index = count; │ │1941 - const url = try std.fmt.allocPrint(aa, "ssh://localhost:{d}/repo/{s}", .{ port, identity });│ │1941 + const url = try std.fmt.allocPrint(aa, "ssh://localhost:{d}/{s}", .{ port, identity }); │ │1942 choices[count] = .{ │ │1943 .selector = "ssh", │ │1944 .text = url, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1949 } │ │1950 }, │ │1951 .fork => |f| if (session.data.git_ssh_port) |port| { │ │1952 - const url = try std.fmt.allocPrint(aa, "ssh://localhost:{d}/fork/{s}/patch:{s}", .{ port, f.identity, f.id });│ │1952 + const url = try std.fmt.allocPrint(aa, "ssh://localhost:{d}/{s}+{s}", .{ port, f.identity, f.id }); │ │1953 choices[count] = .{ │ │1954 .text = url, │ │1955 .copyable_text = try std.fmt.allocPrint(aa, "{s}git clone {s}", .{ session.data.git_ssh_prefix, url }), │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/ui/widget/thread.zig│ ╰────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │771 const action = if (self.data.identity.len == 0) │ │772 try std.fmt.allocPrint(pa, "{s}/{s}:{s}/attach", .{ ui.file_input_prefix, @tagName(kind), entry.id }) │ │773 else │ │774 - try std.fmt.allocPrint(pa, "{s}/repo/{s}/{s}:{s}/attach", .{ ui.file_input_prefix, self.data.identity, @tagName(kind), entry.id });│ │774 + try std.fmt.allocPrint(pa, "{s}/{s}/{s}:{s}/attach", .{ ui.file_input_prefix, self.data.identity, @tagName(kind), entry.id }); │ │775 try addToolButton(allocator, row, label, "", action); │ │776 } │ │777 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │66 if (self.owned_path) |path| allocator.free(path); │ │67 } │ │68 }; │ │69 + │ │70 +// the "owner:name" identity a repo url base names, or null for any other base │ │71 +fn repoBaseIdentity(repo_base: []const u8) ?[]const u8 { │ │72 + if (repo_base.len < 2 or repo_base[0] != '/') return null; │ │73 + const identity = repo_base[1..]; │ │74 + if (std.mem.indexOfScalar(u8, identity, ':') == null or std.mem.indexOfAny(u8, identity, "/+") != null) return null;│ │75 + return identity; │ │76 +} │ │77 + │ │78 +// the route a url names under this host's url scheme │ │79 +fn routeFromUrl(host: Host, path: []const u8) ?ui.RoutablePage { │ │80 + return switch (host) { │ │81 + .server => ui.RoutablePage.fromUrl(path), │ │82 + .local => ui.RoutablePage.fromUrlLocal(path), │ │83 + }; │ │84 +} │ │85 │ │86 fn requestRepoSource(io: std.Io, allocator: std.mem.Allocator, host: Host, repo_base: []const u8) !?RequestRepoSource { │ │87 return switch (host) { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │88 .server => |server| blk: { │ │73 - const repo_prefix = "/repo/"; │ │74 - if (!std.mem.startsWith(u8, repo_base, repo_prefix)) break :blk null; │ │75 - const path = switch (try serve_common.resolveRepoPath(io, allocator, server.users_dir, server.admin_repo_path, repo_base[repo_prefix.len..], false)) {│ │89 + const identity = repoBaseIdentity(repo_base) orelse break :blk null; │ │90 + const path = switch (try serve_common.resolveRepoPath(io, allocator, server.users_dir, server.admin_repo_path, identity, false)) { │ │91 .ok => |value| value, │ │92 .invalid, .not_found => break :blk null, │ │93 }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │228 // before the routable pages │ │229 if (attachmentRequest(path)) |attachment| return serveAttachment(io, request, allocator, attachment, host);│ │230 │ │216 - const route_maybe = switch (host) { │ │217 - .server => ui.RoutablePage.fromUrl(path), │ │218 - .local => ui.RoutablePage.fromUrlLocal(path), │ │219 - }; │ │220 - // a path that's neither a page nor an embed gets the not-found page │ │221 - if (route_maybe == null) if (findEmbed(path)) |embed| { │ │231 + // embeds are matched before the pages so an embed's name never reads as a user │ │232 + if (findEmbed(path)) |embed| { │ │233 // the embeds change with every build, so the browser must revalidate │ │234 // rather than heuristically cache them across server restarts │ │235 try request.respond(embed.body, .{ │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │239 }, │ │240 }); │ │241 return; │ │231 - }; │ │242 + } │ │243 { │ │233 - const current_page = route_maybe orelse .not_found; │ │244 + // a path that's neither a page nor an embed gets the not-found page │ │245 + const current_page = routeFromUrl(host, path) orelse .not_found; │ │246 // resolve the haxy_session cookie's token to a user_id via the │ │247 // store. user_id_buf lives on the stack for the rest of handleRequest,│ │248 // which is plenty for renderIndexHtml to consume. local mode has no │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │500 const authorization: ui.Authorization = blk: { │ │501 const user_id = requestUserId(request, server.session_store) orelse break :blk .login_required; │ │502 │ │491 - const repo_prefix = "/repo/"; │ │492 - if (!std.mem.startsWith(u8, repo_base, repo_prefix)) break :blk .repo_not_found; │ │503 + const identity = repoBaseIdentity(repo_base) orelse break :blk .repo_not_found; │ │504 │ │505 var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = server.admin_repo_path });│ │506 defer admin_repo.deinit(io, allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │507 const moment = try evt.currentMoment(evt.admin_repo_opts, &admin_repo); │ │497 - break :blk try ui.authorizeUser(io, moment, arena, server.users_dir, user_id, repo_base[repo_prefix.len..], min_role);│ │508 + break :blk try ui.authorizeUser(io, moment, arena, server.users_dir, user_id, identity, min_role); │ │509 }; │ │510 switch (authorization) { │ │511 .actor => |actor| return actor, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │529 .server => |server| server, │ │530 .local => return true, │ │531 }; │ │521 - const repo_prefix = "/repo/"; │ │522 - if (!std.mem.startsWith(u8, repo_base, repo_prefix)) return false; │ │532 + const identity = repoBaseIdentity(repo_base) orelse return false; │ │533 │ │534 var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = server.admin_repo_path });│ │535 defer admin_repo.deinit(io, allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯