From 027d71b8a4697b961b4d4307586441f3161ea0f3 Mon Sep 17 00:00:00 2001 From: Danish Bhatti Date: Mon, 6 Jul 2026 17:47:33 +0200 Subject: [PATCH] fix(status): recognize all unmerged modes when staging/discarding, not just UU Staging (`s`) or discarding a conflicted file only routed to the conflict resolver when the file's status mode was exactly "UU" (both modified). Other unmerged modes were missed -- most notably "AA" (add/add, produced whenever two branches independently add the same path) and the modify/delete modes "UD"/"DU". For those, `s` fell through to the plain "stage this file" branch and staged the file with its conflict markers intact, so leftover `<<<<<<<`/`=======`/`>>>>>>>` markers could be committed unnoticed. Add a single `git.status.is_unmerged(mode)` helper covering the full set of unmerged porcelain codes (UU, AA, DU, UD, AU, UA, DD) and route every conflict-detection site through it: normal stage, visual-selection stage, discard hunk/section/file, the diffview conflicting/working split (which was also missing DD), and any_unmerged. --- lua/neogit/buffers/status/actions.lua | 12 ++++++------ lua/neogit/integrations/diffview.lua | 7 ++----- lua/neogit/lib/git/status.lua | 13 ++++++++++++- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/lua/neogit/buffers/status/actions.lua b/lua/neogit/buffers/status/actions.lua index 3879b0fb6..04939c1b3 100644 --- a/lua/neogit/buffers/status/actions.lua +++ b/lua/neogit/buffers/status/actions.lua @@ -281,7 +281,7 @@ M.v_stage = function(self) for _, section in ipairs(selection.sections) do if section.name == "unstaged" or section.name == "untracked" then for _, item in ipairs(section.items) do - if item.mode == "UU" then + if git.status.is_unmerged(item.mode) then notification.info("Conflicts must be resolved before staging lines") return end @@ -834,7 +834,7 @@ M.n_discard = function(self) end end elseif section == "unstaged" then - if selection.item.mode:match("^[UAD][UAD]") then + if git.status.is_unmerged(selection.item.mode) then choices = { "&ours", "&theirs", "&conflict", "&abort" } action = function() local choice = @@ -865,7 +865,7 @@ M.n_discard = function(self) end refresh = { update_diffs = { "unstaged:" .. selection.item.name } } elseif section == "staged" then - if selection.item.mode:match("^[UAD][UAD]") then + if git.status.is_unmerged(selection.item.mode) then choices = { "&ours", "&theirs", "&conflict", "&abort" } action = function() local choice = @@ -915,7 +915,7 @@ M.n_discard = function(self) refresh = {} end elseif selection.item then -- Discard Hunk - if selection.item.mode == "UU" then + if git.status.is_unmerged(selection.item.mode) then notification.warn("Resolve conflicts in file before discarding hunks.") return end @@ -954,7 +954,7 @@ M.n_discard = function(self) elseif section == "unstaged" then local conflict = false for _, item in ipairs(selection.section.items) do - if item.mode == "UU" then + if git.status.is_unmerged(item.mode) then conflict = true break end @@ -1211,7 +1211,7 @@ M.n_stage = function(self) return end - if selection.item and selection.item.mode == "UU" then + if selection.item and git.status.is_unmerged(selection.item.mode) then local diff_viewer = config.get_diff_viewer() if diff_viewer and git.merge.is_conflicted(selection.item.name) then local integration = diff_viewer == "codediff" and require("neogit.integrations.codediff") diff --git a/lua/neogit/integrations/diffview.lua b/lua/neogit/integrations/diffview.lua index 44d88e0bc..6a327d16c 100644 --- a/lua/neogit/integrations/diffview.lua +++ b/lua/neogit/integrations/diffview.lua @@ -30,19 +30,16 @@ local function get_local_diff_view(section_name, item_name, opts) local sections = {} - -- all conflict modes (but I don't know how to generate UA/AU) - local conflict_modes = { "UU", "UD", "DU", "AA", "UA", "AU" } - -- merge section gets both if section_name == "unstaged" or section_name == "merge" then sections.conflicting = { items = vim.tbl_filter(function(item) - return vim.tbl_contains(conflict_modes, item.mode) and item + return git.status.is_unmerged(item.mode) and item end, git.repo.state.unstaged.items), } sections.working = { items = vim.tbl_filter(function(item) - return not vim.tbl_contains(conflict_modes, item.mode) and item + return not git.status.is_unmerged(item.mode) and item end, git.repo.state.unstaged.items), } end diff --git a/lua/neogit/lib/git/status.lua b/lua/neogit/lib/git/status.lua index 966c7bc99..463b4e285 100644 --- a/lua/neogit/lib/git/status.lua +++ b/lua/neogit/lib/git/status.lua @@ -265,10 +265,21 @@ function M.anything_unstaged() end) end +-- All two-character porcelain status codes that denote an unmerged (conflicted) +-- entry. During a rebase or merge a conflict is not always "UU" -- an add/add +-- conflict is "AA", a delete/delete is "DD", etc. +local UNMERGED_MODES = { "UU", "AA", "DU", "UD", "AU", "UA", "DD" } + +---@param mode string a StatusItem `mode` (two-character porcelain status code) +---@return boolean whether the mode denotes an unmerged/conflicted entry +function M.is_unmerged(mode) + return vim.tbl_contains(UNMERGED_MODES, mode) +end + ---@return boolean function M.any_unmerged() return vim.iter(git.repo.state.unstaged.items):any(function(item) - return vim.tbl_contains({ "UU", "AA", "DU", "UD", "AU", "UA", "DD" }, item.mode) + return M.is_unmerged(item.mode) end) or #git.cli["ls-files"].unmerged.call().stdout > 0 end