@@ -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(