diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 000000000..644cd0954
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: [ckolkey]
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 711bddf38..05eab19a8 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -39,3 +39,20 @@ jobs:
with:
bundler-cache: true
- run: bundle exec rubocop
+
+ lua_types:
+ name: lua-typecheck
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: Homebrew/actions/setup-homebrew@master
+ - run: brew install lua-language-server
+ - uses: luarocks/gh-actions-lua@v10
+ with:
+ luaVersion: luajit
+ - uses: luarocks/gh-actions-luarocks@v5
+ with:
+ luaRocksVersion: "3.12.1"
+ - run: |
+ luarocks install llscheck
+ llscheck lua/
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 86d4776f1..0a397c918 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -33,6 +33,10 @@ jobs:
git config --global core.compression 0
git clone https://github.com/nvim-lua/plenary.nvim tmp/plenary
git clone https://github.com/nvim-telescope/telescope.nvim tmp/telescope
+ git clone https://github.com/sindrets/diffview.nvim tmp/diffview
+ git clone https://github.com/nvim-telescope/telescope-fzf-native.nvim tmp/telescope-fzf-native
+ cd tmp/telescope-fzf-native
+ make
- name: E2E Test
run: |
diff --git a/.luarc.json b/.luarc.json
index b03227d4b..3a2495a62 100644
--- a/.luarc.json
+++ b/.luarc.json
@@ -1,6 +1,6 @@
{
- "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
- "Lua.diagnostics.disable": [
+ "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
+ "diagnostics.disable": [
"redefined-local"
],
"diagnostics.globals": [
@@ -9,5 +9,6 @@
"describe",
"before_each"
],
- "workspace.checkThirdParty": "Disable",
+ "workspace.checkThirdParty": false,
+ "runtime.version": "LuaJIT"
}
diff --git a/.ruby-version b/.ruby-version
index bea438e9a..9c25013db 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-3.3.1
+3.3.6
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ac606bc0e..10fd3df64 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -50,9 +50,23 @@ Simply clone *Neogit* to your project directory of choice to be able to use your
Logging is a useful tool for inspecting what happens in the code and in what order. Neogit uses
[`Plenary`](https://github.com/nvim-lua/plenary.nvim) for logging.
-Export the environment variables `NEOGIT_LOG_CONSOLE="sync"` to enable logging, and `NEOGIT_LOG_LEVEL="debug"` for more
-verbose logging.
+#### Enabling logging via environment variables
+- To enable logging to console, export `NEOGIT_LOG_CONSOLE="sync"`
+- To enable logging to a file, export `NEOGIT_LOG_FILE="true"`
+- For more verbose logging, set the log level to `debug` via `NEOGIT_LOG_LEVEL="debug"`
+
+#### Enabling logging via lua api
+
+To turn on logging while neovim is already running, you can use:
+
+```lua
+:lua require("neogit.logger").config.use_file = true -- for logs to ~/.cache/nvim/neogit.log.
+:lua require("neogit.logger").config.use_console = true -- for logs to console.
+:lua require("neogit.logger").config.level = 'debug' -- to set the log level
+```
+
+#### Using the logger from the neogit codebase
```lua
local logger = require("neogit.logger")
@@ -99,9 +113,11 @@ See [the test documentation for more details](./tests/README.md).
### Linting
Additionally, linting is enforced using `selene` to catch common errors, most of which are also caught by
-`lua-language-server`.
+`lua-language-server`. Source code spell checking is done via `typos`.
-```sh make lint ```
+```sh
+make lint
+```
### Formatting
diff --git a/Gemfile b/Gemfile
index f88bcb270..e906795c1 100644
--- a/Gemfile
+++ b/Gemfile
@@ -18,3 +18,5 @@ gem "rubocop-performance"
gem "rubocop-rspec"
gem "super_diff"
gem "tmpdir"
+
+gem "lefthook", "~> 1.7"
diff --git a/Gemfile.lock b/Gemfile.lock
index 905e68bdf..212087ae9 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -43,6 +43,7 @@ GEM
reline (>= 0.4.2)
json (2.7.2)
language_server-protocol (3.17.0.3)
+ lefthook (1.7.22)
logger (1.6.0)
minitest (5.25.1)
msgpack (1.7.2)
@@ -124,6 +125,7 @@ GEM
PLATFORMS
arm64-darwin-22
arm64-darwin-23
+ arm64-darwin-24
x64-mingw-ucrt
x86_64-darwin-20
x86_64-linux
@@ -134,6 +136,7 @@ DEPENDENCIES
debug
fuubar
git
+ lefthook (~> 1.7)
neovim
pastel
quickfix_formatter
@@ -145,7 +148,7 @@ DEPENDENCIES
tmpdir
RUBY VERSION
- ruby 3.3.1p55
+ ruby 3.3.6p108
BUNDLED WITH
- 2.4.21
+ 2.5.23
diff --git a/Makefile b/Makefile
index 5005f526e..19efec42a 100644
--- a/Makefile
+++ b/Makefile
@@ -1,14 +1,17 @@
test:
TEMP_DIR=$$TEMP_DIR TEST_FILES=$$TEST_FILES GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null NVIM_APPNAME=neogit-test nvim --headless -S "./tests/init.lua"
+specs:
+ bundle install && CI=1 bundle exec rspec --format Fuubar
+
lint:
selene --config selene/config.toml lua
typos
-lint-short:
- selene --config selene/config.toml --display-style Quiet lua
-
format:
stylua .
-.PHONY: format lint test
+typecheck:
+ llscheck lua/
+
+.PHONY: format lint typecheck
diff --git a/README.md b/README.md
index 6bf878492..5ab8cae3c 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,17 @@
+
+
+
+
+
+
+
+### [Warp, the intelligent terminal for developers](https://www.warp.dev/neogit)
+#### [Try running neogit in Warp](https://www.warp.dev/neogit)
+
+
+
+
+
@@ -37,19 +51,10 @@ Here's an example spec for [Lazy](https://github.com/folke/lazy.nvim), but you'r
-- Only one of these is needed.
"nvim-telescope/telescope.nvim", -- optional
"ibhagwan/fzf-lua", -- optional
- "echasnovski/mini.pick", -- optional
+ "nvim-mini/mini.pick", -- optional
+ "folke/snacks.nvim", -- optional
},
- config = true
}
-
-```
-
-If you're not using lazy, you'll need to require and setup the plugin like so:
-
-```lua
--- init.lua
-local neogit = require('neogit')
-neogit.setup {}
```
## Compatibility
@@ -58,7 +63,7 @@ The `master` branch will always be compatible with the latest **stable** release
## Configuration
-You can configure neogit by running the `neogit.setup()` function, passing a table as the argument.
+You can configure neogit by running the `require('neogit').setup {}` function, passing a table as the argument.
Default Config
@@ -73,6 +78,8 @@ neogit.setup {
disable_context_highlighting = false,
-- Disables signs for sections/items/hunks
disable_signs = false,
+ -- Offer to force push when branches diverge
+ prompt_force_push = true,
-- Changes what mode the Commit Editor starts in. `true` will leave nvim in normal mode, `false` will change nvim to
-- insert mode, and `"auto"` will change nvim to insert mode IF the commit message is empty, otherwise leaving it in
-- normal mode.
@@ -85,13 +92,35 @@ neogit.setup {
},
-- "ascii" is the graph the git CLI generates
-- "unicode" is the graph like https://github.com/rbong/vim-flog
+ -- "kitty" is the graph like https://github.com/isakbm/gitgraph.nvim - use https://github.com/rbong/flog-symbols if you don't use Kitty
graph_style = "ascii",
- -- Used to generate URL's for branch popup action "pull request".
+ -- Show relative date by default. When set, use `strftime` to display dates
+ commit_date_format = nil,
+ log_date_format = nil,
+ -- Show message with spinning animation when a git command is running.
+ process_spinner = false,
+ -- Used to generate URL's for branch popup action "pull request", "open commit" and "open tree"
git_services = {
- ["github.com"] = "https://github.com/$%7Bowner%7D/$%7Brepository%7D/compare/$%7Bbranch_name%7D?expand=1",
- ["bitbucket.org"] = "https://bitbucket.org/$%7Bowner%7D/$%7Brepository%7D/pull-requests/new?source=${branch_name}&t=1",
- ["gitlab.com"] = "https://gitlab.com/$%7Bowner%7D/$%7Brepository%7D/merge_requests/new?merge_request[source_branch]=${branch_name}",
- ["azure.com"] = "https://dev.azure.com/$%7Bowner%7D/_git/$%7Brepository%7D/pullrequestcreate?sourceRef=${branch_name}&targetRef=${target}",
+ ["github.com"] = {
+ pull_request = "https://github.com/$%7Bowner%7D/$%7Brepository%7D/compare/$%7Bbranch_name%7D?expand=1",
+ commit = "https://github.com/$%7Bowner%7D/$%7Brepository%7D/commit/$%7Boid%7D",
+ tree = "https://${host}/$%7Bowner%7D/$%7Brepository%7D/tree/$%7Bbranch_name%7D",
+ },
+ ["bitbucket.org"] = {
+ pull_request = "https://bitbucket.org/$%7Bowner%7D/$%7Brepository%7D/pull-requests/new?source=${branch_name}&t=1",
+ commit = "https://bitbucket.org/$%7Bowner%7D/$%7Brepository%7D/commits/$%7Boid%7D",
+ tree = "https://bitbucket.org/$%7Bowner%7D/$%7Brepository%7D/branch/$%7Bbranch_name%7D",
+ },
+ ["gitlab.com"] = {
+ pull_request = "https://gitlab.com/$%7Bowner%7D/$%7Brepository%7D/merge_requests/new?merge_request[source_branch]=${branch_name}",
+ commit = "https://gitlab.com/$%7Bowner%7D/$%7Brepository%7D/-/commit/$%7Boid%7D",
+ tree = "https://gitlab.com/$%7Bowner%7D/$%7Brepository%7D/-/tree/$%7Bbranch_name%7D?ref_type=heads",
+ },
+ ["azure.com"] = {
+ pull_request = "https://dev.azure.com/$%7Bowner%7D/_git/$%7Brepository%7D/pullrequestcreate?sourceRef=${branch_name}&targetRef=${target}",
+ commit = "",
+ tree = "",
+ },
},
-- Allows a different telescope sorter. Defaults to 'fuzzy_with_index_bias'. The example below will use the native fzf
-- sorter instead. By default, this function returns `nil`.
@@ -103,13 +132,7 @@ neogit.setup {
-- Scope persisted settings on a per-project basis
use_per_project_settings = true,
-- Table of settings to never persist. Uses format "Filetype--cli-value"
- ignored_settings = {
- "NeogitPushPopup--force-with-lease",
- "NeogitPushPopup--force",
- "NeogitPullPopup--rebase",
- "NeogitCommitPopup--allow-empty",
- "NeogitRevertPopup--no-edit",
- },
+ ignored_settings = {},
-- Configure highlight group features
highlight = {
italic = true,
@@ -126,16 +149,36 @@ neogit.setup {
-- Flag description: https://git-scm.com/docs/git-branch#Documentation/git-branch.txt---sortltkeygt
-- Sorting keys: https://git-scm.com/docs/git-for-each-ref#_options
sort_branches = "-committerdate",
+ -- Value passed to the `---order` flag of the `git log` command
+ -- Determines how commits are traversed and displayed in the log / graph:
+ -- "topo" topological order (parents always before children, good for graphs, slower on large repos)
+ -- "date" chronological order by commit date
+ -- "author-date" chronological order by author date
+ -- "" disable explicit ordering (fastest, recommended for very large repos)
+ commit_order = "topo"
+ -- Default for new branch name prompts
+ initial_branch_name = "",
-- Change the default way of opening neogit
kind = "tab",
- -- Disable line numbers and relative line numbers
+ -- Floating window style
+ floating = {
+ relative = "editor",
+ width = 0.8,
+ height = 0.7,
+ style = "minimal",
+ border = "rounded",
+ },
+ -- Disable line numbers
disable_line_numbers = true,
+ -- Disable relative line numbers
+ disable_relative_line_numbers = true,
-- The time after which an output console is shown for slow running commands
console_timeout = 2000,
-- Automatically show console if a command takes more than console_timeout milliseconds
auto_show_console = true,
-- Automatically close the console if the process exits with a 0 (success) status
auto_close_console = true,
+ notification_icon = "",
status = {
show_head_commit_hash = true,
recent_commit_count = 10,
@@ -191,15 +234,18 @@ neogit.setup {
merge_editor = {
kind = "auto",
},
- tag_editor = {
- kind = "auto",
- },
preview_buffer = {
- kind = "floating",
+ kind = "floating_console",
},
popup = {
kind = "split",
},
+ stash = {
+ kind = "tab",
+ },
+ refs_view = {
+ kind = "tab",
+ },
signs = {
-- { CLOSED, OPENED }
hunk = { "", "" },
@@ -226,6 +272,11 @@ neogit.setup {
-- is also selected then telescope is used instead
-- Requires you to have `echasnovski/mini.pick` installed.
mini_pick = nil,
+
+ -- If enabled, uses snacks.picker for menu selection. If the telescope integration
+ -- is also selected then telescope is used instead
+ -- Requires you to have `folke/snacks.nvim` installed.
+ snacks = nil,
},
sections = {
-- Reverting/Cherry Picking
@@ -279,6 +330,9 @@ neogit.setup {
["q"] = "Close",
[""] = "Submit",
[""] = "Abort",
+ [""] = "PrevMessage",
+ [""] = "NextMessage",
+ [""] = "ResetMessage",
},
commit_editor_I = {
[""] = "Submit",
@@ -314,21 +368,32 @@ neogit.setup {
[""] = "Previous",
[""] = "Next",
[""] = "Previous",
- [""] = "MultiselectToggleNext",
- [""] = "MultiselectTogglePrevious",
+ [""] = "InsertCompletion",
+ [""] = "CopySelection",
+ [""] = "MultiselectToggleNext",
+ [""] = "MultiselectTogglePrevious",
[""] = "NOP",
+ [""] = "ScrollWheelDown",
+ [""] = "ScrollWheelUp",
+ [""] = "NOP",
+ [""] = "NOP",
+ [""] = "MouseClick",
+ ["<2-LeftMouse>"] = "NOP",
},
-- Setting any of these to `false` will disable the mapping.
popup = {
["?"] = "HelpPopup",
["A"] = "CherryPickPopup",
- ["D"] = "DiffPopup",
+ ["d"] = "DiffPopup",
["M"] = "RemotePopup",
["P"] = "PushPopup",
["X"] = "ResetPopup",
["Z"] = "StashPopup",
+ ["i"] = "IgnorePopup",
+ ["t"] = "TagPopup",
["b"] = "BranchPopup",
["B"] = "BisectPopup",
+ ["w"] = "WorktreePopup",
["c"] = "CommitPopup",
["f"] = "FetchPopup",
["l"] = "LogPopup",
@@ -336,26 +401,29 @@ neogit.setup {
["p"] = "PullPopup",
["r"] = "RebasePopup",
["v"] = "RevertPopup",
- ["w"] = "WorktreePopup",
},
status = {
- ["k"] = "MoveUp",
["j"] = "MoveDown",
- ["q"] = "Close",
+ ["k"] = "MoveUp",
["o"] = "OpenTree",
+ ["q"] = "Close",
["I"] = "InitRepo",
["1"] = "Depth1",
["2"] = "Depth2",
["3"] = "Depth3",
["4"] = "Depth4",
+ ["Q"] = "Command",
[""] = "Toggle",
+ ["za"] = "Toggle",
+ ["zo"] = "OpenFold",
["x"] = "Discard",
["s"] = "Stage",
["S"] = "StageUnstaged",
[""] = "StageAll",
- ["K"] = "Untrack",
["u"] = "Unstage",
+ ["K"] = "Untrack",
["U"] = "UnstageStaged",
+ ["y"] = "ShowRefs",
["$"] = "CommandHistory",
["Y"] = "YankSelected",
[""] = "RefreshBuffer",
@@ -370,6 +438,8 @@ neogit.setup {
["]c"] = "OpenOrScrollDown",
[""] = "PeekUp",
[""] = "PeekDown",
+ [""] = "NextSection",
+ [""] = "PreviousSection",
},
},
}
@@ -415,6 +485,7 @@ The `kind` option can be one of the following values:
- `split_below`
- `split_below_all`
- `vsplit`
+- `floating`
- `auto` (`vsplit` if window would have 80 cols, otherwise `split`)
## Popups
@@ -470,6 +541,7 @@ Neogit emits the following events:
| `NeogitTagDelete` | A tag was removed | `{ name: string }` |
| `NeogitCherryPick` | One or more commits were cherry-picked | `{ commits: string[] }` |
| `NeogitMerge` | A merge finished | `{ branch: string, args = string[], status: "ok"\|"conflict" }` |
+| `NeogitStash` | A stash finished | `{ success: boolean }` |
## Versioning
@@ -479,6 +551,15 @@ Neogit follows semantic versioning.
See [CONTRIBUTING.md](https://github.com/NeogitOrg/neogit/blob/master/CONTRIBUTING.md) for more details.
-## Credit
+## Contributors
+
+
+
+
+
+## Special Thanks
+
+- [kolja](https://github.com/kolja) for the Neogit Logo
+- [gitgraph.nvim](https://github.com/isakbm/gitgraph.nvim) for the "kitty" git graph renderer
+- [vim-flog](https://github.com/rbong/vim-flog) for the "unicode" git graph renderer
-Thank you to [kolja](https://github.com/kolja) for the Neogit Logo
diff --git a/bin/specs b/bin/specs
index 4539bfb51..aebb9e8ac 100755
--- a/bin/specs
+++ b/bin/specs
@@ -11,11 +11,96 @@ gemfile do
gem "debug"
end
+require "async"
+require "async/barrier"
+require "async/semaphore"
+
COLOR = Pastel.new
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
-tests = Dir["spec/**/*_spec.rb"]
-length = tests.max_by(&:size).size
+class Runner # rubocop:disable Style/Documentation
+ def initialize(test, spinner, length)
+ @test = test
+ @spinner = spinner
+ @length = length
+ @title = test.gsub("spec/", "")
+ @retries = 0
+ @failed_lines = []
+ end
+
+ def register
+ spinner.update(test: title, padding: " " * (length - test.length))
+ spinner.auto_spin
+ self
+ end
+
+ def call(results, failures) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
+ start!
+
+ loop do
+ output, wait = run
+ results[test] = JSON.parse(output)
+
+ time = results[test].dig("summary", "duration").round(3)
+
+ if wait.value.success?
+ register_success!(time)
+ break
+ elsif retries < 5
+ @failed_lines = JSON.parse(output)["examples"]
+ .select { _1["status"] == "failed" }
+ .map { _1["line_number"] }
+ .uniq
+
+ @retries += 1
+ register_retry!
+ else
+ failures << test
+ register_failure!(time)
+ break
+ end
+ end
+ end
+
+ private
+
+ attr_reader :title, :spinner, :test, :length, :retries
+
+ def start!
+ spinner.update(test: COLOR.blue(title))
+ end
+
+ def run
+ failed = @failed_lines.empty? ? "" : "[#{@failed_lines.join(',')}]"
+ stdin, stdout, wait = Open3.popen2(
+ { "CI" => "1" },
+ "bundle exec rspec #{test}#{failed} --format json --order random"
+ )
+
+ stdin.close
+ output = stdout.read.lines.last
+ stdout.close
+
+ [output, wait]
+ end
+
+ def register_success!(time)
+ spinner.update(test: COLOR.green(title))
+ spinner.success(COLOR.green(time))
+ end
+
+ def register_retry!
+ spinner.update(test: "#{COLOR.yellow(title)} (#{retries})")
+ end
+
+ def register_failure!(time)
+ spinner.update(test: COLOR.red(title))
+ spinner.error(COLOR.red(time))
+ end
+end
+
+tests = Dir["spec/**/*_spec.rb"]
+length = tests.max_by(&:size).size
spinners = TTY::Spinner::Multi.new(
COLOR.blue(":spinner Running #{tests.size} specs"),
format: :bouncing_ball,
@@ -27,37 +112,22 @@ failures = []
start = now
-Sync do |parent| # rubocop:disable Metrics/BlockLength
- tests.map do |test|
- parent.async do
- spinner = spinners.register(
- ":test:padding\t",
- success_mark: COLOR.green.bold("+"),
- error_mark: COLOR.red.bold("x")
- )
+barrier = Async::Barrier.new
+Sync do
+ semaphore = Async::Semaphore.new(Etc.nprocessors - 2, parent: barrier)
- title = test.gsub("spec/", "")
- spinner.update(test: title, padding: " " * (length - test.length))
- spinner.auto_spin
+ runners = tests.map do |test|
+ spinner = spinners.register(
+ ":test:padding\t",
+ success_mark: COLOR.green.bold("+"),
+ error_mark: COLOR.red.bold("x")
+ )
- stdin, stdout, wait = Open3.popen2({ "CI" => "1" }, "bundle exec rspec #{test} --format json --order random")
- stdin.close
-
- output = stdout.read.lines.last
- results[test] = JSON.parse(output)
- stdout.close
-
- time = results[test].dig("summary", "duration").round(3)
+ Runner.new(test, spinner, length).register
+ end
- if wait.value.success?
- spinner.update(test: COLOR.green(title))
- spinner.success(COLOR.green(time))
- else
- failures << test
- spinner.update(test: COLOR.red(title))
- spinner.error(COLOR.red(time))
- end
- end
+ runners.map do |runner|
+ semaphore.async { runner.call(results, failures) }
end.map(&:wait)
end
@@ -68,7 +138,7 @@ if failures.any?
output = results[test]
puts "\nFail: #{output.dig('examples', 0, 'full_description')}"
- puts " #{test}"
+ puts " #{test}:#{output.dig('examples', 0, 'line_number')}"
puts " #{output.dig('examples', 0, 'exception', 'class')}"
puts " #{output.dig('examples', 0, 'exception', 'message')}"
end
diff --git a/doc/neogit.txt b/doc/neogit.txt
index 8cc136c1b..5616a1d92 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|
@@ -82,152 +84,263 @@ to Neovim users.
==============================================================================
2. Plugin Setup *neogit_setup_plugin*
-TODO: Detail what these do
+ >lua
+ local neogit = require("neogit")
- use_default_keymaps = true,
+ neogit.setup {
+ -- Hides the hints at the top of the status buffer
disable_hint = false,
+ -- Disables changing the buffer highlights based on where the cursor is.
disable_context_highlighting = false,
+ -- Disables signs for sections/items/hunks
disable_signs = false,
- graph_style = "ascii",
+ -- Offer to force push when branches diverge
+ prompt_force_push = true,
+ -- Changes what mode the Commit Editor starts in. `true` will leave nvim in normal mode, `false` will change nvim to
+ -- insert mode, and `"auto"` will change nvim to insert mode IF the commit message is empty, otherwise leaving it in
+ -- normal mode.
+ disable_insert_on_commit = "auto",
+ -- When enabled, will watch the `.git/` directory for changes and refresh the status buffer in response to filesystem
+ -- events.
filewatcher = {
- enabled = true,
+ interval = 1000,
+ enabled = true,
},
- telescope_sorter = function()
- return nil
- end,
+ -- "ascii" is the graph the git CLI generates
+ -- "unicode" is the graph like https://github.com/rbong/vim-flog
+ -- "kitty" is the graph like https://github.com/isakbm/gitgraph.nvim - use https://github.com/rbong/flog-symbols if you don't use Kitty
+ graph_style = "ascii",
+ -- Show relative date by default. When set, use `strftime` to display dates
+ commit_date_format = nil,
+ log_date_format = nil,
+ -- Used to generate URL's for branch popup action "pull request" or opening a commit.
git_services = {
- ["github.com"] = "https://github.com/$%7Bowner%7D/$%7Brepository%7D/compare/$%7Bbranch_name%7D?expand=1",
- ["bitbucket.org"] = "https://bitbucket.org/$%7Bowner%7D/$%7Brepository%7D/pull-requests/new?source=${branch_name}&t=1",
- ["gitlab.com"] = "https://gitlab.com/$%7Bowner%7D/$%7Brepository%7D/merge_requests/new?merge_request[source_branch]=${branch_name}",
- ["azure.com"] = "https://dev.azure.com/$%7Bowner%7D/_git/$%7Brepository%7D/pullrequestcreate?sourceRef=${branch_name}&targetRef=${target}",
+ ["github.com"] = {
+ pull_request = "https://github.com/$%7Bowner%7D/$%7Brepository%7D/compare/$%7Bbranch_name%7D?expand=1",
+ commit = "https://github.com/$%7Bowner%7D/$%7Brepository%7D/commit/$%7Boid%7D",
+ tree = "https://${host}/$%7Bowner%7D/$%7Brepository%7D/tree/$%7Bbranch_name%7D",
+ },
+ ["bitbucket.org"] = {
+ pull_request = "https://bitbucket.org/$%7Bowner%7D/$%7Brepository%7D/pull-requests/new?source=${branch_name}&t=1",
+ commit = "https://bitbucket.org/$%7Bowner%7D/$%7Brepository%7D/commits/$%7Boid%7D",
+ tree = "https://bitbucket.org/$%7Bowner%7D/$%7Brepository%7D/branch/$%7Bbranch_name%7D",
+ },
+ ["gitlab.com"] = {
+ pull_request = "https://gitlab.com/$%7Bowner%7D/$%7Brepository%7D/merge_requests/new?merge_request[source_branch]=${branch_name}",
+ commit = "https://gitlab.com/$%7Bowner%7D/$%7Brepository%7D/-/commit/$%7Boid%7D",
+ tree = "https://gitlab.com/$%7Bowner%7D/$%7Brepository%7D/-/tree/$%7Bbranch_name%7D?ref_type=heads",
+ },
+ ["azure.com"] = {
+ pull_request = "https://dev.azure.com/$%7Bowner%7D/_git/$%7Brepository%7D/pullrequestcreate?sourceRef=${branch_name}&targetRef=${target}",
+ commit = "",
+ tree = "",
+ },
},
+ -- Allows a different telescope sorter. Defaults to 'fuzzy_with_index_bias'. The example below will use the native fzf
+ -- sorter instead. By default, this function returns `nil`.
+ telescope_sorter = function()
+ return require("telescope").extensions.fzf.native_fzf_sorter()
+ end,
+ -- Persist the values of switches/options within and across sessions
+ remember_settings = true,
+ -- Scope persisted settings on a per-project basis
+ use_per_project_settings = true,
+ -- Table of settings to never persist. Uses format "Filetype--cli-value"
+ ignored_settings = {},
+ -- Configure highlight group features
highlight = {
- italic = true,
- bold = true,
- underline = true,
+ italic = true,
+ bold = true,
+ underline = true
},
- disable_insert_on_commit = "auto",
- use_per_project_settings = true,
- show_head_commit_hash = true,
- remember_settings = true,
- fetch_after_checkout = false,
+ -- Set to false if you want to be responsible for creating _ALL_ keymappings
+ use_default_keymaps = true,
+ -- Neogit refreshes its internal state after specific events, which can be expensive depending on the repository size.
+ -- Disabling `auto_refresh` will make it so you have to manually refresh the status after you open it.
auto_refresh = true,
+ -- Value used for `--sort` option for `git branch` command
+ -- By default, branches will be sorted by commit date descending
+ -- Flag description: https://git-scm.com/docs/git-branch#Documentation/git-branch.txt---sortltkeygt
+ -- Sorting keys: https://git-scm.com/docs/git-for-each-ref#_options
sort_branches = "-committerdate",
+ -- Value passed to the `---order` flag of the `git log` command
+ -- Determines how commits are traversed and displayed in the log / graph:
+ -- "topo" topological order (parents always before children, good for graphs, slower on large repos)
+ -- "date" chronological order by commit date
+ -- "author-date" chronological order by author date
+ -- "" disable explicit ordering (fastest, recommended for very large repos)
+ commit_order = "topo"
+ -- Default for new branch name prompts
+ initial_branch_name = "",
+ -- Change the default way of opening neogit
kind = "tab",
+ -- Floating window style
+ floating = {
+ relative = "editor",
+ width = 0.8,
+ height = 0.7,
+ style = "minimal",
+ border = "rounded",
+ },
+ -- Disable line numbers
disable_line_numbers = true,
+ -- Disable relative line numbers
+ disable_relative_line_numbers = true,
-- The time after which an output console is shown for slow running commands
console_timeout = 2000,
-- Automatically show console if a command takes more than console_timeout milliseconds
auto_show_console = true,
+ -- Automatically close the console if the process exits with a 0 (success) status
+ auto_close_console = true,
notification_icon = "",
status = {
- recent_commit_count = 10,
- HEAD_folded = false,
+ show_head_commit_hash = true,
+ recent_commit_count = 10,
+ HEAD_padding = 10,
+ HEAD_folded = false,
+ mode_padding = 3,
+ mode_text = {
+ M = "modified",
+ N = "new file",
+ A = "added",
+ D = "deleted",
+ C = "copied",
+ U = "updated",
+ R = "renamed",
+ DD = "unmerged",
+ AU = "unmerged",
+ UD = "unmerged",
+ UA = "unmerged",
+ DU = "unmerged",
+ AA = "unmerged",
+ UU = "unmerged",
+ ["?"] = "",
+ },
},
commit_editor = {
- kind = "tab",
+ kind = "tab",
+ show_staged_diff = true,
+ -- Accepted values:
+ -- "split" to show the staged diff below the commit editor
+ -- "vsplit" to show it to the right
+ -- "split_above" Like :top split
+ -- "vsplit_left" like :vsplit, but open to the left
+ -- "auto" "vsplit" if window would have 80 cols, otherwise "split"
+ staged_diff_split_kind = "split",
+ spell_check = true,
},
commit_select_view = {
- kind = "tab",
+ kind = "tab",
},
commit_view = {
- kind = "vsplit",
- verify_commit = vim.fn.executable("gpg") == 1,
+ kind = "vsplit",
+ verify_commit = vim.fn.executable("gpg") == 1, -- Can be set to true or false, otherwise we try to find the binary
},
log_view = {
- kind = "tab",
+ kind = "tab",
},
rebase_editor = {
- kind = "auto",
+ kind = "auto",
},
reflog_view = {
- kind = "tab",
+ kind = "tab",
},
merge_editor = {
- kind = "auto",
- },
- description_editor = {
- kind = "auto",
- },
- tag_editor = {
- kind = "auto",
+ kind = "auto",
},
preview_buffer = {
- kind = "split",
+ kind = "floating_console",
},
popup = {
- kind = "split",
+ kind = "split",
+ },
+ stash = {
+ kind = "tab",
},
refs_view = {
- kind = "tab",
+ kind = "tab",
},
signs = {
- hunk = { "", "" },
- item = { ">", "v" },
- section = { ">", "v" },
+ -- { CLOSED, OPENED }
+ hunk = { "", "" },
+ item = { ">", "v" },
+ section = { ">", "v" },
},
+ -- Each Integration is auto-detected through plugin presence, however, it can be disabled by setting to `false`
integrations = {
- telescope = nil,
- diffview = nil,
- fzf_lua = nil,
- mini_pick = nil,
+ -- If enabled, use telescope for menu selection rather than vim.ui.select.
+ -- Allows multi-select and some things that vim.ui.select doesn't.
+ telescope = nil,
+ -- Neogit only provides inline diffs. If you want a more traditional way to look at diffs, you can use `diffview`.
+ -- The diffview integration enables the diff popup.
+ --
+ -- Requires you to have `sindrets/diffview.nvim` installed.
+ diffview = nil,
+
+ -- If enabled, uses fzf-lua for menu selection. If the telescope integration
+ -- is also selected then telescope is used instead
+ -- Requires you to have `ibhagwan/fzf-lua` installed.
+ fzf_lua = nil,
+
+ -- If enabled, uses mini.pick for menu selection. If the telescope integration
+ -- is also selected then telescope is used instead
+ -- Requires you to have `echasnovski/mini.pick` installed.
+ mini_pick = nil,
+
+ -- If enabled, uses snacks.picker for menu selection. If the telescope integration
+ -- is also selected then telescope is used instead
+ -- Requires you to have `folke/snacks.nvim` installed.
+ snacks = nil,
},
sections = {
- sequencer = {
+ -- Reverting/Cherry Picking
+ sequencer = {
folded = false,
hidden = false,
- },
- bisect = {
- folded = false,
- hidden = false,
- },
- untracked = {
+ },
+ untracked = {
folded = false,
hidden = false,
- },
- unstaged = {
+ },
+ unstaged = {
folded = false,
hidden = false,
- },
- staged = {
+ },
+ staged = {
folded = false,
hidden = false,
- },
- stashes = {
+ },
+ stashes = {
folded = true,
hidden = false,
- },
- unpulled_upstream = {
+ },
+ unpulled_upstream = {
folded = true,
hidden = false,
- },
- unmerged_upstream = {
+ },
+ unmerged_upstream = {
folded = false,
hidden = false,
- },
- unpulled_pushRemote = {
+ },
+ unpulled_pushRemote = {
folded = true,
hidden = false,
- },
- unmerged_pushRemote = {
+ },
+ unmerged_pushRemote = {
folded = false,
hidden = false,
- },
- recent = {
+ },
+ recent = {
folded = true,
hidden = false,
- },
- rebase = {
+ },
+ rebase = {
folded = true,
hidden = false,
- },
+ },
},
- ignored_settings = {
- "NeogitPushPopup--force-with-lease",
- "NeogitPushPopup--force",
- "NeogitPullPopup--rebase",
- "NeogitCommitPopup--allow-empty",
}
+ >
==============================================================================
Commit Signing / GPG Integration *neogit_setup_gpg*
@@ -295,70 +408,82 @@ The following mappings can all be customized via the setup function.
}
finder = {
- [""] = "Select",
- [""] = "Close",
- [""] = "Close",
- [""] = "Next",
- [""] = "Previous",
- [""] = "Next",
- [""] = "Previous",
- [""] = "MultiselectToggleNext",
- [""] = "MultiselectTogglePrevious",
+ [""] = "Select",
+ [""] = "Close",
+ [""] = "Close",
+ [""] = "Next",
+ [""] = "Previous",
+ [""] = "Next",
+ [""] = "Previous",
+ [""] = "InsertCompletion",
+ [""] = "CopySelection",
+ [""] = "MultiselectToggleNext",
+ [""] = "MultiselectTogglePrevious",
+ [""] = "NOP",
+ [""] = "ScrollWheelDown",
+ [""] = "ScrollWheelUp",
+ [""] = "NOP",
+ [""] = "NOP",
+ [""] = "MouseClick",
+ ["<2-LeftMouse>"] = "NOP",
}
popup = {
["?"] = "HelpPopup",
["A"] = "CherryPickPopup",
- ["B"] = "BisectPopup",
+ ["d"] = "DiffPopup",
+ ["M"] = "RemotePopup",
+ ["P"] = "PushPopup",
+ ["X"] = "ResetPopup",
+ ["Z"] = "StashPopup",
+ ["i"] = "IgnorePopup",
+ ["t"] = "TagPopup",
["b"] = "BranchPopup",
+ ["B"] = "BisectPopup",
+ ["w"] = "WorktreePopup",
["c"] = "CommitPopup",
- ["d"] = "DiffPopup",
["f"] = "FetchPopup",
- ["i"] = "IgnorePopup",
["l"] = "LogPopup",
["m"] = "MergePopup",
- ["M"] = "RemotePopup",
["p"] = "PullPopup",
- ["P"] = "PushPopup",
["r"] = "RebasePopup",
- ["t"] = "TagPopup",
["v"] = "RevertPopup",
- ["w"] = "WorktreePopup",
- ["X"] = "ResetPopup",
- ["Z"] = "StashPopup",
}
status = {
- ["q"] = "Close",
- ["Q"] = "Command",
- ["I"] = "InitRepo",
- ["1"] = "Depth1",
- ["2"] = "Depth2",
- ["3"] = "Depth3",
- ["4"] = "Depth4",
- [""] = "Toggle",
- ["x"] = "Discard",
- ["s"] = "Stage",
- ["S"] = "StageUnstaged",
- [""] = "StageAll",
- ["u"] = "Unstage",
- ["U"] = "UnstageStaged",
- ["y"] = "ShowRefs",
- ["$"] = "CommandHistory",
- ["#"] = "Console",
- ["Y"] = "YankSelected",
- [""] = "RefreshBuffer",
- [""] = "GoToFile",
- [""] = "PeekFile",
- [""] = "VSplitOpen",
- [""] = "SplitOpen",
- [""] = "TabOpen",
- ["{"] = "GoToPreviousHunkHeader",
- ["}"] = "GoToNextHunkHeader",
- ["[c"] = "OpenOrScrollUp",
- ["]c"] = "OpenOrScrollDown",
- [""] = "PeekUp",
- [""] = "PeekDown",
+ ["j"] = "MoveDown",
+ ["k"] = "MoveUp",
+ ["o"] = "OpenTree",
+ ["q"] = "Close",
+ ["I"] = "InitRepo",
+ ["1"] = "Depth1",
+ ["2"] = "Depth2",
+ ["3"] = "Depth3",
+ ["4"] = "Depth4",
+ ["Q"] = "Command",
+ [""] = "Toggle",
+ ["x"] = "Discard",
+ ["s"] = "Stage",
+ ["S"] = "StageUnstaged",
+ [""] = "StageAll",
+ ["u"] = "Unstage",
+ ["K"] = "Untrack",
+ ["U"] = "UnstageStaged",
+ ["y"] = "ShowRefs",
+ ["$"] = "CommandHistory",
+ ["Y"] = "YankSelected",
+ [""] = "RefreshBuffer",
+ [""] = "GoToFile",
+ [""] = "PeekFile",
+ [""] = "VSplitOpen",
+ [""] = "SplitOpen",
+ [""] = "TabOpen",
+ ["{"] = "GoToPreviousHunkHeader",
+ ["}"] = "GoToNextHunkHeader",
+ ["[c"] = "OpenOrScrollUp",
+ ["]c"] = "OpenOrScrollDown",
+ [""] = "PeekUp",
+ [""] = "PeekDown",
}
<
==============================================================================
@@ -381,11 +506,175 @@ The following mappings can all be customized via the setup function.
==============================================================================
4. Events *neogit_events*
-(TODO)
+The following events are emitted by Neogit:
+
+• `NeogitStatusRefreshed`
+ When: Status has been reloaded
+ Data: `{}`
+
+• `NeogitCommitComplete`
+ When: Commit has been created
+ Data: `{}`
+
+• `NeogitPushComplete`
+ When: Push has finished
+ Data: `{}`
+
+• `NeogitPullComplete`
+ When: Push has finished
+ Data: `{}`
+
+• `NeogitFetchComplete`
+ When: Fetch has finished
+ Data: `{}`
+
+• `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*
+To provide a custom color palette directly to the plugin, you can use the
+`config.highlight` table with the following signature: >lua
+
+ ---@class HighlightOptions
+ ---@field italic? boolean
+ ---@field bold? boolean
+ ---@field underline? boolean
+ ---@field bg0? string Darkest background color
+ ---@field bg1? string Second darkest background color
+ ---@field bg2? string Second lightest background color
+ ---@field bg3? string Lightest background color
+ ---@field grey? string middle grey shade for foreground
+ ---@field white? string Foreground white (main text)
+ ---@field red? string Foreground red
+ ---@field bg_red? string Background red
+ ---@field line_red? string Cursor line highlight for red regions
+ ---@field orange? string Foreground orange
+ ---@field bg_orange? string background orange
+ ---@field yellow? string Foreground yellow
+ ---@field bg_yellow? string background yellow
+ ---@field green? string Foreground green
+ ---@field bg_green? string Background green
+ ---@field line_green? string Cursor line highlight for green regions
+ ---@field cyan? string Foreground cyan
+ ---@field bg_cyan? string Background cyan
+ ---@field blue? string Foreground blue
+ ---@field bg_blue? string Background blue
+ ---@field purple? string Foreground purple
+ ---@field bg_purple? string Background purple
+ ---@field md_purple? string Background medium purple
+<
+
+The following highlight groups will all be derived from this palette.
+
The following highlight groups are defined by this plugin. If you set any of
these yourself before the plugin loads, that will be respected. If they do not
exist, they will be created with sensible defaults based on your colorscheme.
@@ -440,7 +729,7 @@ whats recommended. However, if you want to control the style on a per-section
basis, the _actual_ highlight groups on the labels follow this pattern:
`NeogitChange`
-Where `` is one of: (corrospinding to the git mode)
+Where `` is one of: (corresponding to the git mode)
M
A
N
@@ -472,6 +761,7 @@ NeogitDiffContext
NeogitDiffAdd
NeogitDiffDelete
NeogitDiffHeader
+NeogitActiveItem Highlight of current commit-ish open
SIGNS FOR LINE HIGHLIGHTING CURRENT CONTEXT
These are essentially an accented version of the above highlight groups. Only
@@ -497,21 +787,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
@@ -655,13 +945,27 @@ Actions: *neogit_cherry_pick_popup_actions*
Otherwise the user is prompted to select one or more commits.
• Harvest *neogit_cherry_pick_harvest*
- (Not yet implemented)
+ This command moves the selected COMMITS that must be located on another
+ BRANCH onto the current branch instead, removing them from the former.
+ When this command succeeds, then the same branch is current as before.
+
+ Applying the commits on the current branch or removing them from the other
+ branch can lead to conflicts. When that happens, then this command stops
+ and you have to resolve the conflicts and then finish the process manually.
• Squash *neogit_cherry_pick_squash*
- (Not yet implemented)
+ See: |neogit_merge_squash|
• Donate *neogit_cherry_pick_donate*
- (Not yet implemented)
+ This command moves the selected COMMITS from the current branch onto
+ another existing BRANCH, removing them from the former. When this command
+ succeeds, then the same branch is current as before.
+
+ HEAD is allowed to be detached initially.
+
+ Applying the commits on the other branch or removing them from the current
+ branch can lead to conflicts. When that happens, then this command stops
+ and you have to resolve the conflicts and then finish the process manually.
• Spinout *neogit_cherry_pick_spinout*
(Not yet implemented)
@@ -775,7 +1079,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
@@ -786,7 +1090,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
@@ -1034,14 +1338,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.
@@ -1051,7 +1379,44 @@ Actions: *neogit_commit_popup_actions*
==============================================================================
Diff Popup *neogit_diff_popup*
-(TODO)
+The diff popup actions allow inspection of changes in the index (staged
+changes), the working tree (unstaged changes and untracked files), any
+ref range.
+
+For these actions to become available, Neogit needs to be configured to enable
+`diffview.nvim` integration. See |neogit_setup_plugin| and
+https://github.com/sindrets/diffview.nvim.
+
+ • Diff this *neogit_diff_this*
+ Show the diff for the file referred to by a hunk under the cursor.
+
+ • Diff range *neogit_diff_range*
+ View a diff between a specified range of commits. Neogit presents a select
+ for an `A` and a `B` ref and allows specifying the type of range.
+
+ A two-dot range `A..B` shows all of the commits that `B` has that `A`
+ doesn't have. A three-dot range `A...B` shows all of the commits that `A`
+ and `B` have independently, excluding the commits shared by both refs.
+
+
+ • Diff paths *neogit_diff_paths*
+ (Not yet implemented)
+
+ • Diff unstaged *neogit_diff_unstaged*
+ Show the diff for the working tree, without untracked files.
+
+ • Diff staged *neogit_diff_staged*
+ Show the diff for the index.
+
+ • Diff worktree *neogit_diff_worktree*
+ Show the full diff of the working tree, including untracked files.
+
+ • Show commit *neogit_show_commit*
+ Display the diff of the commits within a branch.
+
+ • Show stash *neogit_show_stash*
+ Display the diff of a specific stash.
+
==============================================================================
Fetch Popup *neogit_fetch_popup*
@@ -1123,7 +1488,37 @@ Log Popup *neogit_log_popup*
==============================================================================
Merge Popup *neogit_merge_popup*
-(TODO)
+Arguments: *neogit_merge_popup_args*
+ (TODO)
+
+Actions: *neogit_merge_popup_actions*
+ • Merge *neogit_merge_merge*
+ This command merges another branch or revision into the current branch.
+
+ • Merge and edit message *neogit_merge_editmsg*
+ Like `Merge` above, but opens editor to modify commit message.
+
+ • Merge but don't commit *neogit_merge_nocommit*
+ This command merges another branch or revision into the current branch,
+ but does not actually create the merge commit, allowing the user to make
+ modifications before committing themselves.
+
+ • Absorb *neogit_merge_absorb*
+ (Not yet implemented)
+
+ • Preview Merge *neogit_merge_preview*
+ (Not yet implemented)
+
+ • Squash Merge *neogit_merge_squash*
+ This command squashes the changes introduced by another branch or revision
+ into the current branch. This only applies the changes made by the
+ squashed commits. No information is preserved that would allow creating an
+ actual merge commit.
+
+ Instead of this command you should probably use a cherry-pick command.
+
+ • Dissolve *neogit_merge_dissolve*
+ (Not yet implemented)
==============================================================================
Remote Popup *neogit_remote_popup*
@@ -1178,7 +1573,7 @@ Arguments: *neogit_pull_popup_args*
upstream branch and the upstream branch was rebased since last fetched,
the rebase uses that information to avoid rebasing non-local changes.
- See pull.rebase, branch..rebase and branch.autoSetupRebase if you
+ See pull.rebase, branch..rebase and branch.autoSetupRebase if you
want to make git pull always use --rebase instead of merging.
Note:
@@ -1287,13 +1682,13 @@ Actions: *neogit_push_popup_actions*
the user.
• Push explicit refspecs *neogit_push_explicit_refspecs*
- (Not yet implemented)
+ Push a refspec to a remote.
• Push matching branches *neogit_push_matching_branches*
- (Not yet implemented)
+ Push all matching branches to another repository.
• Push a tag *neogit_push_tag*
- (Not yet implemented)
+ Pushes a single tag to a remote.
• Push all tags *neogit_push_all_tags*
Pushes all tags to selected remote. If only one remote exists, that will
@@ -1532,8 +1927,10 @@ Actions: *neogit_reset_popup_actions*
• Hard *neogit_reset_hard*
Resets the index and working tree. Any changes to tracked files in the
- working tree since are discarded. Any untracked files or
- directories in the way of writing any tracked files are simply deleted.
+ working tree since are discarded, however a reflog entry will be
+ created with their current state, so changes can be restored if needed.
+ Any untracked files or directories in the way of writing any tracked files
+ are simply deleted.
• Keep *neogit_reset_keep*
Resets index entries and updates files in the working tree that are
@@ -1547,12 +1944,80 @@ 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*
-(TODO)
+The stash popup actions will affect the current index (staged changes) and the
+working tree (unstaged changes and untracked files). When the cursor is on a
+stash in the stash list, actions under the "Use" column (pop, apply and drop)
+will affect the stash under the cursor.
+
+Actions: *neogit_stash_popup_actions*
+ • Stash both *neogit_stash_both*
+ Stash both the index and the working tree.
+
+ • Stash index *neogit_stash_index*
+ Stash the index only, excluding unstaged changes and untracked files.
+
+ • Stash worktree *neogit_stash_worktree*
+ (Not yet implemented)
+
+ • Stash keeping index *neogit_stash_keeping_index*
+ Stash both the index and the working tree, but still leave behind the
+ index.
+
+ • Stash push *neogit_stash_push*
+ Select a changed file to push it to the stash.
+
+ • Snapshot both *neogit_snapshot_both*
+ (Not yet implemented)
+
+ • Snapshot index *neogit_snapshot_index*
+ (Not yet implemented)
+
+ • Snapshot worktree *neogit_snapshot_worktree*
+ (Not yet implemented)
+
+ • Snapshot to wip ref *neogit_snapshot_to_wip_ref*
+ (Not yet implemented)
+
+ • Pop *neogit_stash_pop*
+ Apply a stash to the working tree. If there are conflicts, leave the stash
+ intact. If there aren't any conflicts, remove the stash from the list.
+
+ • Apply *neogit_stash_apply*
+ Apply a stash to the working tree without removing it from the stash list.
+
+ • Drop *neogit_stash_drop*
+ Remove a stash from the stash list.
+
+ • List *neogit_stash_list*
+ Display the list of all stashes, and show a diff if a stash is selected.
+
+ • Show *neogit_stash_show*
+ (Not yet implemented)
+
+ • Branch *neogit_stash_branch*
+ (Not yet implemented)
+
+ • Branch here *neogit_stash_branch_here*
+ (Not yet implemented)
+
+ • Rename *neogit_stash_rename*
+ Rename an existing stash.
+
+ • Format patch *neogit_stash_format_patch*
+ (Not yet implemented)
+
==============================================================================
Ignore Popup *neogit_ignore_popup*
@@ -1644,6 +2109,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: ``
@@ -1701,6 +2168,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:
@@ -1714,6 +2183,92 @@ The following keys, in normal mode, will act on the commit under the cursor:
• `b` Insert breakpoint
• `` Open current commit in Commit Buffer
+==============================================================================
+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
+popup instance as it's argument:
+>lua
+ local function my_action(popup)
+ -- You can access the popup state (enabled flags) like so:
+ local cli_args = popup:get_arguments()
+
+ -- You can use Neogit's git abstraction for many common operations
+ -- local git = require("neogit.lib.git")
+
+ -- The input library provides some helpful interfaces for getting user
+ -- input
+ local input = require("neogit.lib.input")
+ local user_input = input.get_user_input("User-specified free text for the action")
+
+ vim.notify(
+ "Hello from my custom action!\n"
+ .. "CLI args: `" .. table.concat(cli_args, " ") .. "`\n"
+ .. "User input: `" .. user_input .. "`")
+ end
+
+ function create_custom_popup()
+ local popup = require("neogit.lib.popup")
+ local p = popup
+ .builder()
+ :name("NeogitMyCustomPopup")
+ -- A switch is a boolean CLI flag, like `--no-verify`
+ :switch("s", "my-switch", "My switch")
+ -- An "_if" variant exists for builder methods, that takes a boolean
+ -- as it's first argument.
+ :switch_if(true, "S", "conditional-switch", "This switch is conditional")
+ -- Options are CLI flags that have a value, like `--strategy=octopus`
+ :option("o", "my-option", "default_value", "My option", { key_prefix = "-" })
+ :new_action_group("My actions")
+ :action("a", "Some action", my_action)
+ -- Data can be stored on the popup instance via the `env`, accessible
+ -- to the action via `popup.state.env.*`
+ :env({ some_data = { "like this" } })
+ :build()
+
+ p:show()
+
+ return p
+ end
+
+ require("neogit")
+<
+
+Look at the builder APIs in `lua/neogit/lib/popup/builder.lua`, the built-in
+popups/actions in `lua/neogit/popups/*`, and the git APIs in
+`lua/neogit/lib/git` for more information (and inspiration!).
+
+To access your custom popup via a keymapping, you can include a mapping when
+calling the setup function:
+>lua
+ require("neogit").setup({
+ mappings = {
+ status = {
+ ["A"] = create_custom_popup,
+ },
+ },
+ })
+<
+
+==============================================================================
+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.
+>lua
+ require("neogit").setup({
+ builders = {
+ NeogitPushPopup = function(builder)
+ builder:switch('m', 'merge_request.create', 'Create merge request', { cli_prefix = '-o ', persisted = false })
+ end,
+ },
+ })
+
+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.
+
------------------------------------------------------------------------------
vim:tw=78:ts=8:ft=help:norl:
-
diff --git a/lefthook.yml b/lefthook.yml
new file mode 100644
index 000000000..0920c0252
--- /dev/null
+++ b/lefthook.yml
@@ -0,0 +1,49 @@
+skip_output:
+ - meta
+pre-push:
+ files: "rg --files"
+ parallel: true
+ commands:
+ rubocop:
+ glob: "*.rb"
+ run: bundle exec rubocop {files}
+ selene:
+ glob: "{lua,plugin}/**/*.lua"
+ run: selene --config selene/config.toml {files}
+ stylua:
+ glob: "*.lua"
+ run: stylua --check {files}
+ typos:
+ run: typos {files}
+ lua-types:
+ glob: "*.lua"
+ run: llscheck lua/ || echo {files}
+ lua-test:
+ glob: "tests/specs/**/*_spec.lua"
+ run: nvim --headless -S "./tests/init.lua" || echo {files}
+ env:
+ - CI: 1
+ - GIT_CONFIG_GLOBAL: /dev/null
+ - 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 ecaac9171..7f66dcce0 100644
--- a/lua/neogit.lua
+++ b/lua/neogit.lua
@@ -84,7 +84,7 @@ local function construct_opts(opts)
if not opts.cwd then
local git = require("neogit.lib.git")
- opts.cwd = git.cli.git_root(".")
+ opts.cwd = git.cli.worktree_root(".")
if opts.cwd == "" then
opts.cwd = vim.uv.cwd()
@@ -111,7 +111,7 @@ local function open_status_buffer(opts)
-- going to open into. We will use vim.fn.lcd() in the status buffer constructor, so this will eventually be
-- correct.
local repo = require("neogit.lib.git.repository").instance(opts.cwd)
- status.new(config.values, repo.git_root, opts.cwd):open(opts.kind):dispatch_refresh()
+ status.new(config.values, repo.worktree_root, opts.cwd):open(opts.kind):dispatch_refresh()
end
---@alias Popup
@@ -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_select_view/init.lua b/lua/neogit/buffers/commit_select_view/init.lua
index 802a30b66..241b0d894 100644
--- a/lua/neogit/buffers/commit_select_view/init.lua
+++ b/lua/neogit/buffers/commit_select_view/init.lua
@@ -7,17 +7,20 @@ local status_maps = require("neogit.config").get_reversed_status_maps()
---@class CommitSelectViewBuffer
---@field commits CommitLogEntry[]
+---@field remotes string[]
---@field header string|nil
local M = {}
M.__index = M
---Opens a popup for selecting a commit
---@param commits CommitLogEntry[]|nil
+---@param remotes string[]
---@param header? string
---@return CommitSelectViewBuffer
-function M.new(commits, header)
+function M.new(commits, remotes, header)
local instance = {
commits = commits,
+ remotes = remotes,
header = header,
buffer = nil,
}
@@ -50,7 +53,7 @@ function M:open(action)
M.instance = self
- ---@type fun(commit: CommitLogEntry[])|nil
+ ---@type fun(commit: string[])|nil
local action = action
self.buffer = Buffer.create {
@@ -114,7 +117,7 @@ function M:open(action)
end
end,
render = function()
- return ui.View(self.commits)
+ return ui.View(self.commits, self.remotes)
end,
}
end
diff --git a/lua/neogit/buffers/commit_select_view/ui.lua b/lua/neogit/buffers/commit_select_view/ui.lua
index 8d5188d38..71841f8e7 100644
--- a/lua/neogit/buffers/commit_select_view/ui.lua
+++ b/lua/neogit/buffers/commit_select_view/ui.lua
@@ -6,13 +6,14 @@ local Graph = require("neogit.buffers.common").CommitGraph
local M = {}
---@param commits CommitLogEntry[]
+---@param remotes string[]
---@return table
-function M.View(commits)
+function M.View(commits, remotes)
return util.filter_map(commits, function(commit)
if commit.oid then
- return Commit(commit, { graph = true, decorate = true })
+ return Commit(commit, remotes, { graph = true, decorate = true })
else
- return Graph(commit)
+ return Graph(commit, #commits[1].abbreviated_commit + 1)
end
end)
end
diff --git a/lua/neogit/buffers/commit_view/init.lua b/lua/neogit/buffers/commit_view/init.lua
index 0a0d32367..c57989942 100644
--- a/lua/neogit/buffers/commit_view/init.lua
+++ b/lua/neogit/buffers/commit_view/init.lua
@@ -5,6 +5,7 @@ local git = require("neogit.lib.git")
local config = require("neogit.config")
local popups = require("neogit.popups")
local status_maps = require("neogit.config").get_reversed_status_maps()
+local notification = require("neogit.lib.notification")
local api = vim.api
@@ -47,8 +48,11 @@ local M = {
---@param filter? string[] Filter diffs to filepaths in table
---@return CommitViewBuffer
function M.new(commit_id, filter)
- local commit_info =
- git.log.parse(git.cli.show.format("fuller").args(commit_id).call({ trim = false }).stdout)[1]
+ local cmd = git.cli.show.format("fuller").args(commit_id)
+ if config.values.commit_date_format ~= nil then
+ cmd = cmd.args("--date=format:" .. config.values.commit_date_format)
+ end
+ local commit_info = git.log.parse(cmd.call({ trim = false }).stdout)[1]
commit_info.commit_arg = commit_id
@@ -78,6 +82,15 @@ function M:close()
M.instance = nil
end
+---@return string
+function M.current_oid()
+ if M.is_open() then
+ return M.instance.commit_info.oid
+ else
+ return "null-oid"
+ end
+end
+
---Opens the CommitViewBuffer if it isn't open or performs the given action
---which is passed the window id of the commit view buffer
---@param commit_id string commit
@@ -89,6 +102,7 @@ function M.open_or_run_in_window(commit_id, filter, cmd)
if M.is_open() and M.instance.commit_info.commit_arg == commit_id then
M.instance.buffer:win_exec(cmd)
else
+ M:close()
local cw = api.nvim_get_current_win()
M.new(commit_id, filter):open()
api.nvim_set_current_win(cw)
@@ -133,18 +147,17 @@ 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
- if M.is_open() then
- M.instance:close()
- end
-
M.instance = self
self.buffer = Buffer.create {
@@ -153,8 +166,29 @@ function M:open(kind)
kind = kind,
status_column = not config.values.disable_signs and "" or nil,
context_highlight = not config.values.disable_context_highlighting,
+ autocmds = {
+ ["WinLeave"] = function()
+ if self.buffer and self.buffer.kind == "floating" then
+ self:close()
+ end
+ end,
+ },
mappings = {
n = {
+ ["o"] = function()
+ if not vim.ui.open then
+ notification.warn("Requires Neovim >= 0.10")
+ return
+ end
+
+ local uri = git.remote.commit_url(https://github.com/self.commit_info.oid)
+ if uri then
+ notification.info(("Opening %q in your browser."):format(uri))
+ vim.ui.open(uri)
+ else
+ notification.warn("Couldn't determine commit URL to open")
+ end
+ end,
[""] = function()
local c = self.buffer.ui:get_component_under_cursor(function(c)
return c.options.highlight == "NeogitFilePath"
@@ -192,9 +226,18 @@ function M:open(kind)
-- Search for a match and jump if we find it
for path, line_nr in pairs(diff_headers) do
+ local path_norm = path
+ for _, kind in ipairs { "modified", "renamed", "new file", "deleted file" } do
+ if vim.startswith(path_norm, kind .. " ") then
+ path_norm = string.sub(path_norm, string.len(kind) + 2)
+ break
+ end
+ end
-- The gsub is to work around the fact that the OverviewFiles use
-- => in renames but the diff header uses ->
- if path:gsub(" %-> ", " => "):match(selected_path) then
+ path_norm = path_norm:gsub(" %-> ", " => ")
+
+ if path_norm == selected_path then
-- Save position in jumplist
vim.cmd("normal! m'")
@@ -244,19 +287,38 @@ function M:open(kind)
vim.cmd("normal! zt")
end
end,
- [popups.mapping_for("CherryPickPopup")] = popups.open("cherry_pick", function(p)
+ [popups.mapping_for("BisectPopup")] = popups.open("bisect", function(p)
p { commits = { self.commit_info.oid } }
end),
[popups.mapping_for("BranchPopup")] = popups.open("branch", function(p)
p { commits = { self.commit_info.oid } }
end),
+ [popups.mapping_for("CherryPickPopup")] = popups.open("cherry_pick", function(p)
+ p { commits = { self.commit_info.oid } }
+ end),
[popups.mapping_for("CommitPopup")] = popups.open("commit", function(p)
p { commit = self.commit_info.oid }
end),
+ [popups.mapping_for("DiffPopup")] = popups.open("diff", function(p)
+ p {
+ section = { name = "log" },
+ item = { name = self.commit_info.oid },
+ }
+ end),
[popups.mapping_for("FetchPopup")] = popups.open("fetch"),
+ -- help
+ [popups.mapping_for("IgnorePopup")] = popups.open("ignore", function(p)
+ local path = self.buffer.ui:get_hunk_or_filename_under_cursor()
+ p {
+ paths = { path and path.escaped_path },
+ worktree_root = git.repo.worktree_root,
+ }
+ end),
+ [popups.mapping_for("LogPopup")] = popups.open("log"),
[popups.mapping_for("MergePopup")] = popups.open("merge", function(p)
p { commit = self.buffer.ui:get_commit_under_cursor() }
end),
+ [popups.mapping_for("PullPopup")] = popups.open("pull"),
[popups.mapping_for("PushPopup")] = popups.open("push", function(p)
p { commit = self.commit_info.oid }
end),
@@ -264,25 +326,18 @@ function M:open(kind)
p { commit = self.commit_info.oid }
end),
[popups.mapping_for("RemotePopup")] = popups.open("remote"),
- [popups.mapping_for("RevertPopup")] = popups.open("revert", function(p)
- p { commits = { self.commit_info.oid } }
- end),
[popups.mapping_for("ResetPopup")] = popups.open("reset", function(p)
p { commit = self.commit_info.oid }
end),
+ [popups.mapping_for("RevertPopup")] = popups.open("revert", function(p)
+ 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("StashPopup")] = popups.open("stash"),
[popups.mapping_for("TagPopup")] = popups.open("tag", function(p)
p { commit = self.commit_info.oid }
end),
- [popups.mapping_for("PullPopup")] = popups.open("pull"),
- [popups.mapping_for("DiffPopup")] = popups.open("diff", function(p)
- p {
- section = { name = "log" },
- item = { name = self.commit_info.oid },
- }
- end),
- [popups.mapping_for("BisectPopup")] = popups.open("bisect", function(p)
- p { commits = { self.commit_info.oid } }
- end),
+ [popups.mapping_for("WorktreePopup")] = popups.open("worktree"),
[status_maps["Close"]] = function()
self:close()
end,
@@ -306,6 +361,8 @@ function M:open(kind)
vim.cmd("normal! zR")
end,
}
+
+ return self
end
return M
diff --git a/lua/neogit/buffers/common.lua b/lua/neogit/buffers/common.lua
index f13c4e4de..625ffbc40 100644
--- a/lua/neogit/buffers/common.lua
+++ b/lua/neogit/buffers/common.lua
@@ -1,6 +1,7 @@
local Ui = require("neogit.lib.ui")
local Component = require("neogit.lib.ui.component")
local util = require("neogit.lib.util")
+local config = require("neogit.config")
local git = require("neogit.lib.git")
local text = Ui.text
@@ -108,10 +109,18 @@ M.List = Component.new(function(props)
return container.tag("List")(children)
end)
-local function build_graph(graph)
+---@return Component[]
+local function build_graph(graph, opts)
+ opts = opts or { remove_dots = false }
+
if type(graph) == "table" then
return util.map(graph, function(g)
- return text(g.text, { highlight = string.format("NeogitGraph%s", g.color) })
+ local char = g.text
+ if opts.remove_dots and vim.tbl_contains({ "", "", "", "", "•" }, char) then
+ char = ""
+ end
+
+ return text(char, { highlight = string.format("NeogitGraph%s", g.color) })
end)
else
return { text(graph, { highlight = "Include" }) }
@@ -137,14 +146,18 @@ local highlight_for_signature = {
N = "NeogitSignatureNone",
}
-M.CommitEntry = Component.new(function(commit, args)
+---@param commit CommitLogEntry
+---@param remotes string[]
+---@param args table
+M.CommitEntry = Component.new(function(commit, remotes, args)
local ref = {}
local ref_last = {}
-
- local info = git.log.branch_info(commit.ref_name, git.remote.list())
+ local info = { head = nil, locals = {}, remotes = {}, tags = {} }
-- Parse out ref names
if args.decorate and commit.ref_name ~= "" then
+ info = git.log.branch_info(commit.ref_name, remotes)
+
-- Render local only branches first
for name, _ in pairs(info.locals) do
if name:match("^refs/") then
@@ -185,11 +198,10 @@ M.CommitEntry = Component.new(function(commit, args)
commit.rel_date = " " .. commit.rel_date
end
- local graph = args.graph and build_graph(commit.graph) or { text("") }
-
local details
if args.details then
- details = col.padding_left(git.log.abbreviated_size() + 1) {
+ local graph = args.graph and build_graph(commit.graph, { remove_dots = true }) or { text("") }
+ details = col.padding_left(#commit.abbreviated_commit + 1) {
row(util.merge(graph, {
text(" "),
text("Author: ", { highlight = "NeogitSubtleText" }),
@@ -243,6 +255,9 @@ M.CommitEntry = Component.new(function(commit, args)
}
end
+ local date = (config.values.log_date_format == nil and commit.rel_date or commit.log_date)
+ local graph = args.graph and build_graph(commit.graph) or { text("") }
+
return col.tag("commit")({
row(
util.merge({
@@ -256,19 +271,25 @@ M.CommitEntry = Component.new(function(commit, args)
virtual_text = {
{ " ", "Constant" },
{
- util.str_clamp(commit.author_name, 30 - (#commit.rel_date > 10 and #commit.rel_date or 10)),
+ util.str_clamp(commit.author_name, 30 - (#date > 10 and #date or 10)),
"NeogitGraphAuthor",
},
- { util.str_min_width(commit.rel_date, 10), "Special" },
+ { util.str_min_width(date, 10), "Special" },
},
}
),
details,
- }, { oid = commit.oid, foldable = args.details == true, folded = true, remote = info.remotes[1] })
+ }, {
+ item = commit,
+ oid = commit.oid,
+ foldable = args.details == true,
+ folded = true,
+ remote = info.remotes[1],
+ })
end)
-M.CommitGraph = Component.new(function(commit, _)
- return col.tag("graph").padding_left(git.log.abbreviated_size() + 1) { row(build_graph(commit.graph)) }
+M.CommitGraph = Component.new(function(commit, padding)
+ return col.tag("graph").padding_left(padding) { row(build_graph(commit.graph)) }
end)
M.Grid = Component.new(function(props)
diff --git a/lua/neogit/buffers/diff/init.lua b/lua/neogit/buffers/diff/init.lua
index 987378d7f..53f42f86e 100644
--- a/lua/neogit/buffers/diff/init.lua
+++ b/lua/neogit/buffers/diff/init.lua
@@ -7,7 +7,7 @@ local api = vim.api
---@class DiffBuffer
---@field buffer Buffer
----@field open fun(self, kind: string)
+---@field open fun(self): DiffBuffer
---@field close fun()
---@field stats table
---@field diffs table
diff --git a/lua/neogit/buffers/editor/init.lua b/lua/neogit/buffers/editor/init.lua
index dbfff356e..e2475ad25 100644
--- a/lua/neogit/buffers/editor/init.lua
+++ b/lua/neogit/buffers/editor/init.lua
@@ -10,21 +10,12 @@ local DiffViewBuffer = require("neogit.buffers.diff")
local pad = util.pad_right
-local M = {}
-
-local filetypes = {
- ["COMMIT_EDITMSG"] = "NeogitCommitMessage",
- ["MERGE_MSG"] = "NeogitMergeMessage",
- ["TAG_EDITMSG"] = "NeogitTagMessage",
- ["EDIT_DESCRIPTION"] = "NeogitBranchDescription",
-}
-
---@class EditorBuffer
---@field filename string filename of buffer
---@field on_unload function callback invoked when buffer is unloaded
---@field show_diff boolean show the diff view or not
---@field buffer Buffer
----@see Buffer
+local M = {}
--- Creates a new EditorBuffer
---@param filename string the filename of buffer
@@ -70,12 +61,9 @@ function M:open(kind)
return message
end
- local filetype = filetypes[self.filename:match("[%u_]+$")] or "NeogitEditor"
- logger.debug("[EDITOR] Filetype " .. filetype)
-
self.buffer = Buffer.create {
name = self.filename,
- filetype = filetype,
+ filetype = "gitcommit",
load = true,
spell_check = config.values.commit_editor.spell_check,
buftype = "",
@@ -96,10 +84,8 @@ function M:open(kind)
end
end,
},
- on_detach = function(buffer)
+ on_detach = function()
logger.debug("[EDITOR] Cleaning Up")
- pcall(vim.treesitter.stop, buffer.handle)
-
if self.on_unload then
logger.debug("[EDITOR] Running on_unload callback")
self.on_unload(aborted and 1 or 0)
@@ -122,10 +108,7 @@ function M:open(kind)
return pad(mapping[name] and mapping[name][1] or "", padding)
end
- local comment_char = git.config.get("core.commentChar"):read()
- or git.config.get_global("core.commentChar"):read()
- or "#"
-
+ local comment_char = git.config.get("core.commentChar"):read() or "#"
logger.debug("[EDITOR] Using comment character '" .. comment_char .. "'")
-- stylua: ignore
@@ -171,19 +154,6 @@ function M:open(kind)
vim.cmd(":startinsert")
end
- -- Source runtime ftplugin
- vim.cmd.source("$VIMRUNTIME/ftplugin/gitcommit.vim")
-
- -- Apply syntax highlighting
- local ok, _ = pcall(vim.treesitter.language.inspect, "gitcommit")
- if ok then
- logger.debug("[EDITOR] Loading treesitter for gitcommit")
- vim.treesitter.start(buffer.handle, "gitcommit")
- else
- logger.debug("[EDITOR] Loading syntax for gitcommit")
- vim.cmd.source("$VIMRUNTIME/syntax/gitcommit.vim")
- end
-
if git.branch.current() then
vim.fn.matchadd("NeogitBranch", git.branch.current(), 100)
end
diff --git a/lua/neogit/buffers/fuzzy_finder.lua b/lua/neogit/buffers/fuzzy_finder.lua
index ae90a5df3..6950e7d7f 100644
--- a/lua/neogit/buffers/fuzzy_finder.lua
+++ b/lua/neogit/buffers/fuzzy_finder.lua
@@ -1,4 +1,5 @@
local Finder = require("neogit.lib.finder")
+local git = require("neogit.lib.git")
local function buffer_height(count)
if count < (vim.o.lines / 2) then
@@ -24,6 +25,17 @@ function M.new(list)
list = list,
}
+ -- If the first item in the list is an git OID, decorate it
+ if type(list[1]) == "string" and list[1]:match("^%x%x%x%x%x%x%x") then
+ local oid = table.remove(list, 1)
+ local ok, result = pcall(git.log.decorate, oid)
+ if ok then
+ table.insert(list, 1, result)
+ else
+ table.insert(list, 1, oid)
+ end
+ end
+
setmetatable(instance, { __index = M })
return instance
diff --git a/lua/neogit/buffers/git_command_history.lua b/lua/neogit/buffers/git_command_history.lua
index 9229a7b9d..c9a2326a4 100644
--- a/lua/neogit/buffers/git_command_history.lua
+++ b/lua/neogit/buffers/git_command_history.lua
@@ -11,8 +11,9 @@ local text = Ui.text
local col = Ui.col
local row = Ui.row
-local command_mask =
- vim.pesc(" --no-pager --literal-pathspecs --no-optional-locks -c core.preloadindex=true -c color.ui=always")
+local command_mask = vim.pesc(
+ " --no-pager --literal-pathspecs --no-optional-locks -c core.preloadindex=true -c color.ui=always -c diff.noprefix=false"
+)
local M = {}
diff --git a/lua/neogit/buffers/log_view/init.lua b/lua/neogit/buffers/log_view/init.lua
index 63ae984fd..d285e0378 100644
--- a/lua/neogit/buffers/log_view/init.lua
+++ b/lua/neogit/buffers/log_view/init.lua
@@ -6,9 +6,12 @@ local status_maps = require("neogit.config").get_reversed_status_maps()
local CommitViewBuffer = require("neogit.buffers.commit_view")
local util = require("neogit.lib.util")
local a = require("plenary.async")
+local notification = require("neogit.lib.notification")
+local git = require("neogit.lib.git")
---@class LogViewBuffer
---@field commits CommitLogEntry[]
+---@field remotes string[]
---@field internal_args table
---@field files string[]
---@field buffer Buffer
@@ -24,11 +27,13 @@ M.__index = M
---@param files string[]|nil list of files to filter by
---@param fetch_func fun(offset: number): CommitLogEntry[]
---@param header string
+---@param remotes string[]
---@return LogViewBuffer
-function M.new(commits, internal_args, files, fetch_func, header)
+function M.new(commits, internal_args, files, fetch_func, header, remotes)
local instance = {
files = files,
commits = commits,
+ remotes = remotes,
internal_args = internal_args,
fetch_func = fetch_func,
buffer = nil,
@@ -78,6 +83,7 @@ function M:open()
context_highlight = false,
header = self.header,
scroll_header = false,
+ active_item_highlight = true,
status_column = not config.values.disable_signs and "" or nil,
mappings = {
v = {
@@ -115,7 +121,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 },
@@ -123,6 +129,25 @@ function M:open()
end),
},
n = {
+ ["o"] = function()
+ if not vim.ui.open then
+ notification.warn("Requires Neovim >= 0.10")
+ return
+ end
+
+ local oid = self.buffer.ui:get_commit_under_cursor()
+ if not oid then
+ return
+ end
+
+ local uri = git.remote.commit_url(https://github.com/oid)
+ if uri then
+ notification.info(("Opening %q in your browser."):format(uri))
+ vim.ui.open(uri)
+ else
+ notification.warn("Couldn't determine commit URL to open")
+ end
+ end,
[popups.mapping_for("BisectPopup")] = popups.open("bisect", function(p)
p { commits = { self.buffer.ui:get_commit_under_cursor() } }
end),
@@ -184,7 +209,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,
@@ -249,7 +276,7 @@ function M:open()
local permit = self.refresh_lock:acquire()
self.commits = util.merge(self.commits, self.fetch_func(self:commit_count()))
- self.buffer.ui:render(unpack(ui.View(self.commits, self.internal_args)))
+ self.buffer.ui:render(unpack(ui.View(self.commits, self.remotes, self.internal_args)))
permit:forget()
end),
@@ -289,7 +316,7 @@ function M:open()
},
},
render = function()
- return ui.View(self.commits, self.internal_args)
+ return ui.View(self.commits, self.remotes, self.internal_args)
end,
after = function(buffer)
-- First line is empty, so move cursor to second line.
diff --git a/lua/neogit/buffers/log_view/ui.lua b/lua/neogit/buffers/log_view/ui.lua
index 131f5b8c8..e4f8e129f 100644
--- a/lua/neogit/buffers/log_view/ui.lua
+++ b/lua/neogit/buffers/log_view/ui.lua
@@ -11,16 +11,17 @@ local row = Ui.row
local M = {}
---@param commits CommitLogEntry[]
+---@param remotes string[]
---@param args table
---@return table
-function M.View(commits, args)
+function M.View(commits, remotes, args)
args.details = true
local graph = util.filter_map(commits, function(commit)
if commit.oid then
- return Commit(commit, args)
+ return Commit(commit, remotes, args)
elseif args.graph then
- return Graph(commit)
+ return Graph(commit, #commits[1].abbreviated_commit + 1)
end
end)
diff --git a/lua/neogit/buffers/process/init.lua b/lua/neogit/buffers/process/init.lua
index 7b59f8483..1e7308ce0 100644
--- a/lua/neogit/buffers/process/init.lua
+++ b/lua/neogit/buffers/process/init.lua
@@ -1,32 +1,22 @@
local Buffer = require("neogit.lib.buffer")
local config = require("neogit.config")
-local status_maps = require("neogit.config").get_reversed_status_maps()
---@class ProcessBuffer
----@field lines integer
+---@field content string[]
---@field truncated boolean
---@field buffer Buffer
----@field open fun(self)
----@field hide fun(self)
----@field close fun(self)
----@field focus fun(self)
----@field show fun(self)
----@field is_visible fun(self): boolean
----@field append fun(self, data: string)
----@field new fun(self, table): ProcessBuffer
----@see Buffer
----@see Ui
+---@field process Process
local M = {}
M.__index = M
+---@param process Process
+---@param mask_fn fun(string):string
---@return ProcessBuffer
----@param process ProcessOpts
-function M:new(process)
+function M:new(process, mask_fn)
local instance = {
- content = string.format("> %s\r\n", table.concat(process.cmd, " ")),
+ content = { string.format("> %s\r\n", mask_fn(table.concat(process.cmd, " "))) },
process = process,
buffer = nil,
- lines = 0,
truncated = false,
}
@@ -42,6 +32,7 @@ end
function M:close()
if self.buffer then
+ self.buffer:close_terminal_channel()
self.buffer:close()
self.buffer = nil
end
@@ -58,42 +49,45 @@ function M:show()
end
self.buffer:show()
- self:refresh()
+ self:flush_content()
end
+---@return boolean
function M:is_visible()
return self.buffer and self.buffer:is_valid() and self.buffer:is_visible()
end
-function M:refresh()
- self.buffer:chan_send(self.content)
- self.buffer:move_cursor(self.buffer:line_count())
-end
-
+---@param data string
function M:append(data)
- self.lines = self.lines + 1
- if self.lines > 300 then
- if not self.truncated then
- self.content = table.concat({ self.content, "\r\n[Output too long - Truncated]" }, "\r\n")
- self.truncated = true
-
- if self:is_visible() then
- self:refresh()
- end
- end
-
- return
+ assert(data, "no data to append")
+
+ if self:is_visible() then
+ self:flush_content()
+ self.buffer:chan_send(data .. "\r\n")
+ else
+ table.insert(self.content, data)
end
+end
- self.content = table.concat({ self.content, data }, "\r\n")
+---@param data string
+function M:append_partial(data)
+ assert(data, "no data to append")
if self:is_visible() then
- self:refresh()
+ self.buffer:chan_send(data)
end
end
-local function hide(self)
+function M:flush_content()
+ if #self.content > 0 then
+ self.buffer:chan_send(table.concat(self.content, "\r\n") .. "\r\n")
+ self.content = {}
+ end
+end
+
+local function close(self)
return function()
+ self.process:stop()
self:close()
end
end
@@ -104,6 +98,8 @@ function M:open()
return self
end
+ local status_maps = config.get_reversed_status_maps()
+
self.buffer = Buffer.create {
name = "NeogitConsole",
filetype = "NeogitConsole",
@@ -111,22 +107,16 @@ function M:open()
open = false,
buftype = false,
kind = config.values.preview_buffer.kind,
- on_detach = function()
- self.buffer = nil
+ after = function(buffer)
+ buffer:open_terminal_channel()
end,
- autocmds = {
- ["WinLeave"] = function()
- pcall(self.close, self)
- end,
- },
mappings = {
- t = {
- [status_maps["Close"]] = hide(self),
- [""] = hide(self),
- },
n = {
- [status_maps["Close"]] = hide(self),
- [""] = hide(self),
+ [""] = function()
+ pcall(self.process.stop, self.process)
+ end,
+ [status_maps["Close"]] = close(self),
+ [""] = close(self),
},
},
}
diff --git a/lua/neogit/buffers/rebase_editor/init.lua b/lua/neogit/buffers/rebase_editor/init.lua
index 671a4b93b..10149c304 100644
--- a/lua/neogit/buffers/rebase_editor/init.lua
+++ b/lua/neogit/buffers/rebase_editor/init.lua
@@ -61,10 +61,7 @@ function M.new(filename, on_unload)
end
function M:open(kind)
- local comment_char = git.config.get("core.commentChar"):read()
- or git.config.get_global("core.commentChar"):read()
- or "#"
-
+ local comment_char = git.config.get("core.commentChar"):read() or "#"
local mapping = config.get_reversed_rebase_editor_maps()
local mapping_I = config.get_reversed_rebase_editor_maps_I()
local aborted = false
@@ -72,7 +69,7 @@ function M:open(kind)
self.buffer = Buffer.create {
name = self.filename,
load = true,
- filetype = "NeogitRebaseTodo",
+ filetype = "gitrebase",
buftype = "",
status_column = not config.values.disable_signs and "" or nil,
kind = kind,
@@ -80,9 +77,7 @@ function M:open(kind)
disable_line_numbers = config.values.disable_line_numbers,
disable_relative_line_numbers = config.values.disable_relative_line_numbers,
readonly = false,
- on_detach = function(buffer)
- pcall(vim.treesitter.stop, buffer.handle)
-
+ on_detach = function()
if self.on_unload then
self.on_unload(aborted and 1 or 0)
end
@@ -130,17 +125,6 @@ function M:open(kind)
buffer:set_lines(-1, -1, false, help_lines)
buffer:write()
buffer:move_cursor(1)
-
- -- Source runtime ftplugin
- vim.cmd.source("$VIMRUNTIME/ftplugin/gitrebase.vim")
-
- -- Apply syntax highlighting
- local ok, _ = pcall(vim.treesitter.language.inspect, "git_rebase")
- if ok then
- vim.treesitter.start(buffer.handle, "git_rebase")
- else
- vim.cmd.source("$VIMRUNTIME/syntax/gitrebase.vim")
- end
end,
mappings = {
i = {
diff --git a/lua/neogit/buffers/reflog_view/init.lua b/lua/neogit/buffers/reflog_view/init.lua
index d740c63b4..986fca7d1 100644
--- a/lua/neogit/buffers/reflog_view/init.lua
+++ b/lua/neogit/buffers/reflog_view/init.lua
@@ -4,6 +4,8 @@ local config = require("neogit.config")
local popups = require("neogit.popups")
local status_maps = require("neogit.config").get_reversed_status_maps()
local CommitViewBuffer = require("neogit.buffers.commit_view")
+local notification = require("neogit.lib.notification")
+local git = require("neogit.lib.git")
---@class ReflogViewBuffer
---@field entries ReflogEntry[]
@@ -56,6 +58,7 @@ function M:open(_)
scroll_header = true,
status_column = not config.values.disable_signs and "" or nil,
context_highlight = true,
+ active_item_highlight = true,
mappings = {
v = {
[popups.mapping_for("CherryPickPopup")] = popups.open("cherry_pick", function(p)
@@ -88,7 +91,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 },
@@ -99,6 +102,25 @@ function M:open(_)
end),
},
n = {
+ ["o"] = function()
+ if not vim.ui.open then
+ notification.warn("Requires Neovim >= 0.10")
+ return
+ end
+
+ local oid = self.buffer.ui:get_commit_under_cursor()
+ if not oid then
+ return
+ end
+
+ local uri = git.remote.commit_url(https://github.com/oid)
+ if uri then
+ notification.info(("Opening %q in your browser."):format(uri))
+ vim.ui.open(uri)
+ else
+ notification.warn("Couldn't determine commit URL to open")
+ end
+ end,
[popups.mapping_for("BisectPopup")] = popups.open("bisect", function(p)
p { commits = { self.buffer.ui:get_commit_under_cursor() } }
end),
diff --git a/lua/neogit/buffers/reflog_view/ui.lua b/lua/neogit/buffers/reflog_view/ui.lua
index 8e4c587de..ac777ed06 100644
--- a/lua/neogit/buffers/reflog_view/ui.lua
+++ b/lua/neogit/buffers/reflog_view/ui.lua
@@ -1,6 +1,7 @@
local Ui = require("neogit.lib.ui")
local Component = require("neogit.lib.ui.component")
local util = require("neogit.lib.util")
+local config = require("neogit.config")
local col = Ui.col
local row = Ui.row
@@ -25,7 +26,13 @@ local function highlight_for_type(type)
end
M.Entry = Component.new(function(entry, total)
- local date_number, date_quantifier = unpack(vim.split(entry.rel_date, " "))
+ local date
+ if config.values.log_date_format == nil then
+ local date_number, date_quantifier = unpack(vim.split(entry.rel_date, " "))
+ date = date_number .. date_quantifier:sub(1, 1)
+ else
+ date = entry.commit_date
+ end
return col({
row({
@@ -38,10 +45,13 @@ M.Entry = Component.new(function(entry, total)
virtual_text = {
{ " ", "Constant" },
-- { util.str_clamp(entry.author_name, 20 - #tostring(date_number)), "Constant" },
- { date_number .. date_quantifier:sub(1, 1), "Special" },
+ { date, "Special" },
},
}),
- }, { oid = entry.oid })
+ }, {
+ oid = entry.oid,
+ item = entry,
+ })
end)
---@param entries ReflogEntry[]
diff --git a/lua/neogit/buffers/refs_view/init.lua b/lua/neogit/buffers/refs_view/init.lua
index 750d143df..cf812c0a1 100644
--- a/lua/neogit/buffers/refs_view/init.lua
+++ b/lua/neogit/buffers/refs_view/init.lua
@@ -3,11 +3,13 @@ local config = require("neogit.config")
local ui = require("neogit.buffers.refs_view.ui")
local popups = require("neogit.popups")
local status_maps = require("neogit.config").get_reversed_status_maps()
+local mapping = config.get_reversed_refs_view_maps()
local CommitViewBuffer = require("neogit.buffers.commit_view")
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
@@ -49,6 +51,36 @@ function M.is_open()
return (M.instance and M.instance.buffer and M.instance.buffer:is_visible()) == true
end
+function M._do_delete(ref)
+ if not ref.remote then
+ git.branch.delete(ref.unambiguous_name)
+ else
+ git.cli.push.remote(ref.remote).delete.to(ref.name).call()
+ end
+end
+
+function M.delete_branch(ref)
+ if ref then
+ local input = require("neogit.lib.input")
+ local message = ("Delete branch: '%s'?"):format(ref.unambiguous_name)
+ if input.get_permission(message) then
+ M._do_delete(ref)
+ end
+ end
+end
+
+function M.delete_branches(refs)
+ if #refs > 0 then
+ local input = require("neogit.lib.input")
+ local message = ("Delete %s branch(es)?"):format(#refs)
+ if input.get_permission(message) then
+ for _, ref in ipairs(refs) do
+ M._do_delete(ref)
+ end
+ end
+ end
+end
+
--- Opens the RefsViewBuffer
function M:open()
if M.is_open() then
@@ -102,12 +134,16 @@ 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 },
}
end),
+ [mapping["DeleteBranch"]] = function()
+ M.delete_branches(self.buffer.ui:get_refs_under_cursor())
+ self:redraw()
+ end,
},
n = {
[popups.mapping_for("CherryPickPopup")] = popups.open("cherry_pick", function(p)
@@ -121,6 +157,10 @@ function M:open()
suggested_branch_name = ref and ref.name,
}
end),
+ [mapping["DeleteBranch"]] = function()
+ M.delete_branch(self.buffer.ui:get_ref_under_cursor())
+ self:redraw()
+ end,
[popups.mapping_for("CommitPopup")] = popups.open("commit", function(p)
p { commit = self.buffer.ui:get_commits_in_selection()[1] }
end),
@@ -290,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 d69b1e340..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)
@@ -133,7 +138,7 @@ function M.Remotes(remotes, head)
text.highlight("NeogitBranch")("Remote "),
text.highlight("NeogitRemote")(name, { align_right = max_len }),
text.highlight("NeogitBranch")(
- string.format(" (%s)", git.config.get(string.format("remote.%s.url", name)):read())
+ string.format(" (%s)", git.config.get_local(string.format("remote.%s.url", name)):read())
),
}, head)
)
diff --git a/lua/neogit/buffers/stash_list_view/init.lua b/lua/neogit/buffers/stash_list_view/init.lua
index adeb08d05..95eb6b484 100644
--- a/lua/neogit/buffers/stash_list_view/init.lua
+++ b/lua/neogit/buffers/stash_list_view/init.lua
@@ -39,9 +39,11 @@ function M:open()
scroll_header = true,
kind = config.values.stash.kind,
context_highlight = true,
+ active_item_highlight = true,
mappings = {
v = {
[popups.mapping_for("CherryPickPopup")] = function()
+ -- TODO: implement
-- local stash = self.buffer.ui:get_commit_under_cursor()[1]
-- if stash then
-- local stash_item = util.find(self.stashes, function(s)
@@ -95,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),
@@ -164,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/stash_list_view/ui.lua b/lua/neogit/buffers/stash_list_view/ui.lua
index eb18ae1cd..76c2b2f34 100644
--- a/lua/neogit/buffers/stash_list_view/ui.lua
+++ b/lua/neogit/buffers/stash_list_view/ui.lua
@@ -1,6 +1,7 @@
local Ui = require("neogit.lib.ui")
local Component = require("neogit.lib.ui.component")
local util = require("neogit.lib.util")
+local config = require("neogit.config")
local text = Ui.text
local col = Ui.col
@@ -19,10 +20,10 @@ M.Stash = Component.new(function(stash)
}, {
virtual_text = {
{ " ", "Constant" },
- { stash.rel_date, "Special" },
+ { config.values.log_date_format ~= nil and stash.date or stash.rel_date, "Special" },
},
}),
- }, { oid = label })
+ }, { oid = label, item = stash })
end)
---@param stashes StashItem[]
diff --git a/lua/neogit/buffers/status/actions.lua b/lua/neogit/buffers/status/actions.lua
index 6dff99b97..a28d2fec6 100644
--- a/lua/neogit/buffers/status/actions.lua
+++ b/lua/neogit/buffers/status/actions.lua
@@ -7,24 +7,47 @@ local logger = require("neogit.logger")
local input = require("neogit.lib.input")
local notification = require("neogit.lib.notification")
local util = require("neogit.lib.util")
+local config = require("neogit.config")
local FuzzyFinderBuffer = require("neogit.buffers.fuzzy_finder")
local fn = vim.fn
local api = vim.api
-local function cleanup_items(...)
+local function cleanup_dir(dir)
if vim.in_fast_event() then
a.util.scheduler()
end
- for _, item in ipairs { ... } do
- local bufnr = fn.bufexists(item.name)
- if bufnr and bufnr > 0 and api.nvim_buf_is_valid(bufnr) then
- api.nvim_buf_delete(bufnr, { force = true })
+ for name, type in vim.fs.dir(dir, { depth = math.huge }) do
+ if type == "file" then
+ local bufnr = fn.bufnr(name)
+ if bufnr > 0 then
+ api.nvim_buf_delete(bufnr, { force = false })
+ end
+ end
+ end
+
+ fn.delete(dir, "rf")
+end
+
+---@param items StatusItem[]
+local function cleanup_items(items)
+ if vim.in_fast_event() then
+ a.util.scheduler()
+ end
+
+ for _, item in ipairs(items) do
+ local path = item.absolute_path or item.name
+ logger.debug("[cleanup_items()] Cleaning " .. vim.inspect(path))
+ assert(path, "cleanup_items() - item must have a name")
+
+ local bufnr = fn.bufnr(path)
+ if bufnr > 0 then
+ api.nvim_buf_delete(bufnr, { force = false })
end
- fn.delete(item.name)
+ fn.delete(fn.fnameescape(path))
end
end
@@ -54,11 +77,13 @@ local function translate_cursor_location(self, item)
end
local function open(type, path, cursor)
- local command = ("silent! %s %s | %s | redraw! | norm! zz"):format(
- type,
- fn.fnameescape(path),
- cursor and cursor[1] or "1"
- )
+ local command = ("silent! %s %s | %s"):format(type, fn.fnameescape(path), cursor and cursor[1] or "1")
+
+ logger.debug("[Status - Open] '" .. command .. "'")
+
+ vim.cmd(command)
+
+ command = "redraw! | norm! zz"
logger.debug("[Status - Open] '" .. command .. "'")
@@ -68,6 +93,7 @@ end
local M = {}
---@param self StatusBuffer
+---@return fun(): nil
M.v_discard = function(self)
return a.void(function()
local selection = self.buffer.ui:get_selection()
@@ -102,7 +128,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))
@@ -154,7 +181,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
@@ -164,10 +191,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
@@ -191,6 +218,7 @@ M.v_discard = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.v_stage = function(self)
return a.void(function()
local selection = self.buffer.ui:get_selection()
@@ -213,7 +241,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
@@ -247,6 +275,7 @@ M.v_stage = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.v_unstage = function(self)
return a.void(function()
local selection = self.buffer.ui:get_selection()
@@ -263,7 +292,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)
@@ -289,6 +321,7 @@ M.v_unstage = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.v_branch_popup = function(self)
return popups.open("branch", function(p)
p { commits = self.buffer.ui:get_commits_in_selection() }
@@ -296,6 +329,7 @@ M.v_branch_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.v_cherry_pick_popup = function(self)
return popups.open("cherry_pick", function(p)
p { commits = self.buffer.ui:get_commits_in_selection() }
@@ -303,6 +337,7 @@ M.v_cherry_pick_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.v_commit_popup = function(self)
return popups.open("commit", function(p)
local commits = self.buffer.ui:get_commits_in_selection()
@@ -313,6 +348,7 @@ M.v_commit_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.v_merge_popup = function(self)
return popups.open("merge", function(p)
local commits = self.buffer.ui:get_commits_in_selection()
@@ -323,6 +359,7 @@ M.v_merge_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.v_push_popup = function(self)
return popups.open("push", function(p)
local commits = self.buffer.ui:get_commits_in_selection()
@@ -333,6 +370,7 @@ M.v_push_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.v_rebase_popup = function(self)
return popups.open("rebase", function(p)
local commits = self.buffer.ui:get_commits_in_selection()
@@ -343,6 +381,7 @@ M.v_rebase_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.v_revert_popup = function(self)
return popups.open("revert", function(p)
p { commits = self.buffer.ui:get_commits_in_selection() }
@@ -350,6 +389,7 @@ M.v_revert_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.v_reset_popup = function(self)
return popups.open("reset", function(p)
local commits = self.buffer.ui:get_commits_in_selection()
@@ -360,6 +400,7 @@ M.v_reset_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.v_tag_popup = function(self)
return popups.open("tag", function(p)
local commits = self.buffer.ui:get_commits_in_selection()
@@ -370,6 +411,7 @@ M.v_tag_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.v_stash_popup = function(self)
return popups.open("stash", function(p)
local stash = self.buffer.ui:get_yankable_under_cursor()
@@ -378,6 +420,7 @@ M.v_stash_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.v_diff_popup = function(self)
return popups.open("diff", function(p)
local section = self.buffer.ui:get_selection().section
@@ -387,13 +430,15 @@ M.v_diff_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.v_ignore_popup = function(self)
return popups.open("ignore", function(p)
- p { paths = self.buffer.ui:get_filepaths_in_selection(), git_root = git.repo.git_root }
+ p { paths = self.buffer.ui:get_filepaths_in_selection(), worktree_root = git.repo.worktree_root }
end)
end
---@param self StatusBuffer
+---@return fun(): nil
M.v_bisect_popup = function(self)
return popups.open("bisect", function(p)
p { commits = self.buffer.ui:get_commits_in_selection() }
@@ -401,36 +446,51 @@ M.v_bisect_popup = function(self)
end
---@param _self StatusBuffer
+---@return fun(): nil
M.v_remote_popup = function(_self)
return popups.open("remote")
end
---@param _self StatusBuffer
+---@return fun(): nil
M.v_fetch_popup = function(_self)
return popups.open("fetch")
end
---@param _self StatusBuffer
+---@return fun(): nil
M.v_pull_popup = function(_self)
return popups.open("pull")
end
---@param _self StatusBuffer
+---@return fun(): nil
M.v_help_popup = function(_self)
return popups.open("help")
end
---@param _self StatusBuffer
+---@return fun(): nil
M.v_log_popup = function(_self)
return popups.open("log")
end
+---@param self StatusBuffer
+---@return fun(): nil
+M.v_margin_popup = function(self)
+ return popups.open("margin", function(p)
+ p { buffer = self }
+ end)
+end
+
---@param _self StatusBuffer
+---@return fun(): nil
M.v_worktree_popup = function(_self)
return popups.open("worktree")
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_down = function(self)
return function()
if vim.v.count > 0 then
@@ -446,6 +506,7 @@ M.n_down = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_up = function(self)
return function()
if vim.v.count > 0 then
@@ -461,6 +522,7 @@ M.n_up = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_toggle = function(self)
return function()
local fold = self.buffer.ui:get_fold_under_cursor()
@@ -480,11 +542,49 @@ M.n_toggle = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
+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
+---@return fun(): nil
+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
+---@return fun(): nil
M.n_close = function(self)
return require("neogit.lib.ui.helpers").close_topmost(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_open_or_scroll_down = function(self)
return function()
local commit = self.buffer.ui:get_commit_under_cursor()
@@ -495,6 +595,7 @@ M.n_open_or_scroll_down = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_open_or_scroll_up = function(self)
return function()
local commit = self.buffer.ui:get_commit_under_cursor()
@@ -505,6 +606,7 @@ M.n_open_or_scroll_up = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_refresh_buffer = function(self)
return a.void(function()
self:dispatch_refresh({ update_diffs = { "*:*" } }, "n_refresh_buffer")
@@ -512,6 +614,7 @@ M.n_refresh_buffer = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_depth1 = function(self)
return function()
local section = self.buffer.ui:get_current_section()
@@ -530,6 +633,7 @@ M.n_depth1 = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_depth2 = function(self)
return function()
local section = self.buffer.ui:get_current_section()
@@ -557,6 +661,7 @@ M.n_depth2 = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_depth3 = function(self)
return function()
local section = self.buffer.ui:get_current_section()
@@ -586,6 +691,7 @@ M.n_depth3 = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_depth4 = function(self)
return function()
local section = self.buffer.ui:get_current_section()
@@ -612,6 +718,7 @@ M.n_depth4 = function(self)
end
---@param _self StatusBuffer
+---@return fun(): nil
M.n_command_history = function(_self)
return a.void(function()
require("neogit.buffers.git_command_history"):new():show()
@@ -619,13 +726,15 @@ M.n_command_history = function(_self)
end
---@param _self StatusBuffer
+---@return fun(): nil
M.n_show_refs = function(_self)
return a.void(function()
- require("neogit.buffers.refs_view").new(git.refs.list_parsed(), git.repo.git_root):open()
+ require("neogit.buffers.refs_view").new(git.refs.list_parsed(), git.repo.worktree_root):open()
end)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_yank_selected = function(self)
return function()
local yank = self.buffer.ui:get_yankable_under_cursor()
@@ -644,6 +753,7 @@ M.n_yank_selected = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_discard = function(self)
return a.void(function()
git.index.update()
@@ -659,11 +769,20 @@ M.n_discard = function(self)
if selection.item and selection.item.first == fn.line(".") then -- Discard File
if section == "untracked" then
- message = ("Discard %q?"):format(selection.item.name)
- action = function()
- cleanup_items(selection.item)
- end
+ local mode = git.config.get("status.showUntrackedFiles"):read()
+
refresh = { update_diffs = { "untracked:" .. selection.item.name } }
+ if mode == "all" then
+ message = ("Discard %q?"):format(selection.item.name)
+ action = function()
+ cleanup_items { selection.item }
+ end
+ else
+ message = ("Recursively discard %q?"):format(selection.item.name)
+ action = function()
+ cleanup_dir(selection.item.name)
+ end
+ end
elseif section == "unstaged" then
if selection.item.mode:match("^[UAD][UAD]") then
choices = { "&ours", "&theirs", "&conflict", "&abort" }
@@ -688,7 +807,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
@@ -719,14 +838,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 }
@@ -747,7 +866,6 @@ M.n_discard = function(self)
end
elseif selection.item then -- Discard Hunk
if selection.item.mode == "UU" then
- -- TODO: https://github.com/emacs-mirror/emacs/blob/master/lisp/vc/smerge-mode.el
notification.warn("Resolve conflicts in file before discarding hunks.")
return
end
@@ -755,17 +873,11 @@ 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?"
action = function()
- local hunks =
- self.buffer.ui:item_hunks(selection.item, selection.first_line, selection.last_line, false)
-
- local patch = git.index.generate_patch(selection.item, hunks[1], hunks[1].from, hunks[1].to, true)
-
- git.index.apply(patch, { reverse = true })
git.index.apply(patch, { reverse = true })
end
refresh = { update_diffs = { "untracked:" .. selection.item.name } }
@@ -786,7 +898,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
@@ -799,7 +911,7 @@ M.n_discard = function(self)
end
if conflict then
- -- TODO: https://github.com/magit/magit/blob/28bcd29db547ab73002fb81b05579e4a2e90f048/lisp/magit-apply.el#Lair
+ -- TODO: https://github.com/magit/magit/blob/28bcd29db547ab73002fb81b05579e4a2e90f048/lisp/magit-apply.el#L515
notification.warn("Resolve conflicts before discarding section.")
return
else
@@ -819,7 +931,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
@@ -832,9 +944,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
@@ -874,6 +987,7 @@ M.n_discard = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_go_to_next_hunk_header = function(self)
return function()
local c = self.buffer.ui:get_component_under_cursor(function(c)
@@ -905,6 +1019,7 @@ M.n_go_to_next_hunk_header = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_go_to_previous_hunk_header = function(self)
return function()
local function previous_hunk_header(self, line)
@@ -931,6 +1046,7 @@ M.n_go_to_previous_hunk_header = function(self)
end
---@param _self StatusBuffer
+---@return fun(): nil
M.n_init_repo = function(_self)
return function()
git.init.init_repo()
@@ -938,6 +1054,43 @@ M.n_init_repo = function(_self)
end
---@param self StatusBuffer
+---@return fun(): nil
+M.n_rename = function(self)
+ return a.void(function()
+ local selection = self.buffer.ui:get_selection()
+ local paths = git.files.all_tree()
+
+ if
+ selection.item
+ and selection.item.escaped_path
+ and git.files.is_tracked(selection.item.escaped_path)
+ then
+ paths = util.deduplicate(util.merge({ selection.item.escaped_path }, paths))
+ end
+
+ local selected = FuzzyFinderBuffer.new(paths):open_async { prompt_prefix = "Rename file" }
+ if (selected or "") == "" then
+ return
+ end
+
+ local destination = input.get_user_input("Move to", { completion = "dir", prepend = selected })
+ if (destination or "") == "" then
+ return
+ end
+
+ assert(destination, "must have a destination")
+ local success = git.files.move(selected, destination)
+
+ if not success then
+ notification.warn("Renaming failed")
+ end
+
+ self:dispatch_refresh({ update_diffs = { "*:*" } }, "n_rename")
+ end)
+end
+
+---@param self StatusBuffer
+---@return fun(): nil
M.n_untrack = function(self)
return a.void(function()
local selection = self.buffer.ui:get_selection()
@@ -968,6 +1121,7 @@ M.n_untrack = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.v_untrack = function(self)
return a.void(function()
local selection = self.buffer.ui:get_selection()
@@ -995,51 +1149,88 @@ M.v_untrack = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_stage = function(self)
return a.void(function()
local stagable = self.buffer.ui:get_hunk_or_filename_under_cursor()
local section = self.buffer.ui:get_current_section()
+ local selection = self.buffer.ui:get_selection()
if stagable and section then
if section.options.section == "staged" then
return
end
- if stagable.hunk then
- local item = self.buffer.ui:get_item_under_cursor()
- assert(item, "Item cannot be nil")
-
- if item.mode == "UU" then
- notification.info("Conflicts must be resolved before staging hunks")
+ if selection.item and selection.item.mode == "UU" then
+ if config.check_integration("diffview") then
+ require("neogit.integrations.diffview").open("conflict", selection.item.name, {
+ on_close = {
+ handle = self.buffer.handle,
+ fn = function()
+ 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")
+ end
+ end,
+ },
+ })
+ else
+ 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")
+ else
+ notification.info("Conflicts must be resolved before staging")
+ end
return
end
+ elseif selection.item and section.options.section == "untracked" then
+ git.index.add { selection.item.name }
+ self:dispatch_refresh({ update_diffs = { "*:" .. selection.item.name } }, "n_stage")
+ elseif stagable.hunk then
+ 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.escaped_path } }, "n_stage")
- elseif stagable.filename then
- if section.options.section == "unstaged" then
- git.status.stage { stagable.filename }
- self:dispatch_refresh({ update_diffs = { "*:" .. stagable.filename } }, "n_stage")
- elseif section.options.section == "untracked" then
- git.index.add { stagable.filename }
- self:dispatch_refresh({ update_diffs = { "*:" .. stagable.filename } }, "n_stage")
- end
+ self:dispatch_refresh({ update_diffs = { "*:" .. item.name } }, "n_stage")
+ elseif stagable.filename and section.options.section == "unstaged" then
+ git.status.stage { stagable.filename }
+ self:dispatch_refresh({ update_diffs = { "*:" .. stagable.filename } }, "n_stage")
end
elseif section then
if section.options.section == "untracked" then
git.status.stage_untracked()
self:dispatch_refresh({ update_diffs = { "untracked:*" } }, "n_stage")
elseif section.options.section == "unstaged" then
- git.status.stage_modified()
- self:dispatch_refresh({ update_diffs = { "*:*" } }, "n_stage")
+ if git.status.any_unmerged() then
+ if config.check_integration("diffview") then
+ require("neogit.integrations.diffview").open("conflict", nil, {
+ on_close = {
+ handle = self.buffer.handle,
+ fn = function()
+ if not git.merge.any_conflicted() then
+ git.status.stage_modified()
+ self:dispatch_refresh({ update_diffs = { "*:*" } }, "n_stage")
+ popups.open("merge")()
+ end
+ end,
+ },
+ })
+ else
+ notification.info("Conflicts must be resolved before staging")
+ return
+ end
+ else
+ git.status.stage_modified()
+ self:dispatch_refresh({ update_diffs = { "*:*" } }, "n_stage")
+ end
end
end
end)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_stage_all = function(self)
return a.void(function()
git.status.stage_all()
@@ -1048,6 +1239,7 @@ M.n_stage_all = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_stage_unstaged = function(self)
return a.void(function()
git.status.stage_modified()
@@ -1056,9 +1248,11 @@ M.n_stage_unstaged = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_unstage = function(self)
return a.void(function()
local unstagable = self.buffer.ui:get_hunk_or_filename_under_cursor()
+ local selection = self.buffer.ui:get_selection()
local section = self.buffer.ui:get_current_section()
if section and section.options.section ~= "staged" then
@@ -1066,14 +1260,19 @@ M.n_unstage = function(self)
end
if unstagable then
- if unstagable.hunk then
+ if selection.item and selection.item.mode == "N" then
+ git.status.unstage { selection.item.name }
+ self:dispatch_refresh({ update_diffs = { "*:" .. selection.item.name } }, "n_unstage")
+ elseif 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.escaped_path } }, "n_unstage")
+ self:dispatch_refresh({ update_diffs = { "*:" .. item.name } }, "n_unstage")
elseif unstagable.filename then
git.status.unstage { unstagable.filename }
self:dispatch_refresh({ update_diffs = { "*:" .. unstagable.filename } }, "n_unstage")
@@ -1086,6 +1285,7 @@ M.n_unstage = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_unstage_staged = function(self)
return a.void(function()
git.status.unstage_all()
@@ -1094,6 +1294,7 @@ M.n_unstage_staged = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_goto_file = function(self)
return function()
local item = self.buffer.ui:get_item_under_cursor()
@@ -1115,6 +1316,7 @@ M.n_goto_file = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_tab_open = function(self)
return function()
local item = self.buffer.ui:get_item_under_cursor()
@@ -1126,6 +1328,7 @@ M.n_tab_open = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_split_open = function(self)
return function()
local item = self.buffer.ui:get_item_under_cursor()
@@ -1137,6 +1340,7 @@ M.n_split_open = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_vertical_split_open = function(self)
return function()
local item = self.buffer.ui:get_item_under_cursor()
@@ -1148,6 +1352,7 @@ M.n_vertical_split_open = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_branch_popup = function(self)
return popups.open("branch", function(p)
p { commits = { self.buffer.ui:get_commit_under_cursor() } }
@@ -1155,6 +1360,7 @@ M.n_branch_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_bisect_popup = function(self)
return popups.open("bisect", function(p)
p { commits = { self.buffer.ui:get_commit_under_cursor() } }
@@ -1162,6 +1368,7 @@ M.n_bisect_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_cherry_pick_popup = function(self)
return popups.open("cherry_pick", function(p)
p { commits = { self.buffer.ui:get_commit_under_cursor() } }
@@ -1169,6 +1376,7 @@ M.n_cherry_pick_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_commit_popup = function(self)
return popups.open("commit", function(p)
p { commit = self.buffer.ui:get_commit_under_cursor() }
@@ -1176,6 +1384,7 @@ M.n_commit_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_merge_popup = function(self)
return popups.open("merge", function(p)
p { commit = self.buffer.ui:get_commit_under_cursor() }
@@ -1183,6 +1392,7 @@ M.n_merge_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_push_popup = function(self)
return popups.open("push", function(p)
p { commit = self.buffer.ui:get_commit_under_cursor() }
@@ -1190,6 +1400,7 @@ M.n_push_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_rebase_popup = function(self)
return popups.open("rebase", function(p)
p { commit = self.buffer.ui:get_commit_under_cursor() }
@@ -1197,6 +1408,7 @@ M.n_rebase_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_revert_popup = function(self)
return popups.open("revert", function(p)
p { commits = { self.buffer.ui:get_commit_under_cursor() } }
@@ -1204,6 +1416,7 @@ M.n_revert_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_reset_popup = function(self)
return popups.open("reset", function(p)
p { commit = self.buffer.ui:get_commit_under_cursor() }
@@ -1211,6 +1424,7 @@ M.n_reset_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_tag_popup = function(self)
return popups.open("tag", function(p)
p { commit = self.buffer.ui:get_commit_under_cursor() }
@@ -1218,6 +1432,7 @@ M.n_tag_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_stash_popup = function(self)
return popups.open("stash", function(p)
local stash = self.buffer.ui:get_yankable_under_cursor()
@@ -1226,6 +1441,7 @@ M.n_stash_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_diff_popup = function(self)
return popups.open("diff", function(p)
local section = self.buffer.ui:get_selection().section
@@ -1238,24 +1454,27 @@ M.n_diff_popup = function(self)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_ignore_popup = function(self)
return popups.open("ignore", function(p)
local path = self.buffer.ui:get_hunk_or_filename_under_cursor()
p {
paths = { path and path.escaped_path },
- git_root = git.repo.git_root,
+ worktree_root = git.repo.worktree_root,
}
end)
end
---@param self StatusBuffer
+---@return fun(): nil
M.n_help_popup = function(self)
return popups.open("help", function(p)
-- Since any other popup can be launched from help, build an ENV for any of them.
local path = self.buffer.ui:get_hunk_or_filename_under_cursor()
local section = self.buffer.ui:get_selection().section
+ local section_name
if section then
- section = section.name
+ section_name = section.name
end
local item = self.buffer.ui:get_yankable_under_cursor()
@@ -1275,14 +1494,15 @@ M.n_help_popup = function(self)
bisect = { commits = commits },
reset = { commit = commit },
tag = { commit = commit },
+ margin = { buffer = self },
stash = { name = stash and stash:match("^stash@{%d+}") },
diff = {
- section = { name = section },
+ section = { name = section_name },
item = { name = item },
},
ignore = {
paths = { path and path.escaped_path },
- git_root = git.repo.git_root,
+ worktree_root = git.repo.worktree_root,
},
remote = {},
fetch = {},
@@ -1294,50 +1514,80 @@ M.n_help_popup = function(self)
end
---@param _self StatusBuffer
+---@return fun(): nil
M.n_remote_popup = function(_self)
return popups.open("remote")
end
---@param _self StatusBuffer
+---@return fun(): nil
M.n_fetch_popup = function(_self)
return popups.open("fetch")
end
---@param _self StatusBuffer
+---@return fun(): nil
M.n_pull_popup = function(_self)
return popups.open("pull")
end
---@param _self StatusBuffer
+---@return fun(): nil
M.n_log_popup = function(_self)
return popups.open("log")
end
+---@param self StatusBuffer
+---@return fun(): nil
+M.n_margin_popup = function(self)
+ return popups.open("margin", function(p)
+ p { buffer = self }
+ end)
+end
+
---@param _self StatusBuffer
+---@return fun(): nil
M.n_worktree_popup = function(_self)
return popups.open("worktree")
end
----@param _self StatusBuffer
-M.n_open_tree = function(_self)
+---@param self StatusBuffer
+---@return fun(): nil
+M.n_open_tree = function(self)
return a.void(function()
- local template = "https://${host}/$%7Bowner%7D/$%7Brepository%7D/tree/$%7Bbranch_name%7D"
+ if not vim.ui.open then
+ notification.warn("Requires Neovim >= 0.10")
+ return
+ end
- local url = git.remote.get_url(https://github.com/git.branch.upstream_remote())[1]
- local format_values = git.remote.parse(url)
- format_values["branch_name"] = git.branch.current()
+ local commit = self.buffer.ui:get_commit_under_cursor()
+ local branch = git.branch.current()
+ local url
- vim.ui.open(util.format(template, format_values))
+ if commit then
+ url = git.remote.commit_url(https://github.com/commit)
+ elseif branch then
+ url = git.remote.tree_url(https://github.com/branch)
+ end
+
+ if url then
+ notification.info(("Opening %q in your browser."):format(url))
+ vim.ui.open(url)
+ else
+ notification.warn("Couldn't determine commit URL to open")
+ end
end)
end
---@param self StatusBuffer|nil
+---@return fun(): nil
M.n_command = function(self)
local process = require("neogit.process")
local runner = require("neogit.runner")
return a.void(function()
- local cmd = input.get_user_input(("Run command in %s"):format(git.repo.git_root), { prepend = "git " })
+ local cmd =
+ input.get_user_input(("Run command in %s"):format(git.repo.worktree_root), { prepend = "git " })
if not cmd then
return
end
@@ -1348,7 +1598,7 @@ M.n_command = function(self)
local proc = process.new {
cmd = cmd,
- cwd = git.repo.git_root,
+ cwd = git.repo.worktree_root,
env = {},
on_error = function()
return false
@@ -1360,18 +1610,46 @@ M.n_command = function(self)
proc:show_console()
- runner.call(
- proc,
- {
- pty = true,
- callback = function()
- if self then
- self:dispatch_refresh()
- end
+ runner.call(proc, {
+ pty = true,
+ callback = function()
+ if self then
+ self:dispatch_refresh()
end
- }
- )
+ end,
+ })
end)
end
+---@param self StatusBuffer
+---@return fun(): nil
+M.n_next_section = function(self)
+ return function()
+ local section = self.buffer.ui:get_current_section()
+ if section then
+ local position = section.position.row_end + 2
+ self.buffer:move_cursor(position)
+ else
+ self.buffer:move_cursor(self.buffer.ui:first_section().first + 1)
+ end
+ end
+end
+
+---@param self StatusBuffer
+---@return fun(): nil
+M.n_prev_section = function(self)
+ return function()
+ local section = self.buffer.ui:get_current_section()
+ if section then
+ local prev_section = self.buffer.ui:get_current_section(section.position.row_start - 1)
+ if prev_section then
+ self.buffer:move_cursor(prev_section.position.row_start + 1)
+ return
+ end
+ end
+
+ self.buffer:win_exec("norm! gg")
+ end
+end
+
return M
diff --git a/lua/neogit/buffers/status/init.lua b/lua/neogit/buffers/status/init.lua
index 983a8db50..387d7d23f 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
@@ -79,7 +78,12 @@ function M:_action(name)
return action(self)
end
----@param kind string<"floating" | "split" | "tab" | "split" | "vsplit">|nil
+---@param kind nil|string
+---| "'floating'"
+---| "'split'"
+---| "'tab'"
+---| "'split'"
+---| "'vsplit'"
---@return StatusBuffer
function M:open(kind)
if self.buffer and self.buffer:is_visible() then
@@ -97,7 +101,9 @@ function M:open(kind)
context_highlight = not config.values.disable_context_highlighting,
kind = kind or config.values.kind or "tab",
disable_line_numbers = config.values.disable_line_numbers,
+ disable_relative_line_numbers = config.values.disable_relative_line_numbers,
foldmarkers = not config.values.disable_signs,
+ active_item_highlight = true,
on_detach = function()
Watcher.instance(self.root):unregister(self)
@@ -121,6 +127,7 @@ function M:open(kind)
[popups.mapping_for("HelpPopup")] = self:_action("v_help_popup"),
[popups.mapping_for("IgnorePopup")] = self:_action("v_ignore_popup"),
[popups.mapping_for("LogPopup")] = self:_action("v_log_popup"),
+ [popups.mapping_for("MarginPopup")] = self:_action("v_margin_popup"),
[popups.mapping_for("MergePopup")] = self:_action("v_merge_popup"),
[popups.mapping_for("PullPopup")] = self:_action("v_pull_popup"),
[popups.mapping_for("PushPopup")] = self:_action("v_push_popup"),
@@ -138,7 +145,10 @@ function M:open(kind)
[mappings["MoveDown"]] = self:_action("n_down"),
[mappings["MoveUp"]] = self:_action("n_up"),
[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"),
@@ -163,6 +173,8 @@ function M:open(kind)
[mappings["TabOpen"]] = self:_action("n_tab_open"),
[mappings["SplitOpen"]] = self:_action("n_split_open"),
[mappings["VSplitOpen"]] = self:_action("n_vertical_split_open"),
+ [mappings["NextSection"]] = self:_action("n_next_section"),
+ [mappings["PreviousSection"]] = self:_action("n_prev_section"),
[popups.mapping_for("BisectPopup")] = self:_action("n_bisect_popup"),
[popups.mapping_for("BranchPopup")] = self:_action("n_branch_popup"),
[popups.mapping_for("CherryPickPopup")] = self:_action("n_cherry_pick_popup"),
@@ -172,6 +184,7 @@ function M:open(kind)
[popups.mapping_for("HelpPopup")] = self:_action("n_help_popup"),
[popups.mapping_for("IgnorePopup")] = self:_action("n_ignore_popup"),
[popups.mapping_for("LogPopup")] = self:_action("n_log_popup"),
+ [popups.mapping_for("MarginPopup")] = self:_action("n_margin_popup"),
[popups.mapping_for("MergePopup")] = self:_action("n_merge_popup"),
[popups.mapping_for("PullPopup")] = self:_action("n_pull_popup"),
[popups.mapping_for("PushPopup")] = self:_action("n_push_popup"),
@@ -203,27 +216,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,
+ -- 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),
},
}
@@ -243,15 +242,19 @@ function M:close()
end
function M:chdir(dir)
- local destination = require("plenary.path").new(dir)
+ local Path = require("plenary.path")
+
+ local destination = Path:new(dir)
vim.wait(5000, function()
return destination:exists()
end)
- logger.debug("[STATUS] Changing Dir: " .. dir)
- vim.api.nvim_set_current_dir(dir)
- self.cwd = dir
- self:dispatch_reset()
+ vim.schedule(function()
+ logger.debug("[STATUS] Changing Dir: " .. dir)
+ vim.api.nvim_set_current_dir(dir)
+ require("neogit.lib.git.repository").instance(dir)
+ self.new(config.values, git.repo.worktree_root, dir):open("replace"):dispatch_refresh()
+ end)
end
function M:focus()
@@ -276,7 +279,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,
}
@@ -293,18 +296,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
@@ -313,6 +316,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 92cb04e82..0452bdeea 100755
--- a/lua/neogit/buffers/status/ui.lua
+++ b/lua/neogit/buffers/status/ui.lua
@@ -2,7 +2,9 @@ local Ui = require("neogit.lib.ui")
local Component = require("neogit.lib.ui.component")
local util = require("neogit.lib.util")
local common = require("neogit.buffers.common")
+local config = require("neogit.config")
local a = require("plenary.async")
+local state = require("neogit.lib.state")
local col = Ui.col
local row = Ui.row
@@ -235,8 +237,10 @@ local SectionItemFile = function(section, config)
end
end
- this:append(DiffHunks(diff))
- ui:update()
+ ui.buf:with_locked_viewport(function()
+ this:append(DiffHunks(diff))
+ ui:update()
+ end)
end)
end
@@ -273,12 +277,27 @@ local SectionItemFile = function(section, config)
text.highlight("NeogitSubtleText")((" %s -> %s"):format(item.file_mode.head, item.file_mode.worktree))
end
+ local submodule = text("")
+ if item.submodule then
+ local submodule_text
+ if item.submodule.commit_changed then
+ submodule_text = " (new commits)"
+ elseif item.submodule.has_tracked_changes then
+ submodule_text = " (modified content)"
+ elseif item.submodule.has_untracked_changes then
+ submodule_text = " (untracked content)"
+ end
+
+ submodule = text.highlight("NeogitTagName")(submodule_text)
+ end
+
return col.tag("Item")({
row {
text.highlight(highlight)(mode_text),
text(name),
text.highlight("NeogitSubtleText")(unmerged_types[item.mode] or ""),
file_mode_change,
+ submodule,
},
}, {
foldable = true,
@@ -299,14 +318,14 @@ local SectionItemStash = Component.new(function(item)
text.highlight("NeogitSubtleText")(name),
text.highlight("NeogitSubtleText")(": "),
text(item.message),
- }, { yankable = name, item = item })
+ }, { yankable = item.oid, item = item })
end)
local SectionItemCommit = Component.new(function(item)
local ref = {}
local ref_last = {}
- if item.commit.ref_name ~= "" then
+ if item.commit.ref_name ~= "" and state.get({ "NeogitMarginPopup", "decorate" }, true) then
-- Render local only branches first
for name, _ in pairs(item.decoration.locals) do
if name:match("^refs/") then
@@ -342,6 +361,79 @@ local SectionItemCommit = Component.new(function(item)
end
end
+ local virtual_text
+
+ -- Render author and date in margin, if visible
+ if state.get({ "margin", "visibility" }, false) then
+ local margin_date_style = state.get({ "margin", "date_style" }, 1)
+ local details = state.get({ "margin", "details" }, false)
+
+ local date
+ local rel_date
+ local date_width = 10
+ local clamp_width = 30 -- to avoid having too much space when relative date is short
+
+ -- Render date
+ if item.commit.rel_date:match(" years?,") then
+ rel_date, _ = item.commit.rel_date:gsub(" years?,", "y")
+ rel_date = rel_date .. " "
+ elseif item.commit.rel_date:match("^%d ") then
+ rel_date = " " .. item.commit.rel_date
+ else
+ rel_date = item.commit.rel_date
+ end
+
+ if margin_date_style == 1 then -- relative date (short)
+ local unpacked = vim.split(rel_date, " ")
+
+ -- above, we added a space if the rel_date started with a single number
+ -- we get the last two elements to deal with that
+ local date_number = unpacked[#unpacked - 1]
+ local date_quantifier = unpacked[#unpacked]
+ if date_quantifier:match("months?") then
+ date_quantifier = date_quantifier:gsub("m", "M") -- to distinguish from minutes
+ end
+
+ -- add back the space if we have a single number
+ local left_pad
+ if #unpacked > 2 then
+ left_pad = " "
+ else
+ left_pad = ""
+ end
+
+ date = left_pad .. date_number .. date_quantifier:sub(1, 1)
+ date_width = 3
+ clamp_width = 23
+ elseif margin_date_style == 2 then -- relative date (long)
+ date = rel_date
+ date_width = 10
+ else -- local iso date
+ if config.values.log_date_format == nil then
+ -- we get the unix date to be able to convert the date to the local timezone
+ date = os.date("%Y-%m-%d %H:%M", item.commit.unix_date)
+ date_width = 16 -- TODO: what should the width be here?
+ else
+ date = item.commit.log_date
+ date_width = 16
+ end
+ end
+
+ local author_table = { "" }
+ if details then
+ author_table = {
+ util.str_clamp(item.commit.author_name, clamp_width - (#date > date_width and #date or date_width)),
+ "NeogitGraphAuthor",
+ }
+ end
+
+ virtual_text = {
+ { " ", "Constant" },
+ author_table,
+ { util.str_min_width(date, date_width), "Special" },
+ }
+ end
+
return row(
util.merge(
{ text.highlight("NeogitObjectId")(item.commit.abbreviated_commit) },
@@ -350,7 +442,12 @@ local SectionItemCommit = Component.new(function(item)
ref_last,
{ text(item.commit.subject) }
),
- { oid = item.commit.oid, yankable = item.commit.oid, item = item }
+ {
+ virtual_text = virtual_text,
+ oid = item.commit.oid,
+ yankable = item.commit.oid,
+ item = item,
+ }
)
end)
@@ -687,6 +784,7 @@ function M.Status(state, config)
},
}
end
+
-- stylua: ignore end
return M
diff --git a/lua/neogit/client.lua b/lua/neogit/client.lua
index 735d8d36b..177bbba0e 100644
--- a/lua/neogit/client.lua
+++ b/lua/neogit/client.lua
@@ -64,13 +64,7 @@ function M.client(opts)
local client = fn.serverstart()
logger.debug(("[CLIENT] Client address: %s"):format(client))
- local lua_cmd =
- fmt('lua require("neogit.client").editor("%s", "%s", %s)', file_target, client, opts.show_diff)
-
- if vim.loop.os_uname().sysname == "Windows_NT" then
- lua_cmd = lua_cmd:gsub("\\", "/")
- end
-
+ local lua_cmd = fmt('lua require("neogit.client").editor(%q, %q, %s)', file_target, client, opts.show_diff)
local rpc_server = RPC.create_connection(nvim_server)
rpc_server:send_cmd(lua_cmd)
end
@@ -102,9 +96,7 @@ function M.editor(target, client, show_diff)
kind = config.values.commit_editor.kind
elseif target:find("MERGE_MSG$") then
kind = config.values.merge_editor.kind
- elseif target:find("TAG_EDITMSG$") then
- kind = "popup"
- elseif target:find("EDIT_DESCRIPTION$") then
+ elseif target:find("TAG_EDITMSG$") or target:find("EDIT_DESCRIPTION$") then
kind = "popup"
elseif target:find("git%-rebase%-todo$") then
kind = config.values.rebase_editor.kind
@@ -152,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 01f8d8476..8b3c7fff0 100644
--- a/lua/neogit/config.lua
+++ b/lua/neogit/config.lua
@@ -62,6 +62,11 @@ 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")
+end
+
---@param set string
---@return table
function M.get_user_mappings(set)
@@ -79,10 +84,17 @@ function M.get_user_mappings(set)
end
---@alias WindowKind
+---| "replace" Like :enew
+---| "tab" Open in a new tab
---| "split" Open in a split
+---| "split_above" Like :top split
+---| "split_above_all" Like :top split
+---| "split_below" Like :below split
+---| "split_below_all" Like :below split
---| "vsplit" Open in a vertical split
---| "floating" Open in a floating window
----| "tab" Open in a new tab
+---| "floating_console" Open in a floating window across the bottom of the screen
+---| "auto" vsplit if window would have 80 cols, otherwise split
---@class NeogitCommitBufferConfig Commit buffer options
---@field kind WindowKind The type of window that should be opened
@@ -91,6 +103,15 @@ end
---@class NeogitConfigPopup Popup window options
---@field kind WindowKind The type of window that should be opened
+---@class NeogitConfigFloating
+---@field relative? string
+---@field width? number
+---@field height? number
+---@field col? number
+---@field row? number
+---@field style? string
+---@field border? string
+
---@alias StagedDiffSplitKind
---| "split" Open in a split
---| "vsplit" Open in a vertical split
@@ -165,10 +186,14 @@ end
---| "Close"
---| "Next"
---| "Previous"
+---| "CopySelection"
---| "MultiselectToggleNext"
---| "MultiselectTogglePrevious"
---| "InsertCompletion"
---| "NOP"
+---| "ScrollWheelDown"
+---| "ScrollWheelUp"
+---| "MouseClick"
---| false
---@alias NeogitConfigMappingsStatus
@@ -176,6 +201,7 @@ end
---| "MoveDown"
---| "MoveUp"
---| "OpenTree"
+---| "OpenFold"
---| "Command"
---| "Depth1"
---| "Depth2"
@@ -191,6 +217,7 @@ end
---| "Untrack"
---| "RefreshBuffer"
---| "GoToFile"
+---| "PeekFile"
---| "VSplitOpen"
---| "SplitOpen"
---| "TabOpen"
@@ -202,6 +229,10 @@ end
---| "YankSelected"
---| "OpenOrScrollUp"
---| "OpenOrScrollDown"
+---| "PeekUp"
+---| "PeekDown"
+---| "NextSection"
+---| "PreviousSection"
---| false
---| fun()
@@ -214,6 +245,7 @@ end
---| "PushPopup"
---| "CommitPopup"
---| "LogPopup"
+---| "MarginPopup"
---| "RevertPopup"
---| "StashPopup"
---| "IgnorePopup"
@@ -268,10 +300,22 @@ end
---| "Abort"
---| false
---| fun()
+---
+---@alias NeogitConfigMappingsRefsView
+---| "DeleteBranch"
+---| false
+---| fun()
---@alias NeogitGraphStyle
---| "ascii"
---| "unicode"
+---| "kitty"
+---
+---@alias NeogitCommitOrder
+---| ""
+---| "topo"
+---| "author-date"
+---| "date"
---@class NeogitConfigStatusOptions
---@field recent_commit_count? integer The number of recent commits to display
@@ -289,47 +333,61 @@ end
---@field rebase_editor_I? { [string]: NeogitConfigMappingsRebaseEditor_I } A dictionary that uses Rebase editor commands to set a single keybind
---@field commit_editor? { [string]: NeogitConfigMappingsCommitEditor } A dictionary that uses Commit editor commands to set a single keybind
---@field commit_editor_I? { [string]: NeogitConfigMappingsCommitEditor_I } A dictionary that uses Commit editor commands to set a single keybind
+---@field refs_view? { [string]: NeogitConfigMappingsRefsView } A dictionary that uses Refs view editor commands to set a single keybind
+
+---@class NeogitConfigGitService
+---@field pull_request? string
+---@field commit? string
+---@field tree? string
---@class NeogitConfig Neogit configuration settings
---@field filewatcher? NeogitFilewatcherConfig Values for filewatcher
---@field graph_style? NeogitGraphStyle Style for graph
+---@field commit_date_format? string Commit date format
+---@field log_date_format? string Log date format
---@field disable_hint? boolean Remove the top hint in the Status buffer
---@field disable_context_highlighting? boolean Disable context highlights based on cursor position
---@field disable_signs? boolean Special signs to draw for sections etc. in Neogit
----@field git_services? table Templartes to use when opening a pull request for a branch
+---@field prompt_force_push? boolean Offer to force push when branches diverge
+---@field git_services? NeogitConfigGitService[] Templates to use when opening a pull request for a branch, or commit
---@field fetch_after_checkout? boolean Perform a fetch if the newly checked out branch has an upstream or pushRemote set
---@field telescope_sorter? function The sorter telescope will use
+---@field process_spinner? boolean Hide/Show the process spinner
---@field disable_insert_on_commit? boolean|"auto" Disable automatically entering insert mode in commit dialogues
---@field use_per_project_settings? boolean Scope persisted settings on a per-project basis
---@field remember_settings? boolean Whether neogit should persist flags from popups, e.g. git push flags
---@field sort_branches? string Value used for `--sort` for the `git branch` command
+---@field commit_order? NeogitCommitOrder Value used for `---order` for the `git log` command
+---@field initial_branch_name? string Default for new branch name prompts
---@field kind? WindowKind The default type of window neogit should open in
+---@field floating? NeogitConfigFloating The floating window style
---@field disable_line_numbers? boolean Whether to disable line numbers
---@field disable_relative_line_numbers? boolean Whether to disable line numbers
---@field console_timeout? integer Time in milliseconds after a console is created for long running commands
---@field auto_show_console? boolean Automatically show the console if a command takes longer than console_timeout
+---@field auto_show_console_on? string Specify "output" (show always; default) or "error" if `auto_show_console` enabled
---@field auto_close_console? boolean Automatically hide the console if the process exits with a 0 status
---@field status? NeogitConfigStatusOptions Status buffer options
---@field commit_editor? NeogitCommitEditorConfigPopup Commit editor options
---@field commit_select_view? NeogitConfigPopup Commit select view options
+---@field stash? NeogitConfigPopup Commit select view options
---@field commit_view? NeogitCommitBufferConfig Commit buffer options
---@field log_view? NeogitConfigPopup Log view options
---@field rebase_editor? NeogitConfigPopup Rebase editor options
---@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
----@field integrations? { diffview: boolean, telescope: boolean, fzf_lua: boolean, mini_pick: boolean } Which integrations to enable
+---@field integrations? { diffview: boolean, telescope: boolean, fzf_lua: boolean, mini_pick: boolean, snacks: boolean } Which integrations to enable
---@field sections? NeogitConfigSections
---@field ignored_settings? string[] Settings to never persist, format: "Filetype--cli-value", i.e. "NeogitCommitPopup--author"
---@field mappings? NeogitConfigMappings
---@field notification_icon? string
---@field use_default_keymaps? boolean
---@field highlight? HighlightOptions
+---@field builders? { [string]: fun(builder: PopupBuilder) }
---Returns the default Neogit configuration
---@return NeogitConfig
@@ -339,7 +397,11 @@ function M.get_default_values()
disable_hint = false,
disable_context_highlighting = false,
disable_signs = false,
+ prompt_force_push = true,
graph_style = "ascii",
+ commit_date_format = nil,
+ log_date_format = nil,
+ process_spinner = false,
filewatcher = {
enabled = true,
},
@@ -347,10 +409,26 @@ function M.get_default_values()
return nil
end,
git_services = {
- ["github.com"] = "https://github.com/$%7Bowner%7D/$%7Brepository%7D/compare/$%7Bbranch_name%7D?expand=1",
- ["bitbucket.org"] = "https://bitbucket.org/$%7Bowner%7D/$%7Brepository%7D/pull-requests/new?source=${branch_name}&t=1",
- ["gitlab.com"] = "https://gitlab.com/$%7Bowner%7D/$%7Brepository%7D/merge_requests/new?merge_request[source_branch]=${branch_name}",
- ["azure.com"] = "https://dev.azure.com/$%7Bowner%7D/_git/$%7Brepository%7D/pullrequestcreate?sourceRef=${branch_name}&targetRef=${target}",
+ ["github.com"] = {
+ pull_request = "https://github.com/$%7Bowner%7D/$%7Brepository%7D/compare/$%7Bbranch_name%7D?expand=1",
+ commit = "https://github.com/$%7Bowner%7D/$%7Brepository%7D/commit/$%7Boid%7D",
+ tree = "https://${host}/$%7Bowner%7D/$%7Brepository%7D/tree/$%7Bbranch_name%7D",
+ },
+ ["bitbucket.org"] = {
+ pull_request = "https://bitbucket.org/$%7Bowner%7D/$%7Brepository%7D/pull-requests/new?source=${branch_name}&t=1",
+ commit = "https://bitbucket.org/$%7Bowner%7D/$%7Brepository%7D/commits/$%7Boid%7D",
+ tree = "https://bitbucket.org/$%7Bowner%7D/$%7Brepository%7D/branch/$%7Bbranch_name%7D",
+ },
+ ["gitlab.com"] = {
+ pull_request = "https://gitlab.com/$%7Bowner%7D/$%7Brepository%7D/merge_requests/new?merge_request[source_branch]=${branch_name}",
+ commit = "https://gitlab.com/$%7Bowner%7D/$%7Brepository%7D/-/commit/$%7Boid%7D",
+ tree = "https://gitlab.com/$%7Bowner%7D/$%7Brepository%7D/-/tree/$%7Bbranch_name%7D?ref_type=heads",
+ },
+ ["azure.com"] = {
+ pull_request = "https://dev.azure.com/$%7Bowner%7D/_git/$%7Brepository%7D/pullrequestcreate?sourceRef=${branch_name}&targetRef=${target}",
+ commit = "",
+ tree = "",
+ },
},
highlight = {},
disable_insert_on_commit = "auto",
@@ -358,13 +436,25 @@ function M.get_default_values()
remember_settings = true,
fetch_after_checkout = false,
sort_branches = "-committerdate",
+ commit_order = "topo",
kind = "tab",
+ floating = {
+ relative = "editor",
+ width = 0.8,
+ height = 0.7,
+ style = "minimal",
+ border = "rounded",
+ },
+ initial_branch_name = "",
disable_line_numbers = true,
disable_relative_line_numbers = true,
-- The time after which an output console is shown for slow running commands
console_timeout = 2000,
-- Automatically show console if a command takes more than console_timeout milliseconds
auto_show_console = true,
+ -- If `auto_show_console` is enabled, specify "output" (default) to show
+ -- the console always, or "error" to auto-show the console only on error
+ auto_show_console_on = "output",
auto_close_console = true,
notification_icon = "",
status = {
@@ -381,6 +471,7 @@ function M.get_default_values()
C = "copied",
U = "updated",
R = "renamed",
+ T = "changed",
DD = "unmerged",
AU = "unmerged",
UD = "unmerged",
@@ -416,12 +507,6 @@ function M.get_default_values()
merge_editor = {
kind = "auto",
},
- description_editor = {
- kind = "auto",
- },
- tag_editor = {
- kind = "auto",
- },
preview_buffer = {
kind = "floating_console",
},
@@ -444,6 +529,7 @@ function M.get_default_values()
diffview = nil,
fzf_lua = nil,
mini_pick = nil,
+ snacks = nil,
},
sections = {
sequencer = {
@@ -495,12 +581,7 @@ function M.get_default_values()
hidden = false,
},
},
- ignored_settings = {
- "NeogitPushPopup--force-with-lease",
- "NeogitPushPopup--force",
- "NeogitPullPopup--rebase",
- "NeogitCommitPopup--allow-empty",
- },
+ ignored_settings = {},
mappings = {
commit_editor = {
["q"] = "Close",
@@ -545,6 +626,7 @@ function M.get_default_values()
[""] = "Next",
[""] = "Previous",
[""] = "InsertCompletion",
+ [""] = "CopySelection",
[""] = "MultiselectToggleNext",
[""] = "MultiselectTogglePrevious",
[""] = "NOP",
@@ -555,6 +637,9 @@ function M.get_default_values()
[""] = "MouseClick",
["<2-LeftMouse>"] = "NOP",
},
+ refs_view = {
+ ["x"] = "DeleteBranch",
+ },
popup = {
["?"] = "HelpPopup",
["A"] = "CherryPickPopup",
@@ -571,6 +656,7 @@ function M.get_default_values()
["c"] = "CommitPopup",
["f"] = "FetchPopup",
["l"] = "LogPopup",
+ ["L"] = "MarginPopup",
["m"] = "MergePopup",
["p"] = "PullPopup",
["r"] = "RebasePopup",
@@ -588,12 +674,18 @@ function M.get_default_values()
["4"] = "Depth4",
["Q"] = "Command",
[""] = "Toggle",
+ ["za"] = "Toggle",
+ ["zo"] = "OpenFold",
+ ["zc"] = "CloseFold",
+ ["zC"] = "Depth1",
+ ["zO"] = "Depth4",
["x"] = "Discard",
["s"] = "Stage",
["S"] = "StageUnstaged",
[""] = "StageAll",
["u"] = "Unstage",
["K"] = "Untrack",
+ ["R"] = "Rename",
["U"] = "UnstageStaged",
["y"] = "ShowRefs",
["$"] = "CommandHistory",
@@ -610,6 +702,8 @@ function M.get_default_values()
["]c"] = "OpenOrScrollDown",
[""] = "PeekUp",
[""] = "PeekDown",
+ [""] = "NextSection",
+ [""] = "PreviousSection",
},
},
}
@@ -746,7 +840,7 @@ function M.validate_config()
end
local function validate_integrations()
- local valid_integrations = { "diffview", "telescope", "fzf_lua", "mini_pick" }
+ local valid_integrations = { "diffview", "telescope", "fzf_lua", "mini_pick", "snacks" }
if not validate_type(config.integrations, "integrations", "table") or #config.integrations == 0 then
return
end
@@ -1087,12 +1181,21 @@ function M.validate_config()
validate_type(config.use_per_project_settings, "use_per_project_settings", "boolean")
validate_type(config.remember_settings, "remember_settings", "boolean")
validate_type(config.sort_branches, "sort_branches", "string")
+ validate_type(config.initial_branch_name, "initial_branch_name", "string")
validate_type(config.notification_icon, "notification_icon", "string")
validate_type(config.console_timeout, "console_timeout", "number")
validate_kind(config.kind, "kind")
+ if validate_type(config.floating, "floating", "table") then
+ validate_type(config.floating.relative, "relative", "string")
+ validate_type(config.floating.width, "width", "number")
+ validate_type(config.floating.height, "height", "number")
+ validate_type(config.floating.style, "style", "string")
+ validate_type(config.floating.border, "border", "string")
+ end
validate_type(config.disable_line_numbers, "disable_line_numbers", "boolean")
validate_type(config.disable_relative_line_numbers, "disable_relative_line_numbers", "boolean")
validate_type(config.auto_show_console, "auto_show_console", "boolean")
+ validate_type(config.auto_show_console_on, "auto_show_console_on", "string")
validate_type(config.auto_close_console, "auto_close_console", "boolean")
if validate_type(config.status, "status", "table") then
validate_type(config.status.show_head_commit_hash, "status.show_head_commit_hash", "boolean")
@@ -1146,6 +1249,15 @@ function M.validate_config()
validate_kind(config.popup.kind, "popup.kind")
end
+ if validate_type(config.git_services, "git_services", "table") then
+ for k, v in pairs(config.git_services) do
+ validate_type(v, "git_services." .. k, "table")
+ validate_type(v.pull_request, "git_services." .. k .. ".pull_request", "string")
+ validate_type(v.commit, "git_services." .. k .. ".commit", "string")
+ validate_type(v.tree, "git_services." .. k .. ".tree", "string")
+ end
+ end
+
validate_integrations()
validate_sections()
validate_ignored_settings()
@@ -1178,7 +1290,14 @@ function M.setup(opts)
end
if opts.use_default_keymaps == false then
- M.values.mappings = { status = {}, popup = {}, finder = {}, commit_editor = {}, rebase_editor = {} }
+ M.values.mappings = {
+ status = {},
+ popup = {},
+ finder = {},
+ commit_editor = {},
+ rebase_editor = {},
+ refs_view = {},
+ }
else
-- Clear our any "false" user mappings from defaults
for section, maps in pairs(opts.mappings or {}) do
diff --git a/lua/neogit/integrations/diffview.lua b/lua/neogit/integrations/diffview.lua
index 1fc7dfe58..d586f6a95 100644
--- a/lua/neogit/integrations/diffview.lua
+++ b/lua/neogit/integrations/diffview.lua
@@ -1,7 +1,5 @@
local M = {}
-local dv = require("diffview")
-local dv_config = require("diffview.config")
local Rev = require("diffview.vcs.adapters.git.rev").GitRev
local RevType = require("diffview.vcs.rev").RevType
local CDiffView = require("diffview.api.views.diff.diff_view").CDiffView
@@ -12,34 +10,35 @@ local Watcher = require("neogit.watcher")
local git = require("neogit.lib.git")
local a = require("plenary.async")
-local old_config
-
-local function close()
- vim.cmd("tabclose")
- Watcher.instance():dispatch_refresh()
- dv.setup(old_config)
-end
-
local function get_local_diff_view(section_name, item_name, opts)
local left = Rev(RevType.STAGE)
local right = Rev(RevType.LOCAL)
- if section_name == "unstaged" then
- section_name = "working"
- end
-
local function update_files()
local files = {}
- local sections = {
- conflicting = {
- items = vim.tbl_filter(function(o)
- return o.mode and o.mode:sub(2, 2) == "U"
- end, git.repo.state.untracked.items),
- },
- working = git.repo.state.unstaged,
- staged = git.repo.state.staged,
- }
+ local sections = {}
+
+ -- all conflict modes (but I don't know how to generate UA/AU)
+ local conflict_modes = { "UU", "UD", "DU", "AA", "UA", "AU" }
+
+ -- merge section gets both
+ if section_name == "unstaged" or section_name == "merge" then
+ sections.conflicting = {
+ items = vim.tbl_filter(function(item)
+ return vim.tbl_contains(conflict_modes, item.mode) and item
+ end, git.repo.state.unstaged.items),
+ }
+ sections.working = {
+ items = vim.tbl_filter(function(item)
+ return not vim.tbl_contains(conflict_modes, item.mode) and item
+ end, git.repo.state.unstaged.items),
+ }
+ end
+
+ if section_name == "staged" or section_name == "merge" then
+ sections.staged = git.repo.state.staged
+ end
for kind, section in pairs(sections) do
files[kind] = {}
@@ -58,7 +57,7 @@ local function get_local_diff_view(section_name, item_name, opts)
}
if opts.only then
- if (item_name and file.selected) or (not item_name and section_name == kind) then
+ if not item_name or (item_name and file.selected) then
table.insert(files[kind], file)
end
else
@@ -73,7 +72,7 @@ local function get_local_diff_view(section_name, item_name, opts)
local files = update_files()
local view = CDiffView {
- git_root = git.repo.git_root,
+ git_root = git.repo.worktree_root,
left = left,
right = right,
files = files,
@@ -103,60 +102,50 @@ local function get_local_diff_view(section_name, item_name, opts)
return view
end
+---@param section_name string
+---@param item_name string|string[]|nil
+---@param opts table|nil
function M.open(section_name, item_name, opts)
opts = opts or {}
- old_config = vim.deepcopy(dv_config.get_config())
-
- local config = dv_config.get_config()
-
- local keymaps = {
- view = {
- ["q"] = close,
- [""] = close,
- },
- file_panel = {
- ["q"] = close,
- [""] = close,
- },
- }
- for key, keymap in pairs(keymaps) do
- config.keymaps[key] = dv_config.extend_keymaps(keymap, config.keymaps[key] or {})
+ -- Hack way to do an on-close callback
+ if opts.on_close then
+ vim.api.nvim_create_autocmd({ "BufEnter" }, {
+ buffer = opts.on_close.handle,
+ once = true,
+ callback = opts.on_close.fn,
+ })
end
- dv.setup(config)
-
local view
-
- if section_name == "recent" or section_name == "unmerged" or section_name == "log" then
+ -- selene: allow(if_same_then_else)
+ 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
- -- TODO: Fix when no item name
- 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))
+ elseif (section_name == "conflict" or section_name == "worktree") and not item_name then
+ view = dv_lib.diffview_open()
elseif section_name ~= nil then
+ -- for staged, unstaged, merge
view = get_local_diff_view(section_name, item_name, opts)
+ elseif section_name == nil and item_name ~= nil then
+ view = dv_lib.diffview_open(dv_utils.tbl_pack(item_name .. "^!"))
else
- -- selene: allow(if_same_then_else)
- if item_name ~= nil then
- view = dv_lib.diffview_open(dv_utils.tbl_pack(item_name .. "^!"))
- else
- view = dv_lib.diffview_open()
- end
+ view = dv_lib.diffview_open()
end
if view then
diff --git a/lua/neogit/lib/buffer.lua b/lua/neogit/lib/buffer.lua
index e902869c9..2c567d25d 100644
--- a/lua/neogit/lib/buffer.lua
+++ b/lua/neogit/lib/buffer.lua
@@ -5,23 +5,21 @@ local util = require("neogit.lib.util")
local signs = require("neogit.lib.signs")
local Ui = require("neogit.lib.ui")
+local config = require("neogit.config")
local Path = require("plenary.path")
---@class Buffer
---@field handle number
---@field win_handle number
+---@field header_win_handle number?
---@field namespaces table
---@field autocmd_group number
---@field ui Ui
---@field kind string
---@field name string
----@field disable_line_numbers boolean
----@field disable_relative_line_numbers boolean
local Buffer = {
kind = "split",
- disable_line_numbers = true,
- disable_relative_line_numbers = true,
}
Buffer.__index = Buffer
@@ -79,6 +77,13 @@ function Buffer:clear()
api.nvim_buf_set_lines(self.handle, 0, -1, false, {})
end
+---@param fn fun()
+function Buffer:with_locked_viewport(fn)
+ local view = self:save_view()
+ self:call(fn)
+ self:restore_view(view)
+end
+
---@return table
function Buffer:save_view()
local view = fn.winsaveview()
@@ -91,11 +96,13 @@ end
---@param view table output of Buffer:save_view()
---@param cursor? number
function Buffer:restore_view(view, cursor)
- if cursor then
- view.lnum = math.min(fn.line("$"), cursor)
- end
+ self:win_call(function()
+ if cursor then
+ view.lnum = math.min(fn.line("$"), cursor)
+ end
- fn.winrestview(view)
+ fn.winrestview(view)
+ end)
end
function Buffer:write()
@@ -158,7 +165,7 @@ function Buffer:set_text(first_line, last_line, first_col, last_col, lines)
api.nvim_buf_set_text(self.handle, first_line, first_col, last_line, last_col, lines)
end
----@param line nil|number|number[]
+---@param line nil|integer|integer[]
function Buffer:move_cursor(line)
if not line or not self:is_focused() then
return
@@ -180,7 +187,7 @@ function Buffer:move_top_line(line)
return
end
- if vim.o.lines < vim.fn.line("$") then
+ if vim.o.lines < fn.line("$") then
return
end
@@ -203,13 +210,25 @@ function Buffer:close(force)
force = false
end
+ if self.header_win_handle ~= nil then
+ api.nvim_win_close(self.header_win_handle, true)
+ end
+
if self.kind == "replace" then
+ if self.old_cwd then
+ api.nvim_set_current_dir(self.old_cwd)
+ self.old_cwd = nil
+ end
+
api.nvim_buf_delete(self.handle, { force = force })
return
end
if self.kind == "tab" then
local ok, _ = pcall(vim.cmd, "tabclose")
+ if not ok and #api.nvim_list_tabpages() == 1 then
+ ok, _ = pcall(vim.cmd, "bd! " .. self.handle)
+ end
if not ok then
vim.cmd("tab sb " .. self.handle)
vim.cmd("tabclose #")
@@ -239,6 +258,11 @@ function Buffer:hide()
vim.cmd("silent! 1only")
vim.cmd("try | tabn # | catch /.*/ | tabp | endtry")
elseif self.kind == "replace" then
+ if self.old_cwd then
+ api.nvim_set_current_dir(self.old_cwd)
+ self.old_cwd = nil
+ end
+
if self.old_buf and api.nvim_buf_is_loaded(self.old_buf) then
api.nvim_set_current_buf(self.old_buf)
self.old_buf = nil
@@ -273,101 +297,105 @@ function Buffer:show()
end
end
- local win
- local kind = self.kind
-
- if kind == "replace" then
- self.old_buf = api.nvim_get_current_buf()
- api.nvim_set_current_buf(self.handle)
- win = api.nvim_get_current_win()
- elseif kind == "tab" then
- vim.cmd("tab sb " .. self.handle)
- win = api.nvim_get_current_win()
- elseif kind == "split" or kind == "split_below" then
- win = api.nvim_open_win(self.handle, true, { split = "below" })
- elseif kind == "split_above" then
- win = api.nvim_open_win(self.handle, true, { split = "above" })
- elseif kind == "split_above_all" then
- win = api.nvim_open_win(self.handle, true, { split = "above", win = -1 })
- elseif kind == "split_below_all" then
- win = api.nvim_open_win(self.handle, true, { split = "below", win = -1 })
- elseif kind == "vsplit" then
- win = api.nvim_open_win(self.handle, true, { split = "right", vertical = true })
- elseif kind == "vsplit_left" then
- win = api.nvim_open_win(self.handle, true, { split = "left", vertical = true })
- elseif kind == "floating" then
- -- Creates the border window
- local vim_height = vim.o.lines
- local vim_width = vim.o.columns
-
- local width = math.floor(vim_width * 0.8) + 3
- local height = math.floor(vim_height * 0.7)
- local col = vim_width * 0.1 - 1
- local row = vim_height * 0.15
-
- local content_window = api.nvim_open_win(self.handle, true, {
- relative = "editor",
- width = width,
- height = height,
- col = col,
- row = row,
- style = "minimal",
- focusable = false,
- border = "rounded",
- })
-
- api.nvim_win_set_cursor(content_window, { 1, 0 })
- win = content_window
- elseif kind == "floating_console" then
- local content_window = api.nvim_open_win(self.handle, true, {
- anchor = "SW",
- relative = "editor",
- width = vim.o.columns,
- height = math.floor(vim.o.lines * 0.3),
- col = 0,
- row = vim.o.lines - 2,
- style = "minimal",
- focusable = false,
- border = { "─", "─", "─", "", "", "", "", "" },
- title = " Git Console ",
- })
-
- api.nvim_win_set_cursor(content_window, { 1, 0 })
- win = content_window
- elseif kind == "popup" then
- -- local title, _ = self.name:gsub("^Neogit", ""):gsub("Popup$", "")
-
- local content_window = api.nvim_open_win(self.handle, true, {
- anchor = "SW",
- relative = "editor",
- width = vim.o.columns,
- height = math.floor(vim.o.lines * 0.3),
- col = 0,
- row = vim.o.lines - 2,
- style = "minimal",
- border = { "─", "─", "─", "", "", "", "", "" },
- -- title = (" %s Actions "):format(title),
- -- title_pos = "center",
- })
+ ---@return integer window handle
+ local function open()
+ local win
+ if self.kind == "replace" then
+ self.old_buf = api.nvim_get_current_buf()
+ self.old_cwd = vim.uv.cwd()
+ api.nvim_set_current_buf(self.handle)
+ win = api.nvim_get_current_win()
+ elseif self.kind == "tab" then
+ vim.cmd("tab sb " .. self.handle)
+ win = api.nvim_get_current_win()
+ elseif self.kind == "split" or self.kind == "split_below" then
+ win = api.nvim_open_win(self.handle, true, { split = "below" })
+ elseif self.kind == "split_above" then
+ win = api.nvim_open_win(self.handle, true, { split = "above" })
+ elseif self.kind == "split_above_all" then
+ win = api.nvim_open_win(self.handle, true, { split = "above", win = -1 })
+ elseif self.kind == "split_below_all" then
+ win = api.nvim_open_win(self.handle, true, { split = "below", win = -1 })
+ elseif self.kind == "vsplit" then
+ win = api.nvim_open_win(self.handle, true, { split = "right", vertical = true })
+ elseif self.kind == "vsplit_left" then
+ win = api.nvim_open_win(self.handle, true, { split = "left", vertical = true })
+ elseif self.kind == "floating" then
+ local width = config.values.floating.width
+ local height = config.values.floating.height
+ local vim_height = vim.o.lines
+ local vim_width = vim.o.columns
+ width = width > 1 and width or math.floor(vim_width * width)
+ height = height > 1 and height or math.floor(vim_height * height)
+
+ local content_window = api.nvim_open_win(self.handle, true, {
+ width = width,
+ height = height,
+ relative = config.values.floating.relative,
+ border = config.values.floating.border,
+ style = config.values.floating.style,
+ col = config.values.floating.col or (vim_width - width) / 2,
+ row = config.values.floating.row or (vim_height - height) / 2,
+ focusable = true,
+ })
+
+ api.nvim_win_set_cursor(content_window, { 1, 0 })
+ win = content_window
+ elseif self.kind == "floating_console" then
+ local content_window = api.nvim_open_win(self.handle, true, {
+ anchor = "SW",
+ relative = "editor",
+ width = vim.o.columns,
+ height = math.floor(vim.o.lines * 0.3),
+ col = 0,
+ -- buffer_height - cmdline - statusline
+ row = vim.o.lines - vim.o.cmdheight - (vim.o.laststatus > 0 and 1 or 0),
+ style = "minimal",
+ focusable = true,
+ border = { "─", "─", "─", "", "", "", "", "" },
+ title = " Git Console ",
+ })
+
+ api.nvim_win_set_cursor(content_window, { 1, 0 })
+ win = content_window
+ elseif self.kind == "popup" then
+ -- local title, _ = self.name:gsub("^Neogit", ""):gsub("Popup$", "")
+
+ local content_window = api.nvim_open_win(self.handle, true, {
+ anchor = "SW",
+ relative = "editor",
+ width = vim.o.columns,
+ height = math.floor(vim.o.lines * 0.3),
+ col = 0,
+ -- buffer_height - cmdline - statusline
+ row = vim.o.lines - vim.o.cmdheight - (vim.o.laststatus > 0 and 1 or 0),
+ style = "minimal",
+ border = { "─", "─", "─", "", "", "", "", "" },
+ -- title = (" %s Actions "):format(title),
+ -- title_pos = "center",
+ })
+
+ api.nvim_win_set_cursor(content_window, { 1, 0 })
+ win = content_window
+ end
- api.nvim_win_set_cursor(content_window, { 1, 0 })
- win = content_window
+ return win
end
- api.nvim_win_call(win, function()
- if self.disable_line_numbers then
- vim.cmd("setlocal nonu")
- end
-
- if self.disable_relative_line_numbers then
- vim.cmd("setlocal nornu")
- end
- end)
+ -- With focus on a popup window, any kind of "split" buffer will crash. Floating windows cannot be split.
+ local ok, win = pcall(open)
+ if not ok then
+ self.kind = "floating"
+ win = open()
+ end
-- Workaround UFO getting folds wrong.
- local ufo, _ = pcall(require, "ufo")
- if ufo then
- require("ufo").detach(self.handle)
+ if package.loaded["ufo"] then
+ local ok, ufo = pcall(require, "ufo")
+ if ok and type(ufo.detach) == "function" then
+ logger.debug("[BUFFER:" .. self.handle .. "] Disabling UFO for buffer")
+ ufo.detach(self.handle)
+ end
end
self.win_handle = win
@@ -405,13 +433,18 @@ end
function Buffer:set_buffer_option(name, value)
if self.handle ~= nil then
- api.nvim_set_option_value(name, value, { 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
function Buffer:set_window_option(name, value)
if self.win_handle ~= nil then
- api.nvim_set_option_value(name, value, { win = self.win_handle })
+ api.nvim_set_option_value(name, value, { scope = "local", win = self.win_handle })
end
end
@@ -508,14 +541,36 @@ 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)
- api.nvim_chan_send(api.nvim_open_term(self.handle, {}), data)
+ assert(self.chan, "Terminal channel not open")
+ assert(data, "data cannot be nil")
+ api.nvim_chan_send(self.chan, data)
+end
+
+function Buffer:open_terminal_channel()
+ assert(self.chan == nil, "Terminal channel already open")
+
+ self.chan = api.nvim_open_term(self.handle, {})
+ assert(self.chan > 0, "Failed to open terminal channel")
+
+ self:unlock()
+ self:set_lines(0, -1, false, {})
+ self:lock()
+end
+
+function Buffer:close_terminal_channel()
+ assert(self.chan, "No terminal channel to close")
+
+ fn.chanclose(self.chan)
+ self.chan = nil
end
function Buffer:win_exec(cmd)
@@ -541,6 +596,14 @@ function Buffer:line_count()
return api.nvim_buf_line_count(self.handle)
end
+function Buffer:resize_header()
+ if not self.header_win_handle then
+ return
+ end
+
+ api.nvim_win_set_width(self.header_win_handle, fn.winwidth(self.win_handle))
+end
+
---@param text string
---@param scroll boolean
function Buffer:set_header(text, scroll)
@@ -561,18 +624,21 @@ function Buffer:set_header(text, scroll)
-- Display the buffer in a floating window
local winid = api.nvim_open_win(buf, false, {
relative = "win",
- width = vim.o.columns,
+ win = self.win_handle,
+ width = fn.winwidth(self.win_handle),
height = 1,
row = 0,
col = 0,
focusable = false,
style = "minimal",
noautocmd = true,
+ border = "none",
})
vim.wo[winid].wrap = false
vim.wo[winid].winhl = "NormalFloat:NeogitFloatHeader"
fn.matchadd("NeogitFloatHeaderHighlight", [[\v\|\]], 100, -1, { window = winid })
+ self.header_win_handle = winid
if scroll then
-- Log view doesn't need scroll because the top line is blank... Because it can't be a fold or the view doesn't work.
@@ -581,6 +647,15 @@ function Buffer:set_header(text, scroll)
vim.api.nvim_feedkeys(keys, "n", false)
end)
end
+
+ -- Ensure the header only covers the intended window.
+ api.nvim_create_autocmd("WinResized", {
+ callback = function()
+ self:resize_header()
+ end,
+ buffer = self.handle,
+ group = self.autocmd_group,
+ })
end
---@class BufferConfig
@@ -595,6 +670,7 @@ end
---@field status_column string|nil
---@field load boolean|nil
---@field context_highlight boolean|nil
+---@field active_item_highlight boolean|nil
---@field open boolean|nil
---@field disable_line_numbers boolean|nil
---@field disable_relative_line_numbers boolean|nil
@@ -622,9 +698,6 @@ function Buffer.create(config)
buffer.name = config.name
buffer.kind = config.kind or "split"
- buffer.disable_line_numbers = (config.disable_line_numbers == nil) or config.disable_line_numbers
- buffer.disable_relative_line_numbers = (config.disable_relative_line_numbers == nil)
- or config.disable_relative_line_numbers
if config.load then
logger.debug("[BUFFER:" .. buffer.handle .. "] Loading content from file: " .. config.name)
@@ -634,11 +707,12 @@ function Buffer.create(config)
local win
if config.open ~= false then
win = buffer:show()
- logger.debug("[BUFFER:" .. buffer.handle .. "] Showing buffer in window " .. win)
+ logger.debug("[BUFFER:" .. buffer.handle .. "] Showing buffer in window " .. win .. " as " .. buffer.kind)
end
logger.debug("[BUFFER:" .. buffer.handle .. "] Setting buffer options")
buffer:set_buffer_option("swapfile", false)
+ buffer:set_buffer_option("modeline", false)
buffer:set_buffer_option("bufhidden", config.bufhidden or "wipe")
buffer:set_buffer_option("modifiable", config.modifiable or false)
buffer:set_buffer_option("modified", config.modifiable or false)
@@ -653,6 +727,11 @@ function Buffer.create(config)
buffer:set_filetype(config.filetype)
end
+ if config.status_column then
+ buffer:set_window_option("statuscolumn", config.status_column)
+ buffer:set_window_option("signcolumn", "no")
+ end
+
if config.user_mappings then
logger.debug("[BUFFER:" .. buffer.handle .. "] Building user key-mappings")
@@ -695,6 +774,7 @@ function Buffer.create(config)
buffer:set_window_option("foldlevel", 99)
buffer:set_window_option("foldminlines", 0)
buffer:set_window_option("foldtext", "")
+ buffer:set_window_option("foldcolumn", "0")
buffer:set_window_option("listchars", "")
buffer:set_window_option("list", false)
buffer:call(function()
@@ -705,6 +785,14 @@ function Buffer.create(config)
vim.opt_local.fillchars:append("fold: ")
end)
+ if (config.disable_line_numbers == nil) or config.disable_line_numbers then
+ buffer:set_window_option("number", false)
+ end
+
+ if (config.disable_relative_line_numbers == nil) or config.disable_relative_line_numbers then
+ buffer:set_window_option("relativenumber", false)
+ end
+
buffer:set_window_option("spell", config.spell_check or false)
buffer:set_window_option("wrap", false)
buffer:set_window_option("foldmethod", "manual")
@@ -795,14 +883,41 @@ function Buffer.create(config)
})
end
- if config.status_column then
- vim.opt_local.statuscolumn = config.status_column
- vim.opt_local.signcolumn = "no"
+ if config.active_item_highlight then
+ logger.debug("[BUFFER:" .. buffer.handle .. "] Setting up active item decorations")
+ buffer:create_namespace("ActiveItem")
+ buffer:set_decorations("ActiveItem", {
+ on_start = function()
+ return buffer:exists() and buffer:is_valid()
+ end,
+ on_win = function()
+ buffer:clear_namespace("ActiveItem")
+
+ local active_oid = require("neogit.buffers.commit_view").current_oid()
+ local item = buffer.ui:find_component_by_oid(active_oid)
+ if item and item.first and item.last then
+ for line = item.first, item.last do
+ buffer:add_line_highlight(line - 1, "NeogitActiveItem", {
+ priority = 200,
+ namespace = "ActiveItem",
+ })
+ end
+ end
+ end,
+ })
+
+ -- The decoration provider will not quite update in time, leaving two lines highlighted unless we use an autocmd too
+ api.nvim_create_autocmd("WinLeave", {
+ buffer = buffer.handle,
+ group = buffer.autocmd_group,
+ callback = function()
+ buffer:clear_namespace("ActiveItem")
+ end,
+ })
end
if config.foldmarkers then
- vim.opt_local.foldcolumn = "0"
- vim.opt_local.signcolumn = "auto"
+ buffer:set_window_option("signcolumn", "auto")
logger.debug("[BUFFER:" .. buffer.handle .. "] Setting up foldmarkers")
buffer:create_namespace("FoldSigns")
diff --git a/lua/neogit/lib/color.lua b/lua/neogit/lib/color.lua
index 6b7790541..c80fed2c4 100644
--- a/lua/neogit/lib/color.lua
+++ b/lua/neogit/lib/color.lua
@@ -97,6 +97,8 @@ function Color.from_hex(c)
end
end
+ assert(type(n) == "number", "must be a number")
+
return Color(
bit.rshift(n, 24) / 0xff,
bit.band(bit.rshift(n, 16), 0xff) / 0xff,
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 03f3fedd1..8f7c31b17 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,
@@ -117,6 +125,7 @@ end
--- Utility function to map actions
---@param on_select fun(item: any|nil)
---@param allow_multi boolean
+---@param refocus_status boolean
local function fzf_actions(on_select, allow_multi, refocus_status)
local function refresh()
if refocus_status then
@@ -144,13 +153,80 @@ local function fzf_actions(on_select, allow_multi, refocus_status)
}
end
+---Convert entries to snack picker items
+---@param entries any[]
+---@return any[]
+local function entries_to_snack_items(entries)
+ local items = {}
+ for idx, entry in ipairs(entries) do
+ table.insert(items, { idx = idx, score = 0, text = entry })
+ end
+ return items
+end
+
+--- Utility function to map actions
+---@param on_select fun(item: any|nil)
+---@param allow_multi boolean
+---@param refocus_status boolean
+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 == 0 then
+ local prompt = picker:filter().pattern
+ table.insert(selection, prompt)
+ elseif #picker_selected > 1 then
+ for _, item in ipairs(picker_selected) do
+ table.insert(selection, item.text)
+ end
+ else
+ local entry = item.text
+ local prompt = picker:filter().pattern
+
+ local navigate_up_level = entry == ".." and #prompt > 0
+ local input_git_refspec = prompt:match("%^")
+ or prompt:match("~")
+ or prompt:match("@")
+ or prompt:match(":")
+
+ table.insert(selection, (navigate_up_level or input_git_refspec) and prompt or entry)
+ end
+
+ if selection and selection[1] and selection[1] ~= "" then
+ complete(allow_multi and selection or selection[1])
+ picker:close()
+ end
+ end
+
+ local function on_close()
+ complete(nil)
+ end
+
+ return confirm, on_close
+end
+
--- Utility function to map finder opts to fzf
---@param opts FinderOpts
---@return table
local function fzf_opts(opts)
local fzf_opts = {}
- if not opts.allow_multi then
+ -- Allow multi by default
+ if opts.allow_multi then
+ fzf_opts["--multi"] = ""
+ else
fzf_opts["--no-multi"] = ""
end
@@ -264,12 +340,31 @@ function Finder:find(on_select)
fzf_opts = fzf_opts(self.opts),
winopts = {
height = self.opts.layout_config.height,
+ border = self.opts.border,
+ preview = { border = self.opts.border },
},
actions = fzf_actions(on_select, self.opts.allow_multi, self.opts.refocus_status),
})
elseif config.check_integration("mini_pick") then
local mini_pick = require("mini.pick")
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),
+ items = entries_to_snack_items(self.entries),
+ format = "text",
+ layout = {
+ preset = self.opts.theme,
+ preview = self.opts.previewer,
+ height = self.opts.layout_config.height,
+ border = self.opts.border and "rounded" or "none",
+ },
+ confirm = confirm,
+ on_close = on_close,
+ })
else
vim.ui.select(self.entries, {
prompt = string.format("%s: ", self.opts.prompt_prefix),
diff --git a/lua/neogit/lib/git/bisect.lua b/lua/neogit/lib/git/bisect.lua
index 3542079d9..9aa9f8f50 100644
--- a/lua/neogit/lib/git/bisect.lua
+++ b/lua/neogit/lib/git/bisect.lua
@@ -1,23 +1,20 @@
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
function M.in_progress()
- return git.repo:git_path("BISECT_LOG"):exists()
+ return git.repo:worktree_git_path("BISECT_LOG"):exists()
end
function M.is_finished()
@@ -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
@@ -74,13 +71,13 @@ M.register = function(meta)
local finished
- for line in git.repo:git_path("BISECT_LOG"):iter() do
+ for line in git.repo:worktree_git_path("BISECT_LOG"):iter() do
if line:match("^#") and line ~= "" then
local action, oid, subject = line:match("^# ([^:]+): %[(.+)%] (.+)")
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
@@ -96,7 +93,7 @@ M.register = function(meta)
end
end
- local expected = vim.trim(git.repo:git_path("BISECT_EXPECTED_REV"):read())
+ local expected = vim.trim(git.repo:worktree_git_path("BISECT_EXPECTED_REV"):read())
state.bisect.current =
git.log.parse(git.cli.show.format("fuller").args(expected).call({ trim = false }).stdout)[1]
diff --git a/lua/neogit/lib/git/branch.lua b/lua/neogit/lib/git/branch.lua
index 8dcd5920e..007d17ec7 100644
--- a/lua/neogit/lib/git/branch.lua
+++ b/lua/neogit/lib/git/branch.lua
@@ -7,6 +7,9 @@ local FuzzyFinderBuffer = require("neogit.buffers.fuzzy_finder")
---@class NeogitGitBranch
local M = {}
+---@param branches string[]
+---@param include_current? boolean
+---@return string[]
local function parse_branches(branches, include_current)
include_current = include_current or false
local other_branches = {}
@@ -37,6 +40,7 @@ local function parse_branches(branches, include_current)
return other_branches
end
+---@return string[]
function M.get_recent_local_branches()
local valid_branches = M.get_local_branches()
@@ -53,37 +57,76 @@ function M.get_recent_local_branches()
return util.deduplicate(branches)
end
+---@param relation? string
+---@param commit? string
+---@param ... any
+---@return string[]
+function M.list_related_branches(relation, commit, ...)
+ local result = git.cli.branch.args(relation or "", commit or "", ...).call { hidden = true }
+
+ local branches = {}
+ for _, branch in ipairs(result.stdout) do
+ branch = branch:match("^%s*(.-)%s*$")
+ if branch and not branch:match("^%(HEAD") and not branch:match("^HEAD ->") and branch ~= "" then
+ table.insert(branches, branch)
+ end
+ end
+
+ return branches
+end
+
+---@param commit string
+---@return string[]
+function M.list_containing_branches(commit, ...)
+ return M.list_related_branches("--contains", commit, ...)
+end
+
+---@param name string
+---@param args? string[]
---@return ProcessResult
function M.checkout(name, args)
return git.cli.checkout.branch(name).arg_list(args or {}).call { await = true }
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
+---@return string[]
function M.get_local_branches(include_current)
- local branches = git.cli.branch.list(config.values.sort_branches).call({ hidden = true }).stdout
+ local branches = git.cli.branch.sort(config.values.sort_branches).call({ hidden = true }).stdout
return parse_branches(branches, include_current)
end
+---@param include_current? boolean
+---@return string[]
function M.get_remote_branches(include_current)
- local branches = git.cli.branch.remotes.list(config.values.sort_branches).call({ hidden = true }).stdout
+ local branches = git.cli.branch.remotes.sort(config.values.sort_branches).call({ hidden = true }).stdout
return parse_branches(branches, include_current)
end
+---@param include_current? boolean
+---@return string[]
function M.get_all_branches(include_current)
return util.merge(M.get_local_branches(include_current), M.get_remote_branches(include_current))
end
+---@param branch string
+---@param base? string
+---@return boolean
function M.is_unmerged(branch, base)
return git.cli.cherry.arg_list({ base or M.base_branch(), branch }).call({ hidden = true }).stdout[1] ~= nil
end
+---@return string|nil
function M.base_branch()
local value = git.config.get("neogit.baseBranch")
if value:is_set() then
- return value:read()
+ return value:read() ---@type string
else
if M.exists("master") then
return "master"
@@ -101,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("^([^/]*)/(.*)$")
@@ -119,10 +162,13 @@ end
---@param name string
---@param base_branch? string
+---@return boolean
function M.create(name, base_branch)
- git.cli.branch.args(name, base_branch).call { await = true }
+ return git.cli.branch.args(name, base_branch).call({ await = true }):success()
end
+---@param name string
+---@return boolean
function M.delete(name)
local input = require("neogit.lib.input")
@@ -136,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
@@ -155,6 +201,7 @@ function M.current()
end
end
+---@return string|nil
function M.current_full_name()
local current = M.current()
if current then
@@ -162,17 +209,21 @@ function M.current_full_name()
end
end
+---@param branch? string
+---@return string|nil
function M.pushRemote(branch)
branch = branch or M.current()
if branch then
- local remote = git.config.get("branch." .. branch .. ".pushRemote")
+ local remote = git.config.get_local("branch." .. branch .. ".pushRemote")
if remote:is_set() then
return remote.value
end
end
end
+---@param branch? string
+---@return string|nil
function M.pushRemote_ref(branch)
branch = branch or M.current()
local pushRemote = M.pushRemote(branch)
@@ -182,14 +233,51 @@ 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"
end
+---@return string
function M.pushRemote_remote_label()
return M.pushRemote() or "pushRemote, setting that"
end
+---@return boolean
function M.is_detached()
return git.repo.state.head.branch == "(detached)"
end
@@ -208,6 +296,8 @@ function M.set_pushRemote()
pushRemote = FuzzyFinderBuffer.new(remotes):open_async { prompt_prefix = "set pushRemote" }
end
+ assert(type(pushRemote) == "nil" or type(pushRemote) == "string", "pushRemote is not a string or nil?")
+
if pushRemote then
git.config.set(string.format("branch.%s.pushRemote", M.current()), pushRemote)
end
@@ -221,12 +311,10 @@ end
---@return string|nil
function M.upstream(name)
if name then
- local result = git.cli["rev-parse"].symbolic_full_name
- .abbrev_ref()
- .args(name .. "@{upstream}")
- .call { ignore_error = true }
+ 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
@@ -234,27 +322,34 @@ function M.upstream(name)
end
end
+---@param name string
+---@param destination string?
+function M.set_upstream(name, destination)
+ git.cli.branch.set_upstream_to(name).args(destination or M.current())
+end
+
+---@return string
function M.upstream_label()
return M.upstream() or "@{upstream}, creating it"
end
+---@return string
function M.upstream_remote_label()
return M.upstream_remote() or "@{upstream}, setting it"
end
+---@return string|nil
function M.upstream_remote()
- local remote = git.repo.state.upstream.remote
-
- if not remote then
- local remotes = git.remote.list()
- if #remotes == 1 then
- remote = remotes[1]
- elseif vim.tbl_contains(remotes, "origin") then
- remote = "origin"
- end
+ if git.repo.state.upstream.remote then
+ return git.repo.state.upstream.remote
end
- return remote
+ local remotes = git.remote.list()
+ if #remotes == 1 then
+ return remotes[1]
+ elseif vim.tbl_contains(remotes, "origin") then
+ return "origin"
+ end
end
---@return string[]
@@ -344,7 +439,7 @@ local function update_branch_information(state)
state.head.oid = status.oid
state.head.detached = status.detached
- if status.oid ~= INITIAL_COMMIT then
+ if status.oid and status.oid ~= INITIAL_COMMIT then
state.head.abbrev = git.rev_parse.abbreviate_commit(status.oid)
state.head.commit_message = git.log.message(status.oid)
@@ -364,7 +459,7 @@ local function update_branch_information(state)
local pushRemote = git.branch.pushRemote_ref()
if pushRemote and not status.detached then
- local remote, branch = unpack(vim.split(pushRemote, "/"))
+ local remote, branch = pushRemote:match("([^/]+)/(.+)")
state.pushRemote.ref = pushRemote
state.pushRemote.remote = remote
state.pushRemote.branch = branch
diff --git a/lua/neogit/lib/git/cherry_pick.lua b/lua/neogit/lib/git/cherry_pick.lua
index f02c4405d..3d48e307e 100644
--- a/lua/neogit/lib/git/cherry_pick.lua
+++ b/lua/neogit/lib/git/cherry_pick.lua
@@ -2,14 +2,14 @@ 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
function M.pick(commits, args)
local cmd = git.cli["cherry-pick"].arg_list(util.merge(args, commits))
@@ -20,10 +20,12 @@ 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
@@ -35,10 +37,67 @@ 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
+
+---@param commits string[]
+---@param src? string
+---@param dst string
+---@param start? string
+---@param checkout_dst? boolean
+function M.move(commits, src, dst, args, start, checkout_dst)
+ local current = git.branch.current()
+
+ if not git.branch.exists(dst) then
+ git.cli.branch.args(start or "", dst).call { hidden = true }
+ local upstream = git.branch.upstream(start)
+ if upstream then
+ git.branch.set_upstream(upstream, dst)
+ end
+ end
+
+ if dst ~= current then
+ git.branch.checkout(dst)
+ end
+
+ if not src then
+ return git.cherry_pick.pick(commits, args)
+ end
+
+ local tip = commits[#commits]
+ local keep = commits[1] .. "^"
+
+ if not git.cherry_pick.pick(commits, args) then
+ return
+ end
+
+ if git.log.is_ancestor(src, tip) then
+ git.cli["update-ref"]
+ .message(string.format("reset: moving to %s", keep))
+ .args(git.rev_parse.full_name(src), keep, tip)
+ .call()
+
+ if not checkout_dst then
+ git.branch.checkout(src)
+ end
+ else
+ git.branch.checkout(src)
+
+ local editor = "nvim -c '%g/^pick \\(" .. table.concat(commits, ".*|") .. ".*\\)/norm! dd/' -c 'wq'"
+ local result =
+ git.cli.rebase.interactive.args(keep).in_pty(true).env({ GIT_SEQUENCE_EDITOR = editor }).call()
+
+ if result:failure() then
+ return notification.error("Picking failed - Fix things manually before continuing.")
+ end
+
+ if checkout_dst then
+ git.branch.checkout(dst)
+ end
end
end
diff --git a/lua/neogit/lib/git/cli.lua b/lua/neogit/lib/git/cli.lua
index 6d34b502c..958f42c98 100644
--- a/lua/neogit/lib/git/cli.lua
+++ b/lua/neogit/lib/git/cli.lua
@@ -1,73 +1,395 @@
-local logger = require("neogit.logger")
local git = require("neogit.lib.git")
local process = require("neogit.process")
local util = require("neogit.lib.util")
local Path = require("plenary.path")
local runner = require("neogit.runner")
+---@class GitCommandSetup
+---@field flags table|nil
+---@field options table|nil
+---@field aliases table|nil
+---@field short_opts table|nil
+
---@class GitCommand
---@field flags table
---@field options table
---@field aliases table
---@field short_opts table
----@field args fun(...): table
----@field arg_list fun(table): table
+
+---@class GitCommandBuilder
+---@field args fun(...): self appends all params to cli as argument
+---@field arguments fun(...): self alias for `args`
+---@field arg_list fun(table): self unpacks table and uses items as cli arguments
+---@field files fun(...): self any filepaths to append to the cli call
+---@field paths fun(...): self alias for `files`
+---@field input fun(string): self string to send to process via STDIN
+---@field stdin fun(string): self alias for `input`
+---@field prefix fun(string): self prefix for CLI call
+---@field env fun(table): self key/value pairs to set as ENV variables for process
+---@field in_pty fun(boolean): self should this be run in a PTY or not?
+---@field call fun(CliCallOptions): ProcessResult
+
+---@class CliCallOptions
+---@field hidden boolean Is the command hidden from user?
+---@field trim boolean remove blank lines from output?
+---@field remove_ansi boolean remove ansi escape-characters from output?
+---@field await boolean run synchronously if true
+---@field long boolean is the command expected to be long running? (like git bisect, commit, rebase, etc)
+---@field pty boolean run command in PTY?
+---@field on_error fun(res: ProcessResult): boolean function to call if the process exits with status > 0. Used to
+--- determine how to handle the error, if user should be alerted or not
+
+---@class GitCommandShow: GitCommandBuilder
+---@field stat self
+---@field oneline self
+---@field no_patch self
+---@field format fun(string): self
+---@field file fun(name: string, rev: string|nil): self
+
+---@class GitCommandNameRev: GitCommandBuilder
+---@field name_only self
+---@field no_undefined self
+---@field refs fun(string): self
+---@field exclude fun(string): self
+
+---@class GitCommandInit: GitCommandBuilder
+
+---@class GitCommandCheckoutIndex: GitCommandBuilder
+---@field all self
+---@field force self
+
+---@class GitCommandWorktree: GitCommandBuilder
+---@field add self
+---@field list self
+---@field move self
+---@field remove self
+
+---@class GitCommandRm: GitCommandBuilder
+---@field cached self
+
+---@class GitCommandMove: GitCommandBuilder
+
+---@class GitCommandStatus: GitCommandBuilder
+---@field short self
+---@field branch self
+---@field verbose self
+---@field null_separated self
+---@field porcelain fun(string): self
+
+---@class GitCommandLog: GitCommandBuilder
+---@field oneline self
+---@field branches self
+---@field remotes self
+---@field all self
+---@field graph self
+---@field color self
+---@field pretty fun(string): self
+---@field max_count fun(string): self
+---@field format fun(string): self
+
+---@class GitCommandConfig: GitCommandBuilder
+---@field _local self
+---@field global self
+---@field list self
+---@field _get self PRIVATE - use alias
+---@field _add self PRIVATE - use alias
+---@field _unset self PRIVATE - use alias
+---@field null self
+---@field set fun(key: string, value: string): self
+---@field unset fun(key: string): self
+---@field get fun(path: string): self
+
+---@class GitCommandDescribe: GitCommandBuilder
+---@field long self
+---@field tags self
+
+---@class GitCommandDiff: GitCommandBuilder
+---@field cached self
+---@field stat self
+---@field shortstat self
+---@field patch self
+---@field name_only self
+---@field no_ext_diff self
+---@field no_index self
+---@field index self
+---@field check self
+
+---@class GitCommandStash: GitCommandBuilder
+---@field apply self
+---@field drop self
+---@field push self
+---@field store self
+---@field index self
+---@field staged self
+---@field keep_index self
+---@field message fun(text: string): self
+
+---@class GitCommandTag: GitCommandBuilder
+---@field n self
+---@field list self
+---@field delete self
+
+---@class GitCommandRebase: GitCommandBuilder
+---@field interactive self
+---@field onto self
+---@field edit_todo self
+---@field continue self
+---@field abort self
+---@field skip self
+---@field autosquash self
+---@field autostash self
+---@field commit fun(rev: string): self
+
+---@class GitCommandMerge: GitCommandBuilder
+---@field continue self
+---@field abort self
+
+---@class GitCommandMergeBase: GitCommandBuilder
+---@field is_ancestor self
+
+---@class GitCommandReset: GitCommandBuilder
+---@field hard self
+---@field mixed self
+---@field soft self
+---@field keep self
+---@field merge self
+
+---@class GitCommandCheckout: GitCommandBuilder
+---@field b fun(): self
+---@field _track self PRIVATE - use alias
+---@field detach self
+---@field ours self
+---@field theirs self
+---@field merge self
+---@field track fun(branch: string): self
+---@field rev fun(rev: string): self
+---@field branch fun(branch: string): self
+---@field commit fun(commit: string): self
+---@field new_branch fun(new_branch: string): self
+---@field new_branch_with_start_point fun(branch: string, start_point: string): self
+
+---@class GitCommandRemote: GitCommandBuilder
+---@field push self
+---@field add self
+---@field rm self
+---@field rename self
+---@field prune self
+---@field get_url fun(remote: string): self
+
+---@class GitCommandRevert: GitCommandBuilder
+---@field no_commit self
+---@field no_edit self
+---@field continue self
+---@field skip self
+---@field abort self
+
+---@class GitCommandApply: GitCommandBuilder
+---@field ignore_space_change self
+---@field cached self
+---@field reverse self
+---@field index self
+---@field with_patch fun(string): self alias for input
+
+---@class GitCommandAdd: GitCommandBuilder
+---@field update self
+---@field all self
+
+---@class GitCommandAbsorb: GitCommandBuilder
+---@field verbose self
+---@field and_rebase self
+---@field base fun(commit: string): self
+
+---@class GitCommandCommit: GitCommandBuilder
+---@field all self
+---@field no_verify self
+---@field amend self
+---@field only self
+---@field dry_run self
+---@field no_edit self
+---@field edit self
+---@field allow_empty self
+---@field with_message fun(message: string): self Passes message via STDIN
+---@field message fun(message: string): self Passes message via CLI
+
+---@class GitCommandPush: GitCommandBuilder
+---@field delete self
+---@field remote fun(remote: string): self
+---@field to fun(to: string): self
+
+---@class GitCommandPull: GitCommandBuilder
+---@field no_commit self
+
+---@class GitCommandCherry: GitCommandBuilder
+---@field verbose self
+
+---@class GitCommandBranch: GitCommandBuilder
+---@field all self
+---@field delete self
+---@field remotes self
+---@field force self
+---@field current self
+---@field edit_description self
+---@field very_verbose self
+---@field move self
+---@field sort fun(sort: string): self
+---@field set_upstream_to fun(name: string): self
+---@field name fun(name: string): self
+
+---@class GitCommandFetch: GitCommandBuilder
+---@field recurse_submodules self
+---@field verbose self
+---@field jobs fun(n: number): self
+
+---@class GitCommandReadTree: GitCommandBuilder
+---@field merge self
+---@field index_output fun(path: string): self
+---@field tree fun(tree: string): self
+
+---@class GitCommandWriteTree: GitCommandBuilder
+
+---@class GitCommandCommitTree: GitCommandBuilder
+---@field no_gpg_sign self
+---@field parent fun(parent: string): self
+---@field message fun(message: string): self
+---@field parents fun(...): self
+---@field tree fun(tree: string): self
+
+---@class GitCommandUpdateIndex: GitCommandBuilder
+---@field add self
+---@field remove self
+---@field refresh self
+
+---@class GitCommandShowRef: GitCommandBuilder
+---@field verify self
+
+---@class GitCommandShowBranch: GitCommandBuilder
+---@field all self
+
+---@class GitCommandReflog: GitCommandBuilder
+---@field show self
+---@field format fun(format: string): self
+---@field date fun(mode: string): self
+
+---@class GitCommandUpdateRef: GitCommandBuilder
+---@field create_reflog self
+---@field message fun(text: string): self
+
+---@class GitCommandLsFiles: GitCommandBuilder
+---@field others self
+---@field deleted self
+---@field modified self
+---@field cached self
+---@field deduplicate self
+---@field exclude_standard self
+---@field full_name self
+---@field error_unmatch self
+
+---@class GitCommandLsTree: GitCommandBuilder
+---@field full_tree self
+---@field name_only self
+---@field recursive self
+
+---@class GitCommandLsRemote: GitCommandBuilder
+---@field tags self
+---@field remote fun(remote: string): self
+
+---@class GitCommandForEachRef: GitCommandBuilder
+---@field format self
+---@field sort self
+
+---@class GitCommandRevList: GitCommandBuilder
+---@field merges self
+---@field parents self
+---@field max_count fun(n: number): self
+
+---@class GitCommandRevParse: GitCommandBuilder
+---@field verify self
+---@field quiet self
+---@field short self
+---@field revs_only self
+---@field no_revs self
+---@field flags self
+---@field no_flags self
+---@field symbolic self
+---@field symbolic_full_name self
+---@field abbrev_ref fun(ref: string): self
+
+---@class GitCommandCherryPick: GitCommandBuilder
+---@field no_commit self
+---@field continue self
+---@field skip self
+---@field abort self
+
+---@class GitCommandVerifyCommit: GitCommandBuilder
+
+---@class GitCommandBisect: GitCommandBuilder
---@class NeogitGitCLI
----@field show GitCommand
----@field name-rev GitCommand
----@field init GitCommand
----@field checkout-index GitCommand
----@field worktree GitCommand
----@field rm GitCommand
----@field status GitCommand
----@field log GitCommand
----@field config GitCommand
----@field describe GitCommand
----@field diff GitCommand
----@field stash GitCommand
----@field tag GitCommand
----@field rebase GitCommand
----@field merge GitCommand
----@field merge-base GitCommand
----@field reset GitCommand
----@field checkout GitCommand
----@field remote GitCommand
----@field apply GitCommand
----@field add GitCommand
----@field absorb GitCommand
----@field commit GitCommand
----@field push GitCommand
----@field pull GitCommand
----@field cherry GitCommand
----@field branch GitCommand
----@field fetch GitCommand
----@field read-tree GitCommand
----@field write-tree GitCommand
----@field commit-tree GitCommand
----@field update-index GitCommand
----@field show-ref GitCommand
----@field show-branch GitCommand
----@field update-ref GitCommand
----@field ls-files GitCommand
----@field ls-tree GitCommand
----@field ls-remote GitCommand
----@field for-each-ref GitCommand
----@field rev-list GitCommand
----@field rev-parse GitCommand
----@field cherry-pick GitCommand
----@field verify-commit GitCommand
----@field bisect GitCommand
----@field git_root fun(dir: string):string
+---@field absorb GitCommandAbsorb
+---@field add GitCommandAdd
+---@field apply GitCommandApply
+---@field bisect GitCommandBisect
+---@field branch GitCommandBranch
+---@field checkout GitCommandCheckout
+---@field checkout-index GitCommandCheckoutIndex
+---@field cherry GitCommandCherry
+---@field cherry-pick GitCommandCherryPick
+---@field commit GitCommandCommit
+---@field commit-tree GitCommandCommitTree
+---@field config GitCommandConfig
+---@field describe GitCommandDescribe
+---@field diff GitCommandDiff
+---@field fetch GitCommandFetch
+---@field for-each-ref GitCommandForEachRef
+---@field init GitCommandInit
+---@field log GitCommandLog
+---@field ls-files GitCommandLsFiles
+---@field ls-remote GitCommandLsRemote
+---@field ls-tree GitCommandLsTree
+---@field merge GitCommandMerge
+---@field merge-base GitCommandMergeBase
+---@field name-rev GitCommandNameRev
+---@field pull GitCommandPull
+---@field push GitCommandPush
+---@field read-tree GitCommandReadTree
+---@field rebase GitCommandRebase
+---@field reflog GitCommandReflog
+---@field remote GitCommandRemote
+---@field revert GitCommandRevert
+---@field reset GitCommandReset
+---@field rev-list GitCommandRevList
+---@field rev-parse GitCommandRevParse
+---@field rm GitCommandRm
+---@field show GitCommandShow
+---@field show-branch GitCommandShowBranch
+---@field show-ref GitCommandShowRef
+---@field stash GitCommandStash
+---@field status GitCommandStatus
+---@field tag GitCommandTag
+---@field update-index GitCommandUpdateIndex
+---@field update-ref GitCommandUpdateRef
+---@field verify-commit GitCommandVerifyCommit
+---@field worktree GitCommandWorktree
+---@field write-tree GitCommandWriteTree
+---@field mv GitCommandMove
+---@field worktree_root fun(dir: string):string
+---@field git_dir fun(dir: string):string
+---@field worktree_git_dir fun(dir: string):string
---@field is_inside_worktree fun(dir: string):boolean
+---@field history ProcessResult[]
+---@param setup GitCommandSetup|nil
+---@return GitCommand
local function config(setup)
setup = setup or {}
- setup.flags = setup.flags or {}
- setup.options = setup.options or {}
- setup.aliases = setup.aliases or {}
- setup.short_opts = setup.short_opts or {}
- return setup
+
+ local command = {}
+ command.flags = setup.flags or {}
+ command.options = setup.options or {}
+ command.aliases = setup.aliases or {}
+ command.short_opts = setup.short_opts or {}
+
+ return command
end
local configurations = {
@@ -150,13 +472,6 @@ local configurations = {
max_count = "--max-count",
format = "--format",
},
- aliases = {
- for_range = function(tbl)
- return function(range)
- return tbl.args(range)
- end
- end,
- },
},
config = config {
@@ -289,11 +604,14 @@ local configurations = {
flags = {
no_commit = "--no-commit",
continue = "--continue",
+ no_edit = "--no-edit",
skip = "--skip",
abort = "--abort",
},
},
+ mv = config {},
+
checkout = config {
short_opts = {
b = "-b",
@@ -409,14 +727,11 @@ local configurations = {
end
end,
message = function(tbl)
- return function(text)
- return tbl.args("-m", text)
+ return function(message)
+ return tbl.args("-m", message)
end
end,
},
- options = {
- commit_message_file = "--file",
- },
},
push = config {
@@ -441,9 +756,6 @@ local configurations = {
flags = {
no_commit = "--no-commit",
},
- pull = config {
- flags = {},
- },
},
cherry = config {
@@ -463,12 +775,11 @@ local configurations = {
very_verbose = "-vv",
move = "-m",
},
+ options = {
+ sort = "--sort",
+ set_upstream_to = "--set-upstream-to",
+ },
aliases = {
- list = function(tbl)
- return function(sort)
- return tbl.args("--sort=" .. sort)
- end
- end,
name = function(tbl)
return function(name)
return tbl.args(name)
@@ -478,16 +789,12 @@ local configurations = {
},
fetch = config {
- options = {
+ flags = {
recurse_submodules = "--recurse-submodules",
verbose = "--verbose",
},
- aliases = {
- jobs = function(tbl)
- return function(n)
- return tbl.args("--jobs=" .. tostring(n))
- end
- end,
+ options = {
+ jobs = "--jobs",
},
},
@@ -577,6 +884,7 @@ local configurations = {
aliases = {
message = function(tbl)
return function(text)
+ -- TODO: Is this escapement needed?
local escaped_text, _ = text:gsub([["]], [[\"]])
return tbl.args("-m", string.format([["%s"]], escaped_text))
end
@@ -666,15 +974,34 @@ local configurations = {
["bisect"] = config {},
}
---- NOTE: Use require("neogit.lib.git").repo.git_root instead of calling this function.
---- repository.git_root is used by all other library functions, so it's most likely the one you want to use.
---- git_root_of_cwd() returns the git repo of the cwd, which can change anytime
---- after git_root_of_cwd() has been called.
+--- NOTE: Use require("neogit.lib.git").repo.worktree_root instead of calling this function.
+--- repository.worktree_root is used by all other library functions, so it's most likely the one you want to use.
+--- worktree_root_of_cwd() returns the git repo of the cwd, which can change anytime
+--- after worktree_root_of_cwd() has been called.
---@param dir string
----@return string
-local function git_root(dir)
+---@return string Absolute path of current worktree
+local function worktree_root(dir)
local cmd = { "git", "-C", dir, "rev-parse", "--show-toplevel" }
local result = vim.system(cmd, { text = true }):wait()
+
+ return Path:new(vim.trim(result.stdout)):absolute()
+end
+
+---@param dir string
+---@return string Absolute path of `.git/` directory
+local function git_dir(dir)
+ local cmd = { "git", "-C", dir, "rev-parse", "--git-common-dir" }
+ local result = vim.system(cmd, { text = true }):wait()
+
+ return Path:new(vim.trim(result.stdout)):absolute()
+end
+
+---@param dir string
+---@return string Absolute path of `.git/` directory
+local function worktree_git_dir(dir)
+ local cmd = { "git", "-C", dir, "rev-parse", "--git-dir" }
+ local result = vim.system(cmd, { text = true }):wait()
+
return Path:new(vim.trim(result.stdout)):absolute()
end
@@ -683,6 +1010,7 @@ end
local function is_inside_worktree(dir)
local cmd = { "git", "-C", dir, "rev-parse", "--is-inside-work-tree" }
local result = vim.system(cmd):wait()
+
return result.code == 0
end
@@ -846,6 +1174,7 @@ local function new_builder(subcommand)
"--no-optional-locks",
"-c", "core.preloadindex=true",
"-c", "color.ui=always",
+ "-c", "diff.noprefix=false",
subcommand
},
cmd
@@ -853,7 +1182,7 @@ local function new_builder(subcommand)
return process.new {
cmd = cmd,
- cwd = git.repo.git_root,
+ cwd = git.repo.worktree_root,
env = state.env,
input = state.input,
on_error = opts.on_error,
@@ -864,6 +1193,7 @@ local function new_builder(subcommand)
}
end
+ ---@return CliCallOptions
local function make_options(options)
local opts = vim.tbl_extend("keep", (options or {}), {
hidden = false,
@@ -929,7 +1259,9 @@ local meta = {
local cli = setmetatable({
history = runner.history,
- git_root = git_root,
+ worktree_root = worktree_root,
+ worktree_git_dir = worktree_git_dir,
+ git_dir = git_dir,
is_inside_worktree = is_inside_worktree,
}, meta)
diff --git a/lua/neogit/lib/git/config.lua b/lua/neogit/lib/git/config.lua
index bb73b1d1b..f6645305f 100644
--- a/lua/neogit/lib/git/config.lua
+++ b/lua/neogit/lib/git/config.lua
@@ -68,12 +68,23 @@ function ConfigEntry:update(value)
end
end
+---@return self
+function ConfigEntry:refresh()
+ if self.scope == "local" then
+ self.value = M.get_local(self.name).value
+ elseif self.scope == "global" then
+ self.value = M.get_global(self.name).value
+ end
+
+ return self
+end
+
---@type table
local config_cache = {}
local cache_key = nil
local function make_cache_key()
- local stat = vim.loop.fs_stat(git.repo:git_path("config"):absolute())
+ local stat = vim.uv.fs_stat(git.repo:git_path("config"):absolute())
if stat then
return stat.mtime.sec
end
@@ -109,7 +120,13 @@ end
---@return ConfigEntry
function M.get(key)
- return config()[key:lower()] or ConfigEntry.new(key, "", "local")
+ if M.get_local(key):is_set() then
+ return M.get_local(key)
+ elseif M.get_global(key):is_set() then
+ return M.get_global(key)
+ else
+ return ConfigEntry.new(key, "", "local")
+ end
end
---@return ConfigEntry
@@ -118,6 +135,11 @@ function M.get_global(key)
return ConfigEntry.new(key, result, "global")
end
+---@return ConfigEntry
+function M.get_local(key)
+ return config()[key:lower()] or ConfigEntry.new(key, "", "local")
+end
+
function M.get_matching(pattern)
local matches = {}
for key, value in pairs(config()) do
diff --git a/lua/neogit/lib/git/diff.lua b/lua/neogit/lib/git/diff.lua
index 7bfaec2f0..64d3be1fa 100644
--- a/lua/neogit/lib/git/diff.lua
+++ b/lua/neogit/lib/git/diff.lua
@@ -6,6 +6,45 @@ local logger = require("neogit.logger")
local insert = table.insert
local sha256 = vim.fn.sha256
+---@class NeogitGitDiff
+---@field parse fun(raw_diff: string[], raw_stats: string[]): Diff
+---@field build fun(section: string, file: StatusItem)
+---@field staged_stats fun(): DiffStagedStats
+---
+---@class Diff
+---@field kind string
+---@field lines string[]
+---@field file string
+---@field info table
+---@field stats table
+---@field hunks Hunk
+---
+---@class DiffStats
+---@field additions number
+---@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
+---@field files DiffStagedStatsFile
+---
+---@class DiffStagedStatsFile
+---@field path string|nil
+---@field changes string|nil
+---@field insertions string|nil
+---@field deletions string|nil
+
+---@param raw string|string[]
+---@return DiffStats
local function parse_diff_stats(raw)
if type(raw) == "string" then
raw = vim.split(raw, ", ")
@@ -33,6 +72,8 @@ local function parse_diff_stats(raw)
return stats
end
+---@param output string[]
+---@return string[], number
local function build_diff_header(output)
local header = {}
local start_idx = 1
@@ -50,9 +91,12 @@ local function build_diff_header(output)
return header, start_idx
end
+---@param header string[]
+---@param kind string
+---@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
@@ -64,6 +108,8 @@ local function build_file(header, kind)
end
end
+---@param header string[]
+---@return string, string[]
local function build_kind(header)
local kind = ""
local info = {}
@@ -83,6 +129,9 @@ local function build_kind(header)
return kind, info
end
+---@param output string[]
+---@param start_idx number
+---@return string[]
local function build_lines(output, start_idx)
local lines = {}
@@ -97,18 +146,13 @@ local function build_lines(output, start_idx)
return lines
end
+---@param content string[]
+---@return string
local function hunk_hash(content)
return sha256(table.concat(content, "\n"))
end
----@class Hunk
----@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
-
+---@param lines string[]
---@return Hunk
local function build_hunks(lines)
local hunks = {}
@@ -171,6 +215,9 @@ local function build_hunks(lines)
return hunks
end
+---@param raw_diff string[]
+---@param raw_stats string[]
+---@return Diff
local function parse_diff(raw_diff, raw_stats)
local header, start_idx = build_diff_header(raw_diff)
local lines = build_lines(raw_diff, start_idx)
@@ -179,7 +226,12 @@ local function parse_diff(raw_diff, raw_stats)
local file = build_file(header, kind)
local stats = parse_diff_stats(raw_stats or {})
- return {
+ util.map(hunks, function(hunk)
+ hunk.file = file
+ return hunk
+ end)
+
+ return { ---@type Diff
kind = kind,
lines = lines,
file = file,
@@ -205,6 +257,8 @@ local function build_metatable(f, raw_output_fn)
end
-- Doing a git-diff with untracked files will exit(1) if a difference is observed, which we can ignore.
+---@param name string
+---@return fun(): table
local function raw_untracked(name)
return function()
local diff = git.cli.diff.no_ext_diff.no_index
@@ -216,6 +270,8 @@ local function raw_untracked(name)
end
end
+---@param name string
+---@return fun(): table
local function raw_unstaged(name)
return function()
local diff = git.cli.diff.no_ext_diff.files(name).call({ hidden = true }).stdout
@@ -225,6 +281,8 @@ local function raw_unstaged(name)
end
end
+---@param name string
+---@return fun(): table
local function raw_staged_unmerged(name)
return function()
local diff = git.cli.diff.no_ext_diff.files(name).call({ hidden = true }).stdout
@@ -234,6 +292,8 @@ local function raw_staged_unmerged(name)
end
end
+---@param name string
+---@return fun(): table
local function raw_staged(name)
return function()
local diff = git.cli.diff.no_ext_diff.cached.files(name).call({ hidden = true }).stdout
@@ -243,6 +303,8 @@ local function raw_staged(name)
end
end
+---@param name string
+---@return fun(): table
local function raw_staged_renamed(name, original)
return function()
local diff = git.cli.diff.no_ext_diff.cached.files(name, original).call({ hidden = true }).stdout
@@ -253,6 +315,8 @@ local function raw_staged_renamed(name, original)
end
end
+---@param section string
+---@param file StatusItem
local function build(section, file)
if section == "untracked" then
build_metatable(file, raw_untracked(file.name))
@@ -269,48 +333,51 @@ local function build(section, file)
end
end
----@class NeogitGitDiff
-return {
- parse = parse_diff,
- staged_stats = function()
- local raw = git.cli.diff.no_ext_diff.cached.stat.call({ hidden = true }).stdout
- local files = {}
- local summary
-
- local idx = 1
- local function advance()
- idx = idx + 1
- end
+---@return DiffStagedStats
+local function staged_stats()
+ local raw = git.cli.diff.no_ext_diff.cached.stat.call({ hidden = true }).stdout
+ local files = {}
+ local summary
- local function peek()
- return raw[idx]
- end
+ local idx = 1
+ local function advance()
+ idx = idx + 1
+ end
- while true do
- local line = peek()
- if not line then
- break
- end
+ local function peek()
+ return raw[idx]
+ end
- if line:match("^ %d+ file[s ]+changed,") then
- summary = vim.trim(line)
- break
- else
- table.insert(files, {
- path = vim.trim(line:match("^ ([^ ]+)")),
- changes = line:match("|%s+(%d+)"),
- insertions = line:match("|%s+%d+ (%+*)"),
- deletions = line:match("|%s+%d+ %+*(%-*)$"),
- })
-
- advance()
- end
+ while true do
+ local line = peek()
+ if not line then
+ break
end
- return {
- summary = summary,
- files = files,
- }
- end,
+ if line:match("^ %d+ file[s ]+changed,") then
+ summary = vim.trim(line)
+ break
+ else
+ local file = { ---@type DiffStagedStatsFile
+ path = vim.trim(line:match("^ ([^ ]+)")),
+ changes = line:match("|%s+(%d+)"),
+ insertions = line:match("|%s+%d+ (%+*)"),
+ deletions = line:match("|%s+%d+ %+*(%-*)$"),
+ }
+
+ insert(files, file)
+ advance()
+ end
+ end
+
+ return {
+ summary = summary,
+ files = files,
+ }
+end
+
+return { ---@type NeogitGitDiff
+ parse = parse_diff,
+ staged_stats = staged_stats,
build = build,
}
diff --git a/lua/neogit/lib/git/files.lua b/lua/neogit/lib/git/files.lua
index 36a880ad2..af652f7d9 100644
--- a/lua/neogit/lib/git/files.lua
+++ b/lua/neogit/lib/git/files.lua
@@ -1,26 +1,49 @@
local git = require("neogit.lib.git")
+local util = require("neogit.lib.util")
+local Path = require("plenary.path")
---@class NeogitGitFiles
local M = {}
+---@return string[]
function M.all()
return git.cli["ls-files"].full_name.deleted.modified.exclude_standard.deduplicate.call({
hidden = true,
}).stdout
end
+---@return string[]
function M.untracked()
return git.cli["ls-files"].others.exclude_standard.call({ hidden = true }).stdout
end
-function M.all_tree()
- return git.cli["ls-tree"].full_tree.name_only.recursive.args("HEAD").call({ hidden = true }).stdout
+---@param opts? { with_dir: boolean }
+---@return string[]
+function M.all_tree(opts)
+ opts = opts or {}
+ local files = git.cli["ls-tree"].full_tree.name_only.recursive.args("HEAD").call({ hidden = true }).stdout
+
+ if opts.with_dir then
+ local dirs = {}
+
+ for _, path in ipairs(files) do
+ local dir = vim.fs.dirname(path) .. Path.path.sep
+ dirs[dir] = true
+ end
+
+ files = util.merge(files, vim.tbl_keys(dirs))
+ table.sort(files)
+ end
+
+ return files
end
+---@return string[]
function M.diff(commit)
return git.cli.diff.name_only.args(commit .. "...").call({ hidden = true }).stdout
end
+---@return string
function M.relpath_from_repository(path)
local result = git.cli["ls-files"].others.cached.modified.deleted.full_name
.args(path)
@@ -29,12 +52,23 @@ function M.relpath_from_repository(path)
return result.stdout[1]
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():success()
end
return M
diff --git a/lua/neogit/lib/git/hooks.lua b/lua/neogit/lib/git/hooks.lua
index 0ee07e6b5..991a2ab07 100644
--- a/lua/neogit/lib/git/hooks.lua
+++ b/lua/neogit/lib/git/hooks.lua
@@ -1,4 +1,4 @@
-local Path = require("plenary.path") ---@class Path
+local Path = require("plenary.path")
local git = require("neogit.lib.git")
local M = {} ---@class NeogitGitHooks
@@ -47,13 +47,13 @@ function M.register(meta)
meta.update_hooks = function(state)
state.hooks = {}
- if not Path:new(state.git_root):joinpath(".git", "hooks"):is_dir() then
+ if not Path:new(state.git_dir):joinpath("hooks"):is_dir() then
return
end
- for file in vim.fs.dir(vim.fs.joinpath(state.git_root, ".git", "hooks")) do
+ for file in vim.fs.dir(vim.fs.joinpath(state.git_dir, "hooks")) do
if not file:match("%.sample$") then
- local path = vim.fs.joinpath(state.git_root, ".git", "hooks", file)
+ local path = vim.fs.joinpath(state.git_dir, "hooks", file)
local stat = vim.uv.fs_stat(path)
if stat and stat.mode and is_executable(stat.mode) then
diff --git a/lua/neogit/lib/git/index.lua b/lua/neogit/lib/git/index.lua
index d929d08b2..354eceee1 100644
--- a/lua/neogit/lib/git/index.lua
+++ b/lua/neogit/lib/git/index.lua
@@ -1,64 +1,53 @@
local git = require("neogit.lib.git")
local Path = require("plenary.path")
local util = require("neogit.lib.util")
-local uv = vim.uv or vim.loop
---@class NeogitGitIndex
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,10 +57,10 @@ function M.generate_patch(item, hunk, from, to, reverse)
string.format("@@ -%d,%d +%d,%d @@", hunk.index_from, len_start, hunk.index_from, len_start + len_offset)
)
- local git_root = git.repo.git_root
+ 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(git_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))
@@ -134,8 +123,8 @@ function M.with_temp_index(revision, fn)
assert(revision, "temp index requires a revision")
assert(fn, "Pass a function to call with temp index")
- local tmp_index = Path:new(uv.os_tmpdir(), ("index.neogit.%s"):format(revision))
- git.cli["read-tree"].args(revision).index_output(tmp_index:absolute()).call { hidden = true }
+ local tmp_index = Path:new(vim.uv.os_tmpdir(), ("index.neogit.%s"):format(revision))
+ git.cli["read-tree"].index_output(tmp_index:absolute()).args(revision).call { hidden = true }
assert(tmp_index:exists(), "Failed to create temp index")
fn(tmp_index:absolute())
@@ -153,8 +142,28 @@ function M.update()
on_error = function(_)
return false
end,
+ suppress_console = true,
+ git_hook = false,
+ user_command = false,
})
:spawn_async()
end
+local function timestamp()
+ local now = os.date("!*t")
+ return string.format("%s-%s-%sT%s.%s.%s", now.year, now.month, now.day, now.hour, now.min, now.sec)
+end
+
+-- https://gist.github.com/chx/3a694c2a077451e3d446f85546bb9278
+-- Capture state of index as reflog entry
+function M.create_backup()
+ git.cli.add.update.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/init.lua b/lua/neogit/lib/git/init.lua
index 1b5365c47..8b6f7fed0 100644
--- a/lua/neogit/lib/git/init.lua
+++ b/lua/neogit/lib/git/init.lua
@@ -28,7 +28,8 @@ M.init_repo = function()
status.instance():chdir(directory)
end
- if git.cli.is_inside_worktree() then
+ if git.cli.is_inside_worktree(directory) then
+ vim.cmd.redraw()
if not input.get_permission(("Reinitialize existing repository %s?"):format(directory)) then
return
end
diff --git a/lua/neogit/lib/git/log.lua b/lua/neogit/lib/git/log.lua
index 4b3c61857..4958f433b 100644
--- a/lua/neogit/lib/git/log.lua
+++ b/lua/neogit/lib/git/log.lua
@@ -2,6 +2,7 @@ local git = require("neogit.lib.git")
local util = require("neogit.lib.util")
local config = require("neogit.config")
local record = require("neogit.lib.record")
+local state = require("neogit.lib.state")
---@class NeogitGitLog
local M = {}
@@ -21,7 +22,16 @@ local commit_header_pat = "([| ]*)(%*?)([| ]*)commit (%w+)"
---@field committer_date string when the committer committed
---@field description string a list of lines
---@field commit_arg string the passed argument of the git command
+---@field subject string
+---@field parent string
---@field diffs any[]
+---@field ref_name string
+---@field abbreviated_commit string
+---@field body string
+---@field verification_flag string?
+---@field rel_date string
+---@field log_date string
+---@field unix_date string
---Parses the provided list of lines into a CommitLogEntry
---@param raw string[]
@@ -134,7 +144,7 @@ function M.parse(raw)
if not line or vim.startswith(line, "diff") then
-- There was a previous diff, parse it
if in_diff then
- table.insert(commit.diffs, git.diff.parse(current_diff))
+ table.insert(commit.diffs, git.diff.parse(current_diff, {}))
current_diff = {}
end
@@ -142,7 +152,7 @@ function M.parse(raw)
elseif line == "" then -- A blank line signifies end of diffs
-- Parse the last diff, consume the blankline, and exit
if in_diff then
- table.insert(commit.diffs, git.diff.parse(current_diff))
+ table.insert(commit.diffs, git.diff.parse(current_diff, {}))
current_diff = {}
end
@@ -280,6 +290,17 @@ local function determine_order(options, graph)
return options
end
+--- Specifies date format when not using relative dates
+--- @param options table
+--- @return table, string|nil
+local function set_date_format(options)
+ if config.values.log_date_format ~= nil then
+ table.insert(options, "--date=format:" .. config.values.log_date_format)
+ end
+
+ return options
+end
+
---@param options table|nil
---@param files? table
---@param color? boolean
@@ -320,6 +341,8 @@ local function format(show_signature)
committer_email = "%cE",
committer_date = "%cD",
rel_date = "%cr",
+ log_date = "%cd",
+ unix_date = "%ct",
}
if show_signature then
@@ -345,6 +368,7 @@ M.list = util.memoize(function(options, graph, files, hidden, graph_color)
options = ensure_max(options or {})
options = determine_order(options, graph)
options, signature = show_signature(options)
+ options = set_date_format(options)
local output = git.cli.log
.format(format(signature))
@@ -353,7 +377,7 @@ M.list = util.memoize(function(options, graph, files, hidden, graph_color)
.files(unpack(files))
.call({ hidden = hidden, ignore_error = hidden }).stdout
- local commits = record.decode(output)
+ local commits = record.decode(output) ---@type CommitLogEntry[]
if vim.tbl_isempty(commits) then
return {}
end
@@ -361,7 +385,9 @@ M.list = util.memoize(function(options, graph, files, hidden, graph_color)
local graph_output
if graph then
if config.values.graph_style == "unicode" then
- graph_output = require("neogit.lib.graph").build(commits)
+ graph_output = require("neogit.lib.graph.unicode").build(commits)
+ elseif config.values.graph_style == "kitty" then
+ graph_output = require("neogit.lib.graph.kitty").build(commits, graph_color)
elseif config.values.graph_style == "ascii" then
util.remove_item_from_table(options, "--show-signature")
graph_output = M.graph(options, files, graph_color)
@@ -380,7 +406,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
@@ -394,17 +421,27 @@ function M.parent(commit)
end
function M.register(meta)
- meta.update_recent = function(state)
- state.recent = { items = {} }
+ meta.update_recent = function(repo_state)
+ repo_state.recent = { items = {} }
local count = config.values.status.recent_commit_count
+ local order = state.get({ "NeogitMarginPopup", "-order" }, config.values.commit_order)
+
if count > 0 then
- state.recent.items =
- util.filter_map(M.list({ "--max-count=" .. tostring(count) }, nil, {}, true), M.present_commit)
+ local args = { "--max-count=" .. tostring(count) }
+ local graph = nil
+ if order and order ~= "" then
+ table.insert(args, "--" .. order .. "-order")
+ graph = {}
+ end
+
+ repo_state.recent.items = util.filter_map(M.list(args, graph, {}, false), M.present_commit)
end
end
end
+---@param from string
+---@param to string
function M.update_ref(from, to)
git.cli["update-ref"].message(string.format("reset: moving to %s", to)).args(from, to).call()
end
@@ -414,7 +451,7 @@ function M.message(commit)
end
function M.full_message(commit)
- return git.cli.log.max_count(1).format("%B").args(commit).call({ hidden = true }).stdout
+ return git.cli.log.max_count(1).format("%B").args(commit).call({ hidden = true, trim = false }).stdout
end
---@class CommitItem
@@ -439,7 +476,7 @@ end
--- Runs `git verify-commit`
---@param commit string Hash of commit
----@return string The stderr output of the command
+---@return string[] The stderr output of the command
function M.verify_commit(commit)
return git.cli["verify-commit"].args(commit).call({ ignore_error = true }).stderr
end
@@ -520,7 +557,7 @@ function M.reflog_message(skip)
end
M.abbreviated_size = util.memoize(function()
- local commits = M.list({ "HEAD", "--max-count=1" }, {}, {}, true)
+ local commits = M.list({ "HEAD", "--max-count=1" }, nil, {}, true)
if vim.tbl_isempty(commits) then
return 7
else
@@ -528,4 +565,19 @@ M.abbreviated_size = util.memoize(function()
end
end, { timeout = math.huge })
+function M.decorate(oid)
+ local result = git.cli.log.format("%D").max_count(1).args(oid).call().stdout
+
+ if result[1] == nil then
+ return oid
+ else
+ local decorated_ref = vim.split(result[1], ",")[1]
+ if decorated_ref:match("%->") or decorated_ref:match("tag: ") then
+ return oid
+ else
+ return decorated_ref
+ end
+ end
+end
+
return M
diff --git a/lua/neogit/lib/git/merge.lua b/lua/neogit/lib/git/merge.lua
index 77d431484..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
@@ -37,6 +34,17 @@ function M.in_progress()
return git.repo.state.merge.head ~= nil
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():failure()
+end
+
+---@return boolean
+function M.any_conflicted()
+ return git.cli.diff.check.call():failure()
+end
+
---@class MergeItem
---Not used, just for a consistent interface
@@ -44,7 +52,7 @@ M.register = function(meta)
meta.update_merge_status = function(state)
state.merge = { head = nil, branch = nil, msg = "", items = {} }
- local merge_head = git.repo:git_path("MERGE_HEAD")
+ local merge_head = git.repo:worktree_git_path("MERGE_HEAD")
if not merge_head:exists() then
return
end
@@ -52,7 +60,7 @@ M.register = function(meta)
state.merge.head = merge_head:read():match("([^\r\n]+)")
state.merge.subject = git.log.message(state.merge.head)
- local message = git.repo:git_path("MERGE_MSG")
+ local message = git.repo:worktree_git_path("MERGE_MSG")
if message:exists() then
state.merge.msg = message:read():match("([^\r\n]+)") -- we need \r? to support windows
state.merge.branch = state.merge.msg:match("Merge branch '(.*)'$")
diff --git a/lua/neogit/lib/git/push.lua b/lua/neogit/lib/git/push.lua
index db142273b..2041add09 100644
--- a/lua/neogit/lib/git/push.lua
+++ b/lua/neogit/lib/git/push.lua
@@ -5,14 +5,32 @@ 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)
return git.cli.push.args(remote or "", branch or "").arg_list(args).call { pty = true }
end
+---@param branch string|nil
+---@return boolean
+function M.auto_setup_remote(branch)
+ if not branch then
+ return false
+ end
+
+ local push_autoSetupRemote = git.config.get("push.autoSetupRemote"):read()
+ local push_default = git.config.get("push.default"):read()
+ local branch_remote = git.config.get_local("branch." .. branch .. ".remote"):read()
+
+ return (
+ push_autoSetupRemote
+ and (push_default == "current" or push_default == "simple" or push_default == "upstream")
+ and not branch_remote
+ ) == true
+end
+
local function update_unmerged(state)
local status = git.branch.status()
diff --git a/lua/neogit/lib/git/rebase.lua b/lua/neogit/lib/git/rebase.lua
index 2493597c4..ed8aa0015 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
@@ -19,16 +16,16 @@ end
---@param args? string[] list of arguments to pass to git rebase
---@return ProcessResult
function M.instantly(commit, args)
- local result = git.cli.rebase
- .env({ GIT_SEQUENCE_EDITOR = ":" }).interactive.autostash.autosquash
- .arg_list(args or {})
+ local result = git.cli.rebase.interactive.autostash.autosquash
.commit(commit)
+ .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,43 @@ 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)
+ if vim.tbl_contains(args, "--root") then
+ start = ""
+ end
+
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
@@ -98,29 +99,29 @@ end
function M.modify(commit)
local short_commit = git.rev_parse.abbreviate_commit(commit)
local editor = "nvim -c '%s/^pick \\(" .. short_commit .. ".*\\)/edit \\1/' -c 'wq'"
- local result = git.cli.rebase
- .env({ GIT_SEQUENCE_EDITOR = editor }).interactive.autosquash.autostash
- .in_pty(true)
+ local result = git.cli.rebase.interactive.autosquash.autostash
.commit(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)
local short_commit = git.rev_parse.abbreviate_commit(commit)
local editor = "nvim -c '%s/^pick \\(" .. short_commit .. ".*\\)/drop \\1/' -c 'wq'"
- local result = git.cli.rebase
- .env({ GIT_SEQUENCE_EDITOR = editor }).interactive.autosquash.autostash
- .in_pty(true)
+ local result = git.cli.rebase.interactive.autosquash.autostash
.commit(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 +145,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 +172,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
@@ -192,8 +193,8 @@ function M.update_rebase_status(state)
state.rebase = { items = {}, onto = {}, head_oid = nil, head = nil, current = nil }
local rebase_file
- local rebase_merge = git.repo:git_path("rebase-merge")
- local rebase_apply = git.repo:git_path("rebase-apply")
+ local rebase_merge = git.repo:worktree_git_path("rebase-merge")
+ local rebase_apply = git.repo:worktree_git_path("rebase-apply")
if rebase_merge:exists() then
rebase_file = rebase_merge
@@ -224,14 +225,18 @@ function M.update_rebase_status(state)
if done:exists() then
for line in done:iter() do
if line:match("^[^#]") and line ~= "" then
- local oid = line:match("^%w+ (%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,
- })
+ local oid = line:match("^%w+ (%x+)") or line:match("^fixup %-C (%x+)")
+ 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 +253,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/reflog.lua b/lua/neogit/lib/git/reflog.lua
index 26f5d4740..ca3c1650c 100644
--- a/lua/neogit/lib/git/reflog.lua
+++ b/lua/neogit/lib/git/reflog.lua
@@ -1,5 +1,6 @@
local git = require("neogit.lib.git")
local util = require("neogit.lib.util")
+local config = require("neogit.config")
---@class NeogitGitReflog
local M = {}
@@ -15,7 +16,7 @@ local function parse(entries)
return util.filter_map(entries, function(entry)
index = index + 1
- local hash, author, name, subject, date = unpack(vim.split(entry, "\30"))
+ local hash, author, name, subject, rel_date, commit_date = unpack(vim.split(entry, "\30"))
local command, message = subject:match([[^(.-): (.*)]])
if not command then
command = subject:match([[^(.-):]])
@@ -42,7 +43,8 @@ local function parse(entries)
author_name = author,
ref_name = name,
ref_subject = message,
- rel_date = date,
+ rel_date = rel_date,
+ commit_date = commit_date,
type = command,
}
end)
@@ -50,17 +52,28 @@ end
function M.list(refname, options)
local format = table.concat({
- "%h", -- Full Hash
+ "%H", -- Full Hash
"%aN", -- Author Name
"%gd", -- Reflog Name
"%gs", -- Reflog Subject
"%cr", -- Commit Date (Relative)
+ "%cd", -- Commit Date
}, "%x1E")
+ util.remove_item_from_table(options, "--simplify-by-decoration")
+ util.remove_item_from_table(options, "--follow")
+
+ local date_format
+ if config.values.log_date_format ~= nil then
+ date_format = "format:" .. config.values.log_date_format
+ else
+ date_format = "raw"
+ end
+
return parse(
git.cli.reflog.show
.format(format)
- .date("raw")
+ .date(date_format)
.arg_list(options or {})
.args(refname, "--")
.call({ hidden = true }).stdout
diff --git a/lua/neogit/lib/git/refs.lua b/lua/neogit/lib/git/refs.lua
index 0c25a2289..b7ae41cba 100644
--- a/lua/neogit/lib/git/refs.lua
+++ b/lua/neogit/lib/git/refs.lua
@@ -75,7 +75,7 @@ local RECORD_TEMPLATE = record.encode({
local insert = table.insert
local format = string.format
local match = string.match
-local substring = string.sub
+local split = vim.split
local LOCAL_BRANCH = "local_branch"
local REMOTE_BRANCH = "remote_branch"
@@ -84,9 +84,9 @@ local TAG_TEMPLATE = "tags/%s"
local BRANCH_TEMPLATE = "%s/%s"
local REMOTE_BRANCH_PATTERN = "^refs/remotes/([^/]*)/(.*)$"
local HEAD = "*"
-local head = "h"
-local remote = "r"
-local tag = "t"
+local head = "heads"
+local remote = "remotes"
+local tag = "tags"
function M.list_parsed()
local result = record.decode(refs(RECORD_TEMPLATE))
@@ -100,7 +100,7 @@ function M.list_parsed()
for _, ref in ipairs(result) do
ref.head = ref.head == HEAD
- local ref_type = substring(ref.ref, 6, 6)
+ local ref_type = split(ref.ref, "/")[2]
if ref_type == head then
ref.type = LOCAL_BRANCH
ref.unambiguous_name = ref.name
@@ -132,7 +132,7 @@ M.heads = util.memoize(function()
local heads = { "HEAD", "ORIG_HEAD", "FETCH_HEAD", "MERGE_HEAD", "CHERRY_PICK_HEAD" }
local present = {}
for _, head in ipairs(heads) do
- if git.repo:git_path(head):exists() then
+ if git.repo:worktree_git_path(head):exists() then
table.insert(present, head)
end
end
diff --git a/lua/neogit/lib/git/remote.lua b/lua/neogit/lib/git/remote.lua
index c7ed74b3e..2d28ebcc8 100644
--- a/lua/neogit/lib/git/remote.lua
+++ b/lua/neogit/lib/git/remote.lua
@@ -5,8 +5,10 @@ local util = require("neogit.lib.util")
local M = {}
-- https://github.com/magit/magit/blob/main/lisp/magit-remote.el#LL141C32-L141C32
+---@param remote string
+---@param new_name string|nil
local function cleanup_push_variables(remote, new_name)
- if remote == git.config.get("remote.pushDefault").value then
+ if remote == git.config.get("remote.pushDefault"):read() then
git.config.set("remote.pushDefault", new_name)
end
@@ -21,36 +23,50 @@ local function cleanup_push_variables(remote, new_name)
end
end
+---@param name string
+---@param url string
+---@param args string[]
+---@return boolean
function M.add(name, url, args)
- return git.cli.remote.add.arg_list(args).args(name, url).call({ await = true }).code == 0
+ return git.cli.remote.add.arg_list(args).args(name, url).call():success()
end
+---@param from string
+---@param to string
+---@return boolean
function M.rename(from, to)
- local result = git.cli.remote.rename.arg_list({ from, to }).call { await = true }
- if result.code == 0 then
+ local result = git.cli.remote.rename.arg_list({ from, to }).call()
+ 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 { await = true }
- if result.code == 0 then
+ local result = git.cli.remote.rm.args(name).call()
+ 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[]
M.list = util.memoize(function()
return git.cli.remote.call({ hidden = true }).stdout
end)
+---@param name string
+---@return string[]
function M.get_url(https://github.com/name)
return git.cli.remote.get_url(https://github.com/name).call({ hidden = true }).stdout
end
@@ -105,7 +121,7 @@ function M.parse(url)
repository = url:match([[/([^/]+)%.git]]) or url:match([[/([^/]+)$]])
end
- return {
+ return { ---@type RemoteInfo
url = url,
protocol = protocol,
user = user,
@@ -118,4 +134,58 @@ function M.parse(url)
}
end
+---@param oid string object-id for commit
+---@return string|nil
+function M.commit_url(https://github.com/oid)
+ local upstream = git.branch.upstream_remote()
+ if not upstream then
+ return
+ end
+
+ local template
+ local url = M.get_url(https://github.com/upstream)[1]
+
+ for s, v in pairs(require("neogit.config").values.git_services) do
+ if url:match(util.pattern_escape(s)) then
+ template = v.commit
+ break
+ end
+ end
+
+ if template and template ~= "" then
+ local format_values = M.parse(url)
+ format_values["oid"] = oid
+ local uri = util.format(template, format_values)
+
+ return uri
+ end
+end
+
+---@param branch string
+---@return string|nil
+function M.tree_url(https://github.com/branch)
+ local upstream = git.branch.upstream_remote()
+ if not upstream then
+ return
+ end
+
+ local template
+ local url = M.get_url(https://github.com/upstream)[1]
+
+ for s, v in pairs(require("neogit.config").values.git_services) do
+ if url:match(util.pattern_escape(s)) then
+ template = v.tree
+ break
+ end
+ end
+
+ if template and template ~= "" then
+ local format_values = M.parse(url)
+ format_values["branch_name"] = branch
+ local uri = util.format(template, format_values)
+
+ return uri
+ end
+end
+
return M
diff --git a/lua/neogit/lib/git/repository.lua b/lua/neogit/lib/git/repository.lua
index b1e3afb85..a8e55fcc4 100644
--- a/lua/neogit/lib/git/repository.lua
+++ b/lua/neogit/lib/git/repository.lua
@@ -1,6 +1,6 @@
local a = require("plenary.async")
local logger = require("neogit.logger")
-local Path = require("plenary.path") ---@class Path
+local Path = require("plenary.path")
local git = require("neogit.lib.git")
local ItemFilter = require("neogit.lib.item_filter")
local util = require("neogit.lib.util")
@@ -21,22 +21,25 @@ local modules = {
}
---@class NeogitRepoState
----@field git_path fun(self, ...):Path
----@field refresh fun(self, table)
----@field git_root string
----@field head NeogitRepoHead
----@field upstream NeogitRepoRemote
----@field pushRemote NeogitRepoRemote
----@field untracked NeogitRepoIndex
----@field unstaged NeogitRepoIndex
----@field staged NeogitRepoIndex
----@field stashes NeogitRepoStash
----@field recent NeogitRepoRecent
----@field sequencer NeogitRepoSequencer
----@field rebase NeogitRepoRebase
----@field merge NeogitRepoMerge
----@field bisect NeogitRepoBisect
----@field hooks string[]
+---@field git_path fun(self, ...): Path
+---@field worktree_git_path fun(self, ...): Path
+---@field refresh fun(self, table)
+---@field worktree_root string Absolute path to the root of the current worktree
+---@field worktree_git_dir string Absolute path to the .git/ dir of the current worktree
+---@field git_dir string Absolute path of the .git/ dir for the repository
+---@field head NeogitRepoHead
+---@field upstream NeogitRepoRemote
+---@field pushRemote NeogitRepoRemote
+---@field untracked NeogitRepoIndex
+---@field unstaged NeogitRepoIndex
+---@field staged NeogitRepoIndex
+---@field stashes NeogitRepoStash
+---@field recent NeogitRepoRecent
+---@field sequencer NeogitRepoSequencer
+---@field rebase NeogitRepoRebase
+---@field merge NeogitRepoMerge
+---@field bisect NeogitRepoBisect
+---@field hooks string[]
---
---@class NeogitRepoHead
---@field branch string|nil
@@ -98,7 +101,9 @@ local modules = {
---@return NeogitRepoState
local function empty_state()
return {
- git_root = "",
+ worktree_root = "",
+ worktree_git_dir = "",
+ git_dir = "",
head = {
branch = nil,
detached = false,
@@ -165,12 +170,14 @@ local function empty_state()
end
---@class NeogitRepo
----@field lib table
----@field state NeogitRepoState
----@field git_root string
----@field running table
----@field interrupt table
----@field tmp_state table
+---@field lib table
+---@field state NeogitRepoState
+---@field worktree_root string Project root, or worktree
+---@field worktree_git_dir string Dir to watch for changes in worktree
+---@field git_dir string '.git/' directory for repo
+---@field running table
+---@field interrupt table
+---@field tmp_state table
---@field refresh_callbacks function[]
local Repo = {}
Repo.__index = Repo
@@ -205,14 +212,18 @@ function Repo.new(dir)
local instance = {
lib = {},
state = empty_state(),
- git_root = git.cli.git_root(dir),
+ worktree_root = git.cli.worktree_root(dir),
+ worktree_git_dir = git.cli.worktree_git_dir(dir),
+ git_dir = git.cli.git_dir(dir),
refresh_callbacks = {},
running = util.weak_table(),
interrupt = util.weak_table(),
tmp_state = util.weak_table("v"),
}
- instance.state.git_root = instance.git_root
+ instance.state.worktree_root = instance.worktree_root
+ instance.state.worktree_git_dir = instance.worktree_git_dir
+ instance.state.git_dir = instance.git_dir
setmetatable(instance, Repo)
@@ -227,8 +238,14 @@ function Repo:reset()
self.state = empty_state()
end
+---@return Path
+function Repo:worktree_git_path(...)
+ return Path:new(self.worktree_git_dir):joinpath(...)
+end
+
+---@return Path
function Repo:git_path(...)
- return Path:new(self.git_root):joinpath(".git", ...)
+ return Path:new(self.git_dir):joinpath(...)
end
function Repo:tasks(filter, state)
@@ -277,7 +294,7 @@ function Repo:set_state(id)
end
function Repo:refresh(opts)
- if self.git_root == "" then
+ if self.worktree_root == "" then
logger.debug("[REPO] No git root found - skipping refresh")
return
end
diff --git a/lua/neogit/lib/git/reset.lua b/lua/neogit/lib/git/reset.lua
index 259ada4bf..c5dbd9017 100644
--- a/lua/neogit/lib/git/reset.lua
+++ b/lua/neogit/lib/git/reset.lua
@@ -1,85 +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
+---@param target string
+---@return boolean
+function M.hard(target)
+ git.index.create_backup()
-function M.hard(commit)
- 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)
+
+ return 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
+---@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/rev_parse.lua b/lua/neogit/lib/git/rev_parse.lua
index 18c05a3e3..b44d98196 100644
--- a/lua/neogit/lib/git/rev_parse.lua
+++ b/lua/neogit/lib/git/rev_parse.lua
@@ -28,8 +28,13 @@ end
---@return string
---@async
function M.verify(rev)
- return git.cli["rev-parse"].verify
- .abbrev_ref()
+ return git.cli["rev-parse"].verify.abbrev_ref(rev).call({ hidden = true, ignore_error = true }).stdout[1]
+end
+
+---@param rev string
+---@return string
+function M.full_name(rev)
+ return git.cli["rev-parse"].verify.symbolic_full_name
.args(rev)
.call({ hidden = true, ignore_error = true }).stdout[1]
end
diff --git a/lua/neogit/lib/git/revert.lua b/lua/neogit/lib/git/revert.lua
index 54064b3b7..a6a3e608f 100644
--- a/lua/neogit/lib/git/revert.lua
+++ b/lua/neogit/lib/git/revert.lua
@@ -4,12 +4,25 @@ local util = require("neogit.lib.util")
---@class NeogitGitRevert
local M = {}
+---@param commits string[]
+---@param args string[]
+---@return boolean, string|nil
function M.commits(commits, args)
- return git.cli.revert.no_commit.arg_list(util.merge(args, commits)).call({ pty = true }).code == 0
+ local result = git.cli.revert.no_commit.arg_list(util.merge(args, commits)).call { pty = true }
+ 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.call()
+ git.cli.revert.continue.no_edit.call { pty = true }
end
function M.skip()
diff --git a/lua/neogit/lib/git/sequencer.lua b/lua/neogit/lib/git/sequencer.lua
index c5fc385e8..a961a7e0e 100644
--- a/lua/neogit/lib/git/sequencer.lua
+++ b/lua/neogit/lib/git/sequencer.lua
@@ -8,6 +8,7 @@ local M = {}
-- And CHERRY_PICK_HEAD does not exist when a conflict happens while picking a series of commits with --no-commit.
-- And REVERT_HEAD does not exist when a conflict happens while reverting a series of commits with --no-commit.
--
+---@return boolean
function M.pick_or_revert_in_progress()
local pick_or_revert_todo = false
@@ -18,7 +19,7 @@ function M.pick_or_revert_in_progress()
end
end
- return git.repo.state.sequencer.head or pick_or_revert_todo
+ return git.repo.state.sequencer.head ~= nil or pick_or_revert_todo
end
---@class SequencerItem
@@ -30,28 +31,30 @@ end
function M.update_sequencer_status(state)
state.sequencer = { items = {}, head = nil, head_oid = nil, revert = false, cherry_pick = false }
- local revert_head = git.repo:git_path("REVERT_HEAD")
- local cherry_head = git.repo:git_path("CHERRY_PICK_HEAD")
+ local revert_head = git.repo:worktree_git_path("REVERT_HEAD")
+ local cherry_head = git.repo:worktree_git_path("CHERRY_PICK_HEAD")
if cherry_head:exists() then
state.sequencer.head = "CHERRY_PICK_HEAD"
- state.sequencer.head_oid = vim.trim(git.repo:git_path("CHERRY_PICK_HEAD"):read())
+ state.sequencer.head_oid = vim.trim(git.repo:worktree_git_path("CHERRY_PICK_HEAD"):read())
state.sequencer.cherry_pick = true
elseif revert_head:exists() then
state.sequencer.head = "REVERT_HEAD"
- state.sequencer.head_oid = vim.trim(git.repo:git_path("REVERT_HEAD"):read())
+ state.sequencer.head_oid = vim.trim(git.repo:worktree_git_path("REVERT_HEAD"):read())
state.sequencer.revert = true
end
local HEAD_oid = git.rev_parse.oid("HEAD")
- table.insert(state.sequencer.items, {
- action = "onto",
- oid = HEAD_oid,
- abbreviated_commit = HEAD_oid:sub(1, git.log.abbreviated_size()),
- subject = git.log.message(HEAD_oid),
- })
+ if HEAD_oid then
+ table.insert(state.sequencer.items, {
+ action = "onto",
+ oid = HEAD_oid,
+ abbreviated_commit = HEAD_oid:sub(1, git.log.abbreviated_size()),
+ subject = git.log.message(HEAD_oid),
+ })
+ end
- local todo = git.repo:git_path("sequencer/todo")
+ local todo = git.repo:worktree_git_path("sequencer/todo")
if todo:exists() then
for line in todo:iter() do
if line:match("^[^#]") and line ~= "" then
@@ -68,7 +71,7 @@ function M.update_sequencer_status(state)
table.insert(state.sequencer.items, {
action = "join",
oid = state.sequencer.head_oid,
- abbreviated_commit = state.sequencer.head_oid:sub(1, git.log.abbreviated_size()),
+ abbreviated_commit = string.sub(state.sequencer.head_oid, 1, git.log.abbreviated_size()),
subject = git.log.message(state.sequencer.head_oid),
})
end
diff --git a/lua/neogit/lib/git/stash.lua b/lua/neogit/lib/git/stash.lua
index e07972729..d82793476 100644
--- a/lua/neogit/lib/git/stash.lua
+++ b/lua/neogit/lib/git/stash.lua
@@ -1,76 +1,69 @@
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 pattern string
-local function fire_stash_event(pattern)
- vim.api.nvim_exec_autocmds("User", { pattern = pattern, modeline = false })
-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
end
end
+---@param args string[]
function M.stash_all(args)
- git.cli.stash.arg_list(args).call { await = true }
- fire_stash_event("NeogitStash")
- -- this should work, but for some reason doesn't.
- --return perform_stash({ worktree = true, index = true })
+ local result = git.cli.stash.push.files(".").arg_list(args).call()
+ event.send("Stash", { success = result:success() })
end
function M.stash_index()
- git.cli.stash.staged.call { await = true }
- fire_stash_event("NeogitStash")
+ local result = git.cli.stash.staged.call()
+ event.send("Stash", { success = result:success() })
end
function M.stash_keep_index()
- local files = git.cli["ls-files"].call({ hidden = true }).stdout
- -- for some reason complains if not passed files,
- -- but this seems to be a git cli error; running:
- -- git --literal-pathspecs stash --keep-index
- -- fails with a bizarre error:
- -- error: pathspec ':/' did not match any file(s) known to git
- git.cli.stash.keep_index.files(unpack(files)).call { await = true }
- fire_stash_event("NeogitStash")
+ local result = git.cli.stash.keep_index.files(".").call()
+ event.send("Stash", { success = result:success() })
end
+---@param args string[]
+---@param files string[]
function M.push(args, files)
- git.cli.stash.push.arg_list(args).files(unpack(files)).call { await = true }
+ local result = git.cli.stash.push.arg_list(args).files(unpack(files)).call()
+ event.send("Stash", { success = result:success() })
end
function M.pop(stash)
- local result = git.cli.stash.apply.index.args(stash).call { await = true }
+ local result = git.cli.stash.apply.index.args(stash).call()
- if result.code == 0 then
- git.cli.stash.drop.args(stash).call { await = true }
+ if result:success() then
+ git.cli.stash.drop.args(stash).call()
else
- git.cli.stash.apply.args(stash).call { await = true }
+ git.cli.stash.apply.args(stash).call()
end
- fire_stash_event("NeogitStash")
+ event.send("Stash", { success = result:success() })
end
function M.apply(stash)
- local result = git.cli.stash.apply.index.args(stash).call { await = true }
+ local result = git.cli.stash.apply.index.args(stash).call()
- if result.code ~= 0 then
- git.cli.stash.apply.args(stash).call { await = true }
+ if result:failure() then
+ git.cli.stash.apply.args(stash).call()
end
- fire_stash_event("NeogitStash")
+ event.send("Stash", { success = result:success() })
end
function M.drop(stash)
- git.cli.stash.drop.args(stash).call { await = true }
- fire_stash_event("NeogitStash")
+ local result = git.cli.stash.drop.args(stash).call()
+ event.send("Stash", { success = result:success() })
end
function M.list()
@@ -78,17 +71,19 @@ 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 { await = true }
- git.cli.stash.store.message(message).args(oid).call { await = true }
+ git.cli.stash.drop.args(stash).call()
+ git.cli.stash.store.message(message).args(oid).call()
end
end
---@class StashItem
---@field idx number string the id of the stash i.e. stash@{7}
---@field name string
+---@field date string timestamp
---@field rel_date string relative timestamp
---@field message string the message associated with each stash.
@@ -98,7 +93,7 @@ function M.register(meta)
local idx, message = line:match("stash@{(%d*)}: (.*)")
idx = tonumber(idx)
- assert(idx, "indx cannot be nil")
+ assert(idx, "index cannot be nil")
---@class StashItem
local item = {
@@ -118,6 +113,18 @@ function M.register(meta)
.call({ hidden = true }).stdout[1]
return self.rel_date
+ elseif key == "date" then
+ self.date = git.cli.log
+ .max_count(1)
+ .format("%cd")
+ .args("--date=format:" .. config.values.log_date_format)
+ .args(("stash@{%s}"):format(idx))
+ .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/status.lua b/lua/neogit/lib/git/status.lua
index dfad7f7af..6383393cb 100644
--- a/lua/neogit/lib/git/status.lua
+++ b/lua/neogit/lib/git/status.lua
@@ -6,14 +6,47 @@ local logger = require("neogit.logger")
---@class StatusItem
---@field mode string
----@field diff string[]
+---@field diff Diff
---@field absolute_path string
---@field escaped_path string
---@field original_name string|nil
---@field file_mode {head: number, index: number, worktree: number}|nil
+---@field submodule SubmoduleStatus|nil
+---@field name string
+---@field first number
+---@field last number
+---@field oid string|nil optional object id
+---@field commit CommitLogEntry|nil optional object id
+---@field folded boolean|nil
+---@field hunks Hunk[]|nil
+
+---@class SubmoduleStatus
+---@field commit_changed boolean C
+---@field has_tracked_changes boolean M
+---@field has_untracked_changes boolean U
+
+---@param status string
+-- A 4 character field describing the submodule state.
+-- "N..." when the entry is not a submodule.
+-- "S" when the entry is a submodule.
+-- is "C" if the commit changed; otherwise ".".
+-- is "M" if it has tracked changes; otherwise ".".
+-- is "U" if there are untracked changes; otherwise ".".
+local function parse_submodule_status(status)
+ local a, b, c, d = status:match("(.)(.)(.)(.)")
+ if a == "N" then
+ return nil
+ else
+ return {
+ commit_changed = b == "C",
+ has_tracked_changes = c == "M",
+ has_untracked_changes = d == "U",
+ }
+ end
+end
---@return StatusItem
-local function update_file(section, cwd, file, mode, name, original_name, file_mode)
+local function update_file(section, cwd, file, mode, name, original_name, file_mode, submodule)
local absolute_path = Path:new(cwd, name):absolute()
local escaped_path = vim.fn.fnameescape(vim.fn.fnamemodify(absolute_path, ":~:."))
@@ -24,6 +57,7 @@ local function update_file(section, cwd, file, mode, name, original_name, file_m
absolute_path = absolute_path,
escaped_path = escaped_path,
file_mode = file_mode,
+ submodule = submodule,
}
if file and rawget(file, "diff") then
@@ -90,16 +124,17 @@ local function update_status(state, filter)
local mode, _, _, _, _, _, _, _, _, name = rest:match(match_u)
table.insert(
state.unstaged.items,
- update_file("unstaged", state.git_root, old_files.unstaged_files[name], mode, name)
+ update_file("unstaged", state.worktree_root, old_files.unstaged_files[name], mode, name)
)
elseif kind == "?" then
table.insert(
state.untracked.items,
- update_file("untracked", state.git_root, old_files.untracked_files[rest], "?", rest)
+ update_file("untracked", state.worktree_root, old_files.untracked_files[rest], "?", rest)
)
elseif kind == "1" then
- local mode_staged, mode_unstaged, _, mH, mI, mW, hH, _, name = rest:match(match_1)
+ local mode_staged, mode_unstaged, submodule, mH, mI, mW, hH, _, name = rest:match(match_1)
local file_mode = { head = mH, index = mI, worktree = mW }
+ local submodule = parse_submodule_status(submodule)
if mode_staged ~= "." then
if hH:match("^0+$") then
@@ -110,12 +145,13 @@ local function update_status(state, filter)
state.staged.items,
update_file(
"staged",
- state.git_root,
+ state.worktree_root,
old_files.staged_files[name],
mode_staged,
name,
nil,
- file_mode
+ file_mode,
+ submodule
)
)
end
@@ -125,30 +161,33 @@ local function update_status(state, filter)
state.unstaged.items,
update_file(
"unstaged",
- state.git_root,
+ state.worktree_root,
old_files.unstaged_files[name],
mode_unstaged,
name,
nil,
- file_mode
+ file_mode,
+ submodule
)
)
end
elseif kind == "2" then
- local mode_staged, mode_unstaged, _, mH, mI, mW, _, _, _, name, orig_name = rest:match(match_2)
+ local mode_staged, mode_unstaged, submodule, mH, mI, mW, _, _, _, name, orig_name = rest:match(match_2)
local file_mode = { head = mH, index = mI, worktree = mW }
+ local submodule = parse_submodule_status(submodule)
if mode_staged ~= "." then
table.insert(
state.staged.items,
update_file(
"staged",
- state.git_root,
+ state.worktree_root,
old_files.staged_files[name],
mode_staged,
name,
orig_name,
- file_mode
+ file_mode,
+ submodule
)
)
end
@@ -158,12 +197,13 @@ local function update_status(state, filter)
state.unstaged.items,
update_file(
"unstaged",
- state.git_root,
+ state.worktree_root,
old_files.unstaged_files[name],
mode_unstaged,
name,
orig_name,
- file_mode
+ file_mode,
+ submodule
)
)
end
@@ -172,42 +212,68 @@ local function update_status(state, filter)
end
---@class NeogitGitStatus
-local status = {
- stage = function(files)
- git.cli.add.files(unpack(files)).call { await = true }
- end,
- stage_modified = function()
- git.cli.add.update.call { await = true }
- end,
- stage_untracked = function()
- local paths = util.map(git.repo.state.untracked.items, function(item)
- return item.escaped_path
- end)
-
- git.cli.add.files(unpack(paths)).call { await = true }
- end,
- stage_all = function()
- git.cli.add.all.call { await = true }
- end,
- unstage = function(files)
- git.cli.reset.files(unpack(files)).call { await = true }
- end,
- unstage_all = function()
- git.cli.reset.call { await = true }
- end,
- is_dirty = function()
- return #git.repo.state.staged.items > 0 or #git.repo.state.unstaged.items > 0
- end,
- anything_staged = function()
- return #git.repo.state.staged.items > 0
- end,
- anything_unstaged = function()
- return #git.repo.state.unstaged.items > 0
- end,
-}
-
-status.register = function(meta)
+local M = {}
+
+---@param files string[]
+function M.stage(files)
+ git.cli.add.files(unpack(files)).call { await = true }
+end
+
+function M.stage_modified()
+ git.cli.add.update.call { await = true }
+end
+
+function M.stage_untracked()
+ local paths = util.map(git.repo.state.untracked.items, function(item)
+ return item.escaped_path
+ end)
+
+ git.cli.add.files(unpack(paths)).call { await = true }
+end
+
+function M.stage_all()
+ git.cli.add.all.call { await = true }
+end
+
+---@param files string[]
+function M.unstage(files)
+ git.cli.reset.files(unpack(files)).call { await = true }
+end
+
+function M.unstage_all()
+ git.cli.reset.call { await = true }
+end
+
+---@return boolean
+function M.is_dirty()
+ return M.anything_unstaged() or M.anything_staged()
+end
+
+---@return boolean
+function M.anything_staged()
+ local output = git.cli.status.porcelain(2).call({ hidden = true }).stdout
+ return vim.iter(output):any(function(line)
+ return line:match("^%d [^%.]")
+ end)
+end
+
+---@return boolean
+function M.anything_unstaged()
+ local output = git.cli.status.porcelain(2).call({ hidden = true }).stdout
+ return vim.iter(output):any(function(line)
+ return line:match("^%d %..")
+ end)
+end
+
+---@return boolean
+function M.any_unmerged()
+ return vim.iter(git.repo.state.unstaged.items):any(function(item)
+ return vim.tbl_contains({ "UU", "AA", "DU", "UD", "AU", "UA", "DD" }, item.mode)
+ end)
+end
+
+M.register = function(meta)
meta.update_status = update_status
end
-return status
+return M
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 9fb46002e..1095eb3b5 100644
--- a/lua/neogit/lib/git/worktree.lua
+++ b/lua/neogit/lib/git/worktree.lua
@@ -8,10 +8,14 @@ local M = {}
---Creates new worktree at path for ref
---@param ref string branch name, tag name, HEAD, etc.
---@param path string absolute path
----@return boolean
+---@return boolean, string
function M.add(ref, path, params)
- local result = git.cli.worktree.add.arg_list(params or {}).args(path, ref).call { await = true }
- return result.code == 0
+ local result = git.cli.worktree.add.arg_list(params or {}).args(path, ref).call()
+ if result:success() then
+ return true, ""
+ else
+ return false, result.stderr[#result.stderr]
+ end
end
---Moves an existing worktree
@@ -19,8 +23,8 @@ end
---@param destination string absolute path for where to move worktree
---@return boolean
function M.move(worktree, destination)
- local result = git.cli.worktree.move.args(worktree, destination).call { await = true }
- return result.code == 0
+ local result = git.cli.worktree.move.args(worktree, destination).call()
+ return result:success()
end
---Removes a worktree
@@ -28,9 +32,8 @@ end
---@param args? table
---@return boolean
function M.remove(worktree, args)
- local result =
- git.cli.worktree.remove.args(worktree).arg_list(args or {}).call { ignore_error = true, await = true }
- return result.code == 0
+ local result = git.cli.worktree.remove.args(worktree).arg_list(args or {}).call { ignore_error = true }
+ return result:success()
end
---@class Worktree
@@ -48,20 +51,22 @@ function M.list(opts)
local list = git.cli.worktree.list.args("--porcelain").call({ hidden = true }).stdout
local worktrees = {}
- for i = 1, #list, 3 do
- local path = list[i]:match("^worktree (.-)$")
- local head = list[i]:match("^HEAD (.-)$")
- local type, ref = list[i + 2]:match("^([^ ]+) (.+)$")
+ for i = 1, #list, 1 do
+ if list[i]:match("^branch.*$") then
+ local path = list[i - 2]:match("^worktree (.-)$")
+ local head = list[i - 1]:match("^HEAD (.-)$")
+ local type, ref = list[i]:match("^([^ ]+) (.+)$")
- if path then
- local main = Path.new(path, ".git"):is_dir()
- table.insert(worktrees, {
- head = head,
- type = type,
- ref = ref,
- main = main,
- path = path,
- })
+ if path then
+ local main = Path.new(path, ".git"):is_dir()
+ table.insert(worktrees, {
+ head = head,
+ type = type,
+ ref = ref,
+ main = main,
+ path = path,
+ })
+ end
end
end
diff --git a/lua/neogit/lib/graph/kitty.lua b/lua/neogit/lib/graph/kitty.lua
new file mode 100644
index 000000000..d9a36f2fa
--- /dev/null
+++ b/lua/neogit/lib/graph/kitty.lua
@@ -0,0 +1,1206 @@
+-- Modified version of graphing algorithm from https://github.com/isakbm/gitgraph.nvim
+--
+-- MIT License
+--
+-- Copyright (c) 2024 Isak Buhl-Mortensen
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a copy
+-- of this software and associated documentation files (the "Software"), to deal
+-- in the Software without restriction, including without limitation the rights
+-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+-- copies of the Software, and to permit persons to whom the Software is
+-- furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in all
+-- copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+-- SOFTWARE.
+
+local M = {}
+
+-- heuristic to check if this row contains a "bi-crossing" of branches
+--
+-- a bi-crossing is when we have more than one branch "propagating" horizontally
+-- on a connector row
+--
+-- this can only happen when the commit on the row
+-- above the connector row is a merge commit
+-- but it doesn't always happen
+--
+-- in addition to needing a merge commit on the row above
+-- we need the span (interval) of the "emphasized" connector cells
+-- (they correspond to connectors to the parents of the merge commit)
+-- we need that span to overlap with at least one connector cell that
+-- is destined for the commit on the next row
+-- (the commit before the merge commit)
+-- in addition, we need there to be more than one connector cell
+-- destined to the next commit
+--
+-- here is an example
+--
+--
+-- j i i ⓮ │ │ j -> g h
+-- g i i h ?─?─?─╮
+-- g i h │ ⓚ │ i
+--
+--
+-- overlap:
+--
+-- g-----h 1 4
+-- i-i 2 3
+--
+-- NOTE how `i` is the commit that the `i` cells are destined for
+-- notice how there is more than on `i` in the connector row
+-- and that it lies in the span of g-h
+--
+-- some more examples
+--
+-- -------------------------------------
+--
+-- S T S │ ⓮ │ T -> R S
+-- S R S ?─?─?
+-- S R ⓚ │ S
+--
+--
+-- overlap:
+--
+-- S-R 1 2
+-- S---S 1 3
+--
+-- -------------------------------------
+--
+--
+-- c b a b ⓮ │ │ │ c -> Z a
+-- Z b a b ?─?─?─?
+-- Z b a │ ⓚ │ b
+--
+-- overlap:
+--
+-- Z---a 1 3
+-- b---b 2 4
+--
+-- -------------------------------------
+--
+-- finally a negative example where there is no problem
+--
+--
+-- W V V ⓮ │ │ W -> S V
+-- S V V ⓸─⓵─╯
+-- S V │ ⓚ V
+--
+-- no overlap:
+--
+-- S-V 1 2
+-- V-V 2 3
+--
+-- the reason why there is no problem (bi-crossing) above
+-- follows from the fact that the span from V <- V only
+-- touches the span S -> V it does not overlap it, so
+-- figuratively we have S -> V <- V which is fine
+--
+-- TODO:
+-- FIXME: need to test if we handle two bi-connectors in succession
+-- correctly
+--
+---@param commit_row I.Row
+---@param connector_row I.Row
+---@param next_commit I.Commit?
+---@return boolean -- whether or not this is a bi crossing
+---@return boolean -- whether or not it can be resolved safely by edge lifting
+local function get_is_bi_crossing(commit_row, connector_row, next_commit)
+ if not next_commit then
+ return false, false
+ end
+
+ local prev = commit_row.commit
+ assert(prev, "expected a prev commit")
+
+ if #prev.parents < 2 then
+ return false, false -- bi-crossings only happen when prev is a merge commit
+ end
+
+ local row = connector_row
+
+ ---@param k integer
+ local function interval_upd(x, k)
+ if k < x.start then
+ x.start = k
+ end
+ if k > x.stop then
+ x.stop = k
+ end
+ end
+
+ -- compute the emphasized interval (merge commit parent interval)
+ local emi = { start = #row.cells, stop = 1 }
+ for k, cell in ipairs(row.cells) do
+ if cell.commit and cell.emphasis then
+ interval_upd(emi, k)
+ end
+ end
+
+ -- compute connector interval
+ local coi = { start = #row.cells, stop = 1 }
+ for k, cell in ipairs(row.cells) do
+ if cell.commit and cell.commit.hash == next_commit.hash then
+ interval_upd(coi, k)
+ end
+ end
+
+ -- unsafe if starts of intervals overlap and are equal to direct parent location
+ local safe = not (emi.start == coi.start and prev.j == emi.start)
+
+ -- return early when connector interval is trivial
+ if coi.start == coi.stop then
+ return false, safe
+ end
+
+ -- print('emi:', vim.inspect(emi))
+ -- print('coi:', vim.inspect(coi))
+
+ -- check overlap
+ do
+ -- are intervals identical, then that counts as overlap
+ if coi.start == emi.start and coi.stop == emi.stop then
+ return true, safe
+ end
+ end
+ for _, k in pairs(emi) do
+ -- emi endpoints inside coi ?
+ if coi.start < k and k < coi.stop then
+ return true, safe
+ end
+ end
+ for _, k in pairs(coi) do
+ -- coi endpoints inside emi ?
+ if emi.start < k and k < emi.stop then
+ return true, safe
+ end
+ end
+
+ return false, safe
+end
+
+---@param next I.Commit
+---@param prev_commit_row I.Row
+---@param prev_connector_row I.Row
+---@param commit_row I.Row
+---@param connector_row I.Row
+local function resolve_bi_crossing(prev_commit_row, prev_connector_row, commit_row, connector_row, next)
+ -- if false then
+ -- if false then -- get_is_bi_crossing(graph, next_commit, #graph) then
+ -- print 'we have a bi crossing'
+ -- void all repeated reservations of `next` from
+ -- this and the previous row
+ local prev_row = commit_row
+ local this_row = connector_row
+ assert(prev_row and this_row, "expecting two prior rows due to bi-connector")
+
+ --- example of what this does
+ ---
+ --- input:
+ ---
+ --- j i i │ │ │
+ --- j i i ⓮ │ │ <- prev
+ --- g i i h ⓸─⓵─ⓥ─╮ <- bi connector
+ ---
+ --- output:
+ ---
+ --- j i i │ ⓶─╯
+ --- j i ⓮ │ <- prev
+ --- g i h ⓸─│───╮ <- bi connector
+ ---
+ ---@param row I.Row
+ ---@return integer
+ local function void_repeats(row)
+ local start_voiding = false
+ local ctr = 0
+ for k, cell in ipairs(row.cells) do
+ if cell.commit and cell.commit.hash == next.hash then
+ if not start_voiding then
+ start_voiding = true
+ elseif not row.cells[k].emphasis then
+ -- else
+
+ row.cells[k] = { connector = " " } -- void it
+ ctr = ctr + 1
+ end
+ end
+ end
+ return ctr
+ end
+
+ void_repeats(prev_row)
+ void_repeats(this_row)
+
+ -- we must also take care when the prev prev has a repeat where
+ -- the repeat is not the direct parent of its child
+ --
+ -- G ⓯
+ -- e d c ⓸─ⓢ─╮
+ -- E D C F │ │ │ ⓯
+ -- e D C c b a d ⓶─⓵─│─⓴─ⓢ─ⓢ─? <--- to resolve this
+ -- E D C C B A ⓮ │ │ │ │ │
+ -- c D C C b A ⓸─│─ⓥ─ⓥ─⓷ │
+ -- C D B A │ ⓮ │ │
+ -- C c b a ⓶─ⓥ─────⓵─⓷
+ -- C B A ⓮ │ │
+ -- b B a ⓸───────ⓥ─⓷
+ -- B A ⓚ │
+ -- a A ⓶─────────╯
+ -- A ⓚ
+ local prev_prev_row = prev_connector_row -- graph[#graph - 2]
+ local prev_prev_prev_row = prev_commit_row -- graph[#graph - 3]
+ assert(prev_prev_row and prev_prev_prev_row, "assertion failed")
+ do
+ local start_voiding = false
+ local ctr = 0
+ ---@type I.Cell?
+ local replacer = nil
+ for k, cell in ipairs(prev_prev_row.cells) do
+ if cell.commit and cell.commit.hash == next.hash then
+ if not start_voiding then
+ start_voiding = true
+ replacer = cell
+ elseif k ~= prev_prev_prev_row.commit.j then
+ local ppcell = prev_prev_prev_row.cells[k]
+ if (not ppcell) or (ppcell and ppcell.connector == " ") then
+ prev_prev_row.cells[k] = { connector = " " } -- void it
+ replacer.emphasis = true
+ ctr = ctr + 1
+ end
+ end
+ end
+ end
+ end
+
+ -- assert(prev_rep_ctr == this_rep_ctr)
+
+ -- newly introduced tracking cells can be squeezed in
+ --
+ -- before:
+ --
+ -- j i i │ ⓶─╯
+ -- j i ⓮ │
+ -- g i h ⓸─│───╮
+ --
+ -- after:
+ --
+ -- j i i │ ⓶─╯
+ -- j i ⓮ │
+ -- g i h ⓸─│─╮
+ --
+ -- can think of this as scooting the cell to the left
+ -- when the cell was just introduced
+ -- TODO: implement this at some point
+ -- for k, cell in ipairs(this_row.cells) do
+ -- if cell.commit and not prev_row.cells[k].commit and not this_row.cells[k - 2] then
+ -- end
+ -- end
+end
+
+---@class I.Row
+---@field cells I.Cell[]
+---@field commit I.Commit? -- there's a single commit for every even row
+
+---@class I.Cell
+---@field is_commit boolean? -- when true this cell is a real commit
+---@field commit I.Commit? -- a cell is associated with a commit, but the empty column gaps don't have them
+---@field symbol string?
+---@field connector string? -- a cell is eventually given a connector
+---@field emphasis boolean? -- when true indicates that this is a direct parent of cell on previous row
+
+---@class I.Commit
+---@field hash string
+---@field msg string
+---@field branch_names string[]
+---@field tags string[]
+---@field debug string?
+---@field author_date string
+---@field author_name string
+---@field i integer
+---@field j integer
+---@field parents string[]
+---@field children string[]
+
+---@class I.Highlight
+---@field hg string
+---@field row integer
+---@field start integer
+---@field stop integer
+
+local sym = {
+ merge_commit = "",
+ commit = "",
+ merge_commit_end = "",
+ commit_end = "",
+ GVER = "",
+ GHOR = "",
+ GCLD = "",
+ GCRD = "╭",
+ GCLU = "",
+ GCRU = "",
+ GLRU = "",
+ GLRD = "",
+ GLUD = "",
+ GRUD = "",
+ GFORKU = "",
+ GFORKD = "",
+ GRUDCD = "",
+ GRUDCU = "",
+ GLUDCD = "",
+ GLUDCU = "",
+ GLRDCL = "",
+ GLRDCR = "",
+ GLRUCL = "",
+ GLRUCR = "",
+}
+
+local BRANCH_COLORS = {
+ "Red",
+ "Yellow",
+ "Blue",
+ "Purple",
+ "Cyan",
+}
+
+local NUM_BRANCH_COLORS = #BRANCH_COLORS
+
+local util = require("neogit.lib.util")
+
+---@param commits CommitLogEntry[]
+---@param color boolean?
+function M.build(commits, color)
+ local GVER = sym.GVER
+ local GHOR = sym.GHOR
+ local GCLD = sym.GCLD
+ local GCRD = sym.GCRD
+ local GCLU = sym.GCLU
+ local GCRU = sym.GCRU
+ local GLRU = sym.GLRU
+ local GLRD = sym.GLRD
+ local GLUD = sym.GLUD
+ local GRUD = sym.GRUD
+
+ local GFORKU = sym.GFORKU
+ local GFORKD = sym.GFORKD
+
+ local GRUDCD = sym.GRUDCD
+ local GRUDCU = sym.GRUDCU
+ local GLUDCD = sym.GLUDCD
+ local GLUDCU = sym.GLUDCU
+
+ local GLRDCL = sym.GLRDCL
+ local GLRDCR = sym.GLRDCR
+ local GLRUCL = sym.GLRUCL
+ -- local GLRUCR = sym.GLRUCR
+
+ local GRCM = sym.commit
+ local GMCM = sym.merge_commit
+ local GRCME = sym.commit_end
+ local GMCME = sym.merge_commit_end
+
+ local raw_commits = util.filter_map(commits, function(item)
+ if item.oid then
+ return {
+ msg = item.subject,
+ branch_names = {},
+ tags = {},
+ author_date = item.author_date,
+ hash = item.oid,
+ parents = vim.split(item.parent, " "),
+ }
+ end
+ end)
+
+ local commits = {} ---@type table
+ local sorted_commits = {} ---@type string[]
+
+ for _, rc in ipairs(raw_commits) do
+ local commit = {
+ msg = rc.msg,
+ branch_names = rc.branch_names,
+ tags = rc.tags,
+ author_date = rc.author_date,
+ author_name = rc.author_name,
+ hash = rc.hash,
+ i = -1,
+ j = -1,
+ parents = rc.parents,
+ children = {},
+ }
+
+ sorted_commits[#sorted_commits + 1] = commit.hash
+ commits[rc.hash] = commit
+ end
+
+ do
+ for _, c_hash in ipairs(sorted_commits) do
+ local c = commits[c_hash]
+
+ for _, h in ipairs(c.parents) do
+ local p = commits[h]
+ if p then
+ p.children[#p.children + 1] = c.hash
+ else
+ -- create a virtual parent, it is not added to the list of commit hashes
+ commits[h] = {
+ hash = h,
+ author_name = "virtual",
+ msg = "virtual parent",
+ author_date = "unknown",
+ parents = {},
+ children = { c.hash },
+ branch_names = {},
+ tags = {},
+ i = -1,
+ j = -1,
+ }
+ end
+ end
+ end
+ end
+
+ ---@param cells I.Cell[]
+ ---@return I.Cell[]
+ local function propagate(cells)
+ local new_cells = {}
+ for _, cell in ipairs(cells) do
+ if cell.connector then
+ -- new_cells[#new_cells + 1] = { connector = " " }
+ new_cells[#new_cells + 1] = { connector = cell.connector }
+ elseif cell.commit then
+ assert(cell.commit, "assertion failed")
+ new_cells[#new_cells + 1] = { commit = cell.commit }
+ else
+ new_cells[#new_cells + 1] = { connector = " " }
+ end
+ end
+ return new_cells
+ end
+
+ ---@param cells I.Cell[]
+ ---@param hash string
+ ---@param start integer?
+ ---@return integer?
+ local function find(cells, hash, start)
+ local start = start or 1
+ for idx = start, #cells, 2 do
+ local c = cells[idx]
+ if c.commit and c.commit.hash == hash then
+ return idx
+ end
+ end
+ return nil
+ end
+
+ ---@param cells I.Cell[]
+ ---@param start integer?
+ ---@return integer
+ local function next_vacant_j(cells, start)
+ local start = start or 1
+ for i = start, #cells, 2 do
+ local cell = cells[i]
+ if cell.connector == " " then
+ return i
+ end
+ end
+ return #cells + 1
+ end
+
+ --- returns the generated row and the integer (j) location of the commit
+ ---@param c I.Commit
+ ---@param prev_row I.Row?
+ ---@return I.Row, integer
+ local function generate_commit_row(c, prev_row)
+ local j = nil ---@type integer?
+
+ local rowc = {} ---@type I.Cell[]
+
+ if prev_row then
+ rowc = propagate(prev_row.cells)
+ j = find(prev_row.cells, c.hash)
+ end
+
+ -- if reserved location use it
+ if j then
+ c.j = j
+ rowc[j] = { commit = c, is_commit = true }
+
+ -- clear any supurfluous reservations
+ for k = j + 1, #rowc do
+ local v = rowc[k]
+ if v.commit and v.commit.hash == c.hash then
+ rowc[k] = { connector = " " }
+ end
+ end
+ else
+ j = next_vacant_j(rowc)
+ c.j = j
+ rowc[j] = { commit = c, is_commit = true }
+ rowc[j + 1] = { connector = " " }
+ end
+
+ return { cells = rowc, commit = c }, j
+ end
+
+ ---@param prev_commit_row I.Row
+ ---@param prev_connector_row I.Row
+ ---@param commit_row I.Row
+ ---@param commit_loc integer
+ ---@param curr_commit I.Commit
+ ---@param next_commit I.Commit?
+ ---@return I.Row
+ local function generate_connector_row(
+ prev_commit_row,
+ prev_connector_row,
+ commit_row,
+ commit_loc,
+ curr_commit,
+ next_commit
+ )
+ -- connector row (reservation row)
+ --
+ -- first we propagate
+ local connector_cells = propagate(commit_row.cells)
+
+ -- connector row
+ --
+ -- now we proceed to add the parents of the commit we just added
+ if #curr_commit.parents > 0 then
+ ---@param rem_parents string[]
+ local function reserve_remainder(rem_parents)
+ --
+ -- reserve the rest of the parents in slots to the right of us
+ --
+ -- ... another alternative is to reserve rest of the parents of c if they have not already been reserved
+ -- for i = 2, #c.parents do
+ for _, h in ipairs(rem_parents) do
+ local j = find(commit_row.cells, h, commit_loc)
+ if not j then
+ local j = next_vacant_j(connector_cells, commit_loc)
+ connector_cells[j] = { commit = commits[h], emphasis = true }
+ connector_cells[j + 1] = { connector = " " }
+ else
+ connector_cells[j].emphasis = true
+ end
+ end
+ end
+
+ -- we start by peeking at next commit and seeing if it is one of our parents
+ -- we only do this if one of our propagating branches is already destined for this commit
+ ---@type I.Cell?
+ local tracker = nil
+ if next_commit then
+ for _, cell in ipairs(connector_cells) do
+ if cell.commit and cell.commit.hash == next_commit.hash then
+ tracker = cell
+ break
+ end
+ end
+ end
+
+ local next_p_idx = nil -- default to picking first parent
+ if tracker and next_commit then
+ -- this loop updates next_p_idx to the next commit if they are identical
+ for k, h in ipairs(curr_commit.parents) do
+ if h == next_commit.hash then
+ next_p_idx = k
+ break
+ end
+ end
+ end
+
+ -- next_p_idx = nil
+
+ -- add parents
+ if next_p_idx then
+ assert(tracker, "assertion failed")
+ -- if next commit is our parent then we do some complex logic
+ if #curr_commit.parents == 1 then
+ -- simply place parent at our location
+ connector_cells[commit_loc].commit = commits[curr_commit.parents[1]]
+ connector_cells[commit_loc].emphasis = true
+ else
+ -- void the cell at our location (will be replaced by our parents in a moment)
+ connector_cells[commit_loc] = { connector = " " }
+
+ -- put emphasis on tracker for the special parent
+ tracker.emphasis = true
+
+ -- only reserve parents that are different from next commit
+ ---@type string[]
+ local rem_parents = {}
+ for k, h in ipairs(curr_commit.parents) do
+ if k ~= next_p_idx then
+ rem_parents[#rem_parents + 1] = h
+ end
+ end
+
+ assert(#rem_parents == #curr_commit.parents - 1, "unexpected amount of rem parents")
+ reserve_remainder(rem_parents)
+
+ -- we fill this with the next commit if it is empty, a bit hacky
+ if connector_cells[commit_loc].connector == " " then
+ connector_cells[commit_loc].commit = tracker.commit
+ connector_cells[commit_loc].emphasis = true
+ connector_cells[commit_loc].connector = nil
+ tracker.emphasis = false
+ end
+ end
+ else
+ -- simply add first parent at our location and then reserve the rest
+ connector_cells[commit_loc].commit = commits[curr_commit.parents[1]]
+ connector_cells[commit_loc].emphasis = true
+
+ local rem_parents = {}
+ for k = 2, #curr_commit.parents do
+ rem_parents[#rem_parents + 1] = curr_commit.parents[k]
+ end
+
+ reserve_remainder(rem_parents)
+ end
+
+ local connector_row = { cells = connector_cells } ---@type I.Row
+
+ -- handle bi-connector rows
+ local is_bi_crossing, bi_crossing_safely_resolvable =
+ get_is_bi_crossing(commit_row, connector_row, next_commit)
+
+ if is_bi_crossing and bi_crossing_safely_resolvable and next_commit then
+ resolve_bi_crossing(prev_commit_row, prev_connector_row, commit_row, connector_row, next_commit)
+ end
+
+ return connector_row
+ else
+ -- if we're here then it means that this commit has no common ancestors with other commits
+ -- ... a different family ... see test `different family`
+
+ -- we must remove the already propagated connector for the current commit since it has no parents
+ for i = 1, #connector_cells, 2 do
+ local cell = connector_cells[i]
+ if cell.commit and cell.commit.hash == curr_commit.hash then
+ connector_cells[i] = { connector = " " }
+ end
+ end
+
+ local connector_row = { cells = connector_cells }
+
+ return connector_row
+ end
+ end
+
+ ---@param commits table
+ ---@param sorted_commits string[]
+ ---@return I.Row[]
+ local function straight_j(commits, sorted_commits)
+ local graph = {} ---@type I.Row[]
+
+ for i, c_hash in ipairs(sorted_commits) do
+ -- get the input parameters
+ local curr_commit = commits[c_hash]
+ local next_commit = commits[sorted_commits[i + 1]]
+ local prev_commit_row = graph[#graph - 1]
+ local prev_connector_row = graph[#graph]
+
+ -- generate commit and connector row for the current commit
+ local commit_row, commit_loc = generate_commit_row(curr_commit, prev_connector_row)
+ local connector_row = nil ---@type I.Row
+ if i < #sorted_commits then
+ connector_row = generate_connector_row(
+ prev_commit_row,
+ prev_connector_row,
+ commit_row,
+ commit_loc,
+ curr_commit,
+ next_commit
+ )
+ end
+
+ -- write the result
+ graph[#graph + 1] = commit_row
+ if connector_row then
+ graph[#graph + 1] = connector_row
+ end
+ end
+
+ return graph
+ end
+
+ local graph = straight_j(commits, sorted_commits)
+
+ ---@param graph I.Row[]
+ ---@return string[]
+ ---@return I.Highlight[]
+ local function graph_to_lines(graph)
+ ---@type table[]
+ local lines = {}
+
+ ---@type I.Highlight[]
+ local highlights = {}
+
+ ---@param cell I.Cell
+ ---@return string
+ local function commit_cell_symb(cell)
+ assert(cell.is_commit, "assertion failed")
+
+ if #cell.commit.parents > 1 then
+ -- merge commit
+ return #cell.commit.children == 0 and GMCME or GMCM
+ else
+ -- regular commit
+ return #cell.commit.children == 0 and GRCME or GRCM
+ end
+ end
+
+ ---@param row I.Row
+ ---@return table
+ local function row_to_str(row)
+ local row_strs = {}
+ for j = 1, #row.cells do
+ local cell = row.cells[j]
+ if cell.connector then
+ cell.symbol = cell.connector -- TODO: connector and symbol should not be duplicating data?
+ else
+ assert(cell.commit, "assertion failed")
+ cell.symbol = commit_cell_symb(cell)
+ end
+ row_strs[#row_strs + 1] = cell.symbol
+ end
+ -- return table.concat(row_strs)
+ return row_strs
+ end
+
+ ---@param row I.Row
+ ---@param row_idx integer
+ ---@return I.Highlight[]
+ local function row_to_highlights(row, row_idx)
+ local row_hls = {}
+ local offset = 1 -- WAS 0
+
+ for j = 1, #row.cells do
+ local cell = row.cells[j]
+
+ local width = cell.symbol and vim.fn.strdisplaywidth(cell.symbol) or 1
+ local start = offset
+ local stop = start + width
+
+ offset = offset + width
+
+ if cell.commit then
+ local hg = (cell.emphasis and "Bold" or "") .. BRANCH_COLORS[(j % NUM_BRANCH_COLORS + 1)]
+ row_hls[#row_hls + 1] = {
+ hg = hg,
+ row = row_idx,
+ start = start,
+ stop = stop,
+ }
+ elseif cell.symbol == GHOR then
+ -- take color from first right cell that attaches to this connector
+ for k = j + 1, #row.cells do
+ local rcell = row.cells[k]
+
+ -- TODO: would be nice with a better way than this hacky method of
+ -- to figure out where our vertical branch is
+ local continuations = {
+ GCLD,
+ GCLU,
+ --
+ GFORKD,
+ GFORKU,
+ --
+ GLUDCD,
+ GLUDCU,
+ --
+ GLRDCL,
+ GLRUCL,
+ }
+
+ if rcell.commit and vim.tbl_contains(continuations, rcell.symbol) then
+ local hg = (cell.emphasis and "Bold" or "")
+ .. BRANCH_COLORS[(rcell.commit.j % NUM_BRANCH_COLORS + 1)]
+ row_hls[#row_hls + 1] = {
+ hg = hg,
+ row = row_idx,
+ start = start,
+ stop = stop,
+ }
+
+ break
+ end
+ end
+ end
+ end
+
+ return row_hls
+ end
+
+ local width = 0
+ for _, row in ipairs(graph) do
+ if #row.cells > width then
+ width = #row.cells
+ end
+ end
+
+ for idx = 1, #graph do
+ local proper_row = graph[idx]
+
+ local row_str_arr = {}
+
+ ---@param stuff table|string
+ local function add_to_row(stuff)
+ row_str_arr[#row_str_arr + 1] = stuff
+ end
+
+ local c = proper_row.commit
+ if c then
+ add_to_row(c.hash) -- Commit row
+ add_to_row(row_to_str(proper_row))
+ else
+ local c = graph[idx - 1].commit
+ assert(c, "assertion failed")
+
+ local row = row_to_str(proper_row)
+ local valid = false
+ for _, char in ipairs(row) do
+ if char ~= " " and char ~= GVER then
+ valid = true
+ break
+ end
+ end
+
+ if valid then
+ add_to_row("") -- Connection Row
+ else
+ add_to_row("strip") -- Useless Connection Row
+ end
+
+ add_to_row(row)
+ end
+
+ for _, hl in ipairs(row_to_highlights(proper_row, idx)) do
+ highlights[#highlights + 1] = hl
+ end
+
+ lines[#lines + 1] = row_str_arr
+ end
+
+ return lines, highlights
+ end
+
+ -- store stage 1 graph
+ --
+ ---@param c I.Cell?
+ ---@return string?
+ local function hash(c)
+ return c and c.commit and c.commit.hash
+ end
+
+ -- inserts vertical and horizontal pipes
+ for i = 2, #graph - 1 do
+ local row = graph[i]
+
+ ---@param cells I.Cell[]
+ local function count_emph(cells)
+ local n = 0
+ for _, c in ipairs(cells) do
+ if c.commit and c.emphasis then
+ n = n + 1
+ end
+ end
+ return n
+ end
+
+ local num_emphasized = count_emph(graph[i].cells)
+
+ -- vertical connections
+ for j = 1, #row.cells, 2 do
+ local this = graph[i].cells[j]
+ local below = graph[i + 1].cells[j]
+
+ local tch, bch = hash(this), hash(below)
+
+ if not this.is_commit and not this.connector then
+ -- local ch = row.commit and row.commit.hash
+ -- local row_commit_is_child = ch and vim.tbl_contains(this.commit.children, ch)
+ -- local trivial_continuation = (not row_commit_is_child) and (new_columns < 1 or ach == tch or acc == GVER)
+ -- local trivial_continuation = (new_columns < 1 or ach == tch or acc == GVER)
+ local ignore_this = (num_emphasized > 1 and (this.emphasis or false))
+
+ if not ignore_this and bch == tch then -- and trivial_continuation then
+ local has_repeats = false
+ local first_repeat = nil
+ for k = 1, #row.cells, 2 do
+ local cell_k, cell_j = row.cells[k], row.cells[j]
+ local rkc, rjc =
+ (not cell_k.connector and cell_k.commit), (not cell_j.connector and cell_j.commit)
+
+ -- local rkc, rjc = row.cells[k].commit, row.cells[j].commit
+
+ if k ~= j and (rkc and rjc) and rkc.hash == rjc.hash then
+ has_repeats = true
+ first_repeat = k
+ break
+ end
+ end
+
+ if not has_repeats then
+ local cell = graph[i].cells[j]
+ cell.connector = GVER
+ else
+ local k = first_repeat
+ local this_k = graph[i].cells[k]
+ local below_k = graph[i + 1].cells[k]
+
+ local bkc, tkc =
+ (not below_k.connector and below_k.commit), (not this_k.connector and this_k.commit)
+
+ -- local bkc, tkc = below_k.commit, this_k.commit
+ if (bkc and tkc) and bkc.hash == tkc.hash then
+ local cell = graph[i].cells[j]
+ cell.connector = GVER
+ end
+ end
+ end
+ end
+ end
+
+ do
+ -- we expect number of rows to be odd always !! since the last
+ -- row is a commit row without a connector row following it
+ assert(#graph % 2 == 1, "assertion failed")
+ local last_row = graph[#graph]
+ for j = 1, #last_row.cells, 2 do
+ local cell = last_row.cells[j]
+ if cell.commit and not cell.is_commit then
+ cell.connector = GVER
+ end
+ end
+ end
+
+ -- horizontal connections
+ --
+ -- a stopped connector is one that has a void cell below it
+ --
+ local stopped = {}
+ for j = 1, #row.cells, 2 do
+ local this = graph[i].cells[j]
+ local below = graph[i + 1].cells[j]
+ if not this.connector and (not below or below.connector == " ") then
+ assert(this.commit, "assertion failed")
+ stopped[#stopped + 1] = j
+ end
+ end
+
+ -- now lets get the intervals between the stopped connectors
+ -- and other connectors of the same commit hash
+ local intervals = {}
+ for _, j in ipairs(stopped) do
+ local curr = 1
+ for k = curr, j do
+ local cell_k, cell_j = row.cells[k], row.cells[j]
+ local rkc, rjc = (not cell_k.connector and cell_k.commit), (not cell_j.connector and cell_j.commit)
+ if (rkc and rjc) and (rkc.hash == rjc.hash) then
+ if k < j then
+ intervals[#intervals + 1] = { start = k, stop = j }
+ end
+ curr = j
+ break
+ end
+ end
+ end
+
+ -- add intervals for the connectors of merge children
+ -- these are where we have multiple connector commit hashes
+ -- for a single merge child, that is, more than one connector
+ --
+ -- TODO: this method presented here is probably universal and covers
+ -- also for the previously computed intervals ... two birds one stone?
+ do
+ local low = #row.cells
+ local high = 1
+ for j = 1, #row.cells, 2 do
+ local c = row.cells[j]
+ if c.emphasis then
+ if j > high then
+ high = j
+ end
+ if j < low then
+ low = j
+ end
+ end
+ end
+
+ if high > low then
+ intervals[#intervals + 1] = { start = low, stop = high }
+ end
+ end
+
+ if i % 2 == 0 then
+ for _, interval in ipairs(intervals) do
+ local a, b = interval.start, interval.stop
+ for j = a + 1, b - 1 do
+ local this = graph[i].cells[j]
+ if this.connector == " " then
+ this.connector = GHOR
+ end
+ end
+ end
+ end
+ end
+
+ -- print '---- stage 2 -------'
+
+ -- insert symbols on connector rows
+ --
+ -- note that there are 8 possible connections
+ -- under the assumption that any connector cell
+ -- has at least 2 neighbors but no more than 3
+ --
+ -- there are 4 ways to make the connections of three neighbors
+ -- there are 6 ways to make the connections of two neighbors
+ -- however two of them are the vertical and horizontal connections
+ -- that have already been taken care of
+ --
+
+ local symb_map = {
+ -- two neighbors (no straights)
+ -- - 8421
+ [10] = GCLU, -- '1010'
+ [9] = GCLD, -- '1001'
+ [6] = GCRU, -- '0110'
+ [5] = GCRD, -- '0101'
+ -- three neighbors
+ [14] = GLRU, -- '1110'
+ [13] = GLRD, -- '1101'
+ [11] = GLUD, -- '1011'
+ [7] = GRUD, -- '0111'
+ }
+
+ for i = 2, #graph, 2 do
+ local row = graph[i]
+ local above = graph[i - 1]
+ local below = graph[i + 1]
+
+ for j = 1, #row.cells, 2 do
+ local this = row.cells[j]
+
+ if this.connector ~= GVER then
+ local lc = row.cells[j - 1]
+ local rc = row.cells[j + 1]
+ local uc = above and above.cells[j]
+ local dc = below and below.cells[j]
+
+ local l = lc and (lc.connector ~= " " or lc.commit) or false
+ local r = rc and (rc.connector ~= " " or rc.commit) or false
+ local u = uc and (uc.connector ~= " " or uc.commit) or false
+ local d = dc and (dc.connector ~= " " or dc.commit) or false
+
+ -- number of neighbors
+ local nn = 0
+
+ local symb_n = 0
+ for i, b in ipairs { l, r, u, d } do
+ if b then
+ nn = nn + 1
+ symb_n = symb_n + bit.lshift(1, 4 - i)
+ end
+ end
+
+ local symbol = symb_map[symb_n] or "?"
+
+ if (i == #graph or i == #graph - 1) and symbol == "?" then
+ symbol = GVER
+ end
+
+ local commit_dir_above = above.commit and above.commit.j == j
+
+ ---@type 'l' | 'r' | nil -- placement of commit horizontally, only relevant if this is a connector row and if the cell is not immediately above or below the commit
+ local clh_above = nil
+ local commit_above = above.commit and above.commit.j ~= j
+ if commit_above then
+ clh_above = above.commit.j < j and "l" or "r"
+ end
+
+ if clh_above and symbol == GLRD then
+ if clh_above == "l" then
+ symbol = GLRDCL -- '<'
+ elseif clh_above == "r" then
+ symbol = GLRDCR -- '>'
+ end
+ elseif symbol == GLRU then
+ -- because nothing else is possible with our
+ -- current implicit graph building rules?
+ symbol = GLRUCL -- '<'
+ end
+
+ local merge_dir_above = commit_dir_above and #above.commit.parents > 1
+
+ if symbol == GLUD then
+ symbol = merge_dir_above and GLUDCU or GLUDCD
+ end
+
+ if symbol == GRUD then
+ symbol = merge_dir_above and GRUDCU or GRUDCD
+ end
+
+ if nn == 4 then
+ symbol = merge_dir_above and GFORKD or GFORKU
+ end
+
+ if row.cells[j].commit then
+ row.cells[j].connector = symbol
+ end
+ end
+ end
+ end
+
+ local lines, highlights = graph_to_lines(graph)
+
+ --
+ -- BEGIN NEOGIT COMPATIBILITY CODE
+ -- Transform graph into what neogit needs to render
+ --
+ local result = {}
+ local hl = {}
+ for _, highlight in ipairs(highlights) do
+ local row = highlight.row
+ if not hl[row] then
+ hl[row] = {}
+ end
+
+ for i = highlight.start, highlight.stop do
+ hl[row][i] = highlight
+ end
+ end
+
+ for row, line in ipairs(lines) do
+ local graph_row = {}
+ local oid = line[1]
+ local parts = line[2]
+
+ for i, part in ipairs(parts) do
+ local current_highlight = hl[row][i] or {}
+
+ table.insert(graph_row, {
+ oid = oid ~= "" and oid,
+ text = part,
+ color = not color and "Purple" or current_highlight.hg,
+ })
+ end
+
+ if oid ~= "strip" then
+ table.insert(result, graph_row)
+ end
+ end
+
+ return result
+end
+
+return M
diff --git a/lua/neogit/lib/graph.lua b/lua/neogit/lib/graph/unicode.lua
similarity index 99%
rename from lua/neogit/lib/graph.lua
rename to lua/neogit/lib/graph/unicode.lua
index b2309b72a..65fdda2eb 100644
--- a/lua/neogit/lib/graph.lua
+++ b/lua/neogit/lib/graph/unicode.lua
@@ -477,6 +477,7 @@ function M.build(commits)
if is_missing_parent and branch_index ~= moved_parent_branch_index then
-- Remove branch
branch_hashes[branch_index] = nil
+ assert(branch_hash, "no branch hash")
branch_indexes[branch_hash] = nil
-- Trim trailing empty branches
diff --git a/lua/neogit/lib/hl.lua b/lua/neogit/lib/hl.lua
index bd9620563..658e32773 100644
--- a/lua/neogit/lib/hl.lua
+++ b/lua/neogit/lib/hl.lua
@@ -89,13 +89,13 @@ end
local function make_palette(config)
local bg = Color.from_hex(get_bg("Normal") or (vim.o.bg == "dark" and "#22252A" or "#eeeeee"))
local fg = Color.from_hex((vim.o.bg == "dark" and "#fcfcfc" or "#22252A"))
- local red = Color.from_hex(get_fg("Error") or "#E06C75")
- local orange = Color.from_hex(get_fg("SpecialChar") or "#ffcb6b")
- local yellow = Color.from_hex(get_fg("PreProc") or "#FFE082")
- local green = Color.from_hex(get_fg("String") or "#C3E88D")
- local cyan = Color.from_hex(get_fg("Operator") or "#89ddff")
- local blue = Color.from_hex(get_fg("Macro") or "#82AAFF")
- local purple = Color.from_hex(get_fg("Include") or "#C792EA")
+ local red = Color.from_hex(config.highlight.red or get_fg("Error") or "#E06C75")
+ local orange = Color.from_hex(config.highlight.orange or get_fg("SpecialChar") or "#ffcb6b")
+ local yellow = Color.from_hex(config.highlight.yellow or get_fg("PreProc") or "#FFE082")
+ local green = Color.from_hex(config.highlight.green or get_fg("String") or "#C3E88D")
+ local cyan = Color.from_hex(config.highlight.cyan or get_fg("Operator") or "#89ddff")
+ local blue = Color.from_hex(config.highlight.blue or get_fg("Macro") or "#82AAFF")
+ local purple = Color.from_hex(config.highlight.purple or get_fg("Include") or "#C792EA")
local bg_factor = vim.o.bg == "dark" and 1 or -1
@@ -178,7 +178,9 @@ function M.setup(config)
NeogitSignatureGoodExpired = { link = "NeogitGraphOrange" },
NeogitSignatureGoodExpiredKey = { link = "NeogitGraphYellow" },
NeogitSignatureGoodRevokedKey = { link = "NeogitGraphRed" },
+ NeogitNormal = { link = "Normal" },
NeogitCursorLine = { link = "CursorLine" },
+ NeogitCursorLineNr = { link = "CursorLineNr" },
NeogitHunkMergeHeader = { fg = palette.bg2, bg = palette.grey, bold = palette.bold },
NeogitHunkMergeHeaderHighlight = { fg = palette.bg0, bg = palette.bg_cyan, bold = palette.bold },
NeogitHunkMergeHeaderCursor = { fg = palette.bg0, bg = palette.bg_cyan, bold = palette.bold },
@@ -230,6 +232,7 @@ function M.setup(config)
NeogitStash = { link = "NeogitSubtleText" },
NeogitRebaseDone = { link = "NeogitSubtleText" },
NeogitFold = { fg = "None", bg = "None" },
+ NeogitWinSeparator = { link = "WinSeparator" },
NeogitChangeMuntracked = { link = "NeogitChangeModified" },
NeogitChangeAuntracked = { link = "NeogitChangeAdded" },
NeogitChangeNuntracked = { link = "NeogitChangeNewFile" },
@@ -252,6 +255,7 @@ function M.setup(config)
NeogitChangeCunstaged = { link = "NeogitChangeCopied" },
NeogitChangeUunstaged = { link = "NeogitChangeUpdated" },
NeogitChangeRunstaged = { link = "NeogitChangeRenamed" },
+ NeogitChangeTunstaged = { link = "NeogitChangeUpdated" },
NeogitChangeDDunstaged = { link = "NeogitChangeUnmerged" },
NeogitChangeUUunstaged = { link = "NeogitChangeUnmerged" },
NeogitChangeAAunstaged = { link = "NeogitChangeUnmerged" },
@@ -267,6 +271,7 @@ function M.setup(config)
NeogitChangeCstaged = { link = "NeogitChangeCopied" },
NeogitChangeUstaged = { link = "NeogitChangeUpdated" },
NeogitChangeRstaged = { link = "NeogitChangeRenamed" },
+ NeogitChangeTstaged = { link = "NeogitChangeUpdated" },
NeogitChangeDDstaged = { link = "NeogitChangeUnmerged" },
NeogitChangeUUstaged = { link = "NeogitChangeUnmerged" },
NeogitChangeAAstaged = { link = "NeogitChangeUnmerged" },
@@ -302,6 +307,7 @@ function M.setup(config)
NeogitTagDistance = { fg = palette.cyan },
NeogitFloatHeader = { bg = palette.bg0, bold = palette.bold },
NeogitFloatHeaderHighlight = { bg = palette.bg2, fg = palette.cyan, bold = palette.bold },
+ NeogitActiveItem = { bg = palette.bg_orange, fg = palette.bg0, bold = palette.bold },
}
for group, hl in pairs(hl_store) do
diff --git a/lua/neogit/lib/input.lua b/lua/neogit/lib/input.lua
index b0308478c..26a336180 100644
--- a/lua/neogit/lib/input.lua
+++ b/lua/neogit/lib/input.lua
@@ -49,6 +49,7 @@ end
---@field completion string?
---@field separator string?
---@field cancel string?
+---@field prepend string?
---@param prompt string Prompt to use for user input
---@param opts GetUserInputOpts? Options table
diff --git a/lua/neogit/lib/popup/builder.lua b/lua/neogit/lib/popup/builder.lua
index 5036697e2..bfc1c7fa5 100644
--- a/lua/neogit/lib/popup/builder.lua
+++ b/lua/neogit/lib/popup/builder.lua
@@ -2,11 +2,12 @@ local git = require("neogit.lib.git")
local state = require("neogit.lib.state")
local util = require("neogit.lib.util")
local notification = require("neogit.lib.notification")
+local config = require("neogit.config")
-local M = {}
-
----@class PopupData
+---@class PopupBuilder
---@field state PopupState
+---@field builder_fn PopupData
+local M = {}
---@class PopupState
---@field name string
@@ -25,69 +26,98 @@ local M = {}
---@field cli string
---@field cli_prefix string
---@field default string|integer|boolean
+---@field dependent table
---@field description string
---@field fn function
---@field id string
+---@field incompatible table
---@field key string
---@field key_prefix string
---@field separator string
---@field type string
----@field value string
+---@field value string?
---@class PopupSwitch
---@field cli string
---@field cli_base string
---@field cli_prefix string
---@field cli_suffix string
----@field dependant table
+---@field dependent table
---@field description string
---@field enabled boolean
---@field fn function
---@field id string
----@field incompatible table
+---@field incompatible string[]
---@field internal boolean
---@field key string
---@field key_prefix string
---@field options table
---@field type string
---@field user_input boolean
+---@field value string?
+---@field persisted? boolean
---@class PopupConfig
---@field id string
---@field key string
---@field name string
----@field entry string
----@field value string
+---@field entry ConfigEntry
+---@field value string?
---@field type string
+---@field passive boolean?
+---@field heading string?
+---@field options PopupConfigOption[]?
+---@field callback fun(popup: PopupData, config: self)? Called after the config is set
+---@field fn fun(popup: PopupData, config: self)? If set, overrides the actual config setting behavior
+
+---@class PopupConfigOption An option that can be selected as a value for a config
+---@field display string The display name for the option
+---@field value string The value to set in git config
+---@field condition? fun(): boolean An option predicate to determine if the option should appear
---@class PopupAction
----@field keys table
+---@field keys string[]
---@field description string
---@field callback function
+---@field heading string?
+---@field persist_popup boolean? set to true to prevent closing the popup when invoking
----@class PopupSwitchOpts
----@field enabled boolean Controls if the switch should default to 'on' state
----@field internal boolean Whether the switch is internal to neogit or should be included in the cli command. If `true` we don't include it in the cli command.
----@field incompatible table A table of strings that represent other cli flags that this one cannot be used with
----@field key_prefix string Allows overwriting the default '-' to toggle switch
----@field cli_prefix string Allows overwriting the default '--' thats used to create the cli flag. Sometimes you may want to use '++' or '-'.
----@field cli_suffix string
----@field options table
----@field value string Allows for pre-building cli flags that can be customised by user input
----@field user_input boolean If true, allows user to customise the value of the cli flag
----@field dependant string[] other switches with a state dependency on this one
+---@class PopupActionOptions
+---@field persist_popup boolean Controls if the action should close the popup (false/nil) or keep it open (true)
----@class PopupOptionsOpts
----@field key_prefix string Allows overwriting the default '=' to set option
----@field cli_prefix string Allows overwriting the default '--' cli prefix
----@field choices table Table of predefined choices that a user can select for option
----@field default string|integer|boolean Default value for option, if the user attempts to unset value
+---@class PopupSwitchOpts
+---@field enabled? boolean Controls if the switch should default to 'on' state
+---@field internal? boolean Whether the switch is internal to neogit or should be included in the cli command. If `true` we don't include it in the cli command.
+---@field incompatible? string[] A table of strings that represent other cli switches/options that this one cannot be used with
+---@field key_prefix? string Allows overwriting the default '-' to toggle switch
+---@field cli_prefix? string Allows overwriting the default '--' that's used to create the cli flag. Sometimes you may want to use '++' or '-'.
+---@field cli_suffix? string
+---@field options? table
+---@field value? string Allows for pre-building cli flags that can be customized by user input
+---@field user_input? boolean If true, allows user to customize the value of the cli flag
+---@field dependent? string[] other switches/options with a state dependency on this one
+---@field persisted? boolean Allows overwriting the default 'true' to decide if this switch should be persisted
+
+---@class PopupOptionOpts
+---@field key_prefix? string Allows overwriting the default '=' to set option
+---@field cli_prefix? string Allows overwriting the default '--' cli prefix
+---@field choices? table Table of predefined choices that a user can select for option
+---@field default? string|integer|boolean Default value for option, if the user attempts to unset value
+---@field dependent? string[] other switches/options with a state dependency on this one
+---@field incompatible? string[] A table of strings that represent other cli switches/options that this one cannot be used with
+---@field separator? string Defaults to `=`, separating the key from the value. Some CLI options are weird.
+---@field setup? fun(PopupBuilder) function called before rendering
+---@field fn? fun() function called - like an action. Used to launch a popup from a popup.
---@class PopupConfigOpts
----@field options { display: string, value: string, config: function? }
----@field passive boolean Controls if this config setting can be manipulated directly, or if it is managed by git, and should just be shown in UI
--- A 'condition' key with function value can also be present in the option, which controls if the option gets shown by returning boolean.
-
+---@field options? PopupConfigOption[]
+---@field fn? fun(popup: PopupData, config: self) If set, overrides the actual config setting behavior
+---@field callback? fun(popup: PopupData, config: PopupConfig)? A callback that will be invoked after the config is set
+---@field passive? boolean? Controls if this config setting can be manipulated directly, or if it is managed by git, and should just be shown in UI
+--- A 'condition' key with function value can also be present in the option, which controls if the option gets shown by returning boolean.
+
+---@param builder_fn fun(): PopupData
+---@return PopupBuilder
function M.new(builder_fn)
local instance = {
state = {
@@ -106,17 +136,23 @@ function M.new(builder_fn)
return instance
end
-function M:name(x)
- self.state.name = x
+-- Set the popup's name. This must be set for all popups.
+---@param name string The name
+---@return self
+function M:name(name)
+ self.state.name = name
return self
end
-function M:env(x)
- self.state.env = x or {}
+-- Set initial context for the popup
+---@param env table The initial context
+---@return self
+function M:env(env)
+ self.state.env = env or {}
return self
end
----Adds new column to actions section of popup
+-- adds a new column to the actions section of the popup
---@param heading string?
---@return self
function M:new_action_group(heading)
@@ -124,7 +160,7 @@ function M:new_action_group(heading)
return self
end
----Conditionally adds new column to actions section of popup
+-- Conditionally adds a new column to the actions section of the popup
---@param cond boolean
---@param heading string?
---@return self
@@ -136,7 +172,7 @@ function M:new_action_group_if(cond, heading)
return self
end
----Adds new heading to current column within actions section of popup
+-- adds a new heading to current column within the actions section of the popup
---@param heading string
---@return self
function M:group_heading(heading)
@@ -144,7 +180,7 @@ function M:group_heading(heading)
return self
end
----Conditionally adds new heading to current column within actions section of popup
+-- Conditionally adds a new heading to current column within the actions section of the popup
---@param cond boolean
---@param heading string
---@return self
@@ -156,10 +192,11 @@ function M:group_heading_if(cond, heading)
return self
end
+-- Adds a switch to the popup
---@param key string Which key triggers switch
---@param cli string Git cli flag to use
---@param description string Description text to show user
----@param opts PopupSwitchOpts?
+---@param opts PopupSwitchOpts? Additional options
---@return self
function M:switch(key, cli, description, opts)
opts = opts or {}
@@ -176,8 +213,8 @@ function M:switch(key, cli, description, opts)
opts.incompatible = {}
end
- if opts.dependant == nil then
- opts.dependant = {}
+ if opts.dependent == nil then
+ opts.dependent = {}
end
if opts.key_prefix == nil then
@@ -192,6 +229,10 @@ function M:switch(key, cli, description, opts)
opts.cli_suffix = ""
end
+ if opts.persisted == nil then
+ opts.persisted = true
+ end
+
local value
if opts.enabled and opts.value then
value = cli .. opts.value
@@ -223,21 +264,22 @@ function M:switch(key, cli, description, opts)
cli_prefix = opts.cli_prefix,
user_input = opts.user_input,
cli_suffix = opts.cli_suffix,
+ persisted = opts.persisted,
options = opts.options,
incompatible = util.build_reverse_lookup(opts.incompatible),
- dependant = util.build_reverse_lookup(opts.dependant),
+ dependent = util.build_reverse_lookup(opts.dependent),
})
return self
end
--- Conditionally adds a switch.
+-- Conditionally adds a switch to the popup
---@see M:switch
----@param cond boolean
+---@param cond boolean The condition under which to add the config
---@param key string Which key triggers switch
---@param cli string Git cli flag to use
---@param description string Description text to show user
----@param opts PopupSwitchOpts?
+---@param opts PopupSwitchOpts? Additional options
---@return self
function M:switch_if(cond, key, cli, description, opts)
if cond then
@@ -247,10 +289,12 @@ function M:switch_if(cond, key, cli, description, opts)
return self
end
+-- Adds an option to the popup
---@param key string Key for the user to engage option
---@param cli string CLI value used
---@param value string Current value of option
---@param description string Description of option, presented to user
+---@param opts PopupOptionOpts? Additional options
function M:option(key, cli, value, description, opts)
opts = opts or {}
@@ -266,6 +310,14 @@ function M:option(key, cli, value, description, opts)
opts.separator = "="
end
+ if opts.dependent == nil then
+ opts.dependent = {}
+ end
+
+ if opts.incompatible == nil then
+ opts.incompatible = {}
+ end
+
if opts.setup then
opts.setup(self)
end
@@ -283,13 +335,15 @@ function M:option(key, cli, value, description, opts)
choices = opts.choices,
default = opts.default,
separator = opts.separator,
+ dependent = util.build_reverse_lookup(opts.dependent),
+ incompatible = util.build_reverse_lookup(opts.incompatible),
fn = opts.fn,
})
return self
end
--- Adds heading text within Arguments (options/switches) section of popup
+-- adds a heading text within Arguments (options/switches) section of the popup
---@param heading string Heading to show
---@return self
function M:arg_heading(heading)
@@ -297,8 +351,13 @@ function M:arg_heading(heading)
return self
end
+-- Conditionally adds an option to the popup
---@see M:option
----@param cond boolean
+---@param cond boolean The condition under which to add the config
+---@param key string Which key triggers switch
+---@param cli string Git cli flag to use
+---@param description string Description text to show user
+---@param opts PopupOptionOpts? Additional options
---@return self
function M:option_if(cond, key, cli, value, description, opts)
if cond then
@@ -308,16 +367,30 @@ function M:option_if(cond, key, cli, value, description, opts)
return self
end
----@param heading string Heading to render within config section of popup
+-- adds a heading text with the config section of the popup
+---@param heading string Heading to render
---@return self
function M:config_heading(heading)
table.insert(self.state.config, { heading = heading })
return self
end
+-- adds a heading text with the config section of the popup
+---@param cond boolean
+---@param heading string Heading to render
+---@return self
+function M:config_heading_if(cond, heading)
+ if cond then
+ table.insert(self.state.config, { heading = heading })
+ end
+
+ return self
+end
+
+-- Adds config to the popup
---@param key string Key for user to use that engages config
---@param name string Name of config
----@param options PopupConfigOpts?
+---@param options PopupConfigOpts? Additional options
---@return self
function M:config(key, name, options)
local entry = git.config.get(name)
@@ -341,9 +414,12 @@ function M:config(key, name, options)
return self
end
--- Conditionally adds config to popup
+-- Conditionally adds config to the popup
---@see M:config
----@param cond boolean
+---@param cond boolean The condition under which to add the config
+---@param key string Key for user to use that engages config
+---@param name string Name of config
+---@param options PopupConfigOpts? Additional options
---@return self
function M:config_if(cond, key, name, options)
if cond then
@@ -353,11 +429,26 @@ 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
----@param callback function Function that gets run in async context
+---@param callback? fun(popup: PopupData) Function that gets run in async context
+---@param opts? PopupActionOptions
---@return self
-function M:action(keys, description, callback)
+function M:action(keys, description, callback, opts)
+ opts = opts or {}
+
if type(keys) == "string" then
keys = { keys }
end
@@ -375,28 +466,39 @@ function M:action(keys, description, callback)
keys = keys,
description = description,
callback = callback,
+ persist_popup = opts.persist_popup or false,
})
return self
end
--- Conditionally adds action to popup
----@param cond boolean
+-- Conditionally adds an action to the popup
---@see M:action
+---@param cond boolean The condition under which to add the action
+---@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
+---@param callback? fun(popup: PopupData) Function that gets run in async context
+---@param opts? PopupActionOptions
---@return self
-function M:action_if(cond, key, description, callback)
+function M:action_if(cond, keys, description, callback, opts)
if cond then
- return self:action(key, description, callback)
+ return self:action(keys, description, callback, opts)
end
return self
end
+-- Builds the popup
+---@return PopupData # The popup
function M:build()
if self.state.name == nil then
error("A popup needs to have a name!")
end
+ if config.values.builders ~= nil and type(config.values.builders[self.state.name]) == "function" then
+ config.values.builders[self.state.name](self)
+ end
+
return self.builder_fn(self.state)
end
diff --git a/lua/neogit/lib/popup/init.lua b/lua/neogit/lib/popup/init.lua
index 909fb5c43..dc11c4143 100644
--- a/lua/neogit/lib/popup/init.lua
+++ b/lua/neogit/lib/popup/init.lua
@@ -2,7 +2,6 @@ local PopupBuilder = require("neogit.lib.popup.builder")
local Buffer = require("neogit.lib.buffer")
local logger = require("neogit.logger")
local util = require("neogit.lib.util")
-local config = require("neogit.config")
local state = require("neogit.lib.state")
local input = require("neogit.lib.input")
local notification = require("neogit.lib.notification")
@@ -26,6 +25,8 @@ local ui = require("neogit.lib.popup.ui")
---@field buffer Buffer
local M = {}
+-- Create a new popup builder
+---@return PopupBuilder
function M.builder()
return PopupBuilder.new(M.new)
end
@@ -63,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
@@ -91,7 +102,7 @@ function M:close()
end
-- Toggle a switch on/off
----@param switch table
+---@param switch PopupSwitch
---@return nil
function M:toggle_switch(switch)
if switch.options then
@@ -107,7 +118,10 @@ function M:toggle_switch(switch)
switch.cli = options[(index + 1)] or options[1]
switch.value = switch.cli
switch.enabled = switch.cli ~= ""
- state.set({ self.state.name, switch.cli_suffix }, switch.cli)
+
+ if switch.persisted ~= false then
+ state.set({ self.state.name, switch.cli_suffix }, switch.cli)
+ end
return
end
@@ -126,70 +140,117 @@ function M:toggle_switch(switch)
end
end
- state.set({ self.state.name, switch.cli }, switch.enabled)
+ if switch.persisted ~= false then
+ state.set({ self.state.name, switch.cli }, switch.enabled)
+ end
- -- Ensure that other switches that are incompatible with this one are disabled
+ -- Ensure that other switches/options that are incompatible with this one are disabled
if switch.enabled and #switch.incompatible > 0 then
for _, var in ipairs(self.state.args) do
- if var.type == "switch" and var.enabled and switch.incompatible[var.cli] then
- var.enabled = false
- state.set({ self.state.name, var.cli }, var.enabled)
+ if switch.incompatible[var.cli] then
+ if var.type == "switch" then
+ ---@cast var PopupSwitch
+ self:disable_switch(var)
+ elseif var.type == "option" then
+ ---@cast var PopupOption
+ self:disable_option(var)
+ end
end
end
end
- -- Ensure that switches that depend on this one are also disabled
- if not switch.enabled and #switch.dependant > 0 then
+ -- Ensure that switches/options that depend on this one are also disabled
+ if not switch.enabled and #switch.dependent > 0 then
for _, var in ipairs(self.state.args) do
- if var.type == "switch" and var.enabled and switch.dependant[var.cli] then
- var.enabled = false
- state.set({ self.state.name, var.cli }, var.enabled)
+ if switch.dependent[var.cli] then
+ if var.type == "switch" then
+ ---@cast var PopupSwitch
+ self:disable_switch(var)
+ elseif var.type == "option" then
+ ---@cast var PopupOption
+ self:disable_option(var)
+ end
end
end
end
end
-- Toggle an option on/off and set it's value
----@param option table
+---@param option PopupOption
+---@param value? string
---@return nil
-function M:set_option(option)
- -- Prompt user to select from predetermined choices
- if option.choices then
- if not option.value or option.value == "" then
- local choice = FuzzyFinderBuffer.new(option.choices):open_async {
- prompt_prefix = option.description,
- }
- if choice then
- option.value = choice
- else
- option.value = ""
- end
- else
- option.value = ""
- end
+function M:set_option(option, value)
+ 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
+ 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,
})
+ end
- -- 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 = option.default
- else
- option.value = input
+ state.set({ self.state.name, option.cli }, option.value)
+
+ -- Ensure that other switches/options that are incompatible with this one are disabled
+ if option.value and option.value ~= "" and #option.incompatible > 0 then
+ for _, var in ipairs(self.state.args) do
+ if option.incompatible[var.cli] then
+ if var.type == "switch" then
+ self:disable_switch(var --[[@as PopupSwitch]])
+ elseif var.type == "option" then
+ self:disable_option(var --[[@as PopupOption]])
+ end
+ end
end
end
- state.set({ self.state.name, option.cli }, option.value)
+ -- Ensure that switches/options that depend on this one are also disabled
+ if option.value and option.value ~= "" and #option.dependent > 0 then
+ for _, var in ipairs(self.state.args) do
+ if option.dependent[var.cli] then
+ if var.type == "switch" then
+ self:disable_switch(var --[[@as PopupSwitch]])
+ elseif var.type == "option" then
+ self:disable_option(var --[[@as PopupOption]])
+ end
+ end
+ end
+ end
+end
+
+---Disables a switch.
+---@param switch PopupSwitch
+function M:disable_switch(switch)
+ if switch.enabled then
+ self:toggle_switch(switch)
+ end
+end
+
+---Disables an option, setting its value to "". Doesn't use the default, which
+---is important to ensure that we don't use incompatible switches/options
+---together.
+---@param option PopupOption
+function M:disable_option(option)
+ if option.value and option.value ~= "" then
+ self:set_option(option, "")
+ end
end
-- Set a config value
----@param config table
+---@param config PopupConfig
---@return nil
function M:set_config(config)
if config.options then
@@ -209,6 +270,7 @@ function M:set_config(config)
else
local result = input.get_user_input(config.name, { default = config.value, cancel = config.value })
+ assert(result, "no input from user - what happened to the default?")
config.value = result
git.config.set(config.name, config.value)
end
@@ -236,7 +298,7 @@ function M:mappings()
[""] = function()
self:close()
end,
- [""] = function()
+ [""] = a.void(function()
local component = self.buffer.ui:get_interactive_component_under_cursor()
if not component then
return
@@ -251,7 +313,7 @@ function M:mappings()
end
self:refresh()
- end,
+ end),
},
}
@@ -261,8 +323,10 @@ function M:mappings()
arg_prefixes[arg.key_prefix] = true
mappings.n[arg.id] = a.void(function()
if arg.type == "switch" then
+ ---@cast arg PopupSwitch
self:toggle_switch(arg)
elseif arg.type == "option" then
+ ---@cast arg PopupOption
self:set_option(arg)
end
@@ -288,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
@@ -301,7 +366,11 @@ function M:mappings()
for _, key in ipairs(action.keys) do
mappings.n[key] = a.void(function()
logger.debug(string.format("[POPUP]: Invoking action %q of %s", key, self.state.name))
- self:close()
+ if not action.persist_popup then
+ logger.debug("[POPUP]: Closing popup")
+ self:close()
+ end
+
action.callback(self)
Watcher.instance():dispatch_refresh()
end)
@@ -321,7 +390,6 @@ end
function M:refresh()
if self.buffer then
- self.buffer:focus()
self.buffer.ui:render(unpack(ui.Popup(self.state)))
end
end
@@ -350,7 +418,7 @@ function M:show()
pcall(self.close, self)
end,
},
- after = function(buf, _win)
+ after = function(buf)
buf:set_window_option("cursorline", false)
buf:set_window_option("list", false)
@@ -368,25 +436,18 @@ function M:show()
end
end
- if
- config.values.popup.kind == "split"
- or config.values.popup.kind == "split_above"
- or config.values.popup.kind == "split_above_all"
- or config.values.popup.kind == "split_below"
- or config.values.popup.kind == "split_below_all"
- then
- vim.cmd.resize(vim.fn.line("$") + 1)
-
- -- We do it again because things like the BranchConfigPopup come from an async context,
- -- but if we only do it schedule wrapped, then you can see it load at one size, and
- -- resize a few ms later
- vim.schedule(function()
- if buf:is_focused() then
- vim.cmd.resize(vim.fn.line("$") + 1)
- buf:set_window_option("winfixheight", true)
- end
- end)
- end
+ local height = vim.fn.line("$") + 1
+ vim.cmd.resize(height)
+
+ -- We do it again because things like the BranchConfigPopup come from an async context,
+ -- but if we only do it schedule wrapped, then you can see it load at one size, and
+ -- resize a few ms later
+ vim.schedule(function()
+ if buf:is_focused() then
+ vim.cmd.resize(height)
+ buf:set_window_option("winfixheight", true)
+ end
+ end)
end,
render = function()
return ui.Popup(self.state)
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/state.lua b/lua/neogit/lib/state.lua
index 246701809..5b6335492 100644
--- a/lua/neogit/lib/state.lua
+++ b/lua/neogit/lib/state.lua
@@ -17,7 +17,7 @@ end
---@return Path
function M.filepath(config)
- local state_path = Path.new(vim.fn.stdpath("state")):joinpath("neogit")
+ local state_path = Path:new(vim.fn.stdpath("state")):joinpath("neogit")
local filename = "state"
if config.use_per_project_settings then
@@ -60,7 +60,12 @@ function M.read()
end
log("Reading file")
- return vim.mpack.decode(M.path:read())
+ local content = M.path:read()
+ if content then
+ return vim.mpack.decode(content)
+ else
+ return {}
+ end
end
---Writes state to disk
diff --git a/lua/neogit/lib/ui/component.lua b/lua/neogit/lib/ui/component.lua
index 82a55d2cc..ae737977c 100644
--- a/lua/neogit/lib/ui/component.lua
+++ b/lua/neogit/lib/ui/component.lua
@@ -25,6 +25,13 @@ local default_component_options = {
---@field section string|nil
---@field item table|nil
---@field id string|nil
+---@field oid string|nil
+---@field ref ParsedRef
+---@field yankable string?
+---@field on_open fun(fold, Ui)
+---@field hunk Hunk
+---@field filename string?
+---@field value any
---@class Component
---@field position ComponentPosition
@@ -35,8 +42,15 @@ local default_component_options = {
---@field index number|nil
---@field value string|nil
---@field id string|nil
+---@field highlight fun(hl_group:string): self
+---@field line_hl fun(hl_group:string): self
+---@field padding_left fun(string): self
+---@field first integer|nil first line component appears rendered in buffer
+---@field last integer|nil last line component appears rendered in buffer
+---@operator call: Component
local Component = {}
+---@return integer, integer
function Component:row_range_abs()
return self.position.row_start, self.position.row_end
end
@@ -140,6 +154,8 @@ function Component:close_all_folds(ui)
end
end
+---@param f fun(...): table
+---@return Component
function Component.new(f)
local instance = {}
diff --git a/lua/neogit/lib/ui/debug.lua b/lua/neogit/lib/ui/debug.lua
index 4e5df63b9..c09bda914 100644
--- a/lua/neogit/lib/ui/debug.lua
+++ b/lua/neogit/lib/ui/debug.lua
@@ -33,13 +33,13 @@ function Ui._visualize_tree(indent, components, tree)
end
end
-function Ui.visualize_component(c, options)
- Ui._print_component(0, c, options or {})
-
- if c.tag == "col" or c.tag == "row" then
- Ui._visualize_tree(1, c.children, options or {})
- end
-end
+-- function Ui.visualize_component(c, options)
+-- Ui._print_component(0, c, options or {})
+--
+-- if c.tag == "col" or c.tag == "row" then
+-- Ui._visualize_tree(1, c.children, options or {})
+-- end
+-- end
function Ui._draw_component(indent, c, _)
local output = string.rep(" ", indent)
diff --git a/lua/neogit/lib/ui/init.lua b/lua/neogit/lib/ui/init.lua
index 92f9814d2..225cc5af5 100644
--- a/lua/neogit/lib/ui/init.lua
+++ b/lua/neogit/lib/ui/init.lua
@@ -5,8 +5,9 @@ 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
---@class Selection
---@field sections Section[]
@@ -15,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
@@ -39,6 +40,7 @@ function Ui.new(buf)
return setmetatable({ buf = buf, layout = {} }, Ui)
end
+---@return Component|nil
function Ui._find_component(components, f, options)
for _, c in ipairs(components) do
if c.tag == "col" or c.tag == "row" then
@@ -63,24 +65,6 @@ function Ui:find_component(f, options)
return Ui._find_component(self.layout, f, options or {})
end
-function Ui._find_components(components, f, result, options)
- for _, c in ipairs(components) do
- if c.tag == "col" or c.tag == "row" then
- Ui._find_components(c.children, f, result, options)
- end
-
- if f(c) then
- table.insert(result, c)
- end
- end
-end
-
-function Ui:find_components(f, options)
- local result = {}
- Ui._find_components(self.layout, f, result, options or {})
- return result
-end
-
---@param fn? fun(c: Component): boolean
---@return Component|nil
function Ui:get_component_under_cursor(fn)
@@ -113,6 +97,12 @@ function Ui:_find_component_by_index(line, f)
end
end
+---@param oid string
+---@return Component|nil
+function Ui:find_component_by_oid(oid)
+ return self.node_index:find_by_oid(oid)
+end
+
---@return Component|nil
function Ui:get_cursor_context(line)
local cursor = line or vim.api.nvim_win_get_cursor(0)[1]
@@ -148,15 +138,6 @@ function Ui:get_fold_under_cursor()
end)
end
----@class StatusItem
----@field name string
----@field first number
----@field last number
----@field oid string|nil optional object id
----@field commit CommitLogEntry|nil optional object id
----@field folded boolean|nil
----@field hunks Hunk[]|nil
-
---@class SelectedHunk: Hunk
---@field from number start offset from the first line of the hunk
---@field to number end offset from the first line of the hunk
@@ -184,25 +165,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
@@ -216,7 +191,6 @@ function Ui:item_hunks(item, first_line, last_line, partial)
to = to,
__index = h,
hunk = h,
- lines = hunk_lines,
-- conflict = conflict,
}
@@ -230,6 +204,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(".")
@@ -292,6 +267,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] }
@@ -301,7 +277,7 @@ function Ui:get_commits_in_selection()
local commits = {}
for i = start, stop do
local component = self:_find_component_by_index(i, function(node)
- return node.options.oid
+ return node.options.oid ~= nil
end)
if component then
@@ -312,6 +288,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] }
@@ -321,7 +324,7 @@ function Ui:get_filepaths_in_selection()
local paths = {}
for i = start, stop do
local component = self:_find_component_by_index(i, function(node)
- return node.options.item and node.options.item.escaped_path
+ return node.options.item ~= nil and node.options.item.escaped_path
end)
if component then
@@ -352,6 +355,27 @@ function Ui:get_ref_under_cursor()
return component and component.options.ref
end
+---
+---@return ParsedRef[]
+function Ui:get_refs_under_cursor()
+ local range = { vim.fn.getpos("v")[2], vim.fn.getpos(".")[2] }
+ table.sort(range)
+ local start, stop = unpack(range)
+
+ local refs = {}
+ for i = start, stop do
+ local component = self:_find_component_by_index(i, function(node)
+ return node.options.ref ~= nil
+ end)
+
+ if component then
+ table.insert(refs, 1, component.options.ref)
+ end
+ end
+
+ return util.deduplicate(refs)
+end
+
---@return string|nil
function Ui:get_yankable_under_cursor()
local cursor = vim.api.nvim_win_get_cursor(0)
@@ -504,7 +528,7 @@ function Ui:resolve_cursor_location(cursor)
if cursor.hunk.index_from == hunk.index_from then
logger.debug(("[UI] Using hunk.first with offset %q"):format(cursor.hunk.name))
- return hunk.first + cursor.hunk_offset - (cursor.last - hunk.last)
+ return hunk.first + (cursor.hunk_offset or 0) - (cursor.last - hunk.last)
else
logger.debug(("[UI] Using hunk.first %q"):format(cursor.hunk.name))
return hunk.first
@@ -515,7 +539,7 @@ end
function Ui:get_hunk_or_filename_under_cursor()
local cursor = vim.api.nvim_win_get_cursor(0)
local component = self:_find_component_by_index(cursor[1], function(node)
- return node.options.hunk or node.options.filename
+ return node.options.hunk ~= nil or node.options.filename ~= nil
end)
return component and {
@@ -528,7 +552,7 @@ end
function Ui:get_item_under_cursor()
local cursor = vim.api.nvim_win_get_cursor(0)
local component = self:_find_component_by_index(cursor[1], function(node)
- return node.options.item
+ return node.options.item ~= nil
end)
return component and component.options.item
@@ -740,10 +764,6 @@ Ui.text = Component.new(function(value, options, ...)
error("Too many arguments")
end
- vim.validate {
- options = { options, "table", true },
- }
-
return {
tag = "text",
value = value or "",
diff --git a/lua/neogit/lib/ui/renderer.lua b/lua/neogit/lib/ui/renderer.lua
index 2c187bcdb..721278565 100644
--- a/lua/neogit/lib/ui/renderer.lua
+++ b/lua/neogit/lib/ui/renderer.lua
@@ -1,8 +1,11 @@
---@source component.lua
+local strdisplaywidth = vim.fn.strdisplaywidth
+
---@class RendererIndex
---@field index table
---@field items table
+---@field oid_index table
local RendererIndex = {}
RendererIndex.__index = RendererIndex
@@ -12,6 +15,12 @@ function RendererIndex:find_by_line(line)
return self.index[line] or {}
end
+---@param oid string
+---@return Component|nil
+function RendererIndex:find_by_oid(oid)
+ return self.oid_index[oid]
+end
+
---@param node Component
function RendererIndex:add(node)
if not self.index[node.position.row_start] then
@@ -32,17 +41,25 @@ 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
item.first = first
item.last = last
table.insert(self.items[#self.items].items, item)
+
+ if item.oid then
+ self.oid_index[item.oid] = item
+ end
end
function RendererIndex.new()
return setmetatable({
index = {},
+ oid_index = {},
items = {
{ items = {} }, -- First section
},
@@ -60,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
@@ -113,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
@@ -240,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)
@@ -252,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)
@@ -279,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)
@@ -290,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 532c8d419..46c9942f6 100644
--- a/lua/neogit/lib/util.lua
+++ b/lua/neogit/lib/util.lua
@@ -3,7 +3,7 @@ local M = {}
---@generic T: any
---@generic U: any
---@param tbl T[]
----@param f fun(v: T): U
+---@param f Component|fun(v: T): U
---@return U[]
function M.map(tbl, f)
local t = {}
@@ -455,7 +455,7 @@ end
---@param callback function
---@return uv_timer_t
local function set_timeout(timeout, callback)
- local timer = vim.loop.new_timer()
+ local timer = vim.uv.new_timer()
timer:start(timeout, 0, function()
timer:stop()
@@ -522,7 +522,7 @@ function M.debounce_trailing(ms, fn, hash)
return function(...)
local id = hash and hash(...) or true
if running[id] == nil then
- running[id] = assert(vim.loop.new_timer())
+ running[id] = assert(vim.uv.new_timer())
end
local timer = running[id]
@@ -600,9 +600,8 @@ end
---@param winid integer
---@param force boolean
function M.safe_win_close(winid, force)
- local ok, _ = pcall(vim.api.nvim_win_close, winid, force)
-
- if not ok then
+ local success = M.try(vim.api.nvim_win_close, winid, force)
+ if not success then
pcall(vim.cmd, "b#")
end
end
@@ -611,4 +610,17 @@ function M.weak_table(mode)
return setmetatable({}, { __mode = mode or "k" })
end
+---@param fn fun(...): any
+---@param ...any
+---@return boolean|any
+function M.try(fn, ...)
+ local ok, result = pcall(fn, ...)
+ if not ok then
+ require("neogit.logger").error(result)
+ return false
+ else
+ return result or true
+ end
+end
+
return M
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 190c5230f..d733e396b 100644
--- a/lua/neogit/popups/branch/actions.lua
+++ b/lua/neogit/popups/branch/actions.lua
@@ -5,27 +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)
- git.branch.checkout(target, args)
- fire_branch_event("NeogitBranchCheckout", { branch_name = target })
+ local result = git.branch.checkout(target, args)
+ if result:failure() then
+ notification.error(table.concat(result.stderr, "\n"))
+ return
+ end
+
+ event.send("BranchCheckout", { branch_name = target })
+ notification.info("Checked out branch " .. target)
if config.values.fetch_after_checkout then
a.void(function()
@@ -43,35 +46,45 @@ local function checkout_branch(target, args)
end
end
+local function get_branch_name_user_input(prompt, default)
+ default = default or config.values.initial_branch_name
+ return input.get_user_input(prompt, { strip_spaces = true, default = default })
+end
+
+---@param checkout boolean
local function spin_off_branch(checkout)
if git.status.is_dirty() and not checkout then
notification.info("Staying on HEAD due to uncommitted changes")
checkout = true
end
- local name =
- input.get_user_input(("%s branch"):format(checkout and "Spin-off" or "Spin-out"), { strip_spaces = true })
+ local name = get_branch_name_user_input(("%s branch"):format(checkout and "Spin-off" or "Spin-out"))
if not name then
return
end
- fire_branch_event("NeogitBranchCreate", { branch_name = name })
- git.branch.create(name)
+ if not git.branch.create(name) then
+ notification.warn("Branch " .. name .. " already exists.")
+ return
+ end
+
+ 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()
if upstream then
if checkout then
+ assert(current_branch_name, "No current branch")
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
@@ -110,19 +123,25 @@ local function create_branch(popup, prompt, checkout, name)
end
local name = name
- or input.get_user_input("Create branch", {
- strip_spaces = true,
- default = popup.state.env.suggested_branch_name or suggested_branch_name,
- })
+ or get_branch_name_user_input(
+ "Create branch",
+ popup.state.env.suggested_branch_name or suggested_branch_name
+ )
if not name then
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
@@ -170,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)
@@ -205,7 +230,7 @@ function M.configure_branch()
return
end
- BranchConfigPopup.create(branch_name)
+ BranchConfigPopup.create { branch = branch_name }
end
function M.rename_branch()
@@ -215,15 +240,18 @@ function M.rename_branch()
return
end
- local new_name = input.get_user_input(("Rename '%s' to"):format(selected_branch), { strip_spaces = true })
+ local new_name = get_branch_name_user_input(("Rename '%s' to"):format(selected_branch))
if not new_name then
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)
@@ -263,30 +291,38 @@ function M.reset_branch(popup)
end
-- Reset the current branch to the desired state & update reflog
- git.cli.reset.hard.args(to).call()
- git.log.update_ref(git.branch.current_full_name(), 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)
local options = util.deduplicate(util.merge({ popup.state.env.ref_name }, git.refs.list_branches()))
- local selected_branch = FuzzyFinderBuffer.new(options):open_async { refocus_status = false }
+ local selected_branch = FuzzyFinderBuffer.new(options)
+ :open_async { prompt_prefix = "Delete branch", refocus_status = false }
if not selected_branch then
return
end
local remote, branch_name = git.branch.parse_remote_branch(selected_branch)
+ local is_remote = remote and remote ~= "."
local success = false
if
- remote
+ is_remote
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
- elseif not remote and branch_name == git.branch.current() then
+ success = git.cli.push.remote(remote).delete.to(branch_name).call():success()
+ elseif not is_remote and branch_name == git.branch.current() then
local choices = {
"&detach HEAD and delete",
"&abort",
@@ -305,6 +341,7 @@ function M.delete_branch(popup)
if choice == "d" then
git.cli.checkout.detach.call()
elseif choice == "c" then
+ assert(upstream, "there should be an upstream by this point")
git.cli.checkout.branch(upstream).call()
else
return
@@ -314,29 +351,34 @@ function M.delete_branch(popup)
if not success then -- Reset HEAD if unsuccessful
git.cli.checkout.branch(branch_name).call()
end
- elseif not remote and branch_name then
+ elseif not is_remote and branch_name then
success = git.branch.delete(branch_name)
end
if success then
- if remote then
+ if is_remote then
notification.info(string.format("Deleted remote branch '%s/%s'", remote, branch_name))
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
function M.open_pull_request()
local template
local service
- local url = git.remote.get_url(https://github.com/git.branch.upstream_remote())[1]
+ local upstream = git.branch.upstream_remote()
+ if not upstream then
+ return
+ end
+
+ local url = git.remote.get_url(https://github.com/upstream)[1]
for s, v in pairs(config.values.git_services) do
if url:match(util.pattern_escape(s)) then
service = s
- template = v
+ template = v.pull_request
break
end
end
@@ -363,9 +405,11 @@ 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")
+ notification.warn("Requires Neovim >= 0.10")
end
else
notification.warn("Pull request URL template not found for this branch's upstream")
diff --git a/lua/neogit/popups/branch/init.lua b/lua/neogit/popups/branch/init.lua
index 34a8ad011..3a4177f4f 100644
--- a/lua/neogit/popups/branch/init.lua
+++ b/lua/neogit/popups/branch/init.lua
@@ -11,16 +11,24 @@ function M.create(env)
local show_config = current_branch ~= ""
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 has_upstream = git.branch.upstream() ~= nil
local p = popup
.builder()
:name("NeogitBranchPopup")
- :switch("r", "recurse-submodules", "Recurse submodules when checking out an existing branch")
+ :config_heading_if(show_config, "Configure branch")
:config_if(show_config, "d", "branch." .. current_branch .. ".description", {
fn = config_actions.description_config(current_branch),
})
:config_if(show_config, "u", "branch." .. current_branch .. ".merge", {
fn = config_actions.merge_config(current_branch),
+ callback = function(popup)
+ for _, config in ipairs(popup.state.config) do
+ if config.name == "branch." .. current_branch .. ".remote" then
+ config.value = tostring(config.entry:refresh():read() or "")
+ end
+ end
+ end,
})
:config_if(show_config, "m", "branch." .. current_branch .. ".remote", { passive = true })
:config_if(show_config, "R", "branch." .. current_branch .. ".rebase", {
@@ -33,6 +41,7 @@ function M.create(env)
:config_if(show_config, "p", "branch." .. current_branch .. ".pushRemote", {
options = config_actions.remotes_for_config(),
})
+ :switch("r", "recurse-submodules", "Recurse submodules when checking out an existing branch")
:group_heading("Checkout")
:action("b", "branch/revision", actions.checkout_branch_revision)
:action("l", "local branch", actions.checkout_local_branch)
@@ -50,7 +59,7 @@ function M.create(env)
:action("m", "rename", actions.rename_branch)
:action("X", "reset", actions.reset_branch)
:action("D", "delete", actions.delete_branch)
- :action_if(git.branch.upstream(), "o", "pull request", actions.open_pull_request)
+ :action_if(has_upstream, "o", "pull request", actions.open_pull_request)
:env(env)
:build()
diff --git a/lua/neogit/popups/branch_config/actions.lua b/lua/neogit/popups/branch_config/actions.lua
index 73444cc92..ecb9b8cc9 100644
--- a/lua/neogit/popups/branch_config/actions.lua
+++ b/lua/neogit/popups/branch_config/actions.lua
@@ -24,7 +24,22 @@ end
function M.merge_config(branch)
local fn = function()
- local target = FuzzyFinderBuffer.new(git.refs.list_branches()):open_async { prompt_prefix = "upstream" }
+ -- When the values are set, clear them and return
+ if git.config.get_local("branch." .. branch .. ".merge"):is_set() then
+ git.config.set("branch." .. branch .. ".merge", nil)
+ git.config.set("branch." .. branch .. ".remote", nil)
+
+ return
+ end
+
+ local eventignore = vim.o.eventignore
+ vim.o.eventignore = "WinLeave"
+ local target = FuzzyFinderBuffer.new(git.refs.list_branches()):open_async {
+ prompt_prefix = "upstream",
+ refocus_status = false,
+ }
+ vim.o.eventignore = eventignore
+
if not target then
return
end
@@ -59,7 +74,7 @@ function M.description_config(branch)
})
vim.o.eventignore = ""
- return git.config.get("branch." .. branch .. ".description"):read()
+ return git.config.get_local("branch." .. branch .. ".description"):read()
end
return a.wrap(fn, 2)
diff --git a/lua/neogit/popups/branch_config/init.lua b/lua/neogit/popups/branch_config/init.lua
index 6c953b42c..2cd1e8171 100644
--- a/lua/neogit/popups/branch_config/init.lua
+++ b/lua/neogit/popups/branch_config/init.lua
@@ -3,20 +3,41 @@ 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")
+
+---@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
-function M.create(branch)
- branch = branch or git.branch.current()
local g_pull_rebase = git.config.get_global("pull.rebase")
- local pull_rebase_entry = git.config.get("pull.rebase")
+ local pull_rebase_entry = git.config.get_local("pull.rebase")
local pull_rebase = pull_rebase_entry:is_set() and pull_rebase_entry.value or "false"
local p = popup
.builder()
:name("NeogitBranchConfigPopup")
:config_heading("Configure branch")
- :config("d", "branch." .. branch .. ".description", { fn = actions.description_config(branch) })
- :config("u", "branch." .. branch .. ".merge", { fn = actions.merge_config(branch) })
- :config("m", "branch." .. branch .. ".remote", { passive = true })
+ :config("d", "branch." .. branch .. ".description", {
+ fn = actions.description_config(branch),
+ })
+ :config("u", "branch." .. branch .. ".merge", {
+ fn = actions.merge_config(branch),
+ callback = function(popup)
+ for _, config in ipairs(popup.state.config) do
+ if config.name == "branch." .. branch .. ".remote" then
+ config.value = tostring(config.entry:refresh():read() or "")
+ end
+ end
+ end,
+ })
+ :config("m", "branch." .. branch .. ".remote", {
+ passive = true,
+ })
:config("r", "branch." .. branch .. ".rebase", {
options = {
{ display = "true", value = "true" },
@@ -24,7 +45,9 @@ function M.create(branch)
{ display = "pull.rebase:" .. pull_rebase, value = "" },
},
})
- :config("p", "branch." .. branch .. ".pushRemote", { options = actions.remotes_for_config() })
+ :config("p", "branch." .. branch .. ".pushRemote", {
+ options = actions.remotes_for_config(),
+ })
:config_heading("")
:config_heading("Configure repository defaults")
:config("R", "pull.rebase", {
@@ -40,7 +63,9 @@ function M.create(branch)
},
},
})
- :config("P", "remote.pushDefault", { options = actions.remotes_for_config() })
+ :config("P", "remote.pushDefault", {
+ options = actions.remotes_for_config(),
+ })
:config("b", "neogit.baseBranch")
:config("A", "neogit.askSetPushDefault", {
options = {
diff --git a/lua/neogit/popups/cherry_pick/actions.lua b/lua/neogit/popups/cherry_pick/actions.lua
index 28a7ea833..764e81934 100644
--- a/lua/neogit/popups/cherry_pick/actions.lua
+++ b/lua/neogit/popups/cherry_pick/actions.lua
@@ -1,8 +1,10 @@
local M = {}
-
+local util = require("neogit.lib.util")
local git = require("neogit.lib.git")
+local notification = require("neogit.lib.notification")
local CommitSelectViewBuffer = require("neogit.buffers.commit_select_view")
+local FuzzyFinderBuffer = require("neogit.buffers.fuzzy_finder")
---@param popup any
---@return table
@@ -13,6 +15,7 @@ local function get_commits(popup)
else
commits = CommitSelectViewBuffer.new(
git.log.list { "--max-count=256" },
+ git.remote.list(),
"Select one or more commits to cherry pick with , or to abort"
):open_async()
end
@@ -38,6 +41,93 @@ function M.apply(popup)
git.cherry_pick.apply(commits, popup:get_arguments())
end
+function M.squash(popup)
+ local refs = util.merge(popup.state.env.commits, git.refs.list_branches(), git.refs.list_tags())
+ local ref = FuzzyFinderBuffer.new(refs):open_async { prompt_prefix = "Squash" }
+ if ref then
+ local args = popup:get_arguments()
+ table.insert(args, "--squash")
+ git.merge.merge(ref, args)
+ end
+end
+
+---@param popup PopupData
+---@param verb string
+---@return string[]
+local function get_cherries(popup, verb)
+ local commits
+ if #popup.state.env.commits > 1 then
+ commits = popup.state.env.commits
+ else
+ local refs = util.merge(popup.state.env.commits, git.refs.list_branches())
+ local ref = FuzzyFinderBuffer.new(refs):open_async { prompt_prefix = verb .. " cherry" }
+
+ if ref == popup.state.env.commits[1] then
+ commits = popup.state.env.commits
+ else
+ commits = util.map(git.cherry.list(git.rev_parse.oid("HEAD"), ref), function(cherry)
+ return cherry.oid or cherry
+ end)
+ end
+
+ if not commits[1] then
+ commits = { git.rev_parse.oid(ref) }
+ end
+ end
+
+ return commits
+end
+
+---@param popup PopupData
+function M.donate(popup)
+ local commits = get_cherries(popup, "Donate")
+ local src = git.branch.current() or git.rev_parse.oid("HEAD")
+
+ if not git.log.is_ancestor(commits[1], git.rev_parse.oid(src)) then
+ return notification.error("Cannot donate cherries that are not reachable from HEAD")
+ end
+
+ local prefix = string.format("Move %d cherr%s to branch", #commits, #commits > 1 and "ies" or "y")
+ local options = git.refs.list_branches()
+ util.remove_item_from_table(options, src)
+
+ local dst = FuzzyFinderBuffer.new(options):open_async { prompt_prefix = prefix }
+ if dst then
+ notification.info(
+ ("Moved %d cherr%s from %q to %q"):format(#commits, #commits > 1 and "ies" or "y", src, dst)
+ )
+ git.cherry_pick.move(commits, src, dst, popup:get_arguments())
+ end
+end
+
+---@param popup PopupData
+function M.harvest(popup)
+ local current = git.branch.current()
+ if not current then
+ return
+ end
+
+ local commits = get_cherries(popup, "Harvest")
+
+ if git.log.is_ancestor(commits[1], git.rev_parse.oid("HEAD")) then
+ return notification.error("Cannot harvest cherries that are reachable from HEAD")
+ end
+
+ local branch
+ local containing_branches = git.branch.list_containing_branches(commits[1])
+ if #containing_branches > 1 then
+ local prefix = string.format("Remove %d cherr%s from branch", #commits, #commits > 1 and "ies" or "y")
+ branch = FuzzyFinderBuffer.new(containing_branches):open_async { prompt_prefix = prefix }
+ else
+ branch = containing_branches[1]
+ end
+
+ if branch then
+ notification.info(("Harvested %d cherr%s"):format(#commits, #commits > 1 and "ies" or "y"))
+ git.cherry_pick.move(commits, branch, current, popup:get_arguments(), nil, true)
+ end
+end
+
function M.continue()
git.cherry_pick.continue()
end
diff --git a/lua/neogit/popups/cherry_pick/init.lua b/lua/neogit/popups/cherry_pick/init.lua
index 180d476a8..2f48714b0 100644
--- a/lua/neogit/popups/cherry_pick/init.lua
+++ b/lua/neogit/popups/cherry_pick/init.lua
@@ -10,29 +10,34 @@ function M.create(env)
local p = popup
.builder()
:name("NeogitCherryPickPopup")
- :option_if(not in_progress, "m", "mainline", "", "Replay merge relative to parent", { key_prefix = "-" })
+ :option_if(not in_progress, "m", "mainline", "", "Replay merge relative to parent", {
+ key_prefix = "-",
+ })
:option_if(not in_progress, "s", "strategy", "", "Strategy", {
key_prefix = "=",
choices = { "octopus", "ours", "resolve", "subtree", "recursive" },
})
- :switch_if(
- not in_progress,
- "F",
- "ff",
- "Attempt fast-forward",
- { enabled = true, incompatible = { "edit" } }
- )
- :switch_if(not in_progress, "x", "x", "Reference cherry in commit message", { cli_prefix = "-" })
- :switch_if(not in_progress, "e", "edit", "Edit commit messages", { incompatible = { "ff" } })
+ :switch_if(not in_progress, "F", "ff", "Attempt fast-forward", {
+ enabled = true,
+ incompatible = { "edit" },
+ })
+ :switch_if(not in_progress, "x", "x", "Reference cherry in commit message", {
+ cli_prefix = "-",
+ })
+ :switch_if(not in_progress, "e", "edit", "Edit commit messages", {
+ incompatible = { "ff" },
+ })
:switch_if(not in_progress, "s", "signoff", "Add Signed-off-by lines")
- :option_if(not in_progress, "S", "gpg-sign", "", "Sign using gpg", { key_prefix = "-" })
+ :option_if(not in_progress, "S", "gpg-sign", "", "Sign using gpg", {
+ key_prefix = "-",
+ })
:group_heading_if(not in_progress, "Apply here")
:action_if(not in_progress, "A", "Pick", actions.pick)
:action_if(not in_progress, "a", "Apply", actions.apply)
- :action_if(not in_progress, "h", "Harvest")
- :action_if(not in_progress, "m", "Squash")
+ :action_if(not in_progress, "h", "Harvest", actions.harvest)
+ :action_if(not in_progress, "m", "Squash", actions.squash)
:new_action_group_if(not in_progress, "Apply elsewhere")
- :action_if(not in_progress, "d", "Donate")
+ :action_if(not in_progress, "d", "Donate", actions.donate)
:action_if(not in_progress, "n", "Spinout")
:action_if(not in_progress, "s", "Spinoff")
:group_heading_if(in_progress, "Cherry Pick")
diff --git a/lua/neogit/popups/commit/actions.lua b/lua/neogit/popups/commit/actions.lua
index 9d188830e..252922a3e 100644
--- a/lua/neogit/popups/commit/actions.lua
+++ b/lua/neogit/popups/commit/actions.lua
@@ -8,6 +8,13 @@ local notification = require("neogit.lib.notification")
local config = require("neogit.config")
local a = require("plenary.async")
+---@param popup PopupData
+---@return boolean
+local function allow_empty(popup)
+ return vim.tbl_contains(popup:get_arguments(), "--allow-empty")
+ or vim.tbl_contains(popup:get_arguments(), "--all")
+end
+
local function confirm_modifications()
if
git.branch.upstream()
@@ -30,6 +37,7 @@ local function do_commit(popup, cmd)
autocmd = "NeogitCommitComplete",
msg = {
success = "Committed",
+ fail = "Commit failed",
},
interactive = true,
show_diff = config.values.commit_editor.show_staged_diff,
@@ -37,7 +45,7 @@ local function do_commit(popup, cmd)
end
local function commit_special(popup, method, opts)
- if not git.status.anything_staged() then
+ if not git.status.anything_staged() and not allow_empty(popup) then
if git.status.anything_unstaged() then
if input.get_permission("Nothing is staged. Commit all uncommitted changed?") then
opts.all = true
@@ -50,7 +58,8 @@ local function commit_special(popup, method, opts)
end
end
- local commit = popup.state.env.commit or CommitSelectViewBuffer.new(git.log.list()):open_async()[1]
+ local commit = popup.state.env.commit
+ or CommitSelectViewBuffer.new(git.log.list(), git.remote.list()):open_async()[1]
if not commit then
return
end
@@ -69,13 +78,13 @@ local function commit_special(popup, method, opts)
if choice == "c" then
opts.rebase = false
elseif choice == "s" then
- commit = CommitSelectViewBuffer.new(git.log.list()):open_async()[1]
+ commit = CommitSelectViewBuffer.new(git.log.list(), git.remote.list()):open_async()[1]
else
return
end
end
- local cmd = git.cli.commit.args(string.format("--%s=%s", method, commit))
+ local cmd = git.cli.commit
if opts.edit then
cmd = cmd.edit
else
@@ -87,7 +96,7 @@ local function commit_special(popup, method, opts)
end
a.util.scheduler()
- do_commit(popup, cmd)
+ do_commit(popup, cmd.args(method:format(commit)))
if opts.rebase then
a.util.scheduler()
@@ -96,10 +105,27 @@ local function commit_special(popup, method, opts)
end
function M.commit(popup)
+ if not git.status.anything_staged() and not allow_empty(popup) then
+ notification.warn("No changes to commit.")
+ return
+ end
+
do_commit(popup, git.cli.commit)
end
function M.extend(popup)
+ if not git.status.anything_staged() and not allow_empty(popup) then
+ if git.status.anything_unstaged() then
+ if input.get_permission("Nothing is staged. Commit all uncommitted changes?") then
+ git.status.stage_modified()
+ else
+ return
+ end
+ else
+ return notification.warn("No changes to commit.")
+ end
+ end
+
if not confirm_modifications() then
return
end
@@ -124,15 +150,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)
@@ -140,7 +174,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)
@@ -148,7 +182,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)
@@ -157,9 +191,9 @@ function M.absorb(popup)
return
end
- if not git.status.anything_staged() then
+ if not git.status.anything_staged() and not allow_empty(popup) then
if git.status.anything_unstaged() then
- if input.get_permission("Nothing is staged. Absorb all unstaged changed?") then
+ if input.get_permission("Nothing is staged. Absorb all unstaged changes?") then
git.status.stage_modified()
else
return
@@ -173,6 +207,7 @@ function M.absorb(popup)
local commit = popup.state.env.commit
or CommitSelectViewBuffer.new(
git.log.list { "HEAD" },
+ git.remote.list(),
"Select a base commit for the absorb stack with , or to abort"
)
:open_async()[1]
@@ -180,7 +215,7 @@ function M.absorb(popup)
return
end
- git.cli.absorb.verbose.base(commit).and_rebase.call()
+ git.cli.absorb.verbose.base(commit .. "^").and_rebase.env({ GIT_SEQUENCE_EDITOR = ":" }).call()
end
return M
diff --git a/lua/neogit/popups/commit/init.lua b/lua/neogit/popups/commit/init.lua
index 6b9223519..fa5fd8a9f 100644
--- a/lua/neogit/popups/commit/init.lua
+++ b/lua/neogit/popups/commit/init.lua
@@ -8,7 +8,7 @@ function M.create(env)
.builder()
:name("NeogitCommitPopup")
:switch("a", "all", "Stage all modified and deleted files")
- :switch("e", "allow-empty", "Allow empty commit")
+ :switch("e", "allow-empty", "Allow empty commit", { persisted = false })
:switch("v", "verbose", "Show diff of changes to be committed")
:switch("h", "no-verify", "Disable hooks")
:switch("R", "reset-author", "Claim authorship and reset author date")
@@ -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 39d3df266..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
@@ -56,7 +81,7 @@ end
function M.worktree(popup)
popup:close()
- diffview.open()
+ diffview.open("worktree")
end
function M.staged(popup)
@@ -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
@@ -81,10 +106,9 @@ end
function M.commit(popup)
popup:close()
- local options =
- util.merge(git.branch.get_all_branches(), git.tag.list(), { "HEAD", "ORIG_HEAD", "FETCH_HEAD" })
+ 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 17d1c8746..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
@@ -121,11 +118,11 @@ end
function M.fetch_submodules(_)
notification.info("Fetching submodules")
- git.cli.fetch.recurse_submodules().verbose().jobs(4).call()
+ git.cli.fetch.recurse_submodules.verbose.jobs(4).call()
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/fetch/init.lua b/lua/neogit/popups/fetch/init.lua
index 2a3371eb6..8fa17f70e 100644
--- a/lua/neogit/popups/fetch/init.lua
+++ b/lua/neogit/popups/fetch/init.lua
@@ -10,7 +10,7 @@ function M.create()
:name("NeogitFetchPopup")
:switch("p", "prune", "Prune deleted branches")
:switch("t", "tags", "Fetch all tags")
- :switch("F", "force", "force")
+ :switch("F", "force", "force", { persisted = false })
:group_heading("Fetch from")
:action("p", git.branch.pushRemote_remote_label(), actions.fetch_pushremote)
:action("u", git.branch.upstream_remote_label(), actions.fetch_upstream)
diff --git a/lua/neogit/popups/help/actions.lua b/lua/neogit/popups/help/actions.lua
index 0249d03df..b46ffe0c5 100644
--- a/lua/neogit/popups/help/actions.lua
+++ b/lua/neogit/popups/help/actions.lua
@@ -21,6 +21,12 @@ local function present(commands)
end
if type(keymap) == "table" and next(keymap) then
+ -- HACK: Remove "za" as listed keymap for toggle action.
+ table.sort(keymap)
+ if name == "Toggle" and keymap[2] == "za" then
+ table.remove(keymap, 2)
+ end
+
return { { name = name, keys = keymap, cmp = table.concat(keymap):lower(), fn = fn } }
else
return { { name = name, keys = {}, cmp = "", fn = fn } }
@@ -75,6 +81,9 @@ M.popups = function(env)
{ "LogPopup", "Log", popups.open("log", function(p)
p(env.log)
end) },
+ { "MarginPopup", "Margin", popups.open("margin", function(p)
+ p(env.margin)
+ end) },
{
"CherryPickPopup",
"Cherry Pick",
@@ -115,10 +124,10 @@ end
M.actions = function()
return present {
{ "Stage", "Stage", NONE },
- { "StageUnstaged", "Stage-Unstaged", NONE },
+ { "StageUnstaged", "Stage unstaged", NONE },
{ "StageAll", "Stage all", NONE },
{ "Unstage", "Unstage", NONE },
- { "UnstageStaged", "Unstage-Staged", NONE },
+ { "UnstageStaged", "Unstage all", NONE },
{ "Discard", "Discard", NONE },
{ "Untrack", "Untrack", NONE },
}
diff --git a/lua/neogit/popups/ignore/actions.lua b/lua/neogit/popups/ignore/actions.lua
index bdcbca280..ebbd3b3f3 100644
--- a/lua/neogit/popups/ignore/actions.lua
+++ b/lua/neogit/popups/ignore/actions.lua
@@ -33,17 +33,16 @@ local function add_rules(path, rules)
end
function M.shared_toplevel(popup)
- local ignore_file = Path:new(git.repo.git_root, ".gitignore")
- local rules = make_rules(popup, git.repo.git_root)
+ local ignore_file = Path:new(git.repo.worktree_root, ".gitignore")
+ local rules = make_rules(popup, git.repo.worktree_root)
add_rules(ignore_file, rules)
end
function M.shared_subdirectory(popup)
- local subdirectory = input.get_user_input("Ignore sub-directory", { completion = "dir" })
- if subdirectory then
- subdirectory = Path:new(vim.uv.cwd(), subdirectory)
-
+ local choice = input.get_user_input("Ignore sub-directory", { completion = "dir" })
+ if choice then
+ local subdirectory = Path:new(vim.uv.cwd(), choice)
local ignore_file = subdirectory:joinpath(".gitignore")
local rules = make_rules(popup, tostring(subdirectory))
@@ -53,14 +52,14 @@ end
function M.private_local(popup)
local ignore_file = git.repo:git_path("info", "exclude")
- local rules = make_rules(popup, git.repo.git_root)
+ local rules = make_rules(popup, git.repo.worktree_root)
add_rules(ignore_file, rules)
end
function M.private_global(popup)
local ignore_file = Path:new(git.config.get_global("core.excludesfile"):read())
- local rules = make_rules(popup, git.repo.git_root)
+ local rules = make_rules(popup, git.repo.worktree_root)
add_rules(ignore_file, rules)
end
diff --git a/lua/neogit/popups/ignore/init.lua b/lua/neogit/popups/ignore/init.lua
index 7b624a461..d359334f8 100644
--- a/lua/neogit/popups/ignore/init.lua
+++ b/lua/neogit/popups/ignore/init.lua
@@ -22,7 +22,7 @@ function M.create(env)
"g",
string.format(
"privately for all repositories (%s)",
- "~/" .. Path:new(excludesFile:read()):make_relative(vim.loop.os_homedir())
+ "~/" .. Path:new(excludesFile:read()):make_relative(vim.uv.os_homedir())
),
actions.private_global
)
diff --git a/lua/neogit/popups/log/actions.lua b/lua/neogit/popups/log/actions.lua
index 8bf49a835..ebc5c836d 100644
--- a/lua/neogit/popups/log/actions.lua
+++ b/lua/neogit/popups/log/actions.lua
@@ -32,14 +32,14 @@ local function fetch_more_commits(popup, flags)
end
end
--- TODO: Handle when head is detached
function M.log_current(popup)
LogViewBuffer.new(
commits(popup, {}),
popup:get_internal_arguments(),
popup.state.env.files,
fetch_more_commits(popup, {}),
- "Commits in " .. git.branch.current()
+ "Commits in " .. (git.branch.current() or ("(detached) " .. git.log.message("HEAD"))),
+ git.remote.list()
):open()
end
@@ -50,7 +50,8 @@ function M.log_related(popup)
popup:get_internal_arguments(),
popup.state.env.files,
fetch_more_commits(popup, flags),
- "Commits in " .. table.concat(flags, ", ")
+ "Commits in " .. table.concat(flags, ", "),
+ git.remote.list()
):open()
end
@@ -61,7 +62,8 @@ function M.log_head(popup)
popup:get_internal_arguments(),
popup.state.env.files,
fetch_more_commits(popup, flags),
- "Commits in HEAD"
+ "Commits in HEAD",
+ git.remote.list()
):open()
end
@@ -72,7 +74,8 @@ function M.log_local_branches(popup)
popup:get_internal_arguments(),
popup.state.env.files,
fetch_more_commits(popup, flags),
- "Commits in --branches"
+ "Commits in --branches",
+ git.remote.list()
):open()
end
@@ -86,7 +89,8 @@ function M.log_other(popup)
popup:get_internal_arguments(),
popup.state.env.files,
fetch_more_commits(popup, flags),
- "Commits in " .. branch
+ "Commits in " .. branch,
+ git.remote.list()
):open()
end
end
@@ -98,7 +102,8 @@ function M.log_all_branches(popup)
popup:get_internal_arguments(),
popup.state.env.files,
fetch_more_commits(popup, flags),
- "Commits in --branches --remotes"
+ "Commits in --branches --remotes",
+ git.remote.list()
):open()
end
@@ -109,7 +114,8 @@ function M.log_all_references(popup)
popup:get_internal_arguments(),
popup.state.env.files,
fetch_more_commits(popup, flags),
- "Commits in --all"
+ "Commits in --all",
+ git.remote.list()
):open()
end
@@ -141,10 +147,13 @@ function M.limit_to_files()
return ""
end
- local files = FuzzyFinderBuffer.new(git.files.all_tree()):open_async {
+ local eventignore = vim.o.eventignore
+ vim.o.eventignore = "WinLeave"
+ local files = FuzzyFinderBuffer.new(git.files.all_tree { with_dir = true }):open_async {
allow_multi = true,
refocus_status = false,
}
+ vim.o.eventignore = eventignore
if not files or vim.tbl_isempty(files) then
popup.state.env.files = nil
diff --git a/lua/neogit/popups/log/init.lua b/lua/neogit/popups/log/init.lua
index 64ffa55ab..700cb2495 100644
--- a/lua/neogit/popups/log/init.lua
+++ b/lua/neogit/popups/log/init.lua
@@ -62,10 +62,10 @@ function M.create()
enabled = true,
internal = true,
incompatible = { "reverse" },
- dependant = { "color" },
+ dependent = { "color" },
})
:switch_if(
- config.values.graph_style == "ascii",
+ config.values.graph_style == "ascii" or config.values.graph_style == "kitty",
"c",
"color",
"Show graph in color",
diff --git a/lua/neogit/popups/margin/actions.lua b/lua/neogit/popups/margin/actions.lua
new file mode 100644
index 000000000..8ab733736
--- /dev/null
+++ b/lua/neogit/popups/margin/actions.lua
@@ -0,0 +1,32 @@
+local M = {}
+
+local state = require("neogit.lib.state")
+local a = require("plenary.async")
+
+function M.refresh_buffer(buffer)
+ return a.void(function()
+ buffer:dispatch_refresh({ update_diffs = { "*:*" } }, "margin_refresh_buffer")
+ end)
+end
+
+function M.toggle_visibility()
+ local visibility = state.get({ "margin", "visibility" }, false)
+ local new_visibility = not visibility
+ state.set({ "margin", "visibility" }, new_visibility)
+end
+
+function M.cycle_date_style()
+ local styles = { "relative_short", "relative_long", "local_datetime" }
+ local current_index = state.get({ "margin", "date_style" }, #styles)
+ local next_index = (current_index % #styles) + 1 -- wrap around to the first style
+
+ state.set({ "margin", "date_style" }, next_index)
+end
+
+function M.toggle_details()
+ local details = state.get({ "margin", "details" }, false)
+ local new_details = not details
+ state.set({ "margin", "details" }, new_details)
+end
+
+return M
diff --git a/lua/neogit/popups/margin/init.lua b/lua/neogit/popups/margin/init.lua
new file mode 100644
index 000000000..272838737
--- /dev/null
+++ b/lua/neogit/popups/margin/init.lua
@@ -0,0 +1,61 @@
+local popup = require("neogit.lib.popup")
+local config = require("neogit.config")
+local actions = require("neogit.popups.margin.actions")
+
+local M = {}
+
+-- TODO: Implement various flags/switches
+
+function M.create(env)
+ local p = popup
+ .builder()
+ :name("NeogitMarginPopup")
+ -- :option("n", "max-count", "256", "Limit number of commits", { default = "256", key_prefix = "-" })
+ :switch(
+ "o",
+ config.values.commit_order,
+ "Order commits by",
+ {
+ cli_suffix = "-order",
+ options = {
+ { display = "", value = "" },
+ { display = "topo", value = "topo" },
+ { display = "author-date", value = "author-date" },
+ { display = "date", value = "date" },
+ },
+ }
+ )
+ -- :switch("g", "graph", "Show graph", {
+ -- enabled = true,
+ -- internal = true,
+ -- incompatible = { "reverse" },
+ -- dependent = { "color" },
+ -- })
+ -- :switch_if(
+ -- config.values.graph_style == "ascii" or config.values.graph_style == "kitty",
+ -- "c",
+ -- "color",
+ -- "Show graph in color",
+ -- { internal = true, incompatible = { "reverse" } }
+ -- )
+ :switch(
+ "d",
+ "decorate",
+ "Show refnames",
+ { enabled = true, internal = true }
+ )
+ :group_heading("Refresh")
+ :action_if(env.buffer, "g", "buffer", actions.refresh_buffer(env.buffer), { persist_popup = true })
+ :new_action_group("Margin")
+ :action("L", "toggle visibility", actions.toggle_visibility, { persist_popup = true })
+ :action("l", "cycle style", actions.cycle_date_style, { persist_popup = true })
+ :action("d", "toggle details", actions.toggle_details, { persist_popup = true })
+ :action("x", "toggle shortstat", actions.log_current, { persist_popup = true })
+ :build()
+
+ p:show()
+
+ return p
+end
+
+return M
diff --git a/lua/neogit/popups/merge/actions.lua b/lua/neogit/popups/merge/actions.lua
index b9b94b208..41dd9e58a 100644
--- a/lua/neogit/popups/merge/actions.lua
+++ b/lua/neogit/popups/merge/actions.lua
@@ -16,10 +16,17 @@ function M.abort()
end
end
-function M.merge(popup)
+---@param popup PopupData
+---@return string[]
+local function get_refs(popup)
local refs = util.merge({ popup.state.env.commit }, git.refs.list_branches(), git.refs.list_tags())
+ util.remove_item_from_table(refs, git.branch.current())
+
+ return refs
+end
- local ref = FuzzyFinderBuffer.new(refs):open_async()
+function M.merge(popup)
+ local ref = FuzzyFinderBuffer.new(get_refs(popup)):open_async { prompt_prefix = "Merge" }
if ref then
local args = popup:get_arguments()
table.insert(args, "--no-edit")
@@ -28,9 +35,7 @@ function M.merge(popup)
end
function M.squash(popup)
- local refs = util.merge({ popup.state.env.commit }, git.refs.list_branches(), git.refs.list_tags())
-
- local ref = FuzzyFinderBuffer.new(refs):open_async()
+ local ref = FuzzyFinderBuffer.new(get_refs(popup)):open_async { prompt_prefix = "Squash" }
if ref then
local args = popup:get_arguments()
table.insert(args, "--squash")
@@ -39,9 +44,7 @@ function M.squash(popup)
end
function M.merge_edit(popup)
- local refs = util.merge({ popup.state.env.commit }, git.refs.list_branches(), git.refs.list_tags())
-
- local ref = FuzzyFinderBuffer.new(refs):open_async()
+ local ref = FuzzyFinderBuffer.new(get_refs(popup)):open_async { prompt_prefix = "Merge" }
if ref then
local args = popup:get_arguments()
table.insert(args, "--edit")
@@ -55,9 +58,7 @@ function M.merge_edit(popup)
end
function M.merge_nocommit(popup)
- local refs = util.merge({ popup.state.env.commit }, git.refs.list_branches(), git.refs.list_tags())
-
- local ref = FuzzyFinderBuffer.new(refs):open_async()
+ local ref = FuzzyFinderBuffer.new(get_refs(popup)):open_async { prompt_prefix = "Merge" }
if ref then
local args = popup:get_arguments()
table.insert(args, "--no-commit")
@@ -69,4 +70,5 @@ function M.merge_nocommit(popup)
git.merge.merge(ref, args)
end
end
+
return M
diff --git a/lua/neogit/popups/pull/actions.lua b/lua/neogit/popups/pull/actions.lua
index b2060e13a..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,13 +26,18 @@ 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 })
+ if res.code == 128 then
+ notification.info(table.concat(res.stdout, "\n"))
+ return
+ end
end
end
@@ -81,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 dcba8a281..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" },
@@ -21,13 +21,14 @@ function M.create()
},
})
:switch("f", "ff-only", "Fast-forward only")
- :switch("r", "rebase", "Rebase local commits")
+ :switch("r", "rebase", "Rebase local commits", { persisted = false })
:switch("a", "autostash", "Autostash")
:switch("t", "tags", "Fetch tags")
- :group_heading_if(current, "Pull into " .. current .. " from")
- :group_heading_if(not current, "Pull from")
- :action_if(current, "p", git.branch.pushRemote_label(), actions.from_pushremote)
- :action_if(current, "u", git.branch.upstream_label(), actions.from_upstream)
+ :switch("F", "force", "Force", { persisted = false })
+ :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 03e782a12..26fd6df56 100644
--- a/lua/neogit/popups/push/actions.lua
+++ b/lua/neogit/popups/push/actions.lua
@@ -3,15 +3,22 @@ local git = require("neogit.lib.git")
local logger = require("neogit.logger")
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 {}
- if opts.set_upstream then
+ if opts.set_upstream or git.push.auto_setup_remote(branch) then
table.insert(args, "--set-upstream")
end
@@ -40,22 +47,22 @@ local function push_to(args, remote, branch, opts)
local using_force = vim.tbl_contains(args, "--force") or vim.tbl_contains(args, "--force-with-lease")
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
- if res and res.code ~= 0 and not using_force and updates_rejected then
+ -- Only ask the user whether to force push if not already specified and feature enabled
+ 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?"
+ local message = "Your branch has diverged from the remote branch. Do you want to force push with lease?"
if input.get_permission(message) then
- table.insert(args, "--force")
+ table.insert(args, "--force-with-lease")
res = git.push.push_interactive(remote, branch, args)
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 })
@@ -105,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
@@ -123,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
@@ -133,23 +133,67 @@ function M.push_other(popup)
push_to(popup:get_arguments(), remote, source .. ":" .. destination)
end
-function M.push_tags(popup)
+---@param prompt string
+---@return string|nil
+local function choose_remote(prompt)
local remotes = git.remote.list()
-
local remote
if #remotes == 1 then
remote = remotes[1]
else
- remote = FuzzyFinderBuffer.new(remotes):open_async { prompt_prefix = "push tags to" }
+ remote = FuzzyFinderBuffer.new(remotes):open_async { prompt_prefix = prompt }
+ end
+
+ return remote
+end
+
+---@param popup PopupData
+function M.push_a_tag(popup)
+ local tags = git.tag.list()
+
+ local tag = FuzzyFinderBuffer.new(tags):open_async { prompt_prefix = "Push tag" }
+ if not tag then
+ return
end
+ local remote = choose_remote(("Push %s to remote"):format(tag))
+ if remote then
+ push_to({ tag, unpack(popup:get_arguments()) }, remote)
+ end
+end
+
+---@param popup PopupData
+function M.push_all_tags(popup)
+ local remote = choose_remote("Push tags to remote")
if remote then
push_to({ "--tags", unpack(popup:get_arguments()) }, remote)
end
end
+---@param popup PopupData
+function M.matching_branches(popup)
+ local remote = choose_remote("Push matching branches to")
+ if remote then
+ push_to({ "-v", unpack(popup:get_arguments()) }, remote, ":")
+ end
+end
+
+---@param popup PopupData
+function M.explicit_refspec(popup)
+ local remote = choose_remote("Push to remote")
+ if not remote then
+ return
+ end
+
+ local options = util.merge({ "HEAD" }, git.refs.list_local_branches())
+ local refspec = FuzzyFinderBuffer.new(options):open_async { prompt_prefix = "Push refspec" }
+ if refspec then
+ push_to({ "-v", unpack(popup:get_arguments()) }, remote, refspec)
+ end
+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 b5644a4ee..b4357a2f8 100644
--- a/lua/neogit/popups/push/init.lua
+++ b/lua/neogit/popups/push/init.lua
@@ -5,30 +5,39 @@ 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()
:name("NeogitPushPopup")
- :switch("f", "force-with-lease", "Force with lease")
- :switch("F", "force", "Force")
- :switch("u", "set-upstream", "Set the upstream before pushing")
+ :switch("f", "force-with-lease", "Force with lease", { persisted = false })
+ :switch("F", "force", "Force", { persisted = false })
:switch("h", "no-verify", "Disable hooks")
:switch("d", "dry-run", "Dry run")
- :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("u", "set-upstream", "Set the upstream before pushing")
+ :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 refspecs")
- :action("m", "matching branches")
- :action("T", "a tag")
- :action("t", "all tags", actions.push_tags)
+ :action("r", "explicit refspec", actions.explicit_refspec)
+ :action("m", "matching branches", actions.matching_branches)
+ :action("T", "a tag", actions.push_a_tag)
+ :action("t", "all tags", actions.push_all_tags)
: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 dd1fcb0b2..4076dd0dc 100644
--- a/lua/neogit/popups/rebase/actions.lua
+++ b/lua/neogit/popups/rebase/actions.lua
@@ -9,7 +9,7 @@ local FuzzyFinderBuffer = require("neogit.buffers.fuzzy_finder")
local M = {}
local function base_commit(popup, list, header)
- return popup.state.env.commit or CommitSelectViewBuffer.new(list, header):open_async()[1]
+ return popup.state.env.commit or CommitSelectViewBuffer.new(list, git.remote.list(), header):open_async()[1]
end
function M.onto_base(popup)
@@ -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)
@@ -80,6 +75,7 @@ function M.interactively(popup)
elseif choice == "s" then
popup.state.env.commit = nil
M.interactively(popup)
+ return
else
return
end
@@ -136,14 +132,23 @@ function M.subset(popup)
else
start = CommitSelectViewBuffer.new(
git.log.list { "HEAD" },
+ git.remote.list(),
"Select a commit with to rebase it and commits above it onto " .. newbase .. ", or to abort"
)
:open_async()[1]
end
+ if not start then
+ return
+ end
- if start then
- git.rebase.onto(start, newbase, popup:get_arguments())
+ local args = popup:get_arguments()
+ local parent = git.log.parent(start)
+ if parent then
+ start = start .. "^"
+ else
+ table.insert(args, "--root")
end
+ git.rebase.onto(start, newbase, args)
end
function M.continue()
diff --git a/lua/neogit/popups/rebase/init.lua b/lua/neogit/popups/rebase/init.lua
index 63125f9ce..e704f52bf 100644
--- a/lua/neogit/popups/rebase/init.lua
+++ b/lua/neogit/popups/rebase/init.lua
@@ -35,7 +35,7 @@ function M.create(env)
:action_if(not in_rebase, "p", git.branch.pushRemote_label(), actions.onto_pushRemote)
:action_if(not in_rebase, "u", git.branch.upstream_label(), actions.onto_upstream)
:action_if(not in_rebase, "e", "elsewhere", actions.onto_elsewhere)
- :action_if(not in_rebase and show_base_branch, "b", base_branch, actions.onto_base)
+ :action_if(not in_rebase and show_base_branch, "b", base_branch or "", actions.onto_base)
:new_action_group_if(not in_rebase, "Rebase")
:action_if(not in_rebase, "i", "interactively", actions.interactively)
:action_if(not in_rebase, "s", "a subset", actions.subset)
diff --git a/lua/neogit/popups/remote/actions.lua b/lua/neogit/popups/remote/actions.lua
index 7e82f6cad..06609bec7 100644
--- a/lua/neogit/popups/remote/actions.lua
+++ b/lua/neogit/popups/remote/actions.lua
@@ -26,14 +26,16 @@ function M.add(popup)
return
end
- local origin = git.config.get("remote.origin.url").value
- local host, _, remote = origin:match("([^:/]+)[:/]([^/]+)/(.+)")
-
- remote = remote and remote:gsub("%.git$", "")
-
local msg
- if host and remote then
- msg = string.format("%s:%s/%s.git", host, name, remote)
+ local origin = git.config.get("remote.origin.url"):read()
+ if origin then
+ assert(type(origin) == "string", "remote.origin.url isn't a string")
+ local host, _, remote = origin:match("([^:/]+)[:/]([^/]+)/(.+)")
+ remote = remote and remote:gsub("%.git$", "")
+
+ if host and remote then
+ msg = string.format("%s:%s/%s.git", host, name, remote)
+ end
end
local remote_url = input.get_user_input("URL for " .. name, { default = msg })
@@ -52,12 +54,21 @@ function M.add(popup)
else
notification.info("Added remote " .. name)
end
+
+ if input.get_permission("Fetch refs from " .. name .. "?") then
+ git.fetch.fetch_interactive(name, "", { "--tags" })
+ end
end
end
function M.rename(_)
- local selected_remote = FuzzyFinderBuffer.new(git.remote.list())
- :open_async { prompt_prefix = "Rename remote" }
+ local options = git.remote.list()
+ if #options == 0 then
+ notification.info("No remotes found")
+ return
+ end
+
+ local selected_remote = FuzzyFinderBuffer.new(options):open_async { prompt_prefix = "Rename remote" }
if not selected_remote then
return
end
@@ -74,7 +85,13 @@ function M.rename(_)
end
function M.remove(_)
- local selected_remote = FuzzyFinderBuffer.new(git.remote.list()):open_async()
+ local options = git.remote.list()
+ if #options == 0 then
+ notification.info("No remotes found")
+ return
+ end
+
+ local selected_remote = FuzzyFinderBuffer.new(options):open_async { prompt_prefix = "Remove remote" }
if not selected_remote then
return
end
@@ -86,22 +103,34 @@ function M.remove(_)
end
function M.configure(_)
- local remote_name = FuzzyFinderBuffer.new(git.remote.list()):open_async()
+ local options = git.remote.list()
+ if #options == 0 then
+ notification.info("No remotes found")
+ return
+ end
+
+ local remote_name = FuzzyFinderBuffer.new(options):open_async()
if not remote_name then
return
end
- RemoteConfigPopup.create(remote_name)
+ RemoteConfigPopup.create { remote = remote_name }
end
function M.prune_branches(_)
- local selected_remote = FuzzyFinderBuffer.new(git.remote.list()):open_async()
+ local options = git.remote.list()
+ if #options == 0 then
+ notification.info("No remotes found")
+ return
+ end
+
+ local selected_remote = FuzzyFinderBuffer.new(options):open_async()
if not selected_remote then
return
end
- notification.info("Pruning remote " .. selected_remote)
git.remote.prune(selected_remote)
+ notification.info("Pruned remote " .. selected_remote)
end
-- https://github.com/magit/magit/blob/main/lisp/magit-remote.el#L159
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..248874d93 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
@@ -78,11 +80,31 @@ function M.a_file(popup)
end
local files = FuzzyFinderBuffer.new(files):open_async { allow_multi = true }
- if not files[1] then
- return
+ if files and files[1] then
+ if git.reset.file(target, files) then
+ 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 })
+ else
+ notification.error("Reset Failed")
+ end
+ end
+end
+
+---@param popup PopupData
+function M.a_branch(popup)
+ -- branch reset expects commits to be set, not commit
+ if popup.state.env.commit then
+ popup.state.env.commits = { popup.state.env.commit }
+ popup.state.env.commit = nil
end
- git.reset.file(target, files)
+ local branch_actions = require("neogit.popups.branch.actions")
+ branch_actions.reset_branch(popup)
end
return M
diff --git a/lua/neogit/popups/reset/init.lua b/lua/neogit/popups/reset/init.lua
index d723c3a3f..82535122f 100644
--- a/lua/neogit/popups/reset/init.lua
+++ b/lua/neogit/popups/reset/init.lua
@@ -1,6 +1,5 @@
local popup = require("neogit.lib.popup")
local actions = require("neogit.popups.reset.actions")
-local branch_actions = require("neogit.popups.branch.actions")
local M = {}
@@ -10,7 +9,7 @@ function M.create(env)
:name("NeogitResetPopup")
:group_heading("Reset")
:action("f", "file", actions.a_file)
- :action("b", "branch", branch_actions.reset_branch)
+ :action("b", "branch", actions.a_branch)
:new_action_group("Reset this")
:action("m", "mixed (HEAD and index)", actions.mixed)
:action("s", "soft (HEAD only)", actions.soft)
diff --git a/lua/neogit/popups/revert/actions.lua b/lua/neogit/popups/revert/actions.lua
index bf0a2638a..2ecd010c5 100644
--- a/lua/neogit/popups/revert/actions.lua
+++ b/lua/neogit/popups/revert/actions.lua
@@ -1,53 +1,41 @@
local M = {}
+local config = require("neogit.config")
local git = require("neogit.lib.git")
local client = require("neogit.client")
local notification = require("neogit.lib.notification")
-local CommitSelectViewBuffer = require("neogit.buffers.commit_select_view")
+local input = require("neogit.lib.input")
+local util = require("neogit.lib.util")
+local FuzzyFinderBuffer = require("neogit.buffers.fuzzy_finder")
---@param popup any
----@return CommitLogEntry[]
-local function get_commits(popup)
- local commits
- if #popup.state.env.commits > 0 then
- commits = popup.state.env.commits
+---@param thing string
+---@return string[]
+local function get_commits(popup, thing)
+ if #popup.state.env.commits > 1 then
+ return popup.state.env.commits
else
- commits = CommitSelectViewBuffer.new(
- git.log.list { "--max-count=256" },
- "Select one or more commits to revert with , or to abort"
- ):open_async()
- end
-
- return commits or {}
-end
+ local refs =
+ util.merge(popup.state.env.commits, git.refs.list_branches(), git.refs.list_tags(), git.refs.heads())
-local function build_commit_message(commits)
- local message = {}
- table.insert(message, string.format("Revert %d commits\n", #commits))
-
- for _, commit in ipairs(commits) do
- table.insert(message, string.format("%s '%s'", commit:sub(1, 7), git.log.message(commit)))
+ return { FuzzyFinderBuffer.new(refs):open_async { prompt_prefix = "Revert " .. thing } }
end
-
- return table.concat(message, "\n")
end
function M.commits(popup)
- local commits = get_commits(popup)
+ local commits = get_commits(popup, "commits")
if #commits == 0 then
return
end
local args = popup:get_arguments()
-
- local success = git.revert.commits(commits, args)
-
+ local success, msg = git.revert.commits(commits, args)
if not success then
- notification.error("Revert failed. Resolve conflicts before continuing")
+ notification.error("Revert failed with " .. msg)
return
end
- local commit_cmd = git.cli.commit.no_verify.with_message(build_commit_message(commits))
+ local commit_cmd = git.cli.commit.no_verify
if vim.tbl_contains(args, "--edit") then
commit_cmd = commit_cmd.edit
else
@@ -56,19 +44,26 @@ function M.commits(popup)
client.wrap(commit_cmd, {
autocmd = "NeogitRevertComplete",
+ interactive = true,
msg = {
success = "Reverted",
},
+ show_diff = config.values.commit_editor.show_staged_diff,
})
end
function M.changes(popup)
- local commits = get_commits(popup)
- if not commits[1] then
- return
+ local commits = get_commits(popup, "changes")
+ if #commits > 0 then
+ local success, msg = git.revert.commits(commits, popup:get_arguments())
+ if not success then
+ notification.error("Revert failed with " .. msg)
+ end
end
+end
- git.revert.commits(commits, popup:get_arguments())
+function M.hunk(popup)
+ git.revert.hunk(popup:get_env("hunk"), popup:get_arguments())
end
function M.continue()
@@ -80,7 +75,9 @@ function M.skip()
end
function M.abort()
- git.revert.abort()
+ if input.get_permission("Abort revert?") then
+ git.revert.abort()
+ end
end
return M
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 7db0d3c21..288d82e6e 100644
--- a/lua/neogit/popups/stash/actions.lua
+++ b/lua/neogit/popups/stash/actions.lua
@@ -10,12 +10,12 @@ function M.both(popup)
git.stash.stash_all(popup:get_arguments())
end
-function M.index(popup)
- git.stash.stash_index(popup:get_arguments())
+function M.index()
+ git.stash.stash_index()
end
-function M.keep_index(popup)
- git.stash.stash_keep_index(popup:get_arguments())
+function M.keep_index()
+ git.stash.stash_keep_index()
end
function M.push(popup)
@@ -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
@@ -72,7 +75,6 @@ function M.rename(popup)
use("rename", popup.state.env.stash)
end
---- git stash list
function M.list()
StashListBuffer.new(git.repo.state.stashes.items):open()
end
diff --git a/lua/neogit/popups/stash/init.lua b/lua/neogit/popups/stash/init.lua
index 1dea0d466..9b461c9a0 100644
--- a/lua/neogit/popups/stash/init.lua
+++ b/lua/neogit/popups/stash/init.lua
@@ -4,13 +4,15 @@ local popup = require("neogit.lib.popup")
local M = {}
function M.create(stash)
- -- TODO:
- -- :switch("u", "include-untracked", "Also save untracked files")
- -- :switch("a", "all", "Also save untracked and ignored files")
-
local p = popup
.builder()
:name("NeogitStashPopup")
+ :switch("u", "include-untracked", "Also save untracked files", {
+ incompatible = { "all" },
+ })
+ :switch("a", "all", "Also save untracked and ignored files", {
+ incompatible = { "include-untracked" },
+ })
:group_heading("Stash")
:action("z", "both", actions.both)
:action("i", "index", actions.index)
diff --git a/lua/neogit/popups/tag/actions.lua b/lua/neogit/popups/tag/actions.lua
index 363a8c374..c93b35307 100644
--- a/lua/neogit/popups/tag/actions.lua
+++ b/lua/neogit/popups/tag/actions.lua
@@ -6,13 +6,14 @@ 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 event = require("neogit.lib.event")
-local function fire_tag_event(pattern, data)
- vim.api.nvim_exec_autocmds("User", { pattern = pattern, modeline = false, data = data })
-end
-
+---@param popup PopupData
function M.create_tag(popup)
- local tag_input = input.get_user_input("Create tag", { strip_spaces = true })
+ local tag_input = input.get_user_input("Create tag", {
+ strip_spaces = true,
+ completion = "customlist,v:lua.require'neogit.lib.git'.refs.list_tags",
+ })
if not tag_input then
return
end
@@ -36,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
@@ -48,7 +50,7 @@ function M.create_release(_) end
--- If there are multiple tags then offer to delete those.
--- Otherwise prompt for a single tag to be deleted.
--- git tag -d TAGS
----@param _ table
+---@param _ PopupData
function M.delete(_)
local tags = FuzzyFinderBuffer.new(git.tag.list()):open_async { allow_multi = true }
if #(tags or {}) == 0 then
@@ -58,28 +60,27 @@ 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
--- Prunes differing tags from local and remote
----@param _ table
+---@param _ PopupData
function M.prune(_)
+ local tags = git.tag.list()
+ if #tags == 0 then
+ notification.info("No tags found")
+ return
+ end
+
local selected_remote = FuzzyFinderBuffer.new(git.remote.list()):open_async {
prompt_prefix = "Prune tags using remote",
}
-
if (selected_remote or "") == "" then
return
end
- local tags = git.tag.list()
- if #tags == 0 then
- notification.info("No tags found")
- return
- end
-
notification.info("Fetching remote tags...")
local r_out = git.tag.list_remote(selected_remote)
local remote_tags = {}
@@ -96,7 +97,7 @@ function M.prune(_)
notification.delete_all()
if #l_tags == 0 and #r_tags == 0 then
- notification.info("Same tags exist locally and remotely")
+ notification.info("Tags are in sync - nothing to do.")
return
end
diff --git a/lua/neogit/popups/tag/init.lua b/lua/neogit/popups/tag/init.lua
index 994a32a4b..c7bbef5e7 100644
--- a/lua/neogit/popups/tag/init.lua
+++ b/lua/neogit/popups/tag/init.lua
@@ -8,7 +8,7 @@ function M.create(env)
.builder()
:name("NeogitTagPopup")
:arg_heading("Arguments")
- :switch("f", "force", "Force")
+ :switch("f", "force", "Force", { persisted = false })
:switch("a", "annotate", "Annotate")
:switch("s", "sign", "Sign")
:option("u", "local-user", "", "Sign as", { key_prefix = "-" })
diff --git a/lua/neogit/popups/worktree/actions.lua b/lua/neogit/popups/worktree/actions.lua
index 60e58fac8..d0ff7b6fb 100644
--- a/lua/neogit/popups/worktree/actions.lua
+++ b/lua/neogit/popups/worktree/actions.lua
@@ -5,53 +5,85 @@ 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")
-local Path = require("plenary.path")
-local scan_dir = require("plenary.scandir").scan_dir
----Poor man's dired
+---@param prompt string
+---@param branch string?
---@return string|nil
-local function get_path(prompt)
- local dir = Path.new(".")
- repeat
- local dirs = scan_dir(dir:absolute(), { depth = 1, only_dirs = true })
- local selected = FuzzyFinderBuffer.new(util.merge({ ".." }, dirs)):open_async {
- prompt_prefix = prompt,
- }
-
- if not selected then
- return
- end
-
- if vim.startswith(selected, "/") then
- dir = Path.new(selected)
+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
- dir = dir:joinpath(selected)
+ return path
end
- until not dir:exists()
+ else
+ return nil
+ end
+end
- local path, _ = dir:absolute():gsub("%s", "_")
- return path
+---@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
-function M.checkout_worktree()
+---@param prompt string
+---@return string|nil
+local function get_ref(prompt)
local options = util.merge(git.refs.list_branches(), git.refs.list_tags(), git.refs.heads())
- local selected = FuzzyFinderBuffer.new(options):open_async { prompt_prefix = "checkout" }
+ return FuzzyFinderBuffer.new(options):open_async { prompt_prefix = prompt }
+end
+
+function M.checkout_worktree()
+ local selected = get_ref("checkout")
if not selected then
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
- if git.worktree.add(selected, path) then
+ 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
end
@@ -61,9 +93,7 @@ function M.create_worktree()
return
end
- local options = util.merge(git.refs.list_branches(), git.refs.list_tags(), git.refs.heads())
- local selected = FuzzyFinderBuffer.new(options)
- :open_async { prompt_prefix = "Create and checkout branch starting at" }
+ local selected = get_ref("Create and checkout branch starting at")
if not selected then
return
end
@@ -73,10 +103,19 @@ function M.create_worktree()
return
end
- if git.worktree.add(selected, path, { "-b", name }) then
- notification.info("Added worktree")
- if status.is_open() then
- status.instance():chdir(path)
+ 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
end
end
@@ -135,8 +174,9 @@ function M.delete()
local success = false
if input.get_permission(("Remove worktree at %q?"):format(selected)) then
- if change_dir and status.is_open() then
- status.instance():chdir(git.worktree.main().path)
+ local main = git.worktree.main() -- A bare repo has no main, so check
+ if change_dir and status.is_open() and main then
+ status.instance():chdir(main.path)
end
-- This might produce some error messages that need to get suppressed
@@ -157,9 +197,15 @@ function M.delete()
end
function M.visit()
- local options = vim.tbl_map(function(w)
- return w.path
- end, git.worktree.list())
+ local options = vim
+ .iter(git.worktree.list())
+ :map(function(w)
+ return w.path
+ end)
+ :filter(function(path)
+ return path ~= vim.uv.cwd()
+ end)
+ :totable()
if #options == 0 then
notification.info("No worktrees present")
diff --git a/lua/neogit/process.lua b/lua/neogit/process.lua
index d950808b1..050b98b13 100644
--- a/lua/neogit/process.lua
+++ b/lua/neogit/process.lua
@@ -5,11 +5,15 @@ local config = require("neogit.config")
local logger = require("neogit.logger")
local util = require("neogit.lib.util")
+local ProcessBuffer = require("neogit.buffers.process")
+local Spinner = require("neogit.spinner")
+
local api = vim.api
local fn = vim.fn
-local command_mask =
- vim.pesc(" --no-pager --literal-pathspecs --no-optional-locks -c core.preloadindex=true -c color.ui=always")
+local command_mask = vim.pesc(
+ " --no-pager --literal-pathspecs --no-optional-locks -c core.preloadindex=true -c color.ui=always -c diff.noprefix=false"
+)
local function mask_command(cmd)
local command, _ = cmd:gsub(command_mask, "")
@@ -17,7 +21,6 @@ local function mask_command(cmd)
end
---@class ProcessOpts
----@field buffer ProcessBuffer|nil
---@field cmd string[]
---@field cwd string|nil
---@field env table|nil
@@ -44,6 +47,7 @@ end
---@field on_partial_line fun(process: Process, data: string)|nil callback on complete lines
---@field on_error (fun(res: ProcessResult): boolean) Intercept the error externally, returning false prevents the error from being logged
---@field defer_show_preview_buffers fun(): nil
+---@field spinner Spinner|nil
local Process = {}
Process.__index = Process
@@ -75,6 +79,7 @@ function ProcessResult:trim()
return self
end
+---@return ProcessResult
function ProcessResult:remove_ansi()
self.stdout = vim.tbl_map(remove_ansi_escape_codes, self.stdout)
self.stderr = vim.tbl_map(remove_ansi_escape_codes, self.stderr)
@@ -82,13 +87,21 @@ 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
---@return Process
function Process.new(process)
- process.buffer = require("neogit.buffers.process"):new(process)
-
return setmetatable(process, Process) ---@class Process
end
@@ -103,7 +116,26 @@ function Process.hide_preview_buffers()
end
function Process:show_console()
- self.buffer:show()
+ if self.buffer then
+ self.buffer:show()
+ end
+end
+
+function Process:show_spinner()
+ if not config.values.process_spinner or self.suppress_console or self.spinner then
+ return
+ end
+
+ self.spinner = Spinner.new(mask_command(table.concat(self.cmd, " ")))
+ self.spinner:start()
+end
+
+function Process:hide_spinner()
+ if not self.spinner then
+ return
+ end
+
+ self.spinner:stop()
end
function Process:start_timer()
@@ -112,10 +144,11 @@ function Process:start_timer()
end
if self.timer == nil then
- local timer = vim.loop.new_timer()
+ local timer = vim.uv.new_timer()
self.timer = timer
- local timeout = assert(self.git_hook and 100 or config.values.console_timeout, "no timeout")
+ local timeout = assert(self.git_hook and 800 or config.values.console_timeout, "no timeout")
+
timer:start(
timeout,
0,
@@ -130,16 +163,15 @@ function Process:start_timer()
return
end
- if config.values.auto_show_console then
- self:show_console()
- else
+ if not config.values.auto_show_console then
local message = string.format(
"Command %q running for more than: %.1f seconds",
mask_command(table.concat(self.cmd, " ")),
- math.ceil((vim.loop.now() - self.start) / 100) / 10
+ math.ceil((vim.uv.now() - self.start) / 100) / 10
)
-
notification.warn(message .. "\n\nOpen the command history for details")
+ elseif config.values.auto_show_console_on == "output" then
+ self:show_console()
end
end)
)
@@ -185,7 +217,7 @@ end
function Process:stop()
if self.job then
- fn.jobstop(self.job)
+ assert(fn.jobstop(self.job) == 1, "invalid job id")
end
end
@@ -266,11 +298,15 @@ function Process:spawn(cb)
if self.on_partial_line then
self:on_partial_line(line)
end
+
+ if self.buffer then
+ self.buffer:append_partial(line)
+ end
end
local stdout_on_line = function(line)
insert(res.stdout, line)
- if not self.suppress_console then
+ if self.buffer and not self.suppress_console then
self.buffer:append(line)
end
end
@@ -279,7 +315,7 @@ function Process:spawn(cb)
local stderr_on_line = function(line)
insert(res.stderr, line)
- if not self.suppress_console then
+ if self.buffer and not self.suppress_console then
self.buffer:append(line)
end
end
@@ -289,17 +325,18 @@ function Process:spawn(cb)
local function on_exit(_, code)
res.code = code
- res.time = (vim.loop.now() - start)
+ res.time = (vim.uv.now() - start)
-- Remove self
processes[self.job] = nil
self.result = res
self:stop_timer()
+ self:hide_spinner()
stdout_cleanup()
stderr_cleanup()
- if not self.suppress_console then
+ if self.buffer and not self.suppress_console then
self.buffer:append(string.format("Process exited with code: %d", code))
if not self.buffer:is_visible() and code > 0 and self.on_error(res) then
@@ -309,10 +346,13 @@ function Process:spawn(cb)
insert(output, "> " .. util.remove_ansi_escape_codes(res.stderr[i]))
end
- local message =
- string.format("%s:\n\n%s", mask_command(table.concat(self.cmd, " ")), table.concat(output, "\n"))
-
- notification.warn(message)
+ if not config.values.auto_close_console then
+ local message =
+ string.format("%s:\n\n%s", mask_command(table.concat(self.cmd, " ")), table.concat(output, "\n"))
+ notification.warn(message)
+ elseif config.values.auto_show_console_on == "error" then
+ self.buffer:show()
+ end
end
if
@@ -358,6 +398,8 @@ function Process:spawn(cb)
self.stdin = job
if not hide_console then
+ self.buffer = ProcessBuffer:new(self, mask_command)
+ self:show_spinner()
self:start_timer()
end
@@ -371,6 +413,7 @@ function Process:spawn(cb)
if not self.cmd[#self.cmd] == "-" then
self:send("\04")
end
+
self:close_stdin()
end
diff --git a/lua/neogit/runner.lua b/lua/neogit/runner.lua
index 76d5b3f88..99dbe649d 100644
--- a/lua/neogit/runner.lua
+++ b/lua/neogit/runner.lua
@@ -62,6 +62,16 @@ local function handle_interactive_password(line)
return input.get_secret_user_input(prompt, { cancel = "__CANCEL__" }) or "__CANCEL__"
end
+---@param line string
+---@return string
+local function handle_fatal_error(line)
+ logger.debug("[RUNNER]: Fatal error encountered")
+ local notification = require("neogit.lib.notification")
+
+ notification.error(line)
+ return "__CANCEL__"
+end
+
---@param process Process
---@param line string
---@return boolean
@@ -76,6 +86,8 @@ local function handle_line_interactive(process, line)
handler = handle_interactive_username
elseif line:match("^Enter passphrase") or line:match("^Password for") or line:match("^Enter PIN for") then
handler = handle_interactive_password
+ elseif line:match("^fatal") then
+ handler = handle_fatal_error
end
if handler then
@@ -100,6 +112,7 @@ end
---@param process Process
---@param opts table
+---@return ProcessResult
function M.call(process, opts)
logger.trace(string.format("[RUNNER]: Executing %q", table.concat(process.cmd, " ")))
diff --git a/lua/neogit/spinner.lua b/lua/neogit/spinner.lua
new file mode 100644
index 000000000..9b6203fef
--- /dev/null
+++ b/lua/neogit/spinner.lua
@@ -0,0 +1,66 @@
+local util = require("neogit.lib.util")
+---@class Spinner
+---@field text string
+---@field count number
+---@field interval number
+---@field pattern string[]
+---@field timer uv_timer_t
+local Spinner = {}
+Spinner.__index = Spinner
+
+---@return Spinner
+function Spinner.new(text)
+ local instance = {
+ text = util.str_truncate(text, vim.v.echospace - 2, "..."),
+ interval = 100,
+ count = 0,
+ timer = nil,
+ pattern = {
+ "⠋",
+ "⠙",
+ "⠹",
+ "⠸",
+ "⠼",
+ "⠴",
+ "⠦",
+ "⠧",
+ "⠇",
+ "⠏",
+ },
+ }
+
+ return setmetatable(instance, Spinner)
+end
+
+function Spinner:start()
+ if not self.timer then
+ self.timer = vim.uv.new_timer()
+ self.timer:start(
+ 250,
+ self.interval,
+ vim.schedule_wrap(function()
+ self.count = self.count + 1
+ local step = self.pattern[(self.count % #self.pattern) + 1]
+ vim.cmd(string.format("echo '%s %s' | redraw", step, self.text))
+ end)
+ )
+ end
+end
+
+function Spinner:stop()
+ if self.timer then
+ local timer = self.timer
+ self.timer = nil
+ timer:stop()
+
+ if not timer:is_closing() then
+ timer:close()
+ end
+ end
+
+ vim.schedule(function()
+ vim.cmd("redraw | echomsg ''")
+ end)
+end
+
+return Spinner
diff --git a/lua/neogit/vendor/types.lua b/lua/neogit/vendor/types.lua
new file mode 100644
index 000000000..68b8fb3fa
--- /dev/null
+++ b/lua/neogit/vendor/types.lua
@@ -0,0 +1,19 @@
+--This file exists to facilitate llscheck CI types
+
+---@class Path
+---@field absolute fun(self): boolean
+---@field exists fun(self): boolean
+---@field touch fun(self, opts:table)
+---@field write fun(self, txt:string, flag:string)
+---@field read fun(self): string|nil
+---@field iter fun(self): self
+
+---@class uv_timer_t
+---@field start fun(self, time:number, repeat: number, fn: function)
+---@field stop fun(self)
+---@field is_closing fun(self): boolean
+---@field close fun(self)
+---
+---@class uv_fs_event_t
+---@field start fun(self, path: string, opts: table, callback: function)
+---@field stop fun(self)
diff --git a/lua/neogit/watcher.lua b/lua/neogit/watcher.lua
index 773ca6ba1..ec233a56b 100644
--- a/lua/neogit/watcher.lua
+++ b/lua/neogit/watcher.lua
@@ -1,14 +1,13 @@
-- Adapted from https://github.com/lewis6991/gitsigns.nvim/blob/main/lua/gitsigns/watcher.lua#L103
local logger = require("neogit.logger")
-local Path = require("plenary.path")
local util = require("neogit.lib.util")
local git = require("neogit.lib.git")
local config = require("neogit.config")
local a = require("plenary.async")
---@class Watcher
----@field git_root string
+---@field git_dir string
---@field buffers table
---@field running boolean
---@field fs_event_handler uv_fs_event_t
@@ -20,9 +19,9 @@ Watcher.__index = Watcher
function Watcher.new(root)
local instance = {
buffers = {},
- git_root = Path:new(root):joinpath(".git"):absolute(),
+ git_dir = git.cli.worktree_git_dir(root),
running = false,
- fs_event_handler = assert(vim.loop.new_fs_event()),
+ fs_event_handler = assert(vim.uv.new_fs_event()),
}
setmetatable(instance, Watcher)
@@ -83,9 +82,9 @@ function Watcher:start()
return self
end
- logger.debug("[WATCHER] Watching git dir: " .. self.git_root)
+ logger.debug("[WATCHER] Watching git dir: " .. self.git_dir)
self.running = true
- self.fs_event_handler:start(self.git_root, {}, self:fs_event_callback())
+ self.fs_event_handler:start(self.git_dir, {}, self:fs_event_callback())
return self
end
@@ -99,7 +98,7 @@ function Watcher:stop()
return self
end
- logger.debug("[WATCHER] Stopped watching git dir: " .. self.git_root)
+ logger.debug("[WATCHER] Stopped watching git dir: " .. self.git_dir)
self.running = false
self.fs_event_handler:stop()
return self
diff --git a/plugin/neogit.lua b/plugin/neogit.lua
index 4f0e4b4f7..465eb55b8 100644
--- a/plugin/neogit.lua
+++ b/plugin/neogit.lua
@@ -16,35 +16,27 @@ api.nvim_create_user_command("NeogitResetState", function()
require("neogit.lib.state")._reset()
end, { nargs = "*", desc = "Reset any saved flags" })
-api.nvim_create_user_command(
- "NeogitLogCurrent",
- function(args)
- local action = require("neogit").action
- local path = vim.fn.expand(args.fargs[1] or "%")
+api.nvim_create_user_command("NeogitLogCurrent", function(args)
+ local action = require("neogit").action
+ local path = vim.fn.expand(args.fargs[1] or "%")
- if args.range > 0 then
- action("log", "log_current", { "-L" .. args.line1 .. "," .. args.line2 .. ":" .. path })()
- else
- action("log", "log_current", { "--", path })()
- end
- end,
- {
- nargs = "?",
- desc = "Open git log (current) for specified file, or current file if unspecified. Optionally accepts a range.",
- range = "%",
- complete = "file"
- }
-)
+ if args.range > 0 then
+ action("log", "log_current", { "-L" .. args.line1 .. "," .. args.line2 .. ":" .. path })()
+ else
+ action("log", "log_current", { "--", path })()
+ end
+end, {
+ nargs = "?",
+ desc = "Open git log (current) for specified file, or current file if unspecified. Optionally accepts a range.",
+ range = "%",
+ complete = "file",
+})
-api.nvim_create_user_command(
- "NeogitCommit",
- function(args)
- local commit = args.fargs[1] or "HEAD"
- local CommitViewBuffer = require("neogit.buffers.commit_view")
- CommitViewBuffer.new(commit):open()
- end,
- {
- nargs = "?",
- desc = "Open git commit view for specified commit, or HEAD",
- }
-)
+api.nvim_create_user_command("NeogitCommit", function(args)
+ local commit = args.fargs[1] or "HEAD"
+ local CommitViewBuffer = require("neogit.buffers.commit_view")
+ CommitViewBuffer.new(commit):open()
+end, {
+ nargs = "?",
+ desc = "Open git commit view for specified commit, or HEAD",
+})
diff --git a/spec/buffers/commit_buffer_spec.rb b/spec/buffers/commit_buffer_spec.rb
new file mode 100644
index 000000000..0ada40be2
--- /dev/null
+++ b/spec/buffers/commit_buffer_spec.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe "Commit Buffer", :git, :nvim do
+ before do
+ nvim.keys("ll")
+ end
+
+ it "can close the view with " do
+ nvim.keys("")
+ expect(nvim.filetype).to eq("NeogitLogView")
+ end
+
+ it "can close the view with q" do
+ nvim.keys("q")
+ expect(nvim.filetype).to eq("NeogitLogView")
+ end
+
+ it "can yank OID" do
+ nvim.keys("Y")
+ expect(nvim.screen.last.strip).to match(/\A[a-f0-9]{40}\z/)
+ end
+
+ it "can open the bisect popup" do
+ nvim.keys("B")
+ expect(nvim.filetype).to eq("NeogitPopup")
+ end
+
+ it "can open the branch popup" do
+ nvim.keys("b")
+ expect(nvim.filetype).to eq("NeogitPopup")
+ end
+
+ it "can open the cherry pick popup" do
+ nvim.keys("A")
+ expect(nvim.filetype).to eq("NeogitPopup")
+ end
+
+ it "can open the commit popup" do
+ nvim.keys("c")
+ expect(nvim.filetype).to eq("NeogitPopup")
+ end
+
+ it "can open the diff popup" do
+ nvim.keys("d")
+ expect(nvim.filetype).to eq("NeogitPopup")
+ end
+
+ it "can open the pull popup" do
+ nvim.keys("p")
+ expect(nvim.filetype).to eq("NeogitPopup")
+ end
+
+ it "can open the fetch popup" do
+ nvim.keys("f")
+ expect(nvim.filetype).to eq("NeogitPopup")
+ end
+
+ it "can open the ignore popup" do
+ nvim.keys("i")
+ expect(nvim.filetype).to eq("NeogitPopup")
+ end
+
+ it "can open the log popup" do
+ nvim.keys("l")
+ expect(nvim.filetype).to eq("NeogitPopup")
+ end
+
+ it "can open the remote popup" do
+ nvim.keys("M")
+ expect(nvim.filetype).to eq("NeogitPopup")
+ end
+
+ it "can open the merge popup" do
+ nvim.keys("m")
+ expect(nvim.filetype).to eq("NeogitPopup")
+ end
+
+ it "can open the push popup" do
+ nvim.keys("P")
+ expect(nvim.filetype).to eq("NeogitPopup")
+ end
+
+ it "can open the rebase popup" do
+ nvim.keys("r")
+ expect(nvim.filetype).to eq("NeogitPopup")
+ end
+
+ it "can open the tag popup" do
+ nvim.keys("t")
+ expect(nvim.filetype).to eq("NeogitPopup")
+ end
+
+ it "can open the revert popup" do
+ nvim.keys("v")
+ expect(nvim.filetype).to eq("NeogitPopup")
+ end
+
+ it "can open the worktree popup" do
+ nvim.keys("w")
+ expect(nvim.filetype).to eq("NeogitPopup")
+ end
+
+ it "can open the reset popup" do
+ nvim.keys("X")
+ expect(nvim.filetype).to eq("NeogitPopup")
+ end
+
+ it "can open the stash popup" do
+ nvim.keys("Z")
+ expect(nvim.filetype).to eq("NeogitPopup")
+ end
+end
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/refs_buffer_spec.rb b/spec/buffers/refs_buffer_spec.rb
index d0802440d..50216688a 100644
--- a/spec/buffers/refs_buffer_spec.rb
+++ b/spec/buffers/refs_buffer_spec.rb
@@ -4,14 +4,8 @@
RSpec.describe "Refs Buffer", :git, :nvim do
it "renders, raising no errors" do
- nvim.keys("lr")
+ nvim.keys("y")
expect(nvim.errors).to be_empty
- expect(nvim.filetype).to eq("NeogitReflogView")
- end
-
- it "can open CommitView" do
- nvim.keys("lr")
- expect(nvim.errors).to be_empty
- expect(nvim.filetype).to eq("NeogitCommitView")
+ expect(nvim.filetype).to eq("NeogitRefsView")
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 c6e28b812..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 ",
@@ -46,11 +45,11 @@
end
end
- describe "rebase" do
- end
+ # describe "rebase" do
+ # end
- describe "pullRemote" do
- end
+ # describe "pullRemote" do
+ # end
end
describe "Actions" do
@@ -67,21 +66,21 @@
end
end
- describe "remote.pushDefault" do
- end
+ # describe "remote.pushDefault" do
+ # end
- describe "neogit.baseBranch" do
- end
+ # describe "neogit.baseBranch" do
+ # end
- describe "neogit.askSetPushDefault" do
- end
+ # describe "neogit.askSetPushDefault" do
+ # end
end
- describe "Branch creation" do
- describe "autoSetupMerge" do
- end
-
- describe "autoSetupRebase" do
- end
- end
+ # describe "Branch creation" do
+ # describe "autoSetupMerge" do
+ # end
+ #
+ # describe "autoSetupRebase" do
+ # end
+ # end
end
diff --git a/spec/popups/branch_popup_spec.rb b/spec/popups/branch_popup_spec.rb
index 8089c1d29..fac371074 100644
--- a/spec/popups/branch_popup_spec.rb
+++ b/spec/popups/branch_popup_spec.rb
@@ -3,11 +3,10 @@
require "spec_helper"
RSpec.describe "Branch Popup", :git, :nvim, :popup do
- before { nvim.keys("b") }
-
+ let(:keymap) { "b" }
let(:view) do
[
- " Variables ",
+ " Configure branch ",
" d branch.master.description unset ",
" u branch.master.merge unset ",
" branch.master.remote unset ",
@@ -48,6 +47,25 @@
expect(git.config("branch.#{git.branch.name}.remote")).to eq(".")
expect(git.config("branch.#{git.branch.name}.merge")).to eq("refs/heads/master")
end
+
+ it "unsets both values if already set" do
+ nvim.keys("umaster")
+
+ expect(nvim.screen[8..9]).to eq(
+ [" u branch.master.merge refs/heads/master ",
+ " branch.master.remote . "]
+ )
+
+ nvim.keys("u")
+
+ expect_git_failure { git.config("branch.#{git.branch.name}.remote") }
+ expect_git_failure { git.config("branch.#{git.branch.name}.merge") }
+
+ expect(nvim.screen[8..9]).to eq(
+ [" u branch.master.merge unset ",
+ " branch.master.remote unset "]
+ )
+ end
end
describe "branch..rebase" do
diff --git a/spec/popups/cherry_pick_popup_spec.rb b/spec/popups/cherry_pick_popup_spec.rb
index 5aed593e7..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 ",
@@ -25,5 +24,5 @@
end
%w[-m =s -F -x -e -s -S].each { include_examples "argument", _1 }
- %w[A a].each { include_examples "interaction", _1 }
+ %w[A a m d h].each { include_examples "interaction", _1 }
end
diff --git a/spec/popups/commit_popup_spec.rb b/spec/popups/commit_popup_spec.rb
index 313a2fbf9..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
@@ -149,19 +150,19 @@
end
end
- describe "Fixup" do
- end
+ # describe "Fixup" do
+ # end
- describe "Squash" do
- end
+ # describe "Squash" do
+ # end
- describe "Augment" do
- end
+ # describe "Augment" do
+ # end
- describe "Instant Fixup" do
- end
+ # describe "Instant Fixup" do
+ # end
- describe "Instant Squash" do
- end
+ # describe "Instant Squash" do
+ # end
end
end
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 0e673f10a..1208765d6 100644
--- a/spec/popups/help_popup_spec.rb
+++ b/spec/popups/help_popup_spec.rb
@@ -3,24 +3,23 @@
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 Bisect p Pull S Stage-Unstaged ",
+ " b Branch p Pull s Stage Toggle ",
+ " B Bisect P Push S Stage unstaged ",
" c Commit Q Command u Unstage ",
- " d Diff r Rebase U Unstage-Staged ",
+ " d Diff r Rebase U Unstage all ",
" f Fetch t Tag x Discard ",
- " I Init v Revert ",
- " i Ignore w Worktree ",
- " l Log X Reset ",
- " Z Stash "
+ " i Ignore v Revert ",
+ " I Init w Worktree ",
+ " L Margin X Reset ",
+ " l Log Z Stash "
]
end
- %w[$ A b B c d f i I l M m P p r t v w X Z].each { include_examples "interaction", _1 }
+ %w[$ A b B c d f i I l L M m P p r t v w X Z].each { include_examples "interaction", _1 }
end
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/margin_popup_spec.rb b/spec/popups/margin_popup_spec.rb
new file mode 100644
index 000000000..f834c940f
--- /dev/null
+++ b/spec/popups/margin_popup_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe "Margin Popup", :git, :nvim, :popup do # rubocop:disable RSpec/EmptyExampleGroup
+ let(:keymap) { "L" }
+ let(:view) do
+ [
+ " Arguments ",
+ # " -n Limit number of commits (--max-count=256) ",
+ " -o Order commits by (--[topo|author-date|date]-order) ",
+ # " -g Show graph (--graph) ",
+ # " -c Show graph in color (--color) ",
+ " -d Show refnames (--decorate) ",
+ " ",
+ " Refresh Margin ",
+ " g buffer L toggle visibility ",
+ " l cycle style ",
+ " d toggle details ",
+ " x toggle shortstat "
+ ]
+ end
+
+ %w[L l d].each { include_examples "interaction", _1 }
+end
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 6e7f489bd..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 ",
@@ -15,6 +14,7 @@
" -r Rebase local commits (--rebase) ",
" -a Autostash (--autostash) ",
" -t Fetch tags (--tags) ",
+ " -F Force (--force) ",
" ",
" Pull into master from Configure ",
" p pushRemote, setting that C Set variables... ",
@@ -23,5 +23,5 @@
]
end
- %w[r -f -r -a -t p u e C].each { include_examples "interaction", _1 }
+ %w[r -f -r -a -t -F p u e C].each { include_examples "interaction", _1 }
end
diff --git a/spec/popups/push_popup_spec.rb b/spec/popups/push_popup_spec.rb
index 817ec4946..fc9af1360 100644
--- a/spec/popups/push_popup_spec.rb
+++ b/spec/popups/push_popup_spec.rb
@@ -3,20 +3,22 @@
require "spec_helper"
RSpec.describe "Push Popup", :git, :nvim, :popup, :with_remote_origin do
- before { nvim.keys("P") }
+ let(:keymap) { "P" }
let(:view) do
[
" Arguments ",
" -f Force with lease (--force-with-lease) ",
" -F Force (--force) ",
- " -u Set the upstream before pushing (--set-upstream) ",
" -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... ",
- " u @{upstream}, creating it r explicit refspecs ",
+ " u @{upstream}, creating it r explicit refspec ",
" e elsewhere m matching branches ",
" T a tag ",
" t all tags "
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 17f61e8b3..cff948627 100644
--- a/spec/popups/remote_popup_spec.rb
+++ b/spec/popups/remote_popup_spec.rb
@@ -2,9 +2,8 @@
require "spec_helper"
-RSpec.describe "Remote Popup", :git, :nvim, :popup do # rubocop:disable RSpec/EmptyExampleGroup
- before { nvim.keys("M") }
-
+RSpec.describe "Remote Popup", :git, :nvim, :popup do
+ let(:keymap) { "M" }
let(:view) do
[
" Variables ",
@@ -28,4 +27,124 @@
%w[u U s S O a d x C p P b z].each { include_examples "interaction", _1 }
%w[-f].each { include_examples "argument", _1 }
+
+ describe "add" do
+ context "with 'origin 'unset" do
+ it "allow user to add remote" do
+ nvim.keys("a")
+ nvim.keys("origin")
+ nvim.keys("git@github.com:NeogitOrg/neogit.git")
+ expect(git.remote.name).to eq("origin")
+ expect(git.remote.url).to eq("git@github.com:NeogitOrg/neogit.git")
+ end
+ end
+
+ context "with 'origin' set" do
+ before do
+ git.config("remote.origin.url", "git@github.com:NeogitOrg/neogit.git")
+ end
+
+ it "auto-populates host/remote" do
+ nvim.keys("a")
+ nvim.keys("fork")
+ expect(nvim.screen.last).to start_with("URL for fork: git@github.com:fork/neogit.git")
+ end
+ end
+ end
+
+ describe "remove" do
+ context "with no remotes configured" do
+ it "notifies user" do
+ nvim.keys("x")
+ expect(nvim.screen.last).to start_with("No remotes found")
+ end
+ end
+
+ context "with a remote configured" do
+ before do
+ git.config("remote.origin.url", "git@github.com:NeogitOrg/neogit.git")
+ end
+
+ it "can remove a remote" do
+ nvim.keys("x")
+ nvim.keys("origin")
+ expect(nvim.screen.last).to start_with("Removed remote 'origin'")
+ expect(git.remotes).to be_empty
+ end
+ end
+ end
+
+ describe "rename" do
+ context "with no remotes configured" do
+ it "notifies user" do
+ nvim.keys("r")
+ expect(nvim.screen.last).to start_with("No remotes found")
+ end
+ end
+
+ context "with a remote configured" do
+ before do
+ git.config("remote.origin.url", "git@github.com:NeogitOrg/neogit.git")
+ end
+
+ it "can rename a remote" do
+ nvim.keys("r")
+ nvim.keys("origin")
+ nvim.keys("fork")
+ expect(nvim.screen.last).to start_with("Renamed 'origin' -> 'fork'")
+ expect(git.remotes.first.name).to eq("fork")
+ end
+ end
+ end
+
+ describe "configure" do
+ context "with no remotes configured" do
+ it "notifies user" do
+ nvim.keys("C")
+ expect(nvim.screen.last).to start_with("No remotes found")
+ end
+ end
+
+ context "with a remote configured" do
+ before do
+ git.config("remote.origin.url", "git@github.com:NeogitOrg/neogit.git")
+ end
+
+ it "can launch remote config popup" do
+ nvim.keys("C")
+ nvim.keys("origin")
+ expect(nvim.screen[14..19]).to eq(
+ [" Configure remote ",
+ " u remote.origin.url git@github.com:NeogitOrg/neogit.git ",
+ " U remote.origin.fetch unset ",
+ " s remote.origin.pushurl unset ",
+ " S remote.origin.push unset ",
+ " O remote.origin.tagOpt [--no-tags|--tags] "]
+ )
+ end
+ end
+ end
+
+ describe "prune_branches" do
+ context "with no remotes configured" do
+ it "notifies user" do
+ nvim.keys("p")
+ expect(nvim.screen.last).to start_with("No remotes found")
+ end
+ end
+
+ context "with a remote configured" do
+ before do
+ git.config("remote.origin.url", "git@github.com:NeogitOrg/neogit.git")
+ end
+
+ it "can launch remote config popup" do
+ nvim.keys("p")
+ nvim.keys("origin")
+ await do
+ expect(nvim.screen.last).to start_with("Pruned remote origin")
+ end
+ end
+ end
+ end
end
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 fdaf15024..c4c339397 100644
--- a/spec/popups/stash_popup_spec.rb
+++ b/spec/popups/stash_popup_spec.rb
@@ -2,11 +2,15 @@
require "spec_helper"
-RSpec.describe "Stash Popup", :git, :nvim, :popup do # rubocop:disable RSpec/EmptyExampleGroup
- before { nvim.keys("Z") }
+RSpec.describe "Stash Popup", :git, :nvim, :popup do
+ let(:keymap) { "Z" }
let(:view) do
[
+ " Arguments ",
+ " -u Also save untracked files (--include-untracked) ",
+ " -a Also save untracked and ignored files (--all) ",
+ " ",
" Stash Snapshot Use Inspect Transform ",
" z both Z both p pop l List b Branch ",
" i index I index a apply v Show B Branch here ",
@@ -17,4 +21,116 @@
end
%w[z i w x P Z I W r p a d l b B m f].each { include_examples "interaction", _1 }
+ %w[-u -a].each { include_examples "argument", _1 }
+
+ describe "Stash both" do
+ before do
+ File.write("foo", "hello foo")
+ File.write("bar", "hello bar")
+ File.write("baz", "hello baz")
+ git.add("foo")
+ git.add("bar")
+ git.commit("initial commit")
+ File.write("foo", "hello world")
+ File.write("bar", "hello world")
+ git.add("foo")
+ end
+
+ context "with --include-untracked" do
+ it "stashes staged, unstaged, and untracked changed" do
+ nvim.keys("-u")
+ nvim.keys("z")
+ expect(git.status.changed).to be_empty
+ expect(git.status.untracked).to be_empty
+ end
+ end
+
+ context "with --all" do
+ it "stashes staged, unstaged, untracked, and ignored changes" do
+ nvim.keys("-a")
+ nvim.keys("z")
+ expect(git.status.changed).to be_empty
+ expect(git.status.untracked).to be_empty
+ end
+ end
+
+ it "stashes both staged and unstaged changes" do
+ nvim.keys("z")
+ expect(git.status.changed).to be_empty
+ expect(git.status.untracked).not_to be_empty
+ end
+ end
+
+ describe "Stash index" do
+ before do
+ File.write("foo", "hello foo") # Staged
+ File.write("bar", "hello bar") # Unstaged
+ File.write("baz", "hello baz") # Untracked
+
+ git.add("foo")
+ git.add("bar")
+ git.commit("initial commit")
+
+ File.write("foo", "hello world")
+ File.write("bar", "hello world")
+
+ git.add("foo")
+ end
+
+ it "stashes only staged changes" do
+ nvim.keys("i")
+ expect(git.status.changed.keys).to contain_exactly("bar")
+ expect(git.status.untracked).not_to be_empty
+ end
+ end
+
+ describe "Stash Keeping index" do
+ before do
+ File.write("foo", "hello foo") # Staged
+ File.write("bar", "hello bar") # Unstaged
+ File.write("baz", "hello baz") # Untracked
+
+ git.add("foo")
+ git.add("bar")
+ git.commit("initial commit")
+
+ File.write("foo", "hello world")
+ File.write("bar", "hello world")
+
+ git.add("foo")
+ end
+
+ it "stashes only unstaged changes" do
+ nvim.keys("x")
+ expect(git.status.changed.keys).to contain_exactly("foo")
+ expect(git.status.untracked).not_to be_empty
+ end
+ end
+
+ describe "Stash push" do
+ before do
+ File.write("foo", "hello foo") # Staged
+ File.write("bar", "hello bar") # Unstaged
+ File.write("baz", "hello baz") # Untracked
+
+ git.add("foo")
+ git.add("bar")
+ git.commit("initial commit")
+
+ File.write("foo", "hello world")
+ File.write("bar", "hello world")
+
+ git.add("foo")
+ end
+
+ it "stashes only specified file" do
+ expect(git.status.changed.keys).to contain_exactly("foo", "bar")
+
+ nvim.keys("Pfoo")
+ expect(git.status.changed.keys).to contain_exactly("bar")
+
+ nvim.keys("ZPbar")
+ expect(git.status.changed.keys).to be_empty
+ end
+ end
end
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 b8c60fd58..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
[
@@ -32,9 +32,9 @@
end
it "creates a worktree for an existing branch and checks it out", :aggregate_failures do
- nvim.keys("w") # Action
- nvim.keys("wor") # Select "worktree-test" branch
- nvim.keys("#{dir}") # go up level, new folder name
+ nvim.keys("w") # Action
+ nvim.keys("wor") # Select "worktree-test" branch
+ nvim.keys("#{dir}/") # go up level, new folder name
expect(git.worktrees.map(&:dir).last).to match(%r{/#{dir}$})
expect(nvim.cmd("pwd").first).to match(%r{/#{dir}$})
@@ -48,11 +48,10 @@
end
it "creates a worktree for a new branch and checks it out", :aggregate_failures do
- nvim.input("create-worktree-test") # Branch name
-
- nvim.keys("W") # Action
- nvim.keys("#{dir}") # go up level, new folder name
- nvim.keys("mas") # Set base branch to 'master'
+ nvim.keys("W") # Action
+ nvim.keys("#{dir}/") # new folder name
+ nvim.keys("mas") # Set base branch to 'master'
+ nvim.keys("create-worktree-test") # branch name
expect(git.worktrees.map(&:dir).last).to match(%r{/#{dir}$})
expect(nvim.cmd("pwd").first).to match(%r{/#{dir}$})
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 16d021f8a..81928d5d7 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -5,10 +5,13 @@
require "neovim"
require "debug"
require "active_support/all"
+require "timeout"
+require "super_diff/rspec"
+require "super_diff/active_support"
ENV["GIT_CONFIG_GLOBAL"] = ""
-PROJECT_DIR = File.expand_path(File.join(__dir__, ".."))
+PROJECT_DIR = File.expand_path(File.join(__dir__, "..")) unless defined?(PROJECT_DIR)
Dir[File.join(File.expand_path("."), "spec", "support", "**", "*.rb")].each { |f| require f }
@@ -49,4 +52,12 @@
end
end
end
+
+ if ENV["CI"].present?
+ config.around do |example|
+ Timeout.timeout(10) do
+ example.run
+ end
+ end
+ end
end
diff --git a/spec/support/dependencies.rb b/spec/support/dependencies.rb
index eb6a53db0..de5b3cc46 100644
--- a/spec/support/dependencies.rb
+++ b/spec/support/dependencies.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-return if ENV["CI"]
-
def dir_name(name)
name.match(%r{[^/]+/(?[^\.]+)})[:dir_name]
end
@@ -16,8 +14,11 @@ def ensure_installed(name, build: nil)
puts "Downloading dependency #{name} to #{dir}"
Dir.mkdir(dir)
- Git.clone("git@github.com:#{name}.git", dir)
- Dir.chdir(dir) { system(build) } if build.present?
+ Git.clone("git@github.com:#{name}.git", dir, filter: "tree:0")
+ return unless build.present?
+
+ puts "Building #{name} via #{build}"
+ Dir.chdir(dir) { system(build) }
end
ensure_installed "nvim-lua/plenary.nvim"
diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb
index 06dea7fc5..9d5ad584c 100644
--- a/spec/support/helpers.rb
+++ b/spec/support/helpers.rb
@@ -9,21 +9,19 @@ def expect_git_failure(&)
expect(&).to raise_error(Git::FailedError)
end
- # def wait_for_expect
- # last_error = nil
- # success = false
- #
- # 5.times do
- # begin
- # yield
- # success = true
- # break
- # rescue RSpec::Expectations::ExpectationNotMetError => e
- # last_error = e
- # sleep 0.5
- # end
- # end
- #
- # raise last_error if !success && last_error
- # end
+ def await # rubocop:disable Metrics/MethodLength
+ last_error = nil
+ success = false
+
+ 10.times do
+ yield
+ success = true
+ break
+ rescue RSpec::Expectations::ExpectationNotMetError => e
+ last_error = e
+ sleep 0.1
+ end
+
+ raise last_error if !success && last_error
+ end
end
diff --git a/spec/support/neovim_client.rb b/spec/support/neovim_client.rb
index a114cce83..d573e8dd7 100644
--- a/spec/support/neovim_client.rb
+++ b/spec/support/neovim_client.rb
@@ -25,6 +25,7 @@ def setup(neogit_config) # rubocop:disable Metrics/MethodLength
lua <<~LUA
require("plenary")
+ require("diffview").setup()
require('neogit').setup(#{neogit_config})
require('neogit').open()
LUA
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/init.lua b/tests/init.lua
index e8dd5130a..700ad858a 100644
--- a/tests/init.lua
+++ b/tests/init.lua
@@ -11,10 +11,9 @@ else
util.ensure_installed("nvim-lua/plenary.nvim", util.neogit_test_base_dir)
end
-require("plenary.test_harness").test_directory(
- os.getenv("TEST_FILES") == "" and "tests/specs" or os.getenv("TEST_FILES"),
- {
- minimal_init = "tests/minimal_init.lua",
- sequential = true,
- }
-)
+local directory = os.getenv("TEST_FILES") == "" and "tests/specs" or os.getenv("TEST_FILES") or "tests/specs"
+
+require("plenary.test_harness").test_directory(directory, {
+ minimal_init = "tests/minimal_init.lua",
+ sequential = true,
+})
diff --git a/tests/specs/neogit/config_spec.lua b/tests/specs/neogit/config_spec.lua
index 3a51eb951..0d169c33c 100644
--- a/tests/specs/neogit/config_spec.lua
+++ b/tests/specs/neogit/config_spec.lua
@@ -56,6 +56,11 @@ describe("Neogit config", function()
assert.True(vim.tbl_count(require("neogit.config").validate_config()) ~= 0)
end)
+ it("should return invalid when initial_branch_name isn't a string", function()
+ config.values.initial_branch_name = false
+ assert.True(vim.tbl_count(require("neogit.config").validate_config()) ~= 0)
+ end)
+
it("should return invalid when kind isn't a string", function()
config.values.kind = true
assert.True(vim.tbl_count(require("neogit.config").validate_config()) ~= 0)
@@ -77,7 +82,12 @@ describe("Neogit config", function()
end)
it("should return invalid when auto_show_console isn't a boolean", function()
- config.values.console_timeout = "not a boolean"
+ config.values.auto_show_console = "not a boolean"
+ assert.True(vim.tbl_count(require("neogit.config").validate_config()) ~= 0)
+ end)
+
+ it("should return invalid when auto_show_console_on isn't a string", function()
+ config.values.auto_show_console_on = true
assert.True(vim.tbl_count(require("neogit.config").validate_config()) ~= 0)
end)
diff --git a/tests/specs/neogit/docs_spec.lua b/tests/specs/neogit/docs_spec.lua
index 644ff1492..8e8e72098 100644
--- a/tests/specs/neogit/docs_spec.lua
+++ b/tests/specs/neogit/docs_spec.lua
@@ -2,7 +2,7 @@ local Path = require("plenary.path")
describe("docs", function()
it("doesn't repeat any tags", function()
- local docs = Path.new(vim.loop.cwd(), "doc", "neogit.txt")
+ local docs = Path.new(vim.uv.cwd(), "doc", "neogit.txt")
local tags = {}
for line in docs:iter() do
@@ -14,7 +14,7 @@ describe("docs", function()
end)
it("doesn't reference any undefined tags", function()
- local docs = Path.new(vim.loop.cwd(), "doc", "neogit.txt")
+ local docs = Path.new(vim.uv.cwd(), "doc", "neogit.txt")
local tags = {}
local refs = {}
diff --git a/tests/specs/neogit/lib/git/cli_spec.lua b/tests/specs/neogit/lib/git/cli_spec.lua
index d80c89e8b..a913e9e7b 100644
--- a/tests/specs/neogit/lib/git/cli_spec.lua
+++ b/tests/specs/neogit/lib/git/cli_spec.lua
@@ -8,7 +8,7 @@ describe("git cli", function()
it(
"finds the correct git root for a non symlinked directory",
in_prepared_repo(function(root_dir)
- local detected_root_dir = git_cli.git_root(".")
+ local detected_root_dir = git_cli.worktree_root(".")
eq(detected_root_dir, root_dir)
end)
)
@@ -35,7 +35,7 @@ describe("git cli", function()
vim.fn.system(cmd)
vim.api.nvim_set_current_dir(symlink_dir)
- local detected_root_dir = git_cli.git_root(".")
+ local detected_root_dir = git_cli.worktree_root(".")
eq(detected_root_dir, git_dir)
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/specs/neogit/lib/git/repository_spec.lua b/tests/specs/neogit/lib/git/repository_spec.lua
index c5757e820..5b8e9b8c9 100644
--- a/tests/specs/neogit/lib/git/repository_spec.lua
+++ b/tests/specs/neogit/lib/git/repository_spec.lua
@@ -8,8 +8,8 @@ describe("lib.git.instance", function()
it(
"creates cached git instance and returns it",
in_prepared_repo(function(root_dir)
- local dir1 = git_repo.instance(root_dir).git_root
- local dir2 = git_repo.instance().git_root
+ local dir1 = git_repo.instance(root_dir).worktree_root
+ local dir2 = git_repo.instance().worktree_root
eq(dir1, dir2)
end)
)
diff --git a/tests/specs/neogit/lib/git/status_spec.lua b/tests/specs/neogit/lib/git/status_spec.lua
deleted file mode 100644
index d5894a62c..000000000
--- a/tests/specs/neogit/lib/git/status_spec.lua
+++ /dev/null
@@ -1,46 +0,0 @@
-local neogit = require("neogit")
-local git_harness = require("tests.util.git_harness")
-local util = require("tests.util.util")
-
-local subject = require("neogit.lib.git.status")
-
-neogit.setup {}
-
-describe("lib.git.status", function()
- before_each(function()
- git_harness.prepare_repository()
- -- plenary_async.util.block_on(neogit.reset)
- end)
-
- describe("#anything_staged", function()
- -- it("returns true when there are staged items", function()
- -- util.system("git add --all")
- -- plenary_async.util.block_on(neogit.refresh)
- --
- -- assert.True(subject.anything_staged())
- -- end)
-
- it("returns false when there are no staged items", function()
- util.system { "git", "reset" }
- neogit.refresh()
-
- assert.False(subject.anything_staged())
- end)
- end)
-
- describe("#anything_unstaged", function()
- -- it("returns true when there are unstaged items", function()
- -- util.system("git reset")
- -- plenary_async.util.block_on(neogit.refresh)
- --
- -- assert.True(subject.anything_unstaged())
- -- end)
-
- it("returns false when there are no unstaged items", function()
- util.system { "git", "add", "--all" }
- neogit.refresh()
-
- assert.False(subject.anything_unstaged())
- end)
- end)
-end)
diff --git a/tests/specs/neogit/process_spec.lua b/tests/specs/neogit/process_spec.lua
deleted file mode 100644
index 6af9ae870..000000000
--- a/tests/specs/neogit/process_spec.lua
+++ /dev/null
@@ -1,65 +0,0 @@
-require("plenary.async").tests.add_to_env()
-local util = require("tests.util.util")
-
-local process = require("neogit.process")
-
-describe("process execution", function()
- it("basic command", function()
- local result =
- process.new({ cmd = { "cat", "process_test" }, cwd = util.get_fixtures_dir() }):spawn_blocking(1)
- assert(result)
- assert.are.same({
- "This is a test file",
- "",
- "",
- "It is intended to be read by cat and returned to neovim using the process api",
- "",
- "",
- }, result.stdout)
- end)
-
- it("can cat a file", function()
- local result = process.new({ cmd = { "cat", "a.txt" }, cwd = util.get_fixtures_dir() }):spawn_blocking(1)
-
- assert(result)
- assert.are.same({
- "Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet.",
- "Nisi anim cupidatat excepteur officia.",
- "Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident.",
- "Nostrud officia pariatur ut officia.",
- "Sit irure elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat officia voluptate.",
- "",
- "Culpa proident adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod.",
- "Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim.",
- "Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa et culpa duis.",
- "",
- }, result.stdout)
- end)
-
- it("process input", function()
- local tmp_dir = util.create_temp_dir()
- local input = { "This is a line", "This is another line", "", "" }
- local p = process.new { cmd = { "tee", tmp_dir .. "/output" } }
-
- p:spawn()
- p:send(table.concat(input, "\n"))
- p:send("\04")
- p:close_stdin()
- p:wait()
-
- local result = process.new({ cmd = { "cat", tmp_dir .. "/output" } }):spawn_blocking(1)
- assert(result)
- assert.are.same({ "This is a line", "This is another line", "", "\04" }, result.stdout)
- end)
-
- it("basic command trim", function()
- local result =
- process.new({ cmd = { "cat", "process_test" }, cwd = util.get_fixtures_dir() }):spawn_blocking(1)
-
- assert(result)
- assert.are.same({
- "This is a test file",
- "It is intended to be read by cat and returned to neovim using the process api",
- }, result:trim().stdout)
- end)
-end)
diff --git a/tests/util/util.lua b/tests/util/util.lua
index a898df127..f9dabdcc5 100644
--- a/tests/util/util.lua
+++ b/tests/util/util.lua
@@ -39,7 +39,7 @@ end
M.neogit_test_base_dir = "/tmp/neogit-testing/"
local function is_macos()
- return vim.loop.os_uname().sysname == "Darwin"
+ return vim.uv.os_uname().sysname == "Darwin"
end
local function is_gnu_mktemp()
@@ -72,7 +72,7 @@ function M.ensure_installed(repo, path)
vim.opt.runtimepath:prepend(install_path)
- if not vim.loop.fs_stat(install_path) then
+ 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 }