╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ patch.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
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 (!evt.Patch.branchValid(branch)) return error.InvalidSourceBranch; 95 if (!evt.Patch.branchValid(patch.target_branch)) 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 if (std.mem.eql(u8, branch, patch.target_branch)) return error.SameBranch; 99 const base = mrg.commonAncestor(repo_kind, repo_opts, state.readOnly(), io, arena.allocator(), &target, &source) catch |err| switch (err) { 100 error.NoCommonAncestor => return error.UnrelatedBranches, 101 else => return err, 102 }; 103 const timestamp: u64 = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()); 104 const prepared = try evt.PatchRev.prepare(repo_kind, repo_opts, state, io, arena, &base, &source, patch.title, author, timestamp); 105 var updated = patch; 106 updated.revision = prepared.revision; 107 return .{ 108 prepared.event, 109 .{ .id = id, .author = author, .timestamp = timestamp, .event = .{ .patch = updated } }, 110 }; 111 } 112 113 // refresh open patches' tracked branches and, on the server, their mergeability. 114 // this runs outside the caller's transaction. 115 pub fn refreshBranches( 116 host_kind: evt.HostKind, 117 comptime repo_kind: rp.RepoKind, 118 comptime repo_opts: rp.RepoOpts(repo_kind), 119 io: std.Io, 120 allocator: std.mem.Allocator, 121 repo: *rp.Repo(repo_kind, repo_opts), 122 updates: ?[]const xit.net_server_receive_pack.AppliedRefUpdate, 123 progress_ctx_maybe: ?repo_opts.ProgressCtx, 124 ) !void { 125 const DB = evt.EventDB(repo_opts.hash); 126 var arena = std.heap.ArenaAllocator.init(allocator); 127 defer arena.deinit(); 128 var patches: std.AutoArrayHashMapUnmanaged([evt.event_id_size]u8, ?evt.Patch.Record) = .empty; 129 defer patches.deinit(allocator); 130 collect: { 131 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; 132 defer if (local_db) |*db| db.deinit(io, allocator); 133 const moment = (if (local_db) |*db| evt.currentMomentFromDb(repo_opts.hash, db.db) else if (repo_kind == .git) break :collect else evt.currentMoment(repo_opts, repo)) catch |err| switch (err) { 134 error.NotFound => break :collect, 135 else => return err, 136 }; 137 if (repo_kind == .xit and host_kind == .server) open: { 138 const statuses_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.status_to_id_set_key)) orelse break :open; 139 const statuses = try DB.SortedMap(.read_only).init(statuses_cursor); 140 const open_cursor = try statuses.getCursor("open") orelse break :open; 141 const open_patches = try DB.SortedSet(.read_only).init(open_cursor); 142 var iter = try open_patches.iteratorFromIndex(0); 143 while (try iter.next()) |cursor| try patches.put(allocator, try evt.readOrderKeyId(DB, cursor), null); 144 } 145 const source_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.source_to_id_set_key)) orelse break :collect; 146 const sources = try DB.SortedMap(.read_only).init(source_cursor); 147 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; 148 var iter = try sources.iterator(); 149 while (try iter.next()) |cursor| { 150 const pair = try cursor.readKeyValuePair(); 151 const branch = try pair.key_cursor.readBytesAlloc(arena.allocator(), null); 152 if (updates) |changed| { 153 var found = false; 154 for (changed) |update| { 155 if (std.mem.startsWith(u8, update.ref_name, "refs/heads/") and std.mem.eql(u8, update.ref_name["refs/heads/".len..], branch)) { 156 found = true; 157 break; 158 } 159 } 160 if (!found) continue; 161 } 162 const source = (try repo.readRef(io, .{ .kind = .head, .name = branch })) orelse { 163 std.log.warn("patch source branch missing: {s}", .{branch}); 164 continue; 165 }; 166 const ids = try DB.CountedHashSet(.read_only).init(pair.value_cursor); 167 var ids_iter = try ids.iterator(); 168 while (try ids_iter.next()) |id_cursor| { 169 const id_pair = try id_cursor.readKeyValuePair(); 170 var id: [evt.event_id_size]u8 = undefined; 171 if ((try id_pair.key_cursor.readBytes(&id)).len != id.len) return error.InvalidPatch; 172 const record = (try evt.Patch.readById(DB, repo_opts.hash, moment, &arena, &id)) orelse continue; 173 if (record.removed or record.event.status.kind() != .open) continue; 174 if (conflicts) |map| if (try map.getCursor(&evt.orderKeyDesc(record.created_order, &id)) != null) continue; 175 if (record.event.revision) |revision| if (std.mem.eql(u8, revision.source_oid, &source)) continue; 176 try patches.put(allocator, id, record); 177 } 178 } 179 } 180 if (repo_opts.ProgressCtx != void) { 181 if (progress_ctx_maybe) |progress_ctx| progress_ctx.run(io, .{ .start = .{ .kind = .writing_patch, .estimated_total_items = patches.count() } }) catch {}; 182 } 183 for (patches.keys(), patches.values()) |id, record_maybe| { 184 if (record_maybe) |record| { 185 const author = evt.CommitAuthor{ .name = "haxy", .email = record.author_email orelse "user@haxy" }; 186 writeBranchPatch(host_kind, repo_kind, repo_opts, io, allocator, repo, std.fmt.bytesToHex(id, .lower), record.event, record.event, author) catch |err| { 187 std.log.warn("failed to refresh branch patch: {s}", .{@errorName(err)}); 188 }; 189 } 190 if (repo_kind == .xit and host_kind == .server) refreshMergeability(repo_opts, io, allocator, repo, id); 191 if (repo_opts.ProgressCtx != void) { 192 if (progress_ctx_maybe) |progress_ctx| progress_ctx.run(io, .{ .complete_one = .writing_patch }) catch {}; 193 } 194 } 195 if (repo_opts.ProgressCtx != void) { 196 if (progress_ctx_maybe) |progress_ctx| progress_ctx.run(io, .{ .end = .writing_patch }) catch {}; 197 } 198 } 199 200 pub const PublishInput = struct { 201 id: [evt.event_id_size * 2]u8, 202 user_id: [evt.event_id_size]u8, 203 repo_id: [evt.event_id_size]u8, 204 author: evt.CommitAuthor, 205 timestamp: u64, 206 }; 207 208 pub const EditDraftInput = struct { 209 id: [evt.event_id_size * 2]u8, 210 user_id: [evt.event_id_size]u8, 211 repo_id: [evt.event_id_size]u8, 212 title: []const u8, 213 tags: []const u8, 214 description: []const u8, 215 target_branch: []const u8, 216 author: evt.CommitAuthor, 217 timestamp: u64, 218 }; 219 220 pub const MergeRevision = enum { squash, source }; 221 222 pub const Mergeability = struct { 223 pub const Status = enum { clean, conflict, unknown }; 224 225 source: Status = .unknown, 226 squash: Status = .unknown, 227 228 pub fn get(self: Mergeability, revision: MergeRevision) Status { 229 return switch (revision) { 230 .source => self.source, 231 .squash => self.squash, 232 }; 233 } 234 235 pub fn status(self: Mergeability) Status { 236 if (self.source == .clean or self.squash == .clean) return .clean; 237 if (self.source == .conflict and self.squash == .conflict) return .conflict; 238 return .unknown; 239 } 240 }; 241 242 const mergeability_key = "patch-id->mergeability"; 243 244 const MergeCheck = struct { 245 revision: evt.Patch.Revision, 246 target_branch: []const u8, 247 target_oid: []const u8, 248 result: Mergeability = .{}, 249 250 fn matches(self: MergeCheck, patch: evt.Patch, target_oid: []const u8) bool { 251 return evt.fieldEqual(?evt.Patch.Revision, self.revision, patch.revision) and 252 std.mem.eql(u8, self.target_branch, patch.target_branch) and 253 std.mem.eql(u8, self.target_oid, target_oid); 254 } 255 }; 256 257 fn readMergeCheck( 258 comptime repo_opts: rp.RepoOpts(.xit), 259 moment: evt.EventDB(repo_opts.hash).HashMap(.read_only), 260 arena: *std.heap.ArenaAllocator, 261 id: *const [evt.event_id_size]u8, 262 ) !?MergeCheck { 263 const DB = evt.EventDB(repo_opts.hash); 264 const map_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, mergeability_key)) orelse return null; 265 const map = try DB.HashMap(.read_only).init(map_cursor); 266 const cursor = try map.getCursor(hash.hashInt(repo_opts.hash, id)) orelse return null; 267 return evt.read(MergeCheck, DB, repo_opts.hash, arena, try DB.HashMap(.read_only).init(cursor)) catch |err| switch (err) { 268 error.InvalidEnumTag => return null, 269 else => return err, 270 }; 271 } 272 273 // reads never run a merge or refresh the cache 274 pub fn readMergeability( 275 comptime repo_opts: rp.RepoOpts(.xit), 276 state: rp.Repo(.xit, repo_opts).State(.read_only), 277 io: std.Io, 278 arena: *std.heap.ArenaAllocator, 279 id: *const [evt.event_id_size]u8, 280 patch: evt.Patch, 281 ) !Mergeability { 282 if (patch.source_branch != null and !try branchCurrent(repo_opts, state, io, arena, patch)) return .{}; 283 const target_oid = try rf.readRecur(.xit, repo_opts, state, io, .{ .ref = .{ .kind = .head, .name = patch.target_branch } }) orelse return .{}; 284 const cached = try readMergeCheck(repo_opts, state.extra.moment.*, arena, id) orelse return .{}; 285 return if (cached.matches(patch, &target_oid)) cached.result else .{}; 286 } 287 288 // refresh after the caller's transaction and locks have finished. a check 289 // failure must not turn an accepted push or event into a failed operation. 290 pub fn refreshMergeability( 291 comptime repo_opts: rp.RepoOpts(.xit), 292 io: std.Io, 293 allocator: std.mem.Allocator, 294 target_repo: *rp.Repo(.xit, repo_opts), 295 id_maybe: ?[evt.event_id_size]u8, 296 ) void { 297 refreshMergeChecks(repo_opts, io, allocator, target_repo, id_maybe) catch |err| { 298 std.log.warn("failed to refresh mergeability: {s}", .{@errorName(err)}); 299 }; 300 } 301 302 fn refreshMergeChecks( 303 comptime repo_opts: rp.RepoOpts(.xit), 304 io: std.Io, 305 allocator: std.mem.Allocator, 306 target_repo: *rp.Repo(.xit, repo_opts), 307 id_maybe: ?[evt.event_id_size]u8, 308 ) !void { 309 if (id_maybe) |id| return refreshMergeCheck(repo_opts, io, allocator, target_repo, &id); 310 const DB = evt.EventDB(repo_opts.hash); 311 const moment = evt.currentMoment(repo_opts, target_repo) catch |err| switch (err) { 312 error.NotFound => return, 313 else => return err, 314 }; 315 const statuses_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.status_to_id_set_key)) orelse return; 316 const statuses = try DB.SortedMap(.read_only).init(statuses_cursor); 317 const open_cursor = try statuses.getCursor("open") orelse return; 318 const open = try DB.SortedSet(.read_only).init(open_cursor); 319 var ids: std.ArrayList([evt.event_id_size]u8) = .empty; 320 defer ids.deinit(allocator); 321 var iter = try open.iteratorFromIndex(0); 322 while (try iter.next()) |cursor| try ids.append(allocator, try evt.readOrderKeyId(DB, cursor)); 323 for (ids.items) |id| refreshMergeability(repo_opts, io, allocator, target_repo, id); 324 } 325 326 fn refreshMergeCheck( 327 comptime repo_opts: rp.RepoOpts(.xit), 328 io: std.Io, 329 allocator: std.mem.Allocator, 330 target_repo: *rp.Repo(.xit, repo_opts), 331 id: *const [evt.event_id_size]u8, 332 ) !void { 333 const Repo = rp.Repo(.xit, repo_opts); 334 const DB = Repo.DB; 335 var arena = std.heap.ArenaAllocator.init(allocator); 336 defer arena.deinit(); 337 const initial = try evt.Patch.readFromRepo(.xit, repo_opts, io, allocator, &arena, target_repo, id); 338 if (initial) |record| if (record.removed or record.event.status.kind() != .open) return; 339 const repo_root_path = std.fs.path.dirname(target_repo.core.work_path) orelse "."; 340 const path = try fork.forkPath(arena.allocator(), repo_root_path, &std.fmt.bytesToHex(id.*, .lower)); 341 const branch_backed = if (initial) |record| record.event.source_branch != null else false; 342 var fork_repo_maybe: ?Repo = if (branch_backed) null else Repo.open(io, allocator, .{ .path = path, .require_repo_root = true }) catch |err| blk: { 343 std.log.warn("mergeability fork unavailable: {s}", .{@errorName(err)}); 344 break :blk null; 345 }; 346 defer if (fork_repo_maybe) |*repo| repo.deinit(io, allocator); 347 if (fork_repo_maybe) |*repo| try repo.core.db_file.lock(io, .shared); 348 defer if (fork_repo_maybe) |*repo| repo.core.db_file.unlock(io); 349 try target_repo.core.db_file.lock(io, .exclusive); 350 defer target_repo.core.db_file.unlock(io); 351 352 var target_moment = try target_repo.core.latestMoment(); 353 const target_state = Repo.State(.read_only){ .core = &target_repo.core, .extra = .{ .moment = &target_moment } }; 354 const target_events = evt.currentMomentFromRepoMoment(repo_opts.hash, target_moment) catch |err| switch (err) { 355 error.NotFound => null, 356 else => return err, 357 }; 358 var record = if (target_events) |events| try evt.Patch.readById(DB, repo_opts.hash, events, &arena, id) else null; 359 var newest: ?evt.PatchRev.WithId = null; 360 if (fork_repo_maybe) |*repo| { 361 if (evt.currentMoment(repo_opts, repo)) |events| { 362 newest = evt.PatchRev.readNewest(DB, repo_opts.hash, events, &arena) catch |err| blk: { 363 std.log.warn("mergeability revision unavailable: {s}", .{@errorName(err)}); 364 break :blk null; 365 }; 366 if (record == null) record = try evt.Patch.readById(DB, repo_opts.hash, events, &arena, id); 367 } else |err| std.log.warn("mergeability revision unavailable: {s}", .{@errorName(err)}); 368 } 369 const patch_record = record orelse return; 370 if (patch_record.removed or patch_record.event.status.kind() != .open) return; 371 const patch = patch_record.event; 372 const revision = patch.revision orelse return; 373 const target_oid = try rf.readRecur(.xit, repo_opts, target_state, io, .{ .ref = .{ .kind = .head, .name = patch.target_branch } }); 374 var check = MergeCheck{ .revision = revision, .target_branch = patch.target_branch, .target_oid = if (target_oid) |*oid| oid else "" }; 375 const cached = try readMergeCheck(repo_opts, target_moment, &arena, id); 376 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; 377 if (current and target_oid != null) { 378 if (cached) |value| if (value.matches(patch, check.target_oid) and value.result.source != .unknown and value.result.squash != .unknown) return; 379 const Ctx = struct { 380 repo: *Repo, 381 fork_repo: ?*Repo, 382 io: std.Io, 383 allocator: std.mem.Allocator, 384 check: *MergeCheck, 385 386 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 387 var moment = try DB.HashMap(.read_write).init(cursor.*); 388 const state = Repo.State(.read_write){ .core = &ctx.repo.core, .extra = .{ .moment = &moment } }; 389 const source = try oidArray(repo_opts.hash, ctx.check.revision.source_oid); 390 const squash = try oidArray(repo_opts.hash, ctx.check.revision.squash_oid); 391 if (ctx.fork_repo) |repo| { 392 var fork_moment = try repo.core.latestMoment(); 393 const fork_state = Repo.State(.read_only){ .core = &repo.core, .extra = .{ .moment = &fork_moment } }; 394 var objects = try obj.ObjectIterator(.xit, repo_opts).init(fork_state, ctx.io, ctx.allocator, .{ .kind = .all }); 395 defer objects.deinit(); 396 try objects.include(&source); 397 try objects.include(&squash); 398 try obj.copyFromObjectIterator(.xit, repo_opts, state, .xit, repo_opts, &objects, ctx.io, null); 399 } 400 const target = rf.Ref{ .kind = .head, .name = ctx.check.target_branch }; 401 ctx.check.result.source = checkCommit(repo_opts, state, ctx.io, ctx.allocator, &source, target); 402 ctx.check.result.squash = checkCommit(repo_opts, state, ctx.io, ctx.allocator, &squash, target); 403 return error.CancelTransaction; 404 } 405 }; 406 const history = try DB.ArrayList(.read_write).init(target_repo.core.db.rootCursor()); 407 history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ 408 .repo = target_repo, 409 .fork_repo = if (fork_repo_maybe) |*repo| repo else null, 410 .io = io, 411 .allocator = allocator, 412 .check = &check, 413 }) catch |err| switch (err) { 414 error.CancelTransaction => {}, 415 else => std.log.warn("mergeability check failed: {s}", .{@errorName(err)}), 416 }; 417 } 418 if (cached) |value| if (evt.fieldEqual(MergeCheck, value, check)) return; 419 const Save = struct { 420 id: [evt.event_id_size]u8, 421 check: MergeCheck, 422 423 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 424 const moment = try DB.HashMap(.read_write).init(cursor.*); 425 const map = try DB.HashMap(.read_write).init(try moment.putCursor(hash.hashInt(repo_opts.hash, mergeability_key))); 426 const entry = try DB.HashMap(.read_write).init(try map.putCursor(hash.hashInt(repo_opts.hash, &ctx.id))); 427 try evt.upsert(MergeCheck, DB, repo_opts.hash, entry, ctx.check); 428 } 429 }; 430 const history = try DB.ArrayList(.read_write).init(target_repo.core.db.rootCursor()); 431 try history.appendContext(.{ .slot = try history.getSlot(-1) }, Save{ .id = id.*, .check = check }); 432 } 433 434 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 { 435 const branch = patch.source_branch orelse return false; 436 const selected = patch.revision orelse return false; 437 const oid = (try rf.readRecur(.xit, repo_opts, state, io, .{ .ref = .{ .kind = .head, .name = branch } })) orelse return false; 438 if (!std.mem.eql(u8, &oid, selected.source_oid)) return false; 439 const events = try evt.currentMomentFromRepoMoment(repo_opts.hash, state.extra.moment.*); 440 const revision = (try evt.PatchRev.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, events, arena, &try evt.parseEventId(&selected.id))) orelse return false; 441 return selected.matches(revision); 442 } 443 444 fn oidArray(comptime hash_kind: hash.HashKind, oid: []const u8) ![hash.hexLen(hash_kind)]u8 { 445 try evt.PatchRev.validateOid(hash_kind, oid); 446 var result: [hash.hexLen(hash_kind)]u8 = undefined; 447 @memcpy(&result, oid); 448 return result; 449 } 450 451 fn checkCommit( 452 comptime repo_opts: rp.RepoOpts(.xit), 453 state: rp.Repo(.xit, repo_opts).State(.read_write), 454 io: std.Io, 455 allocator: std.mem.Allocator, 456 oid: *const [hash.hexLen(repo_opts.hash)]u8, 457 target: rf.Ref, 458 ) Mergeability.Status { 459 var result = mrg.Merge(.xit, repo_opts).init(state, io, allocator, .{ 460 .kind = .full, 461 .action = .{ .new = .{ .source = &.{.{ .oid = oid }}, .algo = .patch } }, 462 .dry_run = true, 463 }, target, null) catch |err| { 464 std.log.warn("mergeability check failed: {s}", .{@errorName(err)}); 465 return .unknown; 466 }; 467 defer result.deinit(); 468 return switch (result.result) { 469 .conflict => .conflict, 470 .clean, .success, .nothing, .fast_forward => .clean, 471 }; 472 } 473 474 pub const MergeInput = struct { 475 id: [evt.event_id_size * 2]u8, 476 revision: MergeRevision, 477 author: evt.CommitAuthor, 478 timestamp: u64, 479 }; 480 481 pub fn publish( 482 comptime repo_opts: rp.RepoOpts(.xit), 483 io: std.Io, 484 allocator: std.mem.Allocator, 485 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), 486 target_repo: *rp.Repo(.xit, repo_opts), 487 fork_path: []const u8, 488 input: PublishInput, 489 ) !void { 490 const patch_id = try evt.parseEventId(&input.id); 491 var arena = std.heap.ArenaAllocator.init(allocator); 492 defer arena.deinit(); 493 494 const admin_moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); 495 const fork_record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &arena, &patch_id)) orelse return error.InvalidPatchDraft; 496 if (fork_record.removed or 497 !std.mem.eql(u8, fork_record.event.user_id, &input.user_id) or 498 !std.mem.eql(u8, fork_record.event.repo_id, &input.repo_id)) return error.InvalidPatchDraft; 499 const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &arena, &input.user_id)) orelse return error.InvalidPatchDraft; 500 if (user.removed or 501 !std.mem.eql(u8, user.event.name, input.author.name) or 502 !std.mem.eql(u8, user.event.email, input.author.email)) return error.InvalidPatchDraft; 503 504 var fork_repo = rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = fork_path }) catch return error.PatchDataUnavailable; 505 defer fork_repo.deinit(io, allocator); 506 const fork_moment = evt.currentMoment(repo_opts, &fork_repo) catch return error.PatchDataUnavailable; 507 const local_patch = (try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena, &patch_id)) orelse return error.PatchDataUnavailable; 508 509 const target_moment = evt.currentMoment(repo_opts, target_repo) catch |err| switch (err) { 510 error.NotFound => null, 511 else => |other| return other, 512 }; 513 const existing = if (target_moment) |moment| 514 try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, moment, &arena, &patch_id) 515 else 516 null; 517 if (existing) |record| { 518 if (record.removed or !std.mem.eql(u8, record.author_email orelse "", input.author.email)) return error.InvalidPatch; 519 } 520 if (local_patch.removed) { 521 if (existing == null) return error.PatchDataUnavailable; 522 } else { 523 var patch = (existing orelse local_patch).event; 524 if (local_patch.event.revision) |selected| { 525 const revision_id = try evt.parseEventId(&selected.id); 526 const revision = (try evt.PatchRev.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena, &revision_id)) orelse return error.PatchNotPushed; 527 if (!selected.matches(revision)) return error.PatchNotPushed; 528 patch.revision = selected; 529 } 530 if (existing == null) patch.status = .open; 531 532 try evt.consume(.server, .repo, .xit, repo_opts, io, allocator, target_repo, evt.events_ref, &.{.{ 533 .id = input.id, 534 .timestamp = input.timestamp, 535 .author = input.author, 536 .event = .{ .patch = patch }, 537 }}); 538 539 try evt.consume(.server, .fork, .xit, repo_opts, io, allocator, &fork_repo, evt.events_ref, &.{.{ 540 .id = input.id, 541 .timestamp = input.timestamp, 542 .author = input.author, 543 .event = .{ .patch = null }, 544 }}); 545 } 546 547 if (fork_record.event.stage == .draft) { 548 var published = fork_record.event; 549 published.stage = .publish; 550 try evt.consume(.server, .admin, .xit, evt.admin_repo_opts, io, allocator, admin_repo, evt.events_ref, &.{.{ 551 .id = input.id, 552 .timestamp = input.timestamp, 553 .author = input.author, 554 .event = .{ .fork = published }, 555 }}); 556 } 557 } 558 559 pub fn editDraft( 560 comptime repo_opts: rp.RepoOpts(.xit), 561 io: std.Io, 562 allocator: std.mem.Allocator, 563 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), 564 fork_path: []const u8, 565 input: EditDraftInput, 566 ) !bool { 567 if (!evt.Patch.fieldsValid(input.title, input.tags)) return error.InvalidFields; 568 if (!evt.Patch.branchValid(input.target_branch)) return error.InvalidTargetBranch; 569 const patch_id = try evt.parseEventId(&input.id); 570 var arena = std.heap.ArenaAllocator.init(allocator); 571 defer arena.deinit(); 572 573 const admin_moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); 574 const fork_record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &arena, &patch_id)) orelse return false; 575 if (fork_record.event.stage != .draft) return false; 576 if (fork_record.removed or 577 !std.mem.eql(u8, fork_record.event.user_id, &input.user_id) or 578 !std.mem.eql(u8, fork_record.event.repo_id, &input.repo_id)) return error.InvalidPatchDraft; 579 const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &arena, &input.user_id)) orelse return error.InvalidPatchDraft; 580 if (user.removed or 581 !std.mem.eql(u8, user.event.name, input.author.name) or 582 !std.mem.eql(u8, user.event.email, input.author.email)) return error.InvalidPatchDraft; 583 584 var fork_repo = rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }) catch return error.PatchDataUnavailable; 585 defer fork_repo.deinit(io, allocator); 586 const fork_moment = evt.currentMoment(repo_opts, &fork_repo) catch return error.PatchDataUnavailable; 587 const record = (try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena, &patch_id)) orelse return error.PatchDataUnavailable; 588 if (record.removed) return error.PatchDataUnavailable; 589 590 var patch = record.event; 591 patch.title = input.title; 592 patch.tags = input.tags; 593 patch.description = input.description; 594 if (!std.mem.eql(u8, patch.target_branch, input.target_branch)) patch.revision = null; 595 patch.target_branch = input.target_branch; 596 try evt.consume(.server, .fork, .xit, repo_opts, io, allocator, &fork_repo, evt.events_ref, &.{.{ 597 .id = input.id, 598 .timestamp = input.timestamp, 599 .author = input.author, 600 .event = .{ .patch = patch }, 601 }}); 602 return true; 603 } 604 605 // merge the selected patch revision into its target branch 606 pub fn merge( 607 comptime repo_opts: rp.RepoOpts(.xit), 608 io: std.Io, 609 allocator: std.mem.Allocator, 610 repo_root_path: []const u8, 611 target_repo: *rp.Repo(.xit, repo_opts), 612 input: MergeInput, 613 ) !void { 614 const patch_id = try evt.parseEventId(&input.id); 615 errdefer |err| { 616 if (err == error.PatchDataUnavailable or err == error.PatchOutOfDate) { 617 refreshMergeability(repo_opts, io, allocator, target_repo, patch_id); 618 } 619 } 620 var arena = std.heap.ArenaAllocator.init(allocator); 621 defer arena.deinit(); 622 623 // validate the target patch 624 const target_moment = try evt.currentMoment(repo_opts, target_repo); 625 const patch_record = (try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, target_moment, &arena, &patch_id)) orelse return error.InvalidPatch; 626 if (patch_record.removed) return error.InvalidPatch; 627 switch (patch_record.event.status) { 628 .open => {}, 629 .closed => return error.PatchClosed, 630 .merged => return error.PatchAlreadyMerged, 631 } 632 const selected = patch_record.event.revision orelse return error.PatchNotPushed; 633 const target_ref = rf.Ref{ .kind = .head, .name = patch_record.event.target_branch }; 634 635 // open the fork 636 const fork_path = try fork.forkPath(arena.allocator(), repo_root_path, &input.id); 637 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; 638 defer if (fork_repo_maybe) |*repo| repo.deinit(io, allocator); 639 // select the commit and merge identity 640 const merge_oid = oidArray(repo_opts.hash, switch (input.revision) { 641 .squash => selected.squash_oid, 642 .source => selected.source_oid, 643 }) catch return error.InvalidPatch; 644 const identity = try std.fmt.allocPrint(arena.allocator(), "{s} <{s}>", .{ input.author.name, input.author.email }); 645 646 // merge the code and events in one target transaction 647 { 648 const DB = rp.Repo(.xit, repo_opts).DB; 649 const State = rp.Repo(.xit, repo_opts).State; 650 const Ctx = struct { 651 core: *rp.Repo(.xit, repo_opts).Core, 652 fork_repo: ?*rp.Repo(.xit, repo_opts), 653 io: std.Io, 654 allocator: std.mem.Allocator, 655 patch_id: [evt.event_id_size]u8, 656 expected_patch: evt.Patch, 657 target_ref: rf.Ref, 658 merge_oid: [hash.hexLen(repo_opts.hash)]u8, 659 revision: MergeRevision, 660 identity: []const u8, 661 author: evt.CommitAuthor, 662 timestamp: u64, 663 664 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 665 var moment = try DB.HashMap(.read_write).init(cursor.*); 666 const state = State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 667 668 var check_arena = std.heap.ArenaAllocator.init(ctx.allocator); 669 defer check_arena.deinit(); 670 671 // require the fork's newest revision while both repos are locked 672 const selected_revision = ctx.expected_patch.revision orelse unreachable; 673 const revision_id = try evt.parseEventId(&selected_revision.id); 674 if (ctx.fork_repo) |repo| { 675 const fork_moment = try evt.currentMoment(repo_opts, repo); 676 const newest = (try evt.PatchRev.readNewest(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &check_arena)) orelse return error.PatchDataUnavailable; 677 if (!std.mem.eql(u8, &newest.id, &revision_id) or !selected_revision.matches(newest.record)) return error.PatchOutOfDate; 678 } else if (!try branchCurrent(repo_opts, state.readOnly(), ctx.io, &check_arena, ctx.expected_patch)) return error.PatchOutOfDate; 679 680 const availability = try readMergeability(repo_opts, state.readOnly(), ctx.io, &check_arena, &ctx.patch_id, ctx.expected_patch); 681 switch (availability.get(ctx.revision)) { 682 .clean => {}, 683 .conflict => return error.MergeConflict, 684 .unknown => return error.MergeCheckUnavailable, 685 } 686 687 // copy the selected commit and its dependencies 688 if (ctx.fork_repo) |repo| { 689 var fork_repo_moment = try repo.core.latestMoment(); 690 const fork_state = State(.read_only){ .core = &repo.core, .extra = .{ .moment = &fork_repo_moment } }; 691 var objects = try obj.ObjectIterator(.xit, repo_opts).init(fork_state, ctx.io, ctx.allocator, .{ .kind = .all }); 692 defer objects.deinit(); 693 try objects.include(&ctx.merge_oid); 694 try obj.copyFromObjectIterator(.xit, repo_opts, state, .xit, repo_opts, &objects, ctx.io, null); 695 } 696 697 // import the revision and mark the patch merged 698 if (!try importMergedRevision(repo_opts, state, &ctx.core.db, &moment, ctx.io, ctx.allocator, ctx.fork_repo, &ctx.patch_id, &ctx.merge_oid, ctx.expected_patch, ctx.author, ctx.timestamp)) return error.PatchOutOfDate; 699 700 // merge the selected commit 701 { 702 var merge_result = try mrg.Merge(.xit, repo_opts).init(state, ctx.io, ctx.allocator, .{ 703 .kind = .full, 704 .action = .{ .new = .{ .source = &.{.{ .oid = &ctx.merge_oid }}, .algo = .patch } }, 705 .commit_metadata = .{ 706 .author = ctx.identity, 707 .committer = ctx.identity, 708 .message = ctx.expected_patch.title, 709 .timestamp = ctx.timestamp, 710 }, 711 }, ctx.target_ref, null); 712 defer merge_result.deinit(); 713 switch (merge_result.result) { 714 .conflict => return error.MergeConflict, 715 .success, .nothing, .fast_forward => {}, 716 .clean => unreachable, 717 } 718 } 719 720 try xit.undo.writeMessage(repo_opts, state, .{ .custom = "merge patch" }); 721 } 722 }; 723 724 if (fork_repo_maybe) |*repo| try repo.core.db_file.lock(io, .shared); 725 defer if (fork_repo_maybe) |*repo| repo.core.db_file.unlock(io); 726 try target_repo.core.db_file.lock(io, .exclusive); 727 defer target_repo.core.db_file.unlock(io); 728 729 const history = try DB.ArrayList(.read_write).init(target_repo.core.db.rootCursor()); 730 try history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ 731 .core = &target_repo.core, 732 .fork_repo = if (fork_repo_maybe) |*repo| repo else null, 733 .io = io, 734 .allocator = allocator, 735 .patch_id = patch_id, 736 .expected_patch = patch_record.event, 737 .target_ref = target_ref, 738 .merge_oid = merge_oid, 739 .revision = input.revision, 740 .identity = identity, 741 .author = input.author, 742 .timestamp = input.timestamp, 743 }); 744 } 745 refreshMergeability(repo_opts, io, allocator, target_repo, null); 746 } 747 748 // merge a published patch and remove its fork 749 pub fn mergeAndRemoveFork( 750 comptime repo_opts: rp.RepoOpts(.xit), 751 io: std.Io, 752 allocator: std.mem.Allocator, 753 repo_root_path: []const u8, 754 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), 755 target_repo: *rp.Repo(.xit, repo_opts), 756 input: MergeInput, 757 ) !void { 758 try merge(repo_opts, io, allocator, repo_root_path, target_repo, input); 759 var arena = std.heap.ArenaAllocator.init(allocator); 760 defer arena.deinit(); 761 const record = (try evt.Patch.readFromRepo(.xit, repo_opts, io, allocator, &arena, target_repo, &try evt.parseEventId(&input.id))) orelse return error.InvalidPatch; 762 if (record.event.source_branch == null) try fork.remove(io, allocator, repo_root_path, admin_repo, &input.id, null, input.author); 763 } 764 765 fn commitAuthor(line: []const u8) !evt.CommitAuthor { 766 const open = std.mem.lastIndexOfScalar(u8, line, '<') orelse return error.AuthorNotFound; 767 const close = std.mem.indexOfScalarPos(u8, line, open + 1, '>') orelse return error.AuthorNotFound; 768 const name = std.mem.trimEnd(u8, line[0..open], " "); 769 if (name.len == 0 or close == open + 1) return error.AuthorNotFound; 770 return .{ .name = name, .email = line[open + 1 .. close] }; 771 } 772 773 // import a revision after detecting a client-side merge 774 fn importMerged( 775 comptime repo_opts: rp.RepoOpts(.xit), 776 state: rp.Repo(.xit, repo_opts).State(.read_write), 777 db: *evt.EventDB(repo_opts.hash), 778 moment: *evt.EventDB(repo_opts.hash).HashMap(.read_write), 779 io: std.Io, 780 allocator: std.mem.Allocator, 781 repo_root_path: []const u8, 782 patch_id: *const [evt.event_id_size]u8, 783 merged_oid: *const [hash.hexLen(repo_opts.hash)]u8, 784 ) !void { 785 var arena = std.heap.ArenaAllocator.init(allocator); 786 defer arena.deinit(); 787 788 const patch_hex = std.fmt.bytesToHex(patch_id.*, .lower); 789 const fork_path = try fork.forkPath(arena.allocator(), repo_root_path, &patch_hex); 790 const events = try evt.currentMomentFromRepoMoment(repo_opts.hash, moment.readOnly()); 791 const record = (try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, events, &arena, patch_id)) orelse return; 792 var fork_repo_maybe: ?rp.Repo(.xit, repo_opts) = if (record.event.source_branch != null) null else try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }); 793 defer if (fork_repo_maybe) |*repo| repo.deinit(io, allocator); 794 795 _ = try importMergedRevision(repo_opts, state, db, moment, io, allocator, if (fork_repo_maybe) |*repo| repo else null, patch_id, merged_oid, null, null, 0); 796 } 797 798 // import the revision and mark its patch merged in the current transaction 799 fn importMergedRevision( 800 comptime repo_opts: rp.RepoOpts(.xit), 801 state: rp.Repo(.xit, repo_opts).State(.read_write), 802 db: *evt.EventDB(repo_opts.hash), 803 moment: *evt.EventDB(repo_opts.hash).HashMap(.read_write), 804 io: std.Io, 805 allocator: std.mem.Allocator, 806 fork_repo: ?*rp.Repo(.xit, repo_opts), 807 patch_id: *const [evt.event_id_size]u8, 808 merged_oid: *const [hash.hexLen(repo_opts.hash)]u8, 809 expected_patch: ?evt.Patch, 810 merged_author: ?evt.CommitAuthor, 811 merged_timestamp: u64, 812 ) !bool { 813 const DB = evt.EventDB(repo_opts.hash); 814 var arena = std.heap.ArenaAllocator.init(allocator); 815 defer arena.deinit(); 816 817 const haxy_moment = try evt.currentMomentFromRepoMoment(repo_opts.hash, moment.readOnly()); 818 const patch_record = (try evt.Patch.readById(DB, repo_opts.hash, haxy_moment, &arena, patch_id)) orelse return false; 819 const selected = patch_record.event.revision orelse return false; 820 if (patch_record.removed or patch_record.event.status.kind() == .merged) return false; 821 if (expected_patch) |expected| { 822 if (!evt.fieldEqual(evt.Patch, expected, patch_record.event)) return false; 823 if (try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.conflicts_key))) |cursor| { 824 const conflicts = try DB.SortedMap(.read_only).init(cursor); 825 if (try conflicts.getCursor(&evt.orderKeyDesc(patch_record.created_order, patch_id)) != null) return false; 826 } 827 } 828 const revision_id = try evt.parseEventId(&selected.id); 829 if (!selected.includesOid(merged_oid)) return false; 830 831 const current_revision = try evt.PatchRev.readById(DB, repo_opts.hash, haxy_moment, &arena, &revision_id); 832 const revision = (if (fork_repo) |repo| 833 try evt.PatchRev.readById(DB, repo_opts.hash, try evt.currentMoment(repo_opts, repo), &arena, &revision_id) 834 else 835 current_revision) orelse return false; 836 if (!selected.matches(revision)) return false; 837 838 var fork_repo_moment = if (fork_repo) |repo| try repo.core.latestMoment() else moment.readOnly(); 839 const fork_state = rp.Repo(.xit, repo_opts).State(.read_only){ .core = if (fork_repo) |repo| &repo.core else state.core, .extra = .{ .moment = &fork_repo_moment } }; 840 var event_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 841 if (revision.event_oid.len != event_oid.len) return false; 842 @memcpy(&event_oid, revision.event_oid); 843 var event_object = try obj.Object(.xit, repo_opts).initCommit(fork_state, io, allocator, &event_oid); 844 defer event_object.deinit(); 845 const original_author = try commitAuthor(event_object.content.commit.metadata.author orelse return false); 846 847 var events: [2]evt.EventWithId = undefined; 848 var event_count: usize = 0; 849 var tree_entries: [2]evt.EventTreeEntry = undefined; 850 const needs_import = if (current_revision) |existing| existing.removed else true; 851 if (needs_import) { 852 tree_entries = .{ 853 .{ .tree = .{ .name = "base", .oid = revision.base_tree_oid } }, 854 .{ .tree = .{ .name = "head", .oid = revision.head_tree_oid } }, 855 }; 856 events[event_count] = .{ 857 .id = selected.id, 858 .timestamp = event_object.content.commit.metadata.timestamp, 859 .author = original_author, 860 .tree_entries = &tree_entries, 861 .event = .{ .patchrev = revision.event }, 862 }; 863 event_count += 1; 864 } else { 865 const existing = current_revision orelse unreachable; 866 if (!selected.matches(existing)) return error.InvalidPatch; 867 } 868 869 var patch = patch_record.event; 870 patch.status = .{ .merged = merged_oid }; 871 events[event_count] = .{ 872 .id = std.fmt.bytesToHex(patch_id.*, .lower), 873 .timestamp = if (merged_author != null) merged_timestamp else @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), 874 .author = merged_author orelse original_author, 875 .event = .{ .patch = patch }, 876 }; 877 event_count += 1; 878 879 try evt.commitEvents(.xit, repo_opts, state, io, allocator, evt.events_ref, events[0..event_count], null); 880 if (!try evt.consumeInTransaction(.repo, .xit, repo_opts, state, db, moment, io, allocator, evt.events_ref)) return error.CancelTransaction; 881 return true; 882 } 883 884 // find patches merged by commits received in a push 885 pub fn detectMerged( 886 comptime repo_opts: rp.RepoOpts(.xit), 887 state: rp.Repo(.xit, repo_opts).State(.read_write), 888 db: *evt.EventDB(repo_opts.hash), 889 moment: *evt.EventDB(repo_opts.hash).HashMap(.read_write), 890 io: std.Io, 891 allocator: std.mem.Allocator, 892 repo_root_path: []const u8, 893 updates: []const xit.net_server_receive_pack.AppliedRefUpdate, 894 error_writer: *std.Io.Writer, 895 ) !void { 896 const DB = evt.EventDB(repo_opts.hash); 897 var arena = std.heap.ArenaAllocator.init(allocator); 898 defer arena.deinit(); 899 900 const haxy_moment = try evt.currentMomentFromRepoMoment(repo_opts.hash, moment.readOnly()); 901 const revisions_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Patch.revision_to_id_set_key)) orelse return; 902 const revisions = try DB.HashMap(.read_only).init(revisions_cursor); 903 var events_ref_buffer: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; 904 const events_ref_path = try evt.events_ref.toPath(&events_ref_buffer); 905 const heads_prefix = "refs/heads/"; 906 for (updates) |update| { 907 if (!std.mem.startsWith(u8, update.ref_name, heads_prefix) or 908 std.mem.eql(u8, update.ref_name, events_ref_path) or 909 std.mem.eql(u8, update.old_oid, update.new_oid)) continue; 910 if (update.old_oid.len != hash.hexLen(repo_opts.hash) or 911 update.new_oid.len != hash.hexLen(repo_opts.hash)) return error.InvalidOid; 912 var old_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 913 var new_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 914 @memcpy(&old_oid, update.old_oid); 915 @memcpy(&new_oid, update.new_oid); 916 if (std.mem.allEqual(u8, &new_oid, '0')) continue; 917 918 var commits = try obj.ObjectIterator(.xit, repo_opts).init(state.readOnly(), io, arena.allocator(), .{ .kind = .commit }); 919 defer commits.deinit(); 920 if (!std.mem.allEqual(u8, &old_oid, '0')) try commits.exclude(&old_oid); 921 try commits.include(&new_oid); 922 var revision_key_buffer: [rf.MAX_REF_CONTENT_SIZE + 1 + hash.hexLen(repo_opts.hash)]u8 = undefined; 923 while (try commits.next(arena.allocator())) |commit| { 924 defer commit.deinit(); 925 const key = try std.fmt.bufPrint(&revision_key_buffer, "{s}\x00{s}", .{ update.ref_name[heads_prefix.len..], &commit.oid }); 926 const ids_cursor = try revisions.getCursor(hash.hashInt(repo_opts.hash, key)) orelse continue; 927 const ids = try DB.CountedHashSet(.read_only).init(ids_cursor); 928 var ids_iter = try ids.iterator(); 929 while (try ids_iter.next()) |cursor| { 930 const pair = try cursor.readKeyValuePair(); 931 var patch_id: [evt.event_id_size]u8 = undefined; 932 if ((try pair.key_cursor.readBytes(&patch_id)).len != patch_id.len) return error.InvalidPatch; 933 const patch_hex = std.fmt.bytesToHex(patch_id, .lower); 934 importMerged(repo_opts, state, db, moment, io, allocator, repo_root_path, &patch_id, &commit.oid) catch |err| { 935 serve_common.logError(io, error_writer, "failed to mark patch {s} as merged: {s}\n", .{ &patch_hex, @errorName(err) }); 936 continue; 937 }; 938 } 939 } 940 } 941 } 942