╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │729 .repo_issues => |a_i| std.mem.eql(u8, a_i.name.slice(), b.repo_issues.name.slice()) and│ │730 std.mem.eql(u8, a_i.tag.slice(), b.repo_issues.tag.slice()) and │ │731 std.mem.eql(u8, a_i.selected.slice(), b.repo_issues.selected.slice()) and │ │732 + std.mem.eql(u8, a_i.theirs.slice(), b.repo_issues.theirs.slice()) and │ │733 a_i.view == b.repo_issues.view, │ │734 .repo_settings => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_settings.slice()), │ │735 .repo_auth => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_auth.slice()), │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭─────────────────────────────────────────────────────────────────────────╮ │106 }; │ │107 const issues_selected: []const u8 = switch (route) { │ │108 .repo_issues => |*i| i.selected.slice(), │ │109 + else => "", │ │110 + }; │ │111 + const issues_theirs: []const u8 = switch (route) { │ │112 + .repo_issues => |*i| i.theirs.slice(), │ │113 else => "", │ │114 }; │ │115 const issues_view: ui.RoutablePage.IssuesView = switch (route) {│ ╰─────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │175 try Files.init(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, repo_identity.identity, requested_ref_or_oid, requested_ref_value, files_dir, files_line), │ │176 try Commits.init(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, session.haxy_moment, repo_identity.identity, requested_ref_or_oid, requested_ref_value, commits_content),│ │177 try Refs.init(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, repo_identity.identity, refs_kind, refs_from), │ │174 - try Issues.init(repo_kind, opened.self_repo_opts, arena, opened, io, session.haxy_moment, repo_identity.identity, issues_tag, issues_selected, issues_view), │ │178 + try Issues.init(repo_kind, opened.self_repo_opts, arena, opened, io, session.haxy_moment, repo_identity.identity, issues_tag, issues_selected, issues_theirs, issues_view), │ │179 }; │ │180 }, │ │181 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │187 try Files.emptyResult(aa, repo_identity.identity, requested_ref_or_oid orelse .branch, requested_ref_value, files_dir), │ │188 try Commits.emptyResult(aa, repo_identity.identity, requested_ref_or_oid orelse .branch, requested_ref_value, commits_content),│ │189 try Refs.emptyResult(arena, repo_identity.identity, refs_kind, refs_from), │ │186 - try Issues.emptyResult(aa, repo_identity.identity, issues_tag, issues_selected, issues_view), │ │190 + try Issues.emptyResult(aa, repo_identity.identity, issues_tag, issues_selected, issues_theirs, issues_view), │ │191 }; │ │192 }; │ │193 issues.repo_source = source; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭────────────────────────────────────────────────────╮ │12 const Grid = xitui.grid.Grid; │ │13 const Focus = xitui.focus.Focus; │ │14 const inp = @import("../input.zig"); │ │15 +const diff3 = @import("../../diff3.zig"); │ │16 │ │17 const wasm = builtin.target.cpu.arch == .wasm32;│ │18 │ ╰────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │33 issue: evt.Issue.Record, │ │34 // the creation commit's author │ │35 author: ui.Author = .unknown, │ │36 + // whether the issue has an unresolved merge conflict │ │37 + conflicted: bool = false, │ │38 +}; │ │39 + │ │40 +// one side of a conflicted field: its value and who set it │ │41 +pub const Side = struct { │ │42 + text: []const u8, │ │43 + author: ui.Author = .unknown, │ │44 +}; │ │45 + │ │46 +pub const FieldConflict = struct { │ │47 + ours: Side, │ │48 + theirs: Side, │ │49 +}; │ │50 + │ │51 +// the selected issue's conflict, read for the resolve view. ours is the live│ │52 +// record; the chunks split the description against the merge base. │ │53 +pub const Conflict = struct { │ │54 + title: ?FieldConflict = null, │ │55 + tags: ?FieldConflict = null, │ │56 + description: ?struct { │ │57 + chunks: []const diff3.Chunk, │ │58 + ours_author: ui.Author = .unknown, │ │59 + theirs_author: ui.Author = .unknown, │ │60 + } = null, │ │61 }; │ │62 │ │63 // one status's windowed listing. │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │84 selected_id: []const u8, │ │85 open: Window, │ │86 closed: Window, │ │87 +// the conflicted issues' listing; its count also gates the conflicts tab. │ │88 +conflicts: Window, │ │89 +// the selected issue's conflict, set when the page shows the resolve view. │ │90 +conflict: ?Conflict = null, │ │91 +// the resolve view's comma-separated fields prefilled from their side, │ │92 +// mirrored from the url. │ │93 +theirs_picks: []const u8 = "", │ │94 // the view the page shows initially (a selected issue's status overrides the route's)│ │95 view: ui.RoutablePage.IssuesView, │ │96 // the /description page: the detail pane shows the selected issue's whole │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │112 }; │ │113 } │ │114 │ │115 +// whether `field` (a field name or "d<n>" hunk name) is listed in the url's│ │116 +// theirs: picks. │ │117 +pub fn theirsPicked(self: *const Self, field: []const u8) bool { │ │118 + var it = std.mem.splitScalar(u8, self.theirs_picks, ','); │ │119 + while (it.next()) |pick| { │ │120 + if (std.mem.eql(u8, pick, field)) return true; │ │121 + } │ │122 + return false; │ │123 +} │ │124 + │ │125 // the issue `selected_id` names, in whichever window holds it. │ │126 pub fn selectedIssue(self: *const Self) ?*const IssueWithId { │ │127 if (self.selected_id.len == 0) return null; │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │134 } │ │135 │ │136 // an empty listing, for the wasm / no-repo paths. │ │ 94 -pub fn emptyResult(aa: std.mem.Allocator, identity: []const u8, tag: []const u8, selected_id: []const u8, view: ui.RoutablePage.IssuesView) !Self { │ │137 +pub fn emptyResult(aa: std.mem.Allocator, identity: []const u8, tag: []const u8, selected_id: []const u8, theirs_picks: []const u8, view: ui.RoutablePage.IssuesView) !Self {│ │138 return .{ │ │139 .identity = try aa.dupe(u8, identity), │ │140 .tag = try aa.dupe(u8, tag), │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │141 .selected_id = try aa.dupe(u8, selected_id), │ │142 .open = .empty, │ │143 .closed = .empty, │ │101 - // a description url shows a status list; the issue's own status picks│ │102 - // it once the issue is read. │ │103 - .view = if (view == .description) .open else view, │ │144 + .conflicts = .empty, │ │145 + .theirs_picks = try aa.dupe(u8, theirs_picks), │ │146 + // a description url shows a status list, the issue's own status │ │147 + // picking it once the issue is read; conflicts and resolve urls also │ │148 + // start there, upgraded by init when the conflict data exists. │ │149 + .view = switch (view) { │ │150 + .description, .conflicts, .resolve => .open, │ │151 + else => view, │ │152 + }, │ │153 .description_page = view == .description, │ │154 .tags = &.{}, │ │155 }; │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯