╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────────╮ │ 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; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │467 // js off (the browser follows it, rooting the list there); │ │468 // with wasm the click just selects it and swaps the diff pane. │ │469 const content = if (index == 0) data.content else Content{ .diff = .{} }; │ │438 - try addRow(allocator, &list_box, firstLine(commit.message), try commitRowLink(session.page_arena, data, commit, content)); │ │470 + try addRow(allocator, &list_box, firstLine(commit.message), try commitRowLink(session.page_arena, data, commit, content), if (commit.merge) "(merge)" else "");│ │471 } │ │472 if (data.next_start) |next| { │ │441 - try addRow(allocator, &list_box, "next →", try nextPageLink(session.page_arena, data, next)); │ │473 + try addRow(allocator, &list_box, "next →", try nextPageLink(session.page_arena, data, next), ""); │ │474 } │ │475 if (list_box.children.count() > 0) list_box.getFocus().child_id = list_box.children.keys()[0]; │ │476 break :blk try wgt.Scroll(ui.Widget).init(allocator, .{ .box = list_box }, .{ .direction = .vert, .web_native = !session.is_terminal, .fill = true }); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │514 }; │ │515 } │ │516 │ │485 - fn addRow(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), label: []const u8, link: []const u8) !void { │ │486 - var row = try wgt.TextBox.init(allocator, label, .{ .border_style = .hidden, .rounded_corners = true, .wrap_kind = .word }); │ │517 + fn addRow(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), label: []const u8, link: []const u8, bottom_label: []const u8) !void { │ │518 + var row = try wgt.TextBox.init(allocator, label, .{ .border_style = .hidden, .rounded_corners = true, .wrap_kind = .word, .bottom_label = bottom_label });│ │519 errdefer row.deinit(allocator); │ │520 row.getFocus().mode = .all; │ │521 if (link.len != 0) row.getFocus().kind = .{ .custom = link }; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │522 try box.children.put(allocator, row.getFocus().id, .{ .widget = .{ .text_box = row }, .rect = null, .min_size = null, .max_size = .{ .width = null, .height = 5 } });│ │523 } │ │524 │ │493 - // a focusable window-navigation row ("previous"/"next"). it's a link to this │ │494 - // commit's route at `target_start`, so activating it (the host follows the │ │495 - // "a:" link) reloads the page on the adjacent window — same on TUI and web. │ │496 - fn addNavLink(self: *View, allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), label: []const u8, oid: []const u8, target_start: usize, path: []const u8) !void { │ │497 - const link = try commitsLink(self.session.page_arena, self.data, oid, target_start, path); │ │525 + // a focusable row following the "a:" `link`. │ │526 + fn addLink(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), label: []const u8, link: []const u8) !void { │ │527 var tb = try wgt.TextBox.init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │499 - errdefer tb.deinit(allocator); │ │500 - tb.getFocus().mode = .all; │ │501 - tb.getFocus().kind = .{ .custom = link }; │ │502 - try box.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │503 - } │ │504 - │ │505 - // a focusable row for the top of the diff pane linking to the files tab at │ │506 - // this commit's tree, so its files are always viewable as of this object. │ │507 - fn addViewFilesLink(self: *View, allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), oid: []const u8) !void { │ │508 - const link = try filesObjectLink(self.session.page_arena, self.data.location, oid); │ │509 - var tb = try wgt.TextBox.init(allocator, "view files at this commit", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │528 errdefer tb.deinit(allocator); │ │529 tb.getFocus().mode = .all; │ │530 tb.getFocus().kind = .{ .custom = link }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │684 │ │685 switch (self.paneContent(sel)) { │ │686 .message => { │ │669 - try self.addNavLink(allocator, inner, "← back to diff", commit.oid, 0, ""); │ │687 + try addLink(allocator, inner, "← back to diff", try commitsLink(self.session.page_arena, self.data, commit.oid, 0, ""));│ │688 try self.addMessageBox(allocator, inner, commit, .whole); │ │689 }, │ │690 .diff => |d| { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │713 tb.getFocus().mode = .all; │ │714 try inner.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │715 } │ │716 + if (commit.merge) if (self.data.location.commitsMergeRoute(commit.oid)) |route| { │ │717 + try addLink(allocator, inner, "view commits from this merge", try std.fmt.allocPrint(self.session.page_arena.allocator(), "a:{s}", .{try route.toUrl(self.session.page_arena)}));│ │718 + }; │ │719 if (commit.stats) |stats| { │ │720 var text: std.Io.Writer.Allocating = .init(allocator); │ │721 defer text.deinit(); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │732 tb.getFocus().mode = .all; │ │733 try inner.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │734 } │ │714 - try self.addViewFilesLink(allocator, inner, commit.oid); │ │735 + try addLink(allocator, inner, "view files at this commit", try filesObjectLink(self.session.page_arena, self.data.location, commit.oid));│ │736 } │ │737 │ │738 try (Diff{ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯