╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ push.zig ─────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭ find file ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ ╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │ │ │http│ ssh │ │ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
120 } 121 } 122 123 fn zeroOid(oid: []const u8) bool { 124 for (oid) |char| if (char != '0') return false; 125 return true; 126 } 127 128 // how long the record's payload would be, to keep it within the buffer 129 fn payloadLen(payload: std.json.ObjectMap) !u64 { 130 var discarding = std.Io.Writer.Discarding.init(&.{}); 131 try std.json.Stringify.value(std.json.Value{ .object = payload }, .{}, &discarding.writer); 132 return discarding.fullCount(); 133 } 134 135 // serve a receive-pack and consume any events it pushed to the events branch, 136 // all in one transaction: the push and the views derived from it commit 137 // atomically, and a failed consume cancels the push 138 pub fn receivePackAndConsume( 139 comptime repo_opts: rp.RepoOpts(.xit), 140 io: std.Io, 141 allocator: std.mem.Allocator, 142 repo: *rp.Repo(.xit, repo_opts), 143 reader: *std.Io.Reader, 144 writer: *std.Io.Writer, 145 options: xit.net_server_receive_pack.Options, 146 author: evt.CommitAuthor, 147 pusher_id: *const [evt.event_id_size]u8, 148 server: evt.Host.Server, 149 max_repo_size: ?u64, 150 error_writer: *std.Io.Writer, 151 sess: ?*ssh.SessionCtx, 152 ) !void { 153 const DB = rp.Repo(.xit, repo_opts).DB; 154 const State = rp.Repo(.xit, repo_opts).State; 155 156 var response = xit.net_server_receive_pack.Response.init(allocator); 157 defer response.deinit(); 158 159 var updates = xit.net_server_receive_pack.AppliedRefUpdates.init(allocator); 160 defer updates.deinit(); 161 162 var sideband = progress.Sideband{ 163 .response = &response, 164 .writer = writer, 165 .sess = sess, 166 .size_limit = if (max_repo_size) |max| .{ .file = repo.core.db_file, .max = max } else null, 167 }; 168 // the moment index the pushed events were consumed into 169 var moment_index: ?u64 = null; 170 const Ctx = struct { 171 updates: *xit.net_server_receive_pack.AppliedRefUpdates, 172 core: *rp.Repo(.xit, repo_opts).Core, 173 io: std.Io, 174 allocator: std.mem.Allocator, 175 reader: *std.Io.Reader, 176 writer: *std.Io.Writer, 177 options: xit.net_server_receive_pack.Options, 178 author: evt.CommitAuthor, 179 response: *xit.net_server_receive_pack.Response, 180 server: evt.Host.Server, 181 error_writer: *std.Io.Writer, 182 sideband: *progress.Sideband, 183 moment_index: *?u64, 184 185 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 186 var moment = try DB.HashMap(.read_write).init(cursor.*); 187 const state = State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 188 189 var receive_options = ctx.options; 190 receive_options.applied_ref_updates = ctx.updates; 191 receive_options.deferred_response = ctx.response; 192 try xit.net_server_receive_pack.run(.xit, repo_opts, state, ctx.io, ctx.allocator, ctx.reader, ctx.writer, receive_options, ctx.sideband); 193 // receive-pack refuses the clear ref alongside any other update. 194 // clearing writes nothing here, and runs once this transaction 195 // lets go of the lock. 196 const applied = ctx.updates.items.items; 197 if (applied.len == 1 and std.mem.eql(u8, applied[0].ref_name, clear_undo_ref)) return error.ClearUndoHistory; 198 try adoptPushedBranch(repo_opts, state, ctx.io, applied); 199 try writeReceivedPatches(repo_opts, state, ctx.io, ctx.allocator, ctx.response, ctx.writer); 200 if (ctx.sideband.gone) return error.ClientGone; 201 202 // a repo without an events branch has no events to consume. a 203 // no-op consume must not cancel here, since the push shares the 204 // transaction and must commit regardless. 205 if (null != try rf.readRecur(.xit, repo_opts, state.readOnly(), ctx.io, .{ .ref = evt.events_ref })) { 206 ctx.moment_index.* = evt.consumeInTransaction(.repo, .xit, repo_opts, state, &ctx.core.db, &moment, ctx.io, ctx.allocator, evt.events_ref) catch |err| switch (err) { 207 error.CancelTransaction => null, 208 else => |e| return e, 209 }; 210 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); 211 212 // the revisions the pushed branches cause belong to the push 213 _ = try pch.refreshBranchesInTransaction(.{ .server = ctx.server }, repo_opts, state, &moment, ctx.io, ctx.allocator, ctx.updates.items.items, null, ctx.sideband); 214 } 215 216 if (ctx.sideband.gone) return error.ClientGone; 217 218 // the indexes the pushed refs invalidate belong to the push too 219 _ = try find.refreshInTransaction(repo_opts, state, &moment, ctx.io, ctx.allocator, ctx.sideband); 220 var moves: std.ArrayList(srch_cmmt.Move) = .empty; 221 defer moves.deinit(ctx.allocator); 222 for (ctx.updates.items.items) |update| try moves.append(ctx.allocator, .{ .old_oid = update.old_oid, .new_oid = update.new_oid }); 223 _ = try srch_cmmt.refreshInTransaction(repo_opts, state, &moment, ctx.io, ctx.allocator, moves.items, ctx.sideband); 224 225 try writeUndo(repo_opts, state, ctx.io, ctx.allocator, ctx.author, ctx.updates.items.items); 226 // the indexes count against the limit too 227 try ctx.sideband.checkSize(ctx.io); 228 } 229 }; 230 231 const result = blk: { 232 try repo.core.db_file.lock(io, .exclusive); 233 defer repo.core.db_file.unlock(io); 234 235 const history = try DB.ArrayList(.read_write).init(repo.core.db.rootCursor()); 236 break :blk history.appendContext( 237 .{ .slot = try history.getSlot(-1) }, 238 Ctx{ 239 .updates = &updates, 240 .core = &repo.core, 241 .io = io, 242 .allocator = allocator, 243 .reader = reader, 244 .writer = writer, 245 .options = options, 246 .author = author, 247 .response = &response, 248 .server = server, 249 .error_writer = error_writer, 250 .sideband = &sideband, 251 .moment_index = &moment_index, 252 }, 253 ); 254 }; 255 result catch |err| switch (err) { 256 // a refused push has already written the report saying why 257 error.CancelTransaction => return response.finish(writer, null), 258 error.ClearUndoHistory => { 259 clearUndoHistory(repo_opts, io, allocator, repo, &sideband) catch |clear_err| { 260 response.finish(writer, @errorName(clear_err)) catch {}; 261 return clear_err; 262 }; 263 return response.finish(writer, null); 264 }, 265 else => { 266 response.finish(writer, @errorName(err)) catch {}; 267 return err; 268 }, 269 }; 270 if (moment_index) |index| notify.deliverTransaction(io, allocator, server, repo_opts, repo, index, pusher_id, &sideband); 271 evt.bumpRepoActivityAt(io, allocator, server, repo.core.work_path); 272 273 pch.refreshBranches(.{ .server = server }, .xit, repo_opts, io, allocator, repo, updates.items.items, &sideband) catch |err| { 274 serve_common.logError(io, error_writer, "failed to refresh branch patches: {s}\n", .{@errorName(err)}); 275 pch.refreshOpenMergeability(repo_opts, io, allocator, repo, server.users_dir, null, &sideband); 276 }; 277 try response.finish(writer, null); 278 } 279 280 // discard every earlier transaction by compacting the database. the patch 281 // revisions events point at are unreachable from the refs, so they are named 282 // as extra roots to survive the collection. 283 fn clearUndoHistory(comptime repo_opts: rp.RepoOpts(.xit), io: std.Io, allocator: std.mem.Allocator, repo: *rp.Repo(.xit, repo_opts), sideband: *progress.Sideband) !void { 284 // a repo that has consumed no events has no revisions to protect 285 const roots = if (evt.currentMoment(repo_opts, repo)) |moment| 286 try evt.PatchRev.gcRoots(rp.Repo(.xit, repo_opts).DB, repo_opts.hash, allocator, moment) 287 else |_| 288 &.{}; 289 defer allocator.free(roots); 290 _ = try repo.garbageCollect(io, allocator, .{ .extra_roots = roots }, sideband); 291 } 292 293 pub fn receiveFork( 294 comptime repo_opts: rp.RepoOpts(.xit), 295 io: std.Io, 296 allocator: std.mem.Allocator, 297 fork_repo: *rp.Repo(.xit, repo_opts), 298 target_repo: *rp.Repo(.xit, repo_opts), 299 server: evt.Host.Server, 300 id: *const [evt.event_id_size * 2]u8, 301 author: evt.CommitAuthor, 302 timestamp: u64, 303 reader: *std.Io.Reader, 304 writer: *std.Io.Writer, 305 max_repo_size: ?u64, 306 error_writer: *std.Io.Writer, 307 sess: ?*ssh.SessionCtx, 308 ) !void { 309 const patch_id = try evt.parseEventId(id); 310 var arena = std.heap.ArenaAllocator.init(allocator); 311 defer arena.deinit(); 312 313 // load the fork and target patch state 314 const fork_moment = evt.currentMoment(repo_opts, fork_repo) catch return error.PatchDataUnavailable; 315 const fork_patch = (try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena, &patch_id)) orelse return error.PatchDataUnavailable; 316 const target_moment = evt.currentMoment(repo_opts, target_repo) catch |err| switch (err) { 317 error.NotFound => null, 318 else => |other| return other, 319 }; 320 const target_patch = if (target_moment) |moment| 321 try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, moment, &arena, &patch_id) 322 else 323 null; 324 if (fork_patch.removed and target_patch == null) return error.PatchDataUnavailable; 325 if (target_patch) |patch| { 326 if (!std.mem.eql(u8, patch.author_email orelse "", author.email)) return error.PatchDataUnavailable; 327 if (patch.event.status.kind() == .merged) return error.PatchAlreadyMerged; 328 } 329 const published = target_patch != null; 330 const active_patch = target_patch orelse fork_patch; 331 const title = active_patch.event.title; 332 333 // resolve the target and the newest fork revision 334 const target_branch = active_patch.event.target_branch; 335 const target_oid = (try target_repo.readRef(io, .{ .kind = .head, .name = target_branch })) orelse return error.TargetNotFound; 336 const newest = try evt.PatchRev.readNewest(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_moment, &arena); 337 var revision_id_maybe: ?[evt.event_id_size]u8 = null; 338 var response = xit.net_server_receive_pack.Response.init(allocator); 339 defer response.deinit(); 340 var sideband = progress.Sideband{ 341 .response = &response, 342 .writer = writer, 343 .sess = sess, 344 .size_limit = if (max_repo_size) |max| .{ .file = fork_repo.core.db_file, .max = max } else null, 345 }; 346 347 // execute a transaction that receives the push and materializes its revision 348 const result = blk: { 349 const DB = rp.Repo(.xit, repo_opts).DB; 350 const State = rp.Repo(.xit, repo_opts).State; 351 const Ctx = struct { 352 core: *rp.Repo(.xit, repo_opts).Core, 353 target_core: *rp.Repo(.xit, repo_opts).Core, 354 io: std.Io, 355 allocator: std.mem.Allocator, 356 reader: *std.Io.Reader, 357 writer: *std.Io.Writer, 358 response: *xit.net_server_receive_pack.Response, 359 patch_id: [evt.event_id_size * 2]u8, 360 patch: evt.Patch.Record, 361 published: bool, 362 target_oid: [hash.hexLen(repo_opts.hash)]u8, 363 title: []const u8, 364 author: evt.CommitAuthor, 365 timestamp: u64, 366 newest: ?evt.PatchRev.WithId, 367 revision_id_maybe: *?[evt.event_id_size]u8, 368 sideband: *progress.Sideband, 369 370 pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { 371 // receive the branch update 372 var moment = try DB.HashMap(.read_write).init(cursor.*); 373 const state = State(.read_write){ .core = ctx.core, .extra = .{ .moment = &moment } }; 374 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); 375 try writeReceivedPatches(repo_opts, state, ctx.io, ctx.allocator, ctx.response, ctx.writer); 376 if (ctx.sideband.gone) return error.ClientGone; 377 378 // copy the target history needed to preserve the merge base, 379 // skipping what the fork already holds 380 const source_oid = (try rf.readRecur(.xit, repo_opts, state.readOnly(), ctx.io, .{ .ref = fork.ref })) orelse return error.CancelTransaction; 381 var target_repo_moment = try ctx.target_core.latestMoment(); 382 const target_state = State(.read_only){ .core = ctx.target_core, .extra = .{ .moment = &target_repo_moment } }; 383 var objects = try obj.ObjectIterator(.xit, repo_opts).init(target_state, ctx.io, ctx.allocator, .{ .kind = .all }); 384 defer objects.deinit(); 385 try pch.includeMissing(repo_opts, &objects, state.readOnly(), target_state, ctx.io, ctx.allocator, &ctx.target_oid); 386 try obj.copyFromObjectIterator(.xit, repo_opts, state, .xit, repo_opts, &objects, ctx.io, ctx.sideband); 387 388 const base_oid = try mrg.commonAncestor(.xit, repo_opts, state.readOnly(), ctx.io, ctx.allocator, &ctx.target_oid, &source_oid); 389 const existing_revision = if (ctx.newest) |latest| 390 if (std.mem.eql(u8, latest.record.event.base_oid, &base_oid) and 391 std.mem.eql(u8, latest.record.event.source_oid, &source_oid)) latest else null 392 else 393 null; 394 395 // reuse an identical revision or record a new one 396 var revision_arena = std.heap.ArenaAllocator.init(ctx.allocator); 397 defer revision_arena.deinit(); 398 var events: [2]evt.EventWithId = undefined; 399 var event_count: usize = 0; 400 if (existing_revision) |latest| { 401 if (ctx.published) ctx.revision_id_maybe.* = latest.id; 402 } else { 403 const prepared = try evt.PatchRev.prepare(.xit, repo_opts, state, ctx.io, &revision_arena, &base_oid, &source_oid, ctx.title, ctx.author, ctx.timestamp); 404 events[event_count] = prepared.event; 405 event_count += 1; 406 ctx.revision_id_maybe.* = try evt.parseEventId(&prepared.event.id); 407 408 if (!ctx.published) { 409 var patch = ctx.patch.event; 410 patch.revision = prepared.revision; 411 events[event_count] = .{ 412 .id = ctx.patch_id, 413 .timestamp = ctx.timestamp, 414 .author = ctx.author, 415 .event = .{ .patch = patch }, 416 }; 417 event_count += 1; 418 } 419 } 420 421 // published patches keep only revisions in the fork 422 if (ctx.published and !ctx.patch.removed) { 423 events[event_count] = .{ 424 .id = ctx.patch_id, 425 .timestamp = ctx.timestamp, 426 .author = ctx.author, 427 .event = .{ .patch = null }, 428 }; 429 event_count += 1; 430 } 431 432 // commit and index any new events 433 if (event_count > 0) { 434 try evt.commitEvents(.xit, repo_opts, state, ctx.io, ctx.allocator, evt.events_ref, events[0..event_count], null); 435 _ = try evt.consumeInTransaction(.fork, .xit, repo_opts, state, &ctx.core.db, &moment, ctx.io, ctx.allocator, evt.events_ref); 436 } 437 // the patch branch's file index lands with the push 438 _ = try find.refreshInTransaction(repo_opts, state, &moment, ctx.io, ctx.allocator, ctx.sideband); 439 440 // a fork push tracks no ref updates, so it records no ranges 441 try writeUndo(repo_opts, state, ctx.io, ctx.allocator, ctx.author, &.{}); 442 // the copied target history and the index count against the limit too 443 try ctx.sideband.checkSize(ctx.io); 444 } 445 }; 446 447 try fork_repo.core.db_file.lock(io, .exclusive); 448 defer fork_repo.core.db_file.unlock(io); 449 450 const history = try DB.ArrayList(.read_write).init(fork_repo.core.db.rootCursor()); 451 break :blk history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ 452 .core = &fork_repo.core, 453 .target_core = &target_repo.core, 454 .io = io, 455 .allocator = allocator, 456 .reader = reader, 457 .writer = writer, 458 .response = &response, 459 .patch_id = id.*, 460 .patch = fork_patch, 461 .published = published, 462 .target_oid = target_oid, 463 .title = title, 464 .author = author, 465 .timestamp = timestamp, 466 .newest = newest, 467 .revision_id_maybe = &revision_id_maybe, 468 .sideband = &sideband, 469 }); 470 }; 471 result catch |err| switch (err) { 472 // a refused push has already written the report saying why 473 error.CancelTransaction => return response.finish(writer, null), 474 else => { 475 response.finish(writer, @errorName(err)) catch {}; 476 return err; 477 }, 478 }; 479 480 if (!published) return response.finish(writer, null); 481 482 sideband.run(io, .{ .text = "Updating patches" }) catch {}; 483 sideband.run(io, .{ .start = .{ .kind = .writing_patch, .estimated_total_items = 1 } }) catch {}; 484 485 // best-effort update the published patch so it has the new revision 486 var refreshed = false; 487 update: { 488 const revision_id = revision_id_maybe orelse break :update; 489 var update_arena = std.heap.ArenaAllocator.init(allocator); 490 defer update_arena.deinit(); 491 const target_update_moment = evt.currentMoment(repo_opts, target_repo) catch break :update; 492 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; 493 if (published_patch.removed) break :update; 494 const fork_update_moment = evt.currentMoment(repo_opts, fork_repo) catch break :update; 495 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; 496 if (revision.removed) break :update; 497 const selected = evt.Patch.Revision.fromRecord(revision_id, revision); 498 if (published_patch.event.revision) |current| { 499 if (evt.fieldEqual(evt.Patch.Revision, current, selected)) break :update; 500 } 501 var patch = published_patch.event; 502 patch.revision = selected; 503 evt.consume(.{ .server = server }, .repo, .xit, repo_opts, io, allocator, target_repo, evt.events_ref, &.{.{ 504 .id = id.*, 505 .timestamp = timestamp, 506 .author = author, 507 .event = .{ .patch = patch }, 508 }}) catch |update_err| { 509 serve_common.logError(io, error_writer, "failed to update published patch {s}: {s}\n", .{ id, @errorName(update_err) }); 510 break :update; 511 }; 512 // consume also refreshed mergeability 513 refreshed = true; 514 } 515 516 if (!refreshed) pch.refreshMergeability(repo_opts, io, allocator, target_repo, server.users_dir, patch_id); 517 sideband.run(io, .{ .complete_one = .writing_patch }) catch {}; 518 sideband.run(io, .{ .end = .writing_patch }) catch {}; 519 520 // the client may read either repo as soon as it gets the final response. 521 try response.finish(writer, null); 522 } 523