╭ changed ────╮ │src/patch.zig│ ╰─────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │79 if (local_patch.removed) { │ │80 if (existing == null) return error.PatchDataUnavailable; │ │81 } else { │ │82 - const selected = local_patch.event.revision orelse return error.PatchNotPushed; │ │83 - const revision_id = try evt.parseEventId(&selected.id); │ │84 - const revision = (try evt.PatchRev.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena, &revision_id)) orelse return error.PatchNotPushed; │ │85 - if (!selected.matches(revision)) return error.PatchNotPushed; │ │82 var patch = (existing orelse local_patch).event; │ │87 - patch.revision = selected; │ │83 + if (local_patch.event.revision) |selected| { │ │84 + const revision_id = try evt.parseEventId(&selected.id); │ │85 + const revision = (try evt.PatchRev.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena, &revision_id)) orelse return error.PatchNotPushed;│ │86 + if (!selected.matches(revision)) return error.PatchNotPushed; │ │87 + patch.revision = selected; │ │88 + } │ │89 if (existing == null) patch.status = .open; │ │90 │ │91 try evt.consume(.repo, .xit, repo_opts, io, allocator, target_repo, evt.events_ref, &.{.{ │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/test/event/patch.zig│ ╰────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │646 try std.testing.expect(null != try evt.PatchRev.readById(Repo.DB, repo_opts.hash, moment, &arena, &first_patchrev_id)); │ │647 } │ │648 try std.testing.expectError(error.NotFound, evt.currentMoment(repo_opts, &target)); │ │649 + │ │650 + // │ │651 + // publish another draft before its first push │ │652 + // │ │653 + │ │654 + const unpushed_id = evt.EventWithId.randomId(prng.random()); │ │655 + const unpushed_id_hex = std.fmt.bytesToHex(unpushed_id, .lower); │ │656 + const unpushed_path = try fork.create(repo_opts, io, allocator, repos_dir, &admin, .{ │ │657 + .id = unpushed_id_hex, │ │658 + .user_id = user_id, │ │659 + .repo_id = repo_id, │ │660 + .title = "explain the answer", │ │661 + .tags = "documentation", │ │662 + .description = "describes the existing answer", │ │663 + .author = author, │ │664 + .timestamp = 3, │ │665 + }); │ │666 + defer allocator.free(unpushed_path); │ │667 + try pch.publish(repo_opts, io, allocator, &admin, &target, unpushed_path, .{ │ │668 + .id = unpushed_id_hex, │ │669 + .user_id = user_id, │ │670 + .repo_id = repo_id, │ │671 + .author = author, │ │672 + .timestamp = 4, │ │673 + }); │ │674 + { │ │675 + var verify_arena = std.heap.ArenaAllocator.init(allocator); │ │676 + defer verify_arena.deinit(); │ │677 + const target_moment = try evt.currentMoment(repo_opts, &target); │ │678 + const unpushed = (try evt.Patch.readById(Repo.DB, repo_opts.hash, target_moment, &verify_arena, &unpushed_id)) orelse return error.NotFound;│ │679 + try std.testing.expectEqual(null, unpushed.event.revision); │ │680 + │ │681 + var unpushed_repo = try Repo.open(io, allocator, .{ .path = unpushed_path }); │ │682 + defer unpushed_repo.deinit(io, allocator); │ │683 + const fork_moment = try evt.currentMoment(repo_opts, &unpushed_repo); │ │684 + const removed = (try evt.Patch.readById(Repo.DB, repo_opts.hash, fork_moment, &verify_arena, &unpushed_id)) orelse return error.NotFound; │ │685 + try std.testing.expect(removed.removed); │ │686 + } │ │687 │ │688 // │ │689 // edit the patch metadata in the fork │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │735 try std.testing.expectEqual(.publish, published_fork.event.stage); │ │736 const draft_key = evt.Fork.draftKey(&repo_id, &user_id); │ │737 try std.testing.expectEqual(0, try indexedForkCount(admin_moment, evt.Fork.repo_user_to_draft_id_set_key, &draft_key));│ │700 - try std.testing.expectEqual(1, try indexedForkCount(admin_moment, evt.Fork.user_id_to_fork_id_set_key, &user_id)); │ │738 + try std.testing.expectEqual(2, try indexedForkCount(admin_moment, evt.Fork.user_id_to_fork_id_set_key, &user_id)); │ │739 var squash_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; │ │740 { │ │741 var draft = try Repo.open(io, allocator, .{ .path = draft_path }); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │165 │ │166 pub const IssuesView = enum { open, closed, tags, new, edit, description, new_comment, edit_comment, remove, conflicts, resolve }; │ │167 │ │168 - pub const PatchesView = enum { open, closed, merged, tags, new, drafts, edit, description, new_comment, edit_comment, remove, conflicts, resolve }; │ │168 + pub const PatchesView = enum { open, closed, merged, tags, new, drafts, edit, publish, description, new_comment, edit_comment, remove, conflicts, resolve };│ │169 │ │170 pub const DiscussionsView = enum { recent, tags, new, edit, description, new_comment, edit_comment, remove }; │ │171 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │570 .name = Array(repo_route_max_len).from(identity) orelse return null, │ │571 .view = .drafts, │ │572 } }; │ │573 + } │ │574 + │ │575 + pub fn repoPatchPublishRoute(identity: []const u8, selected: []const u8) ?RoutablePage { │ │576 + if (selected.len == 0) return null; │ │577 + var route = repoPatchesRoute(identity, .open, "", selected) orelse return null; │ │578 + switch (route) { │ │579 + .repo_patches => |*patch| patch.view = .publish, │ │580 + else => unreachable, │ │581 + } │ │582 + return route; │ │583 } │ │584 │ │585 pub fn repoDiscussionsRoute(identity: []const u8, tag: []const u8, selected: []const u8) ?RoutablePage {│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │883 .repo_patches => |p| blk: { │ │884 const prefix = try repoUrlPrefix(arena, p.name.slice()); │ │885 if (p.view == .edit) break :blk try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ patch_seg ++ "{s}/edit", .{ prefix, p.selected.slice() }); │ │886 + if (p.view == .publish) break :blk try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ patch_seg ++ "{s}/publish", .{ prefix, p.selected.slice() }); │ │887 if (p.view == .description) break :blk try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ patch_seg ++ "{s}/description", .{ prefix, p.selected.slice() });│ │888 if (p.view == .resolve) break :blk if (p.theirs.len == 0) │ │889 try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ patch_seg ++ "{s}/resolve", .{ prefix, p.selected.slice() }) │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1227 if (word) |tail| { │ │1228 if (std.mem.eql(u8, tail, "new")) return if (params.only(&.{})) repoThreadCommentNewRoute(.patch, pair, patch_id, "") else null; │ │1229 if (std.mem.eql(u8, tail, "edit")) return if (params.only(&.{})) repoThreadEditRoute(.patch, pair, patch_id) else null; │ │1230 + if (std.mem.eql(u8, tail, "publish")) return if (params.only(&.{})) repoPatchPublishRoute(pair, patch_id) else null; │ │1231 if (std.mem.eql(u8, tail, "remove")) return if (params.only(&.{})) repoThreadRemoveRoute(.patch, pair, patch_id, "") else null; │ │1232 if (std.mem.eql(u8, tail, "description")) return if (params.only(&.{})) repoThreadDescriptionRoute(.patch, pair, patch_id) else null;│ │1233 if (std.mem.eql(u8, tail, "resolve")) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Patches.zig│ ╰───────────────────────╯ ╭──────────────────────────────────────────────────────╮ │38 comments: Comment.Window = .empty, │ │39 attachments: []const Attachment.WithId = &.{},│ │40 draft: bool = false, │ │41 - revision_ready: bool = false, │ │41 fork_oid: []const u8 = "", │ │42 target_branch: []const u8 = "", │ │43 no_changes: bool = false, │ ╰──────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │150 return ui.RoutablePage.repoPatchesResolveRoute(identity, selected, picks); │ │151 } │ │152 │ │153 +pub fn publishRoute(identity: []const u8, selected: []const u8) ?ui.RoutablePage {│ │154 + return ui.RoutablePage.repoPatchPublishRoute(identity, selected); │ │155 +} │ │156 + │ │157 pub fn createDraft( │ │158 data: *const Self, │ │159 session: *ui.Session, │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │381 } │ │382 }; │ │383 empty.drafts = drafts_window; │ │381 - if (draft_selected and view != .edit and view != .remove) empty.view = .drafts; │ │384 + if (draft_selected and view != .edit and view != .publish and view != .remove) empty.view = .drafts;│ │385 │ │386 // an explicitly named published patch that doesn't exist is a bad url │ │387 // (NotFound -> 404); drafts, tags, and bare routes can use the empty fallback. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯