╭ changed ────╮ │src/patch.zig│ ╰─────────────╯ ╭──────────────────────────────────────────────────────╮ │17 timestamp: u64, │ │18 }; │ │19 │ │20 +pub const EditDraftInput = struct { │ │21 + id: [evt.event_id_size * 2]u8, │ │22 + user_id: [evt.event_id_size]u8, │ │23 + repo_id: [evt.event_id_size]u8, │ │24 + title: []const u8, │ │25 + tags: []const u8, │ │26 + description: []const u8, │ │27 + author: evt.CommitAuthor, │ │28 + timestamp: u64, │ │29 +}; │ │30 + │ │31 pub const MergeRevision = enum { squash, source };│ │32 │ │33 pub const MergeInput = struct { │ ╰──────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────╮ │43 allocator: std.mem.Allocator, │ │44 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), │ │45 target_repo: *rp.Repo(.xit, repo_opts), │ │35 - path: []const u8, │ │46 + fork_path: []const u8, │ │47 input: PublishInput, │ │48 ) !void { │ │49 const patch_id = try evt.parseEventId(&input.id);│ ╰─────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │60 !std.mem.eql(u8, user.event.name, input.author.name) or │ │61 !std.mem.eql(u8, user.event.email, input.author.email)) return error.InvalidPatchDraft; │ │62 │ │52 - var fork_repo = rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = path }) catch return error.PatchDataUnavailable; │ │63 + var fork_repo = rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = fork_path }) catch return error.PatchDataUnavailable; │ │64 defer fork_repo.deinit(io, allocator); │ │65 const fork_moment = evt.currentMoment(repo_opts, &fork_repo) catch return error.PatchDataUnavailable; │ │66 const local_patch = (try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena, &patch_id)) orelse return error.PatchDataUnavailable;│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │112 .event = .{ .fork = published }, │ │113 }}); │ │114 } │ │115 +} │ │116 + │ │117 +pub fn editDraft( │ │118 + comptime repo_opts: rp.RepoOpts(.xit), │ │119 + io: std.Io, │ │120 + allocator: std.mem.Allocator, │ │121 + admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), │ │122 + fork_path: []const u8, │ │123 + input: EditDraftInput, │ │124 +) !bool { │ │125 + if (!evt.Patch.fieldsValid(input.title, input.tags)) return error.InvalidFields; │ │126 + const patch_id = try evt.parseEventId(&input.id); │ │127 + var arena = std.heap.ArenaAllocator.init(allocator); │ │128 + defer arena.deinit(); │ │129 + │ │130 + const admin_moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); │ │131 + const fork_record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &arena, &patch_id)) orelse return false; │ │132 + if (fork_record.event.stage != .draft) return false; │ │133 + if (fork_record.removed or │ │134 + !std.mem.eql(u8, fork_record.event.user_id, &input.user_id) or │ │135 + !std.mem.eql(u8, fork_record.event.repo_id, &input.repo_id)) return error.InvalidPatchDraft; │ │136 + const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &arena, &input.user_id)) orelse return error.InvalidPatchDraft; │ │137 + if (user.removed or │ │138 + !std.mem.eql(u8, user.event.name, input.author.name) or │ │139 + !std.mem.eql(u8, user.event.email, input.author.email)) return error.InvalidPatchDraft; │ │140 + │ │141 + var fork_repo = rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }) catch return error.PatchDataUnavailable; │ │142 + defer fork_repo.deinit(io, allocator); │ │143 + const fork_moment = evt.currentMoment(repo_opts, &fork_repo) catch return error.PatchDataUnavailable; │ │144 + const record = (try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena, &patch_id)) orelse return error.PatchDataUnavailable;│ │145 + if (record.removed) return error.PatchDataUnavailable; │ │146 + │ │147 + var patch = record.event; │ │148 + patch.title = input.title; │ │149 + patch.tags = input.tags; │ │150 + patch.description = input.description; │ │151 + try evt.consume(.fork, .xit, repo_opts, io, allocator, &fork_repo, evt.events_ref, &.{.{ │ │152 + .id = input.id, │ │153 + .timestamp = input.timestamp, │ │154 + .author = input.author, │ │155 + .event = .{ .patch = patch }, │ │156 + }}); │ │157 + return true; │ │158 } │ │159 │ │160 // merge the selected patch revision into its target branch │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ 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 + // edit the patch metadata in the fork │ │652 + // │ │653 + │ │654 + try std.testing.expect(try pch.editDraft(repo_opts, io, allocator, &admin, draft_path, .{ │ │655 + .id = patch_id_hex, │ │656 + .user_id = user_id, │ │657 + .repo_id = repo_id, │ │658 + .title = "explain the answer", │ │659 + .tags = "enhancement documentation", │ │660 + .description = "adds and explains a reusable answer constant", │ │661 + .author = author, │ │662 + .timestamp = 4, │ │663 + })); │ │664 │ │665 // │ │666 // publish the metadata and revision pointer │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │671 .user_id = user_id, │ │672 .repo_id = repo_id, │ │673 .author = author, │ │659 - .timestamp = 3, │ │674 + .timestamp = 5, │ │675 }); │ │676 try pch.publish(repo_opts, io, allocator, &admin, &target, draft_path, .{│ │677 .id = patch_id_hex, │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────╮ │678 .user_id = user_id, │ │679 .repo_id = repo_id, │ │680 .author = author, │ │666 - .timestamp = 4, │ │681 + .timestamp = 6, │ │682 }); │ │683 │ │684 var arena = std.heap.ArenaAllocator.init(allocator);│ ╰─────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │685 defer arena.deinit(); │ │686 const moment = try evt.currentMoment(repo_opts, &target); │ │687 const patch = (try evt.Patch.readById(Repo.DB, repo_opts.hash, moment, &arena, &patch_id)) orelse return error.NotFound; │ │688 + try std.testing.expectEqualStrings("explain the answer", patch.event.title); │ │689 try std.testing.expectEqualStrings(&first_patchrev_hex, &(patch.event.revision orelse return error.NotFound).id); │ │690 try std.testing.expectEqual(.open, patch.event.status); │ │691 try std.testing.expectEqual(null, try evt.PatchRev.readById(Repo.DB, repo_opts.hash, moment, &arena, &first_patchrev_id));│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │1465 self.data.user_name = user.event.name; │ │1466 self.data.enable_ansi = user.event.enable_ansi; │ │1467 } │ │1468 + } │ │1469 + │ │1470 + pub fn userId(self: *const Self) ?[evt.event_id_size]u8 { │ │1471 + const bytes = self.data.user_id orelse return null; │ │1472 + if (bytes.len != evt.event_id_size) return null; │ │1473 + var id: [evt.event_id_size]u8 = undefined; │ │1474 + @memcpy(&id, bytes); │ │1475 + return id; │ │1476 } │ │1477 │ │1478 // the commit author for an event this session creates, or null when it may│ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Patches.zig│ ╰───────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │159 const io = session.io orelse return error.NotFound; │ │160 const repos_dir = session.repos_dir orelse return error.NotFound; │ │161 const admin_repo = session.admin_repo orelse return error.NotFound; │ │162 - const user_id_slice = session.data.user_id orelse return error.NotFound;│ │163 - if (user_id_slice.len != evt.event_id_size) return error.NotFound; │ │162 + const user_id = session.userId() orelse return error.NotFound; │ │163 const repo_id = data.repo_id orelse return error.NotFound; │ │164 const author = (try session.eventAuthor()) orelse return error.NotFound;│ │165 var id_bytes: [evt.event_id_size]u8 = undefined; │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯