diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 07477766c..05eab19a8 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -46,11 +46,13 @@ jobs: steps: - uses: actions/checkout@v4 - uses: Homebrew/actions/setup-homebrew@master + - run: brew install lua-language-server - uses: luarocks/gh-actions-lua@v10 with: luaVersion: luajit - uses: luarocks/gh-actions-luarocks@v5 + with: + luaRocksVersion: "3.12.1" - run: | - HOMEBREW_NO_INSTALL_CLEANUP=1 brew install lua-language-server luarocks install llscheck llscheck lua/ diff --git a/.luarc.json b/.luarc.json index f24169681..3a2495a62 100644 --- a/.luarc.json +++ b/.luarc.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", + "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", "diagnostics.disable": [ "redefined-local" ], diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f3010b30..10fd3df64 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,9 +50,23 @@ Simply clone *Neogit* to your project directory of choice to be able to use your Logging is a useful tool for inspecting what happens in the code and in what order. Neogit uses [`Plenary`](https://github.com/nvim-lua/plenary.nvim) for logging. -Export the environment variables `NEOGIT_LOG_CONSOLE="sync"` to enable logging, and `NEOGIT_LOG_LEVEL="debug"` for more -verbose logging. +#### Enabling logging via environment variables +- To enable logging to console, export `NEOGIT_LOG_CONSOLE="sync"` +- To enable logging to a file, export `NEOGIT_LOG_FILE="true"` +- For more verbose logging, set the log level to `debug` via `NEOGIT_LOG_LEVEL="debug"` + +#### Enabling logging via lua api + +To turn on logging while neovim is already running, you can use: + +```lua +:lua require("neogit.logger").config.use_file = true -- for logs to ~/.cache/nvim/neogit.log. +:lua require("neogit.logger").config.use_console = true -- for logs to console. +:lua require("neogit.logger").config.level = 'debug' -- to set the log level +``` + +#### Using the logger from the neogit codebase ```lua local logger = require("neogit.logger") diff --git a/README.md b/README.md index 9de0281f0..bedd1eaa5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ +
+
+
+ + Warp sponsorship + + +### [Warp, the intelligent terminal for developers](https://www.warp.dev/neogit) +#### [Try running neogit in Warp](https://www.warp.dev/neogit)
+ +
+ +
+
@@ -148,6 +162,8 @@ neogit.setup { HEAD_padding = 10, HEAD_folded = false, mode_padding = 3, + -- group changes by folder + tree_view = false, mode_text = { M = "modified", N = "new file", @@ -197,12 +213,6 @@ neogit.setup { merge_editor = { kind = "auto", }, - description_editor = { - kind = "auto", - }, - tag_editor = { - kind = "auto", - }, preview_buffer = { kind = "floating_console", }, @@ -338,6 +348,7 @@ neogit.setup { [""] = "Next", [""] = "Previous", [""] = "InsertCompletion", + [""] = "CopySelection", [""] = "MultiselectToggleNext", [""] = "MultiselectTogglePrevious", [""] = "NOP", @@ -382,6 +393,8 @@ neogit.setup { ["4"] = "Depth4", ["Q"] = "Command", [""] = "Toggle", + ["za"] = "Toggle", + ["zo"] = "OpenFold", ["x"] = "Discard", ["s"] = "Stage", ["S"] = "StageUnstaged", @@ -517,6 +530,12 @@ Neogit follows semantic versioning. See [CONTRIBUTING.md](https://github.com/NeogitOrg/neogit/blob/master/CONTRIBUTING.md) for more details. +## Contributors + + + + + ## Special Thanks - [kolja](https://github.com/kolja) for the Neogit Logo diff --git a/doc/neogit.txt b/doc/neogit.txt index 893631c6c..5d1cb0276 100644 --- a/doc/neogit.txt +++ b/doc/neogit.txt @@ -18,6 +18,8 @@ CONTENTS *neogit_contents* 4. Events |neogit_events| 5. Highlights |neogit_highlights| 6. API |neogit_api| + • Popup Builder |neogit_popup_builder| + • Customizing Popups |neogit_custom_popups| 7. Usage |neogit_usage| 8. Popups *neogit_popups* • Bisect |neogit_bisect_popup| @@ -223,12 +225,6 @@ to Neovim users. merge_editor = { kind = "auto", }, - description_editor = { - kind = "auto", - }, - tag_editor = { - kind = "auto", - }, preview_buffer = { kind = "floating_console", }, @@ -397,6 +393,7 @@ The following mappings can all be customized via the setup function. [""] = "Next", [""] = "Previous", [""] = "InsertCompletion", + [""] = "CopySelection", [""] = "MultiselectToggleNext", [""] = "MultiselectTogglePrevious", [""] = "NOP", @@ -489,83 +486,135 @@ The following mappings can all be customized via the setup function. The following events are emitted by Neogit: - Event Description ~ - NeogitStatusRefreshed Status has been reloaded ~ - - Event Data: {} ~ - - NeogitCommitComplete Commit has been created ~ - - Event Data: {} ~ - - NeogitPushComplete Push has completed ~ - - Event Data: {} ~ - - NeogitPullComplete Pull has completed ~ - - Event Data: {} ~ - - NeogitFetchComplete Fetch has completed ~ - - Event Data: {} ~ - - NeogitBranchCreate Branch was created, starting from ~ - `base` ~ - - Event Data: ~ - { branch_name: string, base: string? } ~ - - NeogitBranchDelete Branch was deleted ~ - - Event Data: { branch_name: string } ~ - - NeogitBranchCheckout Branch was checked out ~ - - Event Data: { branch_name: string } ~ - - NeogitBranchReset Branch was reset to a commit/branch ~ +• `NeogitStatusRefreshed` + When: Status has been reloaded + Data: `{}` - Event Data: ~ - { branch_name: string, resetting_to: string } ~ +• `NeogitCommitComplete` + When: Commit has been created + Data: `{}` - NeogitBranchRename Branch was renamed ~ +• `NeogitPushComplete` + When: Push has finished + Data: `{}` - Event Data: ~ - { branch_name: string, new_name: string } ~ +• `NeogitPullComplete` + When: Push has finished + Data: `{}` - NeogitRebase A rebase finished ~ +• `NeogitFetchComplete` + When: Fetch has finished + Data: `{}` - Event Data: ~ - { commit: string, status: "ok" | "conflict" } ~ - - NeogitReset A branch was reset to a certain commit ~ - - Event Data: ~ - { commit: string, mode: "soft" | ~ - "mixed" | "hard" | "keep" | "index" } ~ - - NeogitTagCreate A tag was placed on a certain commit ~ - - Event Data: { name: string, ref: string } ~ - - NeogitTagDelete A tag was removed ~ - - Event Data: { name: string } ~ - - NeogitCherryPick One or more commits were cherry-picked ~ - - Event Data: { commits: string[] } ~ - - NeogitMerge A merge finished ~ - - Event Data: ~ - { branch: string, args: string[], ~ - status: "ok" | "conflict" } ~ - - NeogitStash A stash finished ~ - - Event Data: { success: boolean } ~ +• `NeogitBranchCreate` + When: Branch was created, starting from `` + Data: > + { + branch_name: string, + base: string? + } +< +• `NeogitBranchDelete` + When: Branch was deleted + Data: > + { + branch_name: string, + } +< +• `NeogitBranchCheckout` + When: Branch was checked out + Data: > + { + branch_name: string, + } +< +• `NeogitBranchReset` + When: Branch was reset to commit/branch + Data: > + { + branch_name: string, + resetting_to: string + } +< +• `NeogitBranchRename` + When: Branch was renamed + Data: > + { + branch_name: string, + new_name: string + } +< +• `NeogitRebase` + When: A rebase has finished + Data: > + { + commit: string, + status: "ok" | "conflict" + } +< +• `NeogitReset` + When: A reset has been performed + Data: > + { + commit: string, + mode: "soft" | "mixed" | "hard" | "keep" | "index" + } +< +• `NeogitTagCreate` + When: A tag is placed on a commit + Data: > + { + ref: string, + name: string + } +< +• `NeogitTagCreate` + When: A tag is placed on a commit + Data: > + { + ref: string, + name: string + } +< +• `NeogitTagDelete` + When: A tag is removed + Data: > + { + name: string + } +< +• `NeogitCherryPick` + When: One or more commits were cherry picked + Data: > + { + commits: string[] + } +< +• `NeogitMerge` + When: A merge has finished + Data: > + { + branch: string, + args: string[], + status: "ok" | "conflict" + } +< +• `NeogitStash` + When: A stash was performed + Data: > + { + success: boolean + } +< +• `NeogitWorktreeCreate` + When: A worktree was created + Data: > + { + old_cwd: string, + new_cwd: string, + copy_if_present: function(filename: string, callback: function|nil) + } +< ============================================================================== 5. Highlights *neogit_highlights* @@ -715,21 +764,21 @@ NeogitCommitViewHeader Applied to header of Commit View LOG VIEW BUFFER NeogitGraphAuthor Applied to the commit's author in graph view NeogitGraphBlack Used when --colors is enabled for graph -NeogitGraphBlackBold +NeogitGraphBoldBlack NeogitGraphRed -NeogitGraphRedBold +NeogitGraphBoldRed NeogitGraphGreen -NeogitGraphGreenBold +NeogitGraphBoldGreen NeogitGraphYellow -NeogitGraphYellowBold +NeogitGraphBoldYellow NeogitGraphBlue -NeogitGraphBlueBold +NeogitGraphBoldBlue NeogitGraphPurple -NeogitGraphPurpleBold +NeogitGraphBoldPurple NeogitGraphCyan -NeogitGraphCyanBold +NeogitGraphBoldCyan NeogitGraphWhite -NeogitGraphWhiteBold +NeogitGraphBoldWhite NeogitGraphGray NeogitGraphBoldGray NeogitGraphOrange @@ -1007,7 +1056,7 @@ Actions: *neogit_branch_popup_actions* the old branch. • Checkout new worktree *neogit_branch_checkout_worktree* - (Not yet implemented) + see: |neogit_worktree_checkout| • Create new branch *neogit_branch_create_branch* Functionally the same as |neogit_branch_checkout_new|, but does not update @@ -1018,7 +1067,7 @@ Actions: *neogit_branch_popup_actions* index has uncommitted changes, will behave exactly the same as spin_off. • Create new worktree *neogit_branch_create_worktree* - (Not yet implemented) + see: |neogit_worktree_create_branch| • Configure *neogit_branch_configure* Opens selector to choose a branch, then offering some configuration @@ -1266,14 +1315,38 @@ Actions: *neogit_commit_popup_actions* Creates a fixup commit. If a commit is selected it will be used, otherwise the user is prompted to pick a commit. + `git commit --fixup=COMMIT --no-edit` + • Squash *neogit_commit_squash* Creates a squash commit. If a commit is selected it will be used, otherwise the user is prompted to pick a commit. + `git commit --squash=COMMIT --no-edit` + • Augment *neogit_commit_augment* Creates a squash commit, editing the squash message. If a commit is selected it will be used, otherwise the user is prompted to pick a commit. + `git commit --squash=COMMIT --edit` + + • Alter *neogit_commit_alter* + Create a squash commit, authoring the final message now. + + During a later rebase, when this commit gets squashed into it's targeted + commit, the original message of the targeted commit is replaced with the + message of this commit, without the user automatically being given a + chance to edit it again. + + `git commit --fixup=amend:COMMIT --edit` + + • Revise *neogit_commit_revise* + Reword the message of an existing commit, without editing it's tree. + Later, when the commit is squashed into it's targeted commit, a combined + commit is created which uses the message of the fixup commit and the tree + of the targeted commit. + + `git commit --fixup=reword:COMMIT --edit` + • Instant Fixup *neogit_commit_instant_fixup* Similar to |neogit_commit_fixup|, but instantly rebases after. @@ -1848,7 +1921,14 @@ Actions: *neogit_reset_popup_actions* changes. • Worktree *neogit_reset_worktree* - (Not yet implemented) + Resets current worktree to specified commit. + + • Branch *neogit_reset_branch* + see: |neogit_branch_reset| + + • File *neogit_reset_file* + Attempts to perform a `git checkout` from the specified revision, and if + that fails, tries `git reset` instead. ============================================================================== Stash Popup *neogit_stash_popup* @@ -2006,6 +2086,8 @@ Untracked Files *neogit_status_buffer_untracked* ============================================================================== Editor Buffer *neogit_editor_buffer* +User customizations can be made via `gitcommit` ftplugin. + Commands: *neogit_editor_commands* • Submit *neogit_editor_submit* Default key: `` @@ -2063,6 +2145,8 @@ Refs Buffer *neogit_refs_buffer* ============================================================================== Rebase Todo Buffer *neogit_rebase_todo_buffer* +User customizations can be made via `gitrebase` ftplugin. + The Rebase editor has some extra commands, beyond being a normal vim buffer. The following keys, in normal mode, will act on the commit under the cursor: @@ -2077,7 +2161,7 @@ The following keys, in normal mode, will act on the commit under the cursor: • `` Open current commit in Commit Buffer ============================================================================== -Custom Popups *neogit_custom_popups* +Popup Builder *neogit_popup_builder* You can leverage Neogit's infrastructure to create your own popups and actions. For example, you can define actions as a function which will take the @@ -2144,9 +2228,13 @@ calling the setup function: }) < -You can also customize existing popups via the Neogit config. +============================================================================== +Customizing Popups *neogit_custom_popups* + +You can customize existing popups via the Neogit config. + Below is an example of adding a custom switch, but you can use any function -from the builder API. +from the builder API. >lua require("neogit").setup({ builders = { @@ -2156,7 +2244,7 @@ from the builder API. }, }) -Keep in mind that builder hooks are executed at the end of the popup +Keep in mind that builder hooks are executed at the end of the popup builder, so any switches or options added will be placed at the end. ------------------------------------------------------------------------------ diff --git a/lefthook.yml b/lefthook.yml index bef6a3a9a..0920c0252 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -1,8 +1,6 @@ skip_output: - meta pre-push: - only: - - ref: master files: "rg --files" parallel: true commands: @@ -19,7 +17,7 @@ pre-push: run: typos {files} lua-types: glob: "*.lua" - run: llscheck lua/ + run: llscheck lua/ || echo {files} lua-test: glob: "tests/specs/**/*_spec.lua" run: nvim --headless -S "./tests/init.lua" || echo {files} @@ -29,4 +27,23 @@ pre-push: - GIT_CONFIG_SYSTEM: /dev/null - NVIM_APPNAME: neogit-test rspec: + only: + - ref: master run: bin/specs {files} +pre-commit: + parallel: true + commands: + rubocop: + glob: "*.rb" + run: bundle exec rubocop {staged_files} + selene: + glob: "{lua,plugin}/**/*.lua" + run: selene --config selene/config.toml {staged_files} + stylua: + glob: "*.lua" + run: stylua --check {staged_files} + typos: + run: typos {staged_files} + lua-types: + glob: "*.lua" + run: llscheck lua/ diff --git a/lua/neogit.lua b/lua/neogit.lua index 4f052e882..7f66dcce0 100644 --- a/lua/neogit.lua +++ b/lua/neogit.lua @@ -186,6 +186,7 @@ end ---@return function function M.action(popup, action, args) local util = require("neogit.lib.util") + local git = require("neogit.lib.git") local a = require("plenary.async") args = args or {} @@ -202,15 +203,20 @@ function M.action(popup, action, args) if ok then local fn = actions[action] if fn then - fn { - state = { env = {} }, - get_arguments = function() - return args - end, - get_internal_arguments = function() - return internal_args - end, - } + local action = function() + fn { + close = function() end, + state = { env = {} }, + get_arguments = function() + return args + end, + get_internal_arguments = function() + return internal_args + end, + } + end + + git.repo:dispatch_refresh { source = "action", callback = action } else M.notification.error( string.format( diff --git a/lua/neogit/autocmds.lua b/lua/neogit/autocmds.lua index bfb4bdb19..edcd7926c 100644 --- a/lua/neogit/autocmds.lua +++ b/lua/neogit/autocmds.lua @@ -46,6 +46,14 @@ function M.setup() autocmd_disabled = args.event == "QuickFixCmdPre" end, }) + + -- Ensure vim buffers are updated + api.nvim_create_autocmd("User", { + pattern = "NeogitStatusRefreshed", + callback = function() + vim.cmd("set autoread | checktime") + end, + }) end return M diff --git a/lua/neogit/buffers/commit_view/init.lua b/lua/neogit/buffers/commit_view/init.lua index 90942a931..60dbed42f 100644 --- a/lua/neogit/buffers/commit_view/init.lua +++ b/lua/neogit/buffers/commit_view/init.lua @@ -146,11 +146,14 @@ function M:update(commit_id, filter) self.buffer.ui:render( unpack(ui.CommitView(self.commit_info, self.commit_overview, self.commit_signature, self.item_filter)) ) + + self.buffer:win_call(vim.cmd, "normal! gg") end ---Opens the CommitViewBuffer ---If already open will close the buffer ---@param kind? string +---@return CommitViewBuffer function M:open(kind) kind = kind or config.values.commit_view.kind @@ -290,7 +293,8 @@ function M:open(kind) end), [popups.mapping_for("RemotePopup")] = popups.open("remote"), [popups.mapping_for("RevertPopup")] = popups.open("revert", function(p) - p { commits = { self.commit_info.oid } } + local item = self.buffer.ui:get_hunk_or_filename_under_cursor() or {} + p { commits = { self.commit_info.oid }, hunk = item.hunk } end), [popups.mapping_for("ResetPopup")] = popups.open("reset", function(p) p { commit = self.commit_info.oid } @@ -331,6 +335,8 @@ function M:open(kind) vim.cmd("normal! zR") end, } + + return self end return M diff --git a/lua/neogit/buffers/log_view/init.lua b/lua/neogit/buffers/log_view/init.lua index 2dbb52890..92978614c 100644 --- a/lua/neogit/buffers/log_view/init.lua +++ b/lua/neogit/buffers/log_view/init.lua @@ -119,7 +119,7 @@ function M:open() p { commits = self.buffer.ui:get_commits_in_selection() } end), [popups.mapping_for("DiffPopup")] = popups.open("diff", function(p) - local items = self.buffer.ui:get_commits_in_selection() + local items = self.buffer.ui:get_ordered_commits_in_selection() p { section = { name = "log" }, item = { name = items }, @@ -188,7 +188,9 @@ function M:open() [status_maps["PeekFile"]] = function() local commit = self.buffer.ui:get_commit_under_cursor() if commit then - CommitViewBuffer.new(commit, self.files):open() + local buffer = CommitViewBuffer.new(commit, self.files):open() + buffer.buffer:win_call(vim.cmd, "normal! gg") + self.buffer:focus() end end, diff --git a/lua/neogit/buffers/process/init.lua b/lua/neogit/buffers/process/init.lua index aa38db1ab..1e7308ce0 100644 --- a/lua/neogit/buffers/process/init.lua +++ b/lua/neogit/buffers/process/init.lua @@ -52,6 +52,7 @@ function M:show() self:flush_content() end +---@return boolean function M:is_visible() return self.buffer and self.buffer:is_valid() and self.buffer:is_visible() end diff --git a/lua/neogit/buffers/reflog_view/init.lua b/lua/neogit/buffers/reflog_view/init.lua index 8f62d78c1..0187a187d 100644 --- a/lua/neogit/buffers/reflog_view/init.lua +++ b/lua/neogit/buffers/reflog_view/init.lua @@ -89,7 +89,7 @@ function M:open(_) end), [popups.mapping_for("PullPopup")] = popups.open("pull"), [popups.mapping_for("DiffPopup")] = popups.open("diff", function(p) - local items = self.buffer.ui:get_commits_in_selection() + local items = self.buffer.ui:get_ordered_commits_in_selection() p { section = { name = "log" }, item = { name = items }, diff --git a/lua/neogit/buffers/refs_view/init.lua b/lua/neogit/buffers/refs_view/init.lua index 78615806e..cf812c0a1 100644 --- a/lua/neogit/buffers/refs_view/init.lua +++ b/lua/neogit/buffers/refs_view/init.lua @@ -9,6 +9,7 @@ local Watcher = require("neogit.watcher") local logger = require("neogit.logger") local a = require("plenary.async") local git = require("neogit.lib.git") +local event = require("neogit.lib.event") ---@class RefsViewBuffer ---@field buffer Buffer @@ -133,7 +134,7 @@ function M:open() p { commits = self.buffer.ui:get_commits_in_selection() } end), [popups.mapping_for("DiffPopup")] = popups.open("diff", function(p) - local items = self.buffer.ui:get_commits_in_selection() + local items = self.buffer.ui:get_ordered_commits_in_selection() p { section = { name = "log" }, item = { name = items }, @@ -329,7 +330,7 @@ function M:redraw() logger.debug("[REFS] Beginning redraw") self.buffer.ui:render(unpack(ui.RefsView(git.refs.list_parsed(), self.head))) - vim.api.nvim_exec_autocmds("User", { pattern = "NeogitRefsRefreshed", modeline = false }) + event.send("RefsRefreshed") logger.info("[REFS] Redraw complete") end diff --git a/lua/neogit/buffers/refs_view/ui.lua b/lua/neogit/buffers/refs_view/ui.lua index 462d8c84a..084660717 100644 --- a/lua/neogit/buffers/refs_view/ui.lua +++ b/lua/neogit/buffers/refs_view/ui.lua @@ -41,13 +41,18 @@ local function Cherries(ref, head) end local function Ref(ref) - return row { + local ref_content = { text.highlight("NeogitGraphBoldPurple")(ref.head and "@ " or " "), text.highlight(highlights[ref.type])(util.str_truncate(ref.name, 34), { align_right = 35 }), - text.highlight(highlights[ref.upstream_status])(ref.upstream_name), - text(ref.upstream_name ~= "" and " " or ""), text(ref.subject), } + + if ref.upstream_name ~= "" then + table.insert(ref_content, 3, text.highlight(highlights[ref.upstream_status])(ref.upstream_name)) + table.insert(ref_content, 4, text(" ")) + end + + return row(ref_content) end local function section(refs, heading, head) diff --git a/lua/neogit/buffers/stash_list_view/init.lua b/lua/neogit/buffers/stash_list_view/init.lua index fed4d67e7..95eb6b484 100644 --- a/lua/neogit/buffers/stash_list_view/init.lua +++ b/lua/neogit/buffers/stash_list_view/init.lua @@ -97,7 +97,7 @@ function M:open() [popups.mapping_for("DiffPopup")] = popups.open("diff", function(p) local items = self.buffer.ui:get_commits_in_selection() p { - section = { name = "log" }, + section = { name = "stashes" }, item = { name = items }, } end), @@ -166,7 +166,7 @@ function M:open() [popups.mapping_for("DiffPopup")] = popups.open("diff", function(p) local item = self.buffer.ui:get_commit_under_cursor() p { - section = { name = "log" }, + section = { name = "stashes" }, item = { name = item }, } end), diff --git a/lua/neogit/buffers/status/actions.lua b/lua/neogit/buffers/status/actions.lua index db7b6d560..119fb2fee 100644 --- a/lua/neogit/buffers/status/actions.lua +++ b/lua/neogit/buffers/status/actions.lua @@ -31,12 +31,16 @@ local function cleanup_dir(dir) fn.delete(dir, "rf") end -local function cleanup_items(...) +---@param items StatusItem[] +local function cleanup_items(items) if vim.in_fast_event() then a.util.scheduler() end - for _, item in ipairs { ... } do + for _, item in ipairs(items) do + logger.trace("[cleanup_items()] Cleaning " .. vim.inspect(item.name)) + assert(item.name, "cleanup_items() - item must have a name") + local bufnr = fn.bufnr(item.name) if bufnr > 0 then api.nvim_buf_delete(bufnr, { force = false }) @@ -122,7 +126,8 @@ M.v_discard = function(self) for _, hunk in ipairs(hunks) do table.insert(invalidated_diffs, "*:" .. item.name) table.insert(patches, function() - local patch = git.index.generate_patch(item, hunk, hunk.from, hunk.to, true) + local patch = + git.index.generate_patch(hunk, { from = hunk.from, to = hunk.to, reverse = true }) logger.debug(("Discarding Patch: %s"):format(patch)) @@ -174,7 +179,7 @@ M.v_discard = function(self) end if #untracked_files > 0 then - cleanup_items(unpack(untracked_files)) + cleanup_items(untracked_files) end if #unstaged_files > 0 then @@ -184,10 +189,10 @@ M.v_discard = function(self) end if #new_files > 0 then - git.index.reset(util.map(unstaged_files, function(item) + git.index.reset(util.map(new_files, function(item) return item.escaped_path end)) - cleanup_items(unpack(new_files)) + cleanup_items(new_files) end if #staged_files_modified > 0 then @@ -233,7 +238,7 @@ M.v_stage = function(self) if #hunks > 0 then for _, hunk in ipairs(hunks) do - table.insert(patches, git.index.generate_patch(item, hunk, hunk.from, hunk.to)) + table.insert(patches, git.index.generate_patch(hunk.hunk, { from = hunk.from, to = hunk.to })) end else if section.name == "unstaged" then @@ -283,7 +288,10 @@ M.v_unstage = function(self) if #hunks > 0 then for _, hunk in ipairs(hunks) do - table.insert(patches, git.index.generate_patch(item, hunk, hunk.from, hunk.to, true)) + table.insert( + patches, + git.index.generate_patch(hunk, { from = hunk.from, to = hunk.to, reverse = true }) + ) end else table.insert(files, item.escaped_path) @@ -499,6 +507,40 @@ M.n_toggle = function(self) end end +---@param self StatusBuffer +M.n_open_fold = function(self) + return function() + local fold = self.buffer.ui:get_fold_under_cursor() + if fold then + if fold.options.on_open then + fold.options.on_open(fold, self.buffer.ui) + else + local start, _ = fold:row_range_abs() + local ok, _ = pcall(vim.cmd, "normal! zo") + if ok then + self.buffer:move_cursor(start) + fold.options.folded = false + end + end + end + end +end + +---@param self StatusBuffer +M.n_close_fold = function(self) + return function() + local fold = self.buffer.ui:get_fold_under_cursor() + if fold then + local start, _ = fold:row_range_abs() + local ok, _ = pcall(vim.cmd, "normal! zc") + if ok then + self.buffer:move_cursor(start) + fold.options.folded = true + end + end + end +end + ---@param self StatusBuffer M.n_close = function(self) return require("neogit.lib.ui.helpers").close_topmost(self) @@ -685,7 +727,7 @@ M.n_discard = function(self) if mode == "all" then message = ("Discard %q?"):format(selection.item.name) action = function() - cleanup_items(selection.item) + cleanup_items { selection.item } end else message = ("Recursively discard %q?"):format(selection.item.name) @@ -717,7 +759,7 @@ M.n_discard = function(self) action = function() if selection.item.mode == "A" then git.index.reset { selection.item.escaped_path } - cleanup_items(selection.item) + cleanup_items { selection.item } else git.index.checkout { selection.item.name } end @@ -748,14 +790,14 @@ M.n_discard = function(self) action = function() if selection.item.mode == "N" then git.index.reset { selection.item.escaped_path } - cleanup_items(selection.item) + cleanup_items { selection.item } elseif selection.item.mode == "M" then git.index.reset { selection.item.escaped_path } git.index.checkout { selection.item.escaped_path } elseif selection.item.mode == "R" then git.index.reset_HEAD(selection.item.name, selection.item.original_name) git.index.checkout { selection.item.original_name } - cleanup_items(selection.item) + cleanup_items { selection.item } elseif selection.item.mode == "D" then git.index.reset_HEAD(selection.item.escaped_path) git.index.checkout { selection.item.escaped_path } @@ -783,7 +825,7 @@ M.n_discard = function(self) local hunk = self.buffer.ui:item_hunks(selection.item, selection.first_line, selection.last_line, false)[1] - local patch = git.index.generate_patch(selection.item, hunk, hunk.from, hunk.to, true) + local patch = git.index.generate_patch(hunk, { reverse = true }) if section == "untracked" then message = "Discard hunk?" @@ -808,7 +850,7 @@ M.n_discard = function(self) if section == "untracked" then message = ("Discard %s files?"):format(#selection.section.items) action = function() - cleanup_items(unpack(selection.section.items)) + cleanup_items(selection.section.items) end refresh = { update_diffs = { "untracked:*" } } elseif section == "unstaged" then @@ -841,7 +883,7 @@ M.n_discard = function(self) for _, item in ipairs(selection.section.items) do if item.mode == "N" or item.mode == "A" then - table.insert(new_files, item.escaped_path) + table.insert(new_files, item) elseif item.mode == "M" then table.insert(staged_files_modified, item.escaped_path) elseif item.mode == "R" then @@ -854,9 +896,10 @@ M.n_discard = function(self) end if #new_files > 0 then - -- ensure the file is deleted - git.index.reset(new_files) - cleanup_items(unpack(new_files)) + git.index.reset(util.map(new_files, function(item) + return item.escaped_path + end)) + cleanup_items(new_files) end if #staged_files_modified > 0 then @@ -1072,10 +1115,6 @@ M.n_stage = function(self) if not git.merge.is_conflicted(selection.item.name) then git.status.stage { selection.item.name } self:dispatch_refresh({ update_diffs = { "*:" .. selection.item.name } }, "n_stage") - - if not git.merge.any_conflicted() then - popups.open("merge")() - end end end, }, @@ -1093,7 +1132,7 @@ M.n_stage = function(self) local item = self.buffer.ui:get_item_under_cursor() assert(item, "Item cannot be nil") - local patch = git.index.generate_patch(item, stagable.hunk, stagable.hunk.from, stagable.hunk.to) + local patch = git.index.generate_patch(stagable.hunk) git.index.apply(patch, { cached = true }) self:dispatch_refresh({ update_diffs = { "*:" .. item.name } }, "n_stage") elseif stagable.filename then @@ -1167,8 +1206,10 @@ M.n_unstage = function(self) if unstagable.hunk then local item = self.buffer.ui:get_item_under_cursor() assert(item, "Item cannot be nil") - local patch = - git.index.generate_patch(item, unstagable.hunk, unstagable.hunk.from, unstagable.hunk.to, true) + local patch = git.index.generate_patch( + unstagable.hunk, + { from = unstagable.hunk.from, to = unstagable.hunk.to, reverse = true } + ) git.index.apply(patch, { cached = true, reverse = true }) self:dispatch_refresh({ update_diffs = { "*:" .. item.name } }, "n_unstage") diff --git a/lua/neogit/buffers/status/init.lua b/lua/neogit/buffers/status/init.lua index fbfb97ab7..b56db5d73 100644 --- a/lua/neogit/buffers/status/init.lua +++ b/lua/neogit/buffers/status/init.lua @@ -6,8 +6,7 @@ local git = require("neogit.lib.git") local Watcher = require("neogit.watcher") local a = require("plenary.async") local logger = require("neogit.logger") -- TODO: Add logging - -local api = vim.api +local event = require("neogit.lib.event") ---@class Semaphore ---@field permits number @@ -147,6 +146,8 @@ function M:open(kind) [mappings["Untrack"]] = self:_action("n_untrack"), [mappings["Rename"]] = self:_action("n_rename"), [mappings["Toggle"]] = self:_action("n_toggle"), + [mappings["OpenFold"]] = self:_action("n_open_fold"), + [mappings["CloseFold"]] = self:_action("n_close_fold"), [mappings["Close"]] = self:_action("n_close"), [mappings["OpenOrScrollDown"]] = self:_action("n_open_or_scroll_down"), [mappings["OpenOrScrollUp"]] = self:_action("n_open_or_scroll_up"), @@ -213,33 +214,13 @@ function M:open(kind) buffer:move_cursor(buffer.ui:first_section().first) end, user_autocmds = { - ["NeogitPushComplete"] = function() - self:dispatch_refresh(nil, "push_complete") - end, - ["NeogitPullComplete"] = function() - self:dispatch_refresh(nil, "pull_complete") - end, - ["NeogitFetchComplete"] = function() - self:dispatch_refresh(nil, "fetch_complete") - end, - ["NeogitRebase"] = function() - self:dispatch_refresh(nil, "rebase") - end, - ["NeogitMerge"] = function() - self:dispatch_refresh(nil, "merge") - end, - ["NeogitReset"] = function() - self:dispatch_refresh(nil, "reset_complete") - end, - ["NeogitStash"] = function() - self:dispatch_refresh(nil, "stash") - end, - ["NeogitRevertComplete"] = function() - self:dispatch_refresh(nil, "revert") - end, - ["NeogitCherryPick"] = function() - self:dispatch_refresh(nil, "cherry_pick") - end, + -- Resetting doesn't yield the correct repo state instantly, so we need to re-refresh after a few seconds + -- in order to show the user the correct state. + ["NeogitReset"] = self:deferred_refresh("reset"), + ["NeogitBranchReset"] = self:deferred_refresh("reset_branch"), + }, + autocmds = { + ["FocusGained"] = self:deferred_refresh("focused", 10), }, } @@ -296,7 +277,7 @@ function M:refresh(partial, reason) partial = partial, callback = function() self:redraw(cursor, view) - api.nvim_exec_autocmds("User", { pattern = "NeogitStatusRefreshed", modeline = false }) + event.send("StatusRefreshed") logger.info("[STATUS] Refresh complete") end, } @@ -313,18 +294,18 @@ function M:redraw(cursor, view) logger.debug("[STATUS] Rendering UI") self.buffer.ui:render(unpack(ui.Status(git.repo.state, self.config))) - if self.fold_state then + if self.fold_state and self.buffer then logger.debug("[STATUS] Restoring fold state") self.buffer.ui:set_fold_state(self.fold_state) self.fold_state = nil end - if self.cursor_state and self.view_state then + if self.cursor_state and self.view_state and self.buffer then logger.debug("[STATUS] Restoring cursor and view state") self.buffer:restore_view(self.view_state, self.cursor_state) self.view_state = nil self.cursor_state = nil - elseif cursor and view then + elseif cursor and view and self.buffer then self.buffer:restore_view(view, self.buffer.ui:resolve_cursor_location(cursor)) end end @@ -333,6 +314,17 @@ M.dispatch_refresh = a.void(function(self, partial, reason) self:refresh(partial, reason) end) +---@param reason string +---@param wait number? timeout in ms, or 2 seconds +---@return fun() +function M:deferred_refresh(reason, wait) + return function() + vim.defer_fn(function() + self:dispatch_refresh(nil, reason) + end, wait or 2000) + end +end + function M:reset() logger.debug("[STATUS] Resetting repo and refreshing - CWD: " .. vim.uv.cwd()) git.repo:reset() diff --git a/lua/neogit/buffers/status/ui.lua b/lua/neogit/buffers/status/ui.lua index 1efc135f6..a79acaf7c 100755 --- a/lua/neogit/buffers/status/ui.lua +++ b/lua/neogit/buffers/status/ui.lua @@ -175,6 +175,84 @@ local Section = Component.new(function(props) }) end) +local TreeSection = Component.new(function(props) + local count + if props.count then + count = { text(" ("), text.highlight("NeogitSectionHeaderCount")(#props.items), text(")") } + end + + local function appendRows(items) + return a.void(function(this, ui) + this.options.on_open = nil + this.options.folded = false + + ui.buf:with_locked_viewport(function() + for _, item in ipairs(items) do + if item.type == "group" then + local groupPath = item.name + local indent = string.rep("│ ", item.indent_level - 1) + item.name = vim.fn.fnamemodify(item.name, ":t") + this:append(col.tag("Item")({ + row { + text.highlight("NeogitSubtleText")(indent), + text.highlight("NeogitFolderPath")(groupPath), + }, + }, { + foldable = true, + folded = true, + on_open = appendRows(item.children), + context = true, + id = groupPath, + yankable = groupPath, + filename = groupPath, + item = nil, + })) + else + this:append(props.render(item.content)) + end + end + ui:update() + end) + end) + end + + local sections = {} + local groups = util.groupByFilePath(props.items) + for _, item in ipairs(groups) do + local groupPath = item.name + local section + if item.type == "group" then + section = col.tag("Item")({ + row { + text.highlight("NeogitFolderPath")(groupPath), + }, + }, { + foldable = true, + folded = true, + on_open = appendRows(item.children), + context = true, + id = groupPath, + yankable = groupPath, + filename = groupPath, + item = nil, + }) + else + section = props.render(item.content) + end + table.insert(sections, section) + end + return col.tag("Section")({ + row(util.merge(props.title, count or {})), + col(sections), + EmptyLine(), + }, { + foldable = true, + folded = props.folded, + section = props.name, + id = props.name, + }) +end) + local SequencerSection = Component.new(function(props) return col.tag("Section")({ row(util.merge(props.title)), @@ -242,6 +320,13 @@ local SectionItemFile = function(section, config) end) end + local indent = "" + if config.status.tree_view then + local indent_level = #vim.split(item.name, "/", { plain = true }) - 1 + indent = string.rep("│ ", indent_level) + item.name = vim.fn.fnamemodify(item.name, ":t") + end + local mode = config.status.mode_text[item.mode] local mode_text if mode == "" then @@ -291,6 +376,7 @@ local SectionItemFile = function(section, config) return col.tag("Item")({ row { + text.highlight("NeogitSubtleText")(indent), text.highlight(highlight)(mode_text), text(name), text.highlight("NeogitSubtleText")(unmerged_types[item.mode] or ""), @@ -518,6 +604,10 @@ function M.Status(state, config) local show_recent = #state.recent.items > 0 and not config.sections.recent.hidden + local ChangesSection = config.status.tree_view + and TreeSection + or Section + return { List { items = { @@ -612,7 +702,7 @@ function M.Status(state, config) folded = config.sections.bisect.folded, name = "bisect", }, - show_untracked and Section { + show_untracked and ChangesSection { title = SectionTitle { title = "Untracked files", highlight = "NeogitUntrackedfiles" }, count = true, render = SectionItemFile("untracked", config), @@ -620,7 +710,7 @@ function M.Status(state, config) folded = config.sections.untracked.folded, name = "untracked", }, - show_unstaged and Section { + show_unstaged and ChangesSection { title = SectionTitle { title = "Unstaged changes", highlight = "NeogitUnstagedchanges" }, count = true, render = SectionItemFile("unstaged", config), @@ -628,7 +718,7 @@ function M.Status(state, config) folded = config.sections.unstaged.folded, name = "unstaged", }, - show_staged and Section { + show_staged and ChangesSection { title = SectionTitle { title = "Staged changes", highlight = "NeogitStagedchanges" }, count = true, render = SectionItemFile("staged", config), diff --git a/lua/neogit/client.lua b/lua/neogit/client.lua index 37ea05d8c..177bbba0e 100644 --- a/lua/neogit/client.lua +++ b/lua/neogit/client.lua @@ -144,7 +144,7 @@ function M.wrap(cmd, opts) a.util.scheduler() logger.debug("[CLIENT] DONE editor command") - if result.code == 0 then + if result:success() then if opts.msg.success then notification.info(opts.msg.success, { dismiss = true }) end diff --git a/lua/neogit/config.lua b/lua/neogit/config.lua index 311274be2..5e283825b 100644 --- a/lua/neogit/config.lua +++ b/lua/neogit/config.lua @@ -61,7 +61,7 @@ end function M.get_reversed_commit_editor_maps_I() return get_reversed_maps("commit_editor_I") end ---- + ---@return table function M.get_reversed_refs_view_maps() return get_reversed_maps("refs_view") @@ -185,6 +185,7 @@ end ---| "Close" ---| "Next" ---| "Previous" +---| "CopySelection" ---| "MultiselectToggleNext" ---| "MultiselectTogglePrevious" ---| "InsertCompletion" @@ -310,6 +311,7 @@ end ---@field HEAD_folded? boolean Whether or not this section should be open or closed by default ---@field mode_text? { [string]: string } The text to display for each mode ---@field show_head_commit_hash? boolean Show the commit hash for HEADs in the status buffer +---@field tree_view? boolean use a nested directory representation for changes (untracked, unstaged, staged) ---@class NeogitConfigMappings Consult the config file or documentation for values ---@field finder? { [string]: NeogitConfigMappingsFinder } A dictionary that uses finder commands to set multiple keybinds @@ -357,8 +359,6 @@ end ---@field reflog_view? NeogitConfigPopup Reflog view options ---@field refs_view? NeogitConfigPopup Refs view options ---@field merge_editor? NeogitConfigPopup Merge editor options ----@field description_editor? NeogitConfigPopup Merge editor options ----@field tag_editor? NeogitConfigPopup Tag editor options ---@field preview_buffer? NeogitConfigPopup Preview options ---@field popup? NeogitConfigPopup Set the default way of opening popups ---@field signs? NeogitConfigSigns Signs used for toggled regions @@ -428,6 +428,7 @@ function M.get_default_values() HEAD_padding = 10, HEAD_folded = false, mode_padding = 3, + tree_view = false, mode_text = { M = "modified", N = "new file", @@ -472,12 +473,6 @@ function M.get_default_values() merge_editor = { kind = "auto", }, - description_editor = { - kind = "auto", - }, - tag_editor = { - kind = "auto", - }, preview_buffer = { kind = "floating_console", }, @@ -597,6 +592,7 @@ function M.get_default_values() [""] = "Next", [""] = "Previous", [""] = "InsertCompletion", + [""] = "CopySelection", [""] = "MultiselectToggleNext", [""] = "MultiselectTogglePrevious", [""] = "NOP", @@ -643,6 +639,10 @@ function M.get_default_values() ["4"] = "Depth4", ["Q"] = "Command", [""] = "Toggle", + ["za"] = "Toggle", + ["zo"] = "OpenFold", + ["zC"] = "Depth1", + ["zO"] = "Depth4", ["x"] = "Discard", ["s"] = "Stage", ["S"] = "StageUnstaged", diff --git a/lua/neogit/integrations/diffview.lua b/lua/neogit/integrations/diffview.lua index ef66bae39..13fbe2db3 100644 --- a/lua/neogit/integrations/diffview.lua +++ b/lua/neogit/integrations/diffview.lua @@ -47,6 +47,7 @@ local function get_local_diff_view(section_name, item_name, opts) selected = (item_name and item.name == item_name) or (not item_name and idx == 1), } + -- restrict diff to only a particular section if opts.only then if (item_name and file.selected) or (not item_name and section_name == kind) then table.insert(files[kind], file) @@ -94,7 +95,7 @@ local function get_local_diff_view(section_name, item_name, opts) end ---@param section_name string ----@param item_name string|nil +---@param item_name string|string[]|nil ---@param opts table|nil function M.open(section_name, item_name, opts) opts = opts or {} @@ -110,25 +111,21 @@ function M.open(section_name, item_name, opts) local view -- selene: allow(if_same_then_else) - if section_name == "recent" or section_name == "unmerged" or section_name == "log" then + if + (section_name == "recent" or section_name == "log" or (section_name and section_name:match("unmerged$"))) + and item_name + then local range if type(item_name) == "table" then range = string.format("%s..%s", item_name[1], item_name[#item_name]) - elseif item_name ~= nil then - range = string.format("%s^!", item_name:match("[a-f0-9]+")) else - return + range = string.format("%s^!", item_name:match("[a-f0-9]+")) end view = dv_lib.diffview_open(dv_utils.tbl_pack(range)) - elseif section_name == "range" then - local range = item_name - view = dv_lib.diffview_open(dv_utils.tbl_pack(range)) - elseif section_name == "stashes" then - assert(item_name, "No item name for stash!") - local stash_id = item_name:match("stash@{%d+}") - view = dv_lib.diffview_open(dv_utils.tbl_pack(stash_id .. "^!")) - elseif section_name == "commit" then + elseif section_name == "range" and item_name then + view = dv_lib.diffview_open(dv_utils.tbl_pack(item_name)) + elseif (section_name == "stashes" or section_name == "commit") and item_name then view = dv_lib.diffview_open(dv_utils.tbl_pack(item_name .. "^!")) elseif section_name == "conflict" and item_name then view = dv_lib.diffview_open(dv_utils.tbl_pack("--selected-file=" .. item_name)) diff --git a/lua/neogit/lib/buffer.lua b/lua/neogit/lib/buffer.lua index b0db4f880..e63ebb707 100644 --- a/lua/neogit/lib/buffer.lua +++ b/lua/neogit/lib/buffer.lua @@ -430,7 +430,12 @@ end function Buffer:set_buffer_option(name, value) if self.handle ~= nil then - api.nvim_set_option_value(name, value, { scope = "local", buf = self.handle }) + -- TODO: Remove this at some point. Nvim 0.10 throws an error if using both buf and scope + if vim.fn.has("nvim-0.11") == 1 then + api.nvim_set_option_value(name, value, { scope = "local", buf = self.handle }) + else + api.nvim_set_option_value(name, value, { buf = self.handle }) + end end end @@ -452,7 +457,7 @@ end function Buffer:add_highlight(line, col_start, col_end, name, namespace) local ns_id = self:get_namespace_id(namespace) if ns_id then - api.nvim_buf_add_highlight(self.handle, ns_id, name, line, col_start, col_end) + vim.hl.range(self.handle, ns_id, name, { line, col_start }, { line, col_end }) end end @@ -533,10 +538,12 @@ function Buffer:call(f, ...) end function Buffer:win_call(f, ...) - local args = { ... } - api.nvim_win_call(self.win_handle, function() - f(unpack(args)) - end) + if self.win_handle and api.nvim_win_is_valid(self.win_handle) then + local args = { ... } + api.nvim_win_call(self.win_handle, function() + f(unpack(args)) + end) + end end function Buffer:chan_send(data) diff --git a/lua/neogit/lib/event.lua b/lua/neogit/lib/event.lua new file mode 100644 index 000000000..95ae65481 --- /dev/null +++ b/lua/neogit/lib/event.lua @@ -0,0 +1,15 @@ +local M = {} + +---@param name string +---@param data table? +function M.send(name, data) + assert(name, "event must have name") + + vim.api.nvim_exec_autocmds("User", { + pattern = "Neogit" .. name, + modeline = false, + data = data, + }) +end + +return M diff --git a/lua/neogit/lib/finder.lua b/lua/neogit/lib/finder.lua index b24f9d47d..95c34c1e3 100644 --- a/lua/neogit/lib/finder.lua +++ b/lua/neogit/lib/finder.lua @@ -9,6 +9,13 @@ local function refocus_status_buffer() end end +local copy_selection = function() + local selection = require("telescope.actions.state").get_selected_entry() + if selection ~= nil then + vim.cmd.let(("@+=%q"):format(selection[1])) + end +end + local function telescope_mappings(on_select, allow_multi, refocus_status) local action_state = require("telescope.actions.state") local actions = require("telescope.actions") @@ -85,6 +92,7 @@ local function telescope_mappings(on_select, allow_multi, refocus_status) ["InsertCompletion"] = completion_action, ["Next"] = actions.move_selection_next, ["Previous"] = actions.move_selection_previous, + ["CopySelection"] = copy_selection, ["NOP"] = actions.nop, ["MultiselectToggleNext"] = actions.toggle_selection + actions.move_selection_worse, ["MultiselectTogglePrevious"] = actions.toggle_selection + actions.move_selection_better, @@ -160,19 +168,26 @@ end ---@param on_select fun(item: any|nil) ---@param allow_multi boolean ---@param refocus_status boolean -local function snacks_actions(on_select, allow_multi, refocus_status) - local function refresh(picker) - picker:close() +local function snacks_confirm(on_select, allow_multi, refocus_status) + local completed = false + local function complete(selection) + if completed then + return + end + on_select(selection) + completed = true if refocus_status then refocus_status_buffer() end end - local function confirm(picker, item) local selection = {} local picker_selected = picker:selected { fallback = true } - if #picker_selected > 1 then + if #picker_selected == 0 then + complete(nil) + picker:close() + elseif #picker_selected > 1 then for _, item in ipairs(picker_selected) do table.insert(selection, item.text) end @@ -190,18 +205,16 @@ local function snacks_actions(on_select, allow_multi, refocus_status) end if selection and selection[1] and selection[1] ~= "" then - on_select(allow_multi and selection or selection[1]) + complete(allow_multi and selection or selection[1]) + picker:close() end - - refresh(picker) end - local function close(picker) - on_select(nil) - refresh(picker) + local function on_close() + complete(nil) end - return { confirm = confirm, close = close } + return confirm, on_close end --- Utility function to map finder opts to fzf @@ -335,6 +348,7 @@ function Finder:find(on_select) mini_pick.start { source = { items = self.entries, choose = on_select } } elseif config.check_integration("snacks") then local snacks_picker = require("snacks.picker") + local confirm, on_close = snacks_confirm(on_select, self.opts.allow_multi, self.opts.refocus_status) snacks_picker.pick(nil, { title = "Neogit", prompt = string.format("%s > ", self.opts.prompt_prefix), @@ -346,7 +360,8 @@ function Finder:find(on_select) height = self.opts.layout_config.height, border = self.opts.border and "rounded" or "none", }, - actions = snacks_actions(on_select, self.opts.allow_multi, self.opts.refocus_status), + confirm = confirm, + on_close = on_close, }) else vim.ui.select(self.entries, { diff --git a/lua/neogit/lib/git/bisect.lua b/lua/neogit/lib/git/bisect.lua index c552241ef..9aa9f8f50 100644 --- a/lua/neogit/lib/git/bisect.lua +++ b/lua/neogit/lib/git/bisect.lua @@ -1,18 +1,15 @@ local git = require("neogit.lib.git") +local event = require("neogit.lib.event") ---@class NeogitGitBisect local M = {} -local function fire_bisect_event(data) - vim.api.nvim_exec_autocmds("User", { pattern = "NeogitBisect", modeline = false, data = data }) -end - ---@param cmd string local function bisect(cmd) local result = git.cli.bisect.args(cmd).call { long = true } - if result.code == 0 then - fire_bisect_event { type = cmd } + if result:success() then + event.send("Bisect", { type = cmd }) end end @@ -31,8 +28,8 @@ function M.start(bad_revision, good_revision, args) local result = git.cli.bisect.args("start").arg_list(args).args(bad_revision, good_revision).call { long = true } - if result.code == 0 then - fire_bisect_event { type = "start" } + if result:success() then + event.send("Bisect", { type = "start" }) end end @@ -80,7 +77,7 @@ M.register = function(meta) finished = action == "first bad commit" if finished then - fire_bisect_event { type = "finished", oid = oid } + event.send("Bisect", { type = "finished", oid = oid }) end ---@type BisectItem diff --git a/lua/neogit/lib/git/branch.lua b/lua/neogit/lib/git/branch.lua index 8eb80efa3..007d17ec7 100644 --- a/lua/neogit/lib/git/branch.lua +++ b/lua/neogit/lib/git/branch.lua @@ -90,8 +90,9 @@ end ---@param name string ---@param args? string[] +---@return ProcessResult function M.track(name, args) - git.cli.checkout.track(name).arg_list(args or {}).call { await = true } + return git.cli.checkout.track(name).arg_list(args or {}).call { await = true } end ---@param include_current? boolean @@ -143,17 +144,17 @@ function M.exists(branch) .args(string.format("refs/heads/%s", branch)) .call { hidden = true, ignore_error = true } - return result.code == 0 + return result:success() end ---Determine if a branch name ("origin/master", "fix/bug-1000", etc) ---is a remote branch or a local branch ---@param ref string ----@return nil|string remote +---@return string remote ---@return string branch function M.parse_remote_branch(ref) if M.exists(ref) then - return nil, ref + return ".", ref end return ref:match("^([^/]*)/(.*)$") @@ -163,7 +164,7 @@ end ---@param base_branch? string ---@return boolean function M.create(name, base_branch) - return git.cli.branch.args(name, base_branch).call({ await = true }).code == 0 + return git.cli.branch.args(name, base_branch).call({ await = true }):success() end ---@param name string @@ -181,7 +182,7 @@ function M.delete(name) result = git.cli.branch.delete.name(name).call { await = true } end - return result and result.code == 0 or false + return result and result:success() or false end ---Returns current branch name, or nil if detached HEAD @@ -232,6 +233,40 @@ function M.pushRemote_ref(branch) end end +---@return string|nil +function M.pushDefault() + local pushDefault = git.config.get("remote.pushDefault") + if pushDefault:is_set() then + return pushDefault:read() ---@type string + end +end + +---@param branch? string +---@return string|nil +function M.pushDefault_ref(branch) + branch = branch or M.current() + local pushDefault = M.pushDefault() + + if branch and pushDefault then + return string.format("%s/%s", pushDefault, branch) + end +end + +---@return string +function M.pushRemote_or_pushDefault_label() + local ref = M.pushRemote_ref() + if ref then + return ref + end + + local pushDefault = M.pushDefault() + if pushDefault then + return ("%s, creating it"):format(M.pushDefault_ref()) + end + + return "pushRemote, setting that" +end + ---@return string function M.pushRemote_label() return M.pushRemote_ref() or "pushRemote, setting that" @@ -279,7 +314,7 @@ function M.upstream(name) local result = git.cli["rev-parse"].symbolic_full_name.abbrev_ref(name .. "@{upstream}").call { ignore_error = true } - if result.code == 0 then + if result:success() then return result.stdout[1] end else diff --git a/lua/neogit/lib/git/cherry_pick.lua b/lua/neogit/lib/git/cherry_pick.lua index 0e64449b7..3d48e307e 100644 --- a/lua/neogit/lib/git/cherry_pick.lua +++ b/lua/neogit/lib/git/cherry_pick.lua @@ -2,14 +2,11 @@ local git = require("neogit.lib.git") local notification = require("neogit.lib.notification") local util = require("neogit.lib.util") local client = require("neogit.client") +local event = require("neogit.lib.event") ---@class NeogitGitCherryPick local M = {} -local function fire_cherrypick_event(data) - vim.api.nvim_exec_autocmds("User", { pattern = "NeogitCherryPick", modeline = false, data = data }) -end - ---@param commits string[] ---@param args string[] ---@return boolean @@ -23,11 +20,11 @@ function M.pick(commits, args) result = cmd.call { await = true } end - if result.code ~= 0 then + if result:failure() then notification.error("Cherry Pick failed. Resolve conflicts before continuing") return false else - fire_cherrypick_event { commits = commits } + event.send("CherryPick", { commits = commits }) return true end end @@ -40,10 +37,10 @@ function M.apply(commits, args) end) local result = git.cli["cherry-pick"].no_commit.arg_list(util.merge(args, commits)).call { await = true } - if result.code ~= 0 then + if result:failure() then notification.error("Cherry Pick failed. Resolve conflicts before continuing") else - fire_cherrypick_event { commits = commits } + event.send("CherryPick", { commits = commits }) end end @@ -94,7 +91,7 @@ function M.move(commits, src, dst, args, start, checkout_dst) local result = git.cli.rebase.interactive.args(keep).in_pty(true).env({ GIT_SEQUENCE_EDITOR = editor }).call() - if result.code ~= 0 then + if result:failure() then return notification.error("Picking failed - Fix things manually before continuing.") end diff --git a/lua/neogit/lib/git/diff.lua b/lua/neogit/lib/git/diff.lua index 15bea3182..64d3be1fa 100644 --- a/lua/neogit/lib/git/diff.lua +++ b/lua/neogit/lib/git/diff.lua @@ -24,12 +24,14 @@ local sha256 = vim.fn.sha256 ---@field deletions number --- ---@class Hunk +---@field file string ---@field index_from number ---@field index_len number ---@field diff_from number ---@field diff_to number ---@field first number First line number in buffer ---@field last number Last line number in buffer +---@field lines string[] --- ---@class DiffStagedStats ---@field summary string @@ -94,7 +96,7 @@ end ---@return string local function build_file(header, kind) if kind == "modified" then - return header[3]:match("%-%-%- a/(.*)") + return header[3]:match("%-%-%- ./(.*)") elseif kind == "renamed" then return ("%s -> %s"):format(header[3]:match("rename from (.*)"), header[4]:match("rename to (.*)")) elseif kind == "new file" then @@ -224,6 +226,11 @@ local function parse_diff(raw_diff, raw_stats) local file = build_file(header, kind) local stats = parse_diff_stats(raw_stats or {}) + util.map(hunks, function(hunk) + hunk.file = file + return hunk + end) + return { ---@type Diff kind = kind, lines = lines, diff --git a/lua/neogit/lib/git/files.lua b/lua/neogit/lib/git/files.lua index e053573ab..af652f7d9 100644 --- a/lua/neogit/lib/git/files.lua +++ b/lua/neogit/lib/git/files.lua @@ -55,20 +55,20 @@ end ---@param path string ---@return boolean function M.is_tracked(path) - return git.cli["ls-files"].error_unmatch.files(path).call({ hidden = true, ignore_error = true }).code == 0 + return git.cli["ls-files"].error_unmatch.files(path).call({ hidden = true, ignore_error = true }):success() end ---@param paths string[] ---@return boolean function M.untrack(paths) - return git.cli.rm.cached.files(unpack(paths)).call({ hidden = true }).code == 0 + return git.cli.rm.cached.files(unpack(paths)).call({ hidden = true }):success() end ---@param from string ---@param to string ---@return boolean function M.move(from, to) - return git.cli.mv.args(from, to).call().code == 0 + return git.cli.mv.args(from, to).call():success() end return M diff --git a/lua/neogit/lib/git/index.lua b/lua/neogit/lib/git/index.lua index 35b9c8cfe..354eceee1 100644 --- a/lua/neogit/lib/git/index.lua +++ b/lua/neogit/lib/git/index.lua @@ -6,58 +6,48 @@ local util = require("neogit.lib.util") local M = {} ---Generates a patch that can be applied to index ----@param item any ---@param hunk Hunk ----@param from number ----@param to number ----@param reverse boolean|nil +---@param opts table|nil ---@return string -function M.generate_patch(item, hunk, from, to, reverse) - reverse = reverse or false +function M.generate_patch(hunk, opts) + opts = opts or { reverse = false } - if not from and not to then - from = hunk.diff_from + 1 - to = hunk.diff_to - end + local reverse = opts.reverse + + local from = opts.from or 1 + local to = opts.to or (hunk.diff_to - hunk.diff_from) assert(from <= to, string.format("from must be less than or equal to to %d %d", from, to)) - if from > to then - from, to = to, from - end local diff_content = {} local len_start = hunk.index_len local len_offset = 0 - -- + 1 skips the hunk header, since we construct that manually afterwards - -- TODO: could use `hunk.lines` instead if this is only called with the `SelectedHunk` type - for k = hunk.diff_from + 1, hunk.diff_to do - local v = item.diff.lines[k] - local operand, line = v:match("^([+ -])(.*)") - + for k, line in pairs(hunk.lines) do + local operand, l = line:match("^([+ -])(.*)") if operand == "+" or operand == "-" then if from <= k and k <= to then len_offset = len_offset + (operand == "+" and 1 or -1) - table.insert(diff_content, v) + table.insert(diff_content, line) else -- If we want to apply the patch normally, we need to include every `-` line we skip as a normal line, -- since we want to keep that line. if not reverse then if operand == "-" then - table.insert(diff_content, " " .. line) + table.insert(diff_content, " " .. l) end -- If we want to apply the patch in reverse, we need to include every `+` line we skip as a normal line, since -- it's unchanged as far as the diff is concerned and should not be reversed. -- We also need to adapt the original line offset based on if we skip or not elseif reverse then if operand == "+" then - table.insert(diff_content, " " .. line) + table.insert(diff_content, " " .. l) end len_start = len_start + (operand == "-" and -1 or 1) end end else - table.insert(diff_content, v) + table.insert(diff_content, line) end end @@ -68,9 +58,9 @@ function M.generate_patch(item, hunk, from, to, reverse) ) local worktree_root = git.repo.worktree_root + assert(hunk.file, "hunk has no filepath") - assert(item.absolute_path, "Item is not a path") - local path = Path:new(item.absolute_path):make_relative(worktree_root) + local path = Path:new(hunk.file):make_relative(worktree_root) table.insert(diff_content, 1, string.format("+++ b/%s", path)) table.insert(diff_content, 1, string.format("--- a/%s", path)) @@ -168,9 +158,12 @@ end -- Capture state of index as reflog entry function M.create_backup() git.cli.add.update.call { hidden = true, await = true } - git.cli.commit.message("Hard reset backup").call { hidden = true, await = true, pty = true } - git.cli["update-ref"].args("refs/backups/" .. timestamp(), "HEAD").call { hidden = true, await = true } - git.cli.reset.hard.args("HEAD~1").call { hidden = true, await = true } + local result = + git.cli.commit.allow_empty.message("Hard reset backup").call { hidden = true, await = true, pty = true } + if result:success() then + git.cli["update-ref"].args("refs/backups/" .. timestamp(), "HEAD").call { hidden = true, await = true } + git.cli.reset.hard.args("HEAD~1").call { hidden = true, await = true } + end end return M diff --git a/lua/neogit/lib/git/log.lua b/lua/neogit/lib/git/log.lua index b9fef523e..9e15a486e 100644 --- a/lua/neogit/lib/git/log.lua +++ b/lua/neogit/lib/git/log.lua @@ -403,7 +403,8 @@ end) function M.is_ancestor(ancestor, descendant) return git.cli["merge-base"].is_ancestor .args(ancestor, descendant) - .call({ ignore_error = true, hidden = true }).code == 0 + .call({ ignore_error = true, hidden = true }) + :success() end ---Finds parent commit of a commit. If no parent exists, will return nil @@ -560,7 +561,7 @@ function M.decorate(oid) return oid else local decorated_ref = vim.split(result[1], ",")[1] - if decorated_ref:match("%->") then + if decorated_ref:match("%->") or decorated_ref:match("tag: ") then return oid else return decorated_ref diff --git a/lua/neogit/lib/git/merge.lua b/lua/neogit/lib/git/merge.lua index 1fdbcab00..b018bfde1 100644 --- a/lua/neogit/lib/git/merge.lua +++ b/lua/neogit/lib/git/merge.lua @@ -1,6 +1,7 @@ local client = require("neogit.client") local git = require("neogit.lib.git") local notification = require("neogit.lib.notification") +local event = require("neogit.lib.event") ---@class NeogitGitMerge local M = {} @@ -9,18 +10,14 @@ local function merge_command(cmd) return cmd.env(client.get_envs_git_editor()).call { pty = true } end -local function fire_merge_event(data) - vim.api.nvim_exec_autocmds("User", { pattern = "NeogitMerge", modeline = false, data = data }) -end - function M.merge(branch, args) local result = merge_command(git.cli.merge.args(branch).arg_list(args)) - if result.code ~= 0 then + if result:failure() then notification.error("Merging failed. Resolve conflicts before continuing") - fire_merge_event { branch = branch, args = args, status = "conflict" } + event.send("Merge", { branch = branch, args = args, status = "conflict" }) else notification.info("Merged '" .. branch .. "' into '" .. git.branch.current() .. "'") - fire_merge_event { branch = branch, args = args, status = "ok" } + event.send("Merge", { branch = branch, args = args, status = "ok" }) end end @@ -40,12 +37,12 @@ end ---@param path string filepath to check for conflict markers ---@return boolean function M.is_conflicted(path) - return git.cli.diff.check.files(path).call().code ~= 0 + return git.cli.diff.check.files(path).call():failure() end ---@return boolean function M.any_conflicted() - return git.cli.diff.check.call().code ~= 0 + return git.cli.diff.check.call():failure() end ---@class MergeItem diff --git a/lua/neogit/lib/git/push.lua b/lua/neogit/lib/git/push.lua index 6b2137701..2041add09 100644 --- a/lua/neogit/lib/git/push.lua +++ b/lua/neogit/lib/git/push.lua @@ -5,8 +5,8 @@ local util = require("neogit.lib.util") local M = {} ---Pushes to the remote and handles password questions ----@param remote string ----@param branch string +---@param remote string? +---@param branch string? ---@param args string[] ---@return ProcessResult function M.push_interactive(remote, branch, args) diff --git a/lua/neogit/lib/git/rebase.lua b/lua/neogit/lib/git/rebase.lua index 942bd5680..b5890e7b6 100644 --- a/lua/neogit/lib/git/rebase.lua +++ b/lua/neogit/lib/git/rebase.lua @@ -2,14 +2,11 @@ local logger = require("neogit.logger") local git = require("neogit.lib.git") local client = require("neogit.client") local notification = require("neogit.lib.notification") +local event = require("neogit.lib.event") ---@class NeogitGitRebase local M = {} -local function fire_rebase_event(data) - vim.api.nvim_exec_autocmds("User", { pattern = "NeogitRebase", modeline = false, data = data }) -end - local function rebase_command(cmd) return cmd.env(client.get_envs_git_editor()).call { long = true, pty = true } end @@ -21,14 +18,14 @@ end function M.instantly(commit, args) local result = git.cli.rebase.interactive.autostash.autosquash .commit(commit) - .env({ GIT_SEQUENCE_EDITOR = ":" }) + .env({ GIT_SEQUENCE_EDITOR = ":", GIT_EDITOR = ":" }) .arg_list(args or {}) .call { long = true, pty = true } - if result.code ~= 0 then - fire_rebase_event { commit = commit, status = "failed" } + if result:failure() then + event.send("Rebase", { commit = commit, status = "failed" }) else - fire_rebase_event { commit = commit, status = "ok" } + event.send("Rebase", { commit = commit, status = "ok" }) end return result @@ -40,39 +37,39 @@ function M.rebase_interactive(commit, args) end local result = rebase_command(git.cli.rebase.interactive.arg_list(args).args(commit)) - if result.code ~= 0 then + if result:failure() then if result.stdout[1]:match("^hint: Waiting for your editor to close the file%.%.%. error") then notification.info("Rebase aborted") - fire_rebase_event { commit = commit, status = "aborted" } + event.send("Rebase", { commit = commit, status = "aborted" }) else notification.error("Rebasing failed. Resolve conflicts before continuing") - fire_rebase_event { commit = commit, status = "conflict" } + event.send("Rebase", { commit = commit, status = "conflict" }) end else notification.info("Rebased successfully") - fire_rebase_event { commit = commit, status = "ok" } + event.send("Rebase", { commit = commit, status = "ok" }) end end function M.onto_branch(branch, args) local result = rebase_command(git.cli.rebase.args(branch).arg_list(args)) - if result.code ~= 0 then + if result:failure() then notification.error("Rebasing failed. Resolve conflicts before continuing") - fire_rebase_event("conflict") + event.send("Rebase", { commit = branch, status = "conflict" }) else notification.info("Rebased onto '" .. branch .. "'") - fire_rebase_event("ok") + event.send("Rebase", { commit = branch, status = "ok" }) end end function M.onto(start, newbase, args) local result = rebase_command(git.cli.rebase.onto.args(newbase, start).arg_list(args)) - if result.code ~= 0 then + if result:failure() then notification.error("Rebasing failed. Resolve conflicts before continuing") - fire_rebase_event("conflict") + event.send("Rebase", { status = "conflict" }) else notification.info("Rebased onto '" .. newbase .. "'") - fire_rebase_event("ok") + event.send("Rebase", { commit = newbase, status = "ok" }) end end @@ -103,10 +100,10 @@ function M.modify(commit) .in_pty(true) .env({ GIT_SEQUENCE_EDITOR = editor }) .call() - if result.code ~= 0 then - return + + if result:success() then + event.send("Rebase", { commit = commit, status = "ok" }) end - fire_rebase_event { commit = commit, status = "ok" } end function M.drop(commit) @@ -117,10 +114,10 @@ function M.drop(commit) .in_pty(true) .env({ GIT_SEQUENCE_EDITOR = editor }) .call() - if result.code ~= 0 then - return + + if result:success() then + event.send("Rebase", { commit = commit, status = "ok" }) end - fire_rebase_event { commit = commit, status = "ok" } end function M.continue() @@ -144,7 +141,7 @@ end function M.merge_base_HEAD() local result = git.cli["merge-base"].args("HEAD", "HEAD@{upstream}").call { ignore_error = true, hidden = true } - if result.code == 0 then + if result:success() then return result.stdout[1] end end @@ -171,7 +168,7 @@ local function rev_name(oid) .args(oid) .call { hidden = true, ignore_error = true } - if result.code == 0 then + if result:success() then return result.stdout[1] else return oid @@ -225,13 +222,17 @@ function M.update_rebase_status(state) for line in done:iter() do if line:match("^[^#]") and line ~= "" then local oid = line:match("^%w+ (%x+)") or line:match("^fixup %-C (%x+)") - table.insert(state.rebase.items, { - action = line:match("^(%w+) "), - oid = oid, - abbreviated_commit = oid:sub(1, git.log.abbreviated_size()), - subject = line:match("^%w+ %x+ (.+)$"), - done = true, - }) + if oid then + table.insert(state.rebase.items, { + action = line:match("^(%w+) "), + oid = oid, + abbreviated_commit = oid:sub(1, git.log.abbreviated_size()), + subject = line:match("^%w+ %x+ (.+)$"), + done = true, + }) + else + logger.debug("[rebase status] No OID found on line '" .. line .. "'") + end end end end @@ -248,13 +249,15 @@ function M.update_rebase_status(state) for line in todo:iter() do if line:match("^[^#]") and line ~= "" then local oid = line:match("^%w+ (%x+)") - table.insert(state.rebase.items, { - done = false, - action = line:match("^(%w+) "), - oid = oid, - abbreviated_commit = oid:sub(1, git.log.abbreviated_size()), - subject = line:match("^%w+ %x+ (.+)$"), - }) + if oid then + table.insert(state.rebase.items, { + done = false, + action = line:match("^(%w+) "), + oid = oid, + abbreviated_commit = oid:sub(1, git.log.abbreviated_size()), + subject = line:match("^%w+ %x+ (.+)$"), + }) + end end end end diff --git a/lua/neogit/lib/git/remote.lua b/lua/neogit/lib/git/remote.lua index 0627b4d73..31bc633d1 100644 --- a/lua/neogit/lib/git/remote.lua +++ b/lua/neogit/lib/git/remote.lua @@ -28,7 +28,7 @@ end ---@param args string[] ---@return boolean function M.add(name, url, args) - return git.cli.remote.add.arg_list(args).args(name, url).call().code == 0 + return git.cli.remote.add.arg_list(args).args(name, url).call():success() end ---@param from string @@ -36,28 +36,28 @@ end ---@return boolean function M.rename(from, to) local result = git.cli.remote.rename.arg_list({ from, to }).call() - if result.code == 0 then + if result:success() then cleanup_push_variables(from, to) end - return result.code == 0 + return result:success() end ---@param name string ---@return boolean function M.remove(name) local result = git.cli.remote.rm.args(name).call() - if result.code == 0 then + if result:success() then cleanup_push_variables(name) end - return result.code == 0 + return result:success() end ---@param name string ---@return boolean function M.prune(name) - return git.cli.remote.prune.args(name).call().code == 0 + return git.cli.remote.prune.args(name).call():success() end ---@return string[] diff --git a/lua/neogit/lib/git/reset.lua b/lua/neogit/lib/git/reset.lua index 2834c82bb..c5dbd9017 100644 --- a/lua/neogit/lib/git/reset.lua +++ b/lua/neogit/lib/git/reset.lua @@ -1,87 +1,67 @@ -local notification = require("neogit.lib.notification") local git = require("neogit.lib.git") ---@class NeogitGitReset local M = {} -local function fire_reset_event(data) - vim.api.nvim_exec_autocmds("User", { pattern = "NeogitReset", modeline = false, data = data }) +---@param target string +---@return boolean +function M.mixed(target) + local result = git.cli.reset.mixed.args(target).call() + return result:success() end -function M.mixed(commit) - local result = git.cli.reset.mixed.args(commit).call { await = true } - if result.code ~= 0 then - notification.error("Reset Failed") - else - notification.info("Reset to " .. commit) - fire_reset_event { commit = commit, mode = "mixed" } - end +---@param target string +---@return boolean +function M.soft(target) + local result = git.cli.reset.soft.args(target).call() + return result:success() end -function M.soft(commit) - local result = git.cli.reset.soft.args(commit).call { await = true } - if result.code ~= 0 then - notification.error("Reset Failed") - else - notification.info("Reset to " .. commit) - fire_reset_event { commit = commit, mode = "soft" } - end -end - -function M.hard(commit) +---@param target string +---@return boolean +function M.hard(target) git.index.create_backup() - local result = git.cli.reset.hard.args(commit).call { await = true } - if result.code ~= 0 then - notification.error("Reset Failed") - else - notification.info("Reset to " .. commit) - fire_reset_event { commit = commit, mode = "hard" } - end + local result = git.cli.reset.hard.args(target).call() + return result:success() end -function M.keep(commit) - local result = git.cli.reset.keep.args(commit).call { await = true } - if result.code ~= 0 then - notification.error("Reset Failed") - else - notification.info("Reset to " .. commit) - fire_reset_event { commit = commit, mode = "keep" } - end +---@param target string +---@return boolean +function M.keep(target) + local result = git.cli.reset.keep.args(target).call() + return result:success() end -function M.index(commit) - local result = git.cli.reset.args(commit).files(".").call { await = true } - if result.code ~= 0 then - notification.error("Reset Failed") - else - notification.info("Reset to " .. commit) - fire_reset_event { commit = commit, mode = "index" } - end +---@param target string +---@return boolean +function M.index(target) + local result = git.cli.reset.args(target).files(".").call() + return result:success() end --- TODO: Worktree support --- "Reset the worktree to COMMIT. Keep the `HEAD' and index as-is." --- --- (magit-wip-commit-before-change nil " before reset") --- (magit-with-temp-index commit nil (magit-call-git "checkout-index" "--all" "--force")) --- (magit-wip-commit-after-apply nil " after reset") --- --- function M.worktree(commit) --- end +---@param target string revision to reset to +---@return boolean +function M.worktree(target) + local success = false + git.index.with_temp_index(target, function(index) + local result = git.cli["checkout-index"].all.force.env({ GIT_INDEX_FILE = index }).call() + success = result:success() + end) -function M.file(commit, files) - local result = git.cli.checkout.rev(commit).files(unpack(files)).call { await = true } - if result.code ~= 0 then - notification.error("Reset Failed") - else - fire_reset_event { commit = commit, mode = "files" } - if #files > 1 then - notification.info("Reset " .. #files .. " files") - else - notification.info("Reset " .. files[1]) - end + return success +end + +---@param target string +---@param files string[] +---@return boolean +function M.file(target, files) + local result = git.cli.checkout.rev(target).files(unpack(files)).call() + if result:failure() then + result = git.cli.reset.args(target).files(unpack(files)).call() end + + return result:success() end return M diff --git a/lua/neogit/lib/git/revert.lua b/lua/neogit/lib/git/revert.lua index 797ca36be..a6a3e608f 100644 --- a/lua/neogit/lib/git/revert.lua +++ b/lua/neogit/lib/git/revert.lua @@ -9,13 +9,18 @@ local M = {} ---@return boolean, string|nil function M.commits(commits, args) local result = git.cli.revert.no_commit.arg_list(util.merge(args, commits)).call { pty = true } - if result.code == 0 then + if result:success() then return true, "" else return false, result.stdout[1] end end +function M.hunk(hunk, _) + local patch = git.index.generate_patch(hunk, { reverse = true }) + git.index.apply(patch, { reverse = true }) +end + function M.continue() git.cli.revert.continue.no_edit.call { pty = true } end diff --git a/lua/neogit/lib/git/stash.lua b/lua/neogit/lib/git/stash.lua index b0325a38d..83a622825 100644 --- a/lua/neogit/lib/git/stash.lua +++ b/lua/neogit/lib/git/stash.lua @@ -2,22 +2,14 @@ local git = require("neogit.lib.git") local input = require("neogit.lib.input") local util = require("neogit.lib.util") local config = require("neogit.config") +local event = require("neogit.lib.event") ---@class NeogitGitStash local M = {} ----@param success boolean -local function fire_stash_event(success) - vim.api.nvim_exec_autocmds("User", { - pattern = "NeogitStash", - modeline = false, - data = { success = success }, - }) -end - function M.list_refs() local result = git.cli.reflog.show.format("%h").args("stash").call { ignore_error = true } - if result.code > 0 then + if result:failure() then return {} else return result.stdout @@ -27,51 +19,51 @@ end ---@param args string[] function M.stash_all(args) local result = git.cli.stash.push.files(".").arg_list(args).call() - fire_stash_event(result.code == 0) + event.send("Stash", { success = result:success() }) end function M.stash_index() local result = git.cli.stash.staged.call() - fire_stash_event(result.code == 0) + event.send("Stash", { success = result:success() }) end function M.stash_keep_index() local result = git.cli.stash.keep_index.files(".").call() - fire_stash_event(result.code == 0) + event.send("Stash", { success = result:success() }) end ---@param args string[] ---@param files string[] function M.push(args, files) local result = git.cli.stash.push.arg_list(args).files(unpack(files)).call() - fire_stash_event(result.code == 0) + event.send("Stash", { success = result:success() }) end function M.pop(stash) local result = git.cli.stash.apply.index.args(stash).call() - if result.code == 0 then + if result:success() then git.cli.stash.drop.args(stash).call() else git.cli.stash.apply.args(stash).call() end - fire_stash_event(result.code == 0) + event.send("Stash", { success = result:success() }) end function M.apply(stash) local result = git.cli.stash.apply.index.args(stash).call() - if result.code ~= 0 then + if result:failure() then git.cli.stash.apply.args(stash).call() end - fire_stash_event(result.code == 0) + event.send("Stash", { success = result:success() }) end function M.drop(stash) local result = git.cli.stash.drop.args(stash).call() - fire_stash_event(result.code == 0) + event.send("Stash", { success = result:success() }) end function M.list() @@ -79,7 +71,8 @@ function M.list() end function M.rename(stash) - local message = input.get_user_input("New name") + local current = git.log.message(stash) + local message = input.get_user_input("rename", { prepend = current }) if message then local oid = git.rev_parse.abbreviate_commit(stash) git.cli.stash.drop.args(stash).call() @@ -107,7 +100,6 @@ function M.register(meta) idx = idx, name = line, message = message, - oid = git.rev_parse.oid("stash@{" .. idx .. "}"), } -- These calls can be somewhat expensive, so lazy load them @@ -130,6 +122,9 @@ function M.register(meta) .call({ hidden = true }).stdout[1] return self.date + elseif key == "oid" then + self.oid = git.rev_parse.oid("stash@{" .. idx .. "}") + return self.oid end end, }) diff --git a/lua/neogit/lib/git/tag.lua b/lua/neogit/lib/git/tag.lua index 2b025a919..0bc1983e9 100644 --- a/lua/neogit/lib/git/tag.lua +++ b/lua/neogit/lib/git/tag.lua @@ -14,7 +14,7 @@ end ---@return boolean Successfully deleted function M.delete(tags) local result = git.cli.tag.delete.arg_list(tags).call { await = true } - return result.code == 0 + return result:success() end --- Show a list of tags under a selected ref diff --git a/lua/neogit/lib/git/worktree.lua b/lua/neogit/lib/git/worktree.lua index b4e96adf1..1095eb3b5 100644 --- a/lua/neogit/lib/git/worktree.lua +++ b/lua/neogit/lib/git/worktree.lua @@ -11,7 +11,7 @@ local M = {} ---@return boolean, string function M.add(ref, path, params) local result = git.cli.worktree.add.arg_list(params or {}).args(path, ref).call() - if result.code == 0 then + if result:success() then return true, "" else return false, result.stderr[#result.stderr] @@ -24,7 +24,7 @@ end ---@return boolean function M.move(worktree, destination) local result = git.cli.worktree.move.args(worktree, destination).call() - return result.code == 0 + return result:success() end ---Removes a worktree @@ -33,7 +33,7 @@ end ---@return boolean function M.remove(worktree, args) local result = git.cli.worktree.remove.args(worktree).arg_list(args or {}).call { ignore_error = true } - return result.code == 0 + return result:success() end ---@class Worktree diff --git a/lua/neogit/lib/hl.lua b/lua/neogit/lib/hl.lua index 483913a64..9abd81e6c 100644 --- a/lua/neogit/lib/hl.lua +++ b/lua/neogit/lib/hl.lua @@ -207,6 +207,7 @@ function M.setup(config) NeogitPopupOptionDisabled = { link = "NeogitSubtleText" }, NeogitPopupConfigKey = { fg = palette.purple }, NeogitPopupConfigEnabled = { link = "SpecialChar" }, + NeogitFolderPath = { bold = palette.bold }, NeogitPopupConfigDisabled = { link = "NeogitSubtleText" }, NeogitPopupActionKey = { fg = palette.purple }, NeogitPopupActionDisabled = { link = "NeogitSubtleText" }, diff --git a/lua/neogit/lib/input.lua b/lua/neogit/lib/input.lua index 5df738c6f..26a336180 100644 --- a/lua/neogit/lib/input.lua +++ b/lua/neogit/lib/input.lua @@ -1,14 +1,5 @@ local M = {} -local async = require("plenary.async") -local input = async.wrap(function(prompt, default, completion, callback) - vim.ui.input({ - prompt = prompt, - default = default, - completion = completion, - }, callback) -end, 4) - --- Provides the user with a confirmation ---@param msg string Prompt to use for confirmation ---@param options table|nil @@ -66,15 +57,23 @@ end function M.get_user_input(prompt, opts) opts = vim.tbl_extend("keep", opts or {}, { strip_spaces = false, separator = ": " }) + vim.fn.inputsave() + if opts.prepend then vim.defer_fn(function() vim.api.nvim_input(opts.prepend) end, 10) end - local result = input(("%s%s"):format(prompt, opts.separator), opts.default, opts.completion) + local status, result = pcall(vim.fn.input, { + prompt = ("%s%s"):format(prompt, opts.separator), + default = opts.default, + completion = opts.completion, + cancelreturn = opts.cancel, + }) - if result == "" or result == nil then + vim.fn.inputrestore() + if not status then return nil end @@ -82,6 +81,10 @@ function M.get_user_input(prompt, opts) result, _ = result:gsub("%s", "-") end + if result == "" then + return nil + end + return result end diff --git a/lua/neogit/lib/popup/builder.lua b/lua/neogit/lib/popup/builder.lua index cd2920425..aedbfbc03 100644 --- a/lua/neogit/lib/popup/builder.lua +++ b/lua/neogit/lib/popup/builder.lua @@ -425,6 +425,17 @@ function M:config_if(cond, key, name, options) return self end +---Inserts a blank slot +---@return self +function M:spacer() + table.insert(self.state.actions[#self.state.actions], { + keys = "", + description = "", + heading = "", + }) + return self +end + -- Adds an action to the popup ---@param keys string|string[] Key or list of keys for the user to press that runs the action ---@param description string Description of action in UI diff --git a/lua/neogit/lib/popup/init.lua b/lua/neogit/lib/popup/init.lua index e88bb6dd3..b8f927a61 100644 --- a/lua/neogit/lib/popup/init.lua +++ b/lua/neogit/lib/popup/init.lua @@ -64,6 +64,16 @@ function M:get_arguments() return flags end +---@param key string +---@return any|nil +function M:get_env(key) + if not self.state.env then + return nil + end + + return self.state.env[key] +end + -- Returns a table of key/value pairs, where the key is the name of the switch, and value is `true`, for all -- enabled arguments that are NOT for cli consumption (internal use only). ---@return table @@ -170,42 +180,26 @@ end ---@param value? string ---@return nil function M:set_option(option, value) - -- Prompt user to select from predetermined choices - if value then + if option.value and option.value ~= "" then -- Toggle option off when it's currently set + option.value = "" + elseif value then option.value = value elseif option.choices then - if not option.value or option.value == "" then - local eventignore = vim.o.eventignore - vim.o.eventignore = "WinLeave" - local choice = FuzzyFinderBuffer.new(option.choices):open_async { - prompt_prefix = option.description, - } - vim.o.eventignore = eventignore - - if choice then - option.value = choice - else - option.value = "" - end - else - option.value = "" - end + local eventignore = vim.o.eventignore + vim.o.eventignore = "WinLeave" + option.value = FuzzyFinderBuffer.new(option.choices):open_async { + prompt_prefix = option.description, + refocus_status = false, + } + vim.o.eventignore = eventignore elseif option.fn then option.value = option.fn(self, option) else - local input = input.get_user_input(option.cli, { + option.value = input.get_user_input(option.cli, { separator = "=", default = option.value, cancel = option.value, }) - - -- If the option specifies a default value, and the user set the value to be empty, defer to default value. - -- This is handy to prevent the user from accidentally loading thousands of log entries by accident. - if option.default and input == "" then - option.value = tostring(option.default) - else - option.value = input - end end state.set({ self.state.name, option.cli }, option.value) @@ -358,6 +352,7 @@ function M:mappings() mappings.n[config.id] = a.void(function() self:set_config(config) self:refresh() + Watcher.instance():dispatch_refresh() end) end end @@ -391,7 +386,6 @@ end function M:refresh() if self.buffer then - self.buffer:focus() self.buffer.ui:render(unpack(ui.Popup(self.state))) end end diff --git a/lua/neogit/lib/popup/ui.lua b/lua/neogit/lib/popup/ui.lua index 29f758bf6..b8f78e39a 100644 --- a/lua/neogit/lib/popup/ui.lua +++ b/lua/neogit/lib/popup/ui.lua @@ -196,6 +196,8 @@ local function render_action(action) -- selene: allow(empty_if) if action.keys == nil then -- Action group heading + elseif action.keys == "" then + table.insert(items, text("")) -- spacer elseif #action.keys == 0 then table.insert(items, text.highlight("NeogitPopupActionDisabled")("_")) else diff --git a/lua/neogit/lib/ui/init.lua b/lua/neogit/lib/ui/init.lua index 38fb32239..c2dcba05e 100644 --- a/lua/neogit/lib/ui/init.lua +++ b/lua/neogit/lib/ui/init.lua @@ -5,7 +5,7 @@ local Collection = require("neogit.lib.collection") local logger = require("neogit.logger") -- TODO: Add logging ---@class Section ----@field items StatusItem[] +---@field items StatusItem[] ---@field name string ---@field first number @@ -16,8 +16,8 @@ local logger = require("neogit.logger") -- TODO: Add logging ---@field section Section|nil ---@field item StatusItem|nil ---@field commit CommitLogEntry|nil ----@field commits CommitLogEntry[] ----@field items StatusItem[] +---@field commits CommitLogEntry[] +---@field items StatusItem[] local Selection = {} Selection.__index = Selection @@ -182,25 +182,19 @@ function Ui:item_hunks(item, first_line, last_line, partial) if not item.folded and item.diff.hunks then for _, h in ipairs(item.diff.hunks) do - if h.first <= last_line and h.last >= first_line then + if h.first <= first_line and h.last >= last_line then local from, to if partial then - local cursor_offset = first_line - h.first local length = last_line - first_line - from = h.diff_from + cursor_offset + from = first_line - h.first to = from + length else from = h.diff_from + 1 to = h.diff_to end - local hunk_lines = {} - for i = from, to do - table.insert(hunk_lines, item.diff.lines[i]) - end - -- local conflict = false -- for _, n in ipairs(conflict_markers) do -- if from <= n and n <= to then @@ -214,7 +208,6 @@ function Ui:item_hunks(item, first_line, last_line, partial) to = to, __index = h, hunk = h, - lines = hunk_lines, -- conflict = conflict, } @@ -228,6 +221,7 @@ function Ui:item_hunks(item, first_line, last_line, partial) return hunks end +---@return Selection function Ui:get_selection() local visual_pos = vim.fn.line("v") local cursor_pos = vim.fn.line(".") @@ -290,6 +284,7 @@ function Ui:get_selection() return setmetatable(res, Selection) end +--- returns commits in selection in a constant order ---@return string[] function Ui:get_commits_in_selection() local range = { vim.fn.getpos("v")[2], vim.fn.getpos(".")[2] } @@ -310,6 +305,33 @@ function Ui:get_commits_in_selection() return util.deduplicate(commits) end +--- returns commits in selection ordered according to the direction of the selection the user has made +---@return string[] +function Ui:get_ordered_commits_in_selection() + local start = vim.fn.getpos("v")[2] + local stop = vim.fn.getpos(".")[2] + + local increment + if start <= stop then + increment = 1 + else + increment = -1 + end + + local commits = {} + for i = start, stop, increment do + local component = self:_find_component_by_index(i, function(node) + return node.options.oid ~= nil + end) + + if component then + table.insert(commits, component.options.oid) + end + end + + return util.deduplicate(commits) +end + ---@return string[] function Ui:get_filepaths_in_selection() local range = { vim.fn.getpos("v")[2], vim.fn.getpos(".")[2] } @@ -349,6 +371,7 @@ function Ui:get_ref_under_cursor() return component and component.options.ref end + --- ---@return ParsedRef[] function Ui:get_refs_under_cursor() diff --git a/lua/neogit/lib/ui/renderer.lua b/lua/neogit/lib/ui/renderer.lua index 00fce647c..721278565 100644 --- a/lua/neogit/lib/ui/renderer.lua +++ b/lua/neogit/lib/ui/renderer.lua @@ -1,5 +1,7 @@ ---@source component.lua +local strdisplaywidth = vim.fn.strdisplaywidth + ---@class RendererIndex ---@field index table ---@field items table @@ -39,6 +41,9 @@ function RendererIndex:add_section(name, first, last) table.insert(self.items, { items = {} }) end +---@param item table +---@param first number +---@param last number function RendererIndex:add_item(item, first, last) self.items[#self.items].last = last @@ -72,6 +77,11 @@ end ---@field in_row boolean ---@field in_nested_row boolean +---@class RendererHighlight +---@field from integer +---@field to integer +---@field name string + ---@class Renderer ---@field buffer RendererBuffer ---@field flags RendererFlags @@ -125,6 +135,9 @@ function Renderer:item_index() return self.index.items end +---@param child Component +---@param parent Component +---@param index integer function Renderer:_build_child(child, parent, index) child.parent = parent child.index = index @@ -252,6 +265,10 @@ end ---@param child Component ---@param i integer index of child in parent.children +---@param col_start integer +---@param col_end integer|nil +---@param highlights RendererHighlight[] +---@param text string[] function Renderer:_render_child_in_row(child, i, col_start, col_end, highlights, text) if child.tag == "text" then return self:_render_in_row_text(child, i, col_start, highlights, text) @@ -264,6 +281,9 @@ end ---@param child Component ---@param index integer index of child in parent.children +---@param col_start integer +---@param highlights RendererHighlight[] +---@param text string[] function Renderer:_render_in_row_text(child, index, col_start, highlights, text) local padding_left = self.flags.in_nested_row and "" or child:get_padding_left(index == 1) table.insert(text, 1, padding_left) @@ -291,6 +311,10 @@ function Renderer:_render_in_row_text(child, index, col_start, highlights, text) end ---@param child Component +---@param highlights RendererHighlight[] +---@param text string[] +---@param col_start integer +---@param col_end integer|nil function Renderer:_render_in_row_row(child, highlights, text, col_start, col_end) self.flags.in_nested_row = true local res = self:_render(child, child.children, col_start) @@ -302,7 +326,7 @@ function Renderer:_render_in_row_row(child, highlights, text, col_start, col_end table.insert(highlights, h) end - col_end = col_start + vim.fn.strdisplaywidth(res.text) + col_end = col_start + strdisplaywidth(res.text) child.position.col_start = col_start child.position.col_end = col_end diff --git a/lua/neogit/lib/util.lua b/lua/neogit/lib/util.lua index 46c9942f6..0c76d16d4 100644 --- a/lua/neogit/lib/util.lua +++ b/lua/neogit/lib/util.lua @@ -596,6 +596,77 @@ function M.remove_ansi_escape_codes(s) return s end +--- Organizes a list of items by their file path, compressing elements with single children into groups. +---@param items table[] A list of items, each having a 'name' field which is a string representing the file path. +---@return table[] A hierarchical table structure representing the compressed file path groups and files. +function M.groupByFilePath(items) + local root = {} + for _, item in ipairs(items) do + local parts = {} + for part in string.gmatch(item.name, "[^/]+") do + table.insert(parts, part) + end + + local node = root + for i = 1, #parts do + local part = parts[i] + node.children = node.children or {} + node.children[part] = node.children[part] or { name = part, indent_level = i } + + node = node.children[part] + end + node.is_file = true + node.content = item + end + local function compress(node, path_prefix) + local children = node.children + if not children then + return { + name = node.name, + type = "file", + content = node.content, + indent_level = node.indent_level, + } + end + + local keys = {} + for k in pairs(children) do + table.insert(keys, k) + end + table.sort(keys) + while #keys == 1 and not children[keys[1]].is_file do + local only = keys[1] + path_prefix = path_prefix .. "/" .. only + node = children[only] + children = node.children + keys = {} + for k in pairs(children or {}) do + table.insert(keys, k) + end + table.sort(keys) + end + + local result = { + name = path_prefix, + type = "group", + children = {}, + indent_level = node.indent_level or 0, + } + + for _, key in ipairs(keys) do + table.insert(result.children, compress(children[key], key)) + end + + return result + end + local result = {} + for name, child in pairs(root.children or {}) do + table.insert(result, compress(child, name)) + end + + return result +end + --- Safely close a window ---@param winid integer ---@param force boolean diff --git a/lua/neogit/logger.lua b/lua/neogit/logger.lua index a07a21916..b8d725a56 100644 --- a/lua/neogit/logger.lua +++ b/lua/neogit/logger.lua @@ -41,6 +41,8 @@ log.new = function(config, standalone) obj = {} end + obj.config = config + local levels = {} for i, v in ipairs(config.modes) do levels[v.name] = i diff --git a/lua/neogit/popups/branch/actions.lua b/lua/neogit/popups/branch/actions.lua index 31ce1a528..87bf45721 100644 --- a/lua/neogit/popups/branch/actions.lua +++ b/lua/neogit/popups/branch/actions.lua @@ -5,32 +5,30 @@ local config = require("neogit.config") 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 a = require("plenary.async") local FuzzyFinderBuffer = require("neogit.buffers.fuzzy_finder") local BranchConfigPopup = require("neogit.popups.branch_config") -local function fire_branch_event(pattern, data) - vim.api.nvim_exec_autocmds("User", { pattern = pattern, modeline = false, data = data }) -end - local function fetch_remote_branch(target) local remote, branch = git.branch.parse_remote_branch(target) if remote then notification.info("Fetching from " .. remote .. "/" .. branch) git.fetch.fetch(remote, branch) - fire_branch_event("NeogitFetchComplete", { branch = branch, remote = remote }) + event.send("FetchComplete", { branch = branch, remote = remote }) end end local function checkout_branch(target, args) local result = git.branch.checkout(target, args) - if result.code > 0 then + if result:failure() then notification.error(table.concat(result.stderr, "\n")) return end - fire_branch_event("NeogitBranchCheckout", { branch_name = target }) + event.send("BranchCheckout", { branch_name = target }) + notification.info("Checked out branch " .. target) if config.values.fetch_after_checkout then a.void(function() @@ -70,13 +68,13 @@ local function spin_off_branch(checkout) return end - fire_branch_event("NeogitBranchCreate", { branch_name = name }) + event.send("BranchCreate", { branch_name = name }) local current_branch_name = git.branch.current_full_name() if checkout then git.cli.checkout.branch(name).call() - fire_branch_event("NeogitBranchCheckout", { branch_name = name }) + event.send("BranchCheckout", { branch_name = name }) end local upstream = git.branch.upstream() @@ -86,7 +84,7 @@ local function spin_off_branch(checkout) git.log.update_ref(current_branch_name, upstream) else git.cli.reset.hard.args(upstream).call() - fire_branch_event("NeogitReset", { commit = name, mode = "hard" }) + event.send("Reset", { commit = name, mode = "hard" }) end end end @@ -133,11 +131,17 @@ local function create_branch(popup, prompt, checkout, name) return end - git.branch.create(name, base_branch) - fire_branch_event("NeogitBranchCreate", { 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 }) - if checkout then - checkout_branch(name, popup:get_arguments()) + if checkout then + checkout_branch(name, popup:get_arguments()) + else + notification.info("Created branch " .. name) + end + else + notification.warn("Branch " .. name .. " already exists.") end end @@ -185,8 +189,14 @@ function M.checkout_local_branch(popup) if target then if vim.tbl_contains(remote_branches, target) then - git.branch.track(target, popup:get_arguments()) - fire_branch_event("NeogitBranchCheckout", { branch_name = target }) + local result = git.branch.track(target, popup:get_arguments()) + if result:failure() then + notification.error(table.concat(result.stderr, "\n")) + return + end + + notification.info("Created local branch " .. target .. " tracking remote") + event.send("BranchCheckout", { branch_name = target }) elseif not vim.tbl_contains(options, target) then target, _ = target:gsub("%s", "-") create_branch(popup, "Create " .. target .. " starting at", true, target) @@ -220,7 +230,7 @@ function M.configure_branch() return end - BranchConfigPopup.create(branch_name) + BranchConfigPopup.create { branch = branch_name } end function M.rename_branch() @@ -235,10 +245,13 @@ function M.rename_branch() return end - git.cli.branch.move.args(selected_branch, new_name).call { await = true } - - notification.info(string.format("Renamed '%s' to '%s'", selected_branch, new_name)) - fire_branch_event("NeogitBranchRename", { 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)) + event.send("BranchRename", { branch_name = selected_branch, new_name = new_name }) + else + notification.warn(string.format("Couldn't rename '%s' to '%s'", selected_branch, new_name)) + end end function M.reset_branch(popup) @@ -278,13 +291,17 @@ function M.reset_branch(popup) end -- Reset the current branch to the desired state & update reflog - git.cli.reset.hard.args(to).call() - local current = git.branch.current_full_name() - assert(current, "no current branch") - git.log.update_ref(current, to) - - notification.info(string.format("Reset '%s' to '%s'", current, to)) - fire_branch_event("NeogitBranchReset", { 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() + assert(current, "no current branch") + git.log.update_ref(current, to) + + notification.info(string.format("Reset '%s' to '%s'", current, to)) + event.send("BranchReset", { branch_name = current, resetting_to = to }) + else + notification.error("Couldn't reset branch.") + end end function M.delete_branch(popup) @@ -303,7 +320,7 @@ function M.delete_branch(popup) and branch_name and input.get_permission(("Delete remote branch '%s/%s'?"):format(remote, branch_name)) then - success = git.cli.push.remote(remote).delete.to(branch_name).call().code == 0 + success = git.cli.push.remote(remote).delete.to(branch_name).call():success() elseif not remote and branch_name == git.branch.current() then local choices = { "&detach HEAD and delete", @@ -343,7 +360,7 @@ function M.delete_branch(popup) else notification.info(string.format("Deleted branch '%s'", branch_name)) end - fire_branch_event("NeogitBranchDelete", { branch_name = branch_name }) + event.send("BranchDelete", { branch_name = branch_name }) end end @@ -387,7 +404,9 @@ function M.open_pull_request() format_values["target"] = target end - vim.ui.open(util.format(template, format_values)) + local uri = util.format(template, format_values) + notification.info(("Opening %q in your browser."):format(uri)) + vim.ui.open(uri) else notification.warn("Requires Neovim 0.10") end diff --git a/lua/neogit/popups/branch_config/init.lua b/lua/neogit/popups/branch_config/init.lua index 5076d5d56..2cd1e8171 100644 --- a/lua/neogit/popups/branch_config/init.lua +++ b/lua/neogit/popups/branch_config/init.lua @@ -3,9 +3,16 @@ local M = {} local popup = require("neogit.lib.popup") local git = require("neogit.lib.git") local actions = require("neogit.popups.branch_config.actions") +local notification = require("neogit.lib.notification") -function M.create(branch) - branch = branch or git.branch.current() +---@param env table +function M.create(env) + local branch = env.branch or git.branch.current() + + if not branch then + notification.error("Cannot infer branch.") + return + end local g_pull_rebase = git.config.get_global("pull.rebase") local pull_rebase_entry = git.config.get_local("pull.rebase") diff --git a/lua/neogit/popups/commit/actions.lua b/lua/neogit/popups/commit/actions.lua index eb91366d0..9d28dc13c 100644 --- a/lua/neogit/popups/commit/actions.lua +++ b/lua/neogit/popups/commit/actions.lua @@ -95,7 +95,7 @@ local function commit_special(popup, method, opts) end a.util.scheduler() - do_commit(popup, cmd.args(string.format("--%s=%s", method, commit))) + do_commit(popup, cmd.args(method:format(commit))) if opts.rebase then a.util.scheduler() @@ -149,15 +149,23 @@ function M.amend(popup) end function M.fixup(popup) - commit_special(popup, "fixup", { edit = false }) + commit_special(popup, "--fixup=%s", { edit = false }) end function M.squash(popup) - commit_special(popup, "squash", { edit = false }) + commit_special(popup, "--squash=%s", { edit = false }) end function M.augment(popup) - commit_special(popup, "squash", { edit = true }) + commit_special(popup, "--squash=%s", { edit = true }) +end + +function M.alter(popup) + commit_special(popup, "--fixup=amend:%s", { edit = true }) +end + +function M.revise(popup) + commit_special(popup, "--fixup=reword:%s", { edit = true }) end function M.instant_fixup(popup) @@ -165,7 +173,7 @@ function M.instant_fixup(popup) return end - commit_special(popup, "fixup", { rebase = true, edit = false }) + commit_special(popup, "--fixup=%s", { rebase = true, edit = false }) end function M.instant_squash(popup) @@ -173,7 +181,7 @@ function M.instant_squash(popup) return end - commit_special(popup, "squash", { rebase = true, edit = false }) + commit_special(popup, "--squash=%s", { rebase = true, edit = false }) end function M.absorb(popup) diff --git a/lua/neogit/popups/commit/init.lua b/lua/neogit/popups/commit/init.lua index 7885281ce..fa5fd8a9f 100644 --- a/lua/neogit/popups/commit/init.lua +++ b/lua/neogit/popups/commit/init.lua @@ -18,18 +18,23 @@ function M.create(env) :option("C", "reuse-message", "", "Reuse commit message", { key_prefix = "-" }) :group_heading("Create") :action("c", "Commit", actions.commit) - :action("x", "Absorb", actions.absorb) :new_action_group("Edit HEAD") :action("e", "Extend", actions.extend) - :action("w", "Reword", actions.reword) + :spacer() :action("a", "Amend", actions.amend) + :spacer() + :action("w", "Reword", actions.reword) :new_action_group("Edit") :action("f", "Fixup", actions.fixup) :action("s", "Squash", actions.squash) - :action("A", "Augment", actions.augment) - :new_action_group() + :action("A", "Alter", actions.alter) + :action("n", "Augment", actions.augment) + :action("W", "Revise", actions.revise) + :new_action_group("Edit and rebase") :action("F", "Instant Fixup", actions.instant_fixup) :action("S", "Instant Squash", actions.instant_squash) + :new_action_group("Spread across commits") + :action("x", "Absorb", actions.absorb) :env({ highlight = { "HEAD" }, commit = env.commit }) :build() diff --git a/lua/neogit/popups/diff/actions.lua b/lua/neogit/popups/diff/actions.lua index af184cbd4..015b84214 100644 --- a/lua/neogit/popups/diff/actions.lua +++ b/lua/neogit/popups/diff/actions.lua @@ -9,42 +9,67 @@ local input = require("neogit.lib.input") function M.this(popup) popup:close() - if popup.state.env.section and popup.state.env.item then - diffview.open(popup.state.env.section.name, popup.state.env.item.name, { - only = true, - }) - elseif popup.state.env.section then - diffview.open(popup.state.env.section.name, nil, { only = true }) + local item = popup:get_env("item") + local section = popup:get_env("section") + + if section and section.name and item and item.name then + diffview.open(section.name, item.name, { only = true }) + elseif section.name then + diffview.open(section.name, nil, { only = true }) + elseif item.name then + diffview.open("range", item.name .. "..HEAD") + end +end + +function M.this_to_HEAD(popup) + popup:close() + + local item = popup:get_env("item") + if item then + if item.name then + diffview.open("range", item.name .. "..HEAD") + end end end function M.range(popup) + local commit + local item = popup:get_env("item") + local section = popup:get_env("section") + if section and (section.name == "log" or section.name == "recent") then + commit = item and item.name + end + local options = util.deduplicate( util.merge( - { git.branch.current() or "HEAD" }, + { commit, git.branch.current() or "HEAD" }, git.branch.get_all_branches(false), git.tag.list(), git.refs.heads() ) ) - local range_from = FuzzyFinderBuffer.new(options):open_async { prompt_prefix = "Diff for range from" } + local range_from = FuzzyFinderBuffer.new(options):open_async { + prompt_prefix = "Diff for range from", + refocus_status = false, + } + if not range_from then return end local range_to = FuzzyFinderBuffer.new(options) - :open_async { prompt_prefix = "Diff from " .. range_from .. " to" } + :open_async { prompt_prefix = "Diff from " .. range_from .. " to", refocus_status = false } if not range_to then return end local choices = { - "&1. " .. range_from .. ".." .. range_to, - "&2. " .. range_from .. "..." .. range_to, + "&1. Range (a..b)", + "&2. Symmetric Difference (a...b)", "&3. Cancel", } - local choice = input.get_choice("Select range", { values = choices, default = #choices }) + local choice = input.get_choice("Select type", { values = choices, default = #choices }) popup:close() if choice == "1" then @@ -72,7 +97,7 @@ end function M.stash(popup) popup:close() - local selected = FuzzyFinderBuffer.new(git.stash.list()):open_async() + local selected = FuzzyFinderBuffer.new(git.stash.list()):open_async { refocus_status = false } if selected then diffview.open("stashes", selected) end @@ -83,7 +108,7 @@ function M.commit(popup) local options = util.merge(git.refs.list_branches(), git.refs.list_tags(), git.refs.heads()) - local selected = FuzzyFinderBuffer.new(options):open_async() + local selected = FuzzyFinderBuffer.new(options):open_async { refocus_status = false } if selected then diffview.open("commit", selected) end diff --git a/lua/neogit/popups/diff/init.lua b/lua/neogit/popups/diff/init.lua index ed8b849c0..34b32ef83 100644 --- a/lua/neogit/popups/diff/init.lua +++ b/lua/neogit/popups/diff/init.lua @@ -6,12 +6,14 @@ local actions = require("neogit.popups.diff.actions") function M.create(env) local diffview = config.check_integration("diffview") + local commit_selected = (env.section and env.section.name == "log") and type(env.item.name) == "string" local p = popup .builder() :name("NeogitDiffPopup") :group_heading("Diff") - :action_if(diffview, "d", "this", actions.this) + :action_if(diffview and env.item, "d", "this", actions.this) + :action_if(diffview and commit_selected, "h", "this..HEAD", actions.this_to_HEAD) :action_if(diffview, "r", "range", actions.range) :action("p", "paths") :new_action_group() diff --git a/lua/neogit/popups/fetch/actions.lua b/lua/neogit/popups/fetch/actions.lua index 86ee3034d..67991cdf6 100644 --- a/lua/neogit/popups/fetch/actions.lua +++ b/lua/neogit/popups/fetch/actions.lua @@ -5,6 +5,7 @@ local git = require("neogit.lib.git") local logger = require("neogit.logger") local notification = require("neogit.lib.notification") local util = require("neogit.lib.util") +local event = require("neogit.lib.event") local FuzzyFinderBuffer = require("neogit.buffers.fuzzy_finder") @@ -16,15 +17,11 @@ local function fetch_from(name, remote, branch, args) notification.info("Fetching from " .. name) local res = git.fetch.fetch_interactive(remote, branch, args) - if res and res.code == 0 then + if res and res:success() then a.util.scheduler() notification.info("Fetched from " .. name, { dismiss = true }) logger.debug("Fetched from " .. name) - vim.api.nvim_exec_autocmds("User", { - pattern = "NeogitFetchComplete", - modeline = false, - data = { remote = remote, branch = branch }, - }) + event.send("FetchComplete", { remote = remote, branch = branch }) else logger.error("Failed to fetch from " .. name) end @@ -125,7 +122,7 @@ function M.fetch_submodules(_) end function M.set_variables() - require("neogit.popups.branch_config").create() + require("neogit.popups.branch_config").create {} end return M diff --git a/lua/neogit/popups/pull/actions.lua b/lua/neogit/popups/pull/actions.lua index b7a1b631e..d311d6f0d 100644 --- a/lua/neogit/popups/pull/actions.lua +++ b/lua/neogit/popups/pull/actions.lua @@ -2,6 +2,7 @@ local a = require("plenary.async") local git = require("neogit.lib.git") local logger = require("neogit.logger") local notification = require("neogit.lib.notification") +local event = require("neogit.lib.event") local FuzzyFinderBuffer = require("neogit.buffers.fuzzy_finder") @@ -25,11 +26,11 @@ local function pull_from(args, remote, branch, opts) local res = git.pull.pull_interactive(remote, branch, args) - if res and res.code == 0 then + if res and res:success() then a.util.scheduler() notification.info("Pulled from " .. name, { dismiss = true }) logger.debug("Pulled from " .. name) - vim.api.nvim_exec_autocmds("User", { pattern = "NeogitPullComplete", modeline = false }) + event.send("PullComplete") else logger.error("Failed to pull from " .. name) notification.error("Failed to pull from " .. name, { dismiss = true }) @@ -86,7 +87,7 @@ function M.from_elsewhere(popup) end function M.configure() - require("neogit.popups.branch_config").create() + require("neogit.popups.branch_config").create {} end return M diff --git a/lua/neogit/popups/pull/init.lua b/lua/neogit/popups/pull/init.lua index a1da91f69..070dd91ce 100755 --- a/lua/neogit/popups/pull/init.lua +++ b/lua/neogit/popups/pull/init.lua @@ -5,15 +5,15 @@ local popup = require("neogit.lib.popup") local M = {} function M.create() - local current = git.branch.current() - local show_config = current ~= "" and current ~= "(detached)" + local current = git.branch.current() or "" local pull_rebase_entry = git.config.get("pull.rebase") local pull_rebase = pull_rebase_entry:is_set() and pull_rebase_entry.value or "false" + local is_detached = git.branch.is_detached() local p = popup .builder() :name("NeogitPullPopup") - :config_if(show_config, "r", "branch." .. (current or "") .. ".rebase", { + :config_if(not is_detached, "r", "branch." .. current .. ".rebase", { options = { { display = "true", value = "true" }, { display = "false", value = "false" }, @@ -25,10 +25,10 @@ function M.create() :switch("a", "autostash", "Autostash") :switch("t", "tags", "Fetch tags") :switch("F", "force", "Force", { persisted = false }) - :group_heading_if(current ~= nil, "Pull into " .. current .. " from") - :group_heading_if(not current, "Pull from") - :action_if(current ~= nil, "p", git.branch.pushRemote_label(), actions.from_pushremote) - :action_if(current ~= nil, "u", git.branch.upstream_label(), actions.from_upstream) + :group_heading_if(not is_detached, "Pull into " .. current .. " from") + :group_heading_if(is_detached, "Pull from") + :action_if(not is_detached, "p", git.branch.pushRemote_label(), actions.from_pushremote) + :action_if(not is_detached, "u", git.branch.upstream_label(), actions.from_upstream) :action("e", "elsewhere", actions.from_elsewhere) :new_action_group("Configure") :action("C", "Set variables...", actions.configure) diff --git a/lua/neogit/popups/push/actions.lua b/lua/neogit/popups/push/actions.lua index 87c52a486..d7fe25948 100644 --- a/lua/neogit/popups/push/actions.lua +++ b/lua/neogit/popups/push/actions.lua @@ -5,11 +5,16 @@ local notification = require("neogit.lib.notification") local input = require("neogit.lib.input") local util = require("neogit.lib.util") local config = require("neogit.config") +local event = require("neogit.lib.event") local FuzzyFinderBuffer = require("neogit.buffers.fuzzy_finder") local M = {} +---@param args string[] +---@param remote string +---@param branch string|nil +---@param opts table|nil local function push_to(args, remote, branch, opts) opts = opts or {} @@ -43,7 +48,7 @@ local function push_to(args, remote, branch, opts) local updates_rejected = string.find(table.concat(res.stdout), "Updates were rejected") ~= nil -- Only ask the user whether to force push if not already specified and feature enabled - if res and res.code ~= 0 and not using_force and updates_rejected and config.values.prompt_force_push then + if res and res:failure() and not using_force and updates_rejected and config.values.prompt_force_push then logger.error("Attempting force push to " .. name) local message = "Your branch has diverged from the remote branch. Do you want to force push?" @@ -53,11 +58,11 @@ local function push_to(args, remote, branch, opts) end end - if res and res.code == 0 then + if res and res:success() then a.util.scheduler() logger.debug("Pushed to " .. name) notification.info("Pushed to " .. name, { dismiss = true }) - vim.api.nvim_exec_autocmds("User", { pattern = "NeogitPushComplete", modeline = false }) + event.send("PushComplete") else logger.debug("Failed to push to " .. name) notification.error("Failed to push to " .. name, { dismiss = true }) @@ -107,14 +112,7 @@ function M.to_elsewhere(popup) end function M.push_other(popup) - local sources = git.branch.get_local_branches() - table.insert(sources, "HEAD") - table.insert(sources, "ORIG_HEAD") - table.insert(sources, "FETCH_HEAD") - if popup.state.env.commit then - table.insert(sources, 1, popup.state.env.commit) - end - + local sources = util.merge({ popup.state.env.commit }, git.refs.list_local_branches(), git.refs.heads()) local source = FuzzyFinderBuffer.new(sources):open_async { prompt_prefix = "push" } if not source then return @@ -125,7 +123,7 @@ function M.push_other(popup) table.insert(destinations, 1, remote .. "/" .. source) end - local destination = FuzzyFinderBuffer.new(destinations) + local destination = FuzzyFinderBuffer.new(util.deduplicate(destinations)) :open_async { prompt_prefix = "push " .. source .. " to" } if not destination then return @@ -195,7 +193,7 @@ function M.explicit_refspec(popup) end function M.configure() - require("neogit.popups.branch_config").create() + require("neogit.popups.branch_config").create {} end return M diff --git a/lua/neogit/popups/push/init.lua b/lua/neogit/popups/push/init.lua index 6b6124154..b4357a2f8 100644 --- a/lua/neogit/popups/push/init.lua +++ b/lua/neogit/popups/push/init.lua @@ -5,7 +5,8 @@ local git = require("neogit.lib.git") local M = {} function M.create(env) - local current = git.branch.current() + local current = git.branch.current() or "" + local is_detached = git.branch.is_detached() local p = popup .builder() @@ -15,11 +16,14 @@ function M.create(env) :switch("h", "no-verify", "Disable hooks") :switch("d", "dry-run", "Dry run") :switch("u", "set-upstream", "Set the upstream before pushing") - :group_heading("Push " .. ((current and (current .. " ")) or "") .. "to") - :action("p", git.branch.pushRemote_label(), actions.to_pushremote) - :action("u", git.branch.upstream_label(), actions.to_upstream) - :action("e", "elsewhere", actions.to_elsewhere) - :new_action_group("Push") + :switch("T", "tags", "Include all tags") + :switch("t", "follow-tags", "Include related annotated tags") + :group_heading_if(not is_detached, "Push " .. current .. " to") + :action_if(not is_detached, "p", git.branch.pushRemote_or_pushDefault_label(), actions.to_pushremote) + :action_if(not is_detached, "u", git.branch.upstream_label(), actions.to_upstream) + :action_if(not is_detached, "e", "elsewhere", actions.to_elsewhere) + :group_heading_if(is_detached, "Push") + :new_action_group_if(not is_detached, "Push") :action("o", "another branch", actions.push_other) :action("r", "explicit refspec", actions.explicit_refspec) :action("m", "matching branches", actions.matching_branches) @@ -28,7 +32,12 @@ function M.create(env) :new_action_group("Configure") :action("C", "Set variables...", actions.configure) :env({ - highlight = { current, git.branch.upstream(), git.branch.pushRemote_ref() }, + highlight = { + current, + git.branch.upstream(), + git.branch.pushRemote_ref(), + git.branch.pushDefault_ref(), + }, bold = { "pushRemote", "@{upstream}" }, commit = env.commit, }) diff --git a/lua/neogit/popups/rebase/actions.lua b/lua/neogit/popups/rebase/actions.lua index 466d35359..a05fbf0a6 100644 --- a/lua/neogit/popups/rebase/actions.lua +++ b/lua/neogit/popups/rebase/actions.lua @@ -31,19 +31,14 @@ function M.onto_pushRemote(popup) end function M.onto_upstream(popup) - local upstream - if git.repo.state.upstream.ref then - upstream = string.format("refs/remotes/%s", git.repo.state.upstream.ref) - else - local target = FuzzyFinderBuffer.new(git.refs.list_remote_branches()):open_async() - if not target then - return - end - - upstream = string.format("refs/remotes/%s", target) + local upstream = git.branch.upstream(git.branch.current()) + if not upstream then + upstream = FuzzyFinderBuffer.new(git.refs.list_branches()):open_async() end - git.rebase.onto_branch(upstream, popup:get_arguments()) + if upstream then + git.rebase.onto_branch(upstream, popup:get_arguments()) + end end function M.onto_elsewhere(popup) diff --git a/lua/neogit/popups/remote/actions.lua b/lua/neogit/popups/remote/actions.lua index 5582e2549..06609bec7 100644 --- a/lua/neogit/popups/remote/actions.lua +++ b/lua/neogit/popups/remote/actions.lua @@ -114,7 +114,7 @@ function M.configure(_) return end - RemoteConfigPopup.create(remote_name) + RemoteConfigPopup.create { remote = remote_name } end function M.prune_branches(_) diff --git a/lua/neogit/popups/remote_config/init.lua b/lua/neogit/popups/remote_config/init.lua index 0a2d9538a..728aca14b 100644 --- a/lua/neogit/popups/remote_config/init.lua +++ b/lua/neogit/popups/remote_config/init.lua @@ -1,7 +1,29 @@ local M = {} local popup = require("neogit.lib.popup") +local notification = require("neogit.lib.notification") +local git = require("neogit.lib.git") + +---@param env table +function M.create(env) + local remotes = git.remote.list() + if vim.tbl_isempty(remotes) then + notification.warn("Repo has no configured remotes.") + return + end + + local remote = env.remote + + if not remote then + if vim.tbl_contains(remotes, "origin") then + remote = "origin" + elseif #remotes == 1 then + remote = remotes[1] + else + notification.error("Cannot infer remote.") + return + end + end -function M.create(remote) local p = popup .builder() :name("NeogitRemoteConfigPopup") diff --git a/lua/neogit/popups/reset/actions.lua b/lua/neogit/popups/reset/actions.lua index 0b7d79885..d2b594d7a 100644 --- a/lua/neogit/popups/reset/actions.lua +++ b/lua/neogit/popups/reset/actions.lua @@ -2,6 +2,7 @@ local git = require("neogit.lib.git") 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 M = {} @@ -18,50 +19,51 @@ local function target(popup, prompt) return FuzzyFinderBuffer.new(refs):open_async { prompt_prefix = prompt } end ----@param type string +---@param fn fun(target: string): boolean ---@param popup PopupData ---@param prompt string -local function reset(type, popup, prompt) +---@param mode string +local function reset(fn, popup, prompt, mode) local target = target(popup, prompt) if target then - git.reset[type](target) + local success = fn(target) + if success then + notification.info("Reset to " .. target) + event.send("Reset", { commit = target, mode = mode }) + else + notification.error("Reset Failed") + end end end ---@param popup PopupData function M.mixed(popup) - reset("mixed", popup, ("Reset %s to"):format(git.branch.current())) + reset(git.reset.mixed, popup, ("Reset %s to"):format(git.branch.current()), "mixed") end ---@param popup PopupData function M.soft(popup) - reset("soft", popup, ("Soft reset %s to"):format(git.branch.current())) + reset(git.reset.soft, popup, ("Soft reset %s to"):format(git.branch.current()), "soft") end ---@param popup PopupData function M.hard(popup) - reset("hard", popup, ("Hard reset %s to"):format(git.branch.current())) + reset(git.reset.hard, popup, ("Hard reset %s to"):format(git.branch.current()), "hard") end ---@param popup PopupData function M.keep(popup) - reset("keep", popup, ("Reset %s to"):format(git.branch.current())) + reset(git.reset.keep, popup, ("Reset %s to"):format(git.branch.current()), "keep") end ---@param popup PopupData function M.index(popup) - reset("index", popup, "Reset index to") + reset(git.reset.index, popup, "Reset index to", "index") end ---@param popup PopupData function M.worktree(popup) - local target = target(popup, "Reset worktree to") - if target then - git.index.with_temp_index(target, function(index) - git.cli["checkout-index"].all.force.env({ GIT_INDEX_FILE = index }).call() - notification.info(("Reset worktree to %s"):format(target)) - end) - end + reset(git.reset.worktree, popup, "Reset worktree to", "worktree") end ---@param popup PopupData @@ -82,7 +84,18 @@ function M.a_file(popup) return end - git.reset.file(target, files) + local success = git.reset.file(target, files) + if not success then + notification.error("Reset Failed") + else + if #files > 1 then + notification.info("Reset " .. #files .. " files") + else + notification.info("Reset " .. files[1]) + end + + event.send("Reset", { commit = target, mode = "files", files = files }) + end end return M diff --git a/lua/neogit/popups/revert/actions.lua b/lua/neogit/popups/revert/actions.lua index 7d49c28ae..2ecd010c5 100644 --- a/lua/neogit/popups/revert/actions.lua +++ b/lua/neogit/popups/revert/actions.lua @@ -62,6 +62,10 @@ function M.changes(popup) end end +function M.hunk(popup) + git.revert.hunk(popup:get_env("hunk"), popup:get_arguments()) +end + function M.continue() git.revert.continue() end diff --git a/lua/neogit/popups/revert/init.lua b/lua/neogit/popups/revert/init.lua index 092f16596..c926e1e50 100644 --- a/lua/neogit/popups/revert/init.lua +++ b/lua/neogit/popups/revert/init.lua @@ -8,21 +8,31 @@ function M.create(env) local in_progress = git.sequencer.pick_or_revert_in_progress() -- TODO: enabled = true needs to check if incompatible switch is toggled in internal state, and not apply. -- if you enable 'no edit', and revert, next time you load the popup both will be enabled - -- - -- :option("s", "strategy", "", "Strategy") - -- :switch("s", "signoff", "Add Signed-off-by lines") - -- :option("S", "gpg-sign", "", "Sign using gpg") - -- stylua: ignore local p = popup .builder() :name("NeogitRevertPopup") :option_if(not in_progress, "m", "mainline", "", "Replay merge relative to parent") - :switch_if(not in_progress, "e", "edit", "Edit commit messages", { enabled = true, incompatible = { "no-edit" } }) + :switch_if( + not in_progress, + "e", + "edit", + "Edit commit messages", + { enabled = true, incompatible = { "no-edit" } } + ) :switch_if(not in_progress, "E", "no-edit", "Don't edit commit messages", { incompatible = { "edit" } }) + :switch_if(not in_progress, "s", "signoff", "Add Signed-off-by lines") + :option_if(not in_progress, "s", "strategy", "", "Strategy", { + key_prefix = "=", + choices = { "octopus", "ours", "resolve", "subtree", "recursive" }, + }) + :option_if(not in_progress, "S", "gpg-sign", "", "Sign using gpg", { + key_prefix = "-", + }) :group_heading("Revert") :action_if(not in_progress, "v", "Commit(s)", actions.commits) :action_if(not in_progress, "V", "Changes", actions.changes) + :action_if(((not in_progress) and env.hunk ~= nil), "h", "Hunk", actions.hunk) :action_if(in_progress, "v", "continue", actions.continue) :action_if(in_progress, "s", "skip", actions.skip) :action_if(in_progress, "a", "abort", actions.abort) diff --git a/lua/neogit/popups/stash/actions.lua b/lua/neogit/popups/stash/actions.lua index c47adbfe3..288d82e6e 100644 --- a/lua/neogit/popups/stash/actions.lua +++ b/lua/neogit/popups/stash/actions.lua @@ -27,6 +27,9 @@ function M.push(popup) git.stash.push(popup:get_arguments(), files) end +---@param action string +---@param stash { name: string } +---@param opts { confirm: boolean }|nil local function use(action, stash, opts) opts = opts or {} local name, get_permission diff --git a/lua/neogit/popups/tag/actions.lua b/lua/neogit/popups/tag/actions.lua index b775f9457..c93b35307 100644 --- a/lua/neogit/popups/tag/actions.lua +++ b/lua/neogit/popups/tag/actions.lua @@ -6,10 +6,7 @@ local utils = require("neogit.lib.util") local FuzzyFinderBuffer = require("neogit.buffers.fuzzy_finder") local input = require("neogit.lib.input") local notification = require("neogit.lib.notification") - -local function fire_tag_event(pattern, data) - vim.api.nvim_exec_autocmds("User", { pattern = pattern, modeline = false, data = data }) -end +local event = require("neogit.lib.event") ---@param popup PopupData function M.create_tag(popup) @@ -40,10 +37,11 @@ function M.create_tag(popup) }, }) if code == 0 then - fire_tag_event("NeogitTagCreate", { name = tag_input, ref = selected }) + event.send("TagCreate", { name = tag_input, ref = selected }) end end +--TODO: --- Create a release tag for `HEAD'. ---@param _ table function M.create_release(_) end @@ -62,7 +60,7 @@ function M.delete(_) if git.tag.delete(tags) then notification.info("Deleted tags: " .. table.concat(tags, ",")) for _, tag in pairs(tags) do - fire_tag_event("NeogitTagDelete", { name = tag }) + event.send("TagDelete", { name = tag }) end end end diff --git a/lua/neogit/popups/worktree/actions.lua b/lua/neogit/popups/worktree/actions.lua index ba4979d7a..d0ff7b6fb 100644 --- a/lua/neogit/popups/worktree/actions.lua +++ b/lua/neogit/popups/worktree/actions.lua @@ -5,16 +5,53 @@ local input = require("neogit.lib.input") local util = require("neogit.lib.util") local status = require("neogit.buffers.status") local notification = require("neogit.lib.notification") +local event = require("neogit.lib.event") local FuzzyFinderBuffer = require("neogit.buffers.fuzzy_finder") ---@param prompt string +---@param branch string? ---@return string|nil -local function get_path(prompt) - return input.get_user_input(prompt, { +local function get_path(prompt, branch) + local path = input.get_user_input(prompt, { completion = "dir", prepend = vim.fs.normalize(vim.uv.cwd() .. "/..") .. "/", }) + + if path then + if branch and vim.uv.fs_stat(path) then + return vim.fs.joinpath(path, branch) + else + return path + end + else + return nil + end +end + +---@param old_cwd string? +---@param new_cwd string +---@return table +local function autocmd_helpers(old_cwd, new_cwd) + return { + old_cwd = old_cwd, + new_cwd = new_cwd, + ---@param filename string the file you want to copy + ---@param callback function? callback to run if copy was successful + copy_if_present = function(filename, callback) + assert(old_cwd, "couldn't resolve old cwd") + + local source = vim.fs.joinpath(old_cwd, filename) + local destination = vim.fs.joinpath(new_cwd, filename) + + if vim.uv.fs_stat(source) and not vim.uv.fs_stat(destination) then + local ok = vim.uv.fs_copyfile(source, destination) + if ok and type(callback) == "function" then + callback() + end + end + end, + } end ---@param prompt string @@ -30,17 +67,21 @@ function M.checkout_worktree() return end - local path = get_path(("Checkout '%s' in new worktree"):format(selected)) + local path = get_path(("Checkout '%s' in new worktree"):format(selected), selected) if not path then return end local success, err = git.worktree.add(selected, path) if success then + local cwd = vim.uv.cwd() notification.info("Added worktree") + if status.is_open() then status.instance():chdir(path) end + + event.send("WorktreeCreate", autocmd_helpers(cwd, path)) else notification.error(err) end @@ -65,10 +106,14 @@ function M.create_worktree() if git.branch.create(name, selected) then local success, err = git.worktree.add(name, path) if success then + local cwd = vim.uv.cwd() notification.info("Added worktree") + if status.is_open() then status.instance():chdir(path) end + + event.send("WorktreeCreate", autocmd_helpers(cwd, path)) else notification.error(err) end diff --git a/lua/neogit/process.lua b/lua/neogit/process.lua index fb0dd2c8a..60e95f56e 100644 --- a/lua/neogit/process.lua +++ b/lua/neogit/process.lua @@ -86,6 +86,16 @@ function ProcessResult:remove_ansi() return self end +---@return boolean +function ProcessResult:success() + return self.code == 0 +end + +---@return boolean +function ProcessResult:failure() + return self.code ~= 0 +end + ProcessResult.__index = ProcessResult ---@param process ProcessOpts diff --git a/lua/neogit/runner.lua b/lua/neogit/runner.lua index f6cb4366b..99dbe649d 100644 --- a/lua/neogit/runner.lua +++ b/lua/neogit/runner.lua @@ -71,6 +71,7 @@ local function handle_fatal_error(line) notification.error(line) return "__CANCEL__" end + ---@param process Process ---@param line string ---@return boolean diff --git a/spec/buffers/commit_select_buffer_spec.rb b/spec/buffers/commit_select_buffer_spec.rb new file mode 100644 index 000000000..e613ca868 --- /dev/null +++ b/spec/buffers/commit_select_buffer_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "Commit Select Buffer", :git, :nvim do + it "renders, raising no errors" do + nvim.keys("AA") + expect(nvim.errors).to be_empty + expect(nvim.filetype).to eq("NeogitCommitSelectView") + end +end diff --git a/spec/buffers/stash_list_buffer_spec.rb b/spec/buffers/stash_list_buffer_spec.rb new file mode 100644 index 000000000..32c119b6e --- /dev/null +++ b/spec/buffers/stash_list_buffer_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "Stash list Buffer", :git, :nvim do + before do + create_file("1") + git.add("1") + git.commit("test") + create_file("1", content: "hello world") + git.lib.stash_save("test") + nvim.refresh + end + + it "renders, raising no errors" do + nvim.keys("Zl") + expect(nvim.screen[1..2]).to eq( + [ + " Stashes (1) ", + "stash@{0} On master: test 0 seconds ago" + ] + ) + + expect(nvim.errors).to be_empty + expect(nvim.filetype).to eq("NeogitStashView") + end + + it "can open CommitView" do + nvim.keys("Zl") + expect(nvim.errors).to be_empty + expect(nvim.filetype).to eq("NeogitCommitView") + end +end diff --git a/spec/general_spec.rb b/spec/general_spec.rb new file mode 100644 index 000000000..30fb8f4d2 --- /dev/null +++ b/spec/general_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +RSpec.describe "general things", :git, :nvim do + popups = %w[ + bisect branch branch_config cherry_pick commit + diff fetch help ignore log merge pull push rebase + remote remote_config reset revert stash tag worktree + ] + + popups.each do |popup| + it "can invoke #{popup} popup without status buffer", :with_remote_origin do + nvim.keys("q") + nvim.lua("require('neogit').open({ '#{popup}' })") + sleep(0.1) # Allow popup to open + + expect(nvim.filetype).to eq("NeogitPopup") + expect(nvim.errors).to be_empty + end + end +end diff --git a/spec/popups/bisect_popup_spec.rb b/spec/popups/bisect_popup_spec.rb index 87714c9ab..3654228ef 100644 --- a/spec/popups/bisect_popup_spec.rb +++ b/spec/popups/bisect_popup_spec.rb @@ -3,8 +3,7 @@ require "spec_helper" RSpec.describe "Bisect Popup", :git, :nvim, :popup do # rubocop:disable RSpec/EmptyExampleGroup - before { nvim.keys("B") } - + let(:keymap) { "B" } let(:view) do [ " Arguments ", diff --git a/spec/popups/branch_config_popup_spec.rb b/spec/popups/branch_config_popup_spec.rb index 57dee8d30..bc0875581 100644 --- a/spec/popups/branch_config_popup_spec.rb +++ b/spec/popups/branch_config_popup_spec.rb @@ -3,8 +3,7 @@ require "spec_helper" RSpec.describe "Branch Config Popup", :git, :nvim, :popup do - before { nvim.keys("bC") } - + let(:keymap) { "bC" } let(:view) do [ " Configure branch ", diff --git a/spec/popups/branch_popup_spec.rb b/spec/popups/branch_popup_spec.rb index f699c754f..fac371074 100644 --- a/spec/popups/branch_popup_spec.rb +++ b/spec/popups/branch_popup_spec.rb @@ -3,8 +3,7 @@ require "spec_helper" RSpec.describe "Branch Popup", :git, :nvim, :popup do - before { nvim.keys("b") } - + let(:keymap) { "b" } let(:view) do [ " Configure branch ", diff --git a/spec/popups/cherry_pick_popup_spec.rb b/spec/popups/cherry_pick_popup_spec.rb index a98bd109b..2b7cb78eb 100644 --- a/spec/popups/cherry_pick_popup_spec.rb +++ b/spec/popups/cherry_pick_popup_spec.rb @@ -3,8 +3,7 @@ require "spec_helper" RSpec.describe "Cherry Pick Popup", :git, :nvim, :popup do # rubocop:disable RSpec/EmptyExampleGroup - before { nvim.keys("A") } - + let(:keymap) { "A" } let(:view) do [ " Arguments ", diff --git a/spec/popups/commit_popup_spec.rb b/spec/popups/commit_popup_spec.rb index 9bc3b1749..2fa2294ce 100644 --- a/spec/popups/commit_popup_spec.rb +++ b/spec/popups/commit_popup_spec.rb @@ -3,8 +3,7 @@ require "spec_helper" RSpec.describe "Commit Popup", :git, :nvim, :popup do - before { nvim.keys("c") } - + let(:keymap) { "c" } let(:view) do [ " Arguments ", @@ -18,15 +17,17 @@ " -S Sign using gpg (--gpg-sign=) ", " -C Reuse commit message (--reuse-message=) ", " ", - " Create Edit HEAD Edit ", - " c Commit e Extend f Fixup F Instant Fixup ", - " x Absorb w Reword s Squash S Instant Squash ", - " a Amend A Augment " + " Create Edit HEAD Edit Edit and rebase Spread across commits ", + " c Commit e Extend f Fixup F Instant Fixup x Absorb ", + " s Squash S Instant Squash ", + " a Amend A Alter ", + " n Augment ", + " w Reword W Revise " ] end %w[-a -e -v -h -R -A -s -S -C].each { include_examples "argument", _1 } - %w[c x e w a f s A F S].each { include_examples "interaction", _1 } + %w[c x e w a f s A F S n W].each { include_examples "interaction", _1 } describe "Actions" do describe "Create Commit" do diff --git a/spec/popups/diff_popup_spec.rb b/spec/popups/diff_popup_spec.rb index cff609ffa..b78b695ce 100644 --- a/spec/popups/diff_popup_spec.rb +++ b/spec/popups/diff_popup_spec.rb @@ -3,8 +3,7 @@ require "spec_helper" RSpec.describe "Diff Popup", :git, :nvim, :popup do # rubocop:disable RSpec/EmptyExampleGroup - before { nvim.keys("d") } - + let(:keymap) { "d" } let(:view) do [ " Diff Show ", diff --git a/spec/popups/fetch_popup_spec.rb b/spec/popups/fetch_popup_spec.rb index 390abf356..755d83f3d 100644 --- a/spec/popups/fetch_popup_spec.rb +++ b/spec/popups/fetch_popup_spec.rb @@ -3,8 +3,7 @@ require "spec_helper" RSpec.describe "Fetch Popup", :git, :nvim, :popup do # rubocop:disable RSpec/EmptyExampleGroup - before { nvim.keys("f") } - + let(:keymap) { "f" } let(:view) do [ " Arguments ", diff --git a/spec/popups/help_popup_spec.rb b/spec/popups/help_popup_spec.rb index 8b3846a4d..3b55beb47 100644 --- a/spec/popups/help_popup_spec.rb +++ b/spec/popups/help_popup_spec.rb @@ -3,14 +3,13 @@ require "spec_helper" RSpec.describe "Help Popup", :git, :nvim, :popup do # rubocop:disable RSpec/EmptyExampleGroup - before { nvim.keys("?") } - + let(:keymap) { "?" } let(:view) do [ " Commands Applying changes Essential commands ", " $ History M Remote Stage all Refresh ", " A Cherry Pick m Merge K Untrack Go to file ", - " b Branch P Push s Stage Toggle ", + " b Branch P Push s Stage za, Toggle ", " B Bisect p Pull S Stage unstaged ", " c Commit Q Command u Unstage ", " d Diff r Rebase U Unstage all ", diff --git a/spec/popups/ignore_popup_spec.rb b/spec/popups/ignore_popup_spec.rb index ce833f852..3e8deebc2 100644 --- a/spec/popups/ignore_popup_spec.rb +++ b/spec/popups/ignore_popup_spec.rb @@ -3,8 +3,7 @@ require "spec_helper" RSpec.describe "Ignore Popup", :git, :nvim, :popup do # rubocop:disable RSpec/EmptyExampleGroup - before { nvim.keys("i") } - + let(:keymap) { "i" } let(:view) do [ " Gitignore ", diff --git a/spec/popups/log_popup_spec.rb b/spec/popups/log_popup_spec.rb index 88bfebfb1..06e434b89 100644 --- a/spec/popups/log_popup_spec.rb +++ b/spec/popups/log_popup_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" RSpec.describe "Log Popup", :git, :nvim, :popup do # rubocop:disable RSpec/EmptyExampleGroup - before { nvim.keys("l") } + let(:keymap) { "l" } # TODO: PTY needs to be bigger to show the entire popup let(:view) do diff --git a/spec/popups/merge_popup_spec.rb b/spec/popups/merge_popup_spec.rb index 7a4f207a3..e5c9130f5 100644 --- a/spec/popups/merge_popup_spec.rb +++ b/spec/popups/merge_popup_spec.rb @@ -3,8 +3,7 @@ require "spec_helper" RSpec.describe "Merge Popup", :git, :nvim, :popup do # rubocop:disable RSpec/EmptyExampleGroup - before { nvim.keys("m") } - + let(:keymap) { "m" } let(:view) do [ " Arguments ", diff --git a/spec/popups/pull_popup_spec.rb b/spec/popups/pull_popup_spec.rb index b40e4d79f..b535a1573 100644 --- a/spec/popups/pull_popup_spec.rb +++ b/spec/popups/pull_popup_spec.rb @@ -3,8 +3,7 @@ require "spec_helper" RSpec.describe "Pull Popup", :git, :nvim, :popup do # rubocop:disable RSpec/EmptyExampleGroup - before { nvim.keys("p") } - + let(:keymap) { "p" } let(:view) do [ " Variables ", diff --git a/spec/popups/push_popup_spec.rb b/spec/popups/push_popup_spec.rb index fc6e32bf8..fc9af1360 100644 --- a/spec/popups/push_popup_spec.rb +++ b/spec/popups/push_popup_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" RSpec.describe "Push Popup", :git, :nvim, :popup, :with_remote_origin do - before { nvim.keys("P") } + let(:keymap) { "P" } let(:view) do [ @@ -13,6 +13,8 @@ " -h Disable hooks (--no-verify) ", " -d Dry run (--dry-run) ", " -u Set the upstream before pushing (--set-upstream) ", + " -T Include all tags (--tags) ", + " -t Include related annotated tags (--follow-tags) ", " ", " Push master to Push Configure ", " p pushRemote, setting that o another branch C Set variables... ", diff --git a/spec/popups/rebase_popup_spec.rb b/spec/popups/rebase_popup_spec.rb index 1faad3661..ad43d5101 100644 --- a/spec/popups/rebase_popup_spec.rb +++ b/spec/popups/rebase_popup_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" RSpec.describe "Rebase Popup", :git, :nvim, :popup do # rubocop:disable RSpec/EmptyExampleGroup - before { nvim.keys("r") } + let(:keymap) { "r" } let(:view) do [ diff --git a/spec/popups/remote_popup_spec.rb b/spec/popups/remote_popup_spec.rb index f3bbdd35d..cff948627 100644 --- a/spec/popups/remote_popup_spec.rb +++ b/spec/popups/remote_popup_spec.rb @@ -3,8 +3,7 @@ require "spec_helper" RSpec.describe "Remote Popup", :git, :nvim, :popup do - before { nvim.keys("M") } - + let(:keymap) { "M" } let(:view) do [ " Variables ", diff --git a/spec/popups/reset_popup_spec.rb b/spec/popups/reset_popup_spec.rb index d7543366c..81b170145 100644 --- a/spec/popups/reset_popup_spec.rb +++ b/spec/popups/reset_popup_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" RSpec.describe "Reset Popup", :git, :nvim, :popup do # rubocop:disable RSpec/EmptyExampleGroup - before { nvim.keys("X") } + let(:keymap) { "X" } let(:view) do [ diff --git a/spec/popups/revert_popup_spec.rb b/spec/popups/revert_popup_spec.rb index eb1736a03..07d97b487 100644 --- a/spec/popups/revert_popup_spec.rb +++ b/spec/popups/revert_popup_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" RSpec.describe "Revert Popup", :git, :nvim, :popup do # rubocop:disable RSpec/EmptyExampleGroup - before { nvim.keys("v") } + let(:keymap) { "v" } let(:view) do [ @@ -11,6 +11,9 @@ " =m Replay merge relative to parent (--mainline=) ", " -e Edit commit messages (--edit) ", " -E Don't edit commit messages (--no-edit) ", + " -s Add Signed-off-by lines (--signoff) ", + " =s Strategy (--strategy=) ", + " -S Sign using gpg (--gpg-sign=) ", " ", " Revert ", " v Commit(s) ", @@ -19,5 +22,5 @@ end %w[v V].each { include_examples "interaction", _1 } - %w[=m -e -E].each { include_examples "argument", _1 } + %w[=m -e -E -s =s -S].each { include_examples "argument", _1 } end diff --git a/spec/popups/stash_popup_spec.rb b/spec/popups/stash_popup_spec.rb index 25810ab16..c4c339397 100644 --- a/spec/popups/stash_popup_spec.rb +++ b/spec/popups/stash_popup_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" RSpec.describe "Stash Popup", :git, :nvim, :popup do - before { nvim.keys("Z") } + let(:keymap) { "Z" } let(:view) do [ diff --git a/spec/popups/tag_popup_spec.rb b/spec/popups/tag_popup_spec.rb index 31fdf6227..142aaee96 100644 --- a/spec/popups/tag_popup_spec.rb +++ b/spec/popups/tag_popup_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" RSpec.describe "Tag Popup", :git, :nvim, :popup do # rubocop:disable RSpec/EmptyExampleGroup - before { nvim.keys("t") } + let(:keymap) { "t" } let(:view) do [ diff --git a/spec/popups/worktree_popup_spec.rb b/spec/popups/worktree_popup_spec.rb index d6027b14b..e4a495336 100644 --- a/spec/popups/worktree_popup_spec.rb +++ b/spec/popups/worktree_popup_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" RSpec.describe "Worktree Popup", :git, :nvim, :popup do - before { nvim.keys("w") } + let(:keymap) { "w" } let(:view) do [ diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c2c8f628a..81928d5d7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -6,6 +6,8 @@ require "debug" require "active_support/all" require "timeout" +require "super_diff/rspec" +require "super_diff/active_support" ENV["GIT_CONFIG_GLOBAL"] = "" @@ -51,7 +53,11 @@ end end - # config.around do |example| - # Timeout.timeout(10) { example.call } - # end + if ENV["CI"].present? + config.around do |example| + Timeout.timeout(10) do + example.run + end + end + end end diff --git a/spec/support/shared.rb b/spec/support/shared.rb index 187775c0d..c68584ac5 100644 --- a/spec/support/shared.rb +++ b/spec/support/shared.rb @@ -15,10 +15,26 @@ end RSpec.shared_examples "popup", :popup do + before do + nvim.keys(keymap) + end + it "raises no errors" do expect(nvim.errors).to be_empty end + it "raises no errors with detached HEAD" do + nvim.keys("") # close popup + + # Detach HEAD + git.commit("dummy commit", allow_empty: true) + git.checkout("HEAD^") + + sleep(1) # Allow state to propagate + nvim.keys(keymap) # open popup + expect(nvim.errors).to be_empty + end + it "has correct filetype" do expect(nvim.filetype).to eq("NeogitPopup") end diff --git a/tests/specs/neogit/lib/git/index_spec.lua b/tests/specs/neogit/lib/git/index_spec.lua index 3d1be1cc6..cc0358087 100644 --- a/tests/specs/neogit/lib/git/index_spec.lua +++ b/tests/specs/neogit/lib/git/index_spec.lua @@ -10,17 +10,15 @@ local function run_with_hunk(hunk, from, to, reverse) local header_matches = vim.fn.matchlist(lines[1], "@@ -\\(\\d\\+\\),\\(\\d\\+\\) +\\(\\d\\+\\),\\(\\d\\+\\) @@") return generate_patch_from_selection({ - name = "test.txt", - absolute_path = "test.txt", - diff = { lines = lines }, - }, { first = 1, last = #lines, index_from = header_matches[2], index_len = header_matches[3], diff_from = diff_from, diff_to = #lines, - }, diff_from + from, diff_from + to, reverse) + lines = vim.list_slice(lines, 2), + file = "test.txt", + }, { from = from, to = to, reverse = reverse }) end describe("patch creation", function() diff --git a/tests/specs/neogit/lib/git/log_spec.lua b/tests/specs/neogit/lib/git/log_spec.lua index 446eaa07e..f3618c353 100644 --- a/tests/specs/neogit/lib/git/log_spec.lua +++ b/tests/specs/neogit/lib/git/log_spec.lua @@ -96,6 +96,7 @@ describe("lib.git.log.parse", function() index_from = 692, index_len = 33, length = 40, + file = "lua/neogit/status.lua", line = "@@ -692,33 +692,28 @@ end", lines = { " ---@param first_line number", @@ -149,6 +150,7 @@ describe("lib.git.log.parse", function() index_from = 734, index_len = 14, length = 15, + file = "lua/neogit/status.lua", line = "@@ -734,14 +729,10 @@ function M.get_item_hunks(item, first_line, last_line, partial)", lines = { " setmetatable(o, o)", @@ -290,6 +292,7 @@ describe("lib.git.log.parse", function() index_len = 7, length = 9, line = "@@ -1,7 +1,9 @@", + file = "LICENSE", lines = { " MIT License", " ", diff --git a/tests/util/util.lua b/tests/util/util.lua index f9dabdcc5..c37677062 100644 --- a/tests/util/util.lua +++ b/tests/util/util.lua @@ -74,7 +74,7 @@ function M.ensure_installed(repo, path) if not vim.uv.fs_stat(install_path) then print("* Downloading " .. name .. " to '" .. install_path .. "/'") - vim.fn.system { "git", "clone", "--depth=1", "git@github.com:" .. repo .. ".git", install_path } + vim.fn.system { "git", "clone", "--depth=1", "https://github.com/" .. repo .. ".git", install_path } if vim.v.shell_error > 0 then error(