diff --git a/README.md b/README.md index 6c557a4..d58341d 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,44 @@ -# Telescope Advanced Git Search +# Advanced Git Search + +An advanced git search extension for `Telescope` and `fzf-lua`. + +Search your git history by commit message, content and author in Neovim ## 🖥️ Usage -[![Demo](https://img.youtube.com/vi/bO0uYLlHtYo/0.jpg)](https://www.youtube.com/watch?v=bO0uYLlHtYo) +- [Demo](https://www.youtube.com/watch?v=bO0uYLlHtYo) ### 📖 Open a picker +#### 🔭 Telescope + ```vim :Telescope advanced_git_search {function_name} ``` -#### or in lua +> or in lua ```lua require('telescope').extensions.advanced_git_search.{function_name}() ``` -#### or through another Telescope picker +> or through another Telescope picker + +execute `:AdvancedGitSearch`, choose your picker and press `` + +#### 🧎 fzf-lua + +```lua +require('advanced-git-search.fzf').{function_name}() +``` + +> or through another picker execute `:AdvancedGitSearch`, choose your picker and press `` ### 🔎 Enter a query -Your usual telescope experience. See the individual commands for the grep behaviour. +Your usual search experience. See the individual commands for the grep behaviour. ### ✏️ Further search on commit author with `@` @@ -33,7 +49,7 @@ the author name. ### 1. search_log_content -- Search in repo log content -Opens a Telescope window with a list of all previous commit. +Opens a window with a list of all previous commit. _Grep behaviour_: filter on added, updated or removed code (log content: `-G` option in git). @@ -46,7 +62,7 @@ _Grep behaviour_: filter on added, updated or removed code (log content: `-G` op ### 2. search_log_content_file -- Search in file log content -Opens a Telescope window with a list of git commits that changed the +Opens a window with a list of git commits that changed the current file (renames included). _Grep behaviour_: filter on added, updated or removed code (log content: `-G` option in git). @@ -60,7 +76,7 @@ _Grep behaviour_: filter on added, updated or removed code (log content: `-G` op ### 3. diff_commit_file -- Diff current file with commit -Opens a Telescope window with a list of git commits that changed the +Opens a window with a list of git commits that changed the current file (renames included). _Grep behaviour_: filter on commit message. @@ -74,19 +90,18 @@ _Grep behaviour_: filter on commit message. ### 4. diff_commit_line -- Diff current file with selected line history -Opens a Telescope window with a list of previous commit logs with respect to +Opens a window with a list of previous commit logs with respect to selected lines _Grep behaviour_: filter on commit message. #### How to use -_The following only applies when you use one of the commands below._ - -```vim -:Telescope advanced_git_search diff_commit_line -:lua require('telescope').extensions.advanced_git_search.diff_commit_line() -``` +> _This workaround only applies when you use the following command. (Telescope)_ +> +> ```vim +> :Telescope advanced_git_search diff_commit_line +> ``` First you have to select the lines in visual mode, then go back to normal mode and execute this command. @@ -107,7 +122,7 @@ vim.api.nvim_set_keymap( ) ``` -No extra setup is needed when you use `:AdvancedGitSearch`. +> No extra setup is needed when you use `:AdvancedGitSearch`. #### _Keymaps_ @@ -118,7 +133,7 @@ No extra setup is needed when you use `:AdvancedGitSearch`. ### 5. diff_branch_file -- Diff file with branch -Opens a Telescope window with a list of local branches +Opens a window with a list of local branches _Grep behaviour_: filter on branch name. @@ -128,7 +143,7 @@ _Grep behaviour_: filter on branch name. ### 6. changed_on_branch -- Changed on current branch (experimental) -Opens a Telescope window with a list of changed files on the current branch (including staged files). +Opens a window with a list of changed files on the current branch (including staged files). The fork point of the current branch is determined with the following command: ```sh @@ -150,7 +165,7 @@ _Grep behaviour_: filter on filename. ### 7. checkout_reflog -- Checkout from reflog -Opens a Telescope window with all reflog entries +Opens a window with all reflog entries #### _Keymaps_ @@ -159,10 +174,12 @@ Opens a Telescope window with all reflog entries ### 8. show_custom_functions A telescope picker for all functions above. -Enable `show_builtin_git_pickers` to additionally show Telescopes builtin git pickers. +Enable `show_builtin_git_pickers` to additionally show builtin git pickers. ## ⚙️ Installation +### Telescope + With Lazy ```lua @@ -243,6 +260,75 @@ With Packer }) ``` +### Fzf-lua + +With Lazy + +```lua + { + "aaronhallaert/advanced-git-search.nvim", + config = function() + -- optional: setup telescope before loading the extension + require("advanced-git-search.fzf").setup{ + -- fugitive or diffview + diff_plugin = "fugitive", + -- customize git in previewer + -- e.g. flags such as { "--no-pager" }, or { "-c", "delta.side-by-side=false" } + git_flags = {}, + -- customize git diff in previewer + -- e.g. flags such as { "--raw" } + git_diff_flags = {}, + -- Show builtin git pickers when executing "show_custom_functions" or :AdvancedGitSearch + show_builtin_git_pickers = false, + } + end, + dependencies = { + "ibhagwan/fzf-lua", + -- to show diff splits and open commits in browser + "tpope/vim-fugitive", + -- to open commits in browser with fugitive + "tpope/vim-rhubarb", + -- OPTIONAL: to replace the diff from fugitive with diffview.nvim + -- (fugitive is still needed to open in browser) + -- "sindrets/diffview.nvim", + }, + } +``` + +With Packer + +```lua + use({ + "aaronhallaert/advanced-git-search.nvim", + config = function() + -- optional: setup telescope before loading the extension + require("advanced-git-search.fzf").setup{ + -- Fugitive or diffview + diff_plugin = "fugitive", + -- Customize git in previewer + -- e.g. flags such as { "--no-pager" }, or { "-c", "delta.side-by-side=false" } + git_flags = {}, + -- Customize git diff in previewer + -- e.g. flags such as { "--raw" } + git_diff_flags = {}, + -- Show builtin git pickers when executing "show_custom_functions" or :AdvancedGitSearch + show_builtin_git_pickers = false, + } + } + end, + requires = { + "ibhagwan/fzf-lua", + -- to show diff splits and open commits in browser + "tpope/vim-fugitive", + -- to open commits in browser with fugitive + "tpope/vim-rhubarb", + -- optional: to replace the diff from fugitive with diffview.nvim + -- (fugitive is still needed to open in browser) + -- "sindrets/diffview.nvim", + }, + }) +``` + ### Prerequisites - git diff --git a/lua/advanced_git_search/actions.lua b/lua/advanced_git_search/actions.lua new file mode 100644 index 0000000..c220dc9 --- /dev/null +++ b/lua/advanced_git_search/actions.lua @@ -0,0 +1,67 @@ +local config = require("advanced_git_search.utils.config") + +local M = {} + +---General action: Open entire commit with fugitive or diffview +---@param commit_hash string +M.open_commit = function(commit_hash) + local diff_plugin = config.diff_plugin() + + if diff_plugin == "diffview" then + vim.api.nvim_command( + ":DiffviewOpen -uno " .. commit_hash .. "~.." .. commit_hash + ) + elseif diff_plugin == "fugitive" then + vim.api.nvim_command(":Gedit " .. commit_hash) + end +end + +---General action: open diff for current file +---@param commit string commit or branch to diff with +---@param file_name string|nil file name to diff +M.open_diff_view = function(commit, file_name) + local diff_plugin = config.diff_plugin() + + if file_name ~= nil and file_name ~= "" then + if diff_plugin == "diffview" then + vim.api.nvim_command( + ":DiffviewOpen -uno " .. commit .. " -- " .. file_name + ) + elseif diff_plugin == "fugitive" then + vim.api.nvim_command(":Gvdiffsplit " .. commit .. ":" .. file_name) + end + else + if diff_plugin == "diffview" then + vim.api.nvim_command(":DiffviewOpen -uno " .. commit) + elseif diff_plugin == "fugitive" then + vim.api.nvim_command(":Gvdiffsplit " .. commit) + end + end +end + +---General action: Copy commit hash to system clipboard +---@param commit_hash string +M.copy_to_clipboard = function(commit_hash) + vim.notify( + "Copied commit hash " .. commit_hash .. " to clipboard", + vim.log.levels.INFO, + { title = "Advanced Git Search" } + ) + + vim.fn.setreg("+", commit_hash) + vim.fn.setreg("*", commit_hash) +end + +---General action: Open commit in browser +---@param commit_hash string +M.open_in_browser = function(commit_hash) + vim.api.nvim_command(":GBrowse " .. commit_hash) +end + +---General action: Checkout commit +---@param commit_hash string +M.checkout_commit = function(commit_hash) + vim.api.nvim_command(":!git checkout " .. commit_hash) +end + +return M diff --git a/lua/advanced_git_search/actions/init.lua b/lua/advanced_git_search/actions/init.lua deleted file mode 100644 index 28caf44..0000000 --- a/lua/advanced_git_search/actions/init.lua +++ /dev/null @@ -1,28 +0,0 @@ -local config = require("advanced_git_search.utils.config") - -local M = {} - ---- open diff for current file ---- @param commit string commit or branch to diff with ---- @param file_name string|nil file name to diff -M.open_diff_view = function(commit, file_name) - local diff_plugin = config.diff_plugin() - - if file_name ~= nil and file_name ~= "" then - if diff_plugin == "diffview" then - vim.api.nvim_command( - ":DiffviewOpen -uno " .. commit .. " -- " .. file_name - ) - elseif diff_plugin == "fugitive" then - vim.api.nvim_command(":Gvdiffsplit " .. commit .. ":" .. file_name) - end - else - if diff_plugin == "diffview" then - vim.api.nvim_command(":DiffviewOpen -uno " .. commit) - elseif diff_plugin == "fugitive" then - vim.api.nvim_command(":Gvdiffsplit " .. commit) - end - end -end - -return M diff --git a/lua/advanced_git_search/commands/find.lua b/lua/advanced_git_search/commands/find.lua new file mode 100644 index 0000000..4d4db62 --- /dev/null +++ b/lua/advanced_git_search/commands/find.lua @@ -0,0 +1,125 @@ +local file = require("advanced_git_search.utils.file") +local git_utils = require("advanced_git_search.utils.git") + +-- Specify shell commands for each finders in table format +local M = {} + +M.git_branches = function() + return { + "git", + "branch", + "--format='%(refname:short)'", + } +end + +M.reflog = function() + return { + "git", + "reflog", + "--date=iso", + } +end + +---@param prompt string|nil +---@param author string|nil +---@param bufnr number|nil +---@return table +M.git_log_content = function(prompt, author, bufnr) + local command = { + "git", + "log", + "--format='%h %as %an _ %s'", + } + + if author and author ~= "" and author ~= '""' then + table.insert(command, "--author=" .. author) + end + + if prompt and prompt ~= "" and prompt ~= '""' then + table.insert(command, "-G") + table.insert(command, prompt) + table.insert(command, "--pickaxe-all") + end + + if bufnr then + table.insert(command, "--follow") + local filename = file.relative_path(bufnr) + table.insert(command, filename) + end + + return vim.tbl_flatten(command) +end + +---@param prompt string|nil +---@param author string|nil +---@param bufnr number +---@return table +M.git_log_file = function(prompt, author, bufnr) + local filename = file.relative_path(bufnr) + local command = { + "git", + "log", + "--format='%h %as %an _ %s'", + } + + if author and author ~= "" and author ~= '""' then + table.insert(command, "--author=" .. author) + end + + if prompt and prompt ~= "" and prompt ~= '""' then + table.insert(command, "-s") + table.insert(command, "-i") + table.insert(command, "--grep=" .. prompt) + end + + table.insert(command, "--follow") + table.insert(command, filename) + + return vim.tbl_flatten(command) +end + +---@param prompt string|nil +---@param author string|nil +---@param bufnr number +---@param start_line number +---@param end_line number +---@return table +M.git_log_location = function(prompt, author, bufnr, start_line, end_line) + local filename = file.relative_path(bufnr) + local location = string.format("-L%d,%d:%s", start_line, end_line, filename) + local command = { + "git", + "log", + location, + "--no-patch", + "--format='%h %as %an _ %s'", + } + + if author and author ~= "" and author ~= '""' then + table.insert(command, "--author=" .. author) + end + + if prompt and prompt ~= "" and prompt ~= '""' then + table.insert(command, "-s") + table.insert(command, "-i") + table.insert(command, "--grep=" .. prompt) + end + + return vim.tbl_flatten(command) +end + +M.changed_on_branch = function() + return vim.tbl_flatten({ + "git", + "--no-pager", + "diff", + "--name-only", + "--cached", + "--diff-filter=ACMR", + "--merge-base", + git_utils.base_branch(), + "--relative", + }) +end + +return M diff --git a/lua/advanced_git_search/commands/preview.lua b/lua/advanced_git_search/commands/preview.lua new file mode 100644 index 0000000..e080142 --- /dev/null +++ b/lua/advanced_git_search/commands/preview.lua @@ -0,0 +1,109 @@ +local file = require("advanced_git_search.utils.file") +local git_utils = require("advanced_git_search.utils.git") +local cmd_utils = require("advanced_git_search.commands.utils") + +local M = {} + +--- Shows a diff of 2 commit hashes containing changes to the current file +---@param first_commit string +---@param second_commit string +---@param bufnr number +M.git_diff_file = function(first_commit, second_commit, bufnr) + local filename_on_head = file.git_relative_path(bufnr) + + local curr_name = + git_utils.file_name_on_commit(second_commit, filename_on_head) + local prev_name = + git_utils.file_name_on_commit(first_commit, filename_on_head) + + if prev_name ~= nil and curr_name ~= nil then + return cmd_utils.format_git_diff_command({ + "git", + "diff", + "--color=always", + first_commit .. ":" .. prev_name, + second_commit .. ":" .. curr_name, + }) + elseif prev_name == nil and curr_name ~= nil then + return cmd_utils.format_git_diff_command({ + "git", + "diff", + "--color=always", + first_commit, + second_commit, + "--", + file.git_relative_path_to_relative_path(curr_name), + }) + end +end + +--- Shows a diff of the passed file with a calculated base branch +--- @param relative_filename string +M.git_diff_base_branch = function(relative_filename) + return cmd_utils.format_git_diff_command({ + "git", + "diff", + "--color=always", + "--diff-filter=ACMR", + "--cached", + "--merge-base", + git_utils.base_branch(), + "--", + relative_filename, + }) +end + +--- Shows a diff of 2 commit hashes and greps on prompt string +--- @param first_commit string +--- @param second_commit string +--- @param prompt string +M.git_diff_content = function(first_commit, second_commit, prompt) + local command = cmd_utils.format_git_diff_command({ + "git", + "diff", + "--color=always", + first_commit, + second_commit, + }) + + if prompt and prompt ~= "" and prompt ~= '""' then + table.insert(command, "-G") + table.insert(command, prompt) + end + + return command +end + +--- Shows a diff of branch and the file of the bufnr on HEAD +--- @param branch string +--- @param bufnr number +M.git_diff_branch = function(branch, bufnr) + local current_hash = git_utils.branch_hash("HEAD") + + local branch_filename = git_utils.file_name_on_commit( + git_utils.branch_hash(branch), + file.git_relative_path(bufnr) + ) + + if branch_filename ~= nil then + return cmd_utils.format_git_diff_command({ + "git", + "diff", + "--color=always", + branch .. ":" .. branch_filename, + current_hash .. ":" .. file.git_relative_path(bufnr), + }) + else + return cmd_utils.format_git_diff_command({ + "git", + "diff", + "--color=always", + branch, + current_hash, + "--", + file.relative_path(bufnr), + }) + end +end + +return M diff --git a/lua/advanced_git_search/commands/utils.lua b/lua/advanced_git_search/commands/utils.lua new file mode 100644 index 0000000..9e04c48 --- /dev/null +++ b/lua/advanced_git_search/commands/utils.lua @@ -0,0 +1,65 @@ +local config = require("advanced_git_search.utils.config") +local utils = require("advanced_git_search.utils") + +local M = {} + +--- @param command table +--- @param git_flags_ix number|nil +--- @param git_diff_flags_ix number|nil +--- @return table Command with configured git diff flags +M.format_git_diff_command = function(command, git_flags_ix, git_diff_flags_ix) + git_flags_ix = git_flags_ix or 2 + git_diff_flags_ix = git_diff_flags_ix or 3 + + local git_diff_flags = config.git_diff_flags() + local git_flags = config.git_flags() + + if git_flags_ix > git_diff_flags_ix then + vim.notify( + "git_flags must be inserted before git_diff_flags", + vim.log.levels.ERROR + ) + end + + if git_diff_flags ~= nil and #git_diff_flags > 0 then + for i, flag in ipairs(git_diff_flags) do + table.insert(command, git_diff_flags_ix + i - 1, flag) + end + end + + if git_flags ~= nil and #git_flags > 0 then + for i, flag in ipairs(git_flags) do + table.insert(command, git_flags_ix + i - 1, flag) + end + end + + return command +end + +M.split_query_from_author = function(query) + local author = nil + local prompt = nil + if query ~= nil and query ~= "" then + -- starts with @ + if query:sub(1, 1) == "@" then + author = query:sub(2) + return prompt, author + end + + local split = utils.split_string(query, "@") + prompt = split[1] + + -- remove last space from prompt + if prompt:sub(-1) == " " then + prompt = prompt:sub(1, -2) + end + + author = split[2] + end + + prompt = prompt or "" + author = author or "" + return prompt, author +end + +return M diff --git a/lua/advanced_git_search/finders/init.lua b/lua/advanced_git_search/finders/init.lua deleted file mode 100644 index 34ffce9..0000000 --- a/lua/advanced_git_search/finders/init.lua +++ /dev/null @@ -1,126 +0,0 @@ -local finders = require("telescope.finders") -local file = require("advanced_git_search.utils.file") -local finder_utils = require("advanced_git_search.finders.utils") -local git_utils = require("advanced_git_search.utils.git") - -local M = {} - -M.git_branches_finder = function() - return finders.new_oneshot_job({ - "git", - "branch", - "--format=%(refname:short)", - }) -end - ---- Returns all commits that changed the visual selection in the buffer -M.git_log_location_finder = function(bufnr, start_line, end_line) - local filename = file.relative_path(bufnr) - local location = string.format("-L%d,%d:%s", start_line, end_line, filename) - - return finders.new_job(function(query) - local command = { - "git", - "log", - location, - "--no-patch", - "--format=%C(auto)%h %as %C(green)%an _ %Creset %s", - } - - local prompt, author = finder_utils.split_query_from_author(query) - - if author and author ~= "" then - table.insert(command, "--author=" .. author) - end - - if prompt and prompt ~= "" then - table.insert(command, "-s") - table.insert(command, "-i") - table.insert(command, "--grep=" .. prompt) - end - - finder_utils.set_last_prompt(prompt) - return vim.tbl_flatten(command) - end, finder_utils.git_log_entry_maker) -end - ---- Returns all commits that contains the prompt string in the commit content ---- @param opts table with optional key `bufnr` to filter on the file of the buffer -M.git_log_content_finder = function(opts) - opts = opts or {} - - return finders.new_job(function(query) - local command = { - "git", - "log", - "--format=%C(auto)%h %as %C(green)%an _ %Creset %s", - } - - local prompt, author = finder_utils.split_query_from_author(query) - - if author and author ~= "" then - table.insert(command, "--author=" .. author) - end - - if prompt and prompt ~= "" then - table.insert(command, "-G" .. prompt) - table.insert(command, "--pickaxe-all") - -- table.insert(command, [[-G']] .. prompt .. [[']]) - end - - if opts.bufnr then - table.insert(command, "--follow") - local filename = file.relative_path(opts.bufnr) - table.insert(command, filename) - end - - finder_utils.set_last_prompt(prompt) - return vim.tbl_flatten(command) - end, finder_utils.git_log_entry_maker) -end - ---- Returns all commits that changed the file of the passed buffer -M.git_log_file_finder = function(bufnr) - local filename = file.relative_path(bufnr) - return finders.new_job(function(query) - local command = { - "git", - "log", - "--format=%C(auto)%h %as %C(green)%an _ %Creset %s", - } - - local prompt, author = finder_utils.split_query_from_author(query) - - if author and author ~= "" then - table.insert(command, "--author=" .. author) - end - - if prompt and prompt ~= "" then - table.insert(command, "-s") - table.insert(command, "-i") - table.insert(command, "--grep=" .. prompt) - end - - table.insert(command, "--follow") - table.insert(command, filename) - - finder_utils.set_last_prompt(prompt) - return vim.tbl_flatten(command) - end, finder_utils.git_log_entry_maker) -end - -M.changed_files_on_current_branch_finder = function() - return finders.new_oneshot_job(vim.tbl_flatten({ - "git", - "--no-pager", - "diff", - "--name-only", - "--cached", - "--diff-filter=ACMR", - "--merge-base", - git_utils.base_branch(), - "--relative", - })) -end - -return M diff --git a/lua/advanced_git_search/fzf/finders/init.lua b/lua/advanced_git_search/fzf/finders/init.lua new file mode 100644 index 0000000..ec6a759 --- /dev/null +++ b/lua/advanced_git_search/fzf/finders/init.lua @@ -0,0 +1,66 @@ +local fzf_preview_utils = require("advanced_git_search.fzf.previewers.utils") +local command_utils = require("advanced_git_search.commands.utils") +local finder_commands = require("advanced_git_search.commands.find") +local utils = require("advanced_git_search.utils") + +local M = {} + +---@param query string +---@param bufnr number|nil +---@return string +M.git_log_content_finder = function(query, bufnr) + fzf_preview_utils.set_last_query(query) + + local prompt, author = command_utils.split_query_from_author(query) + + author = author or "" + local command = table.concat( + finder_commands.git_log_content( + string.format('"%s"', utils.escape_term(prompt)), + string.format('"%s"', author), + bufnr + ), + " " + ) + + return command +end + +M.git_log_location_finder = function(query, bufnr, s_start, s_end) + fzf_preview_utils.set_last_query(query) + + local prompt, author = command_utils.split_query_from_author(query) + + author = author or "" + local command = table.concat( + finder_commands.git_log_location( + string.format('"%s"', utils.escape_term(prompt)), + string.format('"%s"', author), + bufnr, + s_start, + s_end + ), + " " + ) + + return command +end + +M.git_log_file_finder = function(query, bufnr) + fzf_preview_utils.set_last_query(query) + + local prompt, author = command_utils.split_query_from_author(query) + + local command = table.concat( + finder_commands.git_log_file( + string.format('"%s"', utils.escape_term(prompt)), + string.format('"%s"', author), + bufnr + ), + " " + ) + + return command +end + +return M diff --git a/lua/advanced_git_search/fzf/init.lua b/lua/advanced_git_search/fzf/init.lua new file mode 100644 index 0000000..3c13f0b --- /dev/null +++ b/lua/advanced_git_search/fzf/init.lua @@ -0,0 +1,29 @@ +local M = {} +local config = require("advanced_git_search.utils.config") + +M.setup = function(opts) + config.setup(opts) + + vim.api.nvim_create_user_command( + "AdvancedGitSearch", + "lua require('advanced_git_search.fzf').show_custom_functions()", + { range = true } + ) +end + +M.search_log_content = + require("advanced_git_search.fzf.pickers").search_log_content + +M.search_log_content_file = + require("advanced_git_search.fzf.pickers").search_log_content_file + +M.diff_commit_line = require("advanced_git_search.fzf.pickers").diff_commit_line + +M.diff_commit_file = require("advanced_git_search.fzf.pickers").diff_commit_file + +M.diff_branch_file = require("advanced_git_search.fzf.pickers").diff_branch_file + +M.show_custom_functions = + require("advanced_git_search.fzf.pickers").show_custom_functions + +return M diff --git a/lua/advanced_git_search/fzf/mappings/init.lua b/lua/advanced_git_search/fzf/mappings/init.lua new file mode 100644 index 0000000..dd4d148 --- /dev/null +++ b/lua/advanced_git_search/fzf/mappings/init.lua @@ -0,0 +1,83 @@ +local M = {} +local utils = require("advanced_git_search.utils") + +local global_actions = require("advanced_git_search.actions") +local file_utils = require("advanced_git_search.utils.file") +local git_utils = require("advanced_git_search.utils.git") + +---FZF: Opens the selected commit in browser +---@return table +M.open_commit_in_brower = function() + return { + ["ctrl-o"] = function(selected, _) + local selection = selected[1] + local hash = utils.split_string(selection, " ")[1] + + global_actions.open_in_browser(hash) + end, + } +end + +---FZF: Open diff view of passed bufnr with selected commit +---@param bufnr number +---@return table +M.open_diff_buffer_with_selected_commit = function(bufnr) + return { + ["default"] = function(selected, _) + local selection = selected[1] + local commit_hash = utils.split_string(selection, " ")[1] + + global_actions.open_diff_view( + commit_hash, + file_utils.git_relative_path(bufnr) + ) + end, + } +end + +---FZF: Show entire commit in nvim +---@return table +M.show_entire_commit = function() + return { + ["ctrl-e"] = function(selected, _) + local selection = selected[1] + local commit_hash = utils.split_string(selection, " ")[1] + + global_actions.open_commit(commit_hash) + end, + } +end + +---FZF: Open diff view of passed buffer with selected branch +---@param bufnr number +---@return table +M.diff_buffer_with_branch = function(bufnr) + return { + ["default"] = function(selected, _) + local branch = selected[1] + + global_actions.open_diff_view( + branch, + git_utils.file_name_on_commit( + branch, + file_utils.git_relative_path(bufnr) + ) + ) + end, + } +end + +---FZF: Copy the selected commit hash to clipboard +---@return table +M.copy_commit_hash = function() + return { + ["ctrl-y"] = function(selected, _) + local selection = selected[1] + local commit_hash = utils.split_string(selection, " ")[1] + + global_actions.copy_to_clipboard(commit_hash) + end, + } +end + +return M diff --git a/lua/advanced_git_search/fzf/pickers/init.lua b/lua/advanced_git_search/fzf/pickers/init.lua new file mode 100644 index 0000000..b5ef2e0 --- /dev/null +++ b/lua/advanced_git_search/fzf/pickers/init.lua @@ -0,0 +1,214 @@ +local M = {} + +local fzf_previewers = require("advanced_git_search.fzf.previewers") +local fzf_finders = require("advanced_git_search.fzf.finders") +local fzf_mappings = require("advanced_git_search.fzf.mappings") +local fzf_picker_utils = require("advanced_git_search.fzf.pickers.utils") +local global_picker = require("advanced_git_search.global_picker") + +M.search_log_content = function() + local bufnr = vim.fn.bufnr() + local opts = { + prompt = "Log> ", + exec_empty_query = true, + func_async_callback = false, + fzf_opts = { + ["--preview"] = fzf_previewers.git_diff_content_previewer(), + }, + fn_transform = function(x) + return fzf_picker_utils.make_entry(x) + end, + actions = vim.tbl_extend( + "keep", + fzf_mappings.open_commit_in_brower(), + fzf_mappings.open_diff_buffer_with_selected_commit(bufnr), + fzf_mappings.show_entire_commit(), + fzf_mappings.copy_commit_hash() + ), + } + + require("fzf-lua").fzf_live(function(query) + return fzf_finders.git_log_content_finder(query, nil) + end, opts) +end + +M.search_log_content_file = function() + local bufnr = vim.fn.bufnr() + + local opts = { + prompt = "Log> ", + exec_empty_query = false, + func_async_callback = false, + fzf_opts = { + ["--preview"] = fzf_previewers.git_diff_content_previewer(), + }, + fn_transform = function(x) + return fzf_picker_utils.make_entry(x) + end, + actions = vim.tbl_extend( + "keep", + fzf_mappings.open_commit_in_brower(), + fzf_mappings.open_diff_buffer_with_selected_commit(bufnr), + fzf_mappings.show_entire_commit(), + fzf_mappings.copy_commit_hash() + ), + } + + require("fzf-lua").fzf_live(function(query) + return fzf_finders.git_log_content_finder(query, bufnr) + end, opts) +end + +M.diff_commit_line = function() + local bufnr = vim.fn.bufnr() + local s_start = vim.fn.getpos("'<")[2] + local s_end = vim.fn.getpos("'>")[2] + + if s_start == 0 or s_end == 0 then + vim.notify( + "No visual selection", + vim.log.levels.WARN, + { title = "Advanced Git Search" } + ) + return + end + + local opts = { + prompt = "Commit message> ", + exec_empty_query = true, + func_async_callback = false, + fzf_opts = { + ["--preview"] = fzf_previewers.git_diff_file_previewer(bufnr), + }, + fn_transform = function(x) + return fzf_picker_utils.make_entry(x) + end, + actions = vim.tbl_extend( + "keep", + fzf_mappings.open_commit_in_brower(), + fzf_mappings.open_diff_buffer_with_selected_commit(bufnr), + fzf_mappings.show_entire_commit(), + fzf_mappings.copy_commit_hash() + ), + } + + require("fzf-lua").fzf_live(function(query) + return fzf_finders.git_log_location_finder(query, bufnr, s_start, s_end) + end, opts) +end + +M.diff_commit_file = function() + local bufnr = vim.fn.bufnr() + + local opts = { + prompt = "Commit message> ", + exec_empty_query = true, + func_async_callback = false, + fzf_opts = { + ["--preview"] = fzf_previewers.git_diff_file_previewer(bufnr), + }, + fn_transform = function(x) + return fzf_picker_utils.make_entry(x) + end, + actions = vim.tbl_extend( + "keep", + fzf_mappings.open_commit_in_brower(), + fzf_mappings.open_diff_buffer_with_selected_commit(bufnr), + fzf_mappings.show_entire_commit(), + fzf_mappings.copy_commit_hash() + ), + } + + require("fzf-lua").fzf_live(function(query) + return fzf_finders.git_log_file_finder(query, bufnr) + end, opts) +end + +M.diff_branch_file = function() + local bufnr = vim.fn.bufnr() + + local opts = { + prompt = "Branch> ", + func_async_callback = false, + fzf_opts = { + ["--preview"] = fzf_previewers.git_diff_branch_file_previewer( + bufnr + ), + }, + actions = vim.tbl_extend( + "keep", + fzf_mappings.open_commit_in_brower(), + fzf_mappings.open_diff_buffer_with_selected_commit(bufnr), + fzf_mappings.copy_commit_hash() + ), + } + + require("fzf-lua").fzf_exec( + table.concat( + require("advanced_git_search.commands.find").git_branches(), + " " + ), + opts + ) +end + +M.changed_on_branch = function() + local opts = { + prompt = "File> ", + func_async_callback = false, + fzf_opts = { + ["--preview"] = fzf_previewers.git_diff_base_branch(), + }, + } + + require("fzf-lua").fzf_exec( + table.concat( + require("advanced_git_search.commands.find").changed_on_branch(), + " " + ), + opts + ) +end + +M.checkout_reflog = function() + local opts = { + func_async_callback = false, + fn_transform = function(x) + return fzf_picker_utils.make_reflog_entry(x) + end, + actions = { + ["default"] = function(selected) + local selection = selected[1] + local commit = string.sub(selection, 1, 7) + + require("advanced_git_search.actions").checkout_commit(commit) + end, + }, + } + + require("fzf-lua").fzf_exec( + table.concat(require("advanced_git_search.commands.find").reflog(), " "), + opts + ) +end + +--- Opens a selector for all advanced git search functions +M.show_custom_functions = function() + local keys = global_picker.keys("telescope") + + local opts = { + prompt = "AdvancedGitSearch> ", + func_async_callback = false, + actions = { + ["default"] = function(selected) + local selection = selected[1] + + global_picker.execute_git_function(selection, "fzf_lua") + end, + }, + } + + require("fzf-lua").fzf_exec(keys, opts) +end + +return M diff --git a/lua/advanced_git_search/fzf/pickers/utils.lua b/lua/advanced_git_search/fzf/pickers/utils.lua new file mode 100644 index 0000000..85d1cba --- /dev/null +++ b/lua/advanced_git_search/fzf/pickers/utils.lua @@ -0,0 +1,64 @@ +local color = require("fzf-lua").utils.ansi_codes +local utils = require("advanced_git_search.utils") + +local M = {} + +M.make_entry = function(entry) + if entry == "" or entry == nil then + return + end + -- dce3b0743 2022-09-09 author _ message + -- FIXME: will break if author contains _ + local cleaned = string.gsub(entry, "'", "") + local split = utils.split_string(cleaned, "_") + local attrs = utils.split_string(split[1]) + local hash = attrs[1] + -- local date = attrs[2] + local author = "" + for i = 3, #attrs do + author = author .. attrs[i] .. " " + end + -- join split from second element + local message = split[2] + if #split > 2 then + for i = 3, #split do + message = message .. "_" .. split[i] + end + end + + -- NOTE: make sure the first value is the commit hash + return color.magenta(hash) + .. color.cyan(" @" .. author) + .. color.yellow(message) +end + +M.make_reflog_entry = function(entry) + if entry == "" or entry == nil then + return + end + + local cleaned = string.gsub(entry, "'", "") + local split = utils.split_string(cleaned, " ") + local hash = split[1] + + local rest = split[2] + for i = 3, #split do + rest = rest .. " " .. split[i] + end + + local split_on_double = utils.split_string(rest, ":") + local description = "" + for i = 4, #split_on_double do + description = description .. split_on_double[i] + end + + local meta = "" + for i = 1, 3 do + meta = meta .. split_on_double[i] + end + + -- NOTE: make sure the first value is the commit hash + return color.magenta(hash) .. " " .. meta .. "" .. color.yellow(description) +end + +return M diff --git a/lua/advanced_git_search/fzf/previewers/init.lua b/lua/advanced_git_search/fzf/previewers/init.lua new file mode 100644 index 0000000..6cc4445 --- /dev/null +++ b/lua/advanced_git_search/fzf/previewers/init.lua @@ -0,0 +1,75 @@ +local command_utils = require("advanced_git_search.commands.utils") +local fzf_lua = require("fzf-lua") +local fzf_preview_utils = require("advanced_git_search.fzf.previewers.utils") +local utils = require("advanced_git_search.utils") +local preview_commands = require("advanced_git_search.commands.preview") +local git_utils = require("advanced_git_search.utils.git") + +local M = {} + +M.git_diff_content_previewer = function() + return fzf_lua.shell.preview_action_cmd(function(items) + local selection = items[1] + local hash = string.sub(selection, 1, 7) + + local prev_commit = git_utils.previous_commit_hash(hash) + local prompt, _ = command_utils.split_query_from_author( + fzf_preview_utils.get_last_query() + ) + + local preview_command = table.concat( + preview_commands.git_diff_content( + prev_commit, + hash, + string.format('"%s"', utils.escape_term(prompt)) + ), + " " + ) + + if prompt and prompt ~= "" and prompt ~= '""' then + preview_command = preview_command + .. string.format( + " | GREP_COLOR='3;30;105' grep -A 999999 -B 999999 --color=always '%s'", + prompt + ) + end + + return preview_command + end) +end + +M.git_diff_file_previewer = function(bufnr) + return fzf_lua.shell.preview_action_cmd(function(items) + local selection = items[1] + local commit_hash = string.sub(selection, 1, 7) + local prev_commit = git_utils.previous_commit_hash(commit_hash) + + return table.concat( + preview_commands.git_diff_file(prev_commit, commit_hash, bufnr), + " " + ) + end) +end + +M.git_diff_branch_file_previewer = function(bufnr) + return fzf_lua.shell.preview_action_cmd(function(items) + local branch = items[1] + + return table.concat( + preview_commands.git_diff_branch(branch, bufnr), + " " + ) + end) +end + +M.git_diff_base_branch = function() + return fzf_lua.shell.preview_action_cmd(function(items) + local filename = items[1] + + return table.concat( + preview_commands.git_diff_base_branch(filename), + " " + ) + end) +end +return M diff --git a/lua/advanced_git_search/fzf/previewers/utils.lua b/lua/advanced_git_search/fzf/previewers/utils.lua new file mode 100644 index 0000000..c2ed32d --- /dev/null +++ b/lua/advanced_git_search/fzf/previewers/utils.lua @@ -0,0 +1,12 @@ +local M = {} +local last_query = "" + +M.set_last_query = function(query) + last_query = query +end + +M.get_last_query = function() + return last_query +end + +return M diff --git a/lua/advanced_git_search/global_picker.lua b/lua/advanced_git_search/global_picker.lua new file mode 100644 index 0000000..696fd83 --- /dev/null +++ b/lua/advanced_git_search/global_picker.lua @@ -0,0 +1,121 @@ +local M = {} + +local config = require("advanced_git_search.utils.config") + +---@param finder_plugin "telescope"|"fzf_lua" +---@return table +local custom_git_functions = function(finder_plugin) + local pickers_table = {} + if finder_plugin == "telescope" then + pickers_table = require("advanced_git_search.telescope.pickers") + elseif finder_plugin == "fzf_lua" then + pickers_table = require("advanced_git_search.fzf.pickers") + end + + return { + { + value = "Search in repo log content", + func = pickers_table.search_log_content, + }, + { + value = "Search in file log content", + func = pickers_table.search_log_content_file, + }, + { + value = "Diff current file with commit", + func = pickers_table.diff_commit_file, + }, + { + value = "Diff current file with selected line history", + func = pickers_table.diff_commit_line, + }, + { + value = "Diff file with branch", + func = pickers_table.diff_branch_file, + }, + { + value = "Changed on current branch (experimental)", + func = pickers_table.changed_on_branch, + }, + { + value = "Checkout from reflog", + func = pickers_table.checkout_reflog, + }, + } +end + +---@param finder_plugin "telescope"|"fzf_lua" +---@return table +local builtin_git_functions = function(finder_plugin) + local builtin_functions = {} + if finder_plugin == "telescope" then + builtin_functions = require("telescope.builtin") + elseif finder_plugin == "fzf_lua" then + builtin_functions = require("fzf-lua") + end + + return { + { + value = "Git commits [builtin]", + func = builtin_functions.git_commits, + }, + { + value = "Git branches [builtin]", + func = builtin_functions.git_branches, + }, + { + value = "Git status [builtin]", + func = builtin_functions.git_status, + }, + { + value = "Git stash [builtin]", + func = builtin_functions.git_stash, + }, + } +end + +local function map_item(git_functions_table, f) + local t = {} + for k, v in pairs(git_functions_table) do + t[k] = f(v) + end + return t +end + +---@param finder_plugin "telescope"|"fzf_lua" +---@return table +local git_functions_table = function(finder_plugin) + local t = {} + for _, v in pairs(custom_git_functions(finder_plugin)) do + t[#t + 1] = v + end + + if config.show_builtin_git_pickers() then + for _, v in pairs(builtin_git_functions(finder_plugin)) do + t[#t + 1] = v + end + end + + return t +end + +---@param value any +---@param finder_plugin "telescope"|"fzf_lua" +M.execute_git_function = function(value, finder_plugin) + for _, v in pairs(git_functions_table(finder_plugin)) do + if v["value"] == value then + v["func"]() + return + end + end +end + +---@param finder_plugin "telescope"|"fzf_lua" +---@return table +M.keys = function(finder_plugin) + return map_item(git_functions_table(finder_plugin), function(v) + return v["value"] + end) +end + +return M diff --git a/lua/advanced_git_search/init.lua b/lua/advanced_git_search/init.lua deleted file mode 100644 index 4eb9578..0000000 --- a/lua/advanced_git_search/init.lua +++ /dev/null @@ -1,279 +0,0 @@ -local actions = require("telescope.actions") -local action_state = require("telescope.actions.state") -local git_utils = require("advanced_git_search.utils.git") -local config = require("advanced_git_search.utils.config") - -local pickers = require("telescope.pickers") -local sorters = require("telescope.sorters") -local finders = require("telescope.finders") -local ags_finders = require("advanced_git_search.finders") -local ags_previewers = require("advanced_git_search.previewers") -local ags_mappings = require("advanced_git_search.mappings") - -local M = {} - ---- Opens a Telescope window with all files changed on the current branch ---- Only committed changes will be displayed -M.changed_on_branch = function() - pickers - .new({ - results_title = "Modified " - .. git_utils.base_branch() - .. " -> " - .. git_utils.current_branch(), - sorter = sorters.get_fuzzy_file(), - finder = ags_finders.changed_files_on_current_branch_finder(), - previewer = ags_previewers.changed_files_on_current_branch_previewer(), - }) - :find() -end - ---- Opens a Telescope window with a list of local branches -M.diff_branch_file = function() - -- local previewers = require('telescope.previewers') - local current_branch = git_utils.current_branch() - local bufnr = vim.fn.bufnr() - - pickers - .new({ - results_title = "Local branches :: *" .. current_branch, - prompt_title = "Branch name", - finder = ags_finders.git_branches_finder(), - sorter = sorters.get_fuzzy_file(), - previewer = ags_previewers.git_diff_branch_file_previewer(bufnr), - attach_mappings = function(_, map) - ags_mappings.open_diff_view_current_file_selected_branch(map) - return true - end, - }) - :find() -end - ---- Opens a Telescope window with a list of previous commit logs ---- with respect to selected lines -M.diff_commit_line = function() - local bufnr = vim.fn.bufnr() - local s_start = vim.fn.getpos("'<")[2] - local s_end = vim.fn.getpos("'>")[2] - - -- git log -L741,751:'app/models/patients/patient.rb'\ - -- --format='%C(auto)%h \t %as \t %C(green)%an _ %Creset %s' - pickers - .new({ - results_title = "Commits that affected the selected lines", - prompt_title = "Commit message", - finder = ags_finders.git_log_location_finder(bufnr, s_start, s_end), - previewer = ags_previewers.git_diff_commit_file_previewer(bufnr), - sorter = sorters.highlighter_only(), - attach_mappings = function(_, map) - ags_mappings.open_diff_view_current_file_selected_commit(map) - ags_mappings.open_selected_commit_in_browser(map) - ags_mappings.copy_commit_hash_to_clipboard(map) - ags_mappings.show_entire_commit(map) - return true - end, - }) - :find() -end - ---- Opens a Telescope window with a list of previous commits. ---- Query is used to filter the results based on the ---- content of the commit (added or removed text). -M.search_log_content = function() - -- git log -L741,751:'app/models/patients/patient.rb' \ - -- --format='%C(auto)%h \t %as \t %C(green)%an _ %Creset %s' - pickers - .new({ - results_title = "Commits", - prompt_title = "Git log content (added, removed or updated text)", - finder = ags_finders.git_log_content_finder({}), - previewer = ags_previewers.git_diff_content_previewer(), - attach_mappings = function(_, map) - ags_mappings.open_diff_view_current_file_selected_commit(map) - ags_mappings.open_selected_commit_in_browser(map) - ags_mappings.copy_commit_hash_to_clipboard(map) - ags_mappings.show_entire_commit(map) - return true - end, - }) - :find() -end - ---- Same as `search_log_content` but with respect to the current file -M.search_log_content_file = function() - -- local file_name = vim.fn.expand("%") - -- local relative_file_name = vim.fn.expand("%:~:.") - - -- git log -L741,751:'app/models/patients/patient.rb' \ - -- --format='%C(auto)%h \t %as \t %C(green)%an _ %Creset %s' - pickers - .new({ - results_title = "Commits", - prompt_title = "Git log content (added, removed or updated text in this file)", - finder = ags_finders.git_log_content_finder({ - bufnr = vim.fn.bufnr(), - }), - previewer = ags_previewers.git_diff_content_previewer(), - -- sorter = sorters.highlighter_only(), - attach_mappings = function(_, map) - ags_mappings.open_diff_view_current_file_selected_commit(map) - ags_mappings.open_selected_commit_in_browser(map) - ags_mappings.copy_commit_hash_to_clipboard(map) - ags_mappings.show_entire_commit(map) - - return true - end, - }) - :find() -end - --- Opens a Telescope window with a list of git commits which changed the current file (renames included) -M.diff_commit_file = function() - local bufnr = vim.fn.bufnr() - pickers - .new({ - results_title = "Commits that affected this file (renamed files included)", - prompt_title = "Commit message", - finder = ags_finders.git_log_file_finder(bufnr), - previewer = ags_previewers.git_diff_commit_file_previewer(bufnr), - sorter = sorters.highlighter_only(), - attach_mappings = function(_, map) - ags_mappings.open_diff_view_current_file_selected_commit(map) - ags_mappings.show_entire_commit(map) - ags_mappings.open_selected_commit_in_browser(map) - ags_mappings.copy_commit_hash_to_clipboard(map) - - return true - end, - }) - :find() -end - ---- Opens a Telescope window with all reflog entries -M.checkout_reflog = function() - pickers - .new({ - results_title = "Git Reflog, to checkout", - finder = finders.new_oneshot_job({ "git", "reflog", "--date=iso" }), - sorter = sorters.get_fuzzy_file(), - attach_mappings = function(_, map) - ags_mappings.checkout_reflog_entry(map) - return true - end, - }) - :find() -end - -local custom_git_functions = { - { - value = "Search in repo log content", - func = M.search_log_content, - }, - { - value = "Search in file log content", - func = M.search_log_content_file, - }, - { - value = "Diff current file with commit", - func = M.diff_commit_file, - }, - { - value = "Diff current file with selected line history", - func = M.diff_commit_line, - }, - { - value = "Diff file with branch", - func = M.diff_branch_file, - }, - { - value = "Changed on current branch (experimental)", - func = M.changed_on_branch, - }, - { - value = "Checkout from reflog", - func = M.checkout_reflog, - }, -} - -local builtin_git_functions = { - { - value = "Git commits [telescope.builtin]", - func = require("telescope.builtin").git_commits, - }, - { - value = "Git branches [telescope.builtin]", - func = require("telescope.builtin").git_branches, - }, - { - value = "Git status [telescope.builtin]", - func = require("telescope.builtin").git_status, - }, - { - value = "Git stash [telescope.builtin]", - func = require("telescope.builtin").git_stash, - }, -} - -local function map_item(git_functions_table, f) - local t = {} - for k, v in pairs(git_functions_table) do - t[k] = f(v) - end - return t -end - -local git_functions_table = function() - local t = {} - for _, v in pairs(custom_git_functions) do - t[#t + 1] = v - end - - if config.show_builtin_git_pickers() then - for _, v in pairs(builtin_git_functions) do - t[#t + 1] = v - end - end - - return t -end - -local function execute_git_function(value) - for _, v in pairs(git_functions_table()) do - if v["value"] == value then - v["func"]() - return - end - end -end - ---- Opens all a selector for all advanced git search functions -M.show_custom_functions = function() - local keys = map_item(git_functions_table(), function(item) - return item["value"] - end) - - pickers - .new({ - prompt_title = "Git actions", - finder = finders.new_table(keys), - sorter = sorters.get_fuzzy_file(), - attach_mappings = function(_, map) - ags_mappings.omnimap(map, "", function(prompt_bufnr) - actions.close(prompt_bufnr) - local selection = action_state.get_selected_entry() - execute_git_function(selection.value) - end) - - return true - end, - }) - :find() -end - -vim.api.nvim_create_user_command( - "AdvancedGitSearch", - "lua require('telescope').extensions.advanced_git_search.show_custom_functions()", - { range = true } -) - -return M diff --git a/lua/advanced_git_search/previewers/init.lua b/lua/advanced_git_search/previewers/init.lua deleted file mode 100644 index 4a55cf5..0000000 --- a/lua/advanced_git_search/previewers/init.lua +++ /dev/null @@ -1,121 +0,0 @@ -local previewers = require("telescope.previewers") -local file = require("advanced_git_search.utils.file") -local git_utils = require("advanced_git_search.utils.git") - -local M = {} - ---- Shows a diff of the commit in the finder entry, filtered on the file of the current buffer -M.git_diff_commit_file_previewer = function(bufnr) - local filename_on_head = file.git_relative_path(bufnr) - return previewers.new_termopen_previewer({ - title = "Changes on selected commit for: " .. file.file_name(bufnr), - get_command = function(entry) - local commit_hash = entry.opts.commit_hash - - local prev_commit = git_utils.previous_commit_hash(commit_hash) - - local curr_name = - git_utils.file_name_on_commit(commit_hash, filename_on_head) - local prev_name = - git_utils.file_name_on_commit(prev_commit, filename_on_head) - - if prev_name ~= nil then - return git_utils.git_diff_command({ - "git", - "diff", - prev_commit .. ":" .. prev_name, - commit_hash .. ":" .. curr_name, - }) - else - return git_utils.git_diff_command({ - "git", - "diff", - prev_commit, - commit_hash, - "--", - file.git_relative_path_to_relative_path(curr_name), - }) - end - end, - }) -end - ---- Shows a diff of the commit in the finder entry, filtered on the prompt string for the commit content -M.git_diff_content_previewer = function() - return previewers.new_termopen_previewer({ - title = "Changes including prompt string", - get_command = function(entry) - local commit_hash = entry.opts.commit_hash - local prompt = entry.opts.prompt - local command = git_utils.git_diff_command({ - "git", - "diff", - string.format("%s~", commit_hash), - commit_hash, - }) - - if prompt and prompt ~= "" then - table.insert(command, "-G") - table.insert(command, prompt) - end - - return command - end, - }) -end - ---- Shows a diff of the file in the finder entry and the fork point of the current branch -M.changed_files_on_current_branch_previewer = function() - return previewers.new_termopen_previewer({ - title = "Diff of selected file and fork point", - get_command = function(entry) - return git_utils.git_diff_command({ - "git", - "diff", - "--diff-filter=ACMR", - "--cached", - "--merge-base", - git_utils.base_branch(), - "--", - entry.value, - }) - end, - }) -end - ---- Shows a diff of the branch in the finder entry relative to the passed filename -M.git_diff_branch_file_previewer = function(bufnr) - local filename = file.file_name(bufnr) - return previewers.new_termopen_previewer({ - title = "Diff of current buffer and selected branch for: " .. filename, - get_command = function(entry) - local branch = entry.value - local current_hash = git_utils.branch_hash("HEAD") - - local branch_filename = git_utils.file_name_on_commit( - git_utils.branch_hash(branch), - file.git_relative_path(bufnr) - ) - - if branch_filename ~= nil then - return git_utils.git_diff_command({ - "git", - "diff", - branch .. ":" .. branch_filename, - current_hash .. ":" .. file.git_relative_path(bufnr), - }) - else - return git_utils.git_diff_command({ - "git", - "diff", - branch, - current_hash, - "--", - file.relative_path(bufnr), - }) - end - end, - }) -end - -return M diff --git a/lua/advanced_git_search/telescope/finders/init.lua b/lua/advanced_git_search/telescope/finders/init.lua new file mode 100644 index 0000000..592b025 --- /dev/null +++ b/lua/advanced_git_search/telescope/finders/init.lua @@ -0,0 +1,62 @@ +local finders = require("telescope.finders") +local telescope_finder_utils = + require("advanced_git_search.telescope.finders.utils") +local command_utils = require("advanced_git_search.commands.utils") +local finder_commands = require("advanced_git_search.commands.find") +local utils = require("advanced_git_search.utils") + +local M = {} + +M.git_branches_finder = function() + return finders.new_oneshot_job(finder_commands.git_branches()) +end + +--- Returns all commits that changed the visual selection in the buffer +M.git_log_location_finder = function(bufnr, start_line, end_line) + return finders.new_job(function(query) + local prompt, author = command_utils.split_query_from_author(query) + + telescope_finder_utils.set_last_prompt(prompt) + + return finder_commands.git_log_location( + prompt, + author, + bufnr, + start_line, + end_line + ) + end, telescope_finder_utils.git_log_entry_maker) +end + +--- Returns all commits that contains the prompt string in the commit content +--- @param opts table with optional key `bufnr` to filter on the file of the buffer +M.git_log_content_finder = function(opts) + opts = opts or {} + + return finders.new_job(function(query) + local prompt, author = command_utils.split_query_from_author(query) + + telescope_finder_utils.set_last_prompt(utils.escape_term(prompt)) + return finder_commands.git_log_content( + utils.escape_term(prompt), + author, + opts.bufnr + ) + end, telescope_finder_utils.git_log_entry_maker) +end + +--- Returns all commits that changed the file of the passed buffer +M.git_log_file_finder = function(bufnr) + return finders.new_job(function(query) + local prompt, author = command_utils.split_query_from_author(query) + + telescope_finder_utils.set_last_prompt(prompt) + return finder_commands.git_log_file(prompt, author, bufnr) + end, telescope_finder_utils.git_log_entry_maker) +end + +M.changed_files_on_current_branch_finder = function() + return finders.new_oneshot_job(finder_commands.changed_on_branch()) +end + +return M diff --git a/lua/advanced_git_search/finders/utils.lua b/lua/advanced_git_search/telescope/finders/utils.lua similarity index 54% rename from lua/advanced_git_search/finders/utils.lua rename to lua/advanced_git_search/telescope/finders/utils.lua index 039d7a3..816e305 100644 --- a/lua/advanced_git_search/finders/utils.lua +++ b/lua/advanced_git_search/telescope/finders/utils.lua @@ -1,55 +1,54 @@ local utils = require("advanced_git_search.utils") +local entry_display = require("telescope.pickers.entry_display") local M = {} local last_prompt = nil -M.split_query_from_author = function(query) - local author = nil - local prompt = nil - if query ~= nil and query ~= "" then - -- starts with @ - if query:sub(1, 1) == "@" then - author = query:sub(2) - return prompt, author - end - - local split = utils.split_string(query, "@") - prompt = split[1] - - -- remove last space from prompt - if prompt:sub(-1) == " " then - prompt = prompt:sub(1, -2) - end - - author = split[2] - end - - return prompt, author -end - --- Parse "--format=%C(auto)%h %as %C(green)%an _ %Creset %s" to table --- with opts: commit_hash, date, author, message, prompt --- @param entry string M.git_log_entry_maker = function(entry) -- dce3b0743 2022-09-09 author _ message -- FIXME: will break if author contains _ - local split = utils.split_string(entry, "_") + local cleaned = string.gsub(entry, "'", "") + local split = utils.split_string(cleaned, "_") local attrs = utils.split_string(split[1]) local hash = attrs[1] local date = attrs[2] local author = attrs[3] + for i = 4, #attrs do + author = author .. " " .. attrs[i] + end + -- join split from second element local message = split[2] if #split > 2 then for i = 3, #split do - message = message .. "_" .. split[i] + message = message .. " " .. split[i] end end + local displayer = entry_display.create({ + separator = " ", + items = { + { width = 7 }, + { width = 7 }, + { remaining = true }, + }, + }) + + local make_display = function(display_entry) + return displayer({ + { display_entry.opts.commit_hash, "TelescopeResultsIdentifier" }, + { display_entry.opts.author, "TelescopeResultsVariable" }, + { display_entry.opts.message, "TelescopeResultsConstant" }, + }) + end + return { value = entry, - display = date .. " by " .. author .. " --" .. message, - -- display = hash .. " @ " .. date .. " by " .. author .. " --" .. message, + -- display = date .. " by " .. author .. " --" .. message, + display = make_display, ordinal = author .. " " .. message, preview_title = hash .. " -- " .. message, opts = { diff --git a/lua/advanced_git_search/mappings/init.lua b/lua/advanced_git_search/telescope/mappings/init.lua similarity index 78% rename from lua/advanced_git_search/mappings/init.lua rename to lua/advanced_git_search/telescope/mappings/init.lua index e35c09a..4ec6286 100644 --- a/lua/advanced_git_search/mappings/init.lua +++ b/lua/advanced_git_search/telescope/mappings/init.lua @@ -1,9 +1,10 @@ local actions = require("telescope.actions") -local ags_actions = require("advanced_git_search.actions") local action_state = require("telescope.actions.state") -local file = require("advanced_git_search.utils.file") + +local global_actions = require("advanced_git_search.actions") + +local file_utils = require("advanced_git_search.utils.file") local git_utils = require("advanced_git_search.utils.git") -local config = require("advanced_git_search.utils.config") -- Map a key to both insert and normal modes local function omnimap(map_func, key, handler) @@ -35,7 +36,7 @@ local diff_current_buffer_with_commit = function(prompt_bufnr) local selection = action_state.get_selected_entry() local commit_hash = selection.opts.commit_hash - ags_actions.open_diff_view(commit_hash) + global_actions.open_diff_view(commit_hash) end --- Open diff view of commmit (from entry) with @@ -45,12 +46,12 @@ end ------------------------------------------------------------------------------- local diff_current_buffer_with_branch = function(prompt_bufnr) - local filename = file.git_relative_path(vim.fn.bufnr()) + local filename = file_utils.git_relative_path(vim.fn.bufnr()) actions.close(prompt_bufnr) local selection = action_state.get_selected_entry() local branch = selection.value - ags_actions.open_diff_view( + global_actions.open_diff_view( branch, git_utils.file_name_on_commit(branch, filename) ) @@ -67,14 +68,7 @@ local open_entire_commit = function(prompt_bufnr) local selection = action_state.get_selected_entry() local commit_hash = selection.opts.commit_hash - local diff_plugin = config.diff_plugin() - if diff_plugin == "diffview" then - vim.api.nvim_command( - ":DiffviewOpen -uno " .. commit_hash .. "~.." .. commit_hash - ) - elseif diff_plugin == "fugitive" then - vim.api.nvim_command(":Gedit " .. commit_hash) - end + global_actions.open_commit(commit_hash) end --- open entire commit diff with @@ -86,14 +80,8 @@ end local copy_commit_hash = function(_) local selection = action_state.get_selected_entry() local commit_hash = selection.opts.commit_hash - vim.notify( - "Copied commit hash " .. commit_hash .. " to clipboard", - vim.log.levels.INFO, - { title = "Advanced Git Search" } - ) - vim.fn.setreg("+", commit_hash) - vim.fn.setreg("*", commit_hash) + global_actions.copy_to_clipboard(commit_hash) end --- copy commit hash to clipboard with @@ -113,7 +101,8 @@ local checkout = function(prompt_bufnr) splitted_reflog_entry[count] = i count = count + 1 end - vim.api.nvim_command(":!git checkout " .. splitted_reflog_entry[1]) + + global_actions.checkout_commit(splitted_reflog_entry[1]) end --- Checkout the selected reflog entry with diff --git a/lua/advanced_git_search/telescope/pickers/init.lua b/lua/advanced_git_search/telescope/pickers/init.lua new file mode 100644 index 0000000..e7fd64a --- /dev/null +++ b/lua/advanced_git_search/telescope/pickers/init.lua @@ -0,0 +1,228 @@ +local actions = require("telescope.actions") +local action_state = require("telescope.actions.state") +local git_utils = require("advanced_git_search.utils.git") +local global_picker = require("advanced_git_search.global_picker") + +local pickers = require("telescope.pickers") +local sorters = require("telescope.sorters") +local finders = require("telescope.finders") + +local telescope_ags_finders = require("advanced_git_search.telescope.finders") +local telescope_ags_previewers = + require("advanced_git_search.telescope.previewers") +local telescope_ags_mappings = require("advanced_git_search.telescope.mappings") + +local M = {} + +--- Opens a Telescope window with all files changed on the current branch +--- Only committed changes will be displayed +M.changed_on_branch = function() + pickers + .new({ + results_title = "Modified " + .. git_utils.base_branch() + .. " -> " + .. git_utils.current_branch(), + sorter = sorters.get_fuzzy_file(), + finder = telescope_ags_finders.changed_files_on_current_branch_finder(), + previewer = telescope_ags_previewers.changed_files_on_current_branch_previewer(), + }) + :find() +end + +--- Opens a Telescope window with a list of local branches +M.diff_branch_file = function() + -- local previewers = require('telescope.previewers') + local current_branch = git_utils.current_branch() + local bufnr = vim.fn.bufnr() + + pickers + .new({ + results_title = "Local branches :: *" .. current_branch, + prompt_title = "Branch name", + finder = telescope_ags_finders.git_branches_finder(), + sorter = sorters.get_fuzzy_file(), + previewer = telescope_ags_previewers.git_diff_branch_file_previewer( + bufnr + ), + attach_mappings = function(_, map) + telescope_ags_mappings.open_diff_view_current_file_selected_branch( + map + ) + return true + end, + }) + :find() +end + +--- Opens a Telescope window with a list of previous commit logs +--- with respect to selected lines +M.diff_commit_line = function() + local bufnr = vim.fn.bufnr() + local s_start = vim.fn.getpos("'<")[2] + local s_end = vim.fn.getpos("'>")[2] + + if s_start == 0 or s_end == 0 then + vim.notify( + "No visual selection", + vim.log.levels.WARN, + { title = "Advanced Git Search" } + ) + return + end + + -- git log -L741,751:'app/models/patients/patient.rb'\ + -- --format='%C(auto)%h \t %as \t %C(green)%an _ %Creset %s' + pickers + .new({ + results_title = "Commits that affected the selected lines", + prompt_title = "Commit message", + finder = telescope_ags_finders.git_log_location_finder( + bufnr, + s_start, + s_end + ), + previewer = telescope_ags_previewers.git_diff_commit_file_previewer( + bufnr + ), + sorter = sorters.highlighter_only(), + attach_mappings = function(_, map) + telescope_ags_mappings.open_diff_view_current_file_selected_commit( + map + ) + telescope_ags_mappings.open_selected_commit_in_browser(map) + telescope_ags_mappings.copy_commit_hash_to_clipboard(map) + telescope_ags_mappings.show_entire_commit(map) + return true + end, + }) + :find() +end + +--- Opens a Telescope window with a list of previous commits. +--- Query is used to filter the results based on the +--- content of the commit (added or removed text). +M.search_log_content = function() + -- git log -L741,751:'app/models/patients/patient.rb' \ + -- --format='%C(auto)%h \t %as \t %C(green)%an _ %Creset %s' + pickers + .new({ + results_title = "Commits", + prompt_title = "Git log content (added, removed or updated text)", + finder = telescope_ags_finders.git_log_content_finder({}), + previewer = telescope_ags_previewers.git_diff_content_previewer(), + attach_mappings = function(_, map) + telescope_ags_mappings.open_diff_view_current_file_selected_commit( + map + ) + telescope_ags_mappings.open_selected_commit_in_browser(map) + telescope_ags_mappings.copy_commit_hash_to_clipboard(map) + telescope_ags_mappings.show_entire_commit(map) + return true + end, + }) + :find() +end + +--- Same as `search_log_content` but with respect to the current file +M.search_log_content_file = function() + -- local file_name = vim.fn.expand("%") + -- local relative_file_name = vim.fn.expand("%:~:.") + + -- git log -L741,751:'app/models/patients/patient.rb' \ + -- --format='%C(auto)%h \t %as \t %C(green)%an _ %Creset %s' + pickers + .new({ + results_title = "Commits", + prompt_title = "Git log content (added, removed or updated text in this file)", + finder = telescope_ags_finders.git_log_content_finder({ + bufnr = vim.fn.bufnr(), + }), + previewer = telescope_ags_previewers.git_diff_content_previewer(), + attach_mappings = function(_, map) + telescope_ags_mappings.open_diff_view_current_file_selected_commit( + map + ) + telescope_ags_mappings.open_selected_commit_in_browser(map) + telescope_ags_mappings.copy_commit_hash_to_clipboard(map) + telescope_ags_mappings.show_entire_commit(map) + + return true + end, + }) + :find() +end + +-- Opens a Telescope window with a list of git commits which changed the current file (renames included) +M.diff_commit_file = function() + local bufnr = vim.fn.bufnr() + pickers + .new({ + results_title = "Commits that affected this file (renamed files included)", + prompt_title = "Commit message", + finder = telescope_ags_finders.git_log_file_finder(bufnr), + previewer = telescope_ags_previewers.git_diff_commit_file_previewer( + bufnr + ), + sorter = sorters.highlighter_only(), + attach_mappings = function(_, map) + telescope_ags_mappings.open_diff_view_current_file_selected_commit( + map + ) + telescope_ags_mappings.show_entire_commit(map) + telescope_ags_mappings.open_selected_commit_in_browser(map) + telescope_ags_mappings.copy_commit_hash_to_clipboard(map) + + return true + end, + }) + :find() +end + +--- Opens a Telescope window with all reflog entries +M.checkout_reflog = function() + pickers + .new({ + results_title = "Git Reflog, to checkout", + finder = finders.new_oneshot_job( + require("advanced_git_search.commands.find").reflog() + ), + sorter = sorters.get_fuzzy_file(), + attach_mappings = function(_, map) + telescope_ags_mappings.checkout_reflog_entry(map) + return true + end, + }) + :find() +end + +--- Opens a selector for all advanced git search functions +M.show_custom_functions = function() + local keys = global_picker.keys("telescope") + + pickers + .new({ + prompt_title = "Git actions", + finder = finders.new_table(keys), + sorter = sorters.get_fuzzy_file(), + attach_mappings = function(_, map) + telescope_ags_mappings.omnimap( + map, + "", + function(prompt_bufnr) + actions.close(prompt_bufnr) + local selection = action_state.get_selected_entry() + global_picker.execute_git_function( + selection.value, + "telescope" + ) + end + ) + + return true + end, + }) + :find() +end + +return M diff --git a/lua/advanced_git_search/telescope/previewers/init.lua b/lua/advanced_git_search/telescope/previewers/init.lua new file mode 100644 index 0000000..4c6a51d --- /dev/null +++ b/lua/advanced_git_search/telescope/previewers/init.lua @@ -0,0 +1,64 @@ +local previewers = require("telescope.previewers") +local file_utils = require("advanced_git_search.utils.file") +local git_utils = require("advanced_git_search.utils.git") +local preview_commands = require("advanced_git_search.commands.preview") + +local M = {} + +--- Shows a diff of the commit in the finder entry, filtered on the file of the current buffer +M.git_diff_commit_file_previewer = function(bufnr) + return previewers.new_termopen_previewer({ + title = "Changes on selected commit for: " + .. file_utils.file_name(bufnr), + get_command = function(entry) + local commit_hash = entry.opts.commit_hash + local prev_commit = git_utils.previous_commit_hash(commit_hash) + return preview_commands.git_diff_file( + prev_commit, + commit_hash, + bufnr + ) + end, + }) +end + +--- Shows a diff of the commit in the finder entry, filtered on the prompt string for the commit content +M.git_diff_content_previewer = function() + return previewers.new_termopen_previewer({ + title = "Changes including prompt string", + get_command = function(entry) + local commit_hash = entry.opts.commit_hash + local prompt = entry.opts.prompt + local prev_commit = git_utils.previous_commit_hash(commit_hash) + return preview_commands.git_diff_content( + prev_commit, + commit_hash, + prompt + ) + end, + }) +end + +--- Shows a diff of the file in the finder entry and the fork point of the current branch +M.changed_files_on_current_branch_previewer = function() + return previewers.new_termopen_previewer({ + title = "Diff of selected file and fork point", + get_command = function(entry) + return preview_commands.git_diff_base_branch(entry.value) + end, + }) +end + +--- Shows a diff of the branch in the finder entry relative to the passed filename +M.git_diff_branch_file_previewer = function(bufnr) + local filename = file_utils.file_name(bufnr) + return previewers.new_termopen_previewer({ + title = "Diff of current buffer and selected branch for: " .. filename, + get_command = function(entry) + local branch = entry.value + return preview_commands.git_diff_branch(branch, bufnr) + end, + }) +end + +return M diff --git a/lua/advanced_git_search/utils/git.lua b/lua/advanced_git_search/utils/git.lua index 980e165..ad306e3 100644 --- a/lua/advanced_git_search/utils/git.lua +++ b/lua/advanced_git_search/utils/git.lua @@ -1,6 +1,5 @@ local utils = require("advanced_git_search.utils") local file = require("advanced_git_search.utils.file") -local config = require("advanced_git_search.utils.config") local command_util = require("advanced_git_search.utils.command") local M = {} @@ -22,41 +21,6 @@ local all_commit_hashes_touching_file = function(git_relative_file_path) return utils.split_string(output, "\n") end ---- @param command table ---- @param git_flags_ix number|nil ---- @param git_diff_flags_ix number|nil ---- @return table Command with configured git diff flags -local git_diff_command = function(command, git_flags_ix, git_diff_flags_ix) - git_flags_ix = git_flags_ix or 2 - git_diff_flags_ix = git_diff_flags_ix or 3 - - local git_diff_flags = config.git_diff_flags() - local git_flags = config.git_flags() - - if git_flags_ix > git_diff_flags_ix then - vim.notify( - "git_flags must be inserted before git_diff_flags", - vim.log.levels.ERROR - ) - end - - if git_diff_flags ~= nil and #git_diff_flags > 0 then - for i, flag in ipairs(git_diff_flags) do - table.insert(command, git_diff_flags_ix + i - 1, flag) - end - end - - if git_flags ~= nil and #git_flags > 0 then - for i, flag in ipairs(git_flags) do - table.insert(command, git_flags_ix + i - 1, flag) - end - end - - return command -end - -M.git_diff_command = git_diff_command - M.previous_commit_hash = function(commit_hash) local command = "git rev-parse " .. commit_hash .. "~" @@ -137,7 +101,11 @@ local file_name_on_commit = function(commit_hash, git_relative_file_path) local hash = all_hashes[i] -- search the hash in touched_hashes for _, touched_hash in ipairs(touched_hashes) do - if string.sub(touched_hash, 1, 7) == string.sub(hash, 1, 7) then + if + touched_hash ~= nil + and hash ~= nil + and string.sub(touched_hash, 1, 7) == string.sub(hash, 1, 7) + then last_touched_hash = touched_hash break end diff --git a/lua/advanced_git_search/utils/init.lua b/lua/advanced_git_search/utils/init.lua index be343c2..c2bb999 100644 --- a/lua/advanced_git_search/utils/init.lua +++ b/lua/advanced_git_search/utils/init.lua @@ -28,6 +28,7 @@ M.split_string = function(inputstr, sep) end M.escape_chars = function(x) + x = x or "" return ( x:gsub("%%", "%%%%") :gsub("^%^", "%%^") @@ -44,4 +45,22 @@ M.escape_chars = function(x) ) end +M.escape_term = function(x) + x = x or "" + return ( + x:gsub("%%", "\\%%") + :gsub("^%^", "\\%^") + :gsub("%$$", "\\%$") + :gsub("%(", "\\%(") + :gsub("%)", "\\%)") + :gsub("%.", "\\%.") + :gsub("%[", "\\%[") + :gsub("%]", "\\%]") + :gsub("%*", "\\%*") + :gsub("%+", "\\%+") + :gsub("%-", "\\%-") + :gsub("%?", "\\%?") + ) +end + return M diff --git a/lua/telescope/_extensions/advanced_git_search.lua b/lua/telescope/_extensions/advanced_git_search.lua index e4dc0b6..0acc5d3 100644 --- a/lua/telescope/_extensions/advanced_git_search.lua +++ b/lua/telescope/_extensions/advanced_git_search.lua @@ -1,18 +1,15 @@ -local func = require("advanced_git_search") +local pickers = require("advanced_git_search.telescope.pickers") local config = require("advanced_git_search.utils.config") return require("telescope").register_extension({ setup = function(ext_config, _) config.setup(ext_config) + + vim.api.nvim_create_user_command( + "AdvancedGitSearch", + "lua require('telescope').extensions.advanced_git_search.show_custom_functions()", + { range = true } + ) end, - exports = { - checkout_reflog = func.checkout_reflog, - diff_branch_file = func.diff_branch_file, - diff_commit_file = func.diff_commit_file, - diff_commit_line = func.diff_commit_line, - search_log_content = func.search_log_content, - search_log_content_file = func.search_log_content_file, - show_custom_functions = func.show_custom_functions, - changed_on_branch = func.changed_on_branch, - }, + exports = pickers, })