╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │89 │ │90 // POST routes can be scoped by any page, so they can redirect using │ │91 // the base of the URL. this allows logging in to keep you on the page│ │92 - // you were on. local mode has no accounts, so no POST routes. │ │92 + // you were on. local mode has no accounts, so only the issue routes. │ │93 if (method == .POST) { │ │94 switch (host) { │ │95 .remote => |remote| { │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │102 .login => handleLogin(io, request, allocator, base, remote.admin_repo_path, remote.session_store),│ │103 .logout => handleLogout(request, base, remote.session_store), │ │104 .ansi => handleAnsi(io, request, allocator, base, remote.admin_repo_path, remote.session_store), │ │105 - .issue => handleIssue(io, request, allocator, base, remote.admin_repo_path), │ │106 - .open => handleIssueStatus(io, request, allocator, base, remote.admin_repo_path, .open), │ │107 - .close => handleIssueStatus(io, request, allocator, base, remote.admin_repo_path, .closed), │ │108 - .edit => handleIssueEdit(io, request, allocator, base, remote.admin_repo_path), │ │105 + .issue => handleIssue(io, request, allocator, base, host), │ │106 + .open => handleIssueStatus(io, request, allocator, base, host, .open), │ │107 + .close => handleIssueStatus(io, request, allocator, base, host, .closed), │ │108 + .edit => handleIssueEdit(io, request, allocator, base, host), │ │109 + }; │ │110 + } │ │111 + } │ │112 + }, │ │113 + .local => { │ │114 + const PostRoute = enum { issue, open, close, edit }; │ │115 + inline for (@typeInfo(PostRoute).@"enum".fields) |field| { │ │116 + const suffix = "/" ++ field.name; │ │117 + if (std.mem.endsWith(u8, path, suffix)) { │ │118 + const base = path[0 .. path.len - suffix.len]; │ │119 + return switch (@field(PostRoute, field.name)) { │ │120 + .issue => handleIssue(io, request, allocator, base, host), │ │121 + .open => handleIssueStatus(io, request, allocator, base, host, .open), │ │122 + .close => handleIssueStatus(io, request, allocator, base, host, .closed), │ │123 + .edit => handleIssueEdit(io, request, allocator, base, host), │ │124 }; │ │125 } │ │126 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────╮ │127 }, │ │113 - .local => {},│ │128 } │ │129 } │ │130 │ ╰──────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────╮ │348 } │ │349 │ │350 // create an issue in the repo the form's page names (base is │ │337 -// "/repo/<owner>/<name>") and redirect to it │ │351 +// "/repo/<owner>/<name>", or "" in local mode) and redirect to it│ │352 fn handleIssue( │ │353 io: std.Io, │ │354 request: *std.http.Server.Request, │ ╰───────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────╮ │355 allocator: std.mem.Allocator, │ │356 base: []const u8, │ │343 - admin_repo_path: []const u8, │ │357 + host: Host, │ │358 ) !void { │ │359 var body_buf: [256]u8 = undefined; │ │360 const reader = request.readerExpectNone(&body_buf);│ ╰────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │378 try request.respond("", .{ │ │379 .status = .see_other, │ │380 .extra_headers = &.{.{ .name = "location", .value = form_location }}, │ │367 - }); │ │368 - return; │ │369 - } │ │370 - │ │371 - const not_found = "repo not found"; │ │372 - const repo_prefix = "/repo/"; │ │373 - if (!std.mem.startsWith(u8, base, repo_prefix)) { │ │374 - try request.respond(not_found, .{ │ │375 - .status = .not_found, │ │376 - .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }},│ │381 }); │ │382 return; │ │383 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │384 │ │381 - const repos_dir = try std.fs.path.join(allocator, &.{ std.fs.path.dirname(admin_repo_path) orelse ".", "repos" }); │ │382 - defer allocator.free(repos_dir); │ │383 - const repo_path = switch (try serve_common.resolveRepoPath(io, allocator, repos_dir, admin_repo_path, base[repo_prefix.len..], false)) {│ │384 - .ok => |p| p, │ │385 - .invalid, .not_found => { │ │386 - try request.respond(not_found, .{ │ │387 - .status = .not_found, │ │388 - .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │389 - }); │ │390 - return; │ │391 - }, │ │392 - }; │ │393 - defer allocator.free(repo_path); │ │394 - │ │395 - var repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = repo_path }); │ │396 - defer repo.deinit(io, allocator); │ │397 - │ │385 var id_bytes: [evt.event_id_size]u8 = undefined; │ │386 io.random(&id_bytes); │ │387 const event_id_hex = std.fmt.bytesToHex(id_bytes, .lower); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │388 │ │402 - try evt.commitAndConsume(.xit, .{}, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{.{│ │389 + const event = evt.EventWithId{ │ │390 .id = event_id_hex, │ │391 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │392 .event = .{ .issue = .{ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │394 .description = description, │ │395 .tags = tags, │ │396 } }, │ │410 - }}); │ │397 + }; │ │398 + │ │399 + const not_found = "repo not found"; │ │400 + switch (host) { │ │401 + .remote => |remote| { │ │402 + const repo_prefix = "/repo/"; │ │403 + if (!std.mem.startsWith(u8, base, repo_prefix)) { │ │404 + try request.respond(not_found, .{ │ │405 + .status = .not_found, │ │406 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │407 + }); │ │408 + return; │ │409 + } │ │410 + │ │411 + const repos_dir = try std.fs.path.join(allocator, &.{ std.fs.path.dirname(remote.admin_repo_path) orelse ".", "repos" }); │ │412 + defer allocator.free(repos_dir); │ │413 + const repo_path = switch (try serve_common.resolveRepoPath(io, allocator, repos_dir, remote.admin_repo_path, base[repo_prefix.len..], false)) { │ │414 + .ok => |p| p, │ │415 + .invalid, .not_found => { │ │416 + try request.respond(not_found, .{ │ │417 + .status = .not_found, │ │418 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │419 + }); │ │420 + return; │ │421 + }, │ │422 + }; │ │423 + defer allocator.free(repo_path); │ │424 + │ │425 + var repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = repo_path }); │ │426 + defer repo.deinit(io, allocator); │ │427 + try evt.commitAndConsume(.xit, .{}, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{event}); │ │428 + }, │ │429 + .local => |src| { │ │430 + // the local form posts to "/issue", so base is empty │ │431 + if (base.len != 0) { │ │432 + try request.respond(not_found, .{ │ │433 + .status = .not_found, │ │434 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │435 + }); │ │436 + return; │ │437 + } │ │438 + switch (src.repo_kind) { │ │439 + inline else => |repo_kind| { │ │440 + var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, .{ .path = src.path }); │ │441 + defer any_repo.deinit(io, allocator); │ │442 + switch (any_repo) { │ │443 + inline else => |*repo| try evt.commitAndConsume(repo_kind, repo.self_repo_opts, io, allocator, repo, evt.events_ref, &[_]evt.EventWithId{event}),│ │444 + } │ │445 + }, │ │446 + } │ │447 + }, │ │448 + } │ │449 │ │450 const location = try std.fmt.allocPrint(allocator, "{s}/issues/{s}", .{ base, &event_id_hex }); │ │451 defer allocator.free(location); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │466 return true; │ │467 } │ │468 │ │431 -// split an issue url ("/repo/<owner>/<name>/issues/<id>") into the repo base │ │432 -// and the decoded id, null when it isn't one │ │469 +// split an issue url ("/repo/<owner>/<name>/issues/<id>", identity elided in │ │470 +// local mode) into what precedes "/issues/" and the decoded id, null when it │ │471 +// isn't one. the repo base is validated per host in updateIssue. │ │472 const IssueBaseParts = struct { repo_base: []const u8, id_bytes: [evt.event_id_size]u8 };│ │473 fn issueBaseParts(base: []const u8) ?IssueBaseParts { │ │435 - const repo_prefix = "/repo/"; │ │474 const issues_infix = "/issues/"; │ │475 const id_len = evt.event_id_size * 2; │ │438 - if (base.len < repo_prefix.len + issues_infix.len + id_len) return null; │ │476 + if (base.len < issues_infix.len + id_len) return null; │ │477 var id_bytes: [evt.event_id_size]u8 = undefined; │ │478 _ = std.fmt.hexToBytes(&id_bytes, base[base.len - id_len ..]) catch return null; │ │479 const head = base[0 .. base.len - id_len]; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯