Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions doc/diffview.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1906,6 +1906,14 @@ open_commit_log *diffview-actions-open_commit_lo
Open a panel showing the full commit description of the subject's
target commit range.

open_commit_log_file *diffview-actions-open_commit_log_file*
Contexts: `file_panel`

Like `open_commit_log`, but filter the log to the file under the
cursor. Directory entries are skipped. Renames within the range
silently truncate history at the rename (git `--follow` is not
applied); use |:DiffviewFileHistory| for a rename-aware view.

open_fold *diffview-actions-open_fold*
Contexts: `file_panel`, `file_history_panel`

Expand Down
1 change: 1 addition & 0 deletions doc/diffview_defaults.txt
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ DEFAULT CONFIG *diffview.defaults*
{ "n", "U", actions.unstage_all, { desc = "Unstage all entries" } },
{ "n", "X", actions.restore_entry, { desc = "Restore entry to the state on the left side" } },
{ "n", "L", actions.open_commit_log, { desc = "Open the commit log panel" } },
{ "n", "gL", actions.open_commit_log_file, { desc = "Open the commit log panel filtered to the file under the cursor" } },
{ "n", "zo", actions.open_fold, { desc = "Expand fold" } },
{ "n", "h", actions.close_fold, { desc = "Collapse fold" } },
{ "n", "zc", actions.close_fold, { desc = "Collapse fold" } },
Expand Down
2 changes: 2 additions & 0 deletions lua/diffview/actions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ local pl = lazy.access(utils, "path") --[[@as PathLib ]]
---@field open_all_folds fun()
---@field open_commit_in_browser fun()
---@field open_commit_log fun()
---@field open_commit_log_file fun()
---@field open_fold fun()
---@field open_in_diffview fun()
---@field options fun()
Expand Down Expand Up @@ -1231,6 +1232,7 @@ local action_names = {
"open_all_folds",
"open_commit_in_browser",
"open_commit_log",
"open_commit_log_file",
"open_fold",
"open_in_diffview",
"options",
Expand Down
1 change: 1 addition & 0 deletions lua/diffview/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,7 @@ M.defaults = {
{ "n", "U", actions.unstage_all, { desc = "Unstage all entries" } },
{ "n", "X", actions.restore_entry, { desc = "Restore entry to the state on the left side" } },
{ "n", "L", actions.open_commit_log, { desc = "Open the commit log panel" } },
{ "n", "gL", actions.open_commit_log_file, { desc = "Open the commit log panel filtered to the file under the cursor" } },
{ "n", "<C-w>T", actions.open_in_new_tab, { desc = "Open diffview in a new tab" } },
{ "n", "i", actions.listing_style, { desc = "Toggle between 'list' and 'tree' views" } },
{ "n", "f", actions.toggle_flatten_dirs, { desc = "Flatten empty subdirectories in tree listing style" } },
Expand Down
19 changes: 19 additions & 0 deletions lua/diffview/scene/views/diff/listeners.lua
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,25 @@ return function(view)
view.commit_log_panel:update(range)
end
end,
open_commit_log_file = function()
if view.left.type == RevType.STAGE and view.right.type == RevType.LOCAL then
utils.info("Changes not committed yet. No log available for these changes.")
return
end

-- `infer_cur_file()` (no arg) already skips `DirData` items, so any
-- non-nil return is a real file entry.
local item = view:infer_cur_file()
if not item then
return
end

local range = view.adapter.Rev.to_range(view.left, view.right)

if range then
view.commit_log_panel:update(range, { item.path })
end
end,
toggle_stage_entry = function()
if not (view.left.type == RevType.STAGE and view.right.type == RevType.LOCAL) then
return
Expand Down
89 changes: 89 additions & 0 deletions lua/diffview/tests/functional/git_adapter_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,95 @@ describe("diffview.vcs.adapters.git", function()
end
end)
)

it(
"appends `-- :(literal)<path>` when path filters are given",
test_utils.async_test(function()
local repo, adapter = make_repo_and_adapter()

local ok, err = pcall(function()
local args = adapter:get_log_args({ "abc..def" }, { "a.txt", "b.txt" })

-- Paths trail after a lone `--` separator (so git treats them as
-- pathspecs, not revs) and are prefixed with `:(literal)` to force
-- exact filename match instead of pathspec globbing.
local sep_idx
for i, arg in ipairs(args) do
if arg == "--" then
sep_idx = i
break
end
end
assert.is_not_nil(sep_idx, "get_log_args must emit `--` before pathspecs")
assert.equals(":(literal)a.txt", args[sep_idx + 1])
assert.equals(":(literal)b.txt", args[sep_idx + 2])
assert.is_nil(args[sep_idx + 3])
end)

vim.schedule(function()
pcall(vim.fn.delete, repo, "rf")
end)
async.await(async.scheduler())

if not ok then
error(err)
end
end)
)

it(
"wraps filenames containing pathspec metacharacters as literal",
test_utils.async_test(function()
local repo, adapter = make_repo_and_adapter()

local ok, err = pcall(function()
-- Filenames legally containing `*`, `[`, `?`, or a leading `:` would
-- otherwise trigger pathspec globbing / magic when passed unquoted.
local args = adapter:get_log_args({ "abc..def" }, { "foo*.txt", "[dir]/x.lua", ":odd" })

local last3 = { args[#args - 2], args[#args - 1], args[#args] }
assert.equals(":(literal)foo*.txt", last3[1])
assert.equals(":(literal)[dir]/x.lua", last3[2])
assert.equals(":(literal):odd", last3[3])
end)

vim.schedule(function()
pcall(vim.fn.delete, repo, "rf")
end)
async.await(async.scheduler())

if not ok then
error(err)
end
end)
)

it(
"does not append `--` when no path filter is given",
test_utils.async_test(function()
local repo, adapter = make_repo_and_adapter()

local ok, err = pcall(function()
local args_nil = adapter:get_log_args({ "abc..def" })
local args_empty = adapter:get_log_args({ "abc..def" }, {})

for _, args in ipairs({ args_nil, args_empty }) do
for _, arg in ipairs(args) do
assert.are_not.equal("--", arg)
end
end
end)

vim.schedule(function()
pcall(vim.fn.delete, repo, "rf")
end)
async.await(async.scheduler())

if not ok then
error(err)
end
end)
)
end)

describe("show_untracked", function()
Expand Down
43 changes: 43 additions & 0 deletions lua/diffview/tests/functional/hg_adapter_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,49 @@ describe("diffview.vcs.adapters.hg", function()
eq("-R", args[log_idx - 2])
eq("/some/path", args[log_idx - 1])
end)

it("appends `-- path:<path>` when path filters are given", function()
local adapter = HgAdapter({ toplevel = "/tmp", path_args = {} })
local args = adapter:get_log_args({ "abc..def" }, { "a.txt", "b.txt" })

-- Paths trail after a lone `--` separator and are prefixed with
-- `path:` so hg matches the exact repo-relative path instead of
-- interpreting glob metacharacters (`*`, `?`, `[`).
local sep_idx
for i, arg in ipairs(args) do
if arg == "--" then
sep_idx = i
break
end
end
assert.is_not_nil(sep_idx, "get_log_args must emit `--` before pathspecs")
eq("path:a.txt", args[sep_idx + 1])
eq("path:b.txt", args[sep_idx + 2])
assert.is_nil(args[sep_idx + 3])
end)

it("wraps filenames containing pattern metacharacters as literal", function()
local adapter = HgAdapter({ toplevel = "/tmp", path_args = {} })
-- Filenames legally containing `*`, `?`, or `[` would otherwise be
-- expanded as globs against the working tree by hg.
local args = adapter:get_log_args({ "abc..def" }, { "foo*.txt", "[dir]/x.lua" })

eq("path:foo*.txt", args[#args - 1])
eq("path:[dir]/x.lua", args[#args])
end)

it("does not append `--` when no path filter is given", function()
local adapter = HgAdapter({ toplevel = "/tmp", path_args = {} })

for _, args in ipairs({
adapter:get_log_args({ "abc..def" }),
adapter:get_log_args({ "abc..def" }, {}),
}) do
for _, arg in ipairs(args) do
assert.are_not.equal("--", arg)
end
end
end)
end)

-- ------------------------------------------------------------------
Expand Down
32 changes: 32 additions & 0 deletions lua/diffview/tests/functional/jj_adapter_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,38 @@ describe("diffview.vcs.adapters.jj", function()
adapter:get_log_args({ "abc123" })
)
end)

it("appends fileset-quoted path filters after `--` and revsets", function()
local adapter = new_adapter()

-- `a.txt` is a literal path with no glob metacharacters, so
-- `quote_path_args` wraps it as a plain fileset string literal.
-- Positional filesets always follow `--`, which itself follows
-- the `-r` revset(s), matching `list_files_at_head` and
-- `file_history_dry_run` in the same adapter.
eq(
{ "log", "--no-graph", "-r", "abc123..def456", "--", '"a.txt"' },
adapter:get_log_args({ "abc123..def456" }, { "a.txt" })
)
end)

it("quotes literal paths containing fileset operators", function()
local adapter = new_adapter()

-- A colon-bearing filename would otherwise look like a fileset
-- kind prefix (`foo:bar.txt`) to jj. Quoting forces literal match.
eq(
{ "log", "--no-graph", "-r", "abc123", "--", '"foo:bar.txt"' },
adapter:get_log_args({ "abc123" }, { "foo:bar.txt" })
)
end)

it("does not append `--` or any path when `paths` is nil or empty", function()
local adapter = new_adapter()

eq({ "log", "--no-graph", "-r", "abc123" }, adapter:get_log_args({ "abc123" }, nil))
eq({ "log", "--no-graph", "-r", "abc123" }, adapter:get_log_args({ "abc123" }, {}))
end)
end)

describe("_warn_once()", function()
Expand Down
21 changes: 18 additions & 3 deletions lua/diffview/ui/panels/commit_log_panel.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ local M = {}
---@class CommitLogPanel : Panel
---@field adapter VCSAdapter
---@field args string[]
---@field paths? string[]
---@field job_out string[]
local CommitLogPanel = oop.create_class("CommitLogPanel", Panel)

Expand Down Expand Up @@ -89,15 +90,27 @@ end

---@param self CommitLogPanel
---@param args string|string[]
CommitLogPanel.update = async.void(function(self, args)
args = args or self.args
---@param paths? string[] # Optional file paths to filter the log by.
CommitLogPanel.update = async.void(function(self, args, paths)
-- A bare `update()` (no explicit args) is treated as a refresh: replay the
-- last displayed query so the panel keeps whatever range + path filter was
-- set. `paths` is only recorded once we know we're actually rendering new
-- content, so a failed or empty log leaves `self.paths` on the last
-- successful state — a subsequent refresh replays *that*, not the failed
-- filter.
if args == nil then
args = self.args
if paths == nil then
paths = self.paths
end
end
if type(args) ~= "table" then
args = { args }
end

local job = Job({
command = self.adapter:bin(),
args = self.adapter:get_log_args(args),
args = self.adapter:get_log_args(args, paths),
cwd = self.adapter.ctx.toplevel,
})

Expand All @@ -116,6 +129,8 @@ CommitLogPanel.update = async.void(function(self, args)
return
end

self.paths = paths

if not self:is_open() then
self:init_buffer()
else
Expand Down
4 changes: 3 additions & 1 deletion lua/diffview/vcs/adapter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,10 @@ function VCSAdapter:get_show_args(path, rev)
end

---@param args string[]
---@param paths? string[] # Optional file paths to filter the log by.
---@return string[] args to show commit log message
function VCSAdapter:get_log_args(args)
---@diagnostic disable-next-line: unused-local
function VCSAdapter:get_log_args(args, paths)
oop.abstract_stub()
end

Expand Down
26 changes: 24 additions & 2 deletions lua/diffview/vcs/adapters/git/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,30 @@ function GitAdapter:get_show_args(path, rev)
)
end

function GitAdapter:get_log_args(args)
return utils.vec_join(self:args(), "log", "--no-show-signature", "--first-parent", "--stat", args)
---@param args string[]
---@param paths? string[] # Optional file paths to filter the log by.
---@return string[]
function GitAdapter:get_log_args(args, paths)
-- `git log -- <path>` interprets the path as a pathspec, so a literal
-- filename with `*`, `?`, `[`, or a leading `:` glob/misfire. `:(literal)`
-- pathspec magic (supported since git 1.9) forces exact match, matching
-- how the jj adapter's `quote_path_args` handles the same concern.
local literal_paths = paths
and #paths > 0
and vim.tbl_map(function(p)
return ":(literal)" .. p
end, paths)
or nil
return utils.vec_join(
self:args(),
"log",
"--no-show-signature",
"--first-parent",
"--stat",
args,
literal_paths and "--" or nil,
literal_paths
)
end

function GitAdapter:get_dir(path)
Expand Down
26 changes: 24 additions & 2 deletions lua/diffview/vcs/adapters/hg/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,30 @@ function HgAdapter:get_show_args(path, rev)
return utils.vec_join(self:args(), "cat", "--rev", rev:object_name(), "--", path)
end

function HgAdapter:get_log_args(args)
return utils.vec_join(self:args(), "log", "--stat", "--rev", args)
---@param args string[]
---@param paths? string[] # Optional file paths to filter the log by.
---@return string[]
function HgAdapter:get_log_args(args, paths)
-- Mercurial treats bare filename arguments as glob patterns by default
-- (see `hg help patterns`), so a literal filename with `*`, `?`, or `[`
-- would misfire. The `path:` prefix pins the argument to a literal,
-- repo-relative path, matching how the git adapter uses `:(literal)` for
-- the same concern.
local literal_paths = paths
and #paths > 0
and vim.tbl_map(function(p)
return "path:" .. p
end, paths)
or nil
return utils.vec_join(
self:args(),
"log",
"--stat",
"--rev",
args,
literal_paths and "--" or nil,
literal_paths
)
end

function HgAdapter:get_dir(path)
Expand Down
Loading
Loading