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