╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1003 base: []const u8, │ │1004 host: Host, │ │1005 ) !void { │ │1001 - if (commentBaseParts(base)) |parts| { │ │1002 - if (parts.comment_id != null) return handleCommentEdit(io, request, allocator, base, host, parts);│ │1003 - } │ │1004 - return handleIssueEdit(io, request, allocator, base, host); │ │1006 + const parts = commentBaseParts(base) orelse return respondRemoveNotFound(request); │ │1007 + if (parts.comment_id != null) return handleCommentEdit(io, request, allocator, base, host, parts); │ │1008 + return handleTopicEdit(io, request, allocator, base, host, parts); │ │1009 } │ │1010 │ │1011 // replace the body of the comment the url names, then redirect to its │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │1059 }); │ │1060 } │ │1061 │ │1058 -// split an issue url ("/repo/<owner>/<name>/issue:<id>", identity elided in │ │1059 -// local mode) into what precedes "/issue:" and the decoded id, null when it │ │1060 -// isn't one. the repo base is validated per host in updateIssue. │ │1062 const IssueBaseParts = struct { repo_base: []const u8, id_bytes: [evt.event_id_size]u8 }; │ │1063 fn issueBaseParts(base: []const u8) ?IssueBaseParts { │ │1063 - const issues_infix = "/issue:"; │ │1064 - const id_len = evt.event_id_size * 2; │ │1065 - if (base.len < issues_infix.len + id_len) return null; │ │1066 - var id_bytes: [evt.event_id_size]u8 = undefined; │ │1067 - _ = std.fmt.hexToBytes(&id_bytes, base[base.len - id_len ..]) catch return null; │ │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 };│ │1064 + const parts = commentBaseParts(base) orelse return null; │ │1065 + if (parts.thread_kind != .issue or parts.comment_id != null) return null; │ │1066 + return .{ .repo_base = parts.repo_base, .id_bytes = parts.thread_id }; │ │1067 } │ │1068 │ │1069 const RemoveParts = struct { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │1082 .id = attachment.id, │ │1083 }; │ │1084 } │ │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, │ │1085 + if (commentBaseParts(base)) |thread| return .{ │ │1086 + .repo_base = thread.repo_base, │ │1087 + .kind = if (thread.comment_id != null) .comment else thread.thread_kind, │ │1088 + .id = thread.comment_id orelse thread.thread_id, │ │1089 }; │ │1090 return null; │ │1091 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────╮ │1092 │ │1102 -// remove the issue, comment or attachment named by the form's url│ │1093 +// remove the event named by the form's url │ │1094 fn handleRemove( │ │1095 io: std.Io, │ │1096 request: *std.http.Server.Request, │ ╰────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1194 defer any_repo.deinit(io, allocator); │ │1195 switch (any_repo) { │ │1196 inline else => |*repo| try evt.Issue.update(repo_kind, repo.self_repo_opts, io, allocator, repo, &parts.id_bytes, update, author), │ │1197 + } │ │1198 + }, │ │1199 + } │ │1200 + }, │ │1201 + } │ │1202 +} │ │1203 + │ │1204 +fn updateDiscussion( │ │1205 + io: std.Io, │ │1206 + allocator: std.mem.Allocator, │ │1207 + host: Host, │ │1208 + repo_base: []const u8, │ │1209 + id: *const [evt.event_id_size]u8, │ │1210 + title: []const u8, │ │1211 + tags: []const u8, │ │1212 + description: []const u8, │ │1213 + author: evt.CommitAuthor, │ │1214 +) !void { │ │1215 + switch (host) { │ │1216 + .remote => |remote| { │ │1217 + const repo_prefix = "/repo/"; │ │1218 + if (!std.mem.startsWith(u8, repo_base, repo_prefix)) return error.NotFound; │ │1219 + const repos_dir = try std.fs.path.join(allocator, &.{ std.fs.path.dirname(remote.admin_repo_path) orelse ".", "repos" }); │ │1220 + defer allocator.free(repos_dir); │ │1221 + const repo_path = switch (try serve_common.resolveRepoPath(io, allocator, repos_dir, remote.admin_repo_path, repo_base[repo_prefix.len..], false)) { │ │1222 + .ok => |p| p, │ │1223 + .invalid, .not_found => return error.NotFound, │ │1224 + }; │ │1225 + defer allocator.free(repo_path); │ │1226 + │ │1227 + var repo = try rp.Repo(.xit, .{}).open(io, allocator, .{ .path = repo_path }); │ │1228 + defer repo.deinit(io, allocator); │ │1229 + try evt.Discussion.update(.xit, .{}, io, allocator, &repo, id, title, tags, description, author); │ │1230 + }, │ │1231 + .local => |src| { │ │1232 + if (repo_base.len != 0) return error.NotFound; │ │1233 + switch (src.repo_kind) { │ │1234 + inline else => |repo_kind| { │ │1235 + var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, src.localInitOpts()); │ │1236 + defer any_repo.deinit(io, allocator); │ │1237 + switch (any_repo) { │ │1238 + inline else => |*repo| try evt.Discussion.update(repo_kind, repo.self_repo_opts, io, allocator, repo, id, title, tags, description, author),│ │1239 } │ │1240 }, │ │1241 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │1287 }); │ │1288 } │ │1289 │ │1257 -// replace the title/tags/description of the issue the url names (base is│ │1258 -// "/repo/<owner>/<name>/issue:<id>", identity elided in local mode; the │ │1259 -// form posts to "<base>/edit") by re-emitting its event with its status │ │1260 -// preserved, then redirect back to it │ │1261 -fn handleIssueEdit( │ │1290 +// replace the title, tags and description of an issue or discussion. │ │1291 +fn handleTopicEdit( │ │1292 io: std.Io, │ │1293 request: *std.http.Server.Request, │ │1294 allocator: std.mem.Allocator, │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────╮ │1295 base: []const u8, │ │1296 host: Host, │ │1297 + parts: CommentBaseParts, │ │1298 ) !void { │ │1299 var author_arena = std.heap.ArenaAllocator.init(allocator);│ │1300 defer author_arena.deinit(); │ ╰─────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────╮ │1317 defer allocator.free(description); │ │1318 │ │1319 // invalid fields send the user back to the edit form │ │1289 - if (!evt.Issue.fieldsValid(title, tags)) { │ │1320 + const valid = switch (parts.thread_kind) { │ │1321 + .issue => evt.Issue.fieldsValid(title, tags), │ │1322 + .discussion => evt.Discussion.fieldsValid(title, tags), │ │1323 + else => unreachable, │ │1324 + }; │ │1325 + if (!valid) { │ │1326 const form_location = try std.fmt.allocPrint(allocator, "{s}/edit", .{base});│ │1327 defer allocator.free(form_location); │ │1328 try request.respond("", .{ │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1332 return; │ │1333 } │ │1334 │ │1299 - const not_found = "issue not found"; │ │1300 - const parts = issueBaseParts(base) orelse { │ │1301 - try request.respond(not_found, .{ │ │1302 - .status = .not_found, │ │1303 - .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │1304 - }); │ │1305 - return; │ │1335 + const not_found = switch (parts.thread_kind) { │ │1336 + .issue => "issue not found", │ │1337 + .discussion => "discussion not found", │ │1338 + else => unreachable, │ │1339 }; │ │1307 - │ │1308 - updateIssue(io, allocator, host, parts, .{ .fields = .{ │ │1309 - .title = title, │ │1310 - .tags = tags, │ │1311 - .description = description, │ │1312 - } }, author) catch |err| switch (err) { │ │1340 + (switch (parts.thread_kind) { │ │1341 + .issue => updateIssue(io, allocator, host, .{ .repo_base = parts.repo_base, .id_bytes = parts.thread_id }, .{ .fields = .{│ │1342 + .title = title, │ │1343 + .tags = tags, │ │1344 + .description = description, │ │1345 + } }, author), │ │1346 + .discussion => updateDiscussion(io, allocator, host, parts.repo_base, &parts.thread_id, title, tags, description, author),│ │1347 + else => unreachable, │ │1348 + }) catch |err| switch (err) { │ │1349 error.NotFound => { │ │1350 try request.respond(not_found, .{ │ │1351 .status = .not_found, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯