╭ changed ────╮ │src/patch.zig│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │74 var moment = try DB.HashMap(.read_write).init(cursor.*); │ │75 const state = rp.Repo(repo_kind, repo_opts).State(.read_write){ .core = &ctx.repo.core, .extra = .{ .moment = &moment } }; │ │76 │ │77 - // recheck the expected patch under the repository lock │ │78 - if (ctx.expected_patch) |expected| { │ │79 - const events_moment = try evt.currentMomentFromRepoMoment(repo_opts.hash, moment.readOnly()); │ │80 - const current = (try evt.Patch.readById(DB, repo_opts.hash, events_moment, ctx.arena, &try evt.parseEventId(&ctx.id))) orelse return error.PatchOutOfDate; │ │81 - if (current.removed or !evt.fieldEqual(evt.Patch, current.event, expected)) return error.PatchOutOfDate; │ │82 - } │ │77 + try writeBranchPatchInTransaction(repo_opts, state, &moment, ctx.io, ctx.arena, ctx.id, ctx.patch, ctx.expected_patch, ctx.author, ctx.first_parent, ctx.checks); │ │78 │ │84 - // capture and consume the branch revision │ │85 - const events = try branchEvents(repo_kind, repo_opts, state, ctx.io, ctx.arena, ctx.id, ctx.patch, ctx.author); │ │86 - { │ │87 - const parent = if (try rf.readRecur(repo_kind, repo_opts, state.readOnly(), ctx.io, .{ .ref = evt.events_ref }) == null) ctx.first_parent else null; │ │88 - try evt.commitEvents(repo_kind, repo_opts, state, ctx.io, ctx.arena.child_allocator, evt.events_ref, &events, parent); │ │89 - 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;│ │90 - } │ │91 - │ │92 - // save mergeability before finishing the patch transaction │ │93 - _ = try ctx.checks.update(state, ctx.io, ctx.arena.child_allocator); │ │94 - │ │79 // record the user action for undo │ │80 try xit.undo.write(repo_opts, state, std.Io.Timestamp.now(ctx.io, .real).toSeconds(), .{ .custom = .{ .action = evt.undo_action } }); │ │81 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 84 // a branch patch has no fork, so this only names the patch to check │ │ 85 var checks: MergeCheckInputs(repo_opts) = .{}; │ │ 86 defer checks.deinit(io, allocator); │ │103 - if (host_kind == .server) try checks.add(io, allocator, repo, try evt.parseEventId(&id), patch); │ │ 87 + if (host_kind == .server) try checks.add(io, allocator, repo.core.work_path, try evt.parseEventId(&id), patch);│ │ 88 │ │ 89 try repo.core.db_file.lock(io, .exclusive); │ │ 90 defer repo.core.db_file.unlock(io); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 95 } │ │ 96 } │ │ 97 │ │ 98 +// write one branch patch's revision into a transaction the caller owns. the │ │ 99 +// undo record is the caller's to write, since the transaction may be theirs. │ │100 +// git repos never reach here: they write through the local event database │ │101 +pub fn writeBranchPatchInTransaction( │ │102 + comptime repo_opts: rp.RepoOpts(.xit), │ │103 + state: rp.Repo(.xit, repo_opts).State(.read_write), │ │104 + moment: *evt.EventDB(repo_opts.hash).HashMap(.read_write), │ │105 + io: std.Io, │ │106 + arena: *std.heap.ArenaAllocator, │ │107 + id: [evt.event_id_size * 2]u8, │ │108 + patch: evt.Patch, │ │109 + expected_patch: ?evt.Patch, │ │110 + author: evt.CommitAuthor, │ │111 + first_parent: ?[1][hash.hexLen(repo_opts.hash)]u8, │ │112 + checks: *MergeCheckInputs(repo_opts), │ │113 +) !void { │ │114 + const DB = evt.EventDB(repo_opts.hash); │ │115 + │ │116 + // recheck the expected patch under the repository lock │ │117 + if (expected_patch) |expected| { │ │118 + const events_moment = try evt.currentMomentFromRepoMoment(repo_opts.hash, moment.readOnly()); │ │119 + const current = (try evt.Patch.readById(DB, repo_opts.hash, events_moment, arena, &try evt.parseEventId(&id))) orelse return error.PatchOutOfDate; │ │120 + if (current.removed or !evt.fieldEqual(evt.Patch, current.event, expected)) return error.PatchOutOfDate; │ │121 + } │ │122 + │ │123 + // capture and consume the branch revision │ │124 + const events = try branchEvents(.xit, repo_opts, state, io, arena, id, patch, author); │ │125 + { │ │126 + const parent = if (try rf.readRecur(.xit, repo_opts, state.readOnly(), io, .{ .ref = evt.events_ref }) == null) first_parent else null; │ │127 + try evt.commitEvents(.xit, repo_opts, state, io, arena.child_allocator, evt.events_ref, &events, parent); │ │128 + if (!try evt.consumeInTransaction(.repo, .xit, repo_opts, state, &state.core.db, moment, io, arena.child_allocator, evt.events_ref)) return error.CancelTransaction;│ │129 + } │ │130 + │ │131 + // save mergeability before finishing the patch transaction │ │132 + _ = try checks.update(state, io, arena.child_allocator); │ │133 +} │ │134 + │ │135 fn branchEvents( │ │136 comptime repo_kind: rp.RepoKind, │ │137 comptime repo_opts: rp.RepoOpts(repo_kind), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │167 .{ .id = id, .author = author, .timestamp = timestamp, .event = .{ .patch = updated } }, │ │168 }; │ │169 } │ │170 + │ │171 +// the open patches whose source branch has moved past the revision they │ │172 +// stored, mapped to the record the refresh rewrites. `updates` limits the │ │173 +// search to the branches one push changed │ │174 +fn collectOutdatedBranchPatches( │ │175 + comptime repo_kind: rp.RepoKind, │ │176 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │177 + io: std.Io, │ │178 + allocator: std.mem.Allocator, │ │179 + arena: *std.heap.ArenaAllocator, │ │180 + state: rp.Repo(repo_kind, repo_opts).State(.read_only), │ │181 + moment: evt.EventDB(repo_opts.hash).HashMap(.read_only), │ │182 + updates: ?[]const xit.net_server_receive_pack.AppliedRefUpdate, │ │183 + host_kind: evt.HostKind, │ │184 + patches: *std.AutoArrayHashMapUnmanaged([evt.event_id_size]u8, evt.Patch.Record), │ │185 +) !void { │ │186 + const DB = evt.EventDB(repo_opts.hash); │ │187 + // find tracked source branches and skip conflicting patches │ │188 + const source_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.source_to_id_set_key)) orelse return; │ │189 + const sources = try DB.SortedMap(.read_only).init(source_cursor); │ │190 + 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; │ │191 + var iter = try sources.iterator(); │ │192 + while (try iter.next()) |cursor| { │ │193 + const pair = try cursor.readKeyValuePair(); │ │194 + const branch = try pair.key_cursor.readBytesAlloc(arena.allocator(), null); │ │195 + │ │196 + // limit source refreshes to branches changed by this push │ │197 + if (updates) |changed| { │ │198 + var found = false; │ │199 + for (changed) |update| { │ │200 + if (std.mem.startsWith(u8, update.ref_name, "refs/heads/") and std.mem.eql(u8, update.ref_name["refs/heads/".len..], branch)) { │ │201 + found = true; │ │202 + break; │ │203 + } │ │204 + } │ │205 + if (!found) continue; │ │206 + } │ │207 │ │208 + // read the source tip before comparing stored revisions │ │209 + const source = (try rf.readRecur(repo_kind, repo_opts, state, io, .{ .ref = .{ .kind = .head, .name = branch } })) orelse { │ │210 + if (host_kind == .server) std.log.warn("patch source branch missing: {s}", .{branch}); │ │211 + continue; │ │212 + }; │ │213 + │ │214 + // collect open patches whose stored source tip is outdated │ │215 + const ids = try DB.CountedHashSet(.read_only).init(pair.value_cursor); │ │216 + var ids_iter = try ids.iterator(); │ │217 + while (try ids_iter.next()) |id_cursor| { │ │218 + const id_pair = try id_cursor.readKeyValuePair(); │ │219 + var id: [evt.event_id_size]u8 = undefined; │ │220 + if ((try id_pair.key_cursor.readBytes(&id)).len != id.len) return error.InvalidPatch; │ │221 + const record = (try evt.Patch.readById(DB, repo_opts.hash, moment, arena, &id)) orelse continue; │ │222 + if (record.removed or record.event.status.kind() != .open) continue; │ │223 + if (conflicts) |map| if (try map.getCursor(&evt.orderKeyDesc(record.created_order, &id)) != null) continue; │ │224 + if (record.event.revision) |revision| if (std.mem.eql(u8, revision.source_oid, &source)) continue; │ │225 + try patches.put(allocator, id, record); │ │226 + } │ │227 + } │ │228 +} │ │229 + │ │230 +// refresh the patches their source branch has outrun, inside the caller's │ │231 +// transaction, so a push and the revisions it causes land together. fork │ │232 +// backed patches are left to `refreshBranches`: their mergeability needs a │ │233 +// fork's lock, which has to be taken before this transaction opened │ │234 +pub fn refreshBranchesInTransaction( │ │235 + comptime repo_opts: rp.RepoOpts(.xit), │ │236 + state: rp.Repo(.xit, repo_opts).State(.read_write), │ │237 + moment: *evt.EventDB(repo_opts.hash).HashMap(.read_write), │ │238 + io: std.Io, │ │239 + allocator: std.mem.Allocator, │ │240 + updates: ?[]const xit.net_server_receive_pack.AppliedRefUpdate, │ │241 +) !void { │ │242 + var arena = std.heap.ArenaAllocator.init(allocator); │ │243 + defer arena.deinit(); │ │244 + var patches: std.AutoArrayHashMapUnmanaged([evt.event_id_size]u8, evt.Patch.Record) = .empty; │ │245 + defer patches.deinit(allocator); │ │246 + │ │247 + const events_moment = evt.currentMomentFromRepoMoment(repo_opts.hash, moment.readOnly()) catch |err| switch (err) { │ │248 + error.NotFound => return, │ │249 + else => return err, │ │250 + }; │ │251 + try collectOutdatedBranchPatches(.xit, repo_opts, io, allocator, &arena, state.readOnly(), events_moment, updates, .server, &patches); │ │252 + │ │253 + for (patches.keys(), patches.values()) |id, record| { │ │254 + // a branch patch opens no fork, so its checks need no lock of their own │ │255 + var checks: MergeCheckInputs(repo_opts) = .{}; │ │256 + defer checks.deinit(io, allocator); │ │257 + try checks.add(io, allocator, state.core.work_path, id, record.event); │ │258 + │ │259 + const author = evt.CommitAuthor{ .name = "haxy", .email = record.author_email orelse "user@haxy" }; │ │260 + writeBranchPatchInTransaction(repo_opts, state, moment, io, &arena, std.fmt.bytesToHex(id, .lower), record.event, record.event, author, null, &checks) catch |err| {│ │261 + std.log.warn("failed to refresh branch patch: {s}", .{@errorName(err)}); │ │262 + }; │ │263 + } │ │264 +} │ │265 + │ │266 // refresh open patches' tracked branches and, on the server, their mergeability. │ │267 // this runs outside the caller's transaction. │ │268 pub fn refreshBranches( │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮ │280 defer arena.deinit(); │ │281 var patches: std.AutoArrayHashMapUnmanaged([evt.event_id_size]u8, ?evt.Patch.Record) = .empty;│ │282 defer patches.deinit(allocator); │ │283 + var outdated: std.AutoArrayHashMapUnmanaged([evt.event_id_size]u8, evt.Patch.Record) = .empty;│ │284 + defer outdated.deinit(allocator); │ │285 │ │286 // collect patches whose source or mergeability may have changed │ │287 collect: { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │301 var iter = try open_patches.iteratorFromIndex(0); │ │302 while (try iter.next()) |cursor| try patches.put(allocator, try evt.readOrderKeyId(DB, cursor), null); │ │303 } │ │186 - │ │187 - // find tracked source branches and skip conflicting patches │ │188 - const source_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.source_to_id_set_key)) orelse break :collect; │ │189 - const sources = try DB.SortedMap(.read_only).init(source_cursor); │ │190 - 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; │ │191 - var iter = try sources.iterator(); │ │192 - while (try iter.next()) |cursor| { │ │193 - const pair = try cursor.readKeyValuePair(); │ │194 - const branch = try pair.key_cursor.readBytesAlloc(arena.allocator(), null); │ │195 - │ │196 - // limit source refreshes to branches changed by this push │ │197 - if (updates) |changed| { │ │198 - var found = false; │ │199 - for (changed) |update| { │ │200 - if (std.mem.startsWith(u8, update.ref_name, "refs/heads/") and std.mem.eql(u8, update.ref_name["refs/heads/".len..], branch)) { │ │201 - found = true; │ │202 - break; │ │203 - } │ │204 - } │ │205 - if (!found) continue; │ │206 - } │ │304 │ │208 - // read the source tip before comparing stored revisions │ │209 - const source = (try repo.readRef(io, .{ .kind = .head, .name = branch })) orelse { │ │210 - if (host_kind == .server) std.log.warn("patch source branch missing: {s}", .{branch}); │ │211 - continue; │ │212 - }; │ │213 - │ │214 - // collect open patches whose stored source tip is outdated │ │215 - const ids = try DB.CountedHashSet(.read_only).init(pair.value_cursor); │ │216 - var ids_iter = try ids.iterator(); │ │217 - while (try ids_iter.next()) |id_cursor| { │ │218 - const id_pair = try id_cursor.readKeyValuePair(); │ │219 - var id: [evt.event_id_size]u8 = undefined; │ │220 - if ((try id_pair.key_cursor.readBytes(&id)).len != id.len) return error.InvalidPatch; │ │221 - const record = (try evt.Patch.readById(DB, repo_opts.hash, moment, &arena, &id)) orelse continue; │ │222 - if (record.removed or record.event.status.kind() != .open) continue; │ │223 - if (conflicts) |map| if (try map.getCursor(&evt.orderKeyDesc(record.created_order, &id)) != null) continue; │ │224 - if (record.event.revision) |revision| if (std.mem.eql(u8, revision.source_oid, &source)) continue; │ │225 - try patches.put(allocator, id, record); │ │226 - } │ │305 + switch (repo_kind) { │ │306 + .git => try collectOutdatedBranchPatches(repo_kind, repo_opts, io, allocator, &arena, .{ .core = &repo.core, .extra = .{} }, moment, updates, host_kind, &outdated), │ │307 + .xit => { │ │308 + var repo_moment = try repo.core.latestMoment(); │ │309 + try collectOutdatedBranchPatches(repo_kind, repo_opts, io, allocator, &arena, .{ .core = &repo.core, .extra = .{ .moment = &repo_moment } }, moment, updates, host_kind, &outdated);│ │310 + }, │ │311 } │ │312 + for (outdated.keys(), outdated.values()) |id, record| try patches.put(allocator, id, record); │ │313 } │ │314 │ │315 // start progress reporting after the patch count is known │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │493 // acquire fork locks before the target repository lock │ │494 var checks: MergeCheckInputs(repo_opts) = .{}; │ │495 defer checks.deinit(io, allocator); │ │411 - try checks.add(io, allocator, target_repo, id.*, initial.event); │ │496 + try checks.add(io, allocator, target_repo.core.work_path, id.*, initial.event);│ │497 │ │498 // cancel the cache transaction when the result is unchanged │ │499 { │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │538 for (events) |event| { │ │539 if (event.event != .patch) continue; │ │540 const patch = event.event.patch orelse continue; │ │456 - try self.add(io, allocator, repo, try evt.parseEventId(&event.id), patch); │ │541 + try self.add(io, allocator, repo.core.work_path, try evt.parseEventId(&event.id), patch);│ │542 } │ │543 } │ │544 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │460 - fn add(self: *Self, io: std.Io, allocator: std.mem.Allocator, repo: *Repo, id: [evt.event_id_size]u8, patch: evt.Patch) !void { │ │545 + fn add(self: *Self, io: std.Io, allocator: std.mem.Allocator, work_path: []const u8, id: [evt.event_id_size]u8, patch: evt.Patch) !void {│ │546 // track each open patch once, including branch-backed patches │ │547 if (patch.status.kind() != .open) return; │ │548 const entry = try self.forks.getOrPut(allocator, id); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │551 if (patch.source_branch != null) return; │ │552 │ │553 // open the fork before acquiring its shared lock │ │469 - const path = try fork.forkPath(allocator, std.fs.path.dirname(repo.core.work_path) orelse ".", &std.fmt.bytesToHex(id, .lower));│ │554 + const path = try fork.forkPath(allocator, std.fs.path.dirname(work_path) orelse ".", &std.fmt.bytesToHex(id, .lower)); │ │555 defer allocator.free(path); │ │556 var opened = Repo.open(io, allocator, .{ .path = path, .require_repo_root = true }) catch |err| { │ │557 std.log.warn("mergeability fork unavailable: {s}", .{@errorName(err)}); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯