╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Threads.zig│ ╰───────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │133 - return switch (kind) { │ │134 - .issue => ui.RoutablePage.repoCommentEditRoute(identity, thread_id, comment_id), │ │135 - .discussion => ui.RoutablePage.repoDiscussionCommentEditRoute(identity, thread_id, comment_id),│ │136 - else => @compileError("unsupported thread event kind"), │ │137 - }; │ │121 + return ui.RoutablePage.repoThreadCommentEditRoute(kind, identity, thread_id, comment_id); │ │122 } │ │123 │ │124 fn editRoute(identity: []const u8, selected: []const u8) ?ui.RoutablePage { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │141 - return switch (kind) { │ │142 - .issue => ui.RoutablePage.repoIssuesEditRoute(identity, selected), │ │143 - .discussion => ui.RoutablePage.repoDiscussionsEditRoute(identity, selected),│ │144 - else => @compileError("unsupported thread event kind"), │ │145 - }; │ │125 + return ui.RoutablePage.repoThreadEditRoute(kind, identity, selected); │ │126 } │ │127 │ │128 fn newRoute(identity: []const u8) ?ui.RoutablePage { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │149 - return switch (kind) { │ │150 - .issue => ui.RoutablePage.repoIssuesNewRoute(identity), │ │151 - .discussion => ui.RoutablePage.repoDiscussionsNewRoute(identity), │ │152 - else => @compileError("unsupported thread event kind"), │ │153 - }; │ │129 + return ui.RoutablePage.repoThreadNewRoute(kind, identity); │ │130 } │ │131 │ │132 fn descriptionRoute(identity: []const u8, selected: []const u8) ?ui.RoutablePage {│ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │157 - return switch (kind) { │ │158 - .issue => ui.RoutablePage.repoIssuesDescriptionRoute(identity, selected), │ │159 - .discussion => ui.RoutablePage.repoDiscussionsDescriptionRoute(identity, selected),│ │160 - else => @compileError("unsupported thread event kind"), │ │161 - }; │ │133 + return ui.RoutablePage.repoThreadDescriptionRoute(kind, identity, selected); │ │134 } │ │135 │ │136 fn tagsRoute(identity: []const u8, tag: []const u8) ?ui.RoutablePage { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │165 - return switch (kind) { │ │166 - .issue => ui.RoutablePage.repoIssuesTagsRoute(identity, tag), │ │167 - .discussion => ui.RoutablePage.repoDiscussionsTagsRoute(identity, tag), │ │168 - else => @compileError("unsupported thread event kind"), │ │169 - }; │ │137 + return ui.RoutablePage.repoThreadTagsRoute(kind, identity, tag); │ │138 } │ │139 │ │140 fn conflictsRoute(identity: []const u8, selected: []const u8) ?ui.RoutablePage {│ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │45 clone_ssh_port: ?u16, │ │46 }, │ │47 local: ui.RepoSource, │ │48 +}; │ │49 + │ │50 +// an on-disk repo resolved from a request url. remote paths are owned. │ │51 +const RequestRepoSource = struct { │ │52 + source: ui.RepoSource, │ │53 + owned_path: ?[]const u8 = null, │ │54 + │ │55 + fn deinit(self: RequestRepoSource, allocator: std.mem.Allocator) void { │ │56 + if (self.owned_path) |path| allocator.free(path); │ │57 + } │ │58 }; │ │59 + │ │60 +fn requestRepoSource(io: std.Io, allocator: std.mem.Allocator, host: Host, repo_base: []const u8) !?RequestRepoSource { │ │61 + return switch (host) { │ │62 + .remote => |remote| blk: { │ │63 + const repo_prefix = "/repo/"; │ │64 + if (!std.mem.startsWith(u8, repo_base, repo_prefix)) break :blk null; │ │65 + const repos_dir = try std.fs.path.join(allocator, &.{ std.fs.path.dirname(remote.admin_repo_path) orelse ".", "repos" }); │ │66 + defer allocator.free(repos_dir); │ │67 + const path = switch (try serve_common.resolveRepoPath(io, allocator, repos_dir, remote.admin_repo_path, repo_base[repo_prefix.len..], false)) {│ │68 + .ok => |value| value, │ │69 + .invalid, .not_found => break :blk null, │ │70 + }; │ │71 + break :blk .{ │ │72 + .source = .{ .path = path, .repo_kind = .xit }, │ │73 + .owned_path = path, │ │74 + }; │ │75 + }, │ │76 + .local => |source| if (repo_base.len == 0) .{ .source = source } else null, │ │77 + }; │ │78 +} │ │79 │ │80 pub fn handleConnection( │ │81 io: std.Io, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │539 }; │ │540 │ │541 const not_found = "repo not found"; │ │512 - switch (host) { │ │513 - .remote => |remote| { │ │514 - const repo_prefix = "/repo/"; │ │515 - if (!std.mem.startsWith(u8, base, repo_prefix)) { │ │516 - try request.respond(not_found, .{ │ │517 - .status = .not_found, │ │518 - .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │519 - }); │ │520 - return; │ │542 + const request_repo = (try requestRepoSource(io, allocator, host, base)) orelse { │ │543 + try request.respond(not_found, .{ │ │544 + .status = .not_found, │ │545 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │546 + }); │ │547 + return; │ │548 + }; │ │549 + defer request_repo.deinit(allocator); │ │550 + const source = request_repo.source; │ │551 + switch (source.repo_kind) { │ │552 + inline else => |repo_kind| { │ │553 + var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, source.localInitOpts()); │ │554 + defer any_repo.deinit(io, allocator); │ │555 + switch (any_repo) { │ │556 + inline else => |*repo| try evt.consume(repo_kind, repo.self_repo_opts, io, allocator, repo, evt.events_ref, &[_]evt.EventWithId{event}), │ │557 } │ │522 - │ │523 - const repos_dir = try std.fs.path.join(allocator, &.{ std.fs.path.dirname(remote.admin_repo_path) orelse ".", "repos" }); │ │524 - defer allocator.free(repos_dir); │ │525 - const repo_path = switch (try serve_common.resolveRepoPath(io, allocator, repos_dir, remote.admin_repo_path, base[repo_prefix.len..], false)) { │ │526 - .ok => |p| p, │ │527 - .invalid, .not_found => { │ │528 - try request.respond(not_found, .{ │ │529 - .status = .not_found, │ │530 - .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │531 - }); │ │532 - return; │ │533 - }, │ │534 - }; │ │535 - defer allocator.free(repo_path); │ │536 - │ │537 - var repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = repo_path }); │ │538 - defer repo.deinit(io, allocator); │ │539 - try evt.consume(.xit, .{}, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{event}); │ │558 }, │ │541 - .local => |src| { │ │542 - // local routes elide the repo base │ │543 - if (base.len != 0) { │ │544 - try request.respond(not_found, .{ │ │545 - .status = .not_found, │ │546 - .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │547 - }); │ │548 - return; │ │549 - } │ │550 - switch (src.repo_kind) { │ │551 - inline else => |repo_kind| { │ │552 - var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, src.localInitOpts()); │ │553 - defer any_repo.deinit(io, allocator); │ │554 - switch (any_repo) { │ │555 - inline else => |*repo| try evt.consume(repo_kind, repo.self_repo_opts, io, allocator, repo, evt.events_ref, &[_]evt.EventWithId{event}),│ │556 - } │ │557 - }, │ │558 - } │ │559 - }, │ │559 } │ │560 │ │561 const location = try std.fmt.allocPrint(allocator, "{s}/{s}:{s}", .{ base, @tagName(kind), &event_id_hex }); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │634 const thread_id_hex = std.fmt.bytesToHex(parts.thread_id, .lower); │ │635 const parent_id_hex = std.fmt.bytesToHex(parts.comment_id orelse parts.thread_id, .lower); │ │636 │ │637 + const request_repo = (try requestRepoSource(io, allocator, host, parts.repo_base)) orelse { │ │638 + try request.respond(not_found, .{ │ │639 + .status = .not_found, │ │640 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │641 + }); │ │642 + return; │ │643 + }; │ │644 + defer request_repo.deinit(allocator); │ │645 + const source = request_repo.source; │ │646 var event_id_hex: [evt.event_id_size * 2]u8 = undefined; │ │639 - switch (host) { │ │640 - .remote => |remote| { │ │641 - const repo_prefix = "/repo/"; │ │642 - if (!std.mem.startsWith(u8, parts.repo_base, repo_prefix)) { │ │643 - try request.respond(not_found, .{ │ │644 - .status = .not_found, │ │645 - .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │646 - }); │ │647 - return; │ │648 - } │ │649 - const repos_dir = try std.fs.path.join(allocator, &.{ std.fs.path.dirname(remote.admin_repo_path) orelse ".", "repos" }); │ │650 - defer allocator.free(repos_dir); │ │651 - const repo_path = switch (try serve_common.resolveRepoPath(io, allocator, repos_dir, remote.admin_repo_path, parts.repo_base[repo_prefix.len..], false)) { │ │652 - .ok => |p| p, │ │653 - .invalid, .not_found => { │ │654 - try request.respond(not_found, .{ │ │655 - .status = .not_found, │ │656 - .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │657 - }); │ │658 - return; │ │659 - }, │ │660 - }; │ │661 - defer allocator.free(repo_path); │ │662 - │ │663 - var repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = repo_path }); │ │664 - defer repo.deinit(io, allocator); │ │665 - event_id_hex = try evt.Comment.create(.xit, .{}, io, allocator, &repo, &thread_id_hex, &parent_id_hex, body, author); │ │666 - }, │ │667 - .local => |src| { │ │668 - if (parts.repo_base.len != 0) { │ │669 - try request.respond(not_found, .{ │ │670 - .status = .not_found, │ │671 - .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │672 - }); │ │673 - return; │ │674 - } │ │675 - switch (src.repo_kind) { │ │676 - inline else => |repo_kind| { │ │677 - var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, src.localInitOpts()); │ │678 - defer any_repo.deinit(io, allocator); │ │679 - switch (any_repo) { │ │680 - inline else => |*repo| event_id_hex = try evt.Comment.create(repo_kind, repo.self_repo_opts, io, allocator, repo, &thread_id_hex, &parent_id_hex, body, author),│ │681 - } │ │682 - }, │ │647 + switch (source.repo_kind) { │ │648 + inline else => |repo_kind| { │ │649 + var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, source.localInitOpts()); │ │650 + defer any_repo.deinit(io, allocator); │ │651 + switch (any_repo) { │ │652 + inline else => |*repo| event_id_hex = try evt.Comment.create(repo_kind, repo.self_repo_opts, io, allocator, repo, &thread_id_hex, &parent_id_hex, body, author), │ │653 } │ │654 }, │ │655 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │687 attachment: AttachmentRequest, │ │688 host: Host, │ │689 ) !void { │ │720 - switch (host) { │ │721 - .remote => |remote| { │ │722 - const repo_prefix = "/repo/"; │ │723 - if (!std.mem.startsWith(u8, attachment.repo_base, repo_prefix)) return respondAttachmentNotFound(request); │ │724 - │ │725 - const repos_dir = try std.fs.path.join(allocator, &.{ std.fs.path.dirname(remote.admin_repo_path) orelse ".", "repos" }); │ │726 - defer allocator.free(repos_dir); │ │727 - const repo_path = switch (try serve_common.resolveRepoPath(io, allocator, repos_dir, remote.admin_repo_path, attachment.repo_base[repo_prefix.len..], false)) {│ │728 - .ok => |p| p, │ │729 - .invalid, .not_found => return respondAttachmentNotFound(request), │ │730 - }; │ │731 - defer allocator.free(repo_path); │ │732 - │ │733 - var repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = repo_path }); │ │734 - defer repo.deinit(io, allocator); │ │735 - try sendAttachment(.xit, .{}, io, request, allocator, &repo, &attachment.id); │ │736 - }, │ │737 - .local => |src| { │ │738 - // local routes elide the repo base │ │739 - if (attachment.repo_base.len != 0) return respondAttachmentNotFound(request); │ │740 - switch (src.repo_kind) { │ │741 - inline else => |repo_kind| { │ │742 - var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, src.localInitOpts()); │ │743 - defer any_repo.deinit(io, allocator); │ │744 - switch (any_repo) { │ │745 - inline else => |*repo| try sendAttachment(repo_kind, repo.self_repo_opts, io, request, allocator, repo, &attachment.id), │ │746 - } │ │747 - }, │ │690 + const request_repo = (try requestRepoSource(io, allocator, host, attachment.repo_base)) orelse return respondAttachmentNotFound(request); │ │691 + defer request_repo.deinit(allocator); │ │692 + const source = request_repo.source; │ │693 + switch (source.repo_kind) { │ │694 + inline else => |repo_kind| { │ │695 + var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, source.localInitOpts()); │ │696 + defer any_repo.deinit(io, allocator); │ │697 + switch (any_repo) { │ │698 + inline else => |*repo| try sendAttachment(repo_kind, repo.self_repo_opts, io, request, allocator, repo, &attachment.id), │ │699 } │ │700 }, │ │701 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │842 │ │843 var body_buf: [4096]u8 = undefined; │ │844 const blob: evt.Blob = .{ .name = name, .size = size, .reader = try request.readerExpectContinue(&body_buf) }; │ │894 - │ │895 - switch (host) { │ │896 - .remote => |remote| { │ │897 - const repo_prefix = "/repo/"; │ │898 - if (!std.mem.startsWith(u8, parts.repo_base, repo_prefix)) return respondAttachmentParentNotFound(request); │ │899 - const repos_dir = try std.fs.path.join(allocator, &.{ std.fs.path.dirname(remote.admin_repo_path) orelse ".", "repos" }); │ │900 - defer allocator.free(repos_dir); │ │901 - const repo_path = switch (try serve_common.resolveRepoPath(io, allocator, repos_dir, remote.admin_repo_path, parts.repo_base[repo_prefix.len..], false)) { │ │902 - .ok => |p| p, │ │903 - .invalid, .not_found => return respondAttachmentParentNotFound(request), │ │904 - }; │ │905 - defer allocator.free(repo_path); │ │845 │ │907 - var repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = repo_path }); │ │908 - defer repo.deinit(io, allocator); │ │909 - _ = evt.Attachment.create(.xit, .{}, io, allocator, &repo, &parent_id_hex, blob, author) catch |err| switch (err) { │ │910 - error.ParentNotFound => return respondAttachmentParentNotFound(request), │ │911 - else => return err, │ │912 - }; │ │913 - }, │ │914 - .local => |src| { │ │915 - if (parts.repo_base.len != 0) return respondAttachmentParentNotFound(request); │ │916 - switch (src.repo_kind) { │ │917 - inline else => |repo_kind| { │ │918 - var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, src.localInitOpts()); │ │919 - defer any_repo.deinit(io, allocator); │ │920 - switch (any_repo) { │ │921 - inline else => |*repo| _ = evt.Attachment.create(repo_kind, repo.self_repo_opts, io, allocator, repo, &parent_id_hex, blob, author) catch |err| switch (err) {│ │922 - error.ParentNotFound => return respondAttachmentParentNotFound(request), │ │923 - else => return err, │ │924 - }, │ │925 - } │ │846 + const request_repo = (try requestRepoSource(io, allocator, host, parts.repo_base)) orelse return respondAttachmentParentNotFound(request); │ │847 + defer request_repo.deinit(allocator); │ │848 + const source = request_repo.source; │ │849 + switch (source.repo_kind) { │ │850 + inline else => |repo_kind| { │ │851 + var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, source.localInitOpts()); │ │852 + defer any_repo.deinit(io, allocator); │ │853 + switch (any_repo) { │ │854 + inline else => |*repo| _ = evt.Attachment.create(repo_kind, repo.self_repo_opts, io, allocator, repo, &parent_id_hex, blob, author) catch |err| switch (err) { │ │855 + error.ParentNotFound => return respondAttachmentParentNotFound(request), │ │856 + else => return err, │ │857 }, │ │858 } │ │859 }, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯