╭ changed ────╮ │src/patch.zig│ ╰─────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 30 }; │ │ 31 │ │ 32 pub const MergeRevision = enum { squash, source }; │ │ 33 + │ │ 34 +pub const Mergeability = struct { │ │ 35 + pub const Status = enum { clean, conflict, unknown }; │ │ 36 + │ │ 37 + source: Status = .unknown, │ │ 38 + squash: Status = .unknown, │ │ 39 + │ │ 40 + pub fn get(self: Mergeability, revision: MergeRevision) Status { │ │ 41 + return switch (revision) { │ │ 42 + .source => self.source, │ │ 43 + .squash => self.squash, │ │ 44 + }; │ │ 45 + } │ │ 46 + │ │ 47 + pub fn status(self: Mergeability) Status { │ │ 48 + if (self.source == .clean or self.squash == .clean) return .clean; │ │ 49 + if (self.source == .conflict and self.squash == .conflict) return .conflict; │ │ 50 + return .unknown; │ │ 51 + } │ │ 52 +}; │ │ 53 + │ │ 54 +const mergeability_key = "patch-id->mergeability"; │ │ 55 + │ │ 56 +const MergeCheck = struct { │ │ 57 + revision: evt.Patch.Revision, │ │ 58 + target_branch: []const u8, │ │ 59 + target_oid: []const u8, │ │ 60 + result: Mergeability = .{}, │ │ 61 + │ │ 62 + fn matches(self: MergeCheck, patch: evt.Patch, target_oid: []const u8) bool { │ │ 63 + return evt.fieldEqual(?evt.Patch.Revision, self.revision, patch.revision) and │ │ 64 + std.mem.eql(u8, self.target_branch, patch.target_branch) and │ │ 65 + std.mem.eql(u8, self.target_oid, target_oid); │ │ 66 + } │ │ 67 +}; │ │ 68 + │ │ 69 +fn readMergeCheck( │ │ 70 + comptime repo_opts: rp.RepoOpts(.xit), │ │ 71 + moment: evt.EventDB(repo_opts.hash).HashMap(.read_only), │ │ 72 + arena: *std.heap.ArenaAllocator, │ │ 73 + id: *const [evt.event_id_size]u8, │ │ 74 +) !?MergeCheck { │ │ 75 + const DB = evt.EventDB(repo_opts.hash); │ │ 76 + const map_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, mergeability_key)) orelse return null; │ │ 77 + const map = try DB.HashMap(.read_only).init(map_cursor); │ │ 78 + const cursor = try map.getCursor(hash.hashInt(repo_opts.hash, id)) orelse return null; │ │ 79 + return evt.read(MergeCheck, DB, repo_opts.hash, arena, try DB.HashMap(.read_only).init(cursor)) catch |err| switch (err) { │ │ 80 + error.InvalidEnumTag => return null, │ │ 81 + else => return err, │ │ 82 + }; │ │ 83 +} │ │ 84 + │ │ 85 +// reads never run a merge or refresh the cache │ │ 86 +pub fn readMergeability( │ │ 87 + comptime repo_opts: rp.RepoOpts(.xit), │ │ 88 + state: rp.Repo(.xit, repo_opts).State(.read_only), │ │ 89 + io: std.Io, │ │ 90 + arena: *std.heap.ArenaAllocator, │ │ 91 + id: *const [evt.event_id_size]u8, │ │ 92 + patch: evt.Patch, │ │ 93 +) !Mergeability { │ │ 94 + const target_oid = try rf.readRecur(.xit, repo_opts, state, io, .{ .ref = .{ .kind = .head, .name = patch.target_branch } }) orelse return .{}; │ │ 95 + const cached = try readMergeCheck(repo_opts, state.extra.moment.*, arena, id) orelse return .{}; │ │ 96 + return if (cached.matches(patch, &target_oid)) cached.result else .{}; │ │ 97 +} │ │ 98 + │ │ 99 +// refresh after the caller's transaction and locks have finished. a check │ │100 +// failure must not turn an accepted push or event into a failed operation. │ │101 +pub fn refreshMergeability( │ │102 + comptime repo_opts: rp.RepoOpts(.xit), │ │103 + io: std.Io, │ │104 + allocator: std.mem.Allocator, │ │105 + target_repo: *rp.Repo(.xit, repo_opts), │ │106 + id_maybe: ?[evt.event_id_size]u8, │ │107 +) void { │ │108 + refreshMergeChecks(repo_opts, io, allocator, target_repo, id_maybe) catch |err| { │ │109 + std.log.warn("failed to refresh mergeability: {s}", .{@errorName(err)}); │ │110 + }; │ │111 +} │ │112 + │ │113 +fn refreshMergeChecks( │ │114 + comptime repo_opts: rp.RepoOpts(.xit), │ │115 + io: std.Io, │ │116 + allocator: std.mem.Allocator, │ │117 + target_repo: *rp.Repo(.xit, repo_opts), │ │118 + id_maybe: ?[evt.event_id_size]u8, │ │119 +) !void { │ │120 + if (id_maybe) |id| return refreshMergeCheck(repo_opts, io, allocator, target_repo, &id); │ │121 + const DB = evt.EventDB(repo_opts.hash); │ │122 + const moment = evt.currentMoment(repo_opts, target_repo) catch |err| switch (err) { │ │123 + error.NotFound => return, │ │124 + else => return err, │ │125 + }; │ │126 + const statuses_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.status_to_id_set_key)) orelse return; │ │127 + const statuses = try DB.SortedMap(.read_only).init(statuses_cursor); │ │128 + const open_cursor = try statuses.getCursor("open") orelse return; │ │129 + const open = try DB.SortedSet(.read_only).init(open_cursor); │ │130 + var ids: std.ArrayList([evt.event_id_size]u8) = .empty; │ │131 + defer ids.deinit(allocator); │ │132 + var iter = try open.iteratorFromIndex(0); │ │133 + while (try iter.next()) |cursor| try ids.append(allocator, try evt.readOrderKeyId(DB, cursor)); │ │134 + for (ids.items) |id| refreshMergeability(repo_opts, io, allocator, target_repo, id); │ │135 +} │ │136 + │ │137 +fn refreshMergeCheck( │ │138 + comptime repo_opts: rp.RepoOpts(.xit), │ │139 + io: std.Io, │ │140 + allocator: std.mem.Allocator, │ │141 + target_repo: *rp.Repo(.xit, repo_opts), │ │142 + id: *const [evt.event_id_size]u8, │ │143 +) !void { │ │144 + const Repo = rp.Repo(.xit, repo_opts); │ │145 + const DB = Repo.DB; │ │146 + var arena = std.heap.ArenaAllocator.init(allocator); │ │147 + defer arena.deinit(); │ │148 + const repo_root_path = std.fs.path.dirname(target_repo.core.work_path) orelse "."; │ │149 + const path = try fork.forkPath(arena.allocator(), repo_root_path, &std.fmt.bytesToHex(id.*, .lower)); │ │150 + var fork_repo_maybe: ?Repo = Repo.open(io, allocator, .{ .path = path, .require_repo_root = true }) catch |err| blk: { │ │151 + std.log.warn("mergeability fork unavailable: {s}", .{@errorName(err)}); │ │152 + break :blk null; │ │153 + }; │ │154 + defer if (fork_repo_maybe) |*repo| repo.deinit(io, allocator); │ │155 + if (fork_repo_maybe) |*repo| try repo.core.db_file.lock(io, .shared); │ │156 + defer if (fork_repo_maybe) |*repo| repo.core.db_file.unlock(io); │ │157 + try target_repo.core.db_file.lock(io, .exclusive); │ │158 + defer target_repo.core.db_file.unlock(io); │ │159 + │ │160 + var target_moment = try target_repo.core.latestMoment(); │ │161 + const target_state = Repo.State(.read_only){ .core = &target_repo.core, .extra = .{ .moment = &target_moment } }; │ │162 + const target_events = evt.currentMomentFromRepoMoment(repo_opts.hash, target_moment) catch |err| switch (err) { │ │163 + error.NotFound => null, │ │164 + else => return err, │ │165 + }; │ │166 + var record = if (target_events) |events| try evt.Patch.readById(DB, repo_opts.hash, events, &arena, id) else null; │ │167 + var newest: ?evt.PatchRev.WithId = null; │ │168 + if (fork_repo_maybe) |*repo| { │ │169 + if (evt.currentMoment(repo_opts, repo)) |events| { │ │170 + newest = evt.PatchRev.readNewest(DB, repo_opts.hash, events, &arena) catch |err| blk: { │ │171 + std.log.warn("mergeability revision unavailable: {s}", .{@errorName(err)}); │ │172 + break :blk null; │ │173 + }; │ │174 + if (record == null) record = try evt.Patch.readById(DB, repo_opts.hash, events, &arena, id); │ │175 + } else |err| std.log.warn("mergeability revision unavailable: {s}", .{@errorName(err)}); │ │176 + } │ │177 + const patch_record = record orelse return; │ │178 + if (patch_record.removed or patch_record.event.status.kind() != .open) return; │ │179 + const patch = patch_record.event; │ │180 + const revision = patch.revision orelse return; │ │181 + const target_oid = try rf.readRecur(.xit, repo_opts, target_state, io, .{ .ref = .{ .kind = .head, .name = patch.target_branch } }); │ │182 + var check = MergeCheck{ .revision = revision, .target_branch = patch.target_branch, .target_oid = if (target_oid) |*oid| oid else "" }; │ │183 + const cached = try readMergeCheck(repo_opts, target_moment, &arena, id); │ │184 + const current = if (newest) |latest| std.mem.eql(u8, &std.fmt.bytesToHex(latest.id, .lower), &revision.id) and revision.matches(latest.record) else false;│ │185 + if (current and target_oid != null) { │ │186 + if (cached) |value| if (value.matches(patch, check.target_oid) and value.result.source != .unknown and value.result.squash != .unknown) return; │ │187 + const Ctx = struct { │ │188 + repo: *Repo, │ │189 + fork_repo: *Repo, │ │190 + io: std.Io, │ │191 + allocator: std.mem.Allocator, │ │192 + check: *MergeCheck, │ │193 + │ │194 + pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { │ │195 + var moment = try DB.HashMap(.read_write).init(cursor.*); │ │196 + const state = Repo.State(.read_write){ .core = &ctx.repo.core, .extra = .{ .moment = &moment } }; │ │197 + var fork_moment = try ctx.fork_repo.core.latestMoment(); │ │198 + const fork_state = Repo.State(.read_only){ .core = &ctx.fork_repo.core, .extra = .{ .moment = &fork_moment } }; │ │199 + var objects = try obj.ObjectIterator(.xit, repo_opts).init(fork_state, ctx.io, ctx.allocator, .{ .kind = .all }); │ │200 + defer objects.deinit(); │ │201 + const source = try oidArray(repo_opts.hash, ctx.check.revision.source_oid); │ │202 + const squash = try oidArray(repo_opts.hash, ctx.check.revision.squash_oid); │ │203 + try objects.include(&source); │ │204 + try objects.include(&squash); │ │205 + try obj.copyFromObjectIterator(.xit, repo_opts, state, .xit, repo_opts, &objects, ctx.io, null); │ │206 + const target = rf.Ref{ .kind = .head, .name = ctx.check.target_branch }; │ │207 + ctx.check.result.source = checkCommit(repo_opts, state, ctx.io, ctx.allocator, &source, target); │ │208 + ctx.check.result.squash = checkCommit(repo_opts, state, ctx.io, ctx.allocator, &squash, target); │ │209 + return error.CancelTransaction; │ │210 + } │ │211 + }; │ │212 + const history = try DB.ArrayList(.read_write).init(target_repo.core.db.rootCursor()); │ │213 + history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ │ │214 + .repo = target_repo, │ │215 + .fork_repo = &fork_repo_maybe.?, │ │216 + .io = io, │ │217 + .allocator = allocator, │ │218 + .check = &check, │ │219 + }) catch |err| switch (err) { │ │220 + error.CancelTransaction => {}, │ │221 + else => std.log.warn("mergeability check failed: {s}", .{@errorName(err)}), │ │222 + }; │ │223 + } │ │224 + if (cached) |value| if (evt.fieldEqual(MergeCheck, value, check)) return; │ │225 + const Save = struct { │ │226 + id: [evt.event_id_size]u8, │ │227 + check: MergeCheck, │ │228 + │ │229 + pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { │ │230 + const moment = try DB.HashMap(.read_write).init(cursor.*); │ │231 + const map = try DB.HashMap(.read_write).init(try moment.putCursor(hash.hashInt(repo_opts.hash, mergeability_key))); │ │232 + const entry = try DB.HashMap(.read_write).init(try map.putCursor(hash.hashInt(repo_opts.hash, &ctx.id))); │ │233 + try evt.upsert(MergeCheck, DB, repo_opts.hash, entry, ctx.check); │ │234 + } │ │235 + }; │ │236 + const history = try DB.ArrayList(.read_write).init(target_repo.core.db.rootCursor()); │ │237 + try history.appendContext(.{ .slot = try history.getSlot(-1) }, Save{ .id = id.*, .check = check }); │ │238 +} │ │239 + │ │240 +fn oidArray(comptime hash_kind: hash.HashKind, oid: []const u8) ![hash.hexLen(hash_kind)]u8 { │ │241 + try evt.PatchRev.validateOid(hash_kind, oid); │ │242 + var result: [hash.hexLen(hash_kind)]u8 = undefined; │ │243 + @memcpy(&result, oid); │ │244 + return result; │ │245 +} │ │246 + │ │247 +fn checkCommit( │ │248 + comptime repo_opts: rp.RepoOpts(.xit), │ │249 + state: rp.Repo(.xit, repo_opts).State(.read_write), │ │250 + io: std.Io, │ │251 + allocator: std.mem.Allocator, │ │252 + oid: *const [hash.hexLen(repo_opts.hash)]u8, │ │253 + target: rf.Ref, │ │254 +) Mergeability.Status { │ │255 + var result = mrg.Merge(.xit, repo_opts).init(state, io, allocator, .{ │ │256 + .kind = .full, │ │257 + .action = .{ .new = .{ .source = &.{.{ .oid = oid }}, .algo = .patch } }, │ │258 + .dry_run = true, │ │259 + }, target, null) catch |err| { │ │260 + std.log.warn("mergeability check failed: {s}", .{@errorName(err)}); │ │261 + return .unknown; │ │262 + }; │ │263 + defer result.deinit(); │ │264 + return switch (result.result) { │ │265 + .conflict => .conflict, │ │266 + .clean, .success, .nothing, .fast_forward => .clean, │ │267 + }; │ │268 +} │ │269 │ │270 pub const MergeInput = struct { │ │271 id: [evt.event_id_size * 2]u8, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │350 .event = .{ .fork = published }, │ │351 }}); │ │352 } │ │353 + refreshMergeability(repo_opts, io, allocator, target_repo, patch_id);│ │354 } │ │355 │ │356 pub fn editDraft( │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │440 } │ │441 │ │442 // select the commit and merge identity │ │206 - const merge_oid = blk: { │ │207 - const selected_oid = switch (input.revision) { │ │208 - .squash => selected.squash_oid, │ │209 - .source => selected.source_oid, │ │210 - }; │ │211 - var oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; │ │212 - if (selected_oid.len != oid.len) return error.InvalidPatch; │ │213 - @memcpy(&oid, selected_oid); │ │214 - break :blk oid; │ │215 - }; │ │443 + const merge_oid = oidArray(repo_opts.hash, switch (input.revision) { │ │444 + .squash => selected.squash_oid, │ │445 + .source => selected.source_oid, │ │446 + }) catch return error.InvalidPatch; │ │447 const identity = try std.fmt.allocPrint(arena.allocator(), "{s} <{s}>", .{ input.author.name, input.author.email });│ │448 │ │449 // merge the code and events in one target transaction │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────╮ │459 expected_patch: evt.Patch, │ │460 target_ref: rf.Ref, │ │461 merge_oid: [hash.hexLen(repo_opts.hash)]u8,│ │462 + revision: MergeRevision, │ │463 identity: []const u8, │ │464 author: evt.CommitAuthor, │ │465 timestamp: u64, │ ╰────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │467 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { │ │468 var moment = try DB.HashMap(.read_write).init(cursor.*); │ │469 const state = State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; │ │470 + │ │471 + var check_arena = std.heap.ArenaAllocator.init(ctx.allocator); │ │472 + defer check_arena.deinit(); │ │473 + const availability = try readMergeability(repo_opts, state.readOnly(), ctx.io, &check_arena, &ctx.patch_id, ctx.expected_patch);│ │474 + switch (availability.get(ctx.revision)) { │ │475 + .clean => {}, │ │476 + .conflict => return error.MergeConflict, │ │477 + .unknown => return error.MergeCheckUnavailable, │ │478 + } │ │479 │ │480 // copy the selected commit and its dependencies │ │481 { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────╮ │506 switch (merge_result.result) { │ │507 .conflict => return error.MergeConflict,│ │508 .success, .nothing, .fast_forward => {},│ │509 + .clean => unreachable, │ │510 } │ │511 } │ │512 │ ╰─────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────╮ │527 .expected_patch = patch_record.event,│ │528 .target_ref = target_ref, │ │529 .merge_oid = merge_oid, │ │530 + .revision = input.revision, │ │531 .identity = identity, │ │532 .author = input.author, │ │533 .timestamp = input.timestamp, │ ╰──────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │545 target_repo: *rp.Repo(.xit, repo_opts), │ │546 input: MergeInput, │ │547 ) !void { │ │305 - try merge(repo_opts, io, allocator, repo_root_path, target_repo, input); │ │548 + merge(repo_opts, io, allocator, repo_root_path, target_repo, input) catch |err| { │ │549 + if (err == error.PatchDataUnavailable or err == error.PatchOutOfDate) { │ │550 + const id = evt.parseEventId(&input.id) catch return err; │ │551 + refreshMergeability(repo_opts, io, allocator, target_repo, id); │ │552 + } │ │553 + return err; │ │554 + }; │ │555 try fork.remove(io, allocator, repo_root_path, admin_repo, &input.id, null, input.author);│ │556 + refreshMergeability(repo_opts, io, allocator, target_repo, null); │ │557 } │ │558 │ │559 fn commitAuthor(line: []const u8) !evt.CommitAuthor { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯