╭ changed ────╮ │src/patch.zig│ ╰─────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────╮ │ 5 const hash = xit.hash; │ │ 6 const obj = xit.object; │ │ 7 const rf = xit.ref; │ │ 8 +const mrg = xit.merge; │ │ 9 +const fork = @import("fork.zig"); │ │10 +const serve_common = @import("serve_common.zig");│ │11 │ │12 pub const PostInput = struct { │ │13 id: [evt.event_id_size * 2]u8, │ ╰─────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────╮ │14 user_id: [evt.event_id_size]u8, │ │15 repo_id: [evt.event_id_size]u8, │ │16 + author: evt.CommitAuthor, │ │17 + timestamp: u64, │ │18 +}; │ │19 + │ │20 +pub const MergeRevision = enum { squash, source };│ │21 + │ │22 +pub const MergeInput = struct { │ │23 + id: [evt.event_id_size * 2]u8, │ │24 + revision: MergeRevision, │ │25 author: evt.CommitAuthor, │ │26 timestamp: u64, │ │27 }; │ ╰──────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │100 .author = input.author, │ │101 .event = .{ .fork = posted }, │ │102 }}); │ │103 + } │ │104 +} │ │105 + │ │106 +// merge the selected patch revision into its target branch │ │107 +pub fn merge( │ │108 + comptime repo_opts: rp.RepoOpts(.xit), │ │109 + io: std.Io, │ │110 + allocator: std.mem.Allocator, │ │111 + repo_root_path: []const u8, │ │112 + target_repo: *rp.Repo(.xit, repo_opts), │ │113 + input: MergeInput, │ │114 +) !void { │ │115 + const patch_id = try evt.parseEventId(&input.id); │ │116 + var arena = std.heap.ArenaAllocator.init(allocator); │ │117 + defer arena.deinit(); │ │118 + │ │119 + // validate the target patch │ │120 + const target_moment = try evt.currentMoment(repo_opts, target_repo); │ │121 + const patch_record = (try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, target_moment, &arena, &patch_id)) orelse return error.InvalidPatch; │ │122 + if (patch_record.removed) return error.InvalidPatch; │ │123 + switch (patch_record.event.status) { │ │124 + .open => {}, │ │125 + .closed => return error.PatchClosed, │ │126 + .merged => return error.PatchAlreadyMerged, │ │127 + } │ │128 + const selected = patch_record.event.revision orelse return error.PatchNotPushed; │ │129 + const revision_id = try evt.parseEventId(&selected.id); │ │130 + const target_ref = rf.Ref.initFromPath(selected.target_ref, null) orelse return error.InvalidPatch; │ │131 + switch (target_ref.kind) { │ │132 + .head => {}, │ │133 + else => return error.InvalidPatch, │ │134 + } │ │135 + │ │136 + // open and lock the fork │ │137 + const fork_path = try fork.forkPath(arena.allocator(), repo_root_path, &input.id); │ │138 + var fork_repo = rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }) catch return error.PatchDataUnavailable; │ │139 + defer fork_repo.deinit(io, allocator); │ │140 + try fork_repo.core.db_file.lock(io, .shared); │ │141 + defer fork_repo.core.db_file.unlock(io); │ │142 + │ │143 + // require the fork's newest revision │ │144 + { │ │145 + const fork_moment = try evt.currentMoment(repo_opts, &fork_repo); │ │146 + const newest = (try evt.PatchRev.readNewest(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena)) orelse return error.PatchDataUnavailable; │ │147 + if (!std.mem.eql(u8, &newest.id, &revision_id) or │ │148 + !selected.matches(newest.record)) return error.PatchOutOfDate; │ │149 + } │ │150 + │ │151 + // select the commit and merge identity │ │152 + const merge_oid = blk: { │ │153 + const selected_oid = switch (input.revision) { │ │154 + .squash => selected.squash_oid, │ │155 + .source => selected.source_oid, │ │156 + }; │ │157 + var oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; │ │158 + if (selected_oid.len != oid.len) return error.InvalidPatch; │ │159 + @memcpy(&oid, selected_oid); │ │160 + break :blk oid; │ │161 + }; │ │162 + const identity = try std.fmt.allocPrint(arena.allocator(), "{s} <{s}>", .{ input.author.name, input.author.email }); │ │163 + │ │164 + // merge the code and events in one target transaction │ │165 + { │ │166 + const DB = rp.Repo(.xit, repo_opts).DB; │ │167 + const State = rp.Repo(.xit, repo_opts).State; │ │168 + const Ctx = struct { │ │169 + core: *rp.Repo(.xit, repo_opts).Core, │ │170 + fork_repo: *rp.Repo(.xit, repo_opts), │ │171 + io: std.Io, │ │172 + allocator: std.mem.Allocator, │ │173 + patch_id: [evt.event_id_size]u8, │ │174 + expected_patch: evt.Patch, │ │175 + target_ref: rf.Ref, │ │176 + merge_oid: [hash.hexLen(repo_opts.hash)]u8, │ │177 + identity: []const u8, │ │178 + author: evt.CommitAuthor, │ │179 + timestamp: u64, │ │180 + │ │181 + pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { │ │182 + var moment = try DB.HashMap(.read_write).init(cursor.*); │ │183 + const state = State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; │ │184 + │ │185 + // copy the selected commit and its dependencies │ │186 + { │ │187 + var fork_repo_moment = try ctx.fork_repo.core.latestMoment(); │ │188 + const fork_state = State(.read_only){ .core = &ctx.fork_repo.core, .extra = .{ .moment = &fork_repo_moment } }; │ │189 + var objects = try obj.ObjectIterator(.xit, repo_opts).init(fork_state, ctx.io, ctx.allocator, .{ .kind = .all }); │ │190 + defer objects.deinit(); │ │191 + try objects.include(&ctx.merge_oid); │ │192 + try obj.copyFromObjectIterator(.xit, repo_opts, state, .xit, repo_opts, &objects, ctx.io, null); │ │193 + } │ │194 + │ │195 + // import the revision and mark the patch merged │ │196 + if (!try importMergedFromFork(repo_opts, state, &ctx.core.db, &moment, ctx.io, ctx.allocator, ctx.fork_repo, &ctx.patch_id, ctx.expected_patch, ctx.author, ctx.timestamp)) return error.PatchOutOfDate;│ │197 + │ │198 + // merge the selected commit │ │199 + { │ │200 + var merge_result = try mrg.Merge(.xit, repo_opts).init(state, ctx.io, ctx.allocator, .{ │ │201 + .kind = .full, │ │202 + .action = .{ .new = .{ .source = &.{.{ .oid = &ctx.merge_oid }}, .algo = .diff3 } }, │ │203 + .commit_metadata = .{ │ │204 + .author = ctx.identity, │ │205 + .committer = ctx.identity, │ │206 + .message = ctx.expected_patch.title, │ │207 + .timestamp = ctx.timestamp, │ │208 + }, │ │209 + }, ctx.target_ref, null); │ │210 + defer merge_result.deinit(); │ │211 + switch (merge_result.result) { │ │212 + .conflict => return error.MergeConflict, │ │213 + .success, .nothing, .fast_forward => {}, │ │214 + } │ │215 + } │ │216 + │ │217 + try xit.undo.writeMessage(repo_opts, state, .{ .custom = "merge patch" }); │ │218 + try ctx.core.chunk_store_file.sync(ctx.io); │ │219 + } │ │220 + }; │ │221 + │ │222 + try target_repo.core.db_file.lock(io, .exclusive); │ │223 + defer target_repo.core.db_file.unlock(io); │ │224 + │ │225 + const history = try DB.ArrayList(.read_write).init(target_repo.core.db.rootCursor()); │ │226 + try history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ │ │227 + .core = &target_repo.core, │ │228 + .fork_repo = &fork_repo, │ │229 + .io = io, │ │230 + .allocator = allocator, │ │231 + .patch_id = patch_id, │ │232 + .expected_patch = patch_record.event, │ │233 + .target_ref = target_ref, │ │234 + .merge_oid = merge_oid, │ │235 + .identity = identity, │ │236 + .author = input.author, │ │237 + .timestamp = input.timestamp, │ │238 + }); │ │239 } │ │240 } │ │241 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────╮ │247 return .{ .name = name, .email = line[open + 1 .. close] };│ │248 } │ │249 │ │250 +// import a revision after detecting a client-side merge │ │251 fn importMerged( │ │252 comptime repo_opts: rp.RepoOpts(.xit), │ │253 state: rp.Repo(.xit, repo_opts).State(.read_write), │ ╰────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │258 repo_root_path: []const u8, │ │259 patch_id: *const [evt.event_id_size]u8, │ │260 ) !void { │ │261 + var arena = std.heap.ArenaAllocator.init(allocator); │ │262 + defer arena.deinit(); │ │263 + │ │264 + const patch_hex = std.fmt.bytesToHex(patch_id.*, .lower); │ │265 + const fork_path = try fork.forkPath(arena.allocator(), repo_root_path, &patch_hex); │ │266 + var fork_repo = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = fork_path, .require_repo_root = true });│ │267 + defer fork_repo.deinit(io, allocator); │ │268 + │ │269 + _ = try importMergedFromFork(repo_opts, state, db, moment, io, allocator, &fork_repo, patch_id, null, null, 0); │ │270 +} │ │271 + │ │272 +// import the revision and mark its patch merged in the current transaction │ │273 +fn importMergedFromFork( │ │274 + comptime repo_opts: rp.RepoOpts(.xit), │ │275 + state: rp.Repo(.xit, repo_opts).State(.read_write), │ │276 + db: *evt.EventDB(repo_opts.hash), │ │277 + moment: *evt.EventDB(repo_opts.hash).HashMap(.read_write), │ │278 + io: std.Io, │ │279 + allocator: std.mem.Allocator, │ │280 + fork_repo: *rp.Repo(.xit, repo_opts), │ │281 + patch_id: *const [evt.event_id_size]u8, │ │282 + expected_patch: ?evt.Patch, │ │283 + merged_author: ?evt.CommitAuthor, │ │284 + merged_timestamp: u64, │ │285 +) !bool { │ │286 const DB = evt.EventDB(repo_opts.hash); │ │287 var arena = std.heap.ArenaAllocator.init(allocator); │ │288 defer arena.deinit(); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │289 │ │290 const haxy_moment = try evt.currentMomentFromRepoMoment(repo_opts.hash, moment.readOnly()); │ │117 - const patch_record = (try evt.Patch.readById(DB, repo_opts.hash, haxy_moment, &arena, patch_id)) orelse return; │ │118 - const selected = patch_record.event.revision orelse return; │ │119 - if (patch_record.removed or patch_record.event.status == .merged) return; │ │291 + const patch_record = (try evt.Patch.readById(DB, repo_opts.hash, haxy_moment, &arena, patch_id)) orelse return false; │ │292 + const selected = patch_record.event.revision orelse return false; │ │293 + if (patch_record.removed or patch_record.event.status == .merged) return false; │ │294 + if (expected_patch) |expected| { │ │295 + if (!evt.fieldEqual(evt.Patch, expected, patch_record.event)) return false; │ │296 + } │ │297 const revision_id = try evt.parseEventId(&selected.id); │ │298 │ │122 - const patch_hex = std.fmt.bytesToHex(patch_id.*, .lower); │ │123 - const data_path = std.fs.path.dirname(repo_root_path) orelse "."; │ │124 - const fork_path = try std.fs.path.join(arena.allocator(), &.{ data_path, "forks", &patch_hex }); │ │125 - var fork_repo = rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }) catch return; │ │126 - defer fork_repo.deinit(io, allocator); │ │299 + const fork_moment = try evt.currentMoment(repo_opts, fork_repo); │ │300 + const revision = (try evt.PatchRev.readById(DB, repo_opts.hash, fork_moment, &arena, &revision_id)) orelse return false; │ │301 + if (!selected.matches(revision)) return false; │ │302 │ │128 - const fork_moment = evt.currentMoment(repo_opts, &fork_repo) catch return; │ │129 - const revision = (evt.PatchRev.readById(DB, repo_opts.hash, fork_moment, &arena, &revision_id) catch return) orelse return; │ │130 - if (!selected.matches(revision)) return; │ │131 - │ │303 var fork_repo_moment = try fork_repo.core.latestMoment(); │ │304 const fork_state = rp.Repo(.xit, repo_opts).State(.read_only){ .core = &fork_repo.core, .extra = .{ .moment = &fork_repo_moment } };│ │305 var event_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │135 - if (revision.event_oid.len != event_oid.len) return; │ │306 + if (revision.event_oid.len != event_oid.len) return false; │ │307 @memcpy(&event_oid, revision.event_oid); │ │137 - var event_object = obj.Object(.xit, repo_opts).initCommit(fork_state, io, allocator, &event_oid) catch return;│ │308 + var event_object = try obj.Object(.xit, repo_opts).initCommit(fork_state, io, allocator, &event_oid); │ │309 defer event_object.deinit(); │ │139 - const original_author = commitAuthor(event_object.content.commit.metadata.author orelse return) catch return; │ │310 + const original_author = try commitAuthor(event_object.content.commit.metadata.author orelse return false); │ │311 │ │312 var events: [2]evt.EventWithId = undefined; │ │313 var event_count: usize = 0; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │336 patch.status = .merged; │ │337 events[event_count] = .{ │ │338 .id = std.fmt.bytesToHex(patch_id.*, .lower), │ │168 - .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │169 - .author = original_author, │ │339 + .timestamp = if (merged_author != null) merged_timestamp else @intCast(std.Io.Timestamp.now(io, .real).toSeconds()),│ │340 + .author = merged_author orelse original_author, │ │341 .event = .{ .patch = patch }, │ │342 }; │ │343 event_count += 1; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │344 │ │345 try evt.commitEvents(.xit, repo_opts, state, io, allocator, evt.events_ref, events[0..event_count], null); │ │346 if (!try evt.consumeInTransaction(.repo, .xit, repo_opts, state, db, moment, io, allocator, evt.events_ref)) return error.CancelTransaction;│ │347 + return true; │ │348 } │ │349 │ │350 +// find patches merged by commits received in a push │ │351 pub fn detectMerged( │ │352 comptime repo_opts: rp.RepoOpts(.xit), │ │353 state: rp.Repo(.xit, repo_opts).State(.read_write), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────╮ │357 allocator: std.mem.Allocator, │ │358 repo_root_path: []const u8, │ │359 updates: []const xit.net_server_receive_pack.AppliedRefUpdate,│ │360 + error_writer: *std.Io.Writer, │ │361 ) !void { │ │362 const DB = evt.EventDB(repo_opts.hash); │ │363 var arena = std.heap.ArenaAllocator.init(allocator); │ ╰───────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯