╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────╮ │ 6 const evt = @import("./event.zig"); │ │ 7 const serve_common = @import("./serve_common.zig");│ │ 8 const ui = @import("./ui.zig"); │ │ 9 +const fork = @import("./fork.zig"); │ │10 +const pch = @import("./patch.zig"); │ │11 const xitui = xit.xitui; │ │12 const wgt = xitui.widget; │ │13 const Focus = xitui.focus.Focus; │ ╰───────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │122 host: Host, │ │123 ) !void { │ │124 const method = request.head.method; │ │123 - const uri = try std.Uri.parseAfterScheme("", request.head.target); │ │125 + const owned_target = if (method == .POST) try allocator.dupe(u8, request.head.target) else null;│ │126 + defer if (owned_target) |target| allocator.free(target); │ │127 + const uri = try std.Uri.parseAfterScheme("", owned_target orelse request.head.target); │ │128 const path = uri.path.percent_encoded; │ │129 │ │130 // POST routes can be scoped by any page, so they can redirect using │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │133 if (method == .POST) { │ │134 switch (host) { │ │135 .remote => |remote| { │ │132 - const PostRoute = enum { login, logout, ansi, new, edit, remove, open, close, resolve, attach }; │ │136 + const PostRoute = enum { login, logout, ansi, new, edit, remove, open, close, resolve, post, attach };│ │137 inline for (@typeInfo(PostRoute).@"enum".fields) |field| { │ │138 const suffix = "/" ++ field.name; │ │139 if (std.mem.endsWith(u8, path, suffix)) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 }; │ ╰──────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯