╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │546 var id_bytes: [evt.event_id_size]u8 = undefined; │ │547 io.random(&id_bytes); │ │548 const event_id_hex = std.fmt.bytesToHex(id_bytes, .lower); │ │549 + │ │550 + if (kind == .patch) { │ │551 + const remote = switch (host) { │ │552 + .remote => |remote| remote, │ │553 + .local => return respondRemoveNotFound(request), │ │554 + }; │ │555 + const user_id = patch_user_id orelse return respondLoginRequired(request); │ │556 + const request_repo = (try requestRepoSource(io, allocator, host, base)) orelse return respondRemoveNotFound(request); │ │557 + defer request_repo.deinit(allocator); │ │558 + const repo_id = evt.parseEventId(std.fs.path.basename(request_repo.source.path)) catch return respondRemoveNotFound(request);│ │559 + var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = remote.admin_repo_path }); │ │560 + defer admin_repo.deinit(io, allocator); │ │561 + const repos_dir = try std.fs.path.join(allocator, &.{ std.fs.path.dirname(remote.admin_repo_path) orelse ".", "repos" }); │ │562 + defer allocator.free(repos_dir); │ │563 + const path = try fork.create(.{}, io, allocator, repos_dir, &admin_repo, .{ │ │564 + .id = event_id_hex, │ │565 + .user_id = user_id, │ │566 + .repo_id = repo_id, │ │567 + .title = title, │ │568 + .description = description, │ │569 + .tags = tags, │ │570 + .author = author, │ │571 + .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │572 + }); │ │573 + defer allocator.free(path); │ │574 + const location = try std.fmt.allocPrint(allocator, "{s}/patch:{s}", .{ base, &event_id_hex }); │ │575 + defer allocator.free(location); │ │576 + try request.respond("", .{ .status = .see_other, .extra_headers = &.{.{ .name = "location", .value = location }} }); │ │577 + return; │ │578 + } │ │579 │ │580 const event = evt.EventWithId{ │ │581 .id = event_id_hex, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │583 .author = author, │ │584 .event = switch (kind) { │ │585 .issue => .{ .issue = .{ .title = title, .description = description, .tags = tags } }, │ │586 + .patch => unreachable, │ │587 .discuss => .{ .discuss = .{ .title = title, .description = description, .tags = tags } },│ │588 else => unreachable, │ │589 }, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │628 const route = ui.RoutablePage.fromUrl(base) orelse ui.RoutablePage.fromUrlLocal(base) orelse return null; │ │629 const identity, const thread_kind, const thread_hex, const comment_hex = switch (route) { │ │630 .repo_issues => |*issue| .{ issue.name.slice(), evt.EventKind.issue, issue.selected.slice(), issue.comment.slice() }, │ │631 + .repo_patches => |*patch| .{ patch.name.slice(), evt.EventKind.patch, patch.selected.slice(), patch.comment.slice() }, │ │632 .repo_discussions => |*discussion| .{ discussion.name.slice(), evt.EventKind.discuss, discussion.selected.slice(), discussion.comment.slice() },│ │633 else => return null, │ │634 }; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯