╭ changed ────╮ │src/patch.zig│ ╰─────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │26 ) !void { │ │27 var arena = std.heap.ArenaAllocator.init(allocator); │ │28 defer arena.deinit(); │ │29 + │ │30 + // write git events through the local event database │ │31 if (repo_kind == .git) { │ │32 + // reject edits based on an outdated patch │ │33 if (expected_patch) |expected| { │ │34 const current = (try evt.readFromRepo(evt.Patch, repo_kind, repo_opts, io, allocator, &arena, repo, &try evt.parseEventId(&id))) orelse return error.PatchOutOfDate;│ │35 if (current.removed or !evt.fieldEqual(evt.Patch, current.event, expected)) return error.PatchOutOfDate; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │36 } │ │37 + │ │38 + // capture the branch revision and consume its events │ │39 const events = try branchEvents(repo_kind, repo_opts, .{ .core = &repo.core, .extra = .{} }, io, &arena, id, patch, author);│ │40 try evt.consume(host_kind, .repo, repo_kind, repo_opts, io, allocator, repo, evt.events_ref, &events); │ │41 } else { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │42 const DB = evt.EventDB(repo_opts.hash); │ │43 + │ │44 + // continue from a remote event history when the local ref is absent│ │45 var first_parent: ?[1][hash.hexLen(repo_opts.hash)]u8 = null; │ │46 if (try repo.readRef(io, evt.events_ref) == null) { │ │47 var remotes = try repo.listRemotes(io, allocator); │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 53 } │ │ 54 } │ │ 55 } │ │ 49 - const Ctx = struct { │ │ 50 - repo: *rp.Repo(repo_kind, repo_opts), │ │ 51 - io: std.Io, │ │ 52 - arena: *std.heap.ArenaAllocator, │ │ 53 - id: [evt.event_id_size * 2]u8, │ │ 54 - patch: evt.Patch, │ │ 55 - expected_patch: ?evt.Patch, │ │ 56 - author: evt.CommitAuthor, │ │ 57 - first_parent: ?[1][hash.hexLen(repo_opts.hash)]u8, │ │ 56 │ │ 59 - pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { │ │ 60 - var moment = try DB.HashMap(.read_write).init(cursor.*); │ │ 61 - const state = rp.Repo(repo_kind, repo_opts).State(.read_write){ .core = &ctx.repo.core, .extra = .{ .moment = &moment } }; │ │ 62 - if (ctx.expected_patch) |expected| { │ │ 63 - const events_moment = try evt.currentMomentFromRepoMoment(repo_opts.hash, moment.readOnly()); │ │ 64 - const current = (try evt.Patch.readById(DB, repo_opts.hash, events_moment, ctx.arena, &try evt.parseEventId(&ctx.id))) orelse return error.PatchOutOfDate; │ │ 65 - if (current.removed or !evt.fieldEqual(evt.Patch, current.event, expected)) return error.PatchOutOfDate; │ │ 66 - } │ │ 67 - const events = try branchEvents(repo_kind, repo_opts, state, ctx.io, ctx.arena, ctx.id, ctx.patch, ctx.author); │ │ 68 - const parent = if (try rf.readRecur(repo_kind, repo_opts, state.readOnly(), ctx.io, .{ .ref = evt.events_ref }) == null) ctx.first_parent else null; │ │ 69 - try evt.commitEvents(repo_kind, repo_opts, state, ctx.io, ctx.arena.child_allocator, evt.events_ref, &events, parent); │ │ 70 - if (!try evt.consumeInTransaction(.repo, repo_kind, repo_opts, state, &ctx.repo.core.db, &moment, ctx.io, ctx.arena.child_allocator, evt.events_ref)) return error.CancelTransaction; │ │ 71 - try xit.undo.write(repo_opts, state, std.Io.Timestamp.now(ctx.io, .real).toSeconds(), .{ .custom = .{ .action = "update_patch" } }); │ │ 72 - } │ │ 73 - }; │ │ 57 + // commit the patch, mergeability, and undo record together │ │ 58 { │ │ 59 + const Ctx = struct { │ │ 60 + repo: *rp.Repo(repo_kind, repo_opts), │ │ 61 + io: std.Io, │ │ 62 + arena: *std.heap.ArenaAllocator, │ │ 63 + id: [evt.event_id_size * 2]u8, │ │ 64 + patch: evt.Patch, │ │ 65 + expected_patch: ?evt.Patch, │ │ 66 + author: evt.CommitAuthor, │ │ 67 + checks: *MergeCheckInputs(repo_opts), │ │ 68 + first_parent: ?[1][hash.hexLen(repo_opts.hash)]u8, │ │ 69 + │ │ 70 + pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { │ │ 71 + var moment = try DB.HashMap(.read_write).init(cursor.*); │ │ 72 + const state = rp.Repo(repo_kind, repo_opts).State(.read_write){ .core = &ctx.repo.core, .extra = .{ .moment = &moment } }; │ │ 73 + │ │ 74 + // recheck the expected patch under the repository lock │ │ 75 + if (ctx.expected_patch) |expected| { │ │ 76 + const events_moment = try evt.currentMomentFromRepoMoment(repo_opts.hash, moment.readOnly()); │ │ 77 + const current = (try evt.Patch.readById(DB, repo_opts.hash, events_moment, ctx.arena, &try evt.parseEventId(&ctx.id))) orelse return error.PatchOutOfDate; │ │ 78 + if (current.removed or !evt.fieldEqual(evt.Patch, current.event, expected)) return error.PatchOutOfDate; │ │ 79 + } │ │ 80 + │ │ 81 + // capture and consume the branch revision │ │ 82 + const events = try branchEvents(repo_kind, repo_opts, state, ctx.io, ctx.arena, ctx.id, ctx.patch, ctx.author); │ │ 83 + { │ │ 84 + const parent = if (try rf.readRecur(repo_kind, repo_opts, state.readOnly(), ctx.io, .{ .ref = evt.events_ref }) == null) ctx.first_parent else null; │ │ 85 + try evt.commitEvents(repo_kind, repo_opts, state, ctx.io, ctx.arena.child_allocator, evt.events_ref, &events, parent); │ │ 86 + if (!try evt.consumeInTransaction(.repo, repo_kind, repo_opts, state, &ctx.repo.core.db, &moment, ctx.io, ctx.arena.child_allocator, evt.events_ref)) return error.CancelTransaction; │ │ 87 + } │ │ 88 + │ │ 89 + // save mergeability before finishing the patch transaction │ │ 90 + _ = try ctx.checks.update(state, ctx.io, ctx.arena.child_allocator); │ │ 91 + │ │ 92 + // record the user action for undo │ │ 93 + try xit.undo.write(repo_opts, state, std.Io.Timestamp.now(ctx.io, .real).toSeconds(), .{ .custom = .{ .action = evt.undo_action } }); │ │ 94 + } │ │ 95 + }; │ │ 96 + │ │ 97 + // a branch patch has no fork, so this only names the patch to check │ │ 98 + var checks: MergeCheckInputs(repo_opts) = .{}; │ │ 99 + defer checks.deinit(io, allocator); │ │100 + if (host_kind == .server) try checks.add(io, allocator, repo, try evt.parseEventId(&id), patch); │ │101 + │ │102 try repo.core.db_file.lock(io, .exclusive); │ │103 defer repo.core.db_file.unlock(io); │ │104 + │ │105 const history = try DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); │ │ 78 - try history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ .repo = repo, .io = io, .arena = &arena, .id = id, .patch = patch, .expected_patch = expected_patch, .author = author, .first_parent = first_parent }); │ │106 + try history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ .repo = repo, .io = io, .arena = &arena, .id = id, .patch = patch, .expected_patch = expected_patch, .author = author, .checks = &checks, .first_parent = first_parent });│ │107 } │ │ 80 - if (host_kind == .server) refreshMergeability(repo_opts, io, allocator, repo, try evt.parseEventId(&id)); │ │108 } │ │109 } │ │110 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │118 patch: evt.Patch, │ │119 author: evt.CommitAuthor, │ │120 ) ![2]evt.EventWithId { │ │121 + // validate the patch fields and branch names │ │122 const branch = patch.source_branch orelse return error.InvalidSourceBranch; │ │123 if (!evt.Patch.fieldsValid(patch.title, patch.tags)) return error.InvalidFields;│ │124 if (!evt.Patch.branchValid(branch)) return error.InvalidSourceBranch; │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │125 if (!evt.Patch.branchValid(patch.target_branch)) return error.InvalidTargetBranch; │ │126 + │ │127 + // resolve both tips and their common ancestor │ │128 const source = (try rf.readRecur(repo_kind, repo_opts, state.readOnly(), io, .{ .ref = .{ .kind = .head, .name = branch } })) orelse return error.InvalidSourceBranch; │ │129 const target = (try rf.readRecur(repo_kind, repo_opts, state.readOnly(), io, .{ .ref = .{ .kind = .head, .name = patch.target_branch } })) orelse return error.InvalidTargetBranch;│ │130 if (std.mem.eql(u8, branch, patch.target_branch)) return error.SameBranch; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │132 error.NoCommonAncestor => return error.UnrelatedBranches, │ │133 else => return err, │ │134 }; │ │135 + │ │136 + // prepare the revision and attach it to the patch event │ │137 const timestamp: u64 = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()); │ │138 const prepared = try evt.PatchRev.prepare(repo_kind, repo_opts, state, io, arena, &base, &source, patch.title, author, timestamp);│ │139 var updated = patch; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │161 defer arena.deinit(); │ │162 var patches: std.AutoArrayHashMapUnmanaged([evt.event_id_size]u8, ?evt.Patch.Record) = .empty; │ │163 defer patches.deinit(allocator); │ │164 + │ │165 + // collect patches whose source or mergeability may have changed │ │166 collect: { │ │167 var local_db: ?evt.LocalEventDB(repo_opts.hash) = if (repo_kind == .git) try evt.LocalEventDB(repo_opts.hash).openReadOnly(io, allocator, repo.core.repo_dir) else null;│ │168 defer if (local_db) |*db| db.deinit(io, allocator); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │170 error.NotFound => break :collect, │ │171 else => return err, │ │172 }; │ │173 + │ │174 + // include all open server patches for mergeability checks │ │175 if (repo_kind == .xit and host_kind == .server) open: { │ │176 const statuses_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.status_to_id_set_key)) orelse break :open;│ │177 const statuses = try DB.SortedMap(.read_only).init(statuses_cursor); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │180 var iter = try open_patches.iteratorFromIndex(0); │ │181 while (try iter.next()) |cursor| try patches.put(allocator, try evt.readOrderKeyId(DB, cursor), null); │ │182 } │ │183 + │ │184 + // find tracked source branches and skip conflicting patches │ │185 const source_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.source_to_id_set_key)) orelse break :collect; │ │186 const sources = try DB.SortedMap(.read_only).init(source_cursor); │ │187 const conflicts = if (try moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.conflicts_key))) |cursor| try DB.SortedMap(.read_only).init(cursor) else null;│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯