╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │145 .new => handleNew(io, request, allocator, base, host), │ │146 .edit => handleEdit(io, request, allocator, base, host), │ │147 .remove => handleRemove(io, request, allocator, base, host), │ │144 - .open => handleIssueStatus(io, request, allocator, base, host, .open), │ │145 - .close => handleIssueStatus(io, request, allocator, base, host, .closed),│ │146 - .resolve => handleIssueResolve(io, request, allocator, base, host), │ │148 + .open => handleThreadStatus(io, request, allocator, base, host, true), │ │149 + .close => handleThreadStatus(io, request, allocator, base, host, false), │ │150 + .resolve => handleThreadResolve(io, request, allocator, base, host), │ │151 + .post => handlePatchPost(io, request, allocator, base, host), │ │152 .attach => handleAttach(io, request, allocator, base, host), │ │153 }; │ │154 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │164 .new => handleNew(io, request, allocator, base, host), │ │165 .edit => handleEdit(io, request, allocator, base, host), │ │166 .remove => handleRemove(io, request, allocator, base, host), │ │162 - .open => handleIssueStatus(io, request, allocator, base, host, .open), │ │163 - .close => handleIssueStatus(io, request, allocator, base, host, .closed),│ │164 - .resolve => handleIssueResolve(io, request, allocator, base, host), │ │167 + .open => handleThreadStatus(io, request, allocator, base, host, true), │ │168 + .close => handleThreadStatus(io, request, allocator, base, host, false), │ │169 + .resolve => handleThreadResolve(io, request, allocator, base, host), │ │170 .sync => handleSync(io, request, allocator, base, local), │ │171 .attach => handleAttach(io, request, allocator, base, host), │ │172 }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │309 admin_repo_path: []const u8, │ │310 session_store: SessionStore, │ │311 ) !void { │ │307 - var body_buf: [256]u8 = undefined; │ │308 - const reader = request.readerExpectNone(&body_buf); │ │309 - const body = reader.allocRemaining(allocator, .limited(65536)) catch &[_]u8{}; │ │312 + const body = try readFormBody(request, allocator); │ │313 defer allocator.free(body); │ │314 │ │315 const username = (try parseFormField(allocator, body, "username")) orelse try allocator.dupe(u8, "");│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │474 const discussions_suffix = "/discussions"; │ │475 if (std.mem.endsWith(u8, base, discussions_suffix)) │ │476 return handleTopicNew(io, request, allocator, base[0 .. base.len - discussions_suffix.len], host, .discuss);│ │477 + const patches_suffix = "/patches"; │ │478 + if (std.mem.endsWith(u8, base, patches_suffix)) │ │479 + return handleTopicNew(io, request, allocator, base[0 .. base.len - patches_suffix.len], host, .patch); │ │480 │ │481 try request.respond("new event target not found", .{ │ │482 .status = .not_found, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │496 var author_arena = std.heap.ArenaAllocator.init(allocator); │ │497 defer author_arena.deinit(); │ │498 const author = (try eventAuthor(io, allocator, &author_arena, request, host)) orelse return respondLoginRequired(request);│ │493 - │ │494 - var body_buf: [256]u8 = undefined; │ │495 - const reader = request.readerExpectNone(&body_buf); │ │496 - const body = reader.allocRemaining(allocator, .limited(65536)) catch &[_]u8{}; │ │499 + var patch_user_id: ?[evt.event_id_size]u8 = null; │ │500 + if (kind == .patch) { │ │501 + const remote = switch (host) { │ │502 + .remote => |remote| remote, │ │503 + .local => return respondRemoveNotFound(request), │ │504 + }; │ │505 + const token = getCookieValue(request, cookie_name) orelse return respondLoginRequired(request); │ │506 + var user_id: [evt.event_id_size]u8 = undefined; │ │507 + if (!remote.session_store.lookup(token, &user_id)) return respondLoginRequired(request); │ │508 + patch_user_id = user_id; │ │509 + } │ │510 + const body = try readFormBody(request, allocator); │ │511 defer allocator.free(body); │ │512 │ │513 const title = (try parseFormField(allocator, body, "title")) orelse try allocator.dupe(u8, ""); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────╮ │523 │ │524 const valid = switch (kind) { │ │525 .issue => evt.Issue.fieldsValid(title, tags), │ │526 + .patch => evt.Patch.fieldsValid(title, tags), │ │527 .discuss => evt.Discussion.fieldsValid(title, tags),│ │528 else => unreachable, │ │529 }; │ ╰─────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────╮ │530 if (!valid) { │ │531 const list_name: []const u8 = switch (kind) {│ │532 .issue => "issues", │ │533 + .patch => "patches", │ │534 .discuss => "discussions", │ │535 else => unreachable, │ │536 }; │ ╰──────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 }; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯