Skip to content

Commit

Permalink
feat(git): resolve symlinks before getting Git related data
Browse files Browse the repository at this point in the history
  • Loading branch information
echasnovski committed Jan 10, 2025
1 parent bf3c00f commit 89c9f7b
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 30 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

- BREAKING FEATURE: add `max_output_steps` option to `gen_path.line()` and `gen_path.angle()` to limit the number of steps the return. Default is 1000 to improve performance on large cursor jumps which also is set for `config.cursor.path`.

## mini.git

- FEATURE: Git data is computed after resolving symlinks. This allows working with files symlinked into outside of Git repo. This behavior is the same as in 'mini.diff'.

## mini.hipatterns

- BREAKING FEATURE: make `MiniHipatterns{Fixme,Hack,Todo,Note}` highlight groups by default be reverse and bold variant of `Diagnostic{Error,Warn,Info,Hint}` group instead of directly link to them. This ensures better visibility for color schemes which don't have explicit 'mini.hipatterns' support.
Expand Down
25 changes: 15 additions & 10 deletions lua/mini/git.lua
Original file line number Diff line number Diff line change
Expand Up @@ -334,12 +334,14 @@ MiniGit.show_at_cursor = function(opts)
-- Try showing diff source
if H.diff_pos_to_source() ~= nil then return MiniGit.show_diff_source(opts) end

-- Try showing range history if possible: either in Git repo (tracked or not)
-- or diff source output.
local buf_id, path = vim.api.nvim_get_current_buf(), vim.api.nvim_buf_get_name(0)
local is_in_git = H.is_buf_enabled(buf_id)
or #H.git_cli_output({ 'rev-parse', '--show-toplevel' }, vim.fn.fnamemodify(path, ':h')) > 0
local is_diff_source_output = H.parse_diff_source_buf_name(path) ~= nil
-- Try showing range history if possible: either in Git repo (tracked or not;
-- after resolving symlinks) or diff source output.
local buf_id, buf_name = vim.api.nvim_get_current_buf(), vim.api.nvim_buf_get_name(0)
local path = vim.loop.fs_realpath(buf_name)
local path_dir = path == nil and '' or vim.fn.fnamemodify(path, ':h')

local is_in_git = H.is_buf_enabled(buf_id) or #H.git_cli_output({ 'rev-parse', '--show-toplevel' }, path_dir) > 0
local is_diff_source_output = H.parse_diff_source_buf_name(buf_name) ~= nil
if is_in_git or is_diff_source_output then return MiniGit.show_range_history(opts) end

H.notify('Nothing Git-related to show at cursor', 'WARN')
Expand Down Expand Up @@ -443,7 +445,7 @@ MiniGit.show_range_history = function(opts)
if commit == nil then
commit = 'HEAD'
local cwd_pattern = '^' .. vim.pesc(cwd:gsub('\\', '/')) .. '/'
rel_path = buf_name:gsub('\\', '/'):gsub(cwd_pattern, '')
rel_path = H.get_buf_realpath(0):gsub('\\', '/'):gsub(cwd_pattern, '')
end

-- Ensure no uncommitted changes as they might result into improper `-L` arg
Expand Down Expand Up @@ -517,7 +519,7 @@ MiniGit.enable = function(buf_id)
if H.is_buf_enabled(buf_id) or H.is_disabled(buf_id) or not H.has_git then return end

-- Enable only in buffers which *can* be part of Git repo
local path = vim.api.nvim_buf_get_name(buf_id)
local path = H.get_buf_realpath(buf_id)
if path == '' or vim.fn.filereadable(path) ~= 1 then return end

-- Start tracking
Expand Down Expand Up @@ -1177,7 +1179,7 @@ H.define_minigit_window = function(cleanup)
end

H.git_cli_output = function(args, cwd, env)
if cwd ~= nil and vim.fn.isdirectory(cwd) ~= 1 then return {} end
if cwd ~= nil and (vim.fn.isdirectory(cwd) ~= 1 or cwd == '') then return {} end
local command = { MiniGit.config.job.git_executable, '--no-pager', unpack(args) }
local res = H.cli_run(command, cwd, nil, { env = env }).out
if res == '' then return {} end
Expand Down Expand Up @@ -1423,7 +1425,7 @@ H.update_git_status = function(root, bufs)
local root_len, path_data = string.len(root), {}
for _, buf_id in ipairs(bufs) do
-- Use paths relative to the root as in `git status --porcelain` output
local rel_path = vim.api.nvim_buf_get_name(buf_id):sub(root_len + 2)
local rel_path = H.get_buf_realpath(buf_id):sub(root_len + 2)
table.insert(command, rel_path)
-- Completely not modified paths should be the only ones missing in the
-- output. Use this status as default.
Expand Down Expand Up @@ -1698,6 +1700,9 @@ end
-- TODO: Remove after compatibility with Neovim=0.9 is dropped
H.islist = vim.fn.has('nvim-0.10') == 1 and vim.islist or vim.tbl_islist

-- Try getting buffer's full real path (after resolving symlinks)
H.get_buf_realpath = function(buf_id) return vim.loop.fs_realpath(vim.api.nvim_buf_get_name(buf_id)) or '' end

H.redrawstatus = function() vim.cmd('redrawstatus') end
if vim.api.nvim__redraw ~= nil then H.redrawstatus = function() vim.api.nvim__redraw({ statusline = true }) end end

Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
09|~
10|~
11|~
12|~
13|~ .git-dir/
14|~ dir-in-git/
15|~ file-in-git
12|~ .git-dir/
13|~ dir-in-git/
14|~ file-in-git
15|~ file-in-git_symlink-
16|:Git add -- .git-dir/
17|

Expand All @@ -29,9 +29,9 @@
09|11111111111111111111111111111111
10|11111111111111111111111111111111
11|11111111111111111111111111111111
12|11111111111111111111111111111111
13|11111111111222222222222222211111
14|11111111111333333333333333311111
15|11111111111333333333333333311111
12|11111111111222222222222222222222
13|11111111111333333333333333333333
14|11111111111333333333333333333333
15|11111111111333333333333333333333
16|44444444444444444444444444444444
17|44444444444444444444444444444444
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
07|~
08|~
09|~
10|~
11|~ git-repo/.git-dir/
12|~ git-repo/dir-in-git/
13|~ git-repo/file-in-git
10|~ git-repo/.git-dir/
11|~ git-repo/dir-in-git/
12|~ git-repo/file-in-git
13|~ git-repo/file-in-git_symlink
14|:Git add -- git-repo/.git-dir/
15|

Expand All @@ -25,9 +25,9 @@
07|1111111111111111111111111111111111111111
08|1111111111111111111111111111111111111111
09|1111111111111111111111111111111111111111
10|1111111111111111111111111111111111111111
11|1111111111122222222222222222222221111111
12|1111111111133333333333333333333331111111
13|1111111111133333333333333333333331111111
10|1111111111122222222222222222222222222222
11|1111111111133333333333333333333333333333
12|1111111111133333333333333333333333333333
13|1111111111133333333333333333333333333333
14|4444444444444444444444444444444444444444
15|4444444444444444444444444444444444444444
57 changes: 53 additions & 4 deletions tests/test_git.lua
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ T['show_at_cursor()']['works on commit'] = function()
child.bo.iskeyword = child.bo.iskeyword .. ',.'
show_at_cursor()
-- - No `git show` calls because "abc1234.def" does not match commit pattern
eq(get_spawn_log()[1].options.args, { '--no-pager', 'rev-parse', '--show-toplevel' })
validate_git_spawn_log({})
end

T['show_at_cursor()']['uses correct pattern to match commit'] = function()
Expand All @@ -283,7 +283,8 @@ T['show_at_cursor()']['uses correct pattern to match commit'] = function()

show_at_cursor()
local ref_args = { '--no-pager', 'show', '--stat', '--patch', cword }
local is_commit = vim.deep_equal(get_spawn_log()[1].options.args, ref_args)
local spawn_log = get_spawn_log()
local is_commit = spawn_log[1] ~= nil and vim.deep_equal(spawn_log[1].options.args, ref_args)
eq(is_commit, ref_is_commit)
end

Expand Down Expand Up @@ -475,10 +476,19 @@ T['show_at_cursor()']['works for range history in not tracked file'] = function(
child.lua('MiniGit.show_range_history = function() end')
log_calls('MiniGit.show_range_history')

-- Mock real path presence
child.lua([[
vim.loop.fs_realpath = function() return "/home/user/repo-root/source/of/symlink" end
vim.fn.isdirectory = function() return 1 end
]])
child.lua([[_G.stdio_queue = { { { 'out', '/home/user/repo-root' } } }]])
set_lines({ 'Line 1' })

-- Should try to find Git root at symlink's source
show_at_cursor()
validate_git_spawn_log({ { '--no-pager', 'rev-parse', '--show-toplevel' } })
validate_git_spawn_log({
{ args = { '--no-pager', 'rev-parse', '--show-toplevel' }, cwd = '/home/user/repo-root/source/of' },
})
validate_calls({ { 'MiniGit.show_range_history' } })
end

Expand Down Expand Up @@ -996,6 +1006,7 @@ T['show_range_history()'] = new_set({
set_lines({ 'aaa', 'bbb', 'ccc' })
child.fn.chdir(git_root_dir)
child.api.nvim_buf_set_name(0, git_root_dir .. '/dir/tmp-file')
child.lua('vim.loop.fs_realpath = function(path) return path end')
child.lua([[_G.stdio_queue = {
{ { 'out', '' } }, -- No uncommitted changes
{ { 'out', 'commit abc1234\nLog output' } }, -- Asked logs
Expand Down Expand Up @@ -1214,6 +1225,17 @@ T['show_range_history()']['uses correct working directory'] = function()
validate_git_spawn_log(ref_git_spawn_log)
end

T['show_range_history()']['resolves symlinks'] = function()
child.lua('vim.loop.fs_realpath = function(path) return path .. "_symlink-source" end')
show_range_history()

local ref_git_spawn_log = {
{ args = { '--no-pager', 'diff', '-U0', 'HEAD', '--', 'dir/tmp-file_symlink-source' }, cwd = git_root_dir },
{ args = { '--no-pager', 'log', '-L1,1:dir/tmp-file_symlink-source', 'HEAD' }, cwd = git_root_dir },
}
validate_git_spawn_log(ref_git_spawn_log)
end

T['show_range_history()']['validates arguments'] = function()
local validate = function(opts, error_pattern)
expect.error(function() show_range_history(opts) end, error_pattern)
Expand Down Expand Up @@ -1365,6 +1387,25 @@ T['enable()']['does not work in non-file buffer'] = function()
validate_git_spawn_log({})
end

T['enable()']['resolves symlinks'] = function()
child.lua('vim.loop.fs_realpath = function(path) return path .. "_symlink-source" end')

child.lua([[
_G.stdio_queue = {
{ { 'out', _G.rev_parse_track } }, -- Get path to root and repo
{ { 'out', 'abc1234\nmain' } }, -- Get HEAD data
{ { 'out', '?? file-in-git_symlink-source' } }, -- Get file status data
}
]])
enable()

-- Should run Git CLI with data *after* resolving symlink
local status_args = get_spawn_log()[3].options.args
eq({ status_args[3], status_args[#status_args] }, { 'status', 'file-in-git_symlink-source' })
eq(get_buf_data().status, '??')
eq(child.b.minigit_summary_string, 'main (??)')
end

T['enable()']['normalizes input buffer'] = function()
enable(0)
eq(is_buf_enabled(), true)
Expand Down Expand Up @@ -1671,7 +1712,15 @@ T['Auto enable']['works after `:edit`'] = function()
eq(get_buf_data(buf_id).root, git_root_dir)
end

T['Tracking'] = new_set({ hooks = { pre_case = load_module } })
T['Tracking'] = new_set({
hooks = {
pre_case = function()
load_module()
-- Ensure proper separators when dealing with paths
if helpers.is_windows() then child.lua('vim.loop.fs_realpath = function(path) return path end') end
end,
},
})

T['Tracking']['works outside of Git repo'] = function()
child.lua('_G.process_mock_data = { { exit_code = 1 } }')
Expand Down

0 comments on commit 89c9f7b

Please sign in to comment.