╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │271 │ │272 const haxy_moment = try DB.HashMap(.read_write).init(haxy_moment_cursor); │ │273 │ │274 - // merge changes from every parent after the first parent. the first parent │ │275 - // is the baseline, so a later parent only contributes values it changed. │ │274 + // merge changes from every parent after the first parent. the common │ │275 + // ancestor is the baseline, so a later parent only contributes values │ │276 + // it changed relative to the merge base. │ │277 if (repo_event.parent_oids.len > 1) { │ │277 - var first_parent_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; │ │278 - _ = try std.fmt.hexToBytes(&first_parent_oid, &repo_event.parent_oids[0]); │ │279 - const first_parent_haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &first_parent_oid)) orelse return error.CursorNotFound;│ │280 - const first_parent_haxy_moment = try DB.HashMap(.read_only).init(first_parent_haxy_moment_cursor); │ │281 - │ │278 for (repo_event.parent_oids[1..]) |*parent_oid| { │ │279 var oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; │ │280 _ = try std.fmt.hexToBytes(&oid, parent_oid); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │282 const parent_haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &oid)) orelse return error.CursorNotFound; │ │283 const parent_haxy_moment = try DB.HashMap(.read_only).init(parent_haxy_moment_cursor); │ │284 │ │289 - try mergeChangedMapEntries(DB, haxy_moment, parent_haxy_moment, first_parent_haxy_moment, true); │ │285 + const baseline_oid_hex = try mrg.commonAncestor(.xit, repo_opts, state.readOnly(), io, allocator, &repo_event.parent_oids[0], parent_oid); │ │286 + var baseline_oid: [hash.byteLen(repo_opts.hash)]u8 = undefined; │ │287 + _ = try std.fmt.hexToBytes(&baseline_oid, &baseline_oid_hex); │ │288 + const baseline_haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &baseline_oid)) orelse return error.CursorNotFound;│ │289 + const baseline_haxy_moment = try DB.HashMap(.read_only).init(baseline_haxy_moment_cursor); │ │290 + │ │291 + try mergeChangedMapEntries(DB, haxy_moment, parent_haxy_moment, baseline_haxy_moment, true); │ │292 } │ │293 } │ │294 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │366 const kv_pair = try kv_pair_cursor.readKeyValuePair(); │ │367 │ │368 const baseline_value_cursor = try baseline.getCursor(kv_pair.hash) orelse {│ │369 + if ((try target.getCursor(kv_pair.hash)) != null) { │ │370 + return error.MergeConflict; │ │371 + } │ │372 + │ │373 try target.put(kv_pair.hash, .{ .slot = kv_pair.value_cursor.slot() });│ │374 continue; │ │375 }; │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────╮ │376 │ │371 - if (baseline_value_cursor.slot().value == kv_pair.value_cursor.slot().value) {│ │372 - continue; │ │373 - } │ │377 + const tag = kv_pair.value_cursor.slot().tag; │ │378 │ │375 - if (kv_pair.value_cursor.slot().tag != baseline_value_cursor.slot().tag) { │ │379 + if (tag != baseline_value_cursor.slot().tag) { │ │380 return error.UnexpectedTag; │ │381 } │ │382 │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │379 - const tag = kv_pair.value_cursor.slot().tag; │ │383 + if (kv_pair.value_cursor.slot().value == baseline_value_cursor.slot().value) { │ │384 + continue; │ │385 + } │ │386 │ │387 if (tag == .hash_map) { │ │388 + const target_existing_cursor = try target.getCursor(kv_pair.hash) orelse return error.MergeConflict;│ │389 + if (target_existing_cursor.slot().tag != .hash_map) { │ │390 + return error.MergeConflict; │ │391 + } │ │392 + │ │393 const target_child_cursor = try target.putCursor(kv_pair.hash); │ │394 const target_child = try DB.HashMap(.read_write).init(target_child_cursor); │ │395 const parent_child = try DB.HashMap(.read_only).init(kv_pair.value_cursor); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │396 const baseline_child = try DB.HashMap(.read_only).init(baseline_value_cursor); │ │397 try mergeChangedMapEntries(DB, target_child, parent_child, baseline_child, false); │ │398 } else if (is_top_level) { │ │399 + // the moment-index is a top-level key in the haxy moment │ │400 + // and is not a map. we don't want to do any merge of this. │ │401 continue; │ │402 } else { │ │403 + const target_value_cursor = try target.getCursor(kv_pair.hash) orelse return error.MergeConflict;│ │404 + if (target_value_cursor.slot().value != baseline_value_cursor.slot().value) { │ │405 + return error.MergeConflict; │ │406 + } │ │407 + │ │408 try target.put(kv_pair.hash, .{ .slot = kv_pair.value_cursor.slot() }); │ │409 } │ │410 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/test/event.zig│ ╰──────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │565 // check out the haxy/meta branch and merge the haxy/other branch into it │ │566 // │ │567 │ │568 - { │ │568 + const merge_oid = blk: { │ │569 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "haxy/meta" } } });│ │570 defer result.deinit(); │ │571 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────╮ │573 defer merge.deinit(); │ │574 │ │575 try std.testing.expect(.success == merge.result);│ │576 - } │ │576 │ │577 + break :blk merge.result.success.oid; │ │578 + }; │ │579 + │ │580 // │ │581 // consume events into the database │ │582 // │ ╰──────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │705 try std.testing.expect(null == first_issue_cursor); │ │706 } │ │707 } │ │708 + │ │709 + // │ │710 + // define test events that modify the same issue in conflicting ways │ │711 + // │ │712 + │ │713 + const events_to_consume3 = [_]evt.Event{ │ │714 + .{ │ │715 + .id = events_to_consume[0].id, │ │716 + .data = .{ │ │717 + .issue = .{ │ │718 + .title = "Login form clears password on validation error", │ │719 + .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", │ │720 + .tags = "bug\x00priority-low\x00ui", │ │721 + }, │ │722 + }, │ │723 + }, │ │724 + .{ │ │725 + .id = events_to_consume[0].id, │ │726 + .data = .{ │ │727 + .issue = .{ │ │728 + .title = "Login form clears password on validation error", │ │729 + .description = "Submitting an invalid email address resets the password field. Preserve the field value and show an inline validation message.", │ │730 + .tags = "bug\x00priority-medium\x00ui", │ │731 + }, │ │732 + }, │ │733 + }, │ │734 + }; │ │735 + │ │736 + // │ │737 + // insert issues as commits in the repo on different branches │ │738 + // │ │739 + │ │740 + { │ │741 + var json: std.Io.Writer.Allocating = .init(std.testing.allocator); │ │742 + defer json.deinit(); │ │743 + │ │744 + for (events_to_consume3, 0..) |event, i| { │ │745 + json.clearRetainingCapacity(); │ │746 + │ │747 + try std.json.Stringify.value(event, .{}, &json.writer); │ │748 + │ │749 + // commit the event into a special branch │ │750 + _ = try repo.commitAtRef( │ │751 + io, │ │752 + allocator, │ │753 + .{ .parent_oids = &.{merge_oid}, .message = json.written() }, │ │754 + null, │ │755 + .{ .kind = .head, .name = switch (i) { │ │756 + 0 => "haxy/meta", │ │757 + 1 => "haxy/other", │ │758 + else => unreachable, │ │759 + } }, │ │760 + ); │ │761 + } │ │762 + } │ │763 + │ │764 + // │ │765 + // check out the haxy/meta branch and merge the haxy/other branch into it │ │766 + // │ │767 + │ │768 + { │ │769 + var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = .{ .kind = .head, .name = "haxy/meta" } } }); │ │770 + defer result.deinit(); │ │771 + │ │772 + var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = .{ .kind = .head, .name = "haxy/other" } }} } } }, null);│ │773 + defer merge.deinit(); │ │774 + │ │775 + try std.testing.expect(.success == merge.result); │ │776 + } │ │777 + │ │778 + // │ │779 + // consume events into the database │ │780 + // │ │781 + │ │782 + try std.testing.expectError(error.MergeConflict, evt.consume(repo_opts, io, allocator, &repo, .{ .kind = .head, .name = "haxy/meta" })); │ │783 } │ │784 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯