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