╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────╮ │1 const std = @import("std"); │ │2 const xit = @import("xit"); │ │3 const rp = xit.repo; │ │4 +const hash = xit.hash; │ │5 const evt = @import("./event.zig"); │ │6 const serve_common = @import("./serve_common.zig");│ │7 const ui = @import("./ui.zig"); │ ╰──────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │93 if (method == .POST) { │ │94 switch (host) { │ │95 .remote => |remote| { │ │95 - const PostRoute = enum { login, logout, ansi, issue }; │ │96 + const PostRoute = enum { login, logout, ansi, issue, open, close };│ │97 inline for (@typeInfo(PostRoute).@"enum".fields) |field| { │ │98 const suffix = "/" ++ field.name; │ │99 if (std.mem.endsWith(u8, path, suffix)) { │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │103 .logout => handleLogout(request, base, remote.session_store), │ │104 .ansi => handleAnsi(io, request, allocator, base, remote.admin_repo_path, remote.session_store),│ │105 .issue => handleIssue(io, request, allocator, base, remote.admin_repo_path), │ │106 + .open => handleIssueStatus(io, request, allocator, base, remote.admin_repo_path, .open), │ │107 + .close => handleIssueStatus(io, request, allocator, base, remote.admin_repo_path, .closed), │ │108 }; │ │109 } │ │110 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │426 }); │ │427 } │ │428 │ │429 +// set the status of the issue the url names (base is │ │430 +// "/repo/<owner>/<name>/issues/<id>") by re-emitting its event, then │ │431 +// redirect back to it │ │432 +fn handleIssueStatus( │ │433 + io: std.Io, │ │434 + request: *std.http.Server.Request, │ │435 + allocator: std.mem.Allocator, │ │436 + base: []const u8, │ │437 + admin_repo_path: []const u8, │ │438 + status: evt.Issue.Status, │ │439 +) !void { │ │440 + const repo_prefix = "/repo/"; │ │441 + const issues_infix = "/issues/"; │ │442 + const id_len = evt.event_id_size * 2; │ │443 + │ │444 + var id_bytes: [evt.event_id_size]u8 = undefined; │ │445 + const repo_base_maybe: ?[]const u8 = blk: { │ │446 + if (base.len < repo_prefix.len + issues_infix.len + id_len) break :blk null; │ │447 + _ = std.fmt.hexToBytes(&id_bytes, base[base.len - id_len ..]) catch break :blk null; │ │448 + const head = base[0 .. base.len - id_len]; │ │449 + if (!std.mem.endsWith(u8, head, issues_infix)) break :blk null; │ │450 + const repo_base = head[0 .. head.len - issues_infix.len]; │ │451 + if (!std.mem.startsWith(u8, repo_base, repo_prefix)) break :blk null; │ │452 + break :blk repo_base; │ │453 + }; │ │454 + │ │455 + const not_found = "issue not found"; │ │456 + const repo_base = repo_base_maybe orelse { │ │457 + try request.respond(not_found, .{ │ │458 + .status = .not_found, │ │459 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │460 + }); │ │461 + return; │ │462 + }; │ │463 + │ │464 + const repos_dir = try std.fs.path.join(allocator, &.{ std.fs.path.dirname(admin_repo_path) orelse ".", "repos" }); │ │465 + defer allocator.free(repos_dir); │ │466 + const repo_path = switch (try serve_common.resolveRepoPath(io, allocator, repos_dir, admin_repo_path, repo_base[repo_prefix.len..], false)) {│ │467 + .ok => |p| p, │ │468 + .invalid, .not_found => { │ │469 + try request.respond(not_found, .{ │ │470 + .status = .not_found, │ │471 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │472 + }); │ │473 + return; │ │474 + }, │ │475 + }; │ │476 + defer allocator.free(repo_path); │ │477 + │ │478 + const repo_opts: rp.RepoOpts(.xit) = .{}; │ │479 + var repo = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = repo_path }); │ │480 + defer repo.deinit(io, allocator); │ │481 + │ │482 + var arena = std.heap.ArenaAllocator.init(allocator); │ │483 + defer arena.deinit(); │ │484 + │ │485 + const issue_maybe: ?evt.Issue = blk: { │ │486 + const DB = rp.Repo(.xit, repo_opts).DB; │ │487 + const moment = evt.currentMoment(repo_opts, &repo) catch break :blk null; │ │488 + const event_id_to_issue_cursor = (try moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue"))) orelse break :blk null; │ │489 + const event_id_to_issue = try DB.HashMap(.read_only).init(event_id_to_issue_cursor); │ │490 + const issue_cursor = (try event_id_to_issue.getCursor(hash.hashInt(repo_opts.hash, &id_bytes))) orelse break :blk null; │ │491 + const issue_map = try DB.HashMap(.read_only).init(issue_cursor); │ │492 + break :blk try evt.read(evt.Issue, DB, repo_opts.hash, &arena, issue_map); │ │493 + }; │ │494 + const issue = issue_maybe orelse { │ │495 + try request.respond(not_found, .{ │ │496 + .status = .not_found, │ │497 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │498 + }); │ │499 + return; │ │500 + }; │ │501 + │ │502 + if (issue.status != status) { │ │503 + var updated = issue; │ │504 + updated.status = status; │ │505 + try evt.commitAndConsume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{.{ │ │506 + .id = base[base.len - id_len ..][0..id_len].*, │ │507 + .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │508 + .event = .{ .issue = updated }, │ │509 + }}); │ │510 + } │ │511 + │ │512 + // like logout, this is a bodyless POST, so close the connection rather than │ │513 + // letting the keep-alive path try to discard a body that isn't framed. │ │514 + try request.respond("", .{ │ │515 + .status = .see_other, │ │516 + .keep_alive = false, │ │517 + .extra_headers = &.{.{ .name = "location", .value = base }}, │ │518 + }); │ │519 +} │ │520 + │ │521 fn renderIndexHtml( │ │522 io: std.Io, │ │523 allocator: std.mem.Allocator, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯