╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │587 - var thread_id: [evt.event_id_size]u8 = undefined; │ │588 - _ = std.fmt.hexToBytes(&thread_id, thread_hex) catch return null; │ │589 - const comment_id = if (comment_hex.len == 0) null else blk: { │ │590 - var id: [evt.event_id_size]u8 = undefined; │ │591 - _ = std.fmt.hexToBytes(&id, comment_hex) catch return null; │ │592 - break :blk id; │ │593 - }; │ │635 + const thread_id = evt.parseEventId(thread_hex) catch return null; │ │636 + const comment_id = if (comment_hex.len == 0) null else evt.parseEventId(comment_hex) catch return null; │ │637 const repo_base_len = if (identity.len == 0) 0 else "/repo/".len + identity.len; │ │638 return .{ .repo_base = base[0..repo_base_len], .thread_kind = thread_kind, .thread_id = thread_id, .comment_id = comment_id }; │ │639 +} │ │640 + │ │641 +fn handlePatchPost( │ │642 + io: std.Io, │ │643 + request: *std.http.Server.Request, │ │644 + allocator: std.mem.Allocator, │ │645 + base: []const u8, │ │646 + host: Host, │ │647 +) !void { │ │648 + const parts = commentBaseParts(base) orelse return respondRemoveNotFound(request); │ │649 + if (parts.thread_kind != .patch or parts.comment_id != null) return respondRemoveNotFound(request); │ │650 + const remote = switch (host) { │ │651 + .remote => |remote| remote, │ │652 + .local => return respondRemoveNotFound(request), │ │653 + }; │ │654 + const token = getCookieValue(request, cookie_name) orelse return respondLoginRequired(request); │ │655 + var user_id: [evt.event_id_size]u8 = undefined; │ │656 + if (!remote.session_store.lookup(token, &user_id)) return respondLoginRequired(request); │ │657 + var author_arena = std.heap.ArenaAllocator.init(allocator); │ │658 + defer author_arena.deinit(); │ │659 + const author = (try eventAuthor(io, allocator, &author_arena, request, host)) orelse return respondLoginRequired(request); │ │660 + │ │661 + const request_repo = (try requestRepoSource(io, allocator, host, parts.repo_base)) orelse return respondRemoveNotFound(request);│ │662 + defer request_repo.deinit(allocator); │ │663 + const repo_id = evt.parseEventId(std.fs.path.basename(request_repo.source.path)) catch return respondRemoveNotFound(request); │ │664 + var target_repo = try rp.Repo(.xit, .{}).open(io, allocator, request_repo.source.localInitOpts()); │ │665 + defer target_repo.deinit(io, allocator); │ │666 + var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = remote.admin_repo_path }); │ │667 + defer admin_repo.deinit(io, allocator); │ │668 + const id = std.fmt.bytesToHex(parts.thread_id, .lower); │ │669 + const repos_dir = try std.fs.path.join(allocator, &.{ std.fs.path.dirname(remote.admin_repo_path) orelse ".", "repos" }); │ │670 + defer allocator.free(repos_dir); │ │671 + const path = try fork.forkPath(allocator, repos_dir, &id); │ │672 + defer allocator.free(path); │ │673 + try pch.post(.{}, io, allocator, &admin_repo, &target_repo, path, .{ │ │674 + .id = id, │ │675 + .user_id = user_id, │ │676 + .repo_id = repo_id, │ │677 + .author = author, │ │678 + .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │679 + }); │ │680 + try request.respond("", .{ │ │681 + .status = .see_other, │ │682 + .extra_headers = &.{.{ .name = "location", .value = base }}, │ │683 + }); │ │684 } │ │685 │ │686 // create a reply and redirect to its permalink. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │704 defer author_arena.deinit(); │ │705 const author = (try eventAuthor(io, allocator, &author_arena, request, host)) orelse return respondLoginRequired(request);│ │706 │ │619 - var body_buf: [256]u8 = undefined; │ │620 - const reader = request.readerExpectNone(&body_buf); │ │621 - const posted = reader.allocRemaining(allocator, .limited(65536)) catch &[_]u8{}; │ │707 + const posted = try readFormBody(request, allocator); │ │708 defer allocator.free(posted); │ │709 const body_crlf = (try parseFormField(allocator, posted, "body")) orelse try allocator.dupe(u8, ""); │ │710 defer allocator.free(body_crlf); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────╮ │760 // an attachment url is its repo's base and the attachment's event id │ │761 fn attachmentRequest(path: []const u8) ?AttachmentRequest { │ │762 const infix = "/attachment:"; │ │677 - const id_len = evt.event_id_size * 2; │ │763 const at = std.mem.lastIndexOf(u8, path, infix) orelse return null;│ │764 const tail = path[at + infix.len ..]; │ │680 - if (tail.len != id_len) return null; │ │681 - var id: [evt.event_id_size]u8 = undefined; │ │682 - _ = std.fmt.hexToBytes(&id, tail) catch return null; │ │765 + const id = evt.parseEventId(tail) catch return null; │ │766 return .{ .repo_base = path[0..at], .id = id }; │ │767 } │ │768 │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │886 // what an attachment hangs off: the `<kind>:<id>` segment the form's page ends │ │887 // with, whatever kind that is │ │888 fn attachParentParts(base: []const u8) ?AttachParentParts { │ │806 - const id_len = evt.event_id_size * 2; │ │807 - if (base.len < id_len + 1) return null; │ │808 - const head = base[0 .. base.len - id_len]; │ │809 - if (head[head.len - 1] != ':') return null; │ │810 - var id_bytes: [evt.event_id_size]u8 = undefined; │ │811 - _ = std.fmt.hexToBytes(&id_bytes, base[base.len - id_len ..]) catch return null;│ │812 - const segment_at = std.mem.lastIndexOfScalar(u8, head, '/') orelse return null; │ │813 - return .{ .repo_base = head[0..segment_at], .id_bytes = id_bytes }; │ │889 + const segment_at = std.mem.lastIndexOfScalar(u8, base, '/') orelse return null; │ │890 + const segment = base[segment_at + 1 ..]; │ │891 + const colon_at = std.mem.lastIndexOfScalar(u8, segment, ':') orelse return null;│ │892 + const id_bytes = evt.parseEventId(segment[colon_at + 1 ..]) catch return null; │ │893 + return .{ .repo_base = base[0..segment_at], .id_bytes = id_bytes }; │ │894 } │ │895 │ │896 // attach the posted file to the event the url names, then reload. the file is │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │903 base: []const u8, │ │904 host: Host, │ │905 ) !void { │ │826 - // reading the body invalidates the request head's memory, and the url is │ │827 - // still needed to resolve the repo afterward │ │828 - const base_copy = try allocator.dupe(u8, base); │ │829 - defer allocator.free(base_copy); │ │906 + const parts = attachParentParts(base) orelse return respondAttachmentParentNotFound(request); │ │907 │ │831 - const parts = attachParentParts(base_copy) orelse return respondAttachmentParentNotFound(request); │ │832 - │ │908 var author_arena = std.heap.ArenaAllocator.init(allocator); │ │909 defer author_arena.deinit(); │ │910 const author = (try eventAuthor(io, allocator, &author_arena, request, host)) orelse return respondLoginRequired(request);│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1015 defer author_arena.deinit(); │ │1016 const author = (try eventAuthor(io, allocator, &author_arena, request, host)) orelse return respondLoginRequired(request);│ │1017 │ │ 943 - var body_buf: [256]u8 = undefined; │ │ 944 - const reader = request.readerExpectNone(&body_buf); │ │ 945 - const posted = reader.allocRemaining(allocator, .limited(65536)) catch &[_]u8{}; │ │1018 + const posted = try readFormBody(request, allocator); │ │1019 defer allocator.free(posted); │ │1020 const body_crlf = (try parseFormField(allocator, posted, "body")) orelse try allocator.dupe(u8, ""); │ │1021 defer allocator.free(body_crlf); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1050 }); │ │1051 } │ │1052 │ │ 980 -const IssueBaseParts = struct { repo_base: []const u8, id_bytes: [evt.event_id_size]u8 }; │ │ 981 -fn issueBaseParts(base: []const u8) ?IssueBaseParts { │ │ 982 - const parts = commentBaseParts(base) orelse return null; │ │ 983 - if (parts.thread_kind != .issue or parts.comment_id != null) return null; │ │ 984 - return .{ .repo_base = parts.repo_base, .id_bytes = parts.thread_id }; │ │1053 +fn updateThread( │ │1054 + comptime Event: type, │ │1055 + io: std.Io, │ │1056 + allocator: std.mem.Allocator, │ │1057 + host: Host, │ │1058 + repo_base: []const u8, │ │1059 + id: *const [evt.event_id_size]u8, │ │1060 + update: Event.Update, │ │1061 + author: evt.CommitAuthor, │ │1062 +) !void { │ │1063 + const request_repo = (try requestRepoSource(io, allocator, host, repo_base)) orelse return error.NotFound; │ │1064 + defer request_repo.deinit(allocator); │ │1065 + const source = request_repo.source; │ │1066 + switch (source.repo_kind) { │ │1067 + inline else => |repo_kind| { │ │1068 + var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, source.localInitOpts()); │ │1069 + defer any_repo.deinit(io, allocator); │ │1070 + switch (any_repo) { │ │1071 + inline else => |*repo| try Event.update(repo_kind, repo.self_repo_opts, io, allocator, repo, id, update, author),│ │1072 + } │ │1073 + }, │ │1074 + } │ │1075 } │ │1076 │ │1077 const RemoveParts = struct { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1143 .status = .not_found, │ │1144 .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │1145 }); │ │1056 -} │ │1057 - │ │1058 -// re-emit the issue `parts` names with `update` applied, in the repo the │ │1059 -// host resolves the base to. error.NotFound when the base names no repo or │ │1060 -// the id no issue. │ │1061 -fn updateIssue( │ │1062 - io: std.Io, │ │1063 - allocator: std.mem.Allocator, │ │1064 - host: Host, │ │1065 - parts: IssueBaseParts, │ │1066 - update: evt.Issue.Update, │ │1067 - author: evt.CommitAuthor, │ │1068 -) !void { │ │1069 - const request_repo = (try requestRepoSource(io, allocator, host, parts.repo_base)) orelse return error.NotFound; │ │1070 - defer request_repo.deinit(allocator); │ │1071 - const source = request_repo.source; │ │1072 - switch (source.repo_kind) { │ │1073 - inline else => |repo_kind| { │ │1074 - var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, source.localInitOpts()); │ │1075 - defer any_repo.deinit(io, allocator); │ │1076 - switch (any_repo) { │ │1077 - inline else => |*repo| try evt.Issue.update(repo_kind, repo.self_repo_opts, io, allocator, repo, &parts.id_bytes, update, author),│ │1078 - } │ │1079 - }, │ │1080 - } │ │1146 } │ │1147 │ │1148 fn updateDiscussion( │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │1173 // set the status of the issue the url names (base is │ │1174 // "/repo/<owner>/<name>/issue:<id>", identity elided in local mode) by│ │1175 // re-emitting its event, then redirect back to it │ │1111 -fn handleIssueStatus( │ │1176 +fn handleThreadStatus( │ │1177 io: std.Io, │ │1178 request: *std.http.Server.Request, │ │1179 allocator: std.mem.Allocator, │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │1180 base: []const u8, │ │1181 host: Host, │ │1117 - status: evt.Issue.Status, │ │1182 + open: bool, │ │1183 ) !void { │ │1119 - const not_found = "issue not found"; │ │1120 - const parts = issueBaseParts(base) orelse { │ │1184 + const not_found = "thread not found"; │ │1185 + const parts = commentBaseParts(base) orelse { │ │1186 try request.respond(not_found, .{ │ │1187 .status = .not_found, │ │1188 .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }},│ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯