From a66c4c2f11896757edf208f9b2363bca560bca01 Mon Sep 17 00:00:00 2001 From: Chris Roscher Date: Wed, 28 May 2025 23:09:05 +0200 Subject: [PATCH 1/9] Fix: focus popup diff "range to", keep diff tab focused --- lua/neogit/popups/diff/actions.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lua/neogit/popups/diff/actions.lua b/lua/neogit/popups/diff/actions.lua index af184cbd4..2d7523641 100644 --- a/lua/neogit/popups/diff/actions.lua +++ b/lua/neogit/popups/diff/actions.lua @@ -28,13 +28,15 @@ function M.range(popup) ) ) - local range_from = FuzzyFinderBuffer.new(options):open_async { prompt_prefix = "Diff for range from" } + local range_from = FuzzyFinderBuffer.new(options):open_async { + prompt_prefix = "Diff for range from", refocus_status = false + } if not range_from then return end local range_to = FuzzyFinderBuffer.new(options) - :open_async { prompt_prefix = "Diff from " .. range_from .. " to" } + :open_async { prompt_prefix = "Diff from " .. range_from .. " to", refocus_status = false } if not range_to then return end From ce2a5effbe1457ae5b69f8026d48fd55c38667f2 Mon Sep 17 00:00:00 2001 From: Chris Roscher Date: Thu, 29 May 2025 01:26:49 +0200 Subject: [PATCH 2/9] Fix: keep stash and commit diff tab focused --- lua/neogit/popups/diff/actions.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/neogit/popups/diff/actions.lua b/lua/neogit/popups/diff/actions.lua index 2d7523641..11e009ced 100644 --- a/lua/neogit/popups/diff/actions.lua +++ b/lua/neogit/popups/diff/actions.lua @@ -74,7 +74,7 @@ end function M.stash(popup) popup:close() - local selected = FuzzyFinderBuffer.new(git.stash.list()):open_async() + local selected = FuzzyFinderBuffer.new(git.stash.list()):open_async{ refocus_status = false } if selected then diffview.open("stashes", selected) end @@ -85,7 +85,7 @@ function M.commit(popup) local options = util.merge(git.refs.list_branches(), git.refs.list_tags(), git.refs.heads()) - local selected = FuzzyFinderBuffer.new(options):open_async() + local selected = FuzzyFinderBuffer.new(options):open_async{ refocus_status = false } if selected then diffview.open("commit", selected) end From beda8099114a1b2d592e60d0c1cc04cda6affb6a Mon Sep 17 00:00:00 2001 From: Chris Roscher Date: Thu, 29 May 2025 11:32:51 +0200 Subject: [PATCH 3/9] formatting --- lua/neogit/popups/diff/actions.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lua/neogit/popups/diff/actions.lua b/lua/neogit/popups/diff/actions.lua index 11e009ced..bd750668f 100644 --- a/lua/neogit/popups/diff/actions.lua +++ b/lua/neogit/popups/diff/actions.lua @@ -29,7 +29,8 @@ function M.range(popup) ) local range_from = FuzzyFinderBuffer.new(options):open_async { - prompt_prefix = "Diff for range from", refocus_status = false + prompt_prefix = "Diff for range from", + refocus_status = false, } if not range_from then return @@ -74,7 +75,7 @@ end function M.stash(popup) popup:close() - local selected = FuzzyFinderBuffer.new(git.stash.list()):open_async{ refocus_status = false } + local selected = FuzzyFinderBuffer.new(git.stash.list()):open_async { refocus_status = false } if selected then diffview.open("stashes", selected) end @@ -85,7 +86,7 @@ function M.commit(popup) local options = util.merge(git.refs.list_branches(), git.refs.list_tags(), git.refs.heads()) - local selected = FuzzyFinderBuffer.new(options):open_async{ refocus_status = false } + local selected = FuzzyFinderBuffer.new(options):open_async { refocus_status = false } if selected then diffview.open("commit", selected) end From be3758271a690e8eea4de34f36127cdf524a3ed6 Mon Sep 17 00:00:00 2001 From: Chris Roscher Date: Fri, 30 May 2025 00:15:27 +0200 Subject: [PATCH 4/9] Feat: Improve fzf-lua integration for diffs, add diff selection for commits and paths --- lua/neogit/integrations/diffview.lua | 18 +- lua/neogit/popups/diff/actions.lua | 390 ++++++++++++++++++++++----- lua/neogit/popups/diff/init.lua | 25 +- 3 files changed, 354 insertions(+), 79 deletions(-) diff --git a/lua/neogit/integrations/diffview.lua b/lua/neogit/integrations/diffview.lua index 42cf8f92b..2a699cf96 100644 --- a/lua/neogit/integrations/diffview.lua +++ b/lua/neogit/integrations/diffview.lua @@ -47,7 +47,11 @@ local function get_local_diff_view(section_name, item_name, opts) selected = (item_name and item.name == item_name) or (not item_name and idx == 1), } - if opts.only then + if opts and opts.files_filter then + if vim.tbl_contains(opts.files_filter, item.name) then + table.insert(files[kind], file) + end + elseif opts.only then if (item_name and file.selected) or (not item_name and section_name == kind) then table.insert(files[kind], file) end @@ -93,7 +97,7 @@ local function get_local_diff_view(section_name, item_name, opts) return view end ----@param section_name string +---@param section_name string | string[] ---@param item_name string|nil ---@param opts table|nil function M.open(section_name, item_name, opts) @@ -109,8 +113,10 @@ function M.open(section_name, item_name, opts) end local view - -- selene: allow(if_same_then_else) - if section_name == "recent" or section_name:match("unmerged$") or section_name == "log" then + + if type(section_name) == "table" then + view = dv_lib.diffview_open(dv_utils.tbl_pack(unpack(section_name))) + elseif section_name == "recent" or section_name:match("unmerged$") or section_name == "log" then local range if type(item_name) == "table" then range = string.format("%s..%s", item_name[1], item_name[#item_name]) @@ -122,8 +128,8 @@ function M.open(section_name, item_name, opts) view = dv_lib.diffview_open(dv_utils.tbl_pack(range)) elseif section_name == "range" then - local range = item_name - view = dv_lib.diffview_open(dv_utils.tbl_pack(range)) + local range_str = item_name + view = dv_lib.diffview_open(dv_utils.tbl_pack(range_str)) elseif section_name == "stashes" then assert(item_name, "No item name for stash!") local stash_id = item_name:match("stash@{%d+}") diff --git a/lua/neogit/popups/diff/actions.lua b/lua/neogit/popups/diff/actions.lua index bd750668f..225136516 100644 --- a/lua/neogit/popups/diff/actions.lua +++ b/lua/neogit/popups/diff/actions.lua @@ -1,95 +1,361 @@ local M = {} -local diffview = require("neogit.integrations.diffview") + +local config = require("neogit.config") +local diffview_integration = require("neogit.integrations.diffview") local FuzzyFinderBuffer = require("neogit.buffers.fuzzy_finder") -local util = require("neogit.lib.util") local git = require("neogit.lib.git") +local a = require("plenary.async") local input = require("neogit.lib.input") --- aka "dwim" = do what I mean -function M.this(popup) - popup:close() +local function get_fzf_lua() + if config.check_integration("fzf_lua") then + local fzf_ok, fzf_lua_mod = pcall(require, "fzf-lua") + if fzf_ok then + return fzf_lua_mod + end + end + return nil +end - if popup.state.env.section and popup.state.env.item then - diffview.open(popup.state.env.section.name, popup.state.env.item.name, { - only = true, - }) - elseif popup.state.env.section then - diffview.open(popup.state.env.section.name, nil, { only = true }) +local function get_picker_selection(picker_raw_output) + if type(picker_raw_output) == "table" and #picker_raw_output > 0 then + return picker_raw_output[1] + elseif type(picker_raw_output) == "string" then + return picker_raw_output end + return nil +end + +local function clean_branch_name(name) + if not name then + return nil + end + name = name:match("%s*->%s*(.+)$") or name + name = name:gsub("^%s*%*%s*", ""):gsub("^%s+", ""):gsub("%s+$", "") + return name +end + +local function close_popup_if_open(popup) + if popup and type(popup.close) == "function" then + popup:close() + end +end + +local function do_close_popup_and_open_diffview(popup, ...) + close_popup_if_open(popup) + diffview_integration.open(...) end -function M.range(popup) - local options = util.deduplicate( - util.merge( - { git.branch.current() or "HEAD" }, - git.branch.get_all_branches(false), - git.tag.list(), - git.refs.heads() +local function get_refs_for_fallback_picker() + local commits = git.log.list { "--max-count=200" } + local formatted_commits = {} + for _, commit_entry in ipairs(commits) do + table.insert( + formatted_commits, + string.format("%s %s", commit_entry.oid:sub(1, 7), commit_entry.subject or "") ) - ) + end + return formatted_commits +end - local range_from = FuzzyFinderBuffer.new(options):open_async { - prompt_prefix = "Diff for range from", - refocus_status = false, - } - if not range_from then - return +local function extract_commit_sha_from_picker_entry(entry_string) + if not entry_string then + return nil end + return entry_string:match("^%s*([a-f0-9]+)") or entry_string +end - local range_to = FuzzyFinderBuffer.new(options) - :open_async { prompt_prefix = "Diff from " .. range_from .. " to", refocus_status = false } - if not range_to then - return +--- Prompts the user to select item(s) using fzf-lua or a fallback FuzzyFinderBuffer. +--- @param popup table The popup object to close on cancel/completion. +--- @param fzf_lua table|nil The fzf-lua module, or nil to force fallback. +--- @param cfg table Configuration for the picker: +local function prompt_for_items_async(popup, fzf_lua, cfg) + local item_processor = cfg.item_processor_fn or function(item) + return item + end + local on_cancel_handler = cfg.on_cancel or function() + close_popup_if_open(popup) end - local choices = { - "&1. " .. range_from .. ".." .. range_to, - "&2. " .. range_from .. "..." .. range_to, - "&3. Cancel", - } - local choice = input.get_choice("Select range", { values = choices, default = #choices }) + local function handle_selection(raw_selected_items) + if not raw_selected_items then + on_cancel_handler() + return + end - popup:close() - if choice == "1" then - diffview.open("range", range_from .. ".." .. range_to) - elseif choice == "2" then - diffview.open("range", range_from .. "..." .. range_to) + if cfg.allow_multi then + if type(raw_selected_items) ~= "table" or #raw_selected_items == 0 then + on_cancel_handler() + return + end + local processed_items = {} + for _, item in ipairs(raw_selected_items) do + local processed = item_processor(item) + if processed ~= nil then + table.insert(processed_items, processed) + end + end + if #processed_items > 0 then + cfg.on_select(processed_items) + else + on_cancel_handler() + end + else + local single_item + if type(raw_selected_items) == "table" then + single_item = raw_selected_items[1] + else + single_item = raw_selected_items + end + + local processed_single_item = single_item and item_processor(single_item) or nil + if processed_single_item ~= nil then + cfg.on_select(processed_single_item) + else + on_cancel_handler() + end + end + end + + if fzf_lua and cfg.fzf_method_name then + fzf_lua[cfg.fzf_method_name] { + prompt = cfg.fzf_prompt, + actions = { + ["default"] = function(selected) + handle_selection(selected) + end, + ["esc"] = on_cancel_handler, + }, + } + else + local picker_opts = { + prompt_prefix = cfg.fallback_prompt_prefix, + refocus_status = false, + allow_multi = cfg.allow_multi or false, + } + local raw_selection = FuzzyFinderBuffer.new(cfg.fallback_data_fn()):open_async(picker_opts) + + if cfg.allow_multi then + handle_selection(raw_selection) + else + handle_selection(get_picker_selection(raw_selection)) + end end end -function M.worktree(popup) - popup:close() - diffview.open("worktree") +--- Prompts the user to select two items sequentially using `prompt_for_items_async`. +--- @param popup table The popup object. +--- @param fzf_lua table|nil The fzf-lua module. +--- @param cfg1 table Picker configuration for the first item. +--- @param cfg2 table Picker configuration for the second item. +--- @param on_both_selected_fn function(item1, item2): Callback when both (non-nil processed) items are selected. +--- @param on_cancel_fn_outer (function, optional): Callback if any selection is cancelled or results in nil. +local function prompt_for_item_pair_async(popup, fzf_lua, cfg1, cfg2, on_both_selected_fn, on_cancel_fn_outer) + local overall_cancel_handler = on_cancel_fn_outer or function() + close_popup_if_open(popup) + end + + cfg1.on_select = function(item1_processed) + if item1_processed == nil then + overall_cancel_handler() + return + end + + cfg2.on_select = function(item2_processed) + if item2_processed == nil then + overall_cancel_handler() + return + end + on_both_selected_fn(item1_processed, item2_processed) + end + cfg2.on_cancel = overall_cancel_handler + prompt_for_items_async(popup, fzf_lua, cfg2) + end + cfg1.on_cancel = overall_cancel_handler + prompt_for_items_async(popup, fzf_lua, cfg1) end -function M.staged(popup) - popup:close() - diffview.open("staged", nil, { only = true }) +M.this = function(popup) + if popup.state.env.section and popup.state.env.item then + do_close_popup_and_open_diffview(popup, popup.state.env.section.name, popup.state.env.item.name, { + only = true, + }) + elseif popup.state.env.section then + do_close_popup_and_open_diffview(popup, popup.state.env.section.name, nil, { only = true }) + else + vim.notify("Neogit: No context for 'this' diff.", vim.log.levels.WARN) + close_popup_if_open(popup) + end end -function M.unstaged(popup) - popup:close() - diffview.open("unstaged", nil, { only = true }) +M.worktree = function(popup) + do_close_popup_and_open_diffview(popup, "worktree") end -function M.stash(popup) - popup:close() +M.staged = function(popup) + do_close_popup_and_open_diffview(popup, "staged", nil, { only = true }) +end - local selected = FuzzyFinderBuffer.new(git.stash.list()):open_async { refocus_status = false } - if selected then - diffview.open("stashes", selected) - end +M.unstaged = function(popup) + do_close_popup_and_open_diffview(popup, "unstaged", nil, { only = true }) end -function M.commit(popup) - popup:close() +M.branch_range = a.void(function(popup) + local fzf = get_fzf_lua() + local branch_picker_config = { + fzf_method_name = "git_branches", + fallback_data_fn = function() + return git.refs.list_branches() + end, + item_processor_fn = clean_branch_name, + } + + local cfg1 = vim.deepcopy(branch_picker_config) + cfg1.fzf_prompt = "Diff range FROM branch> " + cfg1.fallback_prompt_prefix = "Diff range FROM branch" + + local cfg2 = vim.deepcopy(branch_picker_config) + cfg2.fzf_prompt = "Diff range TO branch> " + cfg2.fallback_prompt_prefix = "Diff range TO branch" + + prompt_for_item_pair_async(popup, fzf, cfg1, cfg2, function(branch1, branch2) + local choices = { "&1. Cumulative (..)", "&2. Distinct (...)", "&3. Cancel" } + local choice_num = + input.get_choice("Select diff type for selected branches:", { values = choices, default = 1 }) + + if choice_num == "1" then + do_close_popup_and_open_diffview(popup, "range", branch1 .. ".." .. branch2) + elseif choice_num == "2" then + do_close_popup_and_open_diffview(popup, "range", branch1 .. "..." .. branch2) + else + close_popup_if_open(popup) + end + end) +end) + +M.commit_range = a.void(function(popup) + local fzf = get_fzf_lua() + local commit_picker_config = { + fzf_method_name = "git_commits", + fallback_data_fn = get_refs_for_fallback_picker, + item_processor_fn = extract_commit_sha_from_picker_entry, + } + + local cfg1 = vim.deepcopy(commit_picker_config) + cfg1.fzf_prompt = "Diff range FROM commit/ref> " + cfg1.fallback_prompt_prefix = "Diff range FROM commit/ref" + + local cfg2 = vim.deepcopy(commit_picker_config) + cfg2.fzf_prompt = "Diff range TO commit/ref> " + cfg2.fallback_prompt_prefix = "Diff range TO commit/ref" - local options = util.merge(git.refs.list_branches(), git.refs.list_tags(), git.refs.heads()) + prompt_for_item_pair_async(popup, fzf, cfg1, cfg2, function(commit_or_ref1, commit_or_ref2) + do_close_popup_and_open_diffview(popup, "range", commit_or_ref1 .. ".." .. commit_or_ref2) + end) +end) - local selected = FuzzyFinderBuffer.new(options):open_async { refocus_status = false } - if selected then - diffview.open("commit", selected) +M.head_to_commit_ref = a.void(function(popup) + local fzf = get_fzf_lua() + prompt_for_items_async(popup, fzf, { + fzf_method_name = "git_commits", + fzf_prompt = "Diff HEAD to commit/ref> ", + fallback_data_fn = get_refs_for_fallback_picker, + fallback_prompt_prefix = "Diff HEAD to commit/ref", + item_processor_fn = extract_commit_sha_from_picker_entry, + on_select = function(commit_or_ref) + do_close_popup_and_open_diffview(popup, "range", "HEAD.." .. commit_or_ref) + end, + }) +end) + +M.branch_commits = a.void(function(popup) + local fzf = get_fzf_lua() + prompt_for_items_async(popup, fzf, { + fzf_method_name = "git_branches", + fzf_prompt = "Diff commits for branch> ", + fallback_data_fn = function() + return git.refs.list_branches() + end, + fallback_prompt_prefix = "Diff commits for branch", + item_processor_fn = clean_branch_name, + on_select = function(branch_name) + do_close_popup_and_open_diffview(popup, { branch_name }) + end, + }) +end) + +M.stash = a.void(function(popup) + local fzf = get_fzf_lua() + + local function process_stash_entry(selected_entry_text) + if selected_entry_text then + return selected_entry_text:match("^(stash@{%d+})") + end + return nil end -end + + prompt_for_items_async(popup, fzf, { + fzf_method_name = "git_stash", + fzf_prompt = "Diff stash> ", + fallback_data_fn = function() + return git.stash.list() + end, + fallback_prompt_prefix = "Select stash to diff", + item_processor_fn = process_stash_entry, + on_select = function(stash_ref) + do_close_popup_and_open_diffview(popup, "stashes", stash_ref) + end, + on_cancel = function() + vim.notify("Invalid stash selected or stash pattern not found.", vim.log.levels.WARN) + close_popup_if_open(popup) + end, + }) +end) + +M.tag_range = a.void(function(popup) + local fzf = get_fzf_lua() + + local function sanitize_tag_name_for_picker(name) + if not name then + return nil + end + return name:match("^%s*([^%s]+)") or name + end + + local tag_picker_config = { + fzf_method_name = "git_tags", + fallback_data_fn = function() + return git.refs.list_tags() + end, + item_processor_fn = sanitize_tag_name_for_picker, + } + + local cfg1 = vim.deepcopy(tag_picker_config) + cfg1.fzf_prompt = "Diff range FROM tag> " + cfg1.fallback_prompt_prefix = "Diff range FROM tag" + + local cfg2 = vim.deepcopy(tag_picker_config) + cfg2.fzf_prompt = "Diff range TO tag> " + cfg2.fallback_prompt_prefix = "Diff range TO tag" + + prompt_for_item_pair_async(popup, fzf, cfg1, cfg2, function(tag1, tag2) + do_close_popup_and_open_diffview(popup, "range", tag1 .. ".." .. tag2) + end) +end) + +M.paths = a.void(function(popup) + prompt_for_items_async(popup, nil, { + fallback_data_fn = function() + return git.files.all() + end, + fallback_prompt_prefix = "Select files to diff against HEAD", + allow_multi = true, + on_select = function(files_to_diff) + local diff_args = { "HEAD", "--" } + vim.list_extend(diff_args, files_to_diff) + do_close_popup_and_open_diffview(popup, diff_args) + end, + }) +end) return M diff --git a/lua/neogit/popups/diff/init.lua b/lua/neogit/popups/diff/init.lua index ed8b849c0..46d8bb16a 100644 --- a/lua/neogit/popups/diff/init.lua +++ b/lua/neogit/popups/diff/init.lua @@ -10,17 +10,20 @@ function M.create(env) local p = popup .builder() :name("NeogitDiffPopup") - :group_heading("Diff") - :action_if(diffview, "d", "this", actions.this) - :action_if(diffview, "r", "range", actions.range) - :action("p", "paths") - :new_action_group() - :action_if(diffview, "u", "unstaged", actions.unstaged) - :action_if(diffview, "s", "staged", actions.staged) - :action_if(diffview, "w", "worktree", actions.worktree) - :new_action_group("Show") - :action_if(diffview, "c", "Commit", actions.commit) - :action_if(diffview, "t", "Stash", actions.stash) + :group_heading("Diff Working Tree/Index") + :action_if(diffview, "d", "Current File/Selection", actions.this) + :action_if(diffview, "w", "Worktree", actions.worktree) + :action_if(diffview, "s", "Staged Changes (Index)", actions.staged) + :action_if(diffview, "u", "Unstaged Changes (HEAD vs Worktree)", actions.unstaged) + :new_action_group("Diff Ranges") + :action_if(diffview, "b", "Branch Range", actions.branch_range) + :action_if(diffview, "c", "Commit/Ref Range", actions.commit_range) + :action_if(diffview, "t", "Tag Range", actions.tag_range) + :action_if(diffview, "h", "HEAD to Commit/Ref", actions.head_to_commit_ref) + :new_action_group("Diff Specific Types") + :action_if(diffview, "C", "Branch Commits", actions.branch_commits) + :action_if(diffview, "S", "Stash", actions.stash) + :action_if(diffview, "p", "Paths", actions.paths) :env(env) :build() From 0ebd6b18cb437a693f11308a3da163889605efd8 Mon Sep 17 00:00:00 2001 From: Chris Roscher Date: Fri, 30 May 2025 02:11:50 +0200 Subject: [PATCH 5/9] Feat: Implement path diff with globs, remove redundant code --- lua/neogit/integrations/diffview.lua | 6 +-- lua/neogit/lib/git/cli.lua | 1 - lua/neogit/popups/diff/actions.lua | 71 +++++++++++++++++++++------- lua/neogit/popups/diff/init.lua | 2 +- 4 files changed, 56 insertions(+), 24 deletions(-) diff --git a/lua/neogit/integrations/diffview.lua b/lua/neogit/integrations/diffview.lua index 2a699cf96..83388efa1 100644 --- a/lua/neogit/integrations/diffview.lua +++ b/lua/neogit/integrations/diffview.lua @@ -47,11 +47,7 @@ local function get_local_diff_view(section_name, item_name, opts) selected = (item_name and item.name == item_name) or (not item_name and idx == 1), } - if opts and opts.files_filter then - if vim.tbl_contains(opts.files_filter, item.name) then - table.insert(files[kind], file) - end - elseif opts.only then + if opts.only then if (item_name and file.selected) or (not item_name and section_name == kind) then table.insert(files[kind], file) end diff --git a/lua/neogit/lib/git/cli.lua b/lua/neogit/lib/git/cli.lua index 315246e41..7a8d526db 100644 --- a/lua/neogit/lib/git/cli.lua +++ b/lua/neogit/lib/git/cli.lua @@ -1170,7 +1170,6 @@ local function new_builder(subcommand) { "git", "--no-pager", - "--literal-pathspecs", "--no-optional-locks", "-c", "core.preloadindex=true", "-c", "color.ui=always", diff --git a/lua/neogit/popups/diff/actions.lua b/lua/neogit/popups/diff/actions.lua index 225136516..646139a21 100644 --- a/lua/neogit/popups/diff/actions.lua +++ b/lua/neogit/popups/diff/actions.lua @@ -268,22 +268,6 @@ M.head_to_commit_ref = a.void(function(popup) }) end) -M.branch_commits = a.void(function(popup) - local fzf = get_fzf_lua() - prompt_for_items_async(popup, fzf, { - fzf_method_name = "git_branches", - fzf_prompt = "Diff commits for branch> ", - fallback_data_fn = function() - return git.refs.list_branches() - end, - fallback_prompt_prefix = "Diff commits for branch", - item_processor_fn = clean_branch_name, - on_select = function(branch_name) - do_close_popup_and_open_diffview(popup, { branch_name }) - end, - }) -end) - M.stash = a.void(function(popup) local fzf = get_fzf_lua() @@ -343,7 +327,7 @@ M.tag_range = a.void(function(popup) end) end) -M.paths = a.void(function(popup) +M.files = a.void(function(popup) prompt_for_items_async(popup, nil, { fallback_data_fn = function() return git.files.all() @@ -351,11 +335,64 @@ M.paths = a.void(function(popup) fallback_prompt_prefix = "Select files to diff against HEAD", allow_multi = true, on_select = function(files_to_diff) + if not files_to_diff or #files_to_diff == 0 then + close_popup_if_open(popup) + return + end local diff_args = { "HEAD", "--" } vim.list_extend(diff_args, files_to_diff) do_close_popup_and_open_diffview(popup, diff_args) end, + on_cancel = function() + close_popup_if_open(popup) + end, + }) +end) + +M.paths = a.void(function(popup) + local path_input_str = input.get_user_input("Enter path(s) to diff (space-separated, globs supported)", { + completion = "dir", + default = "./", }) + + if not path_input_str or path_input_str == "" then + close_popup_if_open(popup) + return + end + + local path_patterns = vim.split(path_input_str, "%s+") + local all_files_to_diff = {} + local found_any_files = false + + for _, pattern in ipairs(path_patterns) do + if pattern ~= "" then + local files_under_path_result = + git.cli["ls-files"].args(pattern).call { hidden = true, ignore_error = true } + + if files_under_path_result.code == 0 and #files_under_path_result.stdout > 0 then + found_any_files = true + vim.list_extend(all_files_to_diff, files_under_path_result.stdout) + end + end + end + + if not found_any_files then + notification.warn("No tracked files found matching: " .. path_input_str) + close_popup_if_open(popup) + return + end + + all_files_to_diff = util.deduplicate(all_files_to_diff) + + if #all_files_to_diff == 0 then + notification.warn("No tracked files found matching: " .. path_input_str) + close_popup_if_open(popup) + return + end + + local diff_args = { "HEAD", "--" } + vim.list_extend(diff_args, all_files_to_diff) + do_close_popup_and_open_diffview(popup, diff_args) end) return M diff --git a/lua/neogit/popups/diff/init.lua b/lua/neogit/popups/diff/init.lua index 46d8bb16a..d99a56488 100644 --- a/lua/neogit/popups/diff/init.lua +++ b/lua/neogit/popups/diff/init.lua @@ -21,9 +21,9 @@ function M.create(env) :action_if(diffview, "t", "Tag Range", actions.tag_range) :action_if(diffview, "h", "HEAD to Commit/Ref", actions.head_to_commit_ref) :new_action_group("Diff Specific Types") - :action_if(diffview, "C", "Branch Commits", actions.branch_commits) :action_if(diffview, "S", "Stash", actions.stash) :action_if(diffview, "p", "Paths", actions.paths) + :action_if(diffview, "f", "Files", actions.files) :env(env) :build() From fe63a498c8fd2957ba90b68165783c67e2f38cb7 Mon Sep 17 00:00:00 2001 From: Chris Roscher Date: Fri, 30 May 2025 09:57:47 +0200 Subject: [PATCH 6/9] Improve branch clean function to support detached HEAD --- lua/neogit/popups/diff/actions.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lua/neogit/popups/diff/actions.lua b/lua/neogit/popups/diff/actions.lua index 646139a21..c8314dd52 100644 --- a/lua/neogit/popups/diff/actions.lua +++ b/lua/neogit/popups/diff/actions.lua @@ -30,8 +30,11 @@ local function clean_branch_name(name) if not name then return nil end - name = name:match("%s*->%s*(.+)$") or name - name = name:gsub("^%s*%*%s*", ""):gsub("^%s+", ""):gsub("%s+$", "") + if name:match("^%s*%*%s*%(HEAD detached .*%)") then + return "HEAD" + end + name = name:gsub("^%s*%*%s*", "") + name = (name:match("%s*->%s*(.+)$") or name):gsub("^%s+", ""):gsub("%s+$", "") return name end From 84fa9075c9ded11e53964e58fa98a90d4ca14b09 Mon Sep 17 00:00:00 2001 From: Chris Roscher Date: Fri, 30 May 2025 12:42:32 +0200 Subject: [PATCH 7/9] Forgot imports --- lua/neogit/popups/diff/actions.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lua/neogit/popups/diff/actions.lua b/lua/neogit/popups/diff/actions.lua index c8314dd52..e6f662d86 100644 --- a/lua/neogit/popups/diff/actions.lua +++ b/lua/neogit/popups/diff/actions.lua @@ -6,6 +6,8 @@ local FuzzyFinderBuffer = require("neogit.buffers.fuzzy_finder") local git = require("neogit.lib.git") local a = require("plenary.async") local input = require("neogit.lib.input") +local notification = require("neogit.lib.notification") +local util = require("neogit.lib.util") local function get_fzf_lua() if config.check_integration("fzf_lua") then From 19db0fa021987e92a37d3d3a5759768ad24815e4 Mon Sep 17 00:00:00 2001 From: Chris Roscher Date: Fri, 30 May 2025 12:44:37 +0200 Subject: [PATCH 8/9] Fix luacheck linting --- lua/neogit/popups/diff/actions.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/neogit/popups/diff/actions.lua b/lua/neogit/popups/diff/actions.lua index e6f662d86..478c2c383 100644 --- a/lua/neogit/popups/diff/actions.lua +++ b/lua/neogit/popups/diff/actions.lua @@ -154,7 +154,7 @@ end --- @param cfg1 table Picker configuration for the first item. --- @param cfg2 table Picker configuration for the second item. --- @param on_both_selected_fn function(item1, item2): Callback when both (non-nil processed) items are selected. ---- @param on_cancel_fn_outer (function, optional): Callback if any selection is cancelled or results in nil. +--- @param on_cancel_fn_outer? function Callback if any selection is cancelled or results in nil. local function prompt_for_item_pair_async(popup, fzf_lua, cfg1, cfg2, on_both_selected_fn, on_cancel_fn_outer) local overall_cancel_handler = on_cancel_fn_outer or function() close_popup_if_open(popup) From 47c28e1d6e300197f1b7123645d59a6ef909c7cb Mon Sep 17 00:00:00 2001 From: Chris Roscher Date: Fri, 30 May 2025 12:52:49 +0200 Subject: [PATCH 9/9] Simplify diffview.open logic --- lua/neogit/integrations/diffview.lua | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lua/neogit/integrations/diffview.lua b/lua/neogit/integrations/diffview.lua index 83388efa1..97d491ebd 100644 --- a/lua/neogit/integrations/diffview.lua +++ b/lua/neogit/integrations/diffview.lua @@ -130,18 +130,17 @@ function M.open(section_name, item_name, opts) assert(item_name, "No item name for stash!") local stash_id = item_name:match("stash@{%d+}") view = dv_lib.diffview_open(dv_utils.tbl_pack(stash_id .. "^!")) - elseif section_name == "commit" then - view = dv_lib.diffview_open(dv_utils.tbl_pack(item_name .. "^!")) elseif section_name == "conflict" and item_name then view = dv_lib.diffview_open(dv_utils.tbl_pack("--selected-file=" .. item_name)) - elseif (section_name == "conflict" or section_name == "worktree") and not item_name then - view = dv_lib.diffview_open() - elseif section_name ~= nil then - view = get_local_diff_view(section_name, item_name, opts) - elseif section_name == nil and item_name ~= nil then + elseif section_name == "commit" or (section_name == nil and item_name ~= nil) then view = dv_lib.diffview_open(dv_utils.tbl_pack(item_name .. "^!")) - else + elseif + ((section_name == "conflict" or section_name == "worktree") and not item_name) + or (section_name == nil and item_name == nil) + then view = dv_lib.diffview_open() + else + view = get_local_diff_view(section_name, item_name, opts) end if view then