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