git.lua 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. local M = {
  2. {
  3. "tpope/vim-fugitive",
  4. config = function()
  5. local git_status = function()
  6. local bufnr = vim.api.nvim_get_current_buf()
  7. if vim.b[bufnr].fugitive_status then
  8. local winnr = vim.fn.bufwinid(bufnr)
  9. vim.api.nvim_win_close(winnr, true)
  10. else
  11. vim.cmd("G")
  12. end
  13. end
  14. vim.g.fugitive_git_executable = "env GPG_TTY=$(tty) git"
  15. vim.env.GPG_TTY = vim.fn.system("tty"):gsub("\n", "")
  16. nmap("<leader>lg", ":G<cr>", { desc = "Git Status" })
  17. nmap("<leader>gs", git_status, { desc = "Toggle Git Status" })
  18. nmap("gs", git_status, { desc = "Toggle Git Status" })
  19. vim.api.nvim_create_autocmd("FileType", {
  20. pattern = { "fugitive", "fugitiveblame", "fugitive-status" },
  21. callback = function()
  22. nmap("P", function()
  23. local cmd = "git push --force-with-lease"
  24. Snacks.notifier.notify("Pushing...", vim.log.levels.INFO)
  25. vim.fn.jobstart(cmd, {
  26. on_exit = function(_, code)
  27. if code == 0 then
  28. Snacks.notifier.notify("Push completed successfully", vim.log.levels.INFO)
  29. else
  30. Snacks.notifier.notify("Push failed with exit code: " .. code, vim.log.levels.ERROR)
  31. end
  32. end,
  33. detach = true,
  34. })
  35. end, { buffer = true, desc = "Git Push Force With Lease (Async)" })
  36. nmap("<C-c>", function()
  37. local win_id = vim.api.nvim_get_current_win()
  38. vim.api.nvim_win_close(win_id, false)
  39. end, { buffer = true, desc = "Close window" })
  40. -- Git Pull
  41. nmap("gp", function()
  42. local cmd = "git pull"
  43. Snacks.notifier.notify("Pulling...", vim.log.levels.INFO)
  44. vim.fn.jobstart(cmd, {
  45. on_exit = function(_, code)
  46. if code == 0 then
  47. Snacks.notifier.notify("Pull completed successfully", vim.log.levels.INFO)
  48. else
  49. Snacks.notifier.notify("Pull failed with exit code: " .. code, vim.log.levels.ERROR)
  50. end
  51. end,
  52. detach = true,
  53. })
  54. end, { buffer = true, desc = "Close window" })
  55. end,
  56. })
  57. end,
  58. },
  59. {
  60. "lewis6991/gitsigns.nvim",
  61. config = function()
  62. require("gitsigns").setup({
  63. current_line_blame_formatter = "<author>, <author_time:%Y-%m-%d> - <summary>",
  64. current_line_blame = true,
  65. signs = {
  66. add = { text = icons.ui.BoldLineMiddle },
  67. change = { text = icons.ui.BoldLineDashedMiddle },
  68. delete = { text = icons.ui.TriangleShortArrowRight },
  69. topdelete = { text = icons.ui.TriangleShortArrowRight },
  70. changedelete = { text = icons.ui.BoldLineMiddle },
  71. },
  72. })
  73. end,
  74. },
  75. {
  76. "ruifm/gitlinker.nvim",
  77. config = function()
  78. require("gitlinker").setup({
  79. message = false,
  80. console_log = false,
  81. })
  82. nmap("<leader>gy", "<cmd>lua require('gitlinker').get_buf_range_url('n')<cr>")
  83. end,
  84. },
  85. {
  86. "polarmutex/git-worktree.nvim",
  87. version = "^2",
  88. dependencies = { "nvim-lua/plenary.nvim", "ibhagwan/fzf-lua", "folke/snacks.nvim" },
  89. config = function()
  90. local fzf_lua = require("fzf-lua")
  91. local git_worktree = require("git-worktree")
  92. -- Basic hooks
  93. local Hooks = require("git-worktree.hooks")
  94. Hooks.register(Hooks.type.SWITCH, Hooks.builtins.update_current_buffer_on_switch)
  95. -- Switch worktrees
  96. local function switch_worktree()
  97. vim.system({ "git", "worktree", "list" }, {}, function(result)
  98. if result.code ~= 0 then
  99. return
  100. end
  101. local items = {}
  102. for line in result.stdout:gmatch("[^\n]+") do
  103. local path, branch = line:match("^([^%s]+)%s+%[?([^%]]*)")
  104. if path and branch then
  105. table.insert(items, path .. " (" .. (branch ~= "" and branch or "detached") .. ")")
  106. end
  107. end
  108. vim.schedule(function()
  109. fzf_lua.fzf_exec(items, {
  110. prompt = "Worktrees> ",
  111. actions = {
  112. default = function(selected)
  113. local path = selected[1]:match("^([^%(]+)")
  114. git_worktree.switch_worktree(path:gsub("%s+$", ""))
  115. end,
  116. },
  117. })
  118. end)
  119. end)
  120. end
  121. -- Create worktree
  122. local function create_worktree()
  123. -- Get the git directory (works for both regular and bare repos)
  124. vim.system({ "git", "rev-parse", "--git-dir" }, {}, function(result)
  125. if result.code ~= 0 then
  126. vim.schedule(function()
  127. vim.notify("Not in a git repository", vim.log.levels.ERROR)
  128. end)
  129. return
  130. end
  131. local git_dir = result.stdout:gsub("\n", "")
  132. -- For bare repos, git-dir is the repo itself; for regular repos, get parent
  133. local repo_root = git_dir:match("%.git$") and git_dir:gsub("/%.git$", "") or git_dir
  134. vim.system({ "git", "branch", "-a" }, { cwd = repo_root }, function(branch_result)
  135. if branch_result.code ~= 0 then
  136. return
  137. end
  138. local branches = {}
  139. for line in branch_result.stdout:gmatch("[^\n]+") do
  140. local branch = line:gsub("^%s*%*?%s*", ""):gsub("^remotes/", "")
  141. if branch ~= "" and not branch:match("HEAD") then
  142. table.insert(branches, branch)
  143. end
  144. end
  145. vim.schedule(function()
  146. fzf_lua.fzf_exec(branches, {
  147. prompt = "Base Branch> ",
  148. actions = {
  149. default = function(selected)
  150. if not selected or #selected == 0 then
  151. return
  152. end
  153. local base_branch = selected[1]
  154. local default_name = base_branch:gsub(".*/", "")
  155. require("snacks").input({
  156. prompt = "Worktree name",
  157. default = default_name,
  158. icon = "🌿",
  159. }, function(name)
  160. if name then
  161. git_worktree.create_worktree(name, base_branch, "origin")
  162. end
  163. end)
  164. end,
  165. },
  166. })
  167. end)
  168. end)
  169. end)
  170. end
  171. -- Delete worktree
  172. local function delete_worktree()
  173. local cwd = vim.fn.getcwd() -- Get this before async call
  174. vim.system({ "git", "worktree", "list" }, {}, function(result)
  175. if result.code ~= 0 then
  176. return
  177. end
  178. local items = {}
  179. for line in result.stdout:gmatch("[^\n]+") do
  180. local path, branch = line:match("^([^%s]+)%s+%[?([^%]]*)")
  181. if path and path ~= cwd then
  182. table.insert(items, path .. " (" .. (branch ~= "" and branch or "detached") .. ")")
  183. end
  184. end
  185. vim.schedule(function()
  186. fzf_lua.fzf_exec(items, {
  187. prompt = "Delete> ",
  188. actions = {
  189. default = function(selected)
  190. local path = selected[1]:match("^([^%(]+)"):gsub("%s+$", "")
  191. -- Simple vim.ui.select for yes/no
  192. vim.ui.select({ "Yes", "No" }, {
  193. prompt = "Delete worktree '" .. path .. "'?",
  194. }, function(choice)
  195. if choice == "Yes" then
  196. git_worktree.delete_worktree(path, true)
  197. end
  198. end)
  199. end,
  200. },
  201. })
  202. end)
  203. end)
  204. end
  205. -- Keymaps
  206. vim.keymap.set("n", "<leader>ws", switch_worktree, { desc = "Switch Worktree" })
  207. vim.keymap.set("n", "<leader>wc", create_worktree, { desc = "Create Worktree" })
  208. vim.keymap.set("n", "<leader>wd", delete_worktree, { desc = "Delete Worktree" })
  209. end,
  210. },
  211. }
  212. return M