╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────╮ │src/patch.zig│ ╰─────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │577 // acquire fork locks before the target repository lock │ │578 var checks: MergeCheckInputs(repo_opts) = .{}; │ │579 defer checks.deinit(io, allocator); │ │580 - try checks.add(io, allocator, target_repo.core.work_path, id.*, initial.event);│ │580 + try checks.add(io, allocator, &target_repo.core, id.*, initial.event); │ │581 │ │582 // cancel the cache transaction when the result is unchanged │ │583 { │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │625 for (events) |event| { │ │626 if (event.event != .patch) continue; │ │627 const patch = event.event.patch orelse continue; │ │628 - try self.add(io, allocator, repo.core.work_path, try evt.parseEventId(&event.id), patch);│ │628 + try self.add(io, allocator, &repo.core, try evt.parseEventId(&event.id), patch); │ │629 } │ │630 } │ │631 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │632 - 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 { │ │633 - // track each open patch once, including branch-backed patches │ │632 + fn add(self: *Self, io: std.Io, allocator: std.mem.Allocator, core: *Repo.Core, id: [evt.event_id_size]u8, patch: evt.Patch) !void { │ │633 + if (patch.source_branch != null) return self.addFork(io, allocator, id, patch, null); │ │634 + │ │635 + // find the fork through the forker recorded in the target's config │ │636 + var moment = try core.latestMoment(); │ │637 + var config = try xit.config.Config(.xit, repo_opts).init(.{ .core = core, .extra = .{ .moment = &moment } }, io, allocator); │ │638 + defer config.deinit(); │ │639 + const forker_id = (try fork.readForkerId(&config.sections, &id)) orelse return self.addFork(io, allocator, id, patch, null); │ │640 + │ │641 + // the target lives at <users dir>/<owner>/repos/<id> │ │642 + const users_dir = std.fs.path.dirname(std.fs.path.dirname(std.fs.path.dirname(core.work_path) orelse ".") orelse ".") orelse "."; │ │643 + const path = try fork.forkPath(allocator, users_dir, &forker_id, &id); │ │644 + defer allocator.free(path); │ │645 + try self.addFork(io, allocator, id, patch, path); │ │646 + } │ │647 + │ │648 + // track an open patch once, locking the fork at `fork_path` when it has one │ │649 + fn addFork(self: *Self, io: std.Io, allocator: std.mem.Allocator, id: [evt.event_id_size]u8, patch: evt.Patch, fork_path: ?[]const u8) !void {│ │650 if (patch.status.kind() != .open) return; │ │651 const entry = try self.forks.getOrPut(allocator, id); │ │652 if (entry.found_existing) return; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │653 entry.value_ptr.* = null; │ │638 - if (patch.source_branch != null) return; │ │654 + const path = fork_path orelse return; │ │655 │ │656 // open the fork before acquiring its shared lock │ │641 - const path = try fork.forkPath(allocator, std.fs.path.dirname(work_path) orelse ".", &std.fmt.bytesToHex(id, .lower));│ │642 - defer allocator.free(path); │ │657 var opened = Repo.open(io, allocator, .{ .path = path, .require_repo_root = true }) catch |err| { │ │658 std.log.warn("mergeability fork unavailable: {s}", .{@errorName(err)}); │ │659 return; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────╮ │882 comptime repo_opts: rp.RepoOpts(.xit), │ │883 io: std.Io, │ │884 allocator: std.mem.Allocator, │ │885 + users_dir: []const u8, │ │886 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), │ │872 - user_repo: *rp.Repo(.xit, evt.user_repo_opts), │ │887 target_repo: *rp.Repo(.xit, repo_opts), │ │874 - fork_path: []const u8, │ │888 input: PublishInput, │ │889 ) !void { │ │890 const patch_id = try evt.parseEventId(&input.id);│ ╰──────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │892 defer arena.deinit(); │ │893 │ │894 // validate the fork ownership and publishing identity │ │895 + var user_repo = (try evt.openUserRepo(io, allocator, users_dir, &input.user_id)) orelse return error.InvalidPatchDraft; │ │896 + defer user_repo.deinit(io, allocator); │ │897 const admin_moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); │ │883 - const user_moment = (try evt.userMoment(user_repo)) orelse return error.InvalidPatchDraft; │ │898 + const user_moment = (try evt.userMoment(&user_repo)) orelse return error.InvalidPatchDraft; │ │899 const fork_record = (try evt.Fork.readById(evt.UserDB, evt.user_repo_opts.hash, user_moment, &arena, &patch_id)) orelse return error.InvalidPatchDraft; │ │900 if (fork_record.removed or !std.mem.eql(u8, fork_record.event.repo_id, &input.repo_id)) return error.InvalidPatchDraft; │ │901 const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &arena, &input.user_id)) orelse return error.InvalidPatchDraft;│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │904 !std.mem.eql(u8, user.event.email, input.author.email)) return error.InvalidPatchDraft; │ │905 │ │906 // read the patch staged in the fork │ │907 + const fork_path = try fork.forkPath(arena.allocator(), users_dir, &input.user_id, &patch_id); │ │908 var fork_repo = rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = fork_path }) catch return error.PatchDataUnavailable;│ │909 defer fork_repo.deinit(io, allocator); │ │910 const fork_moment = evt.currentMoment(repo_opts, &fork_repo) catch return error.PatchDataUnavailable; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 935 patch.revision = selected; │ │ 936 } │ │ 937 if (existing == null) patch.status = .open; │ │ 938 + │ │ 939 + // write the published patch and its forker to the target repository │ │ 940 + { │ │ 941 + var checks: MergeCheckInputs(repo_opts) = .{}; │ │ 942 + defer checks.deinit(io, allocator); │ │ 943 + try checks.addFork(io, allocator, patch_id, patch, fork_path); │ │ 944 + const first_parent = try eventsFirstParent(.xit, repo_opts, io, allocator, target_repo); │ │ 945 │ │ 923 - // write the published patch to the target repository │ │ 924 - try evt.consume(.server, .repo, .xit, repo_opts, io, allocator, target_repo, evt.events_ref, &.{.{ │ │ 925 - .id = input.id, │ │ 926 - .timestamp = input.timestamp, │ │ 927 - .author = input.author, │ │ 928 - .event = .{ .patch = patch }, │ │ 929 - }}); │ │ 946 + const DB = rp.Repo(.xit, repo_opts).DB; │ │ 947 + const State = rp.Repo(.xit, repo_opts).State; │ │ 948 + const Ctx = struct { │ │ 949 + core: *rp.Repo(.xit, repo_opts).Core, │ │ 950 + io: std.Io, │ │ 951 + allocator: std.mem.Allocator, │ │ 952 + events: []const evt.EventWithId, │ │ 953 + patch_id: [evt.event_id_size]u8, │ │ 954 + forker_id: [evt.event_id_size * 2]u8, │ │ 955 + checks: *MergeCheckInputs(repo_opts), │ │ 956 + first_parent: ?[1][hash.hexLen(repo_opts.hash)]u8, │ │ 957 + │ │ 958 + pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { │ │ 959 + var moment = try DB.HashMap(.read_write).init(cursor.*); │ │ 960 + const state = State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; │ │ 961 + │ │ 962 + // code that starts from the target finds the fork through this entry │ │ 963 + var config = try xit.config.Config(.xit, repo_opts).init(state.readOnly(), ctx.io, ctx.allocator); │ │ 964 + defer config.deinit(); │ │ 965 + try config.add(state, ctx.io, .{ .name = &fork.forkerConfigName(&ctx.patch_id), .value = &ctx.forker_id }); │ │ 966 + │ │ 967 + const parent = if (try rf.readRecur(.xit, repo_opts, state.readOnly(), ctx.io, .{ .ref = evt.events_ref }) == null) ctx.first_parent else null; │ │ 968 + try evt.commitEvents(.xit, repo_opts, state, ctx.io, ctx.allocator, evt.events_ref, ctx.events, parent); │ │ 969 + if (!try evt.consumeInTransaction(.repo, .xit, repo_opts, state, &ctx.core.db, &moment, ctx.io, ctx.allocator, evt.events_ref)) return error.CancelTransaction;│ │ 970 + _ = try ctx.checks.update(state, ctx.io, ctx.allocator); │ │ 971 + try xit.undo.write(repo_opts, state, std.Io.Timestamp.now(ctx.io, .real).toSeconds(), .{ .custom = .{ .action_kind = evt.undo_action } }); │ │ 972 + } │ │ 973 + }; │ │ 974 + │ │ 975 + try target_repo.core.db_file.lock(io, .exclusive); │ │ 976 + defer target_repo.core.db_file.unlock(io); │ │ 977 + │ │ 978 + const history = try DB.ArrayList(.read_write).init(target_repo.core.db.rootCursor()); │ │ 979 + history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ │ │ 980 + .core = &target_repo.core, │ │ 981 + .io = io, │ │ 982 + .allocator = allocator, │ │ 983 + .events = &.{.{ │ │ 984 + .id = input.id, │ │ 985 + .timestamp = input.timestamp, │ │ 986 + .author = input.author, │ │ 987 + .event = .{ .patch = patch }, │ │ 988 + }}, │ │ 989 + .patch_id = patch_id, │ │ 990 + .forker_id = std.fmt.bytesToHex(input.user_id, .lower), │ │ 991 + .checks = &checks, │ │ 992 + .first_parent = first_parent, │ │ 993 + }) catch |err| switch (err) { │ │ 994 + error.CancelTransaction => {}, │ │ 995 + else => |e| return e, │ │ 996 + }; │ │ 997 + } │ │ 998 │ │ 999 // remove the consumed draft from the fork │ │1000 try evt.consume(.server, .fork, .xit, repo_opts, io, allocator, &fork_repo, evt.events_ref, &.{.{ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1009 if (fork_record.event.stage == .draft) { │ │1010 var published = fork_record.event; │ │1011 published.stage = .publish; │ │ 944 - try evt.consume(.server, .user, .xit, evt.user_repo_opts, io, allocator, user_repo, evt.events_ref, &.{.{ │ │1012 + try evt.consume(.server, .user, .xit, evt.user_repo_opts, io, allocator, &user_repo, evt.events_ref, &.{.{│ │1013 .id = input.id, │ │1014 .timestamp = input.timestamp, │ │1015 .author = input.author, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────╮ │1022 comptime repo_opts: rp.RepoOpts(.xit), │ │1023 io: std.Io, │ │1024 allocator: std.mem.Allocator, │ │1025 + users_dir: []const u8, │ │1026 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), │ │ 958 - user_repo: *rp.Repo(.xit, evt.user_repo_opts), │ │ 959 - fork_path: []const u8, │ │1027 input: EditDraftInput, │ │1028 ) !bool { │ │1029 // validate the submitted fields before opening repositories│ ╰──────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯