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
16 changes: 16 additions & 0 deletions doc/diffview.txt
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,14 @@ COMMANDS *diffview-commands*
invocation; pass `--no-panel=false` to force the panel
shown.

--rename-threshold={n}
Rename detection similarity, as an integer in [0, 100]
(a trailing `%` is accepted). Overrides
|diffview-config-rename_threshold| for this view.
Invalid values are ignored with a warning.
NOTE: This is Git-specific. For Jujutsu, this option
is ignored with a warning.

*:DiffviewDiffFiles*
:DiffviewDiffFiles {file1} {file2}

Expand Down Expand Up @@ -503,6 +511,14 @@ COMMANDS *diffview-commands*
YYYY-mm-dd, YYYY-mm-dd HH:mm:ss, or natural language (for
instance "2 weeks 6 hours 12 minutes ago")

--rename-threshold={n}
Rename detection similarity, as an integer in [0, 100]
(a trailing `%` is accepted). Overrides
|diffview-config-rename_threshold| for this view.
Invalid values are ignored with a warning.
NOTE: This is Git-specific. For Jujutsu, this option is
ignored with a warning.

Mercurial Options: ~

--rev={rev}
Expand Down
9 changes: 9 additions & 0 deletions lua/diffview/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,7 @@ M._config = M.defaults
---@field path_args string[]
---@field after? string
---@field before? string
---@field rename_threshold? integer Per-view rename similarity threshold (0-100). Overrides |diffview-config-rename_threshold| for this view.

---@class HgLogOptions
---@field follow? string
Expand Down Expand Up @@ -842,6 +843,7 @@ M._config = M.defaults
---@field path_args? string[]
---@field after? string
---@field before? string
---@field rename_threshold? integer Per-view rename similarity threshold (0-100). Overrides |diffview-config-rename_threshold| for this view.

---@class HgLogOptions.user
---@field follow? string
Expand Down Expand Up @@ -889,6 +891,7 @@ M.log_option_defaults = {
G = nil,
S = nil,
path_args = {},
rename_threshold = nil,
},
---@type HgLogOptions
hg = {
Expand Down Expand Up @@ -1886,4 +1889,10 @@ function M.setup(user_config)
end

M.actions = actions

-- Shared value validators. Used internally by `setup()` for the config schema,
-- and re-exported for CLI arg parsing (e.g., `--rename-threshold`) so both
-- surfaces produce the same warn-and-fallback behaviour.
M.validate = validate

return M
1 change: 1 addition & 0 deletions lua/diffview/scene/views/diff/diff_view.lua
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ local same_rev = lazy.access(rev_lib, "same_rev") --[[@as fun(a: Rev?, b: Rev?):
---@field show_untracked? boolean
---@field selected_file? string Path to the preferred initially selected file.
---@field selected_row? integer Row to position the cursor on after opening the selected file.
---@field rename_threshold? integer Per-view rename similarity threshold (0-100). Overrides |diffview-config-rename_threshold| for this view.

---@class DiffView : StandardView
---@operator call : DiffView
Expand Down
174 changes: 174 additions & 0 deletions lua/diffview/tests/functional/git_adapter_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ local GitAdapter = require("diffview.vcs.adapters.git").GitAdapter
local GitRev = require("diffview.vcs.adapters.git.rev").GitRev
local Job = require("diffview.job").Job
local RevType = require("diffview.vcs.rev").RevType
local arg_parser = require("diffview.arg_parser")
local test_utils = require("diffview.tests.helpers")

local run = test_utils.run
Expand Down Expand Up @@ -1847,4 +1848,177 @@ describe("diffview.vcs.adapters.git", function()
end)
)
end)

describe("diffview_options --rename-threshold", function()
it(
"parses an integer value onto DiffViewOptions",
test_utils.async_test(function()
local repo, adapter = make_repo_and_adapter()

local ok, err = pcall(function()
local argo = arg_parser.parse({ "--rename-threshold=40" })
local opt = adapter:diffview_options(argo)
assert.is_not_nil(opt)
assert.equals(40, opt.options.rename_threshold)
end)

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

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

it(
"accepts a trailing percent sign",
test_utils.async_test(function()
local repo, adapter = make_repo_and_adapter()

local ok, err = pcall(function()
local argo = arg_parser.parse({ "--rename-threshold=30%" })
local opt = adapter:diffview_options(argo)
assert.is_not_nil(opt)
assert.equals(30, opt.options.rename_threshold)
end)

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

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

it(
"leaves DiffViewOptions.rename_threshold nil for invalid values",
test_utils.async_test(function()
local repo, adapter = make_repo_and_adapter()

local ok, err = pcall(function()
-- Out of range and non-numeric both fall back to nil (with a warning).
for _, bad in ipairs({ "abc", "-5", "150", "12.5" }) do
local argo = arg_parser.parse({ "--rename-threshold=" .. bad })
local opt = adapter:diffview_options(argo)
assert.is_not_nil(opt)
assert.is_nil(opt.options.rename_threshold)
end
end)

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

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

it(
"leaves DiffViewOptions.rename_threshold nil when the flag is absent",
test_utils.async_test(function()
local repo, adapter = make_repo_and_adapter()

local ok, err = pcall(function()
local argo = arg_parser.parse({})
local opt = adapter:diffview_options(argo)
assert.is_not_nil(opt)
assert.is_nil(opt.options.rename_threshold)
end)

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

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

it(
"leaves DiffViewOptions.rename_threshold nil for a bare or empty value",
test_utils.async_test(function()
local repo, adapter = make_repo_and_adapter()

local ok, err = pcall(function()
-- Bare `--rename-threshold` (arg_parser records the value as "true"
-- and casts it to boolean) and an explicitly-empty `--rename-threshold=`
-- both distinct from "flag absent"; the parser warns and drops them.
for _, args in ipairs({ { "--rename-threshold" }, { "--rename-threshold=" } }) do
local argo = arg_parser.parse(args)
local opt = adapter:diffview_options(argo)
assert.is_not_nil(opt)
assert.is_nil(opt.options.rename_threshold)
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("file_history_options --rename-threshold", function()
it(
"parses an integer value onto GitLogOptions",
test_utils.async_test(function()
local repo, adapter = make_repo_and_adapter()

local ok, err = pcall(function()
local argo = arg_parser.parse({ "--rename-threshold=40" })
local log_opt = adapter:file_history_options(nil, { "init.txt" }, argo)
assert.is_not_nil(log_opt)
assert.equals(40, log_opt.rename_threshold)
end)

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

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

it(
"leaves GitLogOptions.rename_threshold nil for invalid values",
test_utils.async_test(function()
local repo, adapter = make_repo_and_adapter()

local ok, err = pcall(function()
local argo = arg_parser.parse({ "--rename-threshold=nope" })
local log_opt = adapter:file_history_options(nil, { "init.txt" }, argo)
assert.is_not_nil(log_opt)
assert.is_nil(log_opt.rename_threshold)
end)

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

if not ok then
error(err)
end
end)
)
end)
end)
1 change: 1 addition & 0 deletions lua/diffview/vcs/adapter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ local M = {}
---@field pin_local? boolean # When true, file-history entries are constructed with revs.b = LOCAL so the b-window can pin to the working-tree file.
---@field pinned_path? string # Working-tree path used for the b-side File when `pin_local` is true for a single-file history; preserves the pin across renames in older commits.
---@field pinned_b_file_for? fun(path: string): vcs.File # Resolves the shared, view-owned working-tree File for a given path. Set by `FileHistoryPanel` when `pin_local` is active so adapters can hand the same `vcs.File` instance to every entry's b-side; see `FileHistoryView:get_pinned_b_file`. The returned file outlives entry/log destruction (its layout symbol lives in `Diff2*Pinned.shared_symbols`), so adapters must not destroy it.
---@field rename_threshold? integer # Per-view rename similarity threshold (0-100) forwarded from `DiffViewOptions.rename_threshold`. Overrides the global config for this view's diff calls; adapters that don't support rename detection ignore it.

---@class vcs.adapter.VCSAdapter.Bootstrap
---@field done boolean # Did the bootstrapping
Expand Down
34 changes: 32 additions & 2 deletions lua/diffview/vcs/adapters/git/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,32 @@ local uv = vim.uv

local M = {}

---Parse a `--rename-threshold` CLI value.
---Accepts `"40"` or `"40%"` (trailing `%` matches git's own `--find-renames`
---spelling); the value must be an integer in [0, 100]. On invalid input warns
---and returns nil so the caller falls back to the global config. `nil` (flag
---absent) is the only silent return path; bare `--rename-threshold` (boolean
---`true` from `arg_parser`), an empty value (`--rename-threshold=`), and any
---non-integer or out-of-range string all warn via `config.validate.integer`.
---@param raw string[]|string|boolean|nil
---@return integer?
local function parse_rename_threshold_flag(raw)
if raw == nil then
return nil
end
-- `tonumber("40%")` returns nil, so strip a trailing `%` before delegating
-- to `validate.integer`. Non-string inputs (e.g., a boolean from a bare
-- flag) pass through and the validator warns as expected.
local wrap = { v = type(raw) == "string" and (raw:gsub("%%$", "")) or raw }
config.validate.integer(wrap, "v", nil, {
min = 0,
max = 100,
nilable = true,
path = "--rename-threshold",
})
return wrap.v --[[@as integer? ]]
end

---@class GitAdapter : VCSAdapter
---@operator call : GitAdapter
local GitAdapter = oop.create_class("GitAdapter", VCSAdapter)
Expand Down Expand Up @@ -575,7 +601,7 @@ function GitAdapter:stream_fh_data(state)
"--no-show-signature",
"--pretty=format:%x00%n" .. GitAdapter.COMMIT_PRETTY_FMT,
(function()
local t = config.get_config().rename_threshold
local t = state.log_options.rename_threshold or config.get_config().rename_threshold
return t and ("-M" .. t .. "%") or nil
end)(),
"--numstat",
Expand Down Expand Up @@ -877,6 +903,7 @@ function GitAdapter:file_history_options(range, paths, argo)
---@diagnostic disable-next-line: assign-type-mismatch
log_options[key] = v
end
log_options.rename_threshold = parse_rename_threshold_flag(argo:get_flag("rename-threshold"))

if range then
paths, rel_paths = {}, {}
Expand Down Expand Up @@ -1707,6 +1734,7 @@ function GitAdapter:diffview_options(argo)
or nil
) --[[@as string? ]],
selected_row = tonumber(argo:get_flag("selected-row", { no_empty = true })),
rename_threshold = parse_rename_threshold_flag(argo:get_flag("rename-threshold")),
}

return { left = left, right = right, options = options }
Expand Down Expand Up @@ -2260,7 +2288,7 @@ GitAdapter.tracked_files = async.wrap(function(self, left, right, args, kind, op
---@type FileEntry[]
local conflicts = {}
local log_opt = { label = "GitAdapter:tracked_files()" }
local rename_threshold = config.get_config().rename_threshold
local rename_threshold = opt.rename_threshold or config.get_config().rename_threshold
local rename_flag = rename_threshold and ("-M" .. rename_threshold .. "%") or nil

local namestat_job = Job({
Expand Down Expand Up @@ -2776,6 +2804,7 @@ function GitAdapter:init_completion()
end)
self.comp.open:put({ "selected-row" })
self.comp.open:put({ "no-panel" })
self.comp.open:put({ "rename-threshold" }, {})

self.comp.file_history:put({ "base" }, function(_, arg_lead)
return utils.vec_join("LOCAL", self:rev_candidates(arg_lead))
Expand Down Expand Up @@ -2819,6 +2848,7 @@ function GitAdapter:init_completion()
self.comp.file_history:put({ "-S" }, {})
self.comp.file_history:put({ "--after", "--since" }, {})
self.comp.file_history:put({ "--before", "--until" }, {})
self.comp.file_history:put({ "--rename-threshold" }, {})
end

M.GitAdapter = GitAdapter
Expand Down
8 changes: 8 additions & 0 deletions lua/diffview/vcs/adapters/jj/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,10 @@ function JjAdapter:diffview_options(argo)

logger:fmt_debug("Parsed revs: left = %s, right = %s", left, right)

if argo:get_flag("rename-threshold") then
utils.warn("The '--rename-threshold' option is not supported for Jujutsu. Ignoring.")
end

local options = {
show_untracked = arg_parser.ambiguous_bool(
argo:get_flag({ "u", "untracked-files" }, { plain = true }),
Expand Down Expand Up @@ -833,6 +837,10 @@ function JjAdapter:file_history_options(range, paths, argo)
return
end

if argo:get_flag("rename-threshold") then
utils.warn("The '--rename-threshold' option is not supported for Jujutsu. Ignoring.")
end

local rel_paths = vim.tbl_map(function(v)
return v == "." and "." or pl:relative(v, ".")
end, paths) --[[@as string[] ]]
Expand Down
6 changes: 6 additions & 0 deletions lua/diffview/vcs/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ M.diff_file_list = async.wrap(function(adapter, left, right, path_args, dv_opt,
local files = FileDict()
local rev_args = adapter:rev_to_args(left, right)
local errors = {}

-- Forward the CLI `--rename-threshold` override into the per-view `opt` so
-- `tracked_files` picks it up without widening its signature.
if dv_opt.rename_threshold then
opt.rename_threshold = dv_opt.rename_threshold
end
(function()
local err, tfiles, tconflicts = await(
adapter:tracked_files(left, right, utils.vec_join(rev_args, "--", path_args), "working", opt)
Expand Down
Loading