╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────╮ │src/patch.zig│ ╰─────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │791 const fork_path = try fork.forkPath(arena.allocator(), repo_root_path, &patch_hex); │ │792 const events = try evt.currentMomentFromRepoMoment(repo_opts.hash, moment.readOnly()); │ │793 const record = (try evt.Patch.readById(evt.EventDB(repo_opts.hash), repo_opts.hash, events, &arena, patch_id)) orelse return; │ │794 + const selected = record.event.revision orelse return; │ │795 + const revision: evt.Patch.MergeRevision = if (std.mem.eql(u8, matched_oid, selected.source_oid)) .source else if (std.mem.eql(u8, matched_oid, selected.squash_oid)) .squash else return; │ │796 var fork_repo_maybe: ?rp.Repo(.xit, repo_opts) = if (record.event.source_branch != null) null else try rp.Repo(.xit, repo_opts).open(io, allocator, .{ .path = fork_path, .require_repo_root = true });│ │797 defer if (fork_repo_maybe) |*repo| repo.deinit(io, allocator); │ │798 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │795 - _ = try importMergedRevision(repo_opts, state, db, moment, io, allocator, if (fork_repo_maybe) |*repo| repo else null, patch_id, merged_oid, null, null, 0); │ │799 + _ = try importMergedRevision(repo_opts, state, db, moment, io, allocator, if (fork_repo_maybe) |*repo| repo else null, patch_id, .{ .revision = revision, .before_oid = before_oid, .after_oid = after_oid }, null, null, 0);│ │800 } │ │801 │ │802 // import the revision and mark its patch merged in the current transaction │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────╮ │809 allocator: std.mem.Allocator, │ │810 fork_repo: ?*rp.Repo(.xit, repo_opts), │ │811 patch_id: *const [evt.event_id_size]u8, │ │808 - merged_oid: *const [hash.hexLen(repo_opts.hash)]u8,│ │812 + merged: evt.Patch.Status.Merged, │ │813 expected_patch: ?evt.Patch, │ │814 merged_author: ?evt.CommitAuthor, │ │815 merged_timestamp: u64, │ ╰────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │830 } │ │831 } │ │832 const revision_id = try evt.parseEventId(&selected.id); │ │829 - if (!selected.includesOid(merged_oid)) return false; │ │833 │ │834 const current_revision = try evt.PatchRev.readById(DB, repo_opts.hash, haxy_moment, &arena, &revision_id);│ │835 const revision = (if (fork_repo) |repo| │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │870 } │ │871 │ │872 var patch = patch_record.event; │ │870 - patch.status = .{ .merged = merged_oid }; │ │873 + patch.status = .{ .merged = merged }; │ │874 events[event_count] = .{ │ │875 .id = std.fmt.bytesToHex(patch_id.*, .lower), │ │876 .timestamp = if (merged_author != null) merged_timestamp else @intCast(std.Io.Timestamp.now(io, .real).toSeconds()),│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │921 var commits = try obj.ObjectIterator(.xit, repo_opts).init(state.readOnly(), io, arena.allocator(), .{ .kind = .commit }); │ │922 defer commits.deinit(); │ │923 if (!std.mem.allEqual(u8, &old_oid, '0')) try commits.exclude(&old_oid); │ │921 - try commits.include(&new_oid); │ │924 + var targets: std.ArrayList(struct { │ │925 + before_oid: [hash.hexLen(repo_opts.hash)]u8, │ │926 + after_oid: [hash.hexLen(repo_opts.hash)]u8, │ │927 + }) = .empty; │ │928 + { │ │929 + var first_parents = try obj.ObjectIterator(.xit, repo_opts).init(state.readOnly(), io, arena.allocator(), .{ .kind = .commit, .first_parent = true });│ │930 + defer first_parents.deinit(); │ │931 + if (!std.mem.allEqual(u8, &old_oid, '0')) try first_parents.exclude(&old_oid); │ │932 + try first_parents.include(&new_oid); │ │933 + while (try first_parents.next(arena.allocator())) |commit| { │ │934 + defer commit.deinit(); │ │935 + try targets.append(arena.allocator(), .{ │ │936 + .before_oid = (commit.content.commit.metadata.firstParent() orelse &old_oid).*, │ │937 + .after_oid = commit.oid, │ │938 + }); │ │939 + } │ │940 + } │ │941 + │ │942 + // visit target commits oldest first, attributing each revision to the │ │943 + // commit that introduced it; the shared iterator visits each ancestor once │ │944 var revision_key_buffer: [rf.MAX_REF_CONTENT_SIZE + 1 + hash.hexLen(repo_opts.hash)]u8 = undefined; │ │923 - while (try commits.next(arena.allocator())) |commit| { │ │924 - defer commit.deinit(); │ │925 - const key = try std.fmt.bufPrint(&revision_key_buffer, "{s}\x00{s}", .{ update.ref_name[heads_prefix.len..], &commit.oid }); │ │926 - const ids_cursor = try revisions.getCursor(hash.hashInt(repo_opts.hash, key)) orelse continue; │ │927 - const ids = try DB.CountedHashSet(.read_only).init(ids_cursor); │ │928 - var ids_iter = try ids.iterator(); │ │929 - while (try ids_iter.next()) |cursor| { │ │930 - const pair = try cursor.readKeyValuePair(); │ │931 - var patch_id: [evt.event_id_size]u8 = undefined; │ │932 - if ((try pair.key_cursor.readBytes(&patch_id)).len != patch_id.len) return error.InvalidPatch; │ │933 - const patch_hex = std.fmt.bytesToHex(patch_id, .lower); │ │934 - importMerged(repo_opts, state, db, moment, io, allocator, repo_root_path, &patch_id, &commit.oid) catch |err| { │ │935 - serve_common.logError(io, error_writer, "failed to mark patch {s} as merged: {s}\n", .{ &patch_hex, @errorName(err) }); │ │936 - continue; │ │937 - }; │ │945 + while (targets.pop()) |target| { │ │946 + try commits.include(&target.after_oid); │ │947 + while (try commits.next(arena.allocator())) |commit| { │ │948 + defer commit.deinit(); │ │949 + const key = try std.fmt.bufPrint(&revision_key_buffer, "{s}\x00{s}", .{ update.ref_name[heads_prefix.len..], &commit.oid }); │ │950 + const ids_cursor = try revisions.getCursor(hash.hashInt(repo_opts.hash, key)) orelse continue; │ │951 + const ids = try DB.CountedHashSet(.read_only).init(ids_cursor); │ │952 + var ids_iter = try ids.iterator(); │ │953 + while (try ids_iter.next()) |cursor| { │ │954 + const pair = try cursor.readKeyValuePair(); │ │955 + var patch_id: [evt.event_id_size]u8 = undefined; │ │956 + if ((try pair.key_cursor.readBytes(&patch_id)).len != patch_id.len) return error.InvalidPatch; │ │957 + const patch_hex = std.fmt.bytesToHex(patch_id, .lower); │ │958 + // a fast-forward has no recorded merge boundary; use the old ref tip │ │959 + const before_oid = if (std.mem.eql(u8, &commit.oid, &target.after_oid)) &old_oid else &target.before_oid; │ │960 + importMerged(repo_opts, state, db, moment, io, allocator, repo_root_path, &patch_id, &commit.oid, before_oid, &target.after_oid) catch |err| {│ │961 + serve_common.logError(io, error_writer, "failed to mark patch {s} as merged: {s}\n", .{ &patch_hex, @errorName(err) }); │ │962 + continue; │ │963 + }; │ │964 + } │ │965 } │ │966 } │ │967 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/test/event.zig│ ╰──────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 6 const hash = xit.hash; │ │ 7 │ │ 8 const author = evt.CommitAuthor{ .name = "haxy", .email = "user@haxy" }; │ │ 9 + │ │10 +test "union field storage" { │ │11 + const allocator = std.testing.allocator; │ │12 + var arena = std.heap.ArenaAllocator.init(allocator); │ │13 + defer arena.deinit(); │ │14 + │ │15 + const DB = xit.xitdb.Database(.memory, hash.HashInt(.sha1)); │ │16 + var buffer = std.Io.Writer.Allocating.init(allocator); │ │17 + defer buffer.deinit(); │ │18 + var db = try DB.init(.{ .buffer = &buffer, .max_size = 1_000_000 }); │ │19 + const map = try DB.HashMap(.read_write).init(db.rootCursor()); │ │20 + │ │21 + const Value = union(enum) { │ │22 + empty, │ │23 + finished, │ │24 + data: struct { message: []const u8, note: ?[]const u8 = null }, │ │25 + number: ?u64, │ │26 + }; │ │27 + const Data = struct { value: ?Value }; │ │28 + const key = hash.hashInt(.sha1, "value"); │ │29 + │ │30 + // changing variants must replace the old tag and payload │ │31 + for ([_]?Value{ │ │32 + null, │ │33 + .empty, │ │34 + .{ .data = .{ .message = "first", .note = "note" } }, │ │35 + .{ .data = .{ .message = "second" } }, │ │36 + .{ .number = 42 }, │ │37 + .{ .number = null }, │ │38 + .{ .number = 7 }, │ │39 + .finished, │ │40 + .{ .data = .{ .message = "third" } }, │ │41 + null, │ │42 + }) |value| { │ │43 + try evt.upsert(Data, DB, .sha1, map, .{ .value = value }); │ │44 + const stored = try evt.read(Data, DB, .sha1, &arena, map.readOnly()); │ │45 + try std.testing.expectEqualDeep(value, stored.value); │ │46 + if (value) |active| { │ │47 + const cursor = (try map.getCursor(key)) orelse return error.NotFound; │ │48 + switch (active) { │ │49 + .empty, .finished => try std.testing.expectEqualStrings(@tagName(active), try cursor.readBytesAlloc(arena.allocator(), null)),│ │50 + else => try std.testing.expectEqual(.hash_map, cursor.slot().tag), │ │51 + } │ │52 + } else { │ │53 + try std.testing.expectEqual(null, try map.getCursor(key)); │ │54 + } │ │55 + } │ │56 + │ │57 + // a payload-bearing variant cannot be stored as just its name │ │58 + try map.put(key, .{ .bytes = "data" }); │ │59 + try std.testing.expectError(error.InvalidUnion, evt.read(Data, DB, .sha1, &arena, map.readOnly())); │ │60 +} │ │61 │ │62 test "rebase" { │ │63 const io = std.testing.io; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/test/event/patch.zig│ ╰────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │178 try testBranchMerge(.squash); │ │179 } │ │180 │ │181 -fn testBranchMerge(selection: pch.MergeRevision) !void { │ │181 +fn testBranchMerge(selection: evt.Patch.MergeRevision) !void { │ │182 const io = std.testing.io; │ │183 const allocator = std.testing.allocator; │ │184 const opts: rp.RepoOpts(.xit) = .{ .is_test = true, .ProgressCtx = *push.PushProgress };│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │238 var moment = try repo.core.latestMoment(); │ │239 const result = try pch.readMergeability(opts, .{ .core = &repo.core, .extra = .{ .moment = &moment } }, io, &arena, &id, before.event);│ │240 try std.testing.expectEqualDeep(pch.Mergeability{ .source = .clean, .squash = .clean }, result); │ │241 + } │ │242 + │ │243 + // a diverged target produces a merge commit in both merge styles │ │244 + { │ │245 + const file = try repo.core.work_dir.createFile(io, "target.txt", .{}); │ │246 + defer file.close(io); │ │247 + try file.writeStreamingAll(io, "target only\n"); │ │248 } │ │249 + try repo.add(io, allocator, &.{"target.txt"}); │ │250 + const target_oid = try repo.commit(io, allocator, .{ .message = "advance target" }); │ │251 + pch.refreshMergeability(opts, io, allocator, &repo, id); │ │252 │ │253 // both merge styles use the stored revision, without fork data │ │254 try pch.merge(opts, io, allocator, path, &repo, .{ .id = id_hex, .revision = selection, .author = author, .timestamp = 100 }); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │255 const merged = (try evt.Patch.readFromRepo(.xit, opts, io, allocator, &arena, &repo, &id)) orelse return error.NotFound;│ │246 - const oid = switch (merged.event.status) { │ │256 + const range = switch (merged.event.status) { │ │257 .merged => |value| value, │ │258 else => return error.NotMerged, │ │259 }; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯