╭ fork.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 mrg = xit.merge; 8 const bch = xit.branch; 9 const rf = xit.ref; 10 11 pub const ref = rf.Ref{ .kind = .head, .name = "patch" }; 12 const ref_path = "refs/heads/patch"; 13 14 pub const Route = struct { 15 identity: []const u8, 16 id: [evt.event_id_size * 2]u8, 17 target: []const u8, 18 }; 19 20 pub fn parseRoute(route_path: []const u8) ?Route { 21 const patch_segment = "/patch:"; 22 const branch_segment = "/branch:"; 23 const patch_start = std.mem.indexOf(u8, route_path, patch_segment) orelse return null; 24 const branch_start = std.mem.indexOfPos(u8, route_path, patch_start + patch_segment.len, branch_segment) orelse return null; 25 const identity = route_path[0..patch_start]; 26 const id_text = route_path[patch_start + patch_segment.len .. branch_start]; 27 const target = route_path[branch_start + branch_segment.len ..]; 28 if (identity.len == 0 or target.len == 0 or id_text.len != evt.event_id_size * 2) return null; 29 const id_bytes = evt.parseEventId(id_text) catch return null; 30 return .{ .identity = identity, .id = std.fmt.bytesToHex(id_bytes, .lower), .target = target }; 31 } 32 33 pub fn forkPath(allocator: std.mem.Allocator, repo_root_path: []const u8, id: []const u8) ![]u8 { 34 _ = try evt.parseEventId(id); 35 return try std.fs.path.join(allocator, &.{ std.fs.path.dirname(repo_root_path) orelse ".", "forks", id }); 36 } 37 38 pub const CreateInput = struct { 39 id: [evt.event_id_size * 2]u8, 40 user_id: [evt.event_id_size]u8, 41 repo_id: [evt.event_id_size]u8, 42 title: []const u8, 43 description: []const u8, 44 tags: []const u8, 45 author: evt.CommitAuthor, 46 timestamp: u64, 47 }; 48 49 pub fn create( 50 comptime repo_opts: rp.RepoOpts(.xit), 51 io: std.Io, 52 allocator: std.mem.Allocator, 53 repo_root_path: []const u8, 54 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), 55 input: CreateInput, 56 ) ![]u8 { 57 if (!evt.Patch.fieldsValid(input.title, input.tags)) return error.InvalidPatch; 58 59 // get the fork id and path 60 const fork_id = try evt.parseEventId(&input.id); 61 const fork_path = try forkPath(allocator, repo_root_path, &input.id); 62 errdefer allocator.free(fork_path); 63 64 // make sure the fork id doesn't already exist 65 var arena = std.heap.ArenaAllocator.init(allocator); 66 defer arena.deinit(); 67 const existing = if (evt.currentMoment(evt.admin_repo_opts, admin_repo)) |moment| 68 try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, &fork_id) 69 else |err| switch (err) { 70 error.NotFound => null, 71 else => |other| return other, 72 }; 73 if (existing != null) return error.InvalidPatchDraft; 74 75 // get the target repo 76 const target_id = std.fmt.bytesToHex(input.repo_id, .lower); 77 const target_path = try std.fs.path.join(allocator, &.{ repo_root_path, &target_id }); 78 defer allocator.free(target_path); 79 var target_repo = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path, .require_repo_root = true }); 80 defer target_repo.deinit(io, allocator); 81 82 // create the fork repo dir 83 const forks_path = std.fs.path.dirname(fork_path) orelse return error.InvalidPatchDraft; 84 var forks_dir = try std.Io.Dir.cwd().createDirPathOpen(io, forks_path, .{}); 85 defer forks_dir.close(io); 86 const fork_name = std.fs.path.basename(fork_path); 87 forks_dir.createDir(io, fork_name, .default_dir) catch |err| switch (err) { 88 error.PathAlreadyExists => return error.InvalidPatchDraft, 89 else => |other| return other, 90 }; 91 errdefer forks_dir.deleteTree(io, fork_name) catch {}; 92 var fork_dir = try forks_dir.openDir(io, fork_name, .{}); 93 defer fork_dir.close(io); 94 var fork_repo_dir = try fork_dir.createDirPathOpen(io, ".xit", .{}); 95 defer fork_repo_dir.close(io); 96 97 // copy the target repo into the fork repo dir 98 { 99 try target_repo.core.db_file.lock(io, .shared); 100 defer target_repo.core.db_file.unlock(io); 101 try target_repo.core.chunk_store_file.lock(io, .shared); 102 defer target_repo.core.chunk_store_file.unlock(io); 103 104 // TODO: use reflinks here when the filesystem supports them 105 for ([_]struct { file: std.Io.File, name: []const u8 }{ 106 .{ .file = target_repo.core.db_file, .name = "db" }, 107 .{ .file = target_repo.core.chunk_store_file, .name = "chunks" }, 108 }) |source| { 109 const destination = try fork_repo_dir.createFile(io, source.name, .{ .exclusive = true, .read = true }); 110 defer destination.close(io); 111 var read_buffer: [64 * 1024]u8 = undefined; 112 var write_buffer: [64 * 1024]u8 = undefined; 113 var reader = source.file.reader(io, &read_buffer); 114 var writer = destination.writer(io, &write_buffer); 115 _ = try reader.interface.streamRemaining(&writer.interface); 116 try writer.interface.flush(); 117 try destination.sync(io); 118 } 119 } 120 121 var fork_repo = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }); 122 defer fork_repo.deinit(io, allocator); 123 124 // clear the haxy state in the fork repo and create the patch branch 125 { 126 const DB = rp.Repo(.xit, repo_opts).DB; 127 const State = rp.Repo(.xit, repo_opts).State; 128 const Ctx = struct { 129 core: *rp.Repo(.xit, repo_opts).Core, 130 io: std.Io, 131 132 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 133 var moment = try DB.HashMap(.read_write).init(cursor.*); 134 const state = State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 135 const head_oid_maybe = try rf.readHeadRecurMaybe(.xit, repo_opts, state.readOnly(), ctx.io); 136 137 var path_buffer: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; 138 const events_path = try evt.events_ref.toPath(&path_buffer); 139 rf.remove(.xit, repo_opts, state, ctx.io, events_path) catch |err| switch (err) { 140 error.RefNotFound => {}, 141 else => |other| return other, 142 }; 143 const patch_path = try ref.toPath(&path_buffer); 144 rf.remove(.xit, repo_opts, state, ctx.io, patch_path) catch |err| switch (err) { 145 error.RefNotFound => {}, 146 else => |other| return other, 147 }; 148 149 _ = try moment.remove(hash.hashInt(repo_opts.hash, "haxy")); 150 _ = try moment.remove(hash.hashInt(repo_opts.hash, "haxy-last-object-id")); 151 152 if (head_oid_maybe) |*head_oid| { 153 try rf.write(.xit, repo_opts, state, ctx.io, patch_path, .{ .oid = head_oid }); 154 } else { 155 try bch.add(.xit, repo_opts, state, ctx.io, .{ .name = ref.name, .target = .none }); 156 } 157 try rf.replaceHead(.xit, repo_opts, state, ctx.io, .{ .ref = ref }); 158 try xit.undo.writeMessage(repo_opts, state, .{ .custom = "create fork" }); 159 } 160 }; 161 162 try fork_repo.core.db_file.lock(io, .exclusive); 163 defer fork_repo.core.db_file.unlock(io); 164 165 const history = try DB.ArrayList(.read_write).init(fork_repo.core.db.rootCursor()); 166 try history.appendContext( 167 .{ .slot = try history.getSlot(-1) }, 168 Ctx{ .core = &fork_repo.core, .io = io }, 169 ); 170 } 171 172 try fork_repo.addConfig(io, allocator, .{ .name = "receive.denycurrentbranch", .value = "updateinstead" }); 173 try fork_repo.addConfig(io, allocator, .{ .name = "receive.denydeletes", .value = "true" }); 174 175 // create the patch event 176 try evt.consume(.fork, .xit, repo_opts, io, allocator, &fork_repo, evt.events_ref, &.{.{ 177 .id = input.id, 178 .timestamp = input.timestamp, 179 .author = input.author, 180 .event = .{ .patch = .{ 181 .title = input.title, 182 .description = input.description, 183 .tags = input.tags, 184 } }, 185 }}); 186 187 // create the fork event 188 try evt.consume(.admin, .xit, evt.admin_repo_opts, io, allocator, admin_repo, evt.events_ref, &.{.{ 189 .id = input.id, 190 .timestamp = input.timestamp, 191 .author = input.author, 192 .event = .{ .fork = .{ 193 .user_id = &input.user_id, 194 .repo_id = &input.repo_id, 195 } }, 196 }}); 197 198 return fork_path; 199 } 200 201 pub fn receivePack( 202 comptime repo_opts: rp.RepoOpts(.xit), 203 io: std.Io, 204 allocator: std.mem.Allocator, 205 fork_repo: *rp.Repo(.xit, repo_opts), 206 target_repo: *rp.Repo(.xit, repo_opts), 207 id: *const [evt.event_id_size * 2]u8, 208 target_branch: []const u8, 209 author: evt.CommitAuthor, 210 timestamp: u64, 211 reader: *std.Io.Reader, 212 writer: *std.Io.Writer, 213 ) !void { 214 if (!rf.validateName(target_branch)) return error.InvalidTarget; 215 const patch_id = try evt.parseEventId(id); 216 var arena = std.heap.ArenaAllocator.init(allocator); 217 defer arena.deinit(); 218 219 const fork_moment = evt.currentMoment(repo_opts, fork_repo) catch return error.PatchDataUnavailable; 220 const fork_patch = (try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena, &patch_id)) orelse return error.PatchDataUnavailable; 221 222 const target_moment = evt.currentMoment(repo_opts, target_repo) catch |err| switch (err) { 223 error.NotFound => null, 224 else => |other| return other, 225 }; 226 const target_patch = if (target_moment) |moment| 227 try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, moment, &arena, &patch_id) 228 else 229 null; 230 if (fork_patch.removed and target_patch == null) return error.PatchDataUnavailable; 231 if (target_patch) |patch| { 232 if (!std.mem.eql(u8, patch.author_email orelse "", author.email)) return error.PatchDataUnavailable; 233 if (patch.event.status == .merged) return error.PatchAlreadyMerged; 234 } 235 const published = target_patch != null; 236 const title = if (target_patch) |patch| patch.event.title else fork_patch.event.title; 237 238 const target_oid = (try target_repo.readRef(io, .{ .kind = .head, .name = target_branch })) orelse return error.TargetNotFound; 239 const newest = try evt.PatchRev.readNewest(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena); 240 const target_ref = try std.fmt.allocPrint(arena.allocator(), "refs/heads/{s}", .{target_branch}); 241 var revision_id_maybe: ?[evt.event_id_size]u8 = null; 242 243 const DB = rp.Repo(.xit, repo_opts).DB; 244 const State = rp.Repo(.xit, repo_opts).State; 245 const Ctx = struct { 246 core: *rp.Repo(.xit, repo_opts).Core, 247 target_core: *rp.Repo(.xit, repo_opts).Core, 248 io: std.Io, 249 allocator: std.mem.Allocator, 250 reader: *std.Io.Reader, 251 writer: *std.Io.Writer, 252 patch_id: [evt.event_id_size * 2]u8, 253 patch: evt.Patch.Record, 254 published: bool, 255 target_oid: [hash.hexLen(repo_opts.hash)]u8, 256 target_ref: []const u8, 257 title: []const u8, 258 author: evt.CommitAuthor, 259 timestamp: u64, 260 newest: ?evt.PatchRev.WithId, 261 revision_id_maybe: *?[evt.event_id_size]u8, 262 263 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 264 var moment = try DB.HashMap(.read_write).init(cursor.*); 265 const state = State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 266 try xit.net_server_receive_pack.run(.xit, repo_opts, state, ctx.io, ctx.allocator, ctx.reader, ctx.writer, .{ .allowed_ref = ref_path }); 267 268 const source_oid = (try rf.readRecur(.xit, repo_opts, state.readOnly(), ctx.io, .{ .ref = ref })) orelse return error.CancelTransaction; 269 var target_repo_moment = try ctx.target_core.latestMoment(); 270 const target_state = State(.read_only){ .core = ctx.target_core, .extra = .{ .moment = &target_repo_moment } }; 271 var objects = try obj.ObjectIterator(.xit, repo_opts).init(target_state, ctx.io, ctx.allocator, .{ .kind = .all }); 272 defer objects.deinit(); 273 try objects.include(&ctx.target_oid); 274 try obj.copyFromObjectIterator(.xit, repo_opts, state, .xit, repo_opts, &objects, ctx.io, null); 275 276 const base_oid = try mrg.commonAncestor(.xit, repo_opts, state.readOnly(), ctx.io, ctx.allocator, &ctx.target_oid, &source_oid); 277 const existing_revision = if (ctx.newest) |latest| 278 if (std.mem.eql(u8, latest.record.event.base_oid, &base_oid) and 279 std.mem.eql(u8, latest.record.event.source_oid, &source_oid) and 280 std.mem.eql(u8, latest.record.event.target_ref, ctx.target_ref)) latest else null 281 else 282 null; 283 284 var events: [2]evt.EventWithId = undefined; 285 var event_count: usize = 0; 286 var base_tree_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 287 var head_tree_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 288 var tree_entries: [2]evt.EventTreeEntry = undefined; 289 if (existing_revision) |latest| { 290 if (ctx.published) ctx.revision_id_maybe.* = latest.id; 291 } else { 292 var base_object = try obj.Object(.xit, repo_opts).initCommit(state.readOnly(), ctx.io, ctx.allocator, &base_oid); 293 defer base_object.deinit(); 294 var source_object = try obj.Object(.xit, repo_opts).initCommit(state.readOnly(), ctx.io, ctx.allocator, &source_oid); 295 defer source_object.deinit(); 296 base_tree_oid = base_object.content.commit.tree; 297 head_tree_oid = source_object.content.commit.tree; 298 299 var revision_id: [evt.event_id_size]u8 = undefined; 300 ctx.io.random(&revision_id); 301 const revision_hex = std.fmt.bytesToHex(revision_id, .lower); 302 tree_entries = .{ 303 .{ .tree = .{ .name = "base", .oid = &base_tree_oid } }, 304 .{ .tree = .{ .name = "head", .oid = &head_tree_oid } }, 305 }; 306 events[event_count] = .{ 307 .id = revision_hex, 308 .timestamp = ctx.timestamp, 309 .author = ctx.author, 310 .tree_entries = &tree_entries, 311 .event = .{ .patchrev = .{ 312 .base_oid = &base_oid, 313 .source_oid = &source_oid, 314 .target_ref = ctx.target_ref, 315 .message = ctx.title, 316 } }, 317 }; 318 event_count += 1; 319 ctx.revision_id_maybe.* = revision_id; 320 321 if (!ctx.published) { 322 var patch = ctx.patch.event; 323 patch.patchrev_id = revision_hex; 324 events[event_count] = .{ 325 .id = ctx.patch_id, 326 .timestamp = ctx.timestamp, 327 .author = ctx.author, 328 .event = .{ .patch = patch }, 329 }; 330 event_count += 1; 331 } 332 } 333 334 if (ctx.published and !ctx.patch.removed) { 335 events[event_count] = .{ 336 .id = ctx.patch_id, 337 .timestamp = ctx.timestamp, 338 .author = ctx.author, 339 .event = .{ .patch = null }, 340 }; 341 event_count += 1; 342 } 343 if (event_count > 0) { 344 try evt.commitEvents(.xit, repo_opts, state, ctx.io, ctx.allocator, evt.events_ref, events[0..event_count], null); 345 if (!try evt.consumeInTransaction(.fork, .xit, repo_opts, state, &ctx.core.db, &moment, ctx.io, ctx.allocator, evt.events_ref)) return error.CancelTransaction; 346 } 347 try xit.undo.writeMessage(repo_opts, state, .push); 348 try ctx.core.chunk_store_file.sync(ctx.io); 349 } 350 }; 351 352 try fork_repo.core.db_file.lock(io, .exclusive); 353 defer fork_repo.core.db_file.unlock(io); 354 const history = try DB.ArrayList(.read_write).init(fork_repo.core.db.rootCursor()); 355 history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ 356 .core = &fork_repo.core, 357 .target_core = &target_repo.core, 358 .io = io, 359 .allocator = allocator, 360 .reader = reader, 361 .writer = writer, 362 .patch_id = id.*, 363 .patch = fork_patch, 364 .published = published, 365 .target_oid = target_oid, 366 .target_ref = target_ref, 367 .title = title, 368 .author = author, 369 .timestamp = timestamp, 370 .newest = newest, 371 .revision_id_maybe = &revision_id_maybe, 372 }) catch |err| switch (err) { 373 error.CancelTransaction => {}, 374 else => |other| return other, 375 }; 376 try writer.flush(); 377 378 const revision_id = revision_id_maybe orelse return; 379 if (!published) return; 380 var update_arena = std.heap.ArenaAllocator.init(allocator); 381 defer update_arena.deinit(); 382 const update_moment = evt.currentMoment(repo_opts, target_repo) catch return; 383 const published_patch = (evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, update_moment, &update_arena, &patch_id) catch return) orelse return; 384 if (published_patch.removed) return; 385 const revision_hex = std.fmt.bytesToHex(revision_id, .lower); 386 if (published_patch.event.patchrev_id) |*published_revision| { 387 if (std.mem.eql(u8, published_revision, &revision_hex)) return; 388 } 389 var patch = published_patch.event; 390 patch.patchrev_id = revision_hex; 391 evt.consume(.repo, .xit, repo_opts, io, allocator, target_repo, evt.events_ref, &.{.{ 392 .id = id.*, 393 .timestamp = timestamp, 394 .author = author, 395 .event = .{ .patch = patch }, 396 }}) catch {}; 397 } 398 399 pub const PublishInput = struct { 400 id: [evt.event_id_size * 2]u8, 401 user_id: [evt.event_id_size]u8, 402 repo_id: [evt.event_id_size]u8, 403 author: evt.CommitAuthor, 404 timestamp: u64, 405 }; 406 407 pub fn publish( 408 comptime repo_opts: rp.RepoOpts(.xit), 409 io: std.Io, 410 allocator: std.mem.Allocator, 411 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), 412 target_repo: *rp.Repo(.xit, repo_opts), 413 path: []const u8, 414 input: PublishInput, 415 ) !void { 416 const patch_id = try evt.parseEventId(&input.id); 417 var arena = std.heap.ArenaAllocator.init(allocator); 418 defer arena.deinit(); 419 420 const admin_moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); 421 const fork_record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &arena, &patch_id)) orelse return error.InvalidPatchDraft; 422 if (fork_record.removed or 423 !std.mem.eql(u8, fork_record.event.user_id, &input.user_id) or 424 !std.mem.eql(u8, fork_record.event.repo_id, &input.repo_id)) return error.InvalidPatchDraft; 425 const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &arena, &input.user_id)) orelse return error.InvalidPatchDraft; 426 if (user.removed or 427 !std.mem.eql(u8, user.event.name, input.author.name) or 428 !std.mem.eql(u8, user.event.email, input.author.email)) return error.InvalidPatchDraft; 429 430 var fork_repo = rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = path }) catch return error.PatchDataUnavailable; 431 defer fork_repo.deinit(io, allocator); 432 const fork_moment = evt.currentMoment(repo_opts, &fork_repo) catch return error.PatchDataUnavailable; 433 const local_patch = (try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena, &patch_id)) orelse return error.PatchDataUnavailable; 434 435 const target_moment = evt.currentMoment(repo_opts, target_repo) catch |err| switch (err) { 436 error.NotFound => null, 437 else => |other| return other, 438 }; 439 const existing = if (target_moment) |moment| 440 try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, moment, &arena, &patch_id) 441 else 442 null; 443 if (existing) |record| { 444 if (record.removed or !std.mem.eql(u8, record.author_email orelse "", input.author.email)) return error.InvalidPatch; 445 } 446 if (local_patch.removed) { 447 if (existing == null) return error.PatchDataUnavailable; 448 } else { 449 const revision_hex = local_patch.event.patchrev_id orelse return error.PatchNotPushed; 450 const revision_id = try evt.parseEventId(&revision_hex); 451 const revision = (try evt.PatchRev.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena, &revision_id)) orelse return error.PatchNotPushed; 452 if (revision.removed) return error.PatchNotPushed; 453 var patch = (existing orelse local_patch).event; 454 patch.patchrev_id = revision_hex; 455 if (existing == null) patch.status = .open; 456 457 try evt.consume(.repo, .xit, repo_opts, io, allocator, target_repo, evt.events_ref, &.{.{ 458 .id = input.id, 459 .timestamp = input.timestamp, 460 .author = input.author, 461 .event = .{ .patch = patch }, 462 }}); 463 464 try evt.consume(.fork, .xit, repo_opts, io, allocator, &fork_repo, evt.events_ref, &.{.{ 465 .id = input.id, 466 .timestamp = input.timestamp, 467 .author = input.author, 468 .event = .{ .patch = null }, 469 }}); 470 } 471 472 if (fork_record.event.stage == .draft) { 473 var posted = fork_record.event; 474 posted.stage = .posted; 475 try evt.consume(.admin, .xit, evt.admin_repo_opts, io, allocator, admin_repo, evt.events_ref, &.{.{ 476 .id = input.id, 477 .timestamp = input.timestamp, 478 .author = input.author, 479 .event = .{ .fork = posted }, 480 }}); 481 } 482 } 483 484 // delete first so a failed tombstone can be retried safely 485 pub fn remove( 486 io: std.Io, 487 allocator: std.mem.Allocator, 488 repo_root_path: []const u8, 489 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), 490 id: *const [evt.event_id_size * 2]u8, 491 user_id: *const [evt.event_id_size]u8, 492 author: evt.CommitAuthor, 493 ) !void { 494 const fork_id = try evt.parseEventId(id); 495 var arena = std.heap.ArenaAllocator.init(allocator); 496 defer arena.deinit(); 497 const moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); 498 const record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, &fork_id)) orelse return error.InvalidPatchDraft; 499 if (!std.mem.eql(u8, record.event.user_id, user_id)) return error.InvalidPatchDraft; 500 501 const path = try forkPath(allocator, repo_root_path, id); 502 defer allocator.free(path); 503 try std.Io.Dir.cwd().deleteTree(io, path); 504 if (!record.removed) try evt.remove(.admin, .xit, evt.admin_repo_opts, io, allocator, admin_repo, &fork_id, .fork, author); 505 } 506