╭ push.zig ─────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯
1 const std = @import("std"); 2 const xit = @import("xit"); 3 const rp = xit.repo; 4 const hash = xit.hash; 5 const mrg = xit.merge; 6 const obj = xit.object; 7 const rf = xit.ref; 8 const evt = @import("event.zig"); 9 const pch = @import("patch.zig"); 10 const find = @import("find.zig"); 11 const srch_cmmt = @import("search_commit.zig"); 12 const fork = @import("fork.zig"); 13 const serve_common = @import("serve_common.zig"); 14 const ssh = @import("serve_ssh_protocol.zig"); 15 const progress = @import("./progress.zig"); 16 17 // prepare all reachable commits, including history received before patch 18 // generation was enabled. this shares the receive-pack transaction. 19 pub fn writeReceivedPatches( 20 comptime repo_opts: rp.RepoOpts(.xit), 21 state: rp.Repo(.xit, repo_opts).State(.read_write), 22 io: std.Io, 23 allocator: std.mem.Allocator, 24 response: *xit.net_server_receive_pack.Response, 25 writer: *std.Io.Writer, 26 ) !void { 27 var config = try xit.config.Config(.xit, repo_opts).init(state.readOnly(), io, allocator); 28 defer config.deinit(); 29 try config.add(state, io, .{ .name = "merge.algorithm", .value = "patch" }); 30 31 // every ref is a tip 32 var tips: std.ArrayList([hash.hexLen(repo_opts.hash)]u8) = .empty; 33 defer tips.deinit(allocator); 34 var refs = try rf.AllRefIterator(.xit, repo_opts).init(state.readOnly(), allocator); 35 defer refs.deinit(); 36 while (try refs.next()) |ref| { 37 if (try rf.readRecur(.xit, repo_opts, state.readOnly(), io, .{ .ref = ref })) |oid| try tips.append(allocator, oid); 38 } 39 var sideband = progress.Sideband{ .response = response, .writer = writer }; 40 try xit.patch.writePatches(repo_opts, state, io, allocator, tips.items, &sideband); 41 } 42 43 // the action a received push records 44 pub const undo_action = "haxy/push"; 45 46 // the fixture records a stand-in push with this too, so both write one shape 47 pub fn writeUndo( 48 comptime repo_opts: rp.RepoOpts(.xit), 49 state: rp.Repo(.xit, repo_opts).State(.read_write), 50 io: std.Io, 51 allocator: std.mem.Allocator, 52 author: evt.CommitAuthor, 53 updates: []const xit.net_server_receive_pack.AppliedRefUpdate, 54 ) !void { 55 // what the payload may take up: the record holds a timestamp and the 56 // action kind in the same fixed buffer 57 const payload_max_len = repo_opts.max_read_size - @sizeOf(i64) - undo_action.len - 1; 58 59 var arena = std.heap.ArenaAllocator.init(allocator); 60 defer arena.deinit(); 61 const aa = arena.allocator(); 62 63 const author_json: std.json.ObjectMap = try .init(aa, &.{ "username", "email" }, &.{ .{ .string = author.name }, .{ .string = author.email } }); 64 var payload: std.json.ObjectMap = try .init(aa, &.{"author"}, &.{.{ .object = author_json }}); 65 66 // where each ref landed, so the undo tab can say what the push did to it. 67 // an oid of all zeros means the ref has no side there, which is how a 68 // create and a remove are told apart 69 var refs: std.json.Array = .init(aa); 70 for (updates) |update| { 71 const entry: std.json.ObjectMap = try .init(aa, &.{ "name", "old", "new" }, &.{ 72 .{ .string = update.ref_name }, 73 .{ .string = if (zeroOid(update.old_oid)) "" else update.old_oid }, 74 .{ .string = if (zeroOid(update.new_oid)) "" else update.new_oid }, 75 }); 76 try refs.append(.{ .object = entry }); 77 try payload.put(aa, "refs", .{ .array = refs }); 78 79 // the record is written into one fixed buffer, so a ref that no 80 // longer fits is left out rather than failing the push 81 if (try payloadLen(payload) > payload_max_len) { 82 _ = refs.pop(); 83 break; 84 } 85 } 86 87 // the array is stored by value, so the one the payload holds still counts 88 // the entry that was popped until it is replaced here 89 try payload.put(aa, "refs", .{ .array = refs }); 90 91 try xit.undo.write(repo_opts, state, std.Io.Timestamp.now(io, .real).toSeconds(), .{ .custom = .{ .action_kind = undo_action, .payload = payload } }); 92 } 93 94 // the oid a ref update sends when it has no side: all zeros 95 fn zeroOid(oid: []const u8) bool { 96 for (oid) |char| if (char != '0') return false; 97 return true; 98 } 99 100 // how long the record's payload would be, to keep it within the buffer 101 fn payloadLen(payload: std.json.ObjectMap) !u64 { 102 var discarding = std.Io.Writer.Discarding.init(&.{}); 103 try std.json.Stringify.value(std.json.Value{ .object = payload }, .{}, &discarding.writer); 104 return discarding.fullCount(); 105 } 106 107 // serve a receive-pack and consume any events it pushed to the events branch, 108 // all in one transaction: the push and the views derived from it commit 109 // atomically, and a failed consume cancels the push 110 pub fn receivePackAndConsume( 111 comptime repo_opts: rp.RepoOpts(.xit), 112 io: std.Io, 113 allocator: std.mem.Allocator, 114 repo: *rp.Repo(.xit, repo_opts), 115 reader: *std.Io.Reader, 116 writer: *std.Io.Writer, 117 options: xit.net_server_receive_pack.Options, 118 author: evt.CommitAuthor, 119 server: evt.Host.Server, 120 error_writer: *std.Io.Writer, 121 sess: ?*ssh.SessionCtx, 122 ) !void { 123 const DB = rp.Repo(.xit, repo_opts).DB; 124 const State = rp.Repo(.xit, repo_opts).State; 125 126 var response = xit.net_server_receive_pack.Response.init(allocator); 127 defer response.deinit(); 128 129 var updates = xit.net_server_receive_pack.AppliedRefUpdates.init(allocator); 130 defer updates.deinit(); 131 132 var sideband = progress.Sideband{ .response = &response, .writer = writer, .sess = sess }; 133 const Ctx = struct { 134 updates: *xit.net_server_receive_pack.AppliedRefUpdates, 135 core: *rp.Repo(.xit, repo_opts).Core, 136 io: std.Io, 137 allocator: std.mem.Allocator, 138 reader: *std.Io.Reader, 139 writer: *std.Io.Writer, 140 options: xit.net_server_receive_pack.Options, 141 author: evt.CommitAuthor, 142 response: *xit.net_server_receive_pack.Response, 143 server: evt.Host.Server, 144 error_writer: *std.Io.Writer, 145 sideband: *progress.Sideband, 146 147 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 148 var moment = try DB.HashMap(.read_write).init(cursor.*); 149 const state = State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 150 151 var receive_options = ctx.options; 152 receive_options.applied_ref_updates = ctx.updates; 153 receive_options.deferred_response = ctx.response; 154 try xit.net_server_receive_pack.run(.xit, repo_opts, state, ctx.io, ctx.allocator, ctx.reader, ctx.writer, receive_options, ctx.sideband); 155 try writeReceivedPatches(repo_opts, state, ctx.io, ctx.allocator, ctx.response, ctx.writer); 156 if (ctx.sideband.gone) return error.ClientGone; 157 158 // a repo without an events branch has no events to consume. a 159 // no-op consume must not cancel here, since the push shares the 160 // transaction and must commit regardless. 161 if (null != try rf.readRecur(.xit, repo_opts, state.readOnly(), ctx.io, .{ .ref = evt.events_ref })) { 162 _ = try evt.consumeInTransaction(.repo, .xit, repo_opts, state, &ctx.core.db, &moment, ctx.io, ctx.allocator, evt.events_ref); 163 try pch.detectMerged(repo_opts, state, &ctx.core.db, &moment, ctx.io, ctx.allocator, ctx.server.users_dir, ctx.updates.items.items, ctx.error_writer); 164 165 // the revisions the pushed branches cause belong to the push 166 _ = try pch.refreshBranchesInTransaction(.{ .server = ctx.server }, repo_opts, state, &moment, ctx.io, ctx.allocator, ctx.updates.items.items, null, ctx.sideband); 167 } 168 169 if (ctx.sideband.gone) return error.ClientGone; 170 171 // the indexes the pushed refs invalidate belong to the push too 172 _ = try find.refreshInTransaction(repo_opts, state, &moment, ctx.io, ctx.allocator, ctx.sideband); 173 var moves: std.ArrayList(srch_cmmt.Move) = .empty; 174 defer moves.deinit(ctx.allocator); 175 for (ctx.updates.items.items) |update| try moves.append(ctx.allocator, .{ .old_oid = update.old_oid, .new_oid = update.new_oid }); 176 _ = try srch_cmmt.refreshInTransaction(repo_opts, state, &moment, ctx.io, ctx.allocator, moves.items, ctx.sideband); 177 178 try writeUndo(repo_opts, state, ctx.io, ctx.allocator, ctx.author, ctx.updates.items.items); 179 } 180 }; 181 182 const result = blk: { 183 try repo.core.db_file.lock(io, .exclusive); 184 defer repo.core.db_file.unlock(io); 185 186 const history = try DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); 187 break :blk history.appendContext( 188 .{ .slot = try history.getSlot(-1) }, 189 Ctx{ 190 .updates = &updates, 191 .core = &repo.core, 192 .io = io, 193 .allocator = allocator, 194 .reader = reader, 195 .writer = writer, 196 .options = options, 197 .author = author, 198 .response = &response, 199 .server = server, 200 .error_writer = error_writer, 201 .sideband = &sideband, 202 }, 203 ); 204 }; 205 result catch |err| { 206 response.finish(writer, @errorName(err)) catch {}; 207 if (err == error.CancelTransaction) return; 208 return err; 209 }; 210 evt.bumpRepoActivityAt(io, allocator, server, repo.core.work_path); 211 212 pch.refreshBranches(.{ .server = server }, .xit, repo_opts, io, allocator, repo, updates.items.items, &sideband) catch |err| { 213 serve_common.logError(io, error_writer, "failed to refresh branch patches: {s}\n", .{@errorName(err)}); 214 pch.refreshOpenMergeability(repo_opts, io, allocator, repo, server.users_dir, null, &sideband); 215 }; 216 try response.finish(writer, null); 217 } 218 219 pub fn receiveFork( 220 comptime repo_opts: rp.RepoOpts(.xit), 221 io: std.Io, 222 allocator: std.mem.Allocator, 223 fork_repo: *rp.Repo(.xit, repo_opts), 224 target_repo: *rp.Repo(.xit, repo_opts), 225 server: evt.Host.Server, 226 id: *const [evt.event_id_size * 2]u8, 227 author: evt.CommitAuthor, 228 timestamp: u64, 229 reader: *std.Io.Reader, 230 writer: *std.Io.Writer, 231 error_writer: *std.Io.Writer, 232 sess: ?*ssh.SessionCtx, 233 ) !void { 234 const patch_id = try evt.parseEventId(id); 235 var arena = std.heap.ArenaAllocator.init(allocator); 236 defer arena.deinit(); 237 238 // load the fork and target patch state 239 const fork_moment = evt.currentMoment(repo_opts, fork_repo) catch return error.PatchDataUnavailable; 240 const fork_patch = (try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena, &patch_id)) orelse return error.PatchDataUnavailable; 241 const target_moment = evt.currentMoment(repo_opts, target_repo) catch |err| switch (err) { 242 error.NotFound => null, 243 else => |other| return other, 244 }; 245 const target_patch = if (target_moment) |moment| 246 try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, moment, &arena, &patch_id) 247 else 248 null; 249 if (fork_patch.removed and target_patch == null) return error.PatchDataUnavailable; 250 if (target_patch) |patch| { 251 if (!std.mem.eql(u8, patch.author_email orelse "", author.email)) return error.PatchDataUnavailable; 252 if (patch.event.status.kind() == .merged) return error.PatchAlreadyMerged; 253 } 254 const published = target_patch != null; 255 const active_patch = target_patch orelse fork_patch; 256 const title = active_patch.event.title; 257 258 // resolve the target and the newest fork revision 259 const target_branch = active_patch.event.target_branch; 260 const target_oid = (try target_repo.readRef(io, .{ .kind = .head, .name = target_branch })) orelse return error.TargetNotFound; 261 const newest = try evt.PatchRev.readNewest(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena); 262 var revision_id_maybe: ?[evt.event_id_size]u8 = null; 263 var response = xit.net_server_receive_pack.Response.init(allocator); 264 defer response.deinit(); 265 var sideband = progress.Sideband{ .response = &response, .writer = writer, .sess = sess }; 266 267 // execute a transaction that receives the push and materializes its revision 268 const result = blk: { 269 const DB = rp.Repo(.xit, repo_opts).DB; 270 const State = rp.Repo(.xit, repo_opts).State; 271 const Ctx = struct { 272 core: *rp.Repo(.xit, repo_opts).Core, 273 target_core: *rp.Repo(.xit, repo_opts).Core, 274 io: std.Io, 275 allocator: std.mem.Allocator, 276 reader: *std.Io.Reader, 277 writer: *std.Io.Writer, 278 response: *xit.net_server_receive_pack.Response, 279 patch_id: [evt.event_id_size * 2]u8, 280 patch: evt.Patch.Record, 281 published: bool, 282 target_oid: [hash.hexLen(repo_opts.hash)]u8, 283 title: []const u8, 284 author: evt.CommitAuthor, 285 timestamp: u64, 286 newest: ?evt.PatchRev.WithId, 287 revision_id_maybe: *?[evt.event_id_size]u8, 288 sideband: *progress.Sideband, 289 290 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 291 // receive the branch update 292 var moment = try DB.HashMap(.read_write).init(cursor.*); 293 const state = State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 294 try xit.net_server_receive_pack.run(.xit, repo_opts, state, ctx.io, ctx.allocator, ctx.reader, ctx.writer, .{ .allowed_ref = "refs/heads/" ++ fork.ref.name, .deferred_response = ctx.response }, ctx.sideband); 295 try writeReceivedPatches(repo_opts, state, ctx.io, ctx.allocator, ctx.response, ctx.writer); 296 if (ctx.sideband.gone) return error.ClientGone; 297 298 // copy the target history needed to preserve the merge base 299 const source_oid = (try rf.readRecur(.xit, repo_opts, state.readOnly(), ctx.io, .{ .ref = fork.ref })) orelse return error.CancelTransaction; 300 var target_repo_moment = try ctx.target_core.latestMoment(); 301 const target_state = State(.read_only){ .core = ctx.target_core, .extra = .{ .moment = &target_repo_moment } }; 302 var objects = try obj.ObjectIterator(.xit, repo_opts).init(target_state, ctx.io, ctx.allocator, .{ .kind = .all }); 303 defer objects.deinit(); 304 try objects.include(&ctx.target_oid); 305 try obj.copyFromObjectIterator(.xit, repo_opts, state, .xit, repo_opts, &objects, ctx.io, ctx.sideband); 306 307 const base_oid = try mrg.commonAncestor(.xit, repo_opts, state.readOnly(), ctx.io, ctx.allocator, &ctx.target_oid, &source_oid); 308 const existing_revision = if (ctx.newest) |latest| 309 if (std.mem.eql(u8, latest.record.event.base_oid, &base_oid) and 310 std.mem.eql(u8, latest.record.event.source_oid, &source_oid)) latest else null 311 else 312 null; 313 314 // reuse an identical revision or record a new one 315 var revision_arena = std.heap.ArenaAllocator.init(ctx.allocator); 316 defer revision_arena.deinit(); 317 var events: [2]evt.EventWithId = undefined; 318 var event_count: usize = 0; 319 if (existing_revision) |latest| { 320 if (ctx.published) ctx.revision_id_maybe.* = latest.id; 321 } else { 322 const prepared = try evt.PatchRev.prepare(.xit, repo_opts, state, ctx.io, &revision_arena, &base_oid, &source_oid, ctx.title, ctx.author, ctx.timestamp); 323 events[event_count] = prepared.event; 324 event_count += 1; 325 ctx.revision_id_maybe.* = try evt.parseEventId(&prepared.event.id); 326 327 if (!ctx.published) { 328 var patch = ctx.patch.event; 329 patch.revision = prepared.revision; 330 events[event_count] = .{ 331 .id = ctx.patch_id, 332 .timestamp = ctx.timestamp, 333 .author = ctx.author, 334 .event = .{ .patch = patch }, 335 }; 336 event_count += 1; 337 } 338 } 339 340 // published patches keep only revisions in the fork 341 if (ctx.published and !ctx.patch.removed) { 342 events[event_count] = .{ 343 .id = ctx.patch_id, 344 .timestamp = ctx.timestamp, 345 .author = ctx.author, 346 .event = .{ .patch = null }, 347 }; 348 event_count += 1; 349 } 350 351 // commit and index any new events 352 if (event_count > 0) { 353 try evt.commitEvents(.xit, repo_opts, state, ctx.io, ctx.allocator, evt.events_ref, events[0..event_count], null); 354 if (!try evt.consumeInTransaction(.fork, .xit, repo_opts, state, &ctx.core.db, &moment, ctx.io, ctx.allocator, evt.events_ref)) return error.CancelTransaction; 355 } 356 // the patch branch's file index lands with the push 357 _ = try find.refreshInTransaction(repo_opts, state, &moment, ctx.io, ctx.allocator, ctx.sideband); 358 359 // a fork push tracks no ref updates, so it records no ranges 360 try writeUndo(repo_opts, state, ctx.io, ctx.allocator, ctx.author, &.{}); 361 } 362 }; 363 364 try fork_repo.core.db_file.lock(io, .exclusive); 365 defer fork_repo.core.db_file.unlock(io); 366 367 const history = try DB.ArrayList(.read_write).init(fork_repo.core.db.rootCursor()); 368 break :blk history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ 369 .core = &fork_repo.core, 370 .target_core = &target_repo.core, 371 .io = io, 372 .allocator = allocator, 373 .reader = reader, 374 .writer = writer, 375 .response = &response, 376 .patch_id = id.*, 377 .patch = fork_patch, 378 .published = published, 379 .target_oid = target_oid, 380 .title = title, 381 .author = author, 382 .timestamp = timestamp, 383 .newest = newest, 384 .revision_id_maybe = &revision_id_maybe, 385 .sideband = &sideband, 386 }); 387 }; 388 result catch |err| { 389 response.finish(writer, @errorName(err)) catch {}; 390 if (err == error.CancelTransaction) return; 391 return err; 392 }; 393 394 if (!published) return response.finish(writer, null); 395 396 sideband.run(io, .{ .text = "Updating patches" }) catch {}; 397 sideband.run(io, .{ .start = .{ .kind = .writing_patch, .estimated_total_items = 1 } }) catch {}; 398 399 // best-effort update the published patch so it has the new revision 400 var refreshed = false; 401 update: { 402 const revision_id = revision_id_maybe orelse break :update; 403 var update_arena = std.heap.ArenaAllocator.init(allocator); 404 defer update_arena.deinit(); 405 const target_update_moment = evt.currentMoment(repo_opts, target_repo) catch break :update; 406 const published_patch = (evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, target_update_moment, &update_arena, &patch_id) catch break :update) orelse break :update; 407 if (published_patch.removed) break :update; 408 const fork_update_moment = evt.currentMoment(repo_opts, fork_repo) catch break :update; 409 const revision = (evt.PatchRev.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_update_moment, &update_arena, &revision_id) catch break :update) orelse break :update; 410 if (revision.removed) break :update; 411 const selected = evt.Patch.Revision.fromRecord(revision_id, revision); 412 if (published_patch.event.revision) |current| { 413 if (evt.fieldEqual(evt.Patch.Revision, current, selected)) break :update; 414 } 415 var patch = published_patch.event; 416 patch.revision = selected; 417 evt.consume(.{ .server = server }, .repo, .xit, repo_opts, io, allocator, target_repo, evt.events_ref, &.{.{ 418 .id = id.*, 419 .timestamp = timestamp, 420 .author = author, 421 .event = .{ .patch = patch }, 422 }}) catch |update_err| { 423 serve_common.logError(io, error_writer, "failed to update published patch {s}: {s}\n", .{ id, @errorName(update_err) }); 424 break :update; 425 }; 426 // consume also refreshed mergeability 427 refreshed = true; 428 } 429 430 if (!refreshed) pch.refreshMergeability(repo_opts, io, allocator, target_repo, server.users_dir, patch_id); 431 sideband.run(io, .{ .complete_one = .writing_patch }) catch {}; 432 sideband.run(io, .{ .end = .writing_patch }) catch {}; 433 434 // the client may read either repo as soon as it gets the final response. 435 try response.finish(writer, null); 436 } 437