╭ changed ───╮ │src/fork.zig│ ╰────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────╮ │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; │ ╰────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────╮ │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;│ │221 - │ │224 const target_moment = evt.currentMoment(repo_opts, target_repo) catch |err| switch (err) { │ │225 error.NotFound => null, │ │226 else => |other| return other, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 │ │243 - const DB = rp.Repo(.xit, repo_opts).DB; │ │244 - const State = rp.Repo(.xit, repo_opts).State; │ │245 - const Ctx = struct { │ │246 - core: *rp.Repo(.xit, repo_opts).Core, │ │247 - target_core: *rp.Repo(.xit, repo_opts).Core, │ │248 - io: std.Io, │ │249 - allocator: std.mem.Allocator, │ │250 - reader: *std.Io.Reader, │ │251 - writer: *std.Io.Writer, │ │252 - patch_id: [evt.event_id_size * 2]u8, │ │253 - patch: evt.Patch.Record, │ │254 - posted: bool, │ │255 - target_oid: [hash.hexLen(repo_opts.hash)]u8, │ │256 - target_ref: []const u8, │ │257 - title: []const u8, │ │258 - author: evt.CommitAuthor, │ │259 - timestamp: u64, │ │260 - newest: ?evt.PatchRev.WithId, │ │261 - revision_id_maybe: *?[evt.event_id_size]u8, │ │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 │ │263 - pub fn run(ctx: @This(), cursor: *DB.Cursor(.read_write)) !void { │ │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 }); │ │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 │ │268 - const source_oid = (try rf.readRecur(.xit, repo_opts, state.readOnly(), ctx.io, .{ .ref = ref })) orelse return error.CancelTransaction; │ │269 - var target_repo_moment = try ctx.target_core.latestMoment(); │ │270 - const target_state = State(.read_only){ .core = ctx.target_core, .extra = .{ .moment = &target_repo_moment } }; │ │271 - var objects = try obj.ObjectIterator(.xit, repo_opts).init(target_state, ctx.io, ctx.allocator, .{ .kind = .all }); │ │272 - defer objects.deinit(); │ │273 - try objects.include(&ctx.target_oid); │ │274 - try obj.copyFromObjectIterator(.xit, repo_opts, state, .xit, repo_opts, &objects, ctx.io, null); │ │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 │ │276 - const base_oid = try mrg.commonAncestor(.xit, repo_opts, state.readOnly(), ctx.io, ctx.allocator, &ctx.target_oid, &source_oid); │ │277 - const existing_revision = if (ctx.newest) |latest| │ │278 - if (std.mem.eql(u8, latest.record.event.base_oid, &base_oid) and │ │279 - std.mem.eql(u8, latest.record.event.source_oid, &source_oid) and │ │280 - std.mem.eql(u8, latest.record.event.target_ref, ctx.target_ref)) latest else null │ │281 - else │ │282 - null; │ │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 │ │284 - var events: [2]evt.EventWithId = undefined; │ │285 - var event_count: usize = 0; │ │286 - var base_tree_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; │ │287 - var head_tree_oid: [hash.hexLen(repo_opts.hash)]u8 = undefined; │ │288 - var tree_entries: [2]evt.EventTreeEntry = undefined; │ │289 - if (existing_revision) |latest| { │ │290 - if (ctx.posted) ctx.revision_id_maybe.* = latest.id; │ │291 - } else { │ │292 - var base_object = try obj.Object(.xit, repo_opts).initCommit(state.readOnly(), ctx.io, ctx.allocator, &base_oid); │ │293 - defer base_object.deinit(); │ │294 - var source_object = try obj.Object(.xit, repo_opts).initCommit(state.readOnly(), ctx.io, ctx.allocator, &source_oid); │ │295 - defer source_object.deinit(); │ │296 - base_tree_oid = base_object.content.commit.tree; │ │297 - head_tree_oid = source_object.content.commit.tree; │ │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 │ │299 - var revision_id: [evt.event_id_size]u8 = undefined; │ │300 - ctx.io.random(&revision_id); │ │301 - const revision_hex = std.fmt.bytesToHex(revision_id, .lower); │ │302 - const revision_event: evt.PatchRev = .{ │ │303 - .base_oid = &base_oid, │ │304 - .source_oid = &source_oid, │ │305 - .target_ref = ctx.target_ref, │ │306 - .message = ctx.title, │ │307 - }; │ │308 - const identity = try std.fmt.allocPrint(ctx.allocator, "{s} <{s}>", .{ ctx.author.name, ctx.author.email }); │ │309 - defer ctx.allocator.free(identity); │ │310 - const patch_oid = try evt.PatchRev.writeSquashCommit( │ │311 - .xit, │ │312 - repo_opts, │ │313 - state, │ │314 - ctx.io, │ │315 - ctx.allocator, │ │316 - revision_event, │ │317 - &head_tree_oid, │ │318 - identity, │ │319 - identity, │ │320 - ctx.timestamp, │ │321 - ); │ │322 - tree_entries = .{ │ │323 - .{ .tree = .{ .name = "base", .oid = &base_tree_oid } }, │ │324 - .{ .tree = .{ .name = "head", .oid = &head_tree_oid } }, │ │325 - }; │ │326 - events[event_count] = .{ │ │327 - .id = revision_hex, │ │328 - .timestamp = ctx.timestamp, │ │329 - .author = ctx.author, │ │330 - .tree_entries = &tree_entries, │ │331 - .event = .{ .patchrev = revision_event }, │ │332 - }; │ │333 - event_count += 1; │ │334 - ctx.revision_id_maybe.* = revision_id; │ │335 - │ │336 - if (!ctx.posted) { │ │337 - var patch = ctx.patch.event; │ │338 - patch.revision = .{ │ │339 - .id = revision_hex, │ │340 - .squash_oid = &patch_oid, │ │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, │ │348 - .event = .{ .patch = patch },│ │368 + .event = .{ .patch = null }, │ │369 }; │ │370 event_count += 1; │ │371 } │ ╰──────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │352 - } │ │372 │ │354 - if (ctx.posted and !ctx.patch.removed) { │ │355 - events[event_count] = .{ │ │356 - .id = ctx.patch_id, │ │357 - .timestamp = ctx.timestamp, │ │358 - .author = ctx.author, │ │359 - .event = .{ .patch = null }, │ │360 - }; │ │361 - event_count += 1; │ │362 - } │ │363 - if (event_count > 0) { │ │364 - try evt.commitEvents(.xit, repo_opts, state, ctx.io, ctx.allocator, evt.events_ref, events[0..event_count], null); │ │365 - if (!try evt.consumeInTransaction(.fork, .xit, repo_opts, state, &ctx.core.db, &moment, ctx.io, ctx.allocator, evt.events_ref)) return error.CancelTransaction; │ │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 } │ │367 - try xit.undo.writeMessage(repo_opts, state, .push); │ │368 - try ctx.core.chunk_store_file.sync(ctx.io); │ │369 - } │ │370 - }; │ │381 + }; │ │382 + │ │383 + try fork_repo.core.db_file.lock(io, .exclusive); │ │384 + defer fork_repo.core.db_file.unlock(io); │ │385 │ │372 - try fork_repo.core.db_file.lock(io, .exclusive); │ │373 - defer fork_repo.core.db_file.unlock(io); │ │374 - const history = try DB.ArrayList(.read_write).init(fork_repo.core.db.rootCursor()); │ │375 - history.appendContext(.{ .slot = try history.getSlot(-1) }, Ctx{ │ │376 - .core = &fork_repo.core, │ │377 - .target_core = &target_repo.core, │ │378 - .io = io, │ │379 - .allocator = allocator, │ │380 - .reader = reader, │ │381 - .writer = writer, │ │382 - .patch_id = id.*, │ │383 - .patch = fork_patch, │ │384 - .posted = posted, │ │385 - .target_oid = target_oid, │ │386 - .target_ref = target_ref, │ │387 - .title = title, │ │388 - .author = author, │ │389 - .timestamp = timestamp, │ │390 - .newest = newest, │ │391 - .revision_id_maybe = &revision_id_maybe, │ │392 - }) catch |err| switch (err) { │ │393 - error.CancelTransaction => {}, │ │394 - else => |other| return other, │ │395 - }; │ │396 - try writer.flush(); │ │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 │ │398 - const revision_id = revision_id_maybe orelse return; │ │399 - if (!posted) return; │ │400 - var update_arena = std.heap.ArenaAllocator.init(allocator); │ │401 - defer update_arena.deinit(); │ │402 - const update_moment = evt.currentMoment(repo_opts, target_repo) catch return; │ │403 - const posted_patch = (evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, update_moment, &update_arena, &patch_id) catch return) orelse return; │ │404 - if (posted_patch.removed) return; │ │405 - const fork_update_moment = evt.currentMoment(repo_opts, fork_repo) catch return; │ │406 - const revision = (evt.PatchRev.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, fork_update_moment, &update_arena, &revision_id) catch return) orelse return; │ │407 - if (revision.removed) return; │ │408 - const selected = evt.Patch.Revision.fromRecord(revision_id, revision); │ │409 - if (posted_patch.event.revision) |current| { │ │410 - if (evt.fieldEqual(evt.Patch.Revision, current, selected)) return; │ │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 } │ │412 - var patch = posted_patch.event; │ │413 - patch.revision = selected; │ │414 - evt.consume(.repo, .xit, repo_opts, io, allocator, target_repo, evt.events_ref, &.{.{ │ │415 - .id = id.*, │ │416 - .timestamp = timestamp, │ │417 - .author = author, │ │418 - .event = .{ .patch = patch }, │ │419 - }}) catch {}; │ │438 } │ │439 │ │440 // delete first so a failed tombstone can be retried safely │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯