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