╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2214 return self; │ │2215 } │ │2216 │ │2217 + fn addTab(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), tab_ids: *std.AutoArrayHashMapUnmanaged(usize, void), label: []const u8, link: []const u8) !void {│ │2218 + var text_box = try wgt.TextBox(ui.Widget).init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │2219 + errdefer text_box.deinit(allocator); │ │2220 + text_box.getFocus().focusable = true; │ │2221 + text_box.getFocus().kind = .{ .custom = link }; │ │2222 + try tab_ids.put(allocator, text_box.getFocus().id, {}); │ │2223 + try box.children.put(allocator, text_box.getFocus().id, .{ │ │2224 + .widget = .{ .text_box = text_box }, │ │2225 + .rect = null, │ │2226 + .min_size = .{ .width = label.len + 2, .height = null }, │ │2227 + }); │ │2228 + } │ │2229 + │ │2230 pub fn deinit(self: *Header, allocator: std.mem.Allocator) void { │ │2231 self.box.deinit(allocator); │ │2232 self.tab_ids.deinit(allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 95 if (method == .POST) { │ │ 96 switch (host) { │ │ 97 .remote => |remote| { │ │ 98 - const PostRoute = enum { login, logout, ansi, issue, open, close, edit }; │ │ 98 + const PostRoute = enum { login, logout, ansi, issue, open, close, edit, resolve };│ │ 99 inline for (@typeInfo(PostRoute).@"enum".fields) |field| { │ │100 const suffix = "/" ++ field.name; │ │101 if (std.mem.endsWith(u8, path, suffix)) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │108 .open => handleIssueStatus(io, request, allocator, base, host, .open), │ │109 .close => handleIssueStatus(io, request, allocator, base, host, .closed),│ │110 .edit => handleIssueEdit(io, request, allocator, base, host), │ │111 + .resolve => handleIssueResolve(io, request, allocator, base, host), │ │112 }; │ │113 } │ │114 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │115 }, │ │116 .local => { │ │116 - const PostRoute = enum { issue, open, close, edit }; │ │117 + const PostRoute = enum { issue, open, close, edit, resolve };│ │118 inline for (@typeInfo(PostRoute).@"enum".fields) |field| { │ │119 const suffix = "/" ++ field.name; │ │120 if (std.mem.endsWith(u8, path, suffix)) { │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │124 .open => handleIssueStatus(io, request, allocator, base, host, .open), │ │125 .close => handleIssueStatus(io, request, allocator, base, host, .closed),│ │126 .edit => handleIssueEdit(io, request, allocator, base, host), │ │127 + .resolve => handleIssueResolve(io, request, allocator, base, host), │ │128 }; │ │129 } │ │130 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │684 }); │ │685 } │ │686 │ │687 +// resolve the conflict of the issue the url names (base is │ │688 +// "/repo/<owner>/<name>/issues/<id>", identity elided in local mode; the │ │689 +// form posts to "<base>/resolve") │ │690 +fn handleIssueResolve( │ │691 + io: std.Io, │ │692 + request: *std.http.Server.Request, │ │693 + allocator: std.mem.Allocator, │ │694 + base: []const u8, │ │695 + host: Host, │ │696 +) !void { │ │697 + var author_arena = std.heap.ArenaAllocator.init(allocator); │ │698 + defer author_arena.deinit(); │ │699 + const author_email = (try eventAuthorEmail(io, allocator, &author_arena, request, host)) orelse return respondLoginRequired(request);│ │700 + │ │701 + var body_buf: [256]u8 = undefined; │ │702 + const reader = request.readerExpectNone(&body_buf); │ │703 + const body = reader.allocRemaining(allocator, .limited(65536)) catch &[_]u8{}; │ │704 + defer allocator.free(body); │ │705 + │ │706 + var field_arena = std.heap.ArenaAllocator.init(allocator); │ │707 + defer field_arena.deinit(); │ │708 + const aa = field_arena.allocator(); │ │709 + const title = try parseFormField(aa, body, "title"); │ │710 + const tags = try parseFormField(aa, body, "tags"); │ │711 + │ │712 + // the hunk inputs post as d0, d1, ...; form submission normalizes their │ │713 + // textarea line breaks to CRLF │ │714 + var hunks: std.ArrayList([]const u8) = .empty; │ │715 + var hunk_index: usize = 0; │ │716 + while (true) : (hunk_index += 1) { │ │717 + const name = try std.fmt.allocPrint(aa, "d{d}", .{hunk_index}); │ │718 + const posted = (try parseFormField(aa, body, name)) orelse break; │ │719 + try hunks.append(aa, try std.mem.replaceOwned(u8, aa, posted, "\r\n", "\n")); │ │720 + } │ │721 + │ │722 + const not_found = "issue not found"; │ │723 + const parts = issueBaseParts(base) orelse { │ │724 + try request.respond(not_found, .{ │ │725 + .status = .not_found, │ │726 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │727 + }); │ │728 + return; │ │729 + }; │ │730 + │ │731 + updateIssue(io, allocator, host, parts, .{ .resolve = .{ │ │732 + .title = title, │ │733 + .tags = tags, │ │734 + .hunks = hunks.items, │ │735 + } }, author_email) catch |err| switch (err) { │ │736 + error.NotFound => { │ │737 + try request.respond(not_found, .{ │ │738 + .status = .not_found, │ │739 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }}, │ │740 + }); │ │741 + return; │ │742 + }, │ │743 + // invalid fields send the user back to the resolve form │ │744 + error.InvalidFields => { │ │745 + const form_location = try std.fmt.allocPrint(allocator, "{s}/resolve", .{base}); │ │746 + defer allocator.free(form_location); │ │747 + try request.respond("", .{ │ │748 + .status = .see_other, │ │749 + .extra_headers = &.{.{ .name = "location", .value = form_location }}, │ │750 + }); │ │751 + return; │ │752 + }, │ │753 + else => |e| return e, │ │754 + }; │ │755 + │ │756 + try request.respond("", .{ │ │757 + .status = .see_other, │ │758 + .extra_headers = &.{.{ .name = "location", .value = base }}, │ │759 + }); │ │760 +} │ │761 + │ │762 fn renderIndexHtml( │ │763 io: std.Io, │ │764 allocator: std.mem.Allocator, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ added ──────╮ │src/diff3.zig│ ╰─────────────╯