╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │906 const prefix = try repoUrlPrefix(arena, p.name.slice()); │ │907 if (p.view == .edit) break :blk try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ patch_seg ++ "{s}/edit", .{ prefix, p.selected.slice() }); │ │908 if (p.view == .publish) break :blk try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ patch_seg ++ "{s}/publish", .{ prefix, p.selected.slice() }); │ │909 + if (p.view == .merge) break :blk try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ patch_seg ++ "{s}/merge", .{ prefix, p.selected.slice() }); │ │910 if (p.view == .description) break :blk try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ patch_seg ++ "{s}/description", .{ prefix, p.selected.slice() });│ │911 if (p.view == .resolve) break :blk if (p.theirs.len == 0) │ │912 try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ patch_seg ++ "{s}/resolve", .{ prefix, p.selected.slice() }) │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1251 if (std.mem.eql(u8, tail, "new")) return if (params.only(&.{})) repoThreadCommentNewRoute(.patch, pair, patch_id, "") else null; │ │1252 if (std.mem.eql(u8, tail, "edit")) return if (params.only(&.{})) repoThreadEditRoute(.patch, pair, patch_id) else null; │ │1253 if (std.mem.eql(u8, tail, "publish")) return if (params.only(&.{})) repoPatchPublishRoute(pair, patch_id) else null; │ │1254 + if (std.mem.eql(u8, tail, "merge")) return if (params.only(&.{})) repoPatchMergeRoute(pair, patch_id) else null; │ │1255 if (std.mem.eql(u8, tail, "remove")) return if (params.only(&.{})) repoThreadRemoveRoute(.patch, pair, patch_id, "") else null; │ │1256 if (std.mem.eql(u8, tail, "description")) return if (params.only(&.{})) repoThreadDescriptionRoute(.patch, pair, patch_id) else null;│ │1257 if (std.mem.eql(u8, tail, "resolve")) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Patches.zig│ ╰───────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │150 return ui.RoutablePage.repoPatchPublishRoute(identity, selected); │ │151 } │ │152 │ │153 +pub fn mergeRoute(identity: []const u8, selected: []const u8) ?ui.RoutablePage {│ │154 + return ui.RoutablePage.repoPatchMergeRoute(identity, selected); │ │155 +} │ │156 + │ │157 +pub fn canMerge(entry: Entry, session: *const ui.Session) bool { │ │158 + return !entry.draft and │ │159 + entry.fork_exists and │ │160 + !entry.conflicted and │ │161 + entry.record.event.revision != null and │ │162 + entry.record.event.status == .open and │ │163 + !session.data.is_local and │ │164 + session.data.user_id != null; │ │165 +} │ │166 + │ │167 pub fn createDraft( │ │168 data: *const Self, │ │169 session: *ui.Session, │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │216 .id = id_hex, │ │217 .user_id = user_id, │ │218 .repo_id = repo_id, │ │219 + .author = author, │ │220 + .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │221 + }); │ │222 +} │ │223 + │ │224 +pub fn mergePatch(data: *const Self, session: *ui.Session, allocator: std.mem.Allocator, id: []const u8, squash: bool) !void {│ │225 + const io = session.io orelse return error.NotFound; │ │226 + const repos_dir = session.repos_dir orelse return error.NotFound; │ │227 + const admin_repo = session.admin_repo orelse return error.NotFound; │ │228 + const repo_source = data.repo_source orelse return error.NotFound; │ │229 + if (repo_source.repo_kind != .xit) return error.NotFound; │ │230 + const patch_id = evt.parseEventId(id) catch return error.NotFound; │ │231 + const author = (try session.eventAuthor()) orelse return error.NotFound; │ │232 + │ │233 + const id_hex = std.fmt.bytesToHex(patch_id, .lower); │ │234 + var target_repo = try rp.Repo(.xit, .{}).open(io, allocator, repo_source.localInitOpts()); │ │235 + defer target_repo.deinit(io, allocator); │ │236 + try pch.mergeAndRemoveFork(.{}, io, allocator, repos_dir, admin_repo, &target_repo, .{ │ │237 + .id = id_hex, │ │238 + .revision = if (squash) .squash else .source, │ │239 .author = author, │ │240 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │241 }); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │502 .merged => merged_root = order_key, │ │503 } │ │504 // form urls keep their view; otherwise the patch's status picks it. │ │471 - if (view != .edit and view != .new_comment and view != .edit_comment and view != .remove) resolved_view = switch (patch_event.event.status) { │ │505 + if (view != .edit and view != .merge and view != .new_comment and view != .edit_comment and view != .remove) resolved_view = switch (patch_event.event.status) {│ │506 .open => .open, │ │507 .closed => .closed, │ │508 .merged => .merged, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │824 const route = switch (data.view) { │ │825 .edit => ui.RoutablePage.repoThreadEditRoute(.patch, data.identity, data.selected_id) orelse return error.RouteTooLong, │ │826 .publish => ui.RoutablePage.repoPatchPublishRoute(data.identity, data.selected_id) orelse return error.RouteTooLong, │ │827 + .merge => ui.RoutablePage.repoPatchMergeRoute(data.identity, data.selected_id) orelse return error.RouteTooLong, │ │828 .new_comment => ui.RoutablePage.repoThreadCommentNewRoute(.patch, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, │ │829 .edit_comment => ui.RoutablePage.repoThreadCommentEditRoute(.patch, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong,│ │830 .remove => ui.RoutablePage.repoThreadRemoveRoute(.patch, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────╮ │835 const label: []const u8 = switch (data.view) {│ │836 .edit => "edit", │ │837 .publish => "publish", │ │838 + .merge => "merge", │ │839 .new_comment => "reply", │ │840 .edit_comment => "edit", │ │841 .remove => "remove", │ ╰───────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/ui/widget/thread.zig│ ╰────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────╮ │354 const supports_conflicts = Event.merge_policy == .field_conflicts;│ │355 const supports_drafts = @hasField(Data, "drafts"); │ │356 const supports_forks = @hasField(Entry, "fork_exists"); │ │357 + const supports_merge = @hasDecl(Data, "mergeRoute"); │ │358 return struct { │ │359 const This = @This(); │ │360 const thread_name = Data.thread_name; │ ╰───────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │472 │ │473 fn publishRoute(identity: []const u8, selected: []const u8) ?ui.RoutablePage { │ │474 return if (supports_drafts) Data.publishRoute(identity, selected) else null;│ │475 + } │ │476 + │ │477 + fn mergeRoute(identity: []const u8, selected: []const u8) ?ui.RoutablePage { │ │478 + return if (supports_merge) Data.mergeRoute(identity, selected) else null; │ │479 } │ │480 │ │481 fn forkRoute(identity: []const u8, id: []const u8) ?ui.RoutablePage { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │534 var spacer = try Spacer.init(allocator); │ │535 errdefer spacer.deinit(allocator); │ │536 try row.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .spacer = spacer }, .rect = null, .min_size = null }); │ │537 + } │ │538 + │ │539 + if (comptime supports_merge) { │ │540 + if (Data.canMerge(entry, self.session)) { │ │541 + const route = mergeRoute(self.data.identity, entry.id) orelse return error.RouteTooLong; │ │542 + try addToolButton(allocator, row, "merge", try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)}));│ │543 + } │ │544 } │ │545 │ │546 if (supports_drafts and entryDraft(entry)) { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯