╭ fork.zig ─────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("event.zig"); 3 const serve_common = @import("serve_common.zig"); 4 const xit = @import("xit"); 5 const rp = xit.repo; 6 const hash = xit.hash; 7 const obj = xit.object; 8 const mrg = xit.merge; 9 const bch = xit.branch; 10 const rf = xit.ref; 11 12 pub const ref = rf.Ref{ .kind = .head, .name = "patch" }; 13 const ref_path = "refs/heads/patch"; 14 15 pub const Route = struct { 16 identity: []const u8, 17 id: [evt.event_id_size * 2]u8, 18 }; 19 20 pub fn parseRoute(route_path: []const u8) ?Route { 21 const patch_segment = "/patch:"; 22 const patch_start = std.mem.indexOf(u8, route_path, patch_segment) orelse return null; 23 const identity = route_path[0..patch_start]; 24 const id_text = route_path[patch_start + patch_segment.len ..]; 25 if (identity.len == 0 or id_text.len != evt.event_id_size * 2) return null; 26 const id_bytes = evt.parseEventId(id_text) catch return null; 27 return .{ .identity = identity, .id = std.fmt.bytesToHex(id_bytes, .lower) }; 28 } 29 30 pub fn forkPath(allocator: std.mem.Allocator, repo_root_path: []const u8, id: []const u8) ![]u8 { 31 _ = try evt.parseEventId(id); 32 return try std.fs.path.join(allocator, &.{ std.fs.path.dirname(repo_root_path) orelse ".", "forks", id }); 33 } 34 35 pub const CreateInput = struct { 36 id: [evt.event_id_size * 2]u8, 37 user_id: [evt.event_id_size]u8, 38 repo_id: [evt.event_id_size]u8, 39 title: []const u8, 40 description: []const u8, 41 tags: []const u8, 42 target_branch: []const u8, 43 author: evt.CommitAuthor, 44 timestamp: u64, 45 }; 46 47 pub fn create( 48 comptime repo_opts: rp.RepoOpts(.xit), 49 io: std.Io, 50 allocator: std.mem.Allocator, 51 repo_root_path: []const u8, 52 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), 53 input: CreateInput, 54 ) ![]u8 { 55 if (!evt.Patch.fieldsValid(input.title, input.tags)) return error.InvalidPatch; 56 57 // get the fork id and path 58 const fork_id = try evt.parseEventId(&input.id); 59 const fork_path = try forkPath(allocator, repo_root_path, &input.id); 60 errdefer allocator.free(fork_path); 61 62 // make sure the fork id doesn't already exist 63 var arena = std.heap.ArenaAllocator.init(allocator); 64 defer arena.deinit(); 65 const existing = if (evt.currentMoment(evt.admin_repo_opts, admin_repo)) |moment| 66 try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, &fork_id) 67 else |err| switch (err) { 68 error.NotFound => null, 69 else => |other| return other, 70 }; 71 if (existing != null) return error.InvalidPatchDraft; 72 73 // get the target repo 74 const target_id = std.fmt.bytesToHex(input.repo_id, .lower); 75 const target_path = try std.fs.path.join(allocator, &.{ repo_root_path, &target_id }); 76 defer allocator.free(target_path); 77 var target_repo = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = target_path, .require_repo_root = true }); 78 defer target_repo.deinit(io, allocator); 79 if (!rf.validateName(input.target_branch)) return error.InvalidTarget; 80 if ((try target_repo.readRef(io, .{ .kind = .head, .name = input.target_branch })) == null) return error.TargetNotFound; 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 102 // TODO: use reflink here when the filesystem supports it 103 const destination = try fork_repo_dir.createFile(io, "db", .{ .exclusive = true, .read = true }); 104 defer destination.close(io); 105 var read_buffer: [64 * 1024]u8 = undefined; 106 var write_buffer: [64 * 1024]u8 = undefined; 107 var reader = target_repo.core.db_file.reader(io, &read_buffer); 108 var writer = destination.writer(io, &write_buffer); 109 _ = try reader.interface.streamRemaining(&writer.interface); 110 try writer.interface.flush(); 111 try destination.sync(io); 112 } 113 114 var fork_repo = try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = fork_path, .require_repo_root = true }); 115 defer fork_repo.deinit(io, allocator); 116 117 // clear the haxy state in the fork repo and create the patch branch 118 { 119 const DB = rp.Repo(.xit, repo_opts).DB; 120 const State = rp.Repo(.xit, repo_opts).State; 121 const Ctx = struct { 122 core: *rp.Repo(.xit, repo_opts).Core, 123 io: std.Io, 124 125 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 126 var moment = try DB.HashMap(.read_write).init(cursor.*); 127 const state = State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 128 const head_oid_maybe = try rf.readHeadRecurMaybe(.xit, repo_opts, state.readOnly(), ctx.io); 129 130 var path_buffer: [rf.MAX_REF_CONTENT_SIZE]u8 = undefined; 131 const events_path = try evt.events_ref.toPath(&path_buffer); 132 rf.remove(.xit, repo_opts, state, ctx.io, events_path) catch |err| switch (err) { 133 error.RefNotFound => {}, 134 else => |other| return other, 135 }; 136 const patch_path = try ref.toPath(&path_buffer); 137 rf.remove(.xit, repo_opts, state, ctx.io, patch_path) catch |err| switch (err) { 138 error.RefNotFound => {}, 139 else => |other| return other, 140 }; 141 142 _ = try moment.remove(hash.hashInt(repo_opts.hash, evt.materialized_key)); 143 _ = try moment.remove(hash.hashInt(repo_opts.hash, evt.last_object_id_key)); 144 145 if (head_oid_maybe) |*head_oid| { 146 try rf.write(.xit, repo_opts, state, ctx.io, patch_path, .{ .oid = head_oid }); 147 } else { 148 try bch.add(.xit, repo_opts, state, ctx.io, .{ .name = ref.name, .target = .none }); 149 } 150 try rf.replaceHead(.xit, repo_opts, state, ctx.io, .{ .ref = ref }); 151 try xit.undo.writeMessage(repo_opts, state, .{ .custom = "create fork" }); 152 } 153 }; 154 155 try fork_repo.core.db_file.lock(io, .exclusive); 156 defer fork_repo.core.db_file.unlock(io); 157 158 const history = try DB.ArrayList(.read_write).init(fork_repo.core.db.rootCursor()); 159 try history.appendContext( 160 .{ .slot = try history.getSlot(-1) }, 161 Ctx{ .core = &fork_repo.core, .io = io }, 162 ); 163 } 164 165 try fork_repo.addConfig(io, allocator, .{ .name = "core.bare", .value = "true" }); 166 try fork_repo.addConfig(io, allocator, .{ .name = "receive.denydeletes", .value = "true" }); 167 168 // create the patch event 169 try evt.consume(.fork, .xit, repo_opts, io, allocator, &fork_repo, evt.events_ref, &.{.{ 170 .id = input.id, 171 .timestamp = input.timestamp, 172 .author = input.author, 173 .event = .{ .patch = .{ 174 .title = input.title, 175 .description = input.description, 176 .tags = input.tags, 177 .target_branch = input.target_branch, 178 } }, 179 }}); 180 181 // create the fork event 182 try evt.consume(.admin, .xit, evt.admin_repo_opts, io, allocator, admin_repo, evt.events_ref, &.{.{ 183 .id = input.id, 184 .timestamp = input.timestamp, 185 .author = input.author, 186 .event = .{ .fork = .{ 187 .user_id = &input.user_id, 188 .repo_id = &input.repo_id, 189 } }, 190 }}); 191 192 return fork_path; 193 } 194 195 pub fn receivePack( 196 comptime repo_opts: rp.RepoOpts(.xit), 197 io: std.Io, 198 allocator: std.mem.Allocator, 199 fork_repo: *rp.Repo(.xit, repo_opts), 200 target_repo: *rp.Repo(.xit, repo_opts), 201 id: *const [evt.event_id_size * 2]u8, 202 author: evt.CommitAuthor, 203 timestamp: u64, 204 reader: *std.Io.Reader, 205 writer: *std.Io.Writer, 206 error_writer: *std.Io.Writer, 207 ) !void { 208 const patch_id = try evt.parseEventId(id); 209 var arena = std.heap.ArenaAllocator.init(allocator); 210 defer arena.deinit(); 211 212 // load the fork and target patch state 213 const fork_moment = evt.currentMoment(repo_opts, fork_repo) catch return error.PatchDataUnavailable; 214 const fork_patch = (try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena, &patch_id)) orelse return error.PatchDataUnavailable; 215 const target_moment = evt.currentMoment(repo_opts, target_repo) catch |err| switch (err) { 216 error.NotFound => null, 217 else => |other| return other, 218 }; 219 const target_patch = if (target_moment) |moment| 220 try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, moment, &arena, &patch_id) 221 else 222 null; 223 if (fork_patch.removed and target_patch == null) return error.PatchDataUnavailable; 224 if (target_patch) |patch| { 225 if (!std.mem.eql(u8, patch.author_email orelse "", author.email)) return error.PatchDataUnavailable; 226 if (patch.event.status.kind() == .merged) return error.PatchAlreadyMerged; 227 } 228 const published = target_patch != null; 229 const active_patch = target_patch orelse fork_patch; 230 const title = active_patch.event.title; 231 232 // resolve the target and the newest fork revision 233 const target_branch = active_patch.event.target_branch; 234 const target_oid = (try target_repo.readRef(io, .{ .kind = .head, .name = target_branch })) orelse return error.TargetNotFound; 235 const newest = try evt.PatchRev.readNewest(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena); 236 var revision_id_maybe: ?[evt.event_id_size]u8 = null; 237 238 // execute a transaction that receives the push and materializes its revision 239 { 240 var response = xit.net_server_receive_pack.Response.init(allocator); 241 defer response.deinit(); 242 const DB = rp.Repo(.xit, repo_opts).DB; 243 const State = rp.Repo(.xit, repo_opts).State; 244 const Ctx = struct { 245 core: *rp.Repo(.xit, repo_opts).Core, 246 target_core: *rp.Repo(.xit, repo_opts).Core, 247 io: std.Io, 248 allocator: std.mem.Allocator, 249 reader: *std.Io.Reader, 250 writer: *std.Io.Writer, 251 response: *xit.net_server_receive_pack.Response, 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 title: []const u8, 257 author: evt.CommitAuthor, 258 timestamp: u64, 259 newest: ?evt.PatchRev.WithId, 260 revision_id_maybe: *?[evt.event_id_size]u8, 261 262 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 263 // receive the branch update 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, .deferred_response = ctx.response }); 267 try evt.writeReceivedPatches(repo_opts, state, ctx.io, ctx.allocator, ctx.response, ctx.writer); 268 269 // copy the target history needed to preserve the merge base 270 const source_oid = (try rf.readRecur(.xit, repo_opts, state.readOnly(), ctx.io, .{ .ref = ref })) orelse return error.CancelTransaction; 271 var target_repo_moment = try ctx.target_core.latestMoment(); 272 const target_state = State(.read_only){ .core = ctx.target_core, .extra = .{ .moment = &target_repo_moment } }; 273 var objects = try obj.ObjectIterator(.xit, repo_opts).init(target_state, ctx.io, ctx.allocator, .{ .kind = .all }); 274 defer objects.deinit(); 275 try objects.include(&ctx.target_oid); 276 try obj.copyFromObjectIterator(.xit, repo_opts, state, .xit, repo_opts, &objects, ctx.io, null); 277 278 const base_oid = try mrg.commonAncestor(.xit, repo_opts, state.readOnly(), ctx.io, ctx.allocator, &ctx.target_oid, &source_oid); 279 const existing_revision = if (ctx.newest) |latest| 280 if (std.mem.eql(u8, latest.record.event.base_oid, &base_oid) and 281 std.mem.eql(u8, latest.record.event.source_oid, &source_oid)) latest else null 282 else 283 null; 284 285 // reuse an identical revision or record a new one 286 var events: [2]evt.EventWithId = undefined; 287 var event_count: usize = 0; 288 var base_tree_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 289 var head_tree_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; 290 var tree_entries: [2]evt.EventTreeEntry = undefined; 291 if (existing_revision) |latest| { 292 if (ctx.published) ctx.revision_id_maybe.* = latest.id; 293 } else { 294 var base_object = try obj.Object(.xit, repo_opts).initCommit(state.readOnly(), ctx.io, ctx.allocator, &base_oid); 295 defer base_object.deinit(); 296 var source_object = try obj.Object(.xit, repo_opts).initCommit(state.readOnly(), ctx.io, ctx.allocator, &source_oid); 297 defer source_object.deinit(); 298 base_tree_oid = base_object.content.commit.tree; 299 head_tree_oid = source_object.content.commit.tree; 300 301 var revision_id: [evt.event_id_size]u8 = undefined; 302 ctx.io.random(&revision_id); 303 const revision_hex = std.fmt.bytesToHex(revision_id, .lower); 304 const revision_event: evt.PatchRev = .{ 305 .base_oid = &base_oid, 306 .source_oid = &source_oid, 307 .message = ctx.title, 308 }; 309 const identity = try std.fmt.allocPrint(ctx.allocator, "{s} <{s}>", .{ ctx.author.name, ctx.author.email }); 310 defer ctx.allocator.free(identity); 311 const patch_oid = try evt.PatchRev.writeSquashCommit( 312 .xit, 313 repo_opts, 314 state, 315 ctx.io, 316 ctx.allocator, 317 revision_event, 318 &head_tree_oid, 319 identity, 320 identity, 321 ctx.timestamp, 322 ); 323 tree_entries = .{ 324 .{ .tree = .{ .name = "base", .oid = &base_tree_oid } }, 325 .{ .tree = .{ .name = "head", .oid = &head_tree_oid } }, 326 }; 327 events[event_count] = .{ 328 .id = revision_hex, 329 .timestamp = ctx.timestamp, 330 .author = ctx.author, 331 .tree_entries = &tree_entries, 332 .event = .{ .patchrev = revision_event }, 333 }; 334 event_count += 1; 335 ctx.revision_id_maybe.* = revision_id; 336 337 if (!ctx.published) { 338 var patch = ctx.patch.event; 339 patch.revision = .{ 340 .id = revision_hex, 341 .squash_oid = &patch_oid, 342 .source_oid = &source_oid, 343 }; 344 events[event_count] = .{ 345 .id = ctx.patch_id, 346 .timestamp = ctx.timestamp, 347 .author = ctx.author, 348 .event = .{ .patch = patch }, 349 }; 350 event_count += 1; 351 } 352 } 353 354 // published patches keep only revisions in the fork 355 if (ctx.published and !ctx.patch.removed) { 356 events[event_count] = .{ 357 .id = ctx.patch_id, 358 .timestamp = ctx.timestamp, 359 .author = ctx.author, 360 .event = .{ .patch = null }, 361 }; 362 event_count += 1; 363 } 364 365 // commit and index any new events 366 if (event_count > 0) { 367 try evt.commitEvents(.xit, repo_opts, state, ctx.io, ctx.allocator, evt.events_ref, events[0..event_count], null); 368 if (!try evt.consumeInTransaction(.fork, .xit, repo_opts, state, &ctx.core.db, &moment, ctx.io, ctx.allocator, evt.events_ref)) return error.CancelTransaction; 369 } 370 try xit.undo.writeMessage(repo_opts, state, .push); 371 } 372 }; 373 374 try fork_repo.core.db_file.lock(io, .exclusive); 375 defer fork_repo.core.db_file.unlock(io); 376 377 const history = try DB.ArrayList(.read_write).init(fork_repo.core.db.rootCursor()); 378 history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ 379 .core = &fork_repo.core, 380 .target_core = &target_repo.core, 381 .io = io, 382 .allocator = allocator, 383 .reader = reader, 384 .writer = writer, 385 .response = &response, 386 .patch_id = id.*, 387 .patch = fork_patch, 388 .published = published, 389 .target_oid = target_oid, 390 .title = title, 391 .author = author, 392 .timestamp = timestamp, 393 .newest = newest, 394 .revision_id_maybe = &revision_id_maybe, 395 }) catch |err| { 396 response.finish(writer, @errorName(err)) catch {}; 397 if (err == error.CancelTransaction) return; 398 return err; 399 }; 400 try response.finish(writer, null); 401 } 402 403 // best-effort update the published patch so it has the new revision 404 { 405 const revision_id = revision_id_maybe orelse return; 406 if (!published) return; 407 var update_arena = std.heap.ArenaAllocator.init(allocator); 408 defer update_arena.deinit(); 409 const target_update_moment = evt.currentMoment(repo_opts, target_repo) catch return; 410 const published_patch = (evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, target_update_moment, &update_arena, &patch_id) catch return) orelse return; 411 if (published_patch.removed) return; 412 const fork_update_moment = evt.currentMoment(repo_opts, fork_repo) catch return; 413 const revision = (evt.PatchRev.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_update_moment, &update_arena, &revision_id) catch return) orelse return; 414 if (revision.removed) return; 415 const selected = evt.Patch.Revision.fromRecord(revision_id, revision); 416 if (published_patch.event.revision) |current| { 417 if (evt.fieldEqual(evt.Patch.Revision, current, selected)) return; 418 } 419 var patch = published_patch.event; 420 patch.revision = selected; 421 evt.consume(.repo, .xit, repo_opts, io, allocator, target_repo, evt.events_ref, &.{.{ 422 .id = id.*, 423 .timestamp = timestamp, 424 .author = author, 425 .event = .{ .patch = patch }, 426 }}) catch |update_err| { 427 serve_common.logError(io, error_writer, "failed to update published patch {s}: {s}\n", .{ id, @errorName(update_err) }); 428 }; 429 } 430 } 431 432 // tombstone first so a missing fork never remains visible 433 pub fn remove( 434 io: std.Io, 435 allocator: std.mem.Allocator, 436 repo_root_path: []const u8, 437 admin_repo: *rp.Repo(.xit, evt.admin_repo_opts), 438 id: *const [evt.event_id_size * 2]u8, 439 expected_user_id: ?*const [evt.event_id_size]u8, 440 author: evt.CommitAuthor, 441 ) !void { 442 const fork_id = try evt.parseEventId(id); 443 var arena = std.heap.ArenaAllocator.init(allocator); 444 defer arena.deinit(); 445 const moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); 446 const record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, &fork_id)) orelse return error.InvalidPatchDraft; 447 if (expected_user_id) |user_id| { 448 if (!std.mem.eql(u8, record.event.user_id, user_id)) return error.InvalidPatchDraft; 449 } 450 if (!record.removed) try evt.remove(.admin, .xit, evt.admin_repo_opts, io, allocator, admin_repo, &fork_id, .fork, author); 451 452 const path = try forkPath(allocator, repo_root_path, id); 453 defer allocator.free(path); 454 try std.Io.Dir.cwd().deleteTree(io, path); 455 } 456