╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1640 } │ │1641 if (std.mem.eql(u8, tab, commits_seg)) { │ │1642 params.scanPairs(&segments) catch return null; │ │1643 + if (params.values.get(.merge)) |oid| { │ │1644 + if (!params.only(&.{.merge}) or segments.next() != null) return null; │ │1645 + return repoCommitsMergeRoute(pair, oid); │ │1646 + } │ │1647 if (!params.only(&.{ .start, .branch, .tag, .object, .base, .search, .from })) return null;│ │1648 const base_oid = params.values.get(.base) orelse ""; │ │1649 const search = params.values.get(.search) orelse ""; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1704 std.mem.eql(u8, a_c.base_oid.slice(), b.repo_commits.base_oid.slice()) and │ │1705 std.mem.eql(u8, a_c.search.slice(), b.repo_commits.search.slice()) and │ │1706 std.mem.eql(u8, a_c.from.slice(), b.repo_commits.from.slice()) and │ │1707 + std.mem.eql(u8, a_c.merge.slice(), b.repo_commits.merge.slice()) and │ │1708 switch (a_c.content) { │ │1709 .diff => |a_d| switch (b.repo_commits.content) { │ │1710 .diff => |b_d| a_d.start == b_d.start and std.mem.eql(u8, a_d.path.slice(), b_d.path.slice()),│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │107 }; │ │108 const commits_from: []const u8 = switch (route) { │ │109 .repo_commits => |*c| c.from.slice(), │ │110 + else => "", │ │111 + }; │ │112 + const commits_merge: []const u8 = switch (route) { │ │113 + .repo_commits => |*c| c.merge.slice(), │ │114 else => "", │ │115 }; │ │116 // the refs tab windows one column at a time: `refs_from` (a url-encoded│ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │299 else => .{ .identity = repo_identity.identity, .failure = @errorName(err) }, │ │300 }; │ │301 } │ │302 + // a merge: route stands for the log of what the merge brought in │ │303 + const ref_or_oid: ?ui.RoutablePage.RefOrOid, const ref_value, const base_oid = if (commits_merge.len == 0) │ │304 + .{ requested_ref_or_oid, requested_ref_value, commits_base_oid } │ │305 + else merged: { │ │306 + const merged = try Commits.resolveMerge(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, commits_merge); │ │307 + break :merged .{ .object, merged.oid, merged.base_oid }; │ │308 + }; │ │309 const files_data = if (patchrev_id.len != 0) │ │310 try Files.initPatchRev(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, location, patchrev_id, files_dir, files_line)│ │311 else │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │301 - try Files.init(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, location, requested_ref_or_oid, requested_ref_value, files_dir, files_line, files_find);│ │312 + try Files.init(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, location, ref_or_oid, ref_value, files_dir, files_line, files_find); │ │313 const changes_data: Changes = if (route == .repo_diff) │ │314 .{ .diff = try Diff.init(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, route.repo_diff) } │ │315 else if (patchrev_id.len != 0) diff: { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │316 const diff_route = ui.RoutablePage.repoPatchRevDiffRoute(repo_identity.identity, patchrev_id, 0, "") orelse return error.RouteTooLong; │ │317 break :diff .{ .diff = try Diff.init(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, diff_route.repo_diff) }; │ │307 - } else .{ .commits = try Commits.init(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, session.haxy_moment, location, requested_ref_or_oid, requested_ref_value, commits_content, commits_base_oid, commits_search, commits_from) };│ │318 + } else .{ .commits = try Commits.init(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, session.haxy_moment, location, ref_or_oid, ref_value, commits_content, base_oid, commits_search, commits_from) }; │ │319 const target_branch = if (files_data.ref_or_oid == .branch) files_data.ref_or_oid_value else ""; │ │320 break :blk .{ │ │321 files_data, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭─────────────────────────────────────────────────────────╮ │ 7 const Diff = @import("Diff.zig"); │ │ 8 const Undo = @import("Undo.zig"); │ │ 9 const obj = xit.object; │ │10 +const mrg = xit.merge; │ │11 const srch_cmmt = @import("../../search_commit.zig");│ │12 const srch = @import("../../search.zig"); │ │13 const xitui = xit.xitui; │ ╰─────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────╮ │37 committer: ui.Author = .unknown, │ │38 // the committer timestamp, human-readable.│ │39 timestamp: []const u8, │ │40 + // whether it has a second parent. │ │41 + merge: bool = false, │ │42 stats: ?xit.patch.CommitStats = null, │ │43 window: Diff.Window = .{}, │ │44 }; │ ╰───────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │265 else │ │266 .unknown, │ │267 .timestamp = try Undo.formatTimestamp(aa, std.math.cast(i64, md.timestamp) orelse -1), │ │268 + .merge = if (md.parent_oids) |parent_oids| parent_oids.len > 1 else false, │ │269 .stats = if (repo_kind == .xit) try repo.commitStats(io, gpa, .{ .oid = &commit_object.oid }) else null,│ │270 }; │ │271 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │272 │ │273 +// what merge `merge_oid` brought in: its second parent's log back to the │ │274 +// parents' common ancestor, or the whole log when they share none. │ │275 +pub fn resolveMerge( │ │276 + comptime repo_kind: rp.RepoKind, │ │277 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │278 + arena: *std.heap.ArenaAllocator, │ │279 + repo: *rp.Repo(repo_kind, repo_opts), │ │280 + io: std.Io, │ │281 + gpa: std.mem.Allocator, │ │282 + merge_oid: []const u8, │ │283 +) !struct { oid: []const u8, base_oid: []const u8 } { │ │284 + const aa = arena.allocator(); │ │285 + evt.PatchRev.validateOid(repo_opts.hash, merge_oid) catch return error.NotFound; │ │286 + var moment = try repo.core.latestMoment(); │ │287 + const state = rp.Repo(repo_kind, repo_opts).State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; │ │288 + var merge = obj.Object(repo_kind, repo_opts).initCommit(state, io, gpa, merge_oid[0..comptime xit.hash.hexLen(repo_opts.hash)]) catch return error.NotFound;│ │289 + defer merge.deinit(); │ │290 + const parent_oids = merge.content.commit.metadata.parent_oids orelse &.{}; │ │291 + if (parent_oids.len < 2) return error.NotFound; │ │292 + const oid = try aa.dupe(u8, &parent_oids[1]); │ │293 + const base_oid = mrg.commonAncestor(repo_kind, repo_opts, state, io, gpa, &parent_oids[0], &parent_oids[1]) catch |err| switch (err) { │ │294 + error.NoCommonAncestor => return .{ .oid = oid, .base_oid = "" }, │ │295 + error.MultipleMergeBases => return error.NotFound, │ │296 + else => return err, │ │297 + }; │ │298 + return .{ .oid = oid, .base_oid = try aa.dupe(u8, &base_oid) }; │ │299 +} │ │300 + │ │301 // an author or committer line without its timestamp. │ │302 fn identityOf(line: []const u8) []const u8 { │ │303 const close_bracket = std.mem.indexOfScalar(u8, line, '>') orelse return line; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯