From 7c21edca8d9323ac5e50f8ca3a85ece2b886f3ef Mon Sep 17 00:00:00 2001 From: James Trew Date: Sat, 8 Jul 2023 21:13:35 -0400 Subject: [PATCH 1/3] feat(git): support detached working trees closes #2595 --- lua/telescope/builtin/__git.lua | 66 ++++++++++++------- lua/telescope/config.lua | 19 ++++++ lua/telescope/previewers/buffer_previewer.lua | 20 +++--- lua/telescope/utils.lua | 18 +++++ 4 files changed, 91 insertions(+), 32 deletions(-) diff --git a/lua/telescope/builtin/__git.lua b/lua/telescope/builtin/__git.lua index a3516b1deb..d839dc92a0 100644 --- a/lua/telescope/builtin/__git.lua +++ b/lua/telescope/builtin/__git.lua @@ -10,9 +10,14 @@ local strings = require "plenary.strings" local Path = require "plenary.path" local conf = require("telescope.config").values +local git_command = utils.__git_command local git = {} +local get_git_command_output = function(args, opts) + return utils.get_os_command_output(git_command(args, opts), opts.cwd) +end + git.files = function(opts) if opts.is_bare then utils.notify("builtin.git_files", { @@ -35,14 +40,14 @@ git.files = function(opts) -- By creating the entry maker after the cwd options, -- we ensure the maker uses the cwd options when being created. opts.entry_maker = vim.F.if_nil(opts.entry_maker, make_entry.gen_from_file(opts)) - local git_command = vim.F.if_nil(opts.git_command, { "git", "ls-files", "--exclude-standard", "--cached" }) + opts.git_command = vim.F.if_nil(opts.git_command, git_command({ "ls-files", "--exclude-standard", "--cached" }, opts)) pickers .new(opts, { prompt_title = "Git Files", finder = finders.new_oneshot_job( vim.tbl_flatten { - git_command, + opts.git_command, show_untracked and "--others" or nil, recurse_submodules and "--recurse-submodules" or nil, }, @@ -56,12 +61,13 @@ end git.commits = function(opts) opts.entry_maker = vim.F.if_nil(opts.entry_maker, make_entry.gen_from_git_commits(opts)) - local git_command = vim.F.if_nil(opts.git_command, { "git", "log", "--pretty=oneline", "--abbrev-commit", "--", "." }) + opts.git_command = + vim.F.if_nil(opts.git_command, git_command({ "log", "--pretty=oneline", "--abbrev-commit", "--", "." }, opts)) pickers .new(opts, { prompt_title = "Git Commits", - finder = finders.new_oneshot_job(git_command, opts), + finder = finders.new_oneshot_job(opts.git_command, opts), previewer = { previewers.git_commit_diff_to_parent.new(opts), previewers.git_commit_diff_to_head.new(opts), @@ -83,19 +89,12 @@ end git.stash = function(opts) opts.show_branch = vim.F.if_nil(opts.show_branch, true) opts.entry_maker = vim.F.if_nil(opts.entry_maker, make_entry.gen_from_git_stash(opts)) + opts.git_command = vim.F.if_nil(opts.git_command, git_command({ "--no-pager", "stash", "list" }, opts)) pickers .new(opts, { prompt_title = "Git Stash", - finder = finders.new_oneshot_job( - vim.tbl_flatten { - "git", - "--no-pager", - "stash", - "list", - }, - opts - ), + finder = finders.new_oneshot_job(opts.git_command, opts), previewer = previewers.git_stash_diff.new(opts), sorter = conf.file_sorter(opts), attach_mappings = function() @@ -115,15 +114,15 @@ git.bcommits = function(opts) opts.current_line = (opts.current_file == nil) and get_current_buf_line(opts.winnr) or nil opts.current_file = vim.F.if_nil(opts.current_file, vim.api.nvim_buf_get_name(opts.bufnr)) opts.entry_maker = vim.F.if_nil(opts.entry_maker, make_entry.gen_from_git_commits(opts)) - local git_command = - vim.F.if_nil(opts.git_command, { "git", "log", "--pretty=oneline", "--abbrev-commit", "--follow" }) + opts.git_command = + vim.F.if_nil(opts.git_command, git_command({ "log", "--pretty=oneline", "--abbrev-commit", "--follow" }, opts)) pickers .new(opts, { prompt_title = "Git BCommits", finder = finders.new_oneshot_job( vim.tbl_flatten { - git_command, + opts.git_command, opts.current_file, }, opts @@ -200,10 +199,12 @@ git.branches = function(opts) .. "%(authorname)" .. "%(upstream:lstrip=2)" .. "%(committerdate:format-local:%Y/%m/%d %H:%M:%S)" - local output = utils.get_os_command_output( - { "git", "for-each-ref", "--perl", "--format", format, "--sort", "-authordate", opts.pattern }, - opts.cwd + + local output = get_git_command_output( + { "for-each-ref", "--perl", "--format", format, "--sort", "-authordate", opts.pattern }, + opts ) + local show_remote_tracking_branches = vim.F.if_nil(opts.show_remote_tracking_branches, true) local results = {} @@ -217,7 +218,7 @@ git.branches = function(opts) return string.gsub(v, "\\([\\'])", "%1") end local parse_line = function(line) - local fields = vim.split(string.sub(line, 2, -2), "''", true) + local fields = vim.split(string.sub(line, 2, -2), "''") local entry = { head = fields[1], refname = unescape_single_quote(fields[2]), @@ -320,7 +321,7 @@ git.status = function(opts) local gen_new_finder = function() local expand_dir = vim.F.if_nil(opts.expand_dir, true) - local git_cmd = { "git", "status", "-z", "--", "." } + local git_cmd = git_command({ "status", "-z", "--", "." }, opts) if expand_dir then table.insert(git_cmd, #git_cmd - 1, "-u") @@ -329,7 +330,6 @@ git.status = function(opts) local output = utils.get_os_command_output(git_cmd, opts.cwd) if #output == 0 then - print "No changes found" utils.notify("builtin.git_status", { msg = "No changes found", level = "WARN", @@ -379,6 +379,26 @@ git.status = function(opts) :find() end +local try_worktrees = function(opts) + local worktrees = conf.git_worktrees + + if vim.tbl_isarray(worktrees) then + for _, wt in ipairs(worktrees) do + local paths, ret = get_git_command_output( + { "rev-parse", "--show-toplevel", "--absolute-git-dir" }, + { toplevel = wt.toplevel, gitdir = wt.gitdir } + ) + if ret == 0 then + opts.toplevel = paths[1] + opts.gitdir = paths[2] + return + end + end + end + + error(opts.cwd .. " is not a git directory") +end + local set_opts_cwd = function(opts) if opts.cwd then opts.cwd = vim.fn.expand(opts.cwd) @@ -397,7 +417,7 @@ local set_opts_cwd = function(opts) local in_bare = utils.get_os_command_output({ "git", "rev-parse", "--is-bare-repository" }, opts.cwd) if in_worktree[1] ~= "true" and in_bare[1] ~= "true" then - error(opts.cwd .. " is not a git directory") + try_worktrees(opts) elseif in_worktree[1] ~= "true" and in_bare[1] == "true" then opts.is_bare = true end diff --git a/lua/telescope/config.lua b/lua/telescope/config.lua index 89791c5e43..85287b8b2b 100644 --- a/lua/telescope/config.lua +++ b/lua/telescope/config.lua @@ -772,6 +772,25 @@ append( ]] ) +append( + "git_worktrees", + nil, + [[ + A table of arrays of detached working trees with keys `gitdir` and `toplevel`. + Used to pass `--git-dir` and `--work-tree` flags to git commands when telescope fails + to infer the top-level directory of a given working tree based on cwd. + Example: + git_worktrees = { + { + toplevel = vim.env.HOME, + gitdir = vim.env.HOME .. '/.cfg' + } + } + + Default: nil + ]] +) + append( "file_previewer", function(...) diff --git a/lua/telescope/previewers/buffer_previewer.lua b/lua/telescope/previewers/buffer_previewer.lua index 5f92ac95bb..703ac6739f 100644 --- a/lua/telescope/previewers/buffer_previewer.lua +++ b/lua/telescope/previewers/buffer_previewer.lua @@ -8,6 +8,7 @@ local conf = require("telescope.config").values local pscan = require "plenary.scandir" local buf_delete = utils.buf_delete +local git_command = utils.__git_command local previewers = {} @@ -757,8 +758,7 @@ previewers.git_branch_log = defaulter(function(opts) end, define_preview = function(self, entry) - local cmd = { - "git", + local cmd = git_command({ "--no-pager", "log", "--graph", @@ -766,7 +766,7 @@ previewers.git_branch_log = defaulter(function(opts) "--abbrev-commit", "--date=relative", entry.value, - } + }, opts) putils.job_maker(cmd, self.state.bufnr, { value = entry.value, @@ -791,7 +791,8 @@ previewers.git_stash_diff = defaulter(function(opts) end, define_preview = function(self, entry, _) - putils.job_maker({ "git", "--no-pager", "stash", "show", "-p", entry.value }, self.state.bufnr, { + local cmd = git_command({ "--no-pager", "stash", "show", "-p", entry.value }, opts) + putils.job_maker(cmd, self.state.bufnr, { value = entry.value, bufname = self.state.bufname, cwd = opts.cwd, @@ -814,7 +815,7 @@ previewers.git_commit_diff_to_parent = defaulter(function(opts) end, define_preview = function(self, entry) - local cmd = { "git", "--no-pager", "diff", entry.value .. "^!" } + local cmd = git_command({ "--no-pager", "diff", entry.value .. "^!" }, opts) if opts.current_file then table.insert(cmd, "--") table.insert(cmd, opts.current_file) @@ -845,7 +846,7 @@ previewers.git_commit_diff_to_head = defaulter(function(opts) end, define_preview = function(self, entry) - local cmd = { "git", "--no-pager", "diff", "--cached", entry.value } + local cmd = git_command({ "--no-pager", "diff", "--cached", entry.value }, opts) if opts.current_file then table.insert(cmd, "--") table.insert(cmd, opts.current_file) @@ -876,7 +877,7 @@ previewers.git_commit_diff_as_was = defaulter(function(opts) end, define_preview = function(self, entry) - local cmd = { "git", "--no-pager", "show" } + local cmd = git_command({ "--no-pager", "show" }, opts) local cf = opts.current_file and Path:new(opts.current_file):make_relative(opts.cwd) local value = cf and (entry.value .. ":" .. cf) or entry.value local ft = cf and putils.filetype_detect(value) or "diff" @@ -910,7 +911,7 @@ previewers.git_commit_message = defaulter(function(opts) end, define_preview = function(self, entry) - local cmd = { "git", "--no-pager", "log", "-n 1", entry.value } + local cmd = git_command({ "--no-pager", "log", "-n 1", entry.value }, opts) putils.job_maker(cmd, self.state.bufnr, { value = entry.value, @@ -950,7 +951,8 @@ previewers.git_file_diff = defaulter(function(opts) winid = self.state.winid, }) else - putils.job_maker({ "git", "--no-pager", "diff", "HEAD", "--", entry.value }, self.state.bufnr, { + local cmd = git_command({ "--no-pager", "diff", "HEAD", "--", entry.value }, opts) + putils.job_maker(cmd, self.state.bufnr, { value = entry.value, bufname = self.state.bufname, cwd = opts.cwd, diff --git a/lua/telescope/utils.lua b/lua/telescope/utils.lua index 20ae91cc6b..c15ba2bdd0 100644 --- a/lua/telescope/utils.lua +++ b/lua/telescope/utils.lua @@ -517,4 +517,22 @@ utils.__warn_no_selection = function(name) }) end +--- Generate git command optionally with git env variables +---@param args string[] +---@param opts? table +---@return string[] +utils.__git_command = function(args, opts) + opts = opts or {} + + local _args = { "git" } + if opts.gitdir then + vim.list_extend(_args, { "--git-dir", opts.gitdir }) + end + if opts.toplevel then + vim.list_extend(_args, { "--work-tree", opts.toplevel }) + end + + return vim.list_extend(_args, args) +end + return utils From b00b9e7650dac1706ea0a1d8e631628c1cd4f51f Mon Sep 17 00:00:00 2001 From: Github Actions Date: Sun, 9 Jul 2023 17:59:51 +0000 Subject: [PATCH 2/3] [docgen] Update doc/telescope.txt skip-checks: true --- doc/telescope.txt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/doc/telescope.txt b/doc/telescope.txt index 5d88c1ad0f..18dc03b2aa 100644 --- a/doc/telescope.txt +++ b/doc/telescope.txt @@ -638,6 +638,22 @@ telescope.setup({opts}) *telescope.setup()* Default: `function() return 0 end` + *telescope.defaults.git_worktrees* + git_worktrees: ~ + A table of arrays of detached working trees with keys `gitdir` and `toplevel`. + Used to pass `--git-dir` and `--work-tree` flags to git commands when telescope fails + to infer the top-level directory of a given working tree based on cwd. + Example: + git_worktrees = { + { + toplevel = vim.env.HOME, + gitdir = vim.env.HOME .. '/.cfg' + } + } + + Default: nil + + *telescope.defaults.file_previewer* file_previewer: ~ Function pointer to the default file_previewer. It is mostly used From 49d2f2eb2c7e5185509273cf6a6e802ba21f52c0 Mon Sep 17 00:00:00 2001 From: James Trew Date: Thu, 13 Jul 2023 22:18:06 -0400 Subject: [PATCH 3/3] fix: use_file_path --- lua/telescope/builtin/__git.lua | 37 +++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/lua/telescope/builtin/__git.lua b/lua/telescope/builtin/__git.lua index d839dc92a0..380570a40e 100644 --- a/lua/telescope/builtin/__git.lua +++ b/lua/telescope/builtin/__git.lua @@ -384,13 +384,12 @@ local try_worktrees = function(opts) if vim.tbl_isarray(worktrees) then for _, wt in ipairs(worktrees) do - local paths, ret = get_git_command_output( - { "rev-parse", "--show-toplevel", "--absolute-git-dir" }, - { toplevel = wt.toplevel, gitdir = wt.gitdir } - ) - if ret == 0 then - opts.toplevel = paths[1] - opts.gitdir = paths[2] + if vim.startswith(opts.cwd, wt.toplevel) then + opts.toplevel = wt.toplevel + opts.gitdir = wt.gitdir + if opts.use_git_root then + opts.cwd = wt.toplevel + end return end end @@ -399,18 +398,30 @@ local try_worktrees = function(opts) error(opts.cwd .. " is not a git directory") end +local current_path_toplevel = function() + local gitdir = vim.fn.finddir(".git", vim.fn.expand "%:p" .. ";") + if gitdir == "" then + return nil + end + return Path:new(gitdir):parent():absolute() +end + local set_opts_cwd = function(opts) + opts.use_git_root = vim.F.if_nil(opts.use_git_root, true) if opts.cwd then opts.cwd = vim.fn.expand(opts.cwd) elseif opts.use_file_path then - opts.cwd = vim.fn.finddir(".git", vim.fn.expand "%:p" .. ";") + opts.cwd = current_path_toplevel() + if not opts.cwd then + opts.cwd = vim.fn.expand "%:p:h" + try_worktrees(opts) + return + end else opts.cwd = vim.loop.cwd() end - -- Find root of git directory and remove trailing newline characters - local git_root, ret = utils.get_os_command_output({ "git", "rev-parse", "--show-toplevel" }, opts.cwd) - local use_git_root = vim.F.if_nil(opts.use_git_root, true) + local toplevel, ret = utils.get_os_command_output({ "git", "rev-parse", "--show-toplevel" }, opts.cwd) if ret ~= 0 then local in_worktree = utils.get_os_command_output({ "git", "rev-parse", "--is-inside-work-tree" }, opts.cwd) @@ -422,8 +433,8 @@ local set_opts_cwd = function(opts) opts.is_bare = true end else - if use_git_root then - opts.cwd = git_root[1] + if opts.use_git_root then + opts.cwd = toplevel[1] end end end