╭ changed ────╮ │src/patch.zig│ ╰─────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────╮ │780 allocator: std.mem.Allocator, │ │781 repo_root_path: []const u8, │ │782 patch_id: *const [evt.event_id_size]u8, │ │783 - merged_oid: *const [hash.hexLen(repo_opts.hash)]u8, │ │783 + matched_oid: *const [hash.hexLen(repo_opts.hash)]u8,│ │784 + before_oid: *const [hash.hexLen(repo_opts.hash)]u8, │ │785 + after_oid: *const [hash.hexLen(repo_opts.hash)]u8, │ │786 ) !void { │ │787 var arena = std.heap.ArenaAllocator.init(allocator);│ │788 defer arena.deinit(); │ ╰─────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯