╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ───────────────╮ │src/ui/widget/thread.zig│ ╰────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2565 const route = listRoute(self.data.identity, @enumFromInt(0), "", entry.id) orelse return;│ │2566 try self.session.navigate(route); │ │2567 } else unreachable, │ │2568 + .merge => if (comptime supports_merge) { │ │2569 + if (comptime wasm) return; │ │2570 + const entry = self.data.selectedThread() orelse return; │ │2571 + try self.data.mergePatch(self.session, allocator, entry.id, button_index > 1); │ │2572 + const merged = std.meta.stringToEnum(Status, "merged") orelse return; │ │2573 + const route = listRoute(self.data.identity, merged, "", entry.id) orelse return; │ │2574 + try self.session.navigate(route); │ │2575 + } else unreachable, │ │2576 .remove => try self.submitRemove(allocator), │ │2577 } │ │2578 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │134 if (method == .POST) { │ │135 switch (host) { │ │136 .remote => |remote| { │ │137 - const PostRoute = enum { login, logout, ansi, new, edit, remove, open, close, resolve, publish, attach }; │ │137 + const PostRoute = enum { login, logout, ansi, new, edit, remove, open, close, resolve, publish, merge, squash, attach };│ │138 inline for (@typeInfo(PostRoute).@"enum".fields) |field| { │ │139 const suffix = "/" ++ field.name; │ │140 if (std.mem.endsWith(u8, path, suffix)) { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │150 .close => handleThreadStatus(io, request, allocator, base, host, false), │ │151 .resolve => handleThreadResolve(io, request, allocator, base, host), │ │152 .publish => handlePatchPublish(io, request, allocator, base, host), │ │153 + .merge => handlePatchMerge(io, request, allocator, base, host, .source), │ │154 + .squash => handlePatchMerge(io, request, allocator, base, host, .squash),│ │155 .attach => handleAttach(io, request, allocator, base, host), │ │156 }; │ │157 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │681 try request.respond("", .{ │ │682 .status = .see_other, │ │683 .extra_headers = &.{.{ .name = "location", .value = base }}, │ │684 + }); │ │685 +} │ │686 + │ │687 +fn handlePatchMerge( │ │688 + io: std.Io, │ │689 + request: *std.http.Server.Request, │ │690 + allocator: std.mem.Allocator, │ │691 + base: []const u8, │ │692 + host: Host, │ │693 + revision: pch.MergeRevision, │ │694 +) !void { │ │695 + const parts = commentBaseParts(base) orelse return respondRemoveNotFound(request); │ │696 + if (parts.thread_kind != .patch or parts.comment_id != null) return respondRemoveNotFound(request); │ │697 + const remote = switch (host) { │ │698 + .remote => |remote| remote, │ │699 + .local => return respondRemoveNotFound(request), │ │700 + }; │ │701 + var author_arena = std.heap.ArenaAllocator.init(allocator); │ │702 + defer author_arena.deinit(); │ │703 + const author = ((try eventAuthor(io, allocator, &author_arena, request, host)) orelse return respondLoginRequired(request)).author;│ │704 + │ │705 + const request_repo = (try requestRepoSource(io, allocator, host, parts.repo_base)) orelse return respondRemoveNotFound(request); │ │706 + defer request_repo.deinit(allocator); │ │707 + var target_repo = try rp.Repo(.xit, .{}).open(io, allocator, request_repo.source.localInitOpts()); │ │708 + defer target_repo.deinit(io, allocator); │ │709 + var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = remote.admin_repo_path }); │ │710 + defer admin_repo.deinit(io, allocator); │ │711 + │ │712 + const id = std.fmt.bytesToHex(parts.thread_id, .lower); │ │713 + const repos_dir = try std.fs.path.join(allocator, &.{ std.fs.path.dirname(remote.admin_repo_path) orelse ".", "repos" }); │ │714 + defer allocator.free(repos_dir); │ │715 + try pch.mergeAndRemoveFork(.{}, io, allocator, repos_dir, &admin_repo, &target_repo, .{ │ │716 + .id = id, │ │717 + .revision = revision, │ │718 + .author = author, │ │719 + .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │720 + }); │ │721 + │ │722 + const location = try std.fmt.allocPrint(allocator, "{s}/patch:{s}", .{ parts.repo_base, &id }); │ │723 + defer allocator.free(location); │ │724 + try request.respond("", .{ │ │725 + .status = .see_other, │ │726 + .keep_alive = false, │ │727 + .extra_headers = &.{.{ .name = "location", .value = location }}, │ │728 }); │ │729 } │ │730 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯