╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │100 if (method == .POST) { │ │101 switch (host) { │ │102 .remote => |remote| { │ │103 - const PostRoute = enum { login, logout, ansi, new, edit, open, close, resolve, attach }; │ │103 + const PostRoute = enum { login, logout, ansi, new, edit, remove, open, close, resolve, attach };│ │104 inline for (@typeInfo(PostRoute).@"enum".fields) |field| { │ │105 const suffix = "/" ++ field.name; │ │106 if (std.mem.endsWith(u8, path, suffix)) { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │111 .ansi => handleAnsi(io, request, allocator, base, remote.admin_repo_path, remote.session_store),│ │112 .new => handleNew(io, request, allocator, base, host), │ │113 .edit => handleEdit(io, request, allocator, base, host), │ │114 + .remove => handleRemove(io, request, allocator, base, host), │ │115 .open => handleIssueStatus(io, request, allocator, base, host, .open), │ │116 .close => handleIssueStatus(io, request, allocator, base, host, .closed), │ │117 .resolve => handleIssueResolve(io, request, allocator, base, host), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │121 } │ │122 }, │ │123 .local => |local| { │ │123 - const PostRoute = enum { new, edit, open, close, resolve, sync, attach }; │ │124 + const PostRoute = enum { new, edit, remove, open, close, resolve, sync, attach };│ │125 inline for (@typeInfo(PostRoute).@"enum".fields) |field| { │ │126 const suffix = "/" ++ field.name; │ │127 if (std.mem.endsWith(u8, path, suffix)) { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │129 return switch (@field(PostRoute, field.name)) { │ │130 .new => handleNew(io, request, allocator, base, host), │ │131 .edit => handleEdit(io, request, allocator, base, host), │ │132 + .remove => handleRemove(io, request, allocator, base, host), │ │133 .open => handleIssueStatus(io, request, allocator, base, host, .open), │ │134 .close => handleIssueStatus(io, request, allocator, base, host, .closed),│ │135 .resolve => handleIssueResolve(io, request, allocator, base, host), │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │767 evt.currentMoment(repo_opts, repo)) catch return respondAttachmentNotFound(request); │ │768 │ │769 const record = (try evt.Attachment.readById(DB, repo_opts.hash, haxy_moment, &arena, id)) orelse return respondAttachmentNotFound(request);│ │768 - if (record.deleted) return respondAttachmentNotFound(request); │ │770 + if (record.removed) return respondAttachmentNotFound(request); │ │771 if (record.blob_oid.len != hash.hexLen(repo_opts.hash)) return respondAttachmentNotFound(request); │ │772 const blob_oid: *const [hash.hexLen(repo_opts.hash)]u8 = @ptrCast(record.blob_oid.ptr); │ │773 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1068 const head = base[0 .. base.len - id_len]; │ │1069 if (!std.mem.endsWith(u8, head, issues_infix)) return null; │ │1070 return .{ .repo_base = head[0 .. head.len - issues_infix.len], .id_bytes = id_bytes }; │ │1071 +} │ │1072 + │ │1073 +const RemoveParts = struct { │ │1074 + repo_base: []const u8, │ │1075 + kind: evt.EventKind, │ │1076 + id: [evt.event_id_size]u8, │ │1077 +}; │ │1078 + │ │1079 +// split a remove url into the repo and event it names │ │1080 +fn removeParts(base: []const u8) ?RemoveParts { │ │1081 + if (attachmentRequest(base)) |attachment| { │ │1082 + const parent = attachParentParts(attachment.repo_base) orelse return null; │ │1083 + return .{ │ │1084 + .repo_base = parent.repo_base, │ │1085 + .kind = .attach, │ │1086 + .id = attachment.id, │ │1087 + }; │ │1088 + } │ │1089 + if (commentBaseParts(base)) |comment| if (comment.comment_id) |comment_id| return .{ │ │1090 + .repo_base = comment.repo_base, │ │1091 + .kind = .comment, │ │1092 + .id = comment_id, │ │1093 + }; │ │1094 + if (issueBaseParts(base)) |issue| return .{ │ │1095 + .repo_base = issue.repo_base, │ │1096 + .kind = .issue, │ │1097 + .id = issue.id_bytes, │ │1098 + }; │ │1099 + return null; │ │1100 +} │ │1101 + │ │1102 +// remove the issue, comment or attachment named by the form's url │ │1103 +fn handleRemove( │ │1104 + io: std.Io, │ │1105 + request: *std.http.Server.Request, │ │1106 + allocator: std.mem.Allocator, │ │1107 + base: []const u8, │ │1108 + host: Host, │ │1109 +) !void { │ │1110 + const parts = removeParts(base) orelse return respondRemoveNotFound(request); │ │1111 + │ │1112 + var author_arena = std.heap.ArenaAllocator.init(allocator); │ │1113 + defer author_arena.deinit(); │ │1114 + const author = (try eventAuthor(io, allocator, &author_arena, request, host)) orelse return respondLoginRequired(request); │ │1115 + │ │1116 + switch (host) { │ │1117 + .remote => |remote| { │ │1118 + const repo_prefix = "/repo/"; │ │1119 + if (!std.mem.startsWith(u8, parts.repo_base, repo_prefix)) return respondRemoveNotFound(request); │ │1120 + const repos_dir = try std.fs.path.join(allocator, &.{ std.fs.path.dirname(remote.admin_repo_path) orelse ".", "repos" }); │ │1121 + defer allocator.free(repos_dir); │ │1122 + const repo_path = switch (try serve_common.resolveRepoPath(io, allocator, repos_dir, remote.admin_repo_path, parts.repo_base[repo_prefix.len..], false)) { │ │1123 + .ok => |p| p, │ │1124 + .invalid, .not_found => return respondRemoveNotFound(request), │ │1125 + }; │ │1126 + defer allocator.free(repo_path); │ │1127 + │ │1128 + var repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = repo_path }); │ │1129 + defer repo.deinit(io, allocator); │ │1130 + evt.remove(.xit, .{}, io, allocator, &repo, &parts.id, parts.kind, author) catch |err| switch (err) { │ │1131 + error.EventNotFound => return respondRemoveNotFound(request), │ │1132 + else => |e| return e, │ │1133 + }; │ │1134 + }, │ │1135 + .local => |src| { │ │1136 + if (parts.repo_base.len != 0) return respondRemoveNotFound(request); │ │1137 + switch (src.repo_kind) { │ │1138 + inline else => |repo_kind| { │ │1139 + var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, src.localInitOpts()); │ │1140 + defer any_repo.deinit(io, allocator); │ │1141 + switch (any_repo) { │ │1142 + inline else => |*repo| evt.remove(repo_kind, repo.self_repo_opts, io, allocator, repo, &parts.id, parts.kind, author) catch |err| switch (err) {│ │1143 + error.EventNotFound => return respondRemoveNotFound(request), │ │1144 + else => |e| return e, │ │1145 + }, │ │1146 + } │ │1147 + }, │ │1148 + } │ │1149 + }, │ │1150 + } │ │1151 + │ │1152 + const id = std.fmt.bytesToHex(parts.id, .lower); │ │1153 + const location = try std.fmt.allocPrint(allocator, "{s}/event:{s}/kind:{s}", .{ parts.repo_base, &id, @tagName(parts.kind) }); │ │1154 + defer allocator.free(location); │ │1155 + try request.respond("", .{ │ │1156 + .status = .see_other, │ │1157 + .keep_alive = false, │ │1158 + .extra_headers = &.{.{ .name = "location", .value = location }}, │ │1159 + }); │ │1160 } │ │1161 │ │1162 +fn respondRemoveNotFound(request: *std.http.Server.Request) !void { │ │1163 + try request.respond("event not found", .{ │ │1164 + .status = .not_found, │ │1165 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │1166 + }); │ │1167 +} │ │1168 + │ │1169 // re-emit the issue `parts` names with `update` applied, in the repo the │ │1170 // host resolves the base to. error.NotFound when the base names no repo or │ │1171 // the id no issue. │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1872 try out.appendSlice(allocator, "</textarea>"); │ │1873 }, │ │1874 .custom => |custom| { │ │1777 - if (std.mem.eql(u8, "submit", custom)) { │ │1875 + if (std.mem.eql(u8, "submit", custom) or std.mem.startsWith(u8, custom, ui.submit_action_prefix)) {│ │1876 try out.appendSlice(allocator, "<button type=\"submit\" data-focus-id=\""); │ │1877 var id_buf: [32]u8 = undefined; │ │1878 try out.appendSlice(allocator, try std.fmt.bufPrint(&id_buf, "{d}", .{inner_id})); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1879 + if (std.mem.startsWith(u8, custom, ui.submit_action_prefix)) { │ │1880 + try out.appendSlice(allocator, "\" formaction=\""); │ │1881 + try appendEscapedHtml(allocator, &out, custom[ui.submit_action_prefix.len..]); │ │1882 + } │ │1883 try out.appendSlice(allocator, "\" style=\"left:"); │ │1884 var pos_buf: [128]u8 = undefined; │ │1885 try out.appendSlice(allocator, try std.fmt.bufPrint(&pos_buf, "{d}ch;top:{d}em;width:{d}ch;height:{d}em", .{ r.x, r.y, r.size.width, r.size.height }));│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯