diff --git a/README.md b/README.md index 481bed510..4bb2aeead 100644 --- a/README.md +++ b/README.md @@ -557,6 +557,33 @@ This works for just about everything that has an object-ID in git, and if you fi See the built-in documentation for a comprehensive list of highlight groups. If your theme doesn't style a particular group, we'll try our best to do a nice job. +## Hooks + +Neogit supports hooks for the following actions: + +| Hook | Description | Hook Data | +| -------------------- | ------------------------------------------------ | -------------------------------------------------------------------- | +| `PreStatusRefreshed` | Before a status is reloaded | `{}` | +| `PreCommit` | Before a commit has been created | `{}` | +| `PrePush` | Before a push is made | `{}` | +| `PrePull` | Before a pull is made | `{}` | +| `PreFetch` | Before a fetch is made | `{}` | +| `PreBranchCreate` | Before a branch is created, starting from `base` | `{ branch_name: string, base: string? }` | +| `PreBranchDelete` | Before a branch is deleted | `{ branch_name: string }` | +| `PreBranchCheckout` | Before a branch is checked out | `{ branch_name: string }` | +| `PreBranchReset` | Before a branch is reset to a commit/branch | `{ branch_name: string, resetting_to: string }` | +| `PreBranchRename` | Before a branch is renamed | `{ branch_name: string, new_name: string }` | +| `PreRebase` | Before a rebase is started | `{ commit: string }` | +| `PreReset` | Before a branch is reset to a certain commit | `{ commit: string, mode: "soft"\|"mixed"\|"hard"\|"keep"\|"index" }` | +| `PreTagCreate` | Before a tag is placed on a certain commit | `{ name: string, ref: string }` | +| `PreTagDelete` | Before one or more tags are removed | `{ names: string[] }` | +| `PreCherryPick` | Before one or more commits are cherry-picked | `{ commits: string[] }` | +| `PreMerge` | Before a merge is started | `{ branch: string, args = string[] }` | +| `PreStash` | Before a stash is made | `{}` | +| `PreRefsRefreshed` | Before refs are refreshed | `{}` | +| `PreDiffLoaded` | Before a diff is loaded | `{?}` | +| `PreBisect` | Before a bisect is started | `{ type: string }` | +| `PreWorktreeCreate` | Before a worktree is created | `{ ref: string, path: string }` | ## Events diff --git a/lua/neogit/buffers/refs_view/init.lua b/lua/neogit/buffers/refs_view/init.lua index cf812c0a1..9c6f39ef6 100644 --- a/lua/neogit/buffers/refs_view/init.lua +++ b/lua/neogit/buffers/refs_view/init.lua @@ -3,6 +3,7 @@ local config = require("neogit.config") local ui = require("neogit.buffers.refs_view.ui") local popups = require("neogit.popups") local status_maps = require("neogit.config").get_reversed_status_maps() +local hook = require("neogit.lib.hook") local mapping = config.get_reversed_refs_view_maps() local CommitViewBuffer = require("neogit.buffers.commit_view") local Watcher = require("neogit.watcher") @@ -327,6 +328,7 @@ function M:open() end function M:redraw() + hook.run("PreRefsRefreshed") logger.debug("[REFS] Beginning redraw") self.buffer.ui:render(unpack(ui.RefsView(git.refs.list_parsed(), self.head))) diff --git a/lua/neogit/buffers/status/init.lua b/lua/neogit/buffers/status/init.lua index fd5ff2f4f..0cc893b66 100644 --- a/lua/neogit/buffers/status/init.lua +++ b/lua/neogit/buffers/status/init.lua @@ -7,6 +7,7 @@ local Watcher = require("neogit.watcher") local a = require("plenary.async") local logger = require("neogit.logger") -- TODO: Add logging local event = require("neogit.lib.event") +local hook = require("neogit.lib.hook") ---@class Semaphore ---@field permits number @@ -319,6 +320,7 @@ function M:refresh(partial, reason) source = "status", partial = partial, callback = function() + hook.run("PreStatusRefreshed") self:redraw(cursor, view) event.send("StatusRefreshed") logger.info("[STATUS] Refresh complete") diff --git a/lua/neogit/buffers/status/ui.lua b/lua/neogit/buffers/status/ui.lua index 5f1135ac5..bc00c3b99 100755 --- a/lua/neogit/buffers/status/ui.lua +++ b/lua/neogit/buffers/status/ui.lua @@ -6,6 +6,7 @@ local config = require("neogit.config") local a = require("plenary.async") local state = require("neogit.lib.state") local event = require("neogit.lib.event") +local hook = require("neogit.lib.hook") local col = Ui.col local row = Ui.row @@ -238,6 +239,7 @@ local SectionItemFile = function(section, config) end end + hook.run("PreDiffLoaded", { diff = diff }) -- TODO: what should the data be? ui.buf:with_locked_viewport(function() this:append(DiffHunks(diff)) ui:update() diff --git a/lua/neogit/config.lua b/lua/neogit/config.lua index 942f7999c..ecb18b576 100644 --- a/lua/neogit/config.lua +++ b/lua/neogit/config.lua @@ -323,6 +323,29 @@ end ---| "author-date" ---| "date" +---@alias NeogitHook +---| "PreStatusRefreshed" +---| "PreCommit" +---| "PrePush" +---| "PrePull" +---| "PreFetch" +---| "PreBranchCreate" +---| "PreBranchDelete" +---| "PreBranchCheckout" +---| "PreBranchReset" +---| "PreBranchRename" +---| "PreRebase" +---| "PreReset" +---| "PreTagCreate" +---| "PreTagDelete" +---| "PreCherryPick" +---| "PreMerge" +---| "PreStash" +---| "PreRefsRefreshed" +---| "PreDiffLoaded" +---| "PreBisect" +---| "PreWorktreeCreate" + ---@class NeogitConfigStatusOptions ---@field recent_commit_count? integer The number of recent commits to display ---@field mode_padding? integer The amount of padding to add to the right of the mode column @@ -401,6 +424,7 @@ end ---@field treesitter_diff_highlight? boolean Apply syntax highlighting to diff hunks via treesitter ---@field word_diff_highlight? boolean Apply word-diff highlighting to diff hunks ---@field builders? { [string]: fun(builder: PopupBuilder) } +---@field hooks? { [NeogitHook]: fun(data: table?) } ---Returns the default Neogit configuration ---@return NeogitConfig @@ -419,6 +443,7 @@ function M.get_default_values() log_date_format = nil, log_pager = nil, process_spinner = false, + hooks = {}, filewatcher = { enabled = true, }, diff --git a/lua/neogit/lib/git/bisect.lua b/lua/neogit/lib/git/bisect.lua index 76136f779..9528c292b 100644 --- a/lua/neogit/lib/git/bisect.lua +++ b/lua/neogit/lib/git/bisect.lua @@ -1,5 +1,6 @@ local git = require("neogit.lib.git") local event = require("neogit.lib.event") +local hook = require("neogit.lib.hook") ---@class NeogitGitBisect local M = {} @@ -25,6 +26,7 @@ end ---@param good_revision string ---@param args? table function M.start(bad_revision, good_revision, args) + hook.run("PreBisect", { type = "start" }) local result = git.cli.bisect.args("start").arg_list(args).args(bad_revision, good_revision).call { long = true } diff --git a/lua/neogit/lib/git/cherry_pick.lua b/lua/neogit/lib/git/cherry_pick.lua index 3d48e307e..7fdb1a996 100644 --- a/lua/neogit/lib/git/cherry_pick.lua +++ b/lua/neogit/lib/git/cherry_pick.lua @@ -3,6 +3,7 @@ local notification = require("neogit.lib.notification") local util = require("neogit.lib.util") local client = require("neogit.client") local event = require("neogit.lib.event") +local hook = require("neogit.lib.hook") ---@class NeogitGitCherryPick local M = {} @@ -13,6 +14,7 @@ local M = {} function M.pick(commits, args) local cmd = git.cli["cherry-pick"].arg_list(util.merge(args, commits)) + hook.run("PreCherryPick", { commits = commits }) local result if vim.tbl_contains(args, "--edit") then result = cmd.env(client.get_envs_git_editor()).call { pty = true } @@ -36,6 +38,7 @@ function M.apply(commits, args) end end) + hook.run("PreCherryPick", { commits = commits }) local result = git.cli["cherry-pick"].no_commit.arg_list(util.merge(args, commits)).call { await = true } if result:failure() then notification.error("Cherry Pick failed. Resolve conflicts before continuing") diff --git a/lua/neogit/lib/git/fetch.lua b/lua/neogit/lib/git/fetch.lua index 332c1333b..301f2f16c 100644 --- a/lua/neogit/lib/git/fetch.lua +++ b/lua/neogit/lib/git/fetch.lua @@ -1,4 +1,5 @@ local git = require("neogit.lib.git") +local hook = require("neogit.lib.hook") ---@class NeogitGitFetch local M = {} @@ -9,6 +10,7 @@ local M = {} ---@param args string[] ---@return ProcessResult function M.fetch_interactive(remote, branch, args) + hook.run("PreFetch") return git.cli.fetch.args(remote or "", branch or "").arg_list(args).call { pty = true } end @@ -16,6 +18,7 @@ end ---@param branch string ---@return ProcessResult function M.fetch(remote, branch) + hook.run("PreFetch") return git.cli.fetch.args(remote, branch).call { ignore_error = true } end diff --git a/lua/neogit/lib/git/merge.lua b/lua/neogit/lib/git/merge.lua index b018bfde1..52b324698 100644 --- a/lua/neogit/lib/git/merge.lua +++ b/lua/neogit/lib/git/merge.lua @@ -2,6 +2,7 @@ local client = require("neogit.client") local git = require("neogit.lib.git") local notification = require("neogit.lib.notification") local event = require("neogit.lib.event") +local hook = require("neogit.lib.hook") ---@class NeogitGitMerge local M = {} @@ -11,6 +12,7 @@ local function merge_command(cmd) end function M.merge(branch, args) + hook.run("PreMerge", { branch = branch, args = args }) local result = merge_command(git.cli.merge.args(branch).arg_list(args)) if result:failure() then notification.error("Merging failed. Resolve conflicts before continuing") diff --git a/lua/neogit/lib/git/pull.lua b/lua/neogit/lib/git/pull.lua index 5e59a7b81..33c800171 100644 --- a/lua/neogit/lib/git/pull.lua +++ b/lua/neogit/lib/git/pull.lua @@ -1,5 +1,6 @@ local git = require("neogit.lib.git") local util = require("neogit.lib.util") +local hook = require("neogit.lib.hook") ---@class NeogitGitPull local M = {} @@ -7,6 +8,7 @@ local M = {} function M.pull_interactive(remote, branch, args) local client = require("neogit.client") local envs = client.get_envs_git_editor() + hook.run("PrePull") return git.cli.pull.env(envs).args(remote or "", branch or "").arg_list(args).call { pty = true } end diff --git a/lua/neogit/lib/git/push.lua b/lua/neogit/lib/git/push.lua index 2041add09..417bc3786 100644 --- a/lua/neogit/lib/git/push.lua +++ b/lua/neogit/lib/git/push.lua @@ -1,5 +1,6 @@ local git = require("neogit.lib.git") local util = require("neogit.lib.util") +local hook = require("neogit.lib.hook") ---@class NeogitGitPush local M = {} @@ -10,6 +11,7 @@ local M = {} ---@param args string[] ---@return ProcessResult function M.push_interactive(remote, branch, args) + hook.run("PrePush") return git.cli.push.args(remote or "", branch or "").arg_list(args).call { pty = true } end diff --git a/lua/neogit/lib/git/rebase.lua b/lua/neogit/lib/git/rebase.lua index ed8aa0015..51eb11370 100644 --- a/lua/neogit/lib/git/rebase.lua +++ b/lua/neogit/lib/git/rebase.lua @@ -3,6 +3,7 @@ local git = require("neogit.lib.git") local client = require("neogit.client") local notification = require("neogit.lib.notification") local event = require("neogit.lib.event") +local hook = require("neogit.lib.hook") ---@class NeogitGitRebase local M = {} @@ -16,6 +17,7 @@ end ---@param args? string[] list of arguments to pass to git rebase ---@return ProcessResult function M.instantly(commit, args) + hook.run("PreRebase", { commit = commit }) local result = git.cli.rebase.interactive.autostash.autosquash .commit(commit) .env({ GIT_SEQUENCE_EDITOR = ":", GIT_EDITOR = ":" }) @@ -36,6 +38,7 @@ function M.rebase_interactive(commit, args) commit = "" end + hook.run("PreRebase", { commit = commit }) local result = rebase_command(git.cli.rebase.interactive.arg_list(args).args(commit)) if result:failure() then if result.stdout[1]:match("^hint: Waiting for your editor to close the file%.%.%. error") then @@ -52,6 +55,7 @@ function M.rebase_interactive(commit, args) end function M.onto_branch(branch, args) + hook.run("PreRebase", { commit = branch }) local result = rebase_command(git.cli.rebase.args(branch).arg_list(args)) if result:failure() then notification.error("Rebasing failed. Resolve conflicts before continuing") @@ -67,6 +71,7 @@ function M.onto(start, newbase, args) start = "" end + hook.run("PreRebase", { commit = newbase }) local result = rebase_command(git.cli.rebase.onto.args(newbase, start).arg_list(args)) if result:failure() then notification.error("Rebasing failed. Resolve conflicts before continuing") @@ -80,6 +85,7 @@ end ---@param commit string rev name of the commit to reword ---@return ProcessResult|nil function M.reword(commit) + hook.run("PreCommit") local message = table.concat(git.log.full_message(commit), "\n") local status = client.wrap( git.cli.commit.only.allow_empty.edit.with_message(("amend! %s\n\n%s"):format(commit, message)), @@ -99,6 +105,7 @@ end function M.modify(commit) local short_commit = git.rev_parse.abbreviate_commit(commit) local editor = "nvim -c '%s/^pick \\(" .. short_commit .. ".*\\)/edit \\1/' -c 'wq'" + hook.run("PreRebase", { commit = commit }) local result = git.cli.rebase.interactive.autosquash.autostash .commit(commit) .in_pty(true) @@ -113,6 +120,7 @@ end function M.drop(commit) local short_commit = git.rev_parse.abbreviate_commit(commit) local editor = "nvim -c '%s/^pick \\(" .. short_commit .. ".*\\)/drop \\1/' -c 'wq'" + hook.run("PreRebase", { commit = commit }) local result = git.cli.rebase.interactive.autosquash.autostash .commit(commit) .in_pty(true) diff --git a/lua/neogit/lib/git/stash.lua b/lua/neogit/lib/git/stash.lua index d82793476..b6ce113cb 100644 --- a/lua/neogit/lib/git/stash.lua +++ b/lua/neogit/lib/git/stash.lua @@ -3,6 +3,7 @@ local input = require("neogit.lib.input") local util = require("neogit.lib.util") local config = require("neogit.config") local event = require("neogit.lib.event") +local hook = require("neogit.lib.hook") ---@class NeogitGitStash local M = {} @@ -18,16 +19,19 @@ end ---@param args string[] function M.stash_all(args) + hook.run("PreStash") local result = git.cli.stash.push.files(".").arg_list(args).call() event.send("Stash", { success = result:success() }) end function M.stash_index() + hook.run("PreStash") local result = git.cli.stash.staged.call() event.send("Stash", { success = result:success() }) end function M.stash_keep_index() + hook.run("PreStash") local result = git.cli.stash.keep_index.files(".").call() event.send("Stash", { success = result:success() }) end @@ -35,11 +39,13 @@ end ---@param args string[] ---@param files string[] function M.push(args, files) + hook.run("PreStash") local result = git.cli.stash.push.arg_list(args).files(unpack(files)).call() event.send("Stash", { success = result:success() }) end function M.pop(stash) + hook.run("PreStash") local result = git.cli.stash.apply.index.args(stash).call() if result:success() then @@ -52,6 +58,7 @@ function M.pop(stash) end function M.apply(stash) + hook.run("PreStash") local result = git.cli.stash.apply.index.args(stash).call() if result:failure() then @@ -62,6 +69,7 @@ function M.apply(stash) end function M.drop(stash) + hook.run("PreStash") local result = git.cli.stash.drop.args(stash).call() event.send("Stash", { success = result:success() }) end diff --git a/lua/neogit/lib/git/worktree.lua b/lua/neogit/lib/git/worktree.lua index 1095eb3b5..78f39a0a3 100644 --- a/lua/neogit/lib/git/worktree.lua +++ b/lua/neogit/lib/git/worktree.lua @@ -1,6 +1,7 @@ local git = require("neogit.lib.git") local util = require("neogit.lib.util") local Path = require("plenary.path") +local hook = require("neogit.lib.hook") ---@class NeogitGitWorktree local M = {} @@ -10,6 +11,7 @@ local M = {} ---@param path string absolute path ---@return boolean, string function M.add(ref, path, params) + hook.run("PreWorktreeCreate", { ref = ref, path = path }) local result = git.cli.worktree.add.arg_list(params or {}).args(path, ref).call() if result:success() then return true, "" diff --git a/lua/neogit/lib/hook.lua b/lua/neogit/lib/hook.lua new file mode 100644 index 000000000..3eb8d1434 --- /dev/null +++ b/lua/neogit/lib/hook.lua @@ -0,0 +1,15 @@ +local M = {} + +local config = require("neogit.config") + +---@param name NeogitHook +---@param data table? +function M.run(name, data) + assert(name, "hook must have name") + + if config.values.hooks[name] then + config.values.hooks[name](data) + end +end + +return M diff --git a/lua/neogit/popups/branch/actions.lua b/lua/neogit/popups/branch/actions.lua index ca34c0fc4..f99eca819 100644 --- a/lua/neogit/popups/branch/actions.lua +++ b/lua/neogit/popups/branch/actions.lua @@ -6,6 +6,7 @@ local input = require("neogit.lib.input") local util = require("neogit.lib.util") local notification = require("neogit.lib.notification") local event = require("neogit.lib.event") +local hook = require("neogit.lib.hook") local a = require("plenary.async") local FuzzyFinderBuffer = require("neogit.buffers.fuzzy_finder") @@ -21,6 +22,8 @@ local function fetch_remote_branch(target) end local function checkout_branch(target, args) + hook.run("PreBranchCheckout", { branch_name = target }) + local result = git.branch.checkout(target, args) if result:failure() then notification.error(table.concat(result.stderr, "\n")) @@ -63,6 +66,7 @@ local function spin_off_branch(checkout) return end + hook.run("PreBranchCreate", { branch_name = name }) if not git.branch.create(name) then notification.warn("Branch " .. name .. " already exists.") return @@ -73,6 +77,7 @@ local function spin_off_branch(checkout) local current_branch_name = git.branch.current_full_name() if checkout then + hook.run("PreBranchCheckout", { branch_name = name }) git.cli.checkout.branch(name).call() event.send("BranchCheckout", { branch_name = name }) end @@ -83,6 +88,7 @@ local function spin_off_branch(checkout) assert(current_branch_name, "No current branch") git.log.update_ref(current_branch_name, upstream) else + hook.run("PreReset", { commit = name, mode = "hard" }) git.cli.reset.hard.args(upstream).call() event.send("Reset", { commit = name, mode = "hard" }) end @@ -131,6 +137,7 @@ local function create_branch(popup, prompt, checkout, name) return end + hook.run("PreBranchCreate", { branch_name = name, base = base_branch }) local success = git.branch.create(name, base_branch) if success then event.send("BranchCreate", { branch_name = name, base = base_branch }) @@ -188,6 +195,8 @@ function M.checkout_local_branch(popup) } if target then + hook.run("PreBranchCheckout", { branch_name = target }) + if vim.tbl_contains(remote_branches, target) then local result = git.branch.track(target, popup:get_arguments()) if result:failure() then @@ -246,6 +255,7 @@ function M.rename_branch() return end + hook.run("PreBranchRename", { branch_name = selected_branch, new_name = new_name }) local result = git.cli.branch.move.args(selected_branch, new_name).call { await = true } if result:success() then notification.info(string.format("Renamed '%s' to '%s'", selected_branch, new_name)) @@ -292,6 +302,7 @@ function M.reset_branch(popup) end -- Reset the current branch to the desired state & update reflog + hook.run("PreBranchReset", { branch_name = current, resetting_to = to }) local result = git.cli.reset.hard.args(to).call() if result:success() then local current = git.branch.current_full_name() @@ -348,6 +359,7 @@ function M.delete_branch(popup) return end + hook.run("PreBranchDelete", { branch_name = branch_name }) success = git.branch.delete(branch_name) if not success then -- Reset HEAD if unsuccessful git.cli.checkout.branch(branch_name).call() diff --git a/lua/neogit/popups/commit/actions.lua b/lua/neogit/popups/commit/actions.lua index eaef57c31..1bcdeb72f 100644 --- a/lua/neogit/popups/commit/actions.lua +++ b/lua/neogit/popups/commit/actions.lua @@ -7,6 +7,7 @@ local input = require("neogit.lib.input") local notification = require("neogit.lib.notification") local config = require("neogit.config") local a = require("plenary.async") +local hook = require("neogit.lib.hook") ---@param popup PopupData ---@return boolean @@ -34,6 +35,7 @@ local function confirm_modifications() end local function do_commit(popup, cmd) + hook.run("PreCommit") client.wrap(cmd.arg_list(popup:get_arguments()), { autocmd = "NeogitCommitComplete", msg = { diff --git a/lua/neogit/popups/reset/actions.lua b/lua/neogit/popups/reset/actions.lua index 248874d93..3cc931197 100644 --- a/lua/neogit/popups/reset/actions.lua +++ b/lua/neogit/popups/reset/actions.lua @@ -3,6 +3,7 @@ local util = require("neogit.lib.util") local notification = require("neogit.lib.notification") local FuzzyFinderBuffer = require("neogit.buffers.fuzzy_finder") local event = require("neogit.lib.event") +local hook = require("neogit.lib.hook") local M = {} @@ -26,6 +27,7 @@ end local function reset(fn, popup, prompt, mode) local target = target(popup, prompt) if target then + hook.run("PreReset", { commit = target, mode = mode }) local success = fn(target) if success then notification.info("Reset to " .. target) @@ -81,6 +83,7 @@ function M.a_file(popup) local files = FuzzyFinderBuffer.new(files):open_async { allow_multi = true } if files and files[1] then + hook.run("PreReset", { commit = target, mode = "files", files = files }) if git.reset.file(target, files) then if #files > 1 then notification.info("Reset " .. #files .. " files") diff --git a/lua/neogit/popups/tag/actions.lua b/lua/neogit/popups/tag/actions.lua index c93b35307..049099bdc 100644 --- a/lua/neogit/popups/tag/actions.lua +++ b/lua/neogit/popups/tag/actions.lua @@ -7,6 +7,7 @@ local FuzzyFinderBuffer = require("neogit.buffers.fuzzy_finder") local input = require("neogit.lib.input") local notification = require("neogit.lib.notification") local event = require("neogit.lib.event") +local hook = require("neogit.lib.hook") ---@param popup PopupData function M.create_tag(popup) @@ -28,6 +29,7 @@ function M.create_tag(popup) end end + hook.run("PreTagCreate", { name = tag_input, ref = selected }) local code = client.wrap(git.cli.tag.arg_list(utils.merge(popup:get_arguments(), { tag_input, selected })), { autocmd = "NeogitTagComplete", @@ -57,6 +59,7 @@ function M.delete(_) return end + hook.run("PreTagDelete", { names = tags }) if git.tag.delete(tags) then notification.info("Deleted tags: " .. table.concat(tags, ",")) for _, tag in pairs(tags) do