╭ changed ────╮ │src/patch.zig│ ╰─────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 8 const mrg = xit.merge; │ │ 9 const fork = @import("fork.zig"); │ │ 10 const serve_common = @import("serve_common.zig"); │ │ 11 + │ │ 12 +// capture an existing branch without creating a fork │ │ 13 +pub fn writeBranchPatch( │ │ 14 + host_kind: evt.HostKind, │ │ 15 + comptime repo_kind: rp.RepoKind, │ │ 16 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │ 17 + io: std.Io, │ │ 18 + allocator: std.mem.Allocator, │ │ 19 + repo: *rp.Repo(repo_kind, repo_opts), │ │ 20 + id: [evt.event_id_size * 2]u8, │ │ 21 + patch: evt.Patch, │ │ 22 + expected_patch: ?evt.Patch, │ │ 23 + author: evt.CommitAuthor, │ │ 24 +) !void { │ │ 25 + var arena = std.heap.ArenaAllocator.init(allocator); │ │ 26 + defer arena.deinit(); │ │ 27 + if (repo_kind == .git) { │ │ 28 + if (expected_patch) |expected| { │ │ 29 + const current = (try evt.Patch.readFromRepo(repo_kind, repo_opts, io, allocator, &arena, repo, &try evt.parseEventId(&id))) orelse return error.PatchOutOfDate; │ │ 30 + if (current.removed or !evt.fieldEqual(evt.Patch, current.event, expected)) return error.PatchOutOfDate; │ │ 31 + } │ │ 32 + const events = try branchEvents(repo_kind, repo_opts, .{ .core = &repo.core, .extra = .{} }, io, &arena, id, patch, author); │ │ 33 + try evt.consume(host_kind, .repo, repo_kind, repo_opts, io, allocator, repo, evt.events_ref, &events); │ │ 34 + } else { │ │ 35 + const DB = evt.EventDB(repo_opts.hash); │ │ 36 + var first_parent: ?[1][hash.hexLen(repo_opts.hash)]u8 = null; │ │ 37 + if (try repo.readRef(io, evt.events_ref) == null) { │ │ 38 + var remotes = try repo.listRemotes(io, allocator); │ │ 39 + defer remotes.deinit(); │ │ 40 + for (remotes.sections.keys()) |name| { │ │ 41 + if (try repo.readRef(io, .{ .kind = .{ .remote = name }, .name = evt.events_ref.name })) |oid| { │ │ 42 + first_parent = .{oid}; │ │ 43 + break; │ │ 44 + } │ │ 45 + } │ │ 46 + } │ │ 47 + const Ctx = struct { │ │ 48 + repo: *rp.Repo(repo_kind, repo_opts), │ │ 49 + io: std.Io, │ │ 50 + arena: *std.heap.ArenaAllocator, │ │ 51 + id: [evt.event_id_size * 2]u8, │ │ 52 + patch: evt.Patch, │ │ 53 + expected_patch: ?evt.Patch, │ │ 54 + author: evt.CommitAuthor, │ │ 55 + first_parent: ?[1][hash.hexLen(repo_opts.hash)]u8, │ │ 56 + │ │ 57 + pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { │ │ 58 + var moment = try DB.HashMap(.read_write).init(cursor.*); │ │ 59 + const state = rp.Repo(repo_kind, repo_opts).State(.read_write){ .core = &ctx.repo.core, .extra = .{ .moment = &moment } }; │ │ 60 + if (ctx.expected_patch) |expected| { │ │ 61 + const events_moment = try evt.currentMomentFromRepoMoment(repo_opts.hash, moment.readOnly()); │ │ 62 + const current = (try evt.Patch.readById(DB, repo_opts.hash, events_moment, ctx.arena, &try evt.parseEventId(&ctx.id))) orelse return error.PatchOutOfDate; │ │ 63 + if (current.removed or !evt.fieldEqual(evt.Patch, current.event, expected)) return error.PatchOutOfDate; │ │ 64 + } │ │ 65 + const events = try branchEvents(repo_kind, repo_opts, state, ctx.io, ctx.arena, ctx.id, ctx.patch, ctx.author); │ │ 66 + const parent = if (try rf.readRecur(repo_kind, repo_opts, state.readOnly(), ctx.io, .{ .ref = evt.events_ref }) == null) ctx.first_parent else null; │ │ 67 + try evt.commitEvents(repo_kind, repo_opts, state, ctx.io, ctx.arena.child_allocator, evt.events_ref, &events, parent); │ │ 68 + 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; │ │ 69 + try xit.undo.writeMessage(repo_opts, state, .{ .custom = "update branch patch" }); │ │ 70 + } │ │ 71 + }; │ │ 72 + { │ │ 73 + try repo.core.db_file.lock(io, .exclusive); │ │ 74 + defer repo.core.db_file.unlock(io); │ │ 75 + const history = try DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); │ │ 76 + 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 });│ │ 77 + } │ │ 78 + if (host_kind == .server) refreshMergeability(repo_opts, io, allocator, repo, try evt.parseEventId(&id)); │ │ 79 + } │ │ 80 +} │ │ 81 + │ │ 82 +fn branchEvents( │ │ 83 + comptime repo_kind: rp.RepoKind, │ │ 84 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │ 85 + state: rp.Repo(repo_kind, repo_opts).State(.read_write), │ │ 86 + io: std.Io, │ │ 87 + arena: *std.heap.ArenaAllocator, │ │ 88 + id: [evt.event_id_size * 2]u8, │ │ 89 + patch: evt.Patch, │ │ 90 + author: evt.CommitAuthor, │ │ 91 +) ![2]evt.EventWithId { │ │ 92 + const branch = patch.source_branch orelse return error.InvalidSourceBranch; │ │ 93 + if (!evt.Patch.fieldsValid(patch.title, patch.tags)) return error.InvalidFields; │ │ 94 + if (!rf.validateName(branch) or std.mem.eql(u8, branch, evt.events_ref.name)) return error.InvalidSourceBranch; │ │ 95 + if (!rf.validateName(patch.target_branch) or std.mem.eql(u8, patch.target_branch, evt.events_ref.name)) return error.InvalidTargetBranch; │ │ 96 + const source = (try rf.readRecur(repo_kind, repo_opts, state.readOnly(), io, .{ .ref = .{ .kind = .head, .name = branch } })) orelse return error.InvalidSourceBranch; │ │ 97 + const target = (try rf.readRecur(repo_kind, repo_opts, state.readOnly(), io, .{ .ref = .{ .kind = .head, .name = patch.target_branch } })) orelse return error.InvalidTargetBranch; │ │ 98 + const base = mrg.commonAncestor(repo_kind, repo_opts, state.readOnly(), io, arena.allocator(), &target, &source) catch |err| switch (err) { │ │ 99 + error.NoCommonAncestor => return error.UnrelatedBranches, │ │100 + else => return err, │ │101 + }; │ │102 + const timestamp: u64 = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()); │ │103 + const prepared = try evt.PatchRev.prepare(repo_kind, repo_opts, state, io, arena, &base, &source, patch.title, author, timestamp); │ │104 + var updated = patch; │ │105 + updated.revision = prepared.revision; │ │106 + return .{ │ │107 + prepared.event, │ │108 + .{ .id = id, .author = author, .timestamp = timestamp, .event = .{ .patch = updated } }, │ │109 + }; │ │110 +} │ │111 + │ │112 +// refresh tracked branches outside the caller's transaction │ │113 +pub fn refreshBranches( │ │114 + host_kind: evt.HostKind, │ │115 + comptime repo_kind: rp.RepoKind, │ │116 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │117 + io: std.Io, │ │118 + allocator: std.mem.Allocator, │ │119 + repo: *rp.Repo(repo_kind, repo_opts), │ │120 + updates: ?[]const xit.net_server_receive_pack.AppliedRefUpdate, │ │121 +) !void { │ │122 + const DB = evt.EventDB(repo_opts.hash); │ │123 + var arena = std.heap.ArenaAllocator.init(allocator); │ │124 + defer arena.deinit(); │ │125 + var patches: std.ArrayList(struct { id: [evt.event_id_size]u8, record: evt.Patch.Record }) = .empty; │ │126 + defer patches.deinit(allocator); │ │127 + { │ │128 + 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; │ │129 + defer if (local_db) |*db| db.deinit(io, allocator); │ │130 + const moment = (if (local_db) |*db| evt.currentMomentFromDb(repo_opts.hash, db.db) else if (repo_kind == .git) return else evt.currentMoment(repo_opts, repo)) catch |err| switch (err) { │ │131 + error.NotFound => return, │ │132 + else => return err, │ │133 + }; │ │134 + const source_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.source_to_id_set_key)) orelse return; │ │135 + const sources = try DB.SortedMap(.read_only).init(source_cursor); │ │136 + 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; │ │137 + var iter = try sources.iterator(); │ │138 + while (try iter.next()) |cursor| { │ │139 + const pair = try cursor.readKeyValuePair(); │ │140 + const branch = try pair.key_cursor.readBytesAlloc(arena.allocator(), null); │ │141 + if (updates) |changed| { │ │142 + var found = false; │ │143 + for (changed) |update| { │ │144 + if (std.mem.startsWith(u8, update.ref_name, "refs/heads/") and std.mem.eql(u8, update.ref_name["refs/heads/".len..], branch)) { │ │145 + found = true; │ │146 + break; │ │147 + } │ │148 + } │ │149 + if (!found) continue; │ │150 + } │ │151 + const source = (try repo.readRef(io, .{ .kind = .head, .name = branch })) orelse { │ │152 + std.log.warn("patch source branch missing: {s}", .{branch}); │ │153 + continue; │ │154 + }; │ │155 + const ids = try DB.CountedHashSet(.read_only).init(pair.value_cursor); │ │156 + var ids_iter = try ids.iterator(); │ │157 + while (try ids_iter.next()) |id_cursor| { │ │158 + const id_pair = try id_cursor.readKeyValuePair(); │ │159 + var id: [evt.event_id_size]u8 = undefined; │ │160 + if ((try id_pair.key_cursor.readBytes(&id)).len != id.len) return error.InvalidPatch; │ │161 + const record = (try evt.Patch.readById(DB, repo_opts.hash, moment, &arena, &id)) orelse continue; │ │162 + if (conflicts) |map| if (try map.getCursor(&evt.orderKeyDesc(record.created_order, &id)) != null) continue; │ │163 + if (record.event.revision) |revision| if (std.mem.eql(u8, revision.source_oid, &source)) continue; │ │164 + try patches.append(allocator, .{ .id = id, .record = record }); │ │165 + } │ │166 + } │ │167 + } │ │168 + for (patches.items) |patch| { │ │169 + const author = evt.CommitAuthor{ .name = "haxy", .email = patch.record.author_email orelse "user@haxy" }; │ │170 + writeBranchPatch(host_kind, repo_kind, repo_opts, io, allocator, repo, std.fmt.bytesToHex(patch.id, .lower), patch.record.event, patch.record.event, author) catch |err| { │ │171 + std.log.warn("failed to refresh branch patch: {s}", .{@errorName(err)}); │ │172 + }; │ │173 + } │ │174 +} │ │175 │ │176 pub const PublishInput = struct { │ │177 id: [evt.event_id_size * 2]u8, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │255 id: *const [evt.event_id_size]u8, │ │256 patch: evt.Patch, │ │257 ) !Mergeability { │ │258 + if (patch.source_branch != null and !try branchCurrent(repo_opts, state, io, arena, patch)) return .{}; │ │259 const target_oid = try rf.readRecur(.xit, repo_opts, state, io, .{ .ref = .{ .kind = .head, .name = patch.target_branch } }) orelse return .{};│ │260 const cached = try readMergeCheck(repo_opts, state.extra.moment.*, arena, id) orelse return .{}; │ │261 return if (cached.matches(patch, &target_oid)) cached.result else .{}; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │310 const DB = Repo.DB; │ │311 var arena = std.heap.ArenaAllocator.init(allocator); │ │312 defer arena.deinit(); │ │313 + const initial = try evt.Patch.readFromRepo(.xit, repo_opts, io, allocator, &arena, target_repo, id); │ │314 + if (initial) |record| if (record.removed or record.event.status.kind() != .open) return; │ │315 const repo_root_path = std.fs.path.dirname(target_repo.core.work_path) orelse "."; │ │316 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: { │ │317 + const branch_backed = if (initial) |record| record.event.source_branch != null else false; │ │318 + var fork_repo_maybe: ?Repo = if (branch_backed) null else Repo.open(io, allocator, .{ .path = path, .require_repo_root = true }) catch |err| blk: {│ │319 std.log.warn("mergeability fork unavailable: {s}", .{@errorName(err)}); │ │320 break :blk null; │ │321 }; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │349 const target_oid = try rf.readRecur(.xit, repo_opts, target_state, io, .{ .ref = .{ .kind = .head, .name = patch.target_branch } }); │ │350 var check = MergeCheck{ .revision = revision, .target_branch = patch.target_branch, .target_oid = if (target_oid) |*oid| oid else "" }; │ │351 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; │ │352 + const current = if (patch.source_branch != null) try branchCurrent(repo_opts, target_state, io, &arena, patch) else if (newest) |latest| std.mem.eql(u8, &std.fmt.bytesToHex(latest.id, .lower), &revision.id) and revision.matches(latest.record) else false;│ │353 if (current and target_oid != null) { │ │354 if (cached) |value| if (value.matches(patch, check.target_oid) and value.result.source != .unknown and value.result.squash != .unknown) return; │ │355 const Ctx = struct { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────╮ │356 repo: *Repo, │ │189 - fork_repo: *Repo, │ │357 + fork_repo: ?*Repo, │ │358 io: std.Io, │ │359 allocator: std.mem.Allocator,│ │360 check: *MergeCheck, │ ╰──────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │362 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { │ │363 var moment = try DB.HashMap(.read_write).init(cursor.*); │ │364 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(); │ │365 const source = try oidArray(repo_opts.hash, ctx.check.revision.source_oid); │ │366 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); │ │367 + if (ctx.fork_repo) |repo| { │ │368 + var fork_moment = try repo.core.latestMoment(); │ │369 + const fork_state = Repo.State(.read_only){ .core = &repo.core, .extra = .{ .moment = &fork_moment } }; │ │370 + var objects = try obj.ObjectIterator(.xit, repo_opts).init(fork_state, ctx.io, ctx.allocator, .{ .kind = .all });│ │371 + defer objects.deinit(); │ │372 + try objects.include(&source); │ │373 + try objects.include(&squash); │ │374 + try obj.copyFromObjectIterator(.xit, repo_opts, state, .xit, repo_opts, &objects, ctx.io, null); │ │375 + } │ │376 const target = rf.Ref{ .kind = .head, .name = ctx.check.target_branch }; │ │377 ctx.check.result.source = checkCommit(repo_opts, state, ctx.io, ctx.allocator, &source, target); │ │378 ctx.check.result.squash = checkCommit(repo_opts, state, ctx.io, ctx.allocator, &squash, target); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │382 const history = try DB.ArrayList(.read_write).init(target_repo.core.db.rootCursor());│ │383 history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ │ │384 .repo = target_repo, │ │215 - .fork_repo = if (fork_repo_maybe) |*repo| repo else unreachable, │ │385 + .fork_repo = if (fork_repo_maybe) |*repo| repo else null, │ │386 .io = io, │ │387 .allocator = allocator, │ │388 .check = &check, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │405 }; │ │406 const history = try DB.ArrayList(.read_write).init(target_repo.core.db.rootCursor()); │ │407 try history.appendContext(.{ .slot = try history.getSlot(-1) }, Save{ .id = id.*, .check = check }); │ │408 +} │ │409 + │ │410 +fn branchCurrent(comptime repo_opts: rp.RepoOpts(.xit), state: rp.Repo(.xit, repo_opts).State(.read_only), io: std.Io, arena: *std.heap.ArenaAllocator, patch: evt.Patch) !bool {│ │411 + const branch = patch.source_branch orelse return false; │ │412 + const selected = patch.revision orelse return false; │ │413 + const oid = (try rf.readRecur(.xit, repo_opts, state, io, .{ .ref = .{ .kind = .head, .name = branch } })) orelse return false; │ │414 + if (!std.mem.eql(u8, &oid, selected.source_oid)) return false; │ │415 + const events = try evt.currentMomentFromRepoMoment(repo_opts.hash, state.extra.moment.*); │ │416 + const revision = (try evt.PatchRev.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, events, arena, &try evt.parseEventId(&selected.id))) orelse return false; │ │417 + return selected.matches(revision); │ │418 } │ │419 │ │420 fn oidArray(comptime hash_kind: hash.HashKind, oid: []const u8) ![hash.hexLen(hash_kind)]u8 { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │609 │ │610 // open the fork │ │611 const fork_path = try fork.forkPath(arena.allocator(), repo_root_path, &input.id); │ │432 - var fork_repo = rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }) catch return error.PatchDataUnavailable; │ │433 - defer fork_repo.deinit(io, allocator); │ │612 + var fork_repo_maybe: ?rp.Repo(.xit, repo_opts) = if (patch_record.event.source_branch != null) null else rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }) catch return error.PatchDataUnavailable;│ │613 + defer if (fork_repo_maybe) |*repo| repo.deinit(io, allocator); │ │614 // select the commit and merge identity │ │615 const merge_oid = oidArray(repo_opts.hash, switch (input.revision) { │ │616 .squash => selected.squash_oid, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────╮ │624 const State = rp.Repo(.xit, repo_opts).State;│ │625 const Ctx = struct { │ │626 core: *rp.Repo(.xit, repo_opts).Core, │ │447 - fork_repo: *rp.Repo(.xit, repo_opts), │ │627 + fork_repo: ?*rp.Repo(.xit, repo_opts), │ │628 io: std.Io, │ │629 allocator: std.mem.Allocator, │ │630 patch_id: [evt.event_id_size]u8, │ ╰──────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯