Compare commits
34 Commits
172ce6b57a
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 942dbdcaa0 | |||
| 1a582045f6 | |||
| 7c92b5eff6 | |||
| 6230c2663c | |||
| 5b869334d6 | |||
| 01ca0025dd | |||
| b52f34ce9a | |||
| e050896dc0 | |||
| 4c8b3f0d3e | |||
| 72ab9059fa | |||
| 7c8975af10 | |||
| 2064c629ed | |||
| aaef6621dd | |||
| d629302625 | |||
| f77d26db6b | |||
| f4181b89fc | |||
| d979c961a2 | |||
| d132c00032 | |||
| 73fa92afc8 | |||
| 3b8951758e | |||
| b692f23fe2 | |||
| ffd5584a05 | |||
| 4461a65b90 | |||
| 8121227ba4 | |||
| 26d074c464 | |||
| 897de35688 | |||
| 8fe4d0c6a7 | |||
| 8a8c73ca8b | |||
| ab9b70c70a | |||
| 085216a406 | |||
| 50db85ea5f | |||
| 5f956401c1 | |||
| c66b2f04d2 | |||
| 29575c1e20 |
+3
-1
@@ -1,2 +1,4 @@
|
|||||||
---@type vim.lsp.Config
|
---@type vim.lsp.Config
|
||||||
return {}
|
return {
|
||||||
|
filetypes = { "typescript", "typescriptreact" },
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ require("pack").setup({
|
|||||||
"https://github.com/owallb/mason-auto-install.nvim",
|
"https://github.com/owallb/mason-auto-install.nvim",
|
||||||
"https://github.com/mfussenegger/nvim-dap",
|
"https://github.com/mfussenegger/nvim-dap",
|
||||||
"https://github.com/numToStr/Comment.nvim",
|
"https://github.com/numToStr/Comment.nvim",
|
||||||
"https://github.com/lewis6991/gitsigns.nvim",
|
|
||||||
"https://github.com/MagicDuck/grug-far.nvim",
|
"https://github.com/MagicDuck/grug-far.nvim",
|
||||||
"https://github.com/nvim-tree/nvim-tree.lua",
|
"https://github.com/nvim-tree/nvim-tree.lua",
|
||||||
"https://github.com/stevearc/oil.nvim",
|
"https://github.com/stevearc/oil.nvim",
|
||||||
@@ -61,6 +60,7 @@ require("ts").setup({
|
|||||||
lang = "gotmpl",
|
lang = "gotmpl",
|
||||||
},
|
},
|
||||||
"https://github.com/tree-sitter/tree-sitter-html",
|
"https://github.com/tree-sitter/tree-sitter-html",
|
||||||
|
"https://github.com/tree-sitter/tree-sitter-javascript",
|
||||||
"https://github.com/tree-sitter/tree-sitter-json",
|
"https://github.com/tree-sitter/tree-sitter-json",
|
||||||
"https://github.com/tree-sitter-grammars/tree-sitter-luadoc",
|
"https://github.com/tree-sitter-grammars/tree-sitter-luadoc",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -66,6 +66,18 @@ vim.api.nvim_create_autocmd({ "BufReadPost" }, {
|
|||||||
command = 'silent! normal! g`"zv',
|
command = 'silent! normal! g`"zv',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
vim.api.nvim_create_autocmd("BufWinEnter", {
|
||||||
|
desc = "Reset foldlevel to 0 when entering a diff window."
|
||||||
|
.. " Vim's partial diff-state restoration on buffer re-entry"
|
||||||
|
.. " (e.g. via <C-o>) doesn't re-apply foldlevel=0, so"
|
||||||
|
.. " foldlevelstart leaks through and folds appear open.",
|
||||||
|
callback = function()
|
||||||
|
if vim.wo.diff then
|
||||||
|
vim.wo.foldlevel = 0
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
vim.api.nvim_create_autocmd("FileType", {
|
vim.api.nvim_create_autocmd("FileType", {
|
||||||
pattern = { "c" },
|
pattern = { "c" },
|
||||||
callback = function()
|
callback = function()
|
||||||
|
|||||||
+11
-4
@@ -226,11 +226,18 @@ vim.keymap.set("n", "<leader>fD", vim.diagnostic.setqflist)
|
|||||||
vim.keymap.set("n", "grt", vim.lsp.buf.type_definition)
|
vim.keymap.set("n", "grt", vim.lsp.buf.type_definition)
|
||||||
vim.keymap.set("n", "gd", vim.lsp.buf.definition)
|
vim.keymap.set("n", "gd", vim.lsp.buf.definition)
|
||||||
|
|
||||||
vim.keymap.set("n", "<leader>gd", "<Plug>(git-diff-vertical)")
|
vim.keymap.set("n", "<leader>gd", "<Plug>(git-diffsplit-vertical)")
|
||||||
vim.keymap.set("n", "<leader>gD", "<Plug>(git-diff-vertical-head)")
|
vim.keymap.set("n", "<leader>gD", "<Plug>(git-diffsplit-vertical-head)")
|
||||||
vim.keymap.set("n", "<leader>gh", "<Plug>(git-diff-horizontal)")
|
vim.keymap.set("n", "<leader>gh", "<Plug>(git-diffsplit-horizontal)")
|
||||||
vim.keymap.set("n", "<leader>gH", "<Plug>(git-diff-horizontal-head)")
|
vim.keymap.set("n", "<leader>gH", "<Plug>(git-diffsplit-horizontal-head)")
|
||||||
vim.keymap.set("n", "<leader>gg", "<Plug>(git-status-toggle)")
|
vim.keymap.set("n", "<leader>gg", "<Plug>(git-status-toggle)")
|
||||||
vim.keymap.set("n", "<leader>gc", "<Plug>(git-commit)")
|
vim.keymap.set("n", "<leader>gc", "<Plug>(git-commit)")
|
||||||
vim.keymap.set("n", "<leader>ga", "<Plug>(git-commit-amend)")
|
vim.keymap.set("n", "<leader>ga", "<Plug>(git-commit-amend)")
|
||||||
vim.keymap.set("n", "<leader>gl", "<Plug>(git-log)")
|
vim.keymap.set("n", "<leader>gl", "<Plug>(git-log)")
|
||||||
|
vim.keymap.set("n", "<leader>gv", "<Plug>(git-hunk-select)")
|
||||||
|
vim.keymap.set("n", "<leader>gs", "<Plug>(git-hunk-stage-toggle)")
|
||||||
|
vim.keymap.set("n", "<leader>gr", "<Plug>(git-hunk-reset)")
|
||||||
|
vim.keymap.set("n", "<C-w>g", "<Plug>(git-hunk-preview)")
|
||||||
|
vim.keymap.set("n", "<leader>go", "<Plug>(git-overlay-toggle)")
|
||||||
|
vim.keymap.set({ "n", "x" }, "]g", "<Plug>(git-hunk-next)")
|
||||||
|
vim.keymap.set({ "n", "x" }, "[g", "<Plug>(git-hunk-prev)")
|
||||||
|
|||||||
+384
-63
@@ -20,10 +20,37 @@ end
|
|||||||
---@field index_writer boolean?
|
---@field index_writer boolean?
|
||||||
---@field index_mode string?
|
---@field index_mode string?
|
||||||
|
|
||||||
---@alias ow.Git.Repo.Event "refresh"
|
---@alias ow.Git.Repo.Event
|
||||||
|
---| "change"
|
||||||
|
|
||||||
local global = util.Emitter.new()
|
local global = util.Emitter.new()
|
||||||
|
|
||||||
|
---@type table<string, ow.Git.Repo> keyed by worktree
|
||||||
|
local repos = {}
|
||||||
|
|
||||||
|
---@param r ow.Git.Repo
|
||||||
|
local function release_if_unused(r)
|
||||||
|
if repos[r.worktree] ~= r then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if next(r.buffers) ~= nil or next(r.tabs) ~= nil then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
r:close()
|
||||||
|
repos[r.worktree] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class ow.Git.Repo.Change
|
||||||
|
---@field paths table<string, true>
|
||||||
|
---@field branch_changed boolean
|
||||||
|
|
||||||
|
---@class ow.Git.Repo.RefreshOpts
|
||||||
|
---@field invalidate boolean?
|
||||||
|
|
||||||
|
---@class ow.Git.Repo.SubmoduleEntry
|
||||||
|
---@field worktree string
|
||||||
|
---@field unsub fun()?
|
||||||
|
|
||||||
---@class ow.Git.Repo
|
---@class ow.Git.Repo
|
||||||
---@field gitdir string
|
---@field gitdir string
|
||||||
---@field worktree string
|
---@field worktree string
|
||||||
@@ -35,6 +62,9 @@ local global = util.Emitter.new()
|
|||||||
---@field private _schedule_refresh fun(self: ow.Git.Repo)
|
---@field private _schedule_refresh fun(self: ow.Git.Repo)
|
||||||
---@field private _refresh_handle ow.Git.Util.DebounceHandle
|
---@field private _refresh_handle ow.Git.Util.DebounceHandle
|
||||||
---@field private _cache table<string, any>
|
---@field private _cache table<string, any>
|
||||||
|
---@field private _fetch_epoch integer
|
||||||
|
---@field private _pending_invalidate boolean
|
||||||
|
---@field package _submodules table<string, ow.Git.Repo.SubmoduleEntry>
|
||||||
local Repo = {}
|
local Repo = {}
|
||||||
Repo.__index = Repo
|
Repo.__index = Repo
|
||||||
|
|
||||||
@@ -50,12 +80,142 @@ local STATUS_ARGS = {
|
|||||||
"-z",
|
"-z",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local PSEUDO_REFS = {
|
||||||
|
"HEAD",
|
||||||
|
"FETCH_HEAD",
|
||||||
|
"ORIG_HEAD",
|
||||||
|
"MERGE_HEAD",
|
||||||
|
"REBASE_HEAD",
|
||||||
|
"CHERRY_PICK_HEAD",
|
||||||
|
"REVERT_HEAD",
|
||||||
|
}
|
||||||
|
|
||||||
|
---@type table<string, fun(relpath: string): boolean>
|
||||||
|
local INVALIDATION_RULES = {
|
||||||
|
head = function(relpath)
|
||||||
|
return relpath == "HEAD"
|
||||||
|
or vim.startswith(relpath, "refs/heads/")
|
||||||
|
or relpath == "packed-refs"
|
||||||
|
end,
|
||||||
|
refs = function(relpath)
|
||||||
|
return vim.startswith(relpath, "refs/heads/")
|
||||||
|
or vim.startswith(relpath, "refs/tags/")
|
||||||
|
or vim.startswith(relpath, "refs/remotes/")
|
||||||
|
or relpath == "packed-refs"
|
||||||
|
end,
|
||||||
|
pseudo_refs = function(relpath)
|
||||||
|
return vim.tbl_contains(PSEUDO_REFS, relpath)
|
||||||
|
end,
|
||||||
|
stash_refs = function(relpath)
|
||||||
|
return relpath == "refs/stash" or relpath == "logs/refs/stash"
|
||||||
|
end,
|
||||||
|
config = function(relpath)
|
||||||
|
return relpath == "config"
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
---@param relpath string
|
||||||
|
---@return boolean
|
||||||
|
local function affects_resolve(relpath)
|
||||||
|
return vim.startswith(relpath, "refs/")
|
||||||
|
or relpath == "packed-refs"
|
||||||
|
or relpath == "HEAD"
|
||||||
|
or relpath == "FETCH_HEAD"
|
||||||
|
end
|
||||||
|
|
||||||
|
---@private
|
||||||
|
---@param prefix string
|
||||||
|
function Repo:_clear_cache_prefix(prefix)
|
||||||
|
for key in pairs(self._cache) do
|
||||||
|
if vim.startswith(key, prefix) then
|
||||||
|
self._cache[key] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@private
|
||||||
|
---@param relpath string
|
||||||
|
function Repo:_invalidate(relpath)
|
||||||
|
for key, affects in pairs(INVALIDATION_RULES) do
|
||||||
|
if self._cache[key] ~= nil and affects(relpath) then
|
||||||
|
self._cache[key] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if affects_resolve(relpath) then
|
||||||
|
self:_clear_cache_prefix("resolve:")
|
||||||
|
self:_clear_cache_prefix("head_blob:")
|
||||||
|
end
|
||||||
|
if relpath == "index" then
|
||||||
|
self:_clear_cache_prefix("index:")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param path string
|
||||||
|
---@return table<string, table<string, string>>?
|
||||||
|
local function read_git_config(path)
|
||||||
|
local f = io.open(path, "r")
|
||||||
|
if not f then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local content = f:read("*a")
|
||||||
|
f:close()
|
||||||
|
local out = {}
|
||||||
|
local section
|
||||||
|
for line in content:gmatch("[^\n]+") do
|
||||||
|
local trimmed = line:match("^%s*(.-)%s*$")
|
||||||
|
if trimmed ~= "" and not trimmed:match("^[#;]") then
|
||||||
|
local s = trimmed:match("^%[(.-)%]$")
|
||||||
|
if s then
|
||||||
|
section = s
|
||||||
|
out[section] = out[section] or {}
|
||||||
|
elseif section then
|
||||||
|
local key, value =
|
||||||
|
trimmed:match("^(%S+)%s*=%s*(.-)$")
|
||||||
|
if key then
|
||||||
|
out[section][key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param gitdir string
|
||||||
|
---@return string[]
|
||||||
|
local function find_submodules(gitdir)
|
||||||
|
local handle = vim.uv.fs_scandir(vim.fs.joinpath(gitdir, "modules"))
|
||||||
|
if not handle then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
local out = {}
|
||||||
|
while true do
|
||||||
|
local name, typ = vim.uv.fs_scandir_next(handle)
|
||||||
|
if not name then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
if typ == "directory" then
|
||||||
|
table.insert(out, name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
---@private
|
---@private
|
||||||
function Repo:_fetch_status()
|
function Repo:_fetch_status()
|
||||||
|
if self._pending_invalidate then
|
||||||
|
self._cache = {}
|
||||||
|
self._pending_invalidate = false
|
||||||
|
end
|
||||||
|
local prior_entries = self.status.entries
|
||||||
|
local prior_branch = self.status.branch
|
||||||
|
self._fetch_epoch = self._fetch_epoch + 1
|
||||||
|
local epoch = self._fetch_epoch
|
||||||
util.git(STATUS_ARGS, {
|
util.git(STATUS_ARGS, {
|
||||||
cwd = self.worktree,
|
cwd = self.worktree,
|
||||||
on_exit = function(result)
|
on_exit = function(result)
|
||||||
self._cache = {}
|
if epoch ~= self._fetch_epoch then
|
||||||
|
return
|
||||||
|
end
|
||||||
if result.code ~= 0 then
|
if result.code ~= 0 then
|
||||||
util.error(
|
util.error(
|
||||||
"git status failed: %s",
|
"git status failed: %s",
|
||||||
@@ -64,13 +224,30 @@ function Repo:_fetch_status()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
self.status = status.parse(result.stdout or "")
|
self.status = status.parse(result.stdout or "")
|
||||||
self._events:emit("refresh", self.status)
|
local change = {
|
||||||
global:emit("refresh", self, self.status)
|
paths = status.diff_entries(
|
||||||
|
prior_entries,
|
||||||
|
self.status.entries
|
||||||
|
),
|
||||||
|
branch_changed = not vim.deep_equal(
|
||||||
|
prior_branch,
|
||||||
|
self.status.branch
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if next(change.paths) == nil and not change.branch_changed then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self._events:emit("change", change, self.status)
|
||||||
|
global:emit("change", self, change, self.status)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
function Repo:refresh()
|
---@param opts ow.Git.Repo.RefreshOpts?
|
||||||
|
function Repo:refresh(opts)
|
||||||
|
if opts and opts.invalidate then
|
||||||
|
self._pending_invalidate = true
|
||||||
|
end
|
||||||
self:_schedule_refresh()
|
self:_schedule_refresh()
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -86,11 +263,20 @@ function Repo.new(gitdir, worktree)
|
|||||||
status = status.parse(""),
|
status = status.parse(""),
|
||||||
_events = util.Emitter.new(),
|
_events = util.Emitter.new(),
|
||||||
_cache = {},
|
_cache = {},
|
||||||
|
_fetch_epoch = 0,
|
||||||
|
_pending_invalidate = false,
|
||||||
|
_submodules = {},
|
||||||
}, Repo)
|
}, Repo)
|
||||||
self._schedule_refresh, self._refresh_handle =
|
self._schedule_refresh, self._refresh_handle =
|
||||||
util.debounce(Repo._fetch_status, 50)
|
util.debounce(Repo._fetch_status, 50)
|
||||||
self:start_watcher()
|
self:start_watcher()
|
||||||
self:refresh()
|
self:refresh()
|
||||||
|
if vim.g.git_submodule_recursion ~= false then
|
||||||
|
self:_start_modules_watcher()
|
||||||
|
for _, name in ipairs(find_submodules(gitdir)) do
|
||||||
|
self:_register_submodule(name)
|
||||||
|
end
|
||||||
|
end
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -130,9 +316,114 @@ local function start_fs_event(path, on_event)
|
|||||||
end
|
end
|
||||||
|
|
||||||
---@private
|
---@private
|
||||||
---@param path string
|
---@param name string
|
||||||
---@param on_change fun()
|
function Repo:_unregister_submodule(name)
|
||||||
function Repo:_watch_tree(path, on_change)
|
local entry = self._submodules[name]
|
||||||
|
if not entry then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self._submodules[name] = nil
|
||||||
|
if entry.unsub then
|
||||||
|
entry.unsub()
|
||||||
|
end
|
||||||
|
local child = repos[entry.worktree]
|
||||||
|
if child then
|
||||||
|
release_if_unused(child)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@private
|
||||||
|
---@param name string
|
||||||
|
function Repo:_register_submodule(name)
|
||||||
|
local sub_gitdir = vim.fs.joinpath(self.gitdir, "modules", name)
|
||||||
|
local cfg = read_git_config(vim.fs.joinpath(sub_gitdir, "config"))
|
||||||
|
local raw = cfg and cfg.core and cfg.core.worktree
|
||||||
|
if not raw then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local wt = raw:match("^/") and raw or vim.fs.joinpath(sub_gitdir, raw)
|
||||||
|
wt = vim.fs.normalize(wt)
|
||||||
|
local existing = self._submodules[name]
|
||||||
|
if existing and existing.worktree == wt then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if existing then
|
||||||
|
self:_unregister_submodule(name)
|
||||||
|
end
|
||||||
|
local child = repos[wt] or M.resolve(wt)
|
||||||
|
if not child then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self._submodules[name] = {
|
||||||
|
worktree = wt,
|
||||||
|
unsub = child:on("change", function()
|
||||||
|
self:refresh()
|
||||||
|
end),
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@private
|
||||||
|
function Repo:_start_modules_watcher()
|
||||||
|
local dir = vim.fs.joinpath(self.gitdir, "modules")
|
||||||
|
if self._watchers[dir] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not vim.uv.fs_stat(dir) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self._watchers[dir] = start_fs_event(dir, function(filename)
|
||||||
|
if not filename then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if vim.uv.fs_stat(vim.fs.joinpath(dir, filename)) then
|
||||||
|
self:_register_submodule(filename)
|
||||||
|
else
|
||||||
|
self:_unregister_submodule(filename)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@private
|
||||||
|
function Repo:_stop_modules_watcher()
|
||||||
|
local dir = vim.fs.joinpath(self.gitdir, "modules")
|
||||||
|
local w = self._watchers[dir]
|
||||||
|
if w then
|
||||||
|
w:stop()
|
||||||
|
w:close()
|
||||||
|
self._watchers[dir] = nil
|
||||||
|
end
|
||||||
|
for _, name in ipairs(vim.tbl_keys(self._submodules)) do
|
||||||
|
self:_unregister_submodule(name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@private
|
||||||
|
---@param relpath string
|
||||||
|
function Repo:_handle_fs_event(relpath)
|
||||||
|
if vim.startswith(relpath, "objects") then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self:_invalidate(relpath)
|
||||||
|
if relpath == "modules" and vim.g.git_submodule_recursion ~= false then
|
||||||
|
if vim.uv.fs_stat(vim.fs.joinpath(self.gitdir, "modules")) then
|
||||||
|
self:_start_modules_watcher()
|
||||||
|
for _, name in ipairs(find_submodules(self.gitdir)) do
|
||||||
|
self:_register_submodule(name)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self:_stop_modules_watcher()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if vim.startswith(relpath, "logs") then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self:refresh()
|
||||||
|
end
|
||||||
|
|
||||||
|
---@private
|
||||||
|
---@param relpath string gitdir-relative path of the directory to watch
|
||||||
|
function Repo:_watch_tree(relpath)
|
||||||
|
local path = vim.fs.joinpath(self.gitdir, relpath)
|
||||||
if self._watchers[path] then
|
if self._watchers[path] then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -150,11 +441,14 @@ function Repo:_watch_tree(path, on_change)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
on_change()
|
|
||||||
if filename then
|
if filename then
|
||||||
|
local child = vim.fs.joinpath(relpath, filename)
|
||||||
|
self:_handle_fs_event(child)
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
self:_watch_tree(vim.fs.joinpath(path, filename), on_change)
|
self:_watch_tree(child)
|
||||||
end)
|
end)
|
||||||
|
else
|
||||||
|
self:refresh({ invalidate = true })
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
if not watcher then
|
if not watcher then
|
||||||
@@ -171,7 +465,7 @@ function Repo:_watch_tree(path, on_change)
|
|||||||
break
|
break
|
||||||
end
|
end
|
||||||
if typ == "directory" then
|
if typ == "directory" then
|
||||||
self:_watch_tree(vim.fs.joinpath(path, name), on_change)
|
self:_watch_tree(vim.fs.joinpath(relpath, name))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -179,22 +473,18 @@ end
|
|||||||
function Repo:start_watcher()
|
function Repo:start_watcher()
|
||||||
self._watchers = {}
|
self._watchers = {}
|
||||||
local top = start_fs_event(self.gitdir, function(filename)
|
local top = start_fs_event(self.gitdir, function(filename)
|
||||||
if
|
if not filename then
|
||||||
filename
|
self:refresh({ invalidate = true })
|
||||||
and (filename:match("^objects") or filename:match("^logs"))
|
|
||||||
then
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
self:refresh()
|
self:_handle_fs_event(filename)
|
||||||
end)
|
end)
|
||||||
if not top then
|
if not top then
|
||||||
util.error("git: failed to watch %s", self.gitdir)
|
util.error("git: failed to watch %s", self.gitdir)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
self._watchers[self.gitdir] = top
|
self._watchers[self.gitdir] = top
|
||||||
self:_watch_tree(vim.fs.joinpath(self.gitdir, "refs"), function()
|
self:_watch_tree("refs")
|
||||||
self:refresh()
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Repo:close()
|
function Repo:close()
|
||||||
@@ -203,13 +493,12 @@ function Repo:close()
|
|||||||
watcher:close()
|
watcher:close()
|
||||||
end
|
end
|
||||||
self._watchers = {}
|
self._watchers = {}
|
||||||
|
self:_stop_modules_watcher()
|
||||||
self._refresh_handle.close()
|
self._refresh_handle.close()
|
||||||
self._events:clear()
|
self._events:clear()
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param event ow.Git.Repo.Event
|
---@overload fun(event: "change", fn: fun(change: ow.Git.Repo.Change, status: ow.Git.Status)): fun()
|
||||||
---@param fn fun(...)
|
|
||||||
---@return fun() unsubscribe
|
|
||||||
function Repo:on(event, fn)
|
function Repo:on(event, fn)
|
||||||
return self._events:on(event, fn)
|
return self._events:on(event, fn)
|
||||||
end
|
end
|
||||||
@@ -261,16 +550,6 @@ function Repo:list_refs()
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
local PSEUDO_REFS = {
|
|
||||||
"HEAD",
|
|
||||||
"FETCH_HEAD",
|
|
||||||
"ORIG_HEAD",
|
|
||||||
"MERGE_HEAD",
|
|
||||||
"REBASE_HEAD",
|
|
||||||
"CHERRY_PICK_HEAD",
|
|
||||||
"REVERT_HEAD",
|
|
||||||
}
|
|
||||||
|
|
||||||
---@return string[]
|
---@return string[]
|
||||||
function Repo:list_pseudo_refs()
|
function Repo:list_pseudo_refs()
|
||||||
return self:get_cached("pseudo_refs", function(self)
|
return self:get_cached("pseudo_refs", function(self)
|
||||||
@@ -318,15 +597,33 @@ function Repo:rev_parse(rev, short)
|
|||||||
return trimmed ~= "" and trimmed or nil
|
return trimmed ~= "" and trimmed or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param rel string worktree-relative path
|
||||||
|
---@return string?
|
||||||
|
function Repo:index_sha(rel)
|
||||||
|
local sha = self:get_cached("index:" .. rel, function(self)
|
||||||
|
return self:rev_parse(":" .. rel, false) or false
|
||||||
|
end)
|
||||||
|
return sha or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param rel string worktree-relative path
|
||||||
|
---@return string?
|
||||||
|
function Repo:head_sha(rel)
|
||||||
|
local sha = self:get_cached("head_blob:" .. rel, function(self)
|
||||||
|
return self:rev_parse("HEAD:" .. rel, false) or false
|
||||||
|
end)
|
||||||
|
return sha or nil
|
||||||
|
end
|
||||||
|
|
||||||
---@alias ow.Git.Repo.ResolveStatus "ok"|"ambiguous"|"missing"
|
---@alias ow.Git.Repo.ResolveStatus "ok"|"ambiguous"|"missing"
|
||||||
|
|
||||||
---@param prefix string
|
---@param abbrev string
|
||||||
---@return string? full_sha
|
---@return string? full_sha
|
||||||
---@return ow.Git.Repo.ResolveStatus
|
---@return ow.Git.Repo.ResolveStatus
|
||||||
function Repo:resolve_sha(prefix)
|
function Repo:resolve_sha(abbrev)
|
||||||
local result = self:get_cached("resolve:" .. prefix, function(self)
|
local result = self:get_cached("resolve:" .. abbrev, function(self)
|
||||||
local out = util.git(
|
local out = util.git(
|
||||||
{ "rev-parse", "--disambiguate=" .. prefix },
|
{ "rev-parse", "--disambiguate=" .. abbrev },
|
||||||
{ cwd = self.worktree, silent = true }
|
{ cwd = self.worktree, silent = true }
|
||||||
)
|
)
|
||||||
local trimmed = out and vim.trim(out) or ""
|
local trimmed = out and vim.trim(out) or ""
|
||||||
@@ -342,15 +639,41 @@ function Repo:resolve_sha(prefix)
|
|||||||
return result[1], result[2]
|
return result[1], result[2]
|
||||||
end
|
end
|
||||||
|
|
||||||
---@type table<string, ow.Git.Repo> keyed by worktree
|
---@private
|
||||||
local repos = {}
|
---@return table<string, table<string, string>>
|
||||||
|
function Repo:_config()
|
||||||
|
return self:get_cached("config", function(self)
|
||||||
|
return read_git_config(vim.fs.joinpath(self.gitdir, "config")) or {}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@private
|
||||||
|
---@return boolean
|
||||||
|
function Repo:_ignorecase()
|
||||||
|
local cfg = self:_config()
|
||||||
|
return cfg.core and cfg.core.ignorecase == "true" or false
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param rel string
|
||||||
|
---@return ow.Git.Status.Entry?
|
||||||
|
function Repo:status_entry_for(rel)
|
||||||
|
local direct = self.status.entries[rel]
|
||||||
|
if direct or not self:_ignorecase() then
|
||||||
|
return direct
|
||||||
|
end
|
||||||
|
local lower = rel:lower()
|
||||||
|
for path, entry in pairs(self.status.entries) do
|
||||||
|
if path:lower() == lower then
|
||||||
|
return entry
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
---@type table<string, true>
|
---@type table<string, true>
|
||||||
local no_repo_dirs = {}
|
local no_repo_dirs = {}
|
||||||
|
|
||||||
---@param event ow.Git.Repo.Event
|
---@overload fun(event: "change", fn: fun(r: ow.Git.Repo, change: ow.Git.Repo.Change, status: ow.Git.Status)): fun()
|
||||||
---@param fn fun(...)
|
|
||||||
---@return fun() unsubscribe
|
|
||||||
function M.on(event, fn)
|
function M.on(event, fn)
|
||||||
return global:on(event, fn)
|
return global:on(event, fn)
|
||||||
end
|
end
|
||||||
@@ -358,8 +681,8 @@ end
|
|||||||
---@param prefix string
|
---@param prefix string
|
||||||
---@param fn fun(buf: integer, r: ow.Git.Repo)
|
---@param fn fun(buf: integer, r: ow.Git.Repo)
|
||||||
---@return fun() unsubscribe
|
---@return fun() unsubscribe
|
||||||
function M.on_uri_refresh(prefix, fn)
|
function M.on_uri_change(prefix, fn)
|
||||||
return M.on("refresh", function(r)
|
return M.on("change", function(r)
|
||||||
for buf in pairs(r.buffers) do
|
for buf in pairs(r.buffers) do
|
||||||
if vim.api.nvim_buf_is_loaded(buf) then
|
if vim.api.nvim_buf_is_loaded(buf) then
|
||||||
local name = vim.api.nvim_buf_get_name(buf)
|
local name = vim.api.nvim_buf_get_name(buf)
|
||||||
@@ -371,16 +694,9 @@ function M.on_uri_refresh(prefix, fn)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param r ow.Git.Repo
|
---@return table<string, ow.Git.Repo>
|
||||||
local function release(r)
|
function M.all()
|
||||||
if repos[r.worktree] ~= r then
|
return repos
|
||||||
return
|
|
||||||
end
|
|
||||||
if next(r.buffers) ~= nil or next(r.tabs) ~= nil then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
r:close()
|
|
||||||
repos[r.worktree] = nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param buf integer
|
---@param buf integer
|
||||||
@@ -403,12 +719,15 @@ local function find_by_path(path)
|
|||||||
if repos[path] then
|
if repos[path] then
|
||||||
return repos[path]
|
return repos[path]
|
||||||
end
|
end
|
||||||
for wt, r in pairs(repos) do
|
local best
|
||||||
|
for wt in pairs(repos) do
|
||||||
if path:sub(1, #wt + 1) == wt .. "/" then
|
if path:sub(1, #wt + 1) == wt .. "/" then
|
||||||
return r
|
if not best or #wt > #best then
|
||||||
|
best = wt
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return nil
|
end
|
||||||
|
return best and repos[best] or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param buf integer
|
---@param buf integer
|
||||||
@@ -434,10 +753,12 @@ end
|
|||||||
---@param arg? integer | string bufnr (default current) or worktree path
|
---@param arg? integer | string bufnr (default current) or worktree path
|
||||||
---@return ow.Git.Repo?
|
---@return ow.Git.Repo?
|
||||||
function M.resolve(arg)
|
function M.resolve(arg)
|
||||||
local existing = M.find(arg)
|
if type(arg) ~= "string" then
|
||||||
|
local existing = find_by_buf(expand_buf(arg))
|
||||||
if existing then
|
if existing then
|
||||||
return existing
|
return existing
|
||||||
end
|
end
|
||||||
|
end
|
||||||
local path
|
local path
|
||||||
if type(arg) == "string" then
|
if type(arg) == "string" then
|
||||||
path = vim.fn.resolve(arg)
|
path = vim.fn.resolve(arg)
|
||||||
@@ -508,7 +829,7 @@ function M.bind(buf, r)
|
|||||||
end
|
end
|
||||||
if prev then
|
if prev then
|
||||||
prev.buffers[buf] = nil
|
prev.buffers[buf] = nil
|
||||||
release(prev)
|
release_if_unused(prev)
|
||||||
end
|
end
|
||||||
r.buffers[buf] = { repo = r }
|
r.buffers[buf] = { repo = r }
|
||||||
end
|
end
|
||||||
@@ -521,12 +842,12 @@ function M.unbind(buf)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
r.buffers[buf] = nil
|
r.buffers[buf] = nil
|
||||||
release(r)
|
release_if_unused(r)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param buf integer
|
---@param buf integer
|
||||||
---@return boolean
|
---@return boolean
|
||||||
local function is_worktree_buf(buf)
|
function M.is_worktree_buf(buf)
|
||||||
if not vim.api.nvim_buf_is_valid(buf) or vim.bo[buf].buftype ~= "" then
|
if not vim.api.nvim_buf_is_valid(buf) or vim.bo[buf].buftype ~= "" then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
@@ -537,7 +858,7 @@ end
|
|||||||
---@param buf? integer
|
---@param buf? integer
|
||||||
function M.track(buf)
|
function M.track(buf)
|
||||||
buf = expand_buf(buf)
|
buf = expand_buf(buf)
|
||||||
if not is_worktree_buf(buf) then
|
if not M.is_worktree_buf(buf) then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local r = M.resolve(buf)
|
local r = M.resolve(buf)
|
||||||
@@ -576,7 +897,7 @@ function M.update_cwd_repo()
|
|||||||
end
|
end
|
||||||
if old then
|
if old then
|
||||||
old.tabs[tab] = nil
|
old.tabs[tab] = nil
|
||||||
release(old)
|
release_if_unused(old)
|
||||||
end
|
end
|
||||||
if new then
|
if new then
|
||||||
new.tabs[tab] = true
|
new.tabs[tab] = true
|
||||||
@@ -589,7 +910,7 @@ function M.release_tab(tab)
|
|||||||
for _, r in pairs(repos) do
|
for _, r in pairs(repos) do
|
||||||
if r.tabs[tab] then
|
if r.tabs[tab] then
|
||||||
r.tabs[tab] = nil
|
r.tabs[tab] = nil
|
||||||
release(r)
|
release_if_unused(r)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -274,6 +274,49 @@ local function strip_dir_slash(path)
|
|||||||
return path
|
return path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param a ow.Git.Status.Entry?
|
||||||
|
---@param b ow.Git.Status.Entry?
|
||||||
|
---@return boolean
|
||||||
|
function M.entry_equal(a, b)
|
||||||
|
if a == nil or b == nil then
|
||||||
|
return a == b
|
||||||
|
end
|
||||||
|
if a.kind ~= b.kind or a.path ~= b.path then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if a.kind == "changed" then
|
||||||
|
---@cast a ow.Git.Status.ChangedEntry
|
||||||
|
---@cast b ow.Git.Status.ChangedEntry
|
||||||
|
return a.staged == b.staged
|
||||||
|
and a.unstaged == b.unstaged
|
||||||
|
and a.orig == b.orig
|
||||||
|
end
|
||||||
|
if a.kind == "unmerged" then
|
||||||
|
---@cast a ow.Git.Status.UnmergedEntry
|
||||||
|
---@cast b ow.Git.Status.UnmergedEntry
|
||||||
|
return a.conflict == b.conflict
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param prior table<string, ow.Git.Status.Entry>
|
||||||
|
---@param next_ table<string, ow.Git.Status.Entry>
|
||||||
|
---@return table<string, true>
|
||||||
|
function M.diff_entries(prior, next_)
|
||||||
|
local paths = {}
|
||||||
|
for path, entry in pairs(next_) do
|
||||||
|
if not M.entry_equal(prior[path], entry) then
|
||||||
|
paths[path] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for path in pairs(prior) do
|
||||||
|
if next_[path] == nil then
|
||||||
|
paths[path] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return paths
|
||||||
|
end
|
||||||
|
|
||||||
---@param stdout string
|
---@param stdout string
|
||||||
---@return ow.Git.Status
|
---@return ow.Git.Status
|
||||||
function M.parse(stdout)
|
function M.parse(stdout)
|
||||||
|
|||||||
@@ -194,6 +194,65 @@ function M.debounce(fn, delay)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@class ow.Git.Util.KeyedDebounceHandle<K>
|
||||||
|
---@field cancel fun(key: K)
|
||||||
|
---@field flush fun(key: K)
|
||||||
|
---@field pending fun(key: K): boolean
|
||||||
|
---@field close fun()
|
||||||
|
|
||||||
|
---@generic K, F: fun(key: K, ...)
|
||||||
|
---@param fn F
|
||||||
|
---@param delay integer
|
||||||
|
---@return F, ow.Git.Util.KeyedDebounceHandle<K>
|
||||||
|
function M.keyed_debounce(fn, delay)
|
||||||
|
---@type table<any, { call: fun(...), handle: ow.Git.Util.DebounceHandle }>
|
||||||
|
local slots = {}
|
||||||
|
|
||||||
|
local function call(key, ...)
|
||||||
|
local t = type(key)
|
||||||
|
assert(
|
||||||
|
t == "string" or t == "number" or t == "boolean",
|
||||||
|
"key must be a primitive (string, number, boolean)"
|
||||||
|
)
|
||||||
|
local slot = slots[key]
|
||||||
|
if not slot then
|
||||||
|
local c, h = M.debounce(function(...)
|
||||||
|
fn(key, ...)
|
||||||
|
end, delay)
|
||||||
|
slot = { call = c, handle = h }
|
||||||
|
slots[key] = slot
|
||||||
|
end
|
||||||
|
slot.call(...)
|
||||||
|
end
|
||||||
|
|
||||||
|
return call,
|
||||||
|
{
|
||||||
|
cancel = function(key)
|
||||||
|
local slot = slots[key]
|
||||||
|
if slot then
|
||||||
|
slot.handle.close()
|
||||||
|
slots[key] = nil
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
flush = function(key)
|
||||||
|
local slot = slots[key]
|
||||||
|
if slot then
|
||||||
|
slot.handle.flush()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
pending = function(key)
|
||||||
|
local slot = slots[key]
|
||||||
|
return slot ~= nil and slot.handle.pending()
|
||||||
|
end,
|
||||||
|
close = function()
|
||||||
|
for _, slot in pairs(slots) do
|
||||||
|
slot.handle.close()
|
||||||
|
end
|
||||||
|
slots = {}
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
---@class ow.Git.Util.ExecOpts
|
---@class ow.Git.Util.ExecOpts
|
||||||
---@field cwd string?
|
---@field cwd string?
|
||||||
---@field stdin string?
|
---@field stdin string?
|
||||||
|
|||||||
@@ -1,175 +0,0 @@
|
|||||||
local Revision = require("git.core.revision")
|
|
||||||
local repo = require("git.core.repo")
|
|
||||||
local util = require("git.core.util")
|
|
||||||
|
|
||||||
local M = {}
|
|
||||||
|
|
||||||
---@class ow.Git.Diff.Side
|
|
||||||
---@field buf integer
|
|
||||||
---@field name string?
|
|
||||||
|
|
||||||
---@class ow.Git.Diff.Pair
|
|
||||||
---@field left ow.Git.Diff.Side
|
|
||||||
---@field right ow.Git.Diff.Side
|
|
||||||
|
|
||||||
---@param win integer
|
|
||||||
---@param enabled boolean
|
|
||||||
function M.set_diff(win, enabled)
|
|
||||||
vim.api.nvim_win_call(win, function()
|
|
||||||
vim.cmd(enabled and "diffthis" or "diffoff")
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param left integer
|
|
||||||
---@param right integer
|
|
||||||
---@param vertical boolean
|
|
||||||
function M.open(left, right, vertical)
|
|
||||||
vim.cmd.normal({ "m'", bang = true })
|
|
||||||
vim.api.nvim_set_current_buf(right)
|
|
||||||
vim.cmd.diffthis()
|
|
||||||
vim.api.nvim_open_win(left, true, {
|
|
||||||
split = vertical and "left" or "above",
|
|
||||||
})
|
|
||||||
vim.cmd.diffthis()
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param left_win integer
|
|
||||||
---@param right_win integer
|
|
||||||
---@param pair ow.Git.Diff.Pair
|
|
||||||
function M.update_pair(left_win, right_win, pair)
|
|
||||||
M.set_diff(left_win, false)
|
|
||||||
M.set_diff(right_win, false)
|
|
||||||
vim.api.nvim_win_set_buf(left_win, pair.left.buf)
|
|
||||||
vim.api.nvim_win_set_buf(right_win, pair.right.buf)
|
|
||||||
for _, side in ipairs({ pair.left, pair.right }) do
|
|
||||||
if side.name then
|
|
||||||
util.set_buf_name(side.buf, side.name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
M.set_diff(left_win, true)
|
|
||||||
M.set_diff(right_win, true)
|
|
||||||
vim.api.nvim_win_set_cursor(left_win, { 1, 0 })
|
|
||||||
vim.api.nvim_win_set_cursor(right_win, { 1, 0 })
|
|
||||||
vim.cmd.syncbind()
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param buf_a integer
|
|
||||||
---@param buf_b integer
|
|
||||||
---@param a_left boolean
|
|
||||||
---@param vertical boolean
|
|
||||||
local function place_pair(buf_a, buf_b, a_left, vertical)
|
|
||||||
if a_left then
|
|
||||||
M.open(buf_a, buf_b, vertical)
|
|
||||||
else
|
|
||||||
M.open(buf_b, buf_a, vertical)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param opts ow.Git.Diff.SplitOpts
|
|
||||||
---@param buf integer
|
|
||||||
---@param rev ow.Git.Revision
|
|
||||||
local function uri_split(opts, buf, rev)
|
|
||||||
local r = repo.resolve(buf)
|
|
||||||
if not r then
|
|
||||||
util.error("git URI buffer has no worktree")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if not rev.path then
|
|
||||||
util.error("git URI has no path, cannot diff against worktree")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local object = require("git.object")
|
|
||||||
|
|
||||||
if opts.rev and opts.rev:find(":", 1, true) then
|
|
||||||
if not r:rev_parse(opts.rev, true) then
|
|
||||||
util.error("invalid rev: %s", opts.rev)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
place_pair(
|
|
||||||
buf,
|
|
||||||
object.buf_for(r, Revision.parse(opts.rev)),
|
|
||||||
false,
|
|
||||||
opts.vertical
|
|
||||||
)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if not opts.rev then
|
|
||||||
local worktree_path = vim.fs.joinpath(r.worktree, rev.path)
|
|
||||||
if not vim.uv.fs_stat(worktree_path) then
|
|
||||||
util.error("worktree file does not exist: %s", rev.path)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local worktree_buf = vim.fn.bufadd(worktree_path)
|
|
||||||
vim.fn.bufload(worktree_buf)
|
|
||||||
place_pair(buf, worktree_buf, true, opts.vertical)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if rev.stage == 1 then
|
|
||||||
util.warning("gD on merge base is ambiguous, use :Gdiffsplit <rev>")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local mapping = {
|
|
||||||
[2] = { Revision.new({ stage = 3, path = rev.path }), true },
|
|
||||||
[3] = { Revision.new({ stage = 2, path = rev.path }), false },
|
|
||||||
[0] = { Revision.new({ base = "HEAD", path = rev.path }), false },
|
|
||||||
}
|
|
||||||
local m = mapping[rev.stage]
|
|
||||||
or { Revision.new({ stage = 0, path = rev.path }), true }
|
|
||||||
local other_rev, left = m[1], m[2]
|
|
||||||
if not r:rev_parse(other_rev:format(), true) then
|
|
||||||
util.error("invalid rev: %s", other_rev:format())
|
|
||||||
return
|
|
||||||
end
|
|
||||||
place_pair(buf, object.buf_for(r, other_rev), left, opts.vertical)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@class ow.Git.Diff.SplitOpts
|
|
||||||
---@field rev string?
|
|
||||||
---@field vertical boolean
|
|
||||||
|
|
||||||
---@param opts ow.Git.Diff.SplitOpts
|
|
||||||
function M.split(opts)
|
|
||||||
local cur_buf = vim.api.nvim_get_current_buf()
|
|
||||||
local cur_path = vim.api.nvim_buf_get_name(cur_buf)
|
|
||||||
|
|
||||||
local cur_rev = require("git.object").parse_uri(cur_path)
|
|
||||||
if cur_rev then
|
|
||||||
return uri_split(opts, cur_buf, cur_rev)
|
|
||||||
end
|
|
||||||
|
|
||||||
if cur_path == "" then
|
|
||||||
util.error("no file in current buffer")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if vim.bo[cur_buf].buftype ~= "" then
|
|
||||||
util.error("cannot diff this buffer (not a worktree file)")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
cur_path = vim.fn.resolve(cur_path)
|
|
||||||
local r = repo.resolve(cur_path)
|
|
||||||
if not r then
|
|
||||||
util.error("not in a git repository")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local rel = vim.fs.relpath(r.worktree, cur_path)
|
|
||||||
|
|
||||||
local rev
|
|
||||||
if not opts.rev then
|
|
||||||
rev = Revision.new({ stage = 0, path = rel })
|
|
||||||
elseif opts.rev:find(":", 1, true) then
|
|
||||||
rev = Revision.parse(opts.rev)
|
|
||||||
else
|
|
||||||
rev = Revision.new({ base = opts.rev, path = rel })
|
|
||||||
end
|
|
||||||
if not r:rev_parse(rev:format(), true) then
|
|
||||||
util.error("invalid rev: %s", rev:format())
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local buf = require("git.object").buf_for(r, rev)
|
|
||||||
place_pair(buf, cur_buf, true, opts.vertical)
|
|
||||||
end
|
|
||||||
|
|
||||||
return M
|
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
local Revision = require("git.core.revision")
|
||||||
|
local object = require("git.object")
|
||||||
|
local repo = require("git.core.repo")
|
||||||
|
local util = require("git.core.util")
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
---@class ow.Git.Diffsplit.OpenOpts
|
||||||
|
---@field target string?
|
||||||
|
---@field mods vim.api.keyset.cmd.mods?
|
||||||
|
|
||||||
|
---@param cur_buf integer
|
||||||
|
---@return string? target
|
||||||
|
---@return string? err
|
||||||
|
local function infer_target(cur_buf)
|
||||||
|
local cur_name = vim.api.nvim_buf_get_name(cur_buf)
|
||||||
|
local cur_rev = object.parse_uri(cur_name)
|
||||||
|
if cur_rev then
|
||||||
|
local r = repo.resolve(cur_buf)
|
||||||
|
if not r then
|
||||||
|
return nil, "git URI buffer has no worktree"
|
||||||
|
end
|
||||||
|
if not cur_rev.path then
|
||||||
|
return nil, "git URI has no path, cannot diff against worktree"
|
||||||
|
end
|
||||||
|
local worktree_path = vim.fs.joinpath(r.worktree, cur_rev.path)
|
||||||
|
if not vim.uv.fs_stat(worktree_path) then
|
||||||
|
return nil, "worktree file does not exist: " .. cur_rev.path
|
||||||
|
end
|
||||||
|
return worktree_path, nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if cur_name == "" then
|
||||||
|
return nil, "no file in current buffer"
|
||||||
|
end
|
||||||
|
if vim.bo[cur_buf].buftype ~= "" then
|
||||||
|
return nil, "cannot diff this buffer (not a worktree file)"
|
||||||
|
end
|
||||||
|
local resolved = vim.fn.resolve(cur_name)
|
||||||
|
local r = repo.resolve(resolved)
|
||||||
|
if not r then
|
||||||
|
return nil, "not in a git repository"
|
||||||
|
end
|
||||||
|
local rel = vim.fs.relpath(r.worktree, resolved)
|
||||||
|
if not rel then
|
||||||
|
return nil, "current buffer is outside the worktree"
|
||||||
|
end
|
||||||
|
return object.format_uri(Revision.new({ stage = 0, path = rel })), nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param target string
|
||||||
|
---@param cur_buf integer
|
||||||
|
---@return string? resolved
|
||||||
|
---@return string? err
|
||||||
|
local function resolve_target(target, cur_buf)
|
||||||
|
if vim.startswith(target, object.URI_PREFIX) then
|
||||||
|
return target, nil
|
||||||
|
end
|
||||||
|
if vim.fn.filereadable(target) == 1 then
|
||||||
|
return target, nil
|
||||||
|
end
|
||||||
|
local cur_name = vim.api.nvim_buf_get_name(cur_buf)
|
||||||
|
local cur_rev = object.parse_uri(cur_name)
|
||||||
|
local r, rel
|
||||||
|
if cur_rev and cur_rev.path then
|
||||||
|
r = repo.resolve(cur_buf)
|
||||||
|
rel = cur_rev.path
|
||||||
|
elseif cur_name ~= "" then
|
||||||
|
local resolved = vim.fn.resolve(cur_name)
|
||||||
|
r = repo.resolve(resolved)
|
||||||
|
if r then
|
||||||
|
rel = vim.fs.relpath(r.worktree, resolved)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not r then
|
||||||
|
return nil, "not in a git repository"
|
||||||
|
end
|
||||||
|
if not rel then
|
||||||
|
return nil, "current buffer has no path"
|
||||||
|
end
|
||||||
|
if not r:rev_parse(target, true) then
|
||||||
|
return nil, "invalid rev: " .. target
|
||||||
|
end
|
||||||
|
return object.format_uri(Revision.new({ base = target, path = rel })), nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param cur_buf integer
|
||||||
|
---@param target string
|
||||||
|
---@return 'aboveleft'|'belowright'|nil
|
||||||
|
local function default_split(cur_buf, target)
|
||||||
|
local cur_rev = object.parse_uri(vim.api.nvim_buf_get_name(cur_buf))
|
||||||
|
local target_rev = object.parse_uri(target)
|
||||||
|
if not cur_rev and target_rev then
|
||||||
|
return "aboveleft"
|
||||||
|
end
|
||||||
|
if cur_rev and not target_rev then
|
||||||
|
return "belowright"
|
||||||
|
end
|
||||||
|
if cur_rev and target_rev then
|
||||||
|
if cur_rev.stage == 0 and target_rev.base then
|
||||||
|
return "aboveleft"
|
||||||
|
end
|
||||||
|
if cur_rev.base and target_rev.stage == 0 then
|
||||||
|
return "belowright"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param opts? ow.Git.Diffsplit.OpenOpts
|
||||||
|
function M.open(opts)
|
||||||
|
opts = opts or {}
|
||||||
|
local cur_buf = vim.api.nvim_get_current_buf()
|
||||||
|
local target, err
|
||||||
|
if opts.target then
|
||||||
|
target, err = resolve_target(opts.target, cur_buf)
|
||||||
|
else
|
||||||
|
target, err = infer_target(cur_buf)
|
||||||
|
end
|
||||||
|
if not target then
|
||||||
|
util.error("%s", err or "no diff target")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local mods = opts.mods
|
||||||
|
if not mods or mods.split == nil then
|
||||||
|
local placement = default_split(cur_buf, target)
|
||||||
|
if placement then
|
||||||
|
mods = vim.tbl_extend("force", mods or {}, { split = placement })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
vim.cmd.diffsplit({ args = { target }, mods = mods })
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
@@ -0,0 +1,970 @@
|
|||||||
|
local repo = require("git.core.repo")
|
||||||
|
local util = require("git.core.util")
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local NS_SIGNS = vim.api.nvim_create_namespace("ow.git.hunks")
|
||||||
|
local NS_OVERLAY = vim.api.nvim_create_namespace("ow.git.hunks.overlay")
|
||||||
|
|
||||||
|
---@alias ow.Git.Hunks.HunkType "add"|"change"|"delete"
|
||||||
|
|
||||||
|
---@class ow.Git.Hunks.Hunk
|
||||||
|
---@field old_start integer 1-indexed first old line
|
||||||
|
---@field old_count integer
|
||||||
|
---@field new_start integer 1-indexed first new line
|
||||||
|
---@field new_count integer
|
||||||
|
---@field type ow.Git.Hunks.HunkType
|
||||||
|
---@field old_lines string[]
|
||||||
|
---@field new_lines string[]
|
||||||
|
|
||||||
|
---@class ow.Git.Hunks.BufState
|
||||||
|
---@field repo ow.Git.Repo
|
||||||
|
---@field rel string
|
||||||
|
---@field index string[]?
|
||||||
|
---@field index_sha string?
|
||||||
|
---@field head string[]?
|
||||||
|
---@field head_sha string?
|
||||||
|
---@field index_hl { src: string[], lines: table[][]? }?
|
||||||
|
---@field hunks ow.Git.Hunks.Hunk[]
|
||||||
|
---@field staged ow.Git.Hunks.Hunk[]
|
||||||
|
---@field overlay boolean
|
||||||
|
---@field autocmds integer[]
|
||||||
|
|
||||||
|
---@type table<integer, ow.Git.Hunks.BufState>
|
||||||
|
local states = {}
|
||||||
|
|
||||||
|
---@param buf integer
|
||||||
|
---@return ow.Git.Hunks.BufState?
|
||||||
|
function M.state(buf)
|
||||||
|
return states[buf]
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param buf integer?
|
||||||
|
---@return integer
|
||||||
|
local function resolve_buf(buf)
|
||||||
|
return buf and buf ~= 0 and buf or vim.api.nvim_get_current_buf()
|
||||||
|
end
|
||||||
|
|
||||||
|
---Mirror the hunk-affecting parts of the user's 'diffopt' so the gutter
|
||||||
|
---lines up with what `:diffsplit` shows.
|
||||||
|
---@return table
|
||||||
|
local function diff_opts()
|
||||||
|
local opts = { result_type = "indices", algorithm = "myers" }
|
||||||
|
for _, item in ipairs(vim.split(vim.o.diffopt, ",", { plain = true })) do
|
||||||
|
if item == "indent-heuristic" then
|
||||||
|
opts.indent_heuristic = true
|
||||||
|
else
|
||||||
|
local algorithm = item:match("^algorithm:(.+)$")
|
||||||
|
if algorithm then
|
||||||
|
opts.algorithm = algorithm
|
||||||
|
end
|
||||||
|
local linematch = item:match("^linematch:(%d+)$")
|
||||||
|
if linematch then
|
||||||
|
opts.linematch = tonumber(linematch)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return opts
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param old_lines string[]
|
||||||
|
---@param new_lines string[]
|
||||||
|
---@return ow.Git.Hunks.Hunk[]
|
||||||
|
local function compute_hunks(old_lines, new_lines)
|
||||||
|
local raw = vim.text.diff(
|
||||||
|
table.concat(old_lines, "\n"),
|
||||||
|
table.concat(new_lines, "\n"),
|
||||||
|
diff_opts()
|
||||||
|
)
|
||||||
|
---@type ow.Git.Hunks.Hunk[]
|
||||||
|
local hunks = {}
|
||||||
|
if type(raw) ~= "table" then
|
||||||
|
return hunks
|
||||||
|
end
|
||||||
|
for _, h in ipairs(raw) do
|
||||||
|
local os_ = h[1] --[[@as integer]]
|
||||||
|
local oc = h[2] --[[@as integer]]
|
||||||
|
local ns_ = h[3] --[[@as integer]]
|
||||||
|
local nc = h[4] --[[@as integer]]
|
||||||
|
local typ ---@type ow.Git.Hunks.HunkType
|
||||||
|
if oc == 0 then
|
||||||
|
typ = "add"
|
||||||
|
elseif nc == 0 then
|
||||||
|
typ = "delete"
|
||||||
|
else
|
||||||
|
typ = "change"
|
||||||
|
end
|
||||||
|
local old = {}
|
||||||
|
if typ ~= "add" then
|
||||||
|
for i = os_, os_ + oc - 1 do
|
||||||
|
table.insert(old, old_lines[i] or "")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local new = {}
|
||||||
|
if typ ~= "delete" then
|
||||||
|
for i = ns_, ns_ + nc - 1 do
|
||||||
|
table.insert(new, new_lines[i] or "")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert(hunks, {
|
||||||
|
old_start = os_,
|
||||||
|
old_count = oc,
|
||||||
|
new_start = ns_,
|
||||||
|
new_count = nc,
|
||||||
|
type = typ,
|
||||||
|
old_lines = old,
|
||||||
|
new_lines = new,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
return hunks
|
||||||
|
end
|
||||||
|
|
||||||
|
---@type table<ow.Git.Hunks.HunkType, string>
|
||||||
|
local DEFAULT_SIGNS = { add = "┃", change = "┃", delete = "▁" }
|
||||||
|
|
||||||
|
---@return table<ow.Git.Hunks.HunkType, string>
|
||||||
|
local function resolve_signs()
|
||||||
|
local cfg = vim.g.git_hunk_signs
|
||||||
|
if type(cfg) ~= "table" then
|
||||||
|
return DEFAULT_SIGNS
|
||||||
|
end
|
||||||
|
return vim.tbl_extend("force", DEFAULT_SIGNS, cfg)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@type table<ow.Git.Hunks.HunkType, string>
|
||||||
|
local SIGN_HL = {
|
||||||
|
add = "GitHunkAdded",
|
||||||
|
change = "GitHunkChanged",
|
||||||
|
delete = "GitHunkRemoved",
|
||||||
|
}
|
||||||
|
|
||||||
|
---@type table<ow.Git.Hunks.HunkType, string>
|
||||||
|
local STAGED_SIGN_HL = {
|
||||||
|
add = "GitHunkStagedAdded",
|
||||||
|
change = "GitHunkStagedChanged",
|
||||||
|
delete = "GitHunkStagedRemoved",
|
||||||
|
}
|
||||||
|
|
||||||
|
---@param h ow.Git.Hunks.Hunk
|
||||||
|
---@param line_count integer
|
||||||
|
---@return integer[] 0-indexed buffer rows for the hunk
|
||||||
|
local function hunk_rows(h, line_count)
|
||||||
|
if h.type == "delete" then
|
||||||
|
local row = math.max(h.new_start, 1) - 1
|
||||||
|
if row >= line_count then
|
||||||
|
row = math.max(line_count - 1, 0)
|
||||||
|
end
|
||||||
|
return { row }
|
||||||
|
end
|
||||||
|
local rows = {}
|
||||||
|
for r = h.new_start, h.new_start + h.new_count - 1 do
|
||||||
|
local row = r - 1
|
||||||
|
if row >= 0 and row < line_count then
|
||||||
|
table.insert(rows, row)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return rows
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param h ow.Git.Hunks.Hunk
|
||||||
|
---@return integer 1-indexed last index line the hunk occupies
|
||||||
|
local function index_end(h)
|
||||||
|
if h.old_count == 0 then
|
||||||
|
return h.old_start
|
||||||
|
end
|
||||||
|
return h.old_start + h.old_count - 1
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param unstaged ow.Git.Hunks.Hunk[]
|
||||||
|
---@param iline integer 1-indexed index line
|
||||||
|
---@return integer? 1-indexed buffer line
|
||||||
|
local function index_to_buffer(unstaged, iline)
|
||||||
|
local delta = 0
|
||||||
|
for _, h in ipairs(unstaged) do
|
||||||
|
if
|
||||||
|
h.old_count > 0
|
||||||
|
and iline >= h.old_start
|
||||||
|
and iline <= index_end(h)
|
||||||
|
then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
if iline > index_end(h) then
|
||||||
|
delta = delta + h.new_count - h.old_count
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return iline + delta
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param state ow.Git.Hunks.BufState
|
||||||
|
---@param line_count integer
|
||||||
|
---@return { row: integer, hunk: ow.Git.Hunks.Hunk }[] row is a 0-indexed buffer row
|
||||||
|
local function staged_signs(state, line_count)
|
||||||
|
local out = {}
|
||||||
|
for _, h in ipairs(state.staged) do
|
||||||
|
local index_lines = {}
|
||||||
|
if h.type == "delete" then
|
||||||
|
table.insert(index_lines, math.max(h.new_start, 1))
|
||||||
|
else
|
||||||
|
for i = h.new_start, h.new_start + h.new_count - 1 do
|
||||||
|
table.insert(index_lines, i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, iline in ipairs(index_lines) do
|
||||||
|
local bline = index_to_buffer(state.hunks, iline)
|
||||||
|
if bline then
|
||||||
|
local row = math.min(math.max(bline - 1, 0), line_count - 1)
|
||||||
|
table.insert(out, { row = row, hunk = h })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param buf integer
|
||||||
|
local function render_signs(buf)
|
||||||
|
if not vim.api.nvim_buf_is_valid(buf) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
vim.api.nvim_buf_clear_namespace(buf, NS_SIGNS, 0, -1)
|
||||||
|
local state = states[buf]
|
||||||
|
if not state or state.overlay then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local signs = resolve_signs()
|
||||||
|
local line_count = vim.api.nvim_buf_line_count(buf)
|
||||||
|
local signed = {}
|
||||||
|
for _, h in ipairs(state.hunks) do
|
||||||
|
for _, row in ipairs(hunk_rows(h, line_count)) do
|
||||||
|
signed[row] = true
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, buf, NS_SIGNS, row, 0, {
|
||||||
|
sign_text = signs[h.type],
|
||||||
|
sign_hl_group = SIGN_HL[h.type],
|
||||||
|
priority = 100,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, s in ipairs(staged_signs(state, line_count)) do
|
||||||
|
if not signed[s.row] then
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, buf, NS_SIGNS, s.row, 0, {
|
||||||
|
sign_text = signs[s.hunk.type],
|
||||||
|
sign_hl_group = STAGED_SIGN_HL[s.hunk.type],
|
||||||
|
priority = 100,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local SKIP_CAPTURES = { spell = true, nospell = true, conceal = true }
|
||||||
|
|
||||||
|
---@param buf integer
|
||||||
|
---@param lines string[]
|
||||||
|
---@return table[][]?
|
||||||
|
local function highlight_index(buf, lines)
|
||||||
|
if not vim.treesitter.highlighter.active[buf] then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local got, parser = pcall(vim.treesitter.get_parser, buf)
|
||||||
|
if not got or not parser then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local lang = parser:lang()
|
||||||
|
local query = vim.treesitter.query.get(lang, "highlights")
|
||||||
|
if not query then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local source = table.concat(lines, "\n")
|
||||||
|
local got_root, root = pcall(function()
|
||||||
|
local trees = vim.treesitter.get_string_parser(source, lang):parse()
|
||||||
|
local tree = trees and trees[1]
|
||||||
|
return tree and tree:root()
|
||||||
|
end)
|
||||||
|
if not got_root or not root then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
---@type table<integer, table<integer, string>>
|
||||||
|
local groups = {}
|
||||||
|
for id, node in query:iter_captures(root, source) do
|
||||||
|
local name = query.captures[id]
|
||||||
|
if name and name:sub(1, 1) ~= "_" and not SKIP_CAPTURES[name] then
|
||||||
|
local sr, sc, er, ec = node:range()
|
||||||
|
for row = sr, math.min(er, #lines - 1) do
|
||||||
|
local row_groups = groups[row] or {}
|
||||||
|
groups[row] = row_groups
|
||||||
|
local from = row == sr and sc or 0
|
||||||
|
local to = row == er and ec or #(lines[row + 1] or "")
|
||||||
|
for col = from, to - 1 do
|
||||||
|
row_groups[col] = name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local out = {}
|
||||||
|
for row = 0, #lines - 1 do
|
||||||
|
local line = lines[row + 1] or ""
|
||||||
|
local row_groups = groups[row] or {}
|
||||||
|
local chunks = {}
|
||||||
|
local col = 0
|
||||||
|
while col < #line do
|
||||||
|
local name = row_groups[col]
|
||||||
|
local stop = col + 1
|
||||||
|
while stop < #line and row_groups[stop] == name do
|
||||||
|
stop = stop + 1
|
||||||
|
end
|
||||||
|
local hl ---@type string|string[]
|
||||||
|
if name then
|
||||||
|
hl = { "GitHunkDeleteLine", "@" .. name }
|
||||||
|
else
|
||||||
|
hl = "GitHunkDeleteLine"
|
||||||
|
end
|
||||||
|
table.insert(chunks, { line:sub(col + 1, stop), hl })
|
||||||
|
col = stop
|
||||||
|
end
|
||||||
|
out[row + 1] = chunks
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param h ow.Git.Hunks.Hunk
|
||||||
|
---@param hl_lines table[][]? per-index-line syntax chunks, or nil
|
||||||
|
---@return table[]
|
||||||
|
local function delete_virt_lines(h, hl_lines)
|
||||||
|
local width = vim.o.columns
|
||||||
|
local virt = {}
|
||||||
|
for i, line in ipairs(h.old_lines) do
|
||||||
|
local pad = math.max(width - vim.api.nvim_strwidth(line), 0)
|
||||||
|
local cached = hl_lines and hl_lines[h.old_start + i - 1]
|
||||||
|
if cached then
|
||||||
|
local chunks = vim.list_extend({}, cached)
|
||||||
|
table.insert(chunks, {
|
||||||
|
string.rep(" ", pad),
|
||||||
|
"GitHunkDeleteLine",
|
||||||
|
})
|
||||||
|
table.insert(virt, chunks)
|
||||||
|
else
|
||||||
|
table.insert(virt, {
|
||||||
|
{ line .. string.rep(" ", pad), "GitHunkDeleteLine" },
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return virt
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param state ow.Git.Hunks.BufState
|
||||||
|
---@param buf integer
|
||||||
|
---@return table[][]?
|
||||||
|
local function index_spans(state, buf)
|
||||||
|
if not state.index then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local cache = state.index_hl
|
||||||
|
if cache and cache.src == state.index then
|
||||||
|
return cache.lines
|
||||||
|
end
|
||||||
|
local lines = highlight_index(buf, state.index)
|
||||||
|
state.index_hl = { src = state.index, lines = lines }
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param buf integer
|
||||||
|
local function render_overlay(buf)
|
||||||
|
if not vim.api.nvim_buf_is_valid(buf) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
vim.api.nvim_buf_clear_namespace(buf, NS_OVERLAY, 0, -1)
|
||||||
|
local state = states[buf]
|
||||||
|
if not state or not state.overlay then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local line_count = vim.api.nvim_buf_line_count(buf)
|
||||||
|
local hl_lines = index_spans(state, buf)
|
||||||
|
for _, h in ipairs(state.hunks) do
|
||||||
|
if h.type ~= "delete" then
|
||||||
|
for r = h.new_start, h.new_start + h.new_count - 1 do
|
||||||
|
local row = r - 1
|
||||||
|
if row >= 0 and row < line_count then
|
||||||
|
pcall(
|
||||||
|
vim.api.nvim_buf_set_extmark,
|
||||||
|
buf,
|
||||||
|
NS_OVERLAY,
|
||||||
|
row,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
line_hl_group = "GitHunkAddLine",
|
||||||
|
priority = 100,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if h.type ~= "add" then
|
||||||
|
local row, above
|
||||||
|
if h.type == "delete" then
|
||||||
|
if h.new_start <= 0 then
|
||||||
|
row, above = 0, true
|
||||||
|
elseif h.new_start >= line_count then
|
||||||
|
row, above = math.max(line_count - 1, 0), false
|
||||||
|
else
|
||||||
|
row, above = h.new_start, true
|
||||||
|
end
|
||||||
|
else
|
||||||
|
row, above = math.max(h.new_start - 1, 0), true
|
||||||
|
end
|
||||||
|
pcall(vim.api.nvim_buf_set_extmark, buf, NS_OVERLAY, row, 0, {
|
||||||
|
virt_lines = delete_virt_lines(h, hl_lines),
|
||||||
|
virt_lines_above = above,
|
||||||
|
right_gravity = false,
|
||||||
|
invalidate = true,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param buf integer
|
||||||
|
local function render(buf)
|
||||||
|
render_signs(buf)
|
||||||
|
render_overlay(buf)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param state ow.Git.Hunks.BufState
|
||||||
|
---@param buf integer
|
||||||
|
---@param rev string
|
||||||
|
---@param want string? the wanted blob sha
|
||||||
|
---@param have string? the currently-loaded blob sha
|
||||||
|
---@param apply fun(lines: string[]?, sha: string?)
|
||||||
|
---@param after fun()
|
||||||
|
local function ensure_content(state, buf, rev, want, have, apply, after)
|
||||||
|
if not want then
|
||||||
|
apply(nil, nil)
|
||||||
|
return after()
|
||||||
|
end
|
||||||
|
if want == have then
|
||||||
|
return after()
|
||||||
|
end
|
||||||
|
util.git({ "cat-file", "-p", rev }, {
|
||||||
|
cwd = state.repo.worktree,
|
||||||
|
silent = true,
|
||||||
|
on_exit = function(res)
|
||||||
|
if states[buf] ~= state or not vim.api.nvim_buf_is_valid(buf) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if res.code == 0 then
|
||||||
|
apply(util.split_lines(res.stdout or ""), want)
|
||||||
|
else
|
||||||
|
apply(nil, nil)
|
||||||
|
end
|
||||||
|
after()
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param buf integer
|
||||||
|
local function recompute(buf)
|
||||||
|
if not vim.api.nvim_buf_is_valid(buf) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local state = states[buf]
|
||||||
|
if not state then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local r = state.repo
|
||||||
|
ensure_content(
|
||||||
|
state,
|
||||||
|
buf,
|
||||||
|
":0:" .. state.rel,
|
||||||
|
r:index_sha(state.rel),
|
||||||
|
state.index_sha,
|
||||||
|
function(lines, sha)
|
||||||
|
state.index = lines
|
||||||
|
state.index_sha = sha
|
||||||
|
end,
|
||||||
|
function()
|
||||||
|
ensure_content(
|
||||||
|
state,
|
||||||
|
buf,
|
||||||
|
"HEAD:" .. state.rel,
|
||||||
|
r:head_sha(state.rel),
|
||||||
|
state.head_sha,
|
||||||
|
function(lines, sha)
|
||||||
|
state.head = lines
|
||||||
|
state.head_sha = sha
|
||||||
|
end,
|
||||||
|
function()
|
||||||
|
local new =
|
||||||
|
vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||||
|
state.hunks = state.index
|
||||||
|
and compute_hunks(state.index, new)
|
||||||
|
or {}
|
||||||
|
state.staged = state.head
|
||||||
|
and state.index
|
||||||
|
and compute_hunks(state.head, state.index)
|
||||||
|
or {}
|
||||||
|
render(buf)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
local schedule, sched_handle = util.keyed_debounce(recompute, 100)
|
||||||
|
|
||||||
|
---@param buf integer
|
||||||
|
function M._flush(buf)
|
||||||
|
sched_handle.flush(buf)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param buf integer
|
||||||
|
function M.attach(buf)
|
||||||
|
if states[buf] then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not repo.is_worktree_buf(buf) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local r = repo.find(buf)
|
||||||
|
if not r then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local rel = vim.fs.relpath(r.worktree, vim.fn.resolve(vim.api.nvim_buf_get_name(buf)))
|
||||||
|
if not rel then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
---@type ow.Git.Hunks.BufState
|
||||||
|
local state = {
|
||||||
|
repo = r,
|
||||||
|
rel = rel,
|
||||||
|
index = nil,
|
||||||
|
index_sha = nil,
|
||||||
|
head = nil,
|
||||||
|
head_sha = nil,
|
||||||
|
hunks = {},
|
||||||
|
staged = {},
|
||||||
|
overlay = vim.g.git_hunk_overlay_default == true,
|
||||||
|
autocmds = {},
|
||||||
|
}
|
||||||
|
states[buf] = state
|
||||||
|
|
||||||
|
local group =
|
||||||
|
vim.api.nvim_create_augroup("ow.git.hunks." .. buf, { clear = true })
|
||||||
|
table.insert(
|
||||||
|
state.autocmds,
|
||||||
|
vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, {
|
||||||
|
group = group,
|
||||||
|
buffer = buf,
|
||||||
|
callback = function()
|
||||||
|
schedule(buf)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
table.insert(
|
||||||
|
state.autocmds,
|
||||||
|
vim.api.nvim_create_autocmd("BufWritePost", {
|
||||||
|
group = group,
|
||||||
|
buffer = buf,
|
||||||
|
callback = function()
|
||||||
|
schedule(buf)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
schedule(buf)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param buf integer
|
||||||
|
function M.detach(buf)
|
||||||
|
local state = states[buf]
|
||||||
|
if not state then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if vim.api.nvim_buf_is_valid(buf) then
|
||||||
|
vim.api.nvim_buf_clear_namespace(buf, NS_SIGNS, 0, -1)
|
||||||
|
vim.api.nvim_buf_clear_namespace(buf, NS_OVERLAY, 0, -1)
|
||||||
|
end
|
||||||
|
for _, id in ipairs(state.autocmds) do
|
||||||
|
pcall(vim.api.nvim_del_autocmd, id)
|
||||||
|
end
|
||||||
|
pcall(vim.api.nvim_del_augroup_by_name, "ow.git.hunks." .. buf)
|
||||||
|
sched_handle.cancel(buf)
|
||||||
|
states[buf] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param buf integer?
|
||||||
|
function M.toggle_overlay(buf)
|
||||||
|
buf = resolve_buf(buf)
|
||||||
|
local state = states[buf]
|
||||||
|
if not state then
|
||||||
|
util.warning("git hunks: buffer not attached")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
state.overlay = not state.overlay
|
||||||
|
render(buf)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param hunks ow.Git.Hunks.Hunk[]
|
||||||
|
---@param row integer 1-indexed cursor line
|
||||||
|
---@return ow.Git.Hunks.Hunk?
|
||||||
|
local function hunk_at(hunks, row)
|
||||||
|
for _, h in ipairs(hunks) do
|
||||||
|
if h.type == "delete" then
|
||||||
|
if math.max(h.new_start, 1) == row then
|
||||||
|
return h
|
||||||
|
end
|
||||||
|
elseif row >= h.new_start and row <= h.new_start + h.new_count - 1 then
|
||||||
|
return h
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param state ow.Git.Hunks.BufState
|
||||||
|
---@param buf integer
|
||||||
|
---@param row integer 1-indexed cursor line
|
||||||
|
---@return ow.Git.Hunks.Hunk?
|
||||||
|
local function staged_hunk_at(state, buf, row)
|
||||||
|
local line_count = vim.api.nvim_buf_line_count(buf)
|
||||||
|
for _, s in ipairs(staged_signs(state, line_count)) do
|
||||||
|
if s.row == row - 1 then
|
||||||
|
return s.hunk
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param buf integer?
|
||||||
|
---@return integer buf
|
||||||
|
---@return ow.Git.Hunks.BufState? state
|
||||||
|
---@return ow.Git.Hunks.Hunk? hunk
|
||||||
|
local function cursor_hunk(buf)
|
||||||
|
buf = resolve_buf(buf)
|
||||||
|
local state = states[buf]
|
||||||
|
if not state then
|
||||||
|
return buf, nil, nil
|
||||||
|
end
|
||||||
|
return buf, state, hunk_at(state.hunks, vim.api.nvim_win_get_cursor(0)[1])
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param h ow.Git.Hunks.Hunk
|
||||||
|
---@return integer 1-indexed buffer line to anchor the cursor on
|
||||||
|
local function anchor_line(h)
|
||||||
|
if h.type == "delete" then
|
||||||
|
return math.max(h.new_start, 1)
|
||||||
|
end
|
||||||
|
return h.new_start
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param direction "next"|"prev"
|
||||||
|
function M.nav(direction)
|
||||||
|
local buf = vim.api.nvim_get_current_buf()
|
||||||
|
local state = states[buf]
|
||||||
|
if not state or #state.hunks == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local cur = vim.api.nvim_win_get_cursor(0)[1]
|
||||||
|
local hunks = state.hunks
|
||||||
|
local target = direction == "next" and hunks[1] or hunks[#hunks]
|
||||||
|
if direction == "next" then
|
||||||
|
for _, h in ipairs(hunks) do
|
||||||
|
if anchor_line(h) > cur then
|
||||||
|
target = h
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
for i = #hunks, 1, -1 do
|
||||||
|
if anchor_line(hunks[i]) < cur then
|
||||||
|
target = hunks[i]
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not target then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
vim.api.nvim_win_set_cursor(0, { anchor_line(target), 0 })
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param h ow.Git.Hunks.Hunk
|
||||||
|
---@return string[]
|
||||||
|
local function hunk_body(h)
|
||||||
|
local lines = {
|
||||||
|
string.format(
|
||||||
|
"@@ -%d,%d +%d,%d @@",
|
||||||
|
h.old_start,
|
||||||
|
h.old_count,
|
||||||
|
h.new_start,
|
||||||
|
h.new_count
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for _, l in ipairs(h.old_lines) do
|
||||||
|
table.insert(lines, "-" .. l)
|
||||||
|
end
|
||||||
|
for _, l in ipairs(h.new_lines) do
|
||||||
|
table.insert(lines, "+" .. l)
|
||||||
|
end
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
local PATCH_CONTEXT = 3
|
||||||
|
|
||||||
|
---@param h ow.Git.Hunks.Hunk
|
||||||
|
---@return integer old_before count of old lines before the hunk's changed content
|
||||||
|
---@return integer new_before count of new lines before the hunk's changed content
|
||||||
|
local function hunk_offsets(h)
|
||||||
|
if h.type == "add" then
|
||||||
|
return h.old_start, h.new_start - 1
|
||||||
|
elseif h.type == "delete" then
|
||||||
|
return h.old_start - 1, h.new_start
|
||||||
|
end
|
||||||
|
return h.old_start - 1, h.new_start - 1
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param h ow.Git.Hunks.Hunk
|
||||||
|
---@return ow.Git.Hunks.Hunk
|
||||||
|
local function invert(h)
|
||||||
|
local typ ---@type ow.Git.Hunks.HunkType
|
||||||
|
if h.type == "add" then
|
||||||
|
typ = "delete"
|
||||||
|
elseif h.type == "delete" then
|
||||||
|
typ = "add"
|
||||||
|
else
|
||||||
|
typ = "change"
|
||||||
|
end
|
||||||
|
return {
|
||||||
|
old_start = h.new_start,
|
||||||
|
old_count = h.new_count,
|
||||||
|
new_start = h.old_start,
|
||||||
|
new_count = h.old_count,
|
||||||
|
type = typ,
|
||||||
|
old_lines = h.new_lines,
|
||||||
|
new_lines = h.old_lines,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param h ow.Git.Hunks.Hunk
|
||||||
|
---@param old_lines string[]
|
||||||
|
---@param rel string
|
||||||
|
---@return string patch
|
||||||
|
---@return boolean zero_context
|
||||||
|
local function build_patch(h, old_lines, rel)
|
||||||
|
local old_before, new_before = hunk_offsets(h)
|
||||||
|
local pre = {}
|
||||||
|
for i = math.max(old_before - PATCH_CONTEXT + 1, 1), old_before do
|
||||||
|
pre[#pre + 1] = old_lines[i] or ""
|
||||||
|
end
|
||||||
|
local post = {}
|
||||||
|
local after = old_before + h.old_count
|
||||||
|
for i = after + 1, math.min(after + PATCH_CONTEXT, #old_lines) do
|
||||||
|
post[#post + 1] = old_lines[i] or ""
|
||||||
|
end
|
||||||
|
local old_n = #pre + h.old_count + #post
|
||||||
|
local new_n = #pre + h.new_count + #post
|
||||||
|
local old_start = old_n > 0 and old_before - #pre + 1 or old_before
|
||||||
|
local new_start = new_n > 0 and new_before - #pre + 1 or new_before
|
||||||
|
local body = {
|
||||||
|
string.format(
|
||||||
|
"@@ -%d,%d +%d,%d @@",
|
||||||
|
old_start,
|
||||||
|
old_n,
|
||||||
|
new_start,
|
||||||
|
new_n
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for _, l in ipairs(pre) do
|
||||||
|
body[#body + 1] = " " .. l
|
||||||
|
end
|
||||||
|
for _, l in ipairs(h.old_lines) do
|
||||||
|
body[#body + 1] = "-" .. l
|
||||||
|
end
|
||||||
|
for _, l in ipairs(h.new_lines) do
|
||||||
|
body[#body + 1] = "+" .. l
|
||||||
|
end
|
||||||
|
for _, l in ipairs(post) do
|
||||||
|
body[#body + 1] = " " .. l
|
||||||
|
end
|
||||||
|
local lines = { "--- a/" .. rel, "+++ b/" .. rel }
|
||||||
|
vim.list_extend(lines, body)
|
||||||
|
return table.concat(lines, "\n") .. "\n", #pre == 0 and #post == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param state ow.Git.Hunks.BufState
|
||||||
|
---@param buf integer
|
||||||
|
---@param patch string
|
||||||
|
---@param zero_context boolean
|
||||||
|
local function apply_patch(state, buf, patch, zero_context)
|
||||||
|
local args = { "apply", "--cached" }
|
||||||
|
if zero_context then
|
||||||
|
table.insert(args, "--unidiff-zero")
|
||||||
|
end
|
||||||
|
table.insert(args, "-")
|
||||||
|
util.git(args, {
|
||||||
|
cwd = state.repo.worktree,
|
||||||
|
stdin = patch,
|
||||||
|
on_exit = function(res)
|
||||||
|
if res.code ~= 0 then
|
||||||
|
util.error("git apply failed: %s", vim.trim(res.stderr or ""))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local s = states[buf]
|
||||||
|
if s then
|
||||||
|
s.index_sha = nil
|
||||||
|
schedule(buf)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param buf? integer
|
||||||
|
function M.toggle_stage(buf)
|
||||||
|
buf = resolve_buf(buf)
|
||||||
|
local state = states[buf]
|
||||||
|
if not state then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local row = vim.api.nvim_win_get_cursor(0)[1]
|
||||||
|
local unstaged = hunk_at(state.hunks, row)
|
||||||
|
if unstaged and state.index then
|
||||||
|
local patch, zero = build_patch(unstaged, state.index, state.rel)
|
||||||
|
apply_patch(state, buf, patch, zero)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local staged = staged_hunk_at(state, buf, row)
|
||||||
|
if staged and state.index then
|
||||||
|
local patch, zero = build_patch(invert(staged), state.index, state.rel)
|
||||||
|
apply_patch(state, buf, patch, zero)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
util.warning("git hunks: no hunk at cursor")
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param buf? integer
|
||||||
|
function M.reset_hunk(buf)
|
||||||
|
local target, state, h = cursor_hunk(buf)
|
||||||
|
if not state then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not h then
|
||||||
|
util.warning("git hunks: no hunk at cursor")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if h.type == "add" then
|
||||||
|
vim.api.nvim_buf_set_lines(
|
||||||
|
target,
|
||||||
|
h.new_start - 1,
|
||||||
|
h.new_start - 1 + h.new_count,
|
||||||
|
false,
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
elseif h.type == "delete" then
|
||||||
|
vim.api.nvim_buf_set_lines(
|
||||||
|
target,
|
||||||
|
h.new_start,
|
||||||
|
h.new_start,
|
||||||
|
false,
|
||||||
|
h.old_lines
|
||||||
|
)
|
||||||
|
else
|
||||||
|
vim.api.nvim_buf_set_lines(
|
||||||
|
target,
|
||||||
|
h.new_start - 1,
|
||||||
|
h.new_start - 1 + h.new_count,
|
||||||
|
false,
|
||||||
|
h.old_lines
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param buf? integer
|
||||||
|
function M.select_hunk(buf)
|
||||||
|
local _, _, h = cursor_hunk(buf)
|
||||||
|
if not h or h.type == "delete" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local first = h.new_start
|
||||||
|
local last = h.new_start + math.max(h.new_count, 1) - 1
|
||||||
|
vim.api.nvim_win_set_cursor(0, { first, 0 })
|
||||||
|
vim.cmd("normal! V")
|
||||||
|
vim.api.nvim_win_set_cursor(0, { last, 0 })
|
||||||
|
end
|
||||||
|
|
||||||
|
local preview_win ---@type integer?
|
||||||
|
|
||||||
|
---@param buf? integer
|
||||||
|
function M.preview_hunk(buf)
|
||||||
|
if preview_win and vim.api.nvim_win_is_valid(preview_win) then
|
||||||
|
vim.api.nvim_set_current_win(preview_win)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local target, state, h = cursor_hunk(buf)
|
||||||
|
if not state then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not h then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local lines = hunk_body(h)
|
||||||
|
local pbuf = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_lines(pbuf, 0, -1, false, lines)
|
||||||
|
vim.bo[pbuf].filetype = "diff"
|
||||||
|
vim.bo[pbuf].bufhidden = "wipe"
|
||||||
|
local width = 0
|
||||||
|
for _, l in ipairs(lines) do
|
||||||
|
if #l > width then
|
||||||
|
width = #l
|
||||||
|
end
|
||||||
|
end
|
||||||
|
width = math.min(math.max(width + 2, 40), vim.o.columns - 4)
|
||||||
|
local height = math.min(#lines, math.floor(vim.o.lines / 2))
|
||||||
|
local win = vim.api.nvim_open_win(pbuf, false, {
|
||||||
|
relative = "cursor",
|
||||||
|
row = 1,
|
||||||
|
col = 0,
|
||||||
|
width = width,
|
||||||
|
height = height,
|
||||||
|
style = "minimal",
|
||||||
|
})
|
||||||
|
preview_win = win
|
||||||
|
|
||||||
|
local function close()
|
||||||
|
if vim.api.nvim_win_is_valid(win) then
|
||||||
|
vim.api.nvim_win_close(win, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local group =
|
||||||
|
vim.api.nvim_create_augroup("ow.git.hunks.preview", { clear = true })
|
||||||
|
vim.api.nvim_create_autocmd(
|
||||||
|
{ "CursorMoved", "CursorMovedI", "InsertEnter" },
|
||||||
|
{ group = group, buffer = target, callback = close }
|
||||||
|
)
|
||||||
|
vim.api.nvim_create_autocmd("WinLeave", {
|
||||||
|
group = group,
|
||||||
|
buffer = pbuf,
|
||||||
|
callback = close,
|
||||||
|
})
|
||||||
|
vim.api.nvim_create_autocmd("WinClosed", {
|
||||||
|
group = group,
|
||||||
|
pattern = tostring(win),
|
||||||
|
callback = function()
|
||||||
|
preview_win = nil
|
||||||
|
pcall(vim.api.nvim_del_augroup_by_name, "ow.git.hunks.preview")
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
vim.keymap.set("n", "q", close, { buffer = pbuf, nowait = true })
|
||||||
|
end
|
||||||
|
|
||||||
|
repo.on("change", function(r, change)
|
||||||
|
for buf, state in pairs(states) do
|
||||||
|
if
|
||||||
|
state.repo == r
|
||||||
|
and (change.paths[state.rel] or change.branch_changed)
|
||||||
|
then
|
||||||
|
schedule(buf)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
|
||||||
|
if vim.api.nvim_buf_is_loaded(buf) then
|
||||||
|
M.attach(buf)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
+33
-65
@@ -3,29 +3,36 @@ local util = require("git.core.util")
|
|||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
M.URI_PREFIX = "gitlog://"
|
|
||||||
|
|
||||||
local LOG_FORMAT = "%h %ad {%an}%d %s"
|
local LOG_FORMAT = "%h %ad {%an}%d %s"
|
||||||
|
|
||||||
local cr = vim.api.nvim_replace_termcodes("<CR>", true, false, true)
|
local cr = vim.api.nvim_replace_termcodes("<CR>", true, false, true)
|
||||||
|
|
||||||
---@param buf integer
|
---@param buf integer
|
||||||
local function attach_dispatch(buf)
|
---@return boolean opened
|
||||||
vim.keymap.set("n", "<CR>", function()
|
local function open_under_cursor(buf)
|
||||||
local r = repo.resolve(buf)
|
local r = repo.resolve(buf)
|
||||||
-- Anchor past the leading graph chars (matches the leading sha column,
|
-- Anchor past the leading graph chars (matches the leading sha column,
|
||||||
-- not any hex word that happens to appear later in the subject).
|
-- not any hex word that happens to appear later in the subject).
|
||||||
local sha = r
|
local sha = r
|
||||||
and vim.api
|
and vim.api.nvim_get_current_line():match("^[*|/\\_ ]*(%x%x%x%x%x%x%x+)")
|
||||||
.nvim_get_current_line()
|
if not sha then
|
||||||
:match("^[*|/\\_ ]*(%x%x%x%x%x%x%x+)")
|
return false
|
||||||
if sha then
|
end
|
||||||
---@cast r -nil
|
---@cast r -nil
|
||||||
require("git.object").open(r, sha, { split = false })
|
require("git.object").open(r, sha, { split = false })
|
||||||
else
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param buf integer
|
||||||
|
local function attach_dispatch(buf)
|
||||||
|
vim.keymap.set("n", "<CR>", function()
|
||||||
|
if not open_under_cursor(buf) then
|
||||||
vim.api.nvim_feedkeys(cr, "n", false)
|
vim.api.nvim_feedkeys(cr, "n", false)
|
||||||
end
|
end
|
||||||
end, { buffer = buf, silent = true, desc = "Open commit" })
|
end, { buffer = buf, silent = true, desc = "Open commit" })
|
||||||
|
vim.keymap.set("n", "gd", function()
|
||||||
|
open_under_cursor(buf)
|
||||||
|
end, { buffer = buf, silent = true, desc = "Open commit" })
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param worktree string
|
---@param worktree string
|
||||||
@@ -56,50 +63,7 @@ local function populate(buf, r)
|
|||||||
if not stdout then
|
if not stdout then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local new_lines = util.split_lines(stdout)
|
util.set_buf_lines(buf, 0, -1, util.split_lines(stdout))
|
||||||
local old_str = table.concat(
|
|
||||||
vim.api.nvim_buf_get_lines(buf, 0, -1, false),
|
|
||||||
"\n"
|
|
||||||
) .. "\n"
|
|
||||||
local new_str = table.concat(new_lines, "\n") .. "\n"
|
|
||||||
local hunks = vim.text.diff(old_str, new_str, {
|
|
||||||
result_type = "indices",
|
|
||||||
algorithm = "histogram",
|
|
||||||
})
|
|
||||||
---@cast hunks [integer, integer, integer, integer][]
|
|
||||||
if #hunks == 0 then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
for i = #hunks, 1, -1 do
|
|
||||||
local sa, ca, sb, cb = unpack(hunks[i])
|
|
||||||
local start = ca == 0 and sa or sa - 1
|
|
||||||
util.set_buf_lines(
|
|
||||||
buf,
|
|
||||||
start,
|
|
||||||
start + ca,
|
|
||||||
vim.list_slice(new_lines, sb, sb + cb - 1)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param buf integer
|
|
||||||
function M.read_uri(buf)
|
|
||||||
local name = vim.api.nvim_buf_get_name(buf)
|
|
||||||
local worktree = name:sub(#M.URI_PREFIX + 1)
|
|
||||||
if worktree == "" then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local r = repo.resolve(worktree)
|
|
||||||
if not r then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
repo.bind(buf, r)
|
|
||||||
|
|
||||||
util.setup_scratch(buf, { bufhidden = "hide" })
|
|
||||||
vim.bo[buf].filetype = "gitlog"
|
|
||||||
|
|
||||||
attach_dispatch(buf)
|
|
||||||
populate(buf, r)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@class ow.Git.Log.OpenOpts
|
---@class ow.Git.Log.OpenOpts
|
||||||
@@ -120,19 +84,25 @@ function M.open(opts)
|
|||||||
end
|
end
|
||||||
|
|
||||||
max_counts[r.worktree] = opts.max_count
|
max_counts[r.worktree] = opts.max_count
|
||||||
local buf = vim.fn.bufadd(M.URI_PREFIX .. r.worktree)
|
local buf = vim.fn.bufadd(r.worktree .. "/GitLog")
|
||||||
local was_loaded = vim.api.nvim_buf_is_loaded(buf)
|
|
||||||
|
|
||||||
local win = vim.fn.bufwinid(buf)
|
local visible = vim.fn.bufwinid(buf)
|
||||||
if win == -1 then
|
if visible ~= -1 then
|
||||||
util.place_buf(buf, nil)
|
vim.api.nvim_set_current_win(visible)
|
||||||
else
|
|
||||||
vim.api.nvim_set_current_win(win)
|
|
||||||
end
|
|
||||||
|
|
||||||
if was_loaded then
|
|
||||||
populate(buf, r)
|
populate(buf, r)
|
||||||
|
vim.api.nvim_win_set_cursor(visible, { 1, 0 })
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
vim.fn.bufload(buf)
|
||||||
|
repo.bind(buf, r)
|
||||||
|
util.setup_scratch(buf, { bufhidden = "hide" })
|
||||||
|
vim.bo[buf].filetype = "gitlog"
|
||||||
|
attach_dispatch(buf)
|
||||||
|
|
||||||
|
local win = util.place_buf(buf, nil)
|
||||||
|
vim.api.nvim_win_set_cursor(win, { 1, 0 })
|
||||||
|
populate(buf, r)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param cmd_opts table
|
---@param cmd_opts table
|
||||||
@@ -170,6 +140,4 @@ function M.complete_glog(arg_lead)
|
|||||||
return matches
|
return matches
|
||||||
end
|
end
|
||||||
|
|
||||||
repo.on_uri_refresh(M.URI_PREFIX, populate)
|
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
+11
-4
@@ -150,6 +150,9 @@ function M.attach_dispatch(buf)
|
|||||||
vim.api.nvim_feedkeys(cr, "n", false)
|
vim.api.nvim_feedkeys(cr, "n", false)
|
||||||
end
|
end
|
||||||
end, { buffer = buf, silent = true, desc = "Open file at commit" })
|
end, { buffer = buf, silent = true, desc = "Open file at commit" })
|
||||||
|
vim.keymap.set("n", "gd", function()
|
||||||
|
M.open_under_cursor()
|
||||||
|
end, { buffer = buf, silent = true, desc = "Open file at commit" })
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param r ow.Git.Repo
|
---@param r ow.Git.Repo
|
||||||
@@ -181,8 +184,7 @@ local function populate(buf, r, rev, state, rev_sha)
|
|||||||
local patch = util.git({
|
local patch = util.git({
|
||||||
"diff-tree",
|
"diff-tree",
|
||||||
"-p",
|
"-p",
|
||||||
"-m",
|
"--diff-merges=first-parent",
|
||||||
"--first-parent",
|
|
||||||
"--root",
|
"--root",
|
||||||
"--no-commit-id",
|
"--no-commit-id",
|
||||||
commit_sha,
|
commit_sha,
|
||||||
@@ -334,7 +336,12 @@ local function open_section(r, section)
|
|||||||
local left = side_buf(r, section.blob_a, section.path_a)
|
local left = side_buf(r, section.blob_a, section.path_a)
|
||||||
local right = side_buf(r, section.blob_b, section.path_b)
|
local right = side_buf(r, section.blob_b, section.path_b)
|
||||||
if left and right then
|
if left and right then
|
||||||
require("git.diff").open(left, right, true)
|
vim.cmd.normal({ "m'", bang = true })
|
||||||
|
vim.api.nvim_set_current_buf(right)
|
||||||
|
require("git.diffsplit").open({
|
||||||
|
target = vim.api.nvim_buf_get_name(left),
|
||||||
|
mods = { vertical = true },
|
||||||
|
})
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if not left and not right then
|
if not left and not right then
|
||||||
@@ -428,6 +435,6 @@ function M.open_under_cursor()
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
repo.on_uri_refresh(M.URI_PREFIX, refresh)
|
repo.on_uri_change(M.URI_PREFIX, refresh)
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
+158
-243
@@ -1,5 +1,5 @@
|
|||||||
local Revision = require("git.core.revision")
|
local Revision = require("git.core.revision")
|
||||||
local diff = require("git.diff")
|
local diffsplit = require("git.diffsplit")
|
||||||
local object = require("git.object")
|
local object = require("git.object")
|
||||||
local repo = require("git.core.repo")
|
local repo = require("git.core.repo")
|
||||||
local status = require("git.core.status")
|
local status = require("git.core.status")
|
||||||
@@ -7,8 +7,6 @@ local util = require("git.core.util")
|
|||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
M.URI_PREFIX = "gitstatus://"
|
|
||||||
|
|
||||||
---@type ow.Git.StatusView.Placement[]
|
---@type ow.Git.StatusView.Placement[]
|
||||||
M.PLACEMENTS = { "sidebar", "split", "current" }
|
M.PLACEMENTS = { "sidebar", "split", "current" }
|
||||||
|
|
||||||
@@ -16,14 +14,10 @@ M.PLACEMENTS = { "sidebar", "split", "current" }
|
|||||||
local SECTIONS = { "untracked", "unstaged", "staged", "unmerged" }
|
local SECTIONS = { "untracked", "unstaged", "staged", "unmerged" }
|
||||||
local WINDOW_WIDTH = 50
|
local WINDOW_WIDTH = 50
|
||||||
|
|
||||||
---@param name string
|
---@param r ow.Git.Repo
|
||||||
---@return integer? bufnr
|
---@return string
|
||||||
local function find_buf(name)
|
local function buf_name_for(r)
|
||||||
for _, b in ipairs(vim.api.nvim_list_bufs()) do
|
return r.worktree .. "/GitStatus"
|
||||||
if vim.api.nvim_buf_get_name(b) == name then
|
|
||||||
return b
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@alias ow.Git.StatusView.Placement "sidebar"|"split"|"current"
|
---@alias ow.Git.StatusView.Placement "sidebar"|"split"|"current"
|
||||||
@@ -39,11 +33,7 @@ end
|
|||||||
---@field placement ow.Git.StatusView.Placement
|
---@field placement ow.Git.StatusView.Placement
|
||||||
---@field lines table<integer, ow.Git.StatusView.Item>
|
---@field lines table<integer, ow.Git.StatusView.Item>
|
||||||
---@field win integer?
|
---@field win integer?
|
||||||
---@field invocation_win integer?
|
|
||||||
---@field diff_left_win integer?
|
|
||||||
---@field diff_right_win integer?
|
|
||||||
---@field unsubscribe fun()?
|
---@field unsubscribe fun()?
|
||||||
---@field last_shown_key string?
|
|
||||||
|
|
||||||
---@type table<integer, ow.Git.StatusView.State>
|
---@type table<integer, ow.Git.StatusView.State>
|
||||||
local state = {}
|
local state = {}
|
||||||
@@ -107,32 +97,77 @@ local function display_name(section)
|
|||||||
end
|
end
|
||||||
|
|
||||||
---@param bufnr integer
|
---@param bufnr integer
|
||||||
---@param status ow.Git.Status
|
---@param r ow.Git.Repo
|
||||||
local function render(bufnr, status)
|
local function render(bufnr, r)
|
||||||
|
local status = r.status
|
||||||
local branch = status.branch
|
local branch = status.branch
|
||||||
local lines = { "Head: " .. (branch.head or "?") }
|
local lines = {}
|
||||||
|
local marks = {}
|
||||||
|
local meta = {}
|
||||||
|
|
||||||
|
local function label(row, len)
|
||||||
|
table.insert(marks, { row = row, col = 0, end_col = len, hl = "Label" })
|
||||||
|
end
|
||||||
|
|
||||||
|
local repo_line = vim.fn.fnamemodify(r.worktree, ":t")
|
||||||
|
table.insert(lines, repo_line)
|
||||||
|
table.insert(marks, {
|
||||||
|
row = #lines - 1,
|
||||||
|
col = 0,
|
||||||
|
end_col = #repo_line,
|
||||||
|
hl = "Directory",
|
||||||
|
})
|
||||||
|
|
||||||
|
table.insert(lines, "Branch: " .. (branch.head or "?"))
|
||||||
|
label(#lines - 1, 6)
|
||||||
|
|
||||||
if branch.upstream then
|
if branch.upstream then
|
||||||
local push = "Push: " .. branch.upstream
|
local up = "Upstream: " .. branch.upstream
|
||||||
|
local extras = {}
|
||||||
if branch.ahead > 0 then
|
if branch.ahead > 0 then
|
||||||
push = push .. " +" .. branch.ahead
|
local col = #up + 1
|
||||||
|
up = up .. " +" .. branch.ahead
|
||||||
|
table.insert(extras, {
|
||||||
|
col = col,
|
||||||
|
end_col = #up,
|
||||||
|
hl = "GitUnpushed",
|
||||||
|
})
|
||||||
end
|
end
|
||||||
if branch.behind > 0 then
|
if branch.behind > 0 then
|
||||||
push = push .. " -" .. branch.behind
|
local col = #up + 1
|
||||||
|
up = up .. " -" .. branch.behind
|
||||||
|
table.insert(extras, {
|
||||||
|
col = col,
|
||||||
|
end_col = #up,
|
||||||
|
hl = "GitUnpulled",
|
||||||
|
})
|
||||||
end
|
end
|
||||||
table.insert(lines, push)
|
table.insert(lines, up)
|
||||||
|
local row = #lines - 1
|
||||||
|
label(row, 8)
|
||||||
|
for _, e in ipairs(extras) do
|
||||||
|
e.row = row
|
||||||
|
table.insert(marks, e)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
table.insert(lines, "")
|
table.insert(lines, "")
|
||||||
|
|
||||||
local meta = {}
|
|
||||||
local marks = {}
|
|
||||||
for _, section in ipairs(SECTIONS) do
|
for _, section in ipairs(SECTIONS) do
|
||||||
local rows = status:rows(section)
|
local rows = status:rows(section)
|
||||||
if #rows > 0 then
|
if #rows > 0 then
|
||||||
table.insert(
|
local name = display_name(section)
|
||||||
lines,
|
local header = string.format("%s (%d)", name, #rows)
|
||||||
string.format("%s (%d)", display_name(section), #rows)
|
table.insert(lines, header)
|
||||||
)
|
local header_row = #lines - 1
|
||||||
meta[#lines] = { is_header = true, section = section }
|
meta[#lines] = { is_header = true, section = section }
|
||||||
|
label(header_row, #name)
|
||||||
|
table.insert(marks, {
|
||||||
|
row = header_row,
|
||||||
|
col = #name + 2,
|
||||||
|
end_col = #header - 1,
|
||||||
|
hl = "Number",
|
||||||
|
})
|
||||||
for _, row in ipairs(rows) do
|
for _, row in ipairs(rows) do
|
||||||
local line, hl, hl_len = format_row(row)
|
local line, hl, hl_len = format_row(row)
|
||||||
table.insert(lines, line)
|
table.insert(lines, line)
|
||||||
@@ -165,8 +200,7 @@ local function refresh(bufnr)
|
|||||||
if not s or not vim.api.nvim_buf_is_valid(bufnr) then
|
if not s or not vim.api.nvim_buf_is_valid(bufnr) then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
s.last_shown_key = nil
|
render(bufnr, s.repo)
|
||||||
render(bufnr, s.repo.status)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param bufnr integer
|
---@param bufnr integer
|
||||||
@@ -181,9 +215,13 @@ local function current_entry(bufnr)
|
|||||||
return s, s.lines[lnum]
|
return s, s.lines[lnum]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@class ow.Git.StatusView.Pane
|
||||||
|
---@field buf integer
|
||||||
|
---@field name string?
|
||||||
|
|
||||||
---@param r ow.Git.Repo
|
---@param r ow.Git.Repo
|
||||||
---@param path string
|
---@param path string
|
||||||
---@return ow.Git.Diff.Side
|
---@return ow.Git.StatusView.Pane
|
||||||
local function head_pane(r, path)
|
local function head_pane(r, path)
|
||||||
local rev = Revision.new({ base = "HEAD", path = path })
|
local rev = Revision.new({ base = "HEAD", path = path })
|
||||||
return {
|
return {
|
||||||
@@ -194,7 +232,7 @@ end
|
|||||||
|
|
||||||
---@param r ow.Git.Repo
|
---@param r ow.Git.Repo
|
||||||
---@param path string
|
---@param path string
|
||||||
---@return ow.Git.Diff.Side
|
---@return ow.Git.StatusView.Pane
|
||||||
local function worktree_pane(r, path)
|
local function worktree_pane(r, path)
|
||||||
local buf = vim.fn.bufadd(vim.fs.joinpath(r.worktree, path))
|
local buf = vim.fn.bufadd(vim.fs.joinpath(r.worktree, path))
|
||||||
vim.fn.bufload(buf)
|
vim.fn.bufload(buf)
|
||||||
@@ -203,7 +241,7 @@ end
|
|||||||
|
|
||||||
---@param s ow.Git.StatusView.State
|
---@param s ow.Git.StatusView.State
|
||||||
---@param path string
|
---@param path string
|
||||||
---@return ow.Git.Diff.Side
|
---@return ow.Git.StatusView.Pane
|
||||||
local function index_pane(s, path)
|
local function index_pane(s, path)
|
||||||
local rev = Revision.new({ stage = 0, path = path })
|
local rev = Revision.new({ stage = 0, path = path })
|
||||||
return {
|
return {
|
||||||
@@ -214,7 +252,7 @@ end
|
|||||||
|
|
||||||
---@param s ow.Git.StatusView.State
|
---@param s ow.Git.StatusView.State
|
||||||
---@param row ow.Git.Status.Row
|
---@param row ow.Git.Status.Row
|
||||||
---@return ow.Git.Diff.Side?
|
---@return ow.Git.StatusView.Pane?
|
||||||
local function older_pane(s, row)
|
local function older_pane(s, row)
|
||||||
local entry = row.entry
|
local entry = row.entry
|
||||||
if row.section == "staged" then
|
if row.section == "staged" then
|
||||||
@@ -232,7 +270,7 @@ end
|
|||||||
|
|
||||||
---@param s ow.Git.StatusView.State
|
---@param s ow.Git.StatusView.State
|
||||||
---@param row ow.Git.Status.Row
|
---@param row ow.Git.Status.Row
|
||||||
---@return ow.Git.Diff.Side?
|
---@return ow.Git.StatusView.Pane?
|
||||||
local function newer_pane(s, row)
|
local function newer_pane(s, row)
|
||||||
local entry = row.entry
|
local entry = row.entry
|
||||||
if row.section == "staged" then
|
if row.section == "staged" then
|
||||||
@@ -255,71 +293,6 @@ local function newer_pane(s, row)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param win integer
|
|
||||||
local function reset_diff_win(win)
|
|
||||||
vim.api.nvim_win_call(win, function()
|
|
||||||
vim.cmd(
|
|
||||||
"setlocal winfixwidth< number< relativenumber< signcolumn< wrap< cursorline<"
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param s ow.Git.StatusView.State
|
|
||||||
---@return integer?
|
|
||||||
local function invocation_win_for(s)
|
|
||||||
local win = s.invocation_win
|
|
||||||
if not win or not vim.api.nvim_win_is_valid(win) then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
if win == s.win then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
if
|
|
||||||
vim.api.nvim_win_get_tabpage(win)
|
|
||||||
~= vim.api.nvim_get_current_tabpage()
|
|
||||||
then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
return win
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param s ow.Git.StatusView.State
|
|
||||||
---@param status_win integer
|
|
||||||
---@return integer? left
|
|
||||||
---@return integer? right
|
|
||||||
local function adopt_diff_wins(s, status_win)
|
|
||||||
local left = valid_in_current_tab(s.diff_left_win) and s.diff_left_win
|
|
||||||
or nil
|
|
||||||
local right = valid_in_current_tab(s.diff_right_win) and s.diff_right_win
|
|
||||||
or nil
|
|
||||||
if left and right then
|
|
||||||
return left, right
|
|
||||||
end
|
|
||||||
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
|
||||||
if win ~= status_win then
|
|
||||||
local role = vim.w[win].git_diff_role
|
|
||||||
if role == "left" and not left then
|
|
||||||
left = win
|
|
||||||
elseif role == "right" and not right then
|
|
||||||
right = win
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return left, right
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param row ow.Git.Status.Row
|
|
||||||
---@return string
|
|
||||||
local function row_key(row)
|
|
||||||
local entry = row.entry
|
|
||||||
local orig
|
|
||||||
if entry.kind == "changed" then
|
|
||||||
---@cast entry ow.Git.Status.ChangedEntry
|
|
||||||
orig = entry.orig
|
|
||||||
end
|
|
||||||
return row.section .. "|" .. entry.path .. "|" .. (orig or "")
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param target_win integer
|
---@param target_win integer
|
||||||
---@param dir "left"|"right"
|
---@param dir "left"|"right"
|
||||||
---@return integer
|
---@return integer
|
||||||
@@ -329,27 +302,39 @@ local function vsplit_at(target_win, dir)
|
|||||||
true,
|
true,
|
||||||
{ split = dir, win = target_win }
|
{ split = dir, win = target_win }
|
||||||
)
|
)
|
||||||
|
vim.api.nvim_win_call(win, function()
|
||||||
|
vim.cmd("setlocal winfixwidth<")
|
||||||
|
end)
|
||||||
vim.cmd.clearjumps()
|
vim.cmd.clearjumps()
|
||||||
return win
|
return win
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param s ow.Git.StatusView.State
|
|
||||||
---@param status_win integer
|
---@param status_win integer
|
||||||
---@param right_win integer?
|
---@return integer?
|
||||||
---@return integer
|
local function previous_target_win(status_win)
|
||||||
local function ensure_right_win(s, status_win, right_win)
|
local n = vim.fn.winnr("#")
|
||||||
if right_win then
|
if n == 0 then
|
||||||
return right_win
|
return nil
|
||||||
|
end
|
||||||
|
local win = vim.fn.win_getid(n)
|
||||||
|
if win == 0 or win == status_win or not valid_in_current_tab(win) then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local cfg = vim.api.nvim_win_get_config(win)
|
||||||
|
if cfg.relative and cfg.relative ~= "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return win
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param status_win integer
|
||||||
|
---@param keep integer
|
||||||
|
local function close_other_diff_wins(status_win, keep)
|
||||||
|
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
||||||
|
if win ~= status_win and win ~= keep and vim.wo[win].diff then
|
||||||
|
pcall(vim.api.nvim_win_close, win, false)
|
||||||
end
|
end
|
||||||
local target = invocation_win_for(s)
|
|
||||||
if target then
|
|
||||||
right_win = target
|
|
||||||
else
|
|
||||||
right_win = vsplit_at(status_win, "right")
|
|
||||||
vim.api.nvim_win_set_width(status_win, WINDOW_WIDTH)
|
|
||||||
end
|
end
|
||||||
reset_diff_win(right_win)
|
|
||||||
return right_win
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param s ow.Git.StatusView.State
|
---@param s ow.Git.StatusView.State
|
||||||
@@ -374,8 +359,7 @@ local function view_row(s, row, focus_left)
|
|||||||
|
|
||||||
if s.placement ~= "sidebar" then
|
if s.placement ~= "sidebar" then
|
||||||
local pane = right or left
|
local pane = right or left
|
||||||
---@cast pane ow.Git.Diff.Side
|
---@cast pane -nil
|
||||||
diff.set_diff(status_win, false)
|
|
||||||
vim.cmd.normal({ "m'", bang = true })
|
vim.cmd.normal({ "m'", bang = true })
|
||||||
vim.api.nvim_win_set_buf(status_win, pane.buf)
|
vim.api.nvim_win_set_buf(status_win, pane.buf)
|
||||||
if pane.name then
|
if pane.name then
|
||||||
@@ -384,68 +368,46 @@ local function view_row(s, row, focus_left)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local key = row_key(row)
|
local target = previous_target_win(status_win)
|
||||||
local left_win, right_win = adopt_diff_wins(s, status_win)
|
if not target then
|
||||||
local want_pair = left and right
|
target = vsplit_at(status_win, "right")
|
||||||
|
end
|
||||||
|
close_other_diff_wins(status_win, target)
|
||||||
|
vim.api.nvim_win_set_width(status_win, WINDOW_WIDTH)
|
||||||
|
vim.api.nvim_win_call(target, function()
|
||||||
|
vim.cmd.diffoff()
|
||||||
|
end)
|
||||||
|
|
||||||
if s.last_shown_key == key then
|
if not (left and right) then
|
||||||
local intact = (want_pair and left_win and right_win)
|
local side = right or left
|
||||||
or (not want_pair and right_win and not left_win)
|
---@cast side ow.Git.StatusView.Pane
|
||||||
if intact then
|
vim.api.nvim_win_set_buf(target, side.buf)
|
||||||
local target = focus_left and (left_win or right_win) or status_win
|
|
||||||
vim.api.nvim_set_current_win(target)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not want_pair then
|
|
||||||
if left_win and vim.api.nvim_win_is_valid(left_win) then
|
|
||||||
pcall(vim.api.nvim_win_close, left_win, false)
|
|
||||||
left_win = nil
|
|
||||||
s.diff_left_win = nil
|
|
||||||
end
|
|
||||||
right_win = ensure_right_win(s, status_win, right_win)
|
|
||||||
s.diff_right_win = right_win
|
|
||||||
vim.w[right_win].git_diff_role = "right"
|
|
||||||
local side = left or right
|
|
||||||
---@cast side ow.Git.Diff.Side
|
|
||||||
diff.set_diff(right_win, false)
|
|
||||||
vim.api.nvim_win_set_buf(right_win, side.buf)
|
|
||||||
if side.name then
|
if side.name then
|
||||||
util.set_buf_name(side.buf, side.name)
|
util.set_buf_name(side.buf, side.name)
|
||||||
end
|
end
|
||||||
s.last_shown_key = key
|
vim.api.nvim_set_current_win(focus_left and target or status_win)
|
||||||
vim.api.nvim_set_current_win(focus_left and right_win or status_win)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
---@cast left ow.Git.Diff.Side
|
---@cast left ow.Git.StatusView.Pane
|
||||||
---@cast right ow.Git.Diff.Side
|
---@cast right ow.Git.StatusView.Pane
|
||||||
|
|
||||||
if left_win and not right_win then
|
vim.api.nvim_win_set_buf(target, right.buf)
|
||||||
right_win = vsplit_at(left_win, "right")
|
if right.name then
|
||||||
reset_diff_win(right_win)
|
util.set_buf_name(right.buf, right.name)
|
||||||
elseif right_win and not left_win then
|
|
||||||
left_win = vsplit_at(right_win, "left")
|
|
||||||
reset_diff_win(left_win)
|
|
||||||
elseif not (left_win or right_win) then
|
|
||||||
right_win = ensure_right_win(s, status_win, nil)
|
|
||||||
left_win = vsplit_at(right_win, "left")
|
|
||||||
reset_diff_win(left_win)
|
|
||||||
local combined = vim.api.nvim_win_get_width(left_win)
|
|
||||||
+ vim.api.nvim_win_get_width(right_win)
|
|
||||||
vim.api.nvim_win_set_width(left_win, math.floor(combined / 2))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local older = left.name or vim.api.nvim_buf_get_name(left.buf)
|
||||||
|
local left_win
|
||||||
|
vim.api.nvim_win_call(target, function()
|
||||||
|
diffsplit.open({
|
||||||
|
target = older,
|
||||||
|
mods = { vertical = true },
|
||||||
|
})
|
||||||
|
left_win = vim.api.nvim_get_current_win()
|
||||||
|
vim.api.nvim_win_set_cursor(left_win, { 1, 0 })
|
||||||
|
vim.api.nvim_win_set_cursor(target, { 1, 0 })
|
||||||
|
end)
|
||||||
---@cast left_win -nil
|
---@cast left_win -nil
|
||||||
---@cast right_win -nil
|
|
||||||
|
|
||||||
vim.w[left_win].git_diff_role = "left"
|
|
||||||
vim.w[right_win].git_diff_role = "right"
|
|
||||||
s.diff_left_win = left_win
|
|
||||||
s.diff_right_win = right_win
|
|
||||||
|
|
||||||
diff.update_pair(left_win, right_win, { left = left, right = right })
|
|
||||||
s.last_shown_key = key
|
|
||||||
|
|
||||||
vim.api.nvim_set_current_win(focus_left and left_win or status_win)
|
vim.api.nvim_set_current_win(focus_left and left_win or status_win)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -599,8 +561,10 @@ local function action_help(placement)
|
|||||||
if placement == "sidebar" then
|
if placement == "sidebar" then
|
||||||
table.insert(lines, " <Tab> preview diff (keep focus)")
|
table.insert(lines, " <Tab> preview diff (keep focus)")
|
||||||
table.insert(lines, " <CR> open diff (focus left pane)")
|
table.insert(lines, " <CR> open diff (focus left pane)")
|
||||||
|
table.insert(lines, " <2-LeftMouse> open diff (focus left pane)")
|
||||||
else
|
else
|
||||||
table.insert(lines, " <CR> open file")
|
table.insert(lines, " <CR> open file")
|
||||||
|
table.insert(lines, " <2-LeftMouse> open file")
|
||||||
end
|
end
|
||||||
table.insert(lines, " s stage file")
|
table.insert(lines, " s stage file")
|
||||||
table.insert(lines, " u unstage file")
|
table.insert(lines, " u unstage file")
|
||||||
@@ -640,14 +604,12 @@ end
|
|||||||
---@param r ow.Git.Repo
|
---@param r ow.Git.Repo
|
||||||
---@param placement ow.Git.StatusView.Placement
|
---@param placement ow.Git.StatusView.Placement
|
||||||
---@param win integer?
|
---@param win integer?
|
||||||
---@param invocation_win integer?
|
local function setup_buffer(bufnr, r, placement, win)
|
||||||
local function setup_buffer(bufnr, r, placement, win, invocation_win)
|
|
||||||
state[bufnr] = {
|
state[bufnr] = {
|
||||||
repo = r,
|
repo = r,
|
||||||
placement = placement,
|
placement = placement,
|
||||||
lines = {},
|
lines = {},
|
||||||
win = win,
|
win = win,
|
||||||
invocation_win = invocation_win,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
local function k(lhs, rhs, desc)
|
local function k(lhs, rhs, desc)
|
||||||
@@ -661,6 +623,9 @@ local function setup_buffer(bufnr, r, placement, win, invocation_win)
|
|||||||
k("<CR>", function()
|
k("<CR>", function()
|
||||||
preview_or_open(true)
|
preview_or_open(true)
|
||||||
end, "Open")
|
end, "Open")
|
||||||
|
k("<2-LeftMouse>", function()
|
||||||
|
preview_or_open(true)
|
||||||
|
end, "Open")
|
||||||
k("s", action_stage, "Stage file")
|
k("s", action_stage, "Stage file")
|
||||||
k("u", action_unstage, "Unstage file")
|
k("u", action_unstage, "Unstage file")
|
||||||
k("X", action_discard, "Discard worktree changes")
|
k("X", action_discard, "Discard worktree changes")
|
||||||
@@ -671,7 +636,7 @@ local function setup_buffer(bufnr, r, placement, win, invocation_win)
|
|||||||
action_help(state[bufnr].placement)
|
action_help(state[bufnr].placement)
|
||||||
end, "Help")
|
end, "Help")
|
||||||
|
|
||||||
state[bufnr].unsubscribe = r:on("refresh", function()
|
state[bufnr].unsubscribe = r:on("change", function()
|
||||||
refresh(bufnr)
|
refresh(bufnr)
|
||||||
end)
|
end)
|
||||||
vim.api.nvim_create_autocmd("BufEnter", {
|
vim.api.nvim_create_autocmd("BufEnter", {
|
||||||
@@ -709,7 +674,10 @@ local function set_keymaps(bufnr, placement)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param opts? { placement: ow.Git.StatusView.Placement? }
|
---@class ow.Git.StatusView.OpenOpts
|
||||||
|
---@field placement ow.Git.StatusView.Placement?
|
||||||
|
|
||||||
|
---@param opts? ow.Git.StatusView.OpenOpts
|
||||||
function M.open(opts)
|
function M.open(opts)
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
local placement = opts.placement or "sidebar"
|
local placement = opts.placement or "sidebar"
|
||||||
@@ -726,8 +694,9 @@ function M.open(opts)
|
|||||||
util.error("not in a git repository")
|
util.error("not in a git repository")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local previous_win = vim.api.nvim_get_current_win()
|
local previous_win = vim.api.nvim_get_current_win()
|
||||||
local buf = vim.fn.bufadd(M.URI_PREFIX .. r.worktree)
|
local buf = vim.fn.bufadd(buf_name_for(r))
|
||||||
|
|
||||||
local visible = vim.fn.bufwinid(buf)
|
local visible = vim.fn.bufwinid(buf)
|
||||||
if visible ~= -1 then
|
if visible ~= -1 then
|
||||||
@@ -736,90 +705,36 @@ function M.open(opts)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local was_loaded = vim.api.nvim_buf_is_loaded(buf)
|
if not state[buf] then
|
||||||
local win = place(buf, placement)
|
vim.fn.bufload(buf)
|
||||||
|
repo.bind(buf, r)
|
||||||
vim.bo[buf].bufhidden = placement == "sidebar" and "wipe" or "hide"
|
util.setup_scratch(buf, {})
|
||||||
local s = state[buf]
|
vim.bo[buf].filetype = "gitstatus"
|
||||||
if s then
|
setup_buffer(buf, r, placement)
|
||||||
s.win = win
|
|
||||||
s.invocation_win = previous_win
|
|
||||||
s.placement = placement
|
|
||||||
end
|
end
|
||||||
|
vim.bo[buf].bufhidden = placement == "sidebar" and "wipe" or "hide"
|
||||||
|
|
||||||
|
local win = place(buf, placement)
|
||||||
|
state[buf].win = win
|
||||||
|
state[buf].placement = placement
|
||||||
set_keymaps(buf, placement)
|
set_keymaps(buf, placement)
|
||||||
|
|
||||||
if placement == "sidebar" then
|
if placement == "sidebar" then
|
||||||
vim.api.nvim_set_current_win(previous_win)
|
vim.api.nvim_set_current_win(previous_win)
|
||||||
end
|
end
|
||||||
|
|
||||||
if was_loaded then
|
|
||||||
refresh(buf)
|
|
||||||
end
|
|
||||||
r:refresh()
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param buf integer
|
|
||||||
function M.read_uri(buf)
|
|
||||||
local name = vim.api.nvim_buf_get_name(buf)
|
|
||||||
local raw = name:sub(#M.URI_PREFIX + 1)
|
|
||||||
if raw == "" then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
local worktree = vim.fs.abspath(raw)
|
|
||||||
local r = repo.resolve(worktree)
|
|
||||||
if not r then
|
|
||||||
util.error("not a git worktree: %s", raw)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if r.worktree ~= worktree then
|
|
||||||
util.warning("%s is not a worktree root, using %s", raw, r.worktree)
|
|
||||||
end
|
|
||||||
local canonical = M.URI_PREFIX .. r.worktree
|
|
||||||
if name ~= canonical then
|
|
||||||
local existing = find_buf(canonical)
|
|
||||||
if existing and existing ~= buf then
|
|
||||||
local win = vim.api.nvim_get_current_win()
|
|
||||||
if vim.api.nvim_win_get_buf(win) == buf then
|
|
||||||
vim.api.nvim_win_set_buf(win, existing)
|
|
||||||
end
|
|
||||||
vim.api.nvim_buf_delete(buf, { force = true })
|
|
||||||
local s = state[existing]
|
|
||||||
if s then
|
|
||||||
s.win = win
|
|
||||||
s.placement = "current"
|
|
||||||
end
|
|
||||||
refresh(existing)
|
|
||||||
r:refresh()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
pcall(vim.api.nvim_buf_set_name, buf, canonical)
|
|
||||||
end
|
|
||||||
repo.bind(buf, r)
|
|
||||||
|
|
||||||
util.setup_scratch(buf, { bufhidden = "hide" })
|
|
||||||
vim.bo[buf].filetype = "gitstatus"
|
|
||||||
|
|
||||||
---@type integer?
|
|
||||||
local win = vim.fn.bufwinid(buf)
|
|
||||||
if win == -1 then
|
|
||||||
win = nil
|
|
||||||
end
|
|
||||||
if not state[buf] then
|
|
||||||
setup_buffer(buf, r, "current", win, nil)
|
|
||||||
else
|
|
||||||
state[buf].win = win
|
|
||||||
end
|
|
||||||
refresh(buf)
|
refresh(buf)
|
||||||
r:refresh()
|
r:refresh()
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.toggle()
|
---@param opts? ow.Git.StatusView.OpenOpts
|
||||||
|
function M.toggle(opts)
|
||||||
local existing = find_view()
|
local existing = find_view()
|
||||||
if existing then
|
if existing then
|
||||||
vim.api.nvim_win_close(existing, false)
|
vim.api.nvim_win_close(existing, false)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
M.open({ placement = "sidebar" })
|
M.open(opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
+22
-6
@@ -34,6 +34,15 @@ local function clear(buf)
|
|||||||
vim.b[buf].git_status_string = nil
|
vim.b[buf].git_status_string = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param buf integer
|
||||||
|
---@param r ow.Git.Repo
|
||||||
|
---@param rel string
|
||||||
|
local function set_status(buf, r, rel)
|
||||||
|
local entry = r:status_entry_for(rel)
|
||||||
|
vim.b[buf].git_status = { head = r:head(), entry = entry }
|
||||||
|
vim.b[buf].git_status_string = render(entry)
|
||||||
|
end
|
||||||
|
|
||||||
---@param buf integer
|
---@param buf integer
|
||||||
---@param r ow.Git.Repo?
|
---@param r ow.Git.Repo?
|
||||||
local function update_buf(buf, r)
|
local function update_buf(buf, r)
|
||||||
@@ -52,21 +61,28 @@ local function update_buf(buf, r)
|
|||||||
if not rel then
|
if not rel then
|
||||||
return clear(buf)
|
return clear(buf)
|
||||||
end
|
end
|
||||||
local entry = r.status.entries[rel]
|
set_status(buf, r, rel)
|
||||||
vim.b[buf].git_status = { head = r:head(), entry = entry }
|
|
||||||
vim.b[buf].git_status_string = render(entry)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
repo.on("refresh", function(r)
|
repo.on("change", function(r)
|
||||||
local any_visible = false
|
local any_visible = false
|
||||||
for buf in pairs(r.buffers) do
|
for buf in pairs(r.buffers) do
|
||||||
if vim.api.nvim_buf_is_loaded(buf) then
|
if vim.api.nvim_buf_is_loaded(buf) then
|
||||||
update_buf(buf, r)
|
local name = vim.api.nvim_buf_get_name(buf)
|
||||||
if not any_visible and #vim.fn.win_findbuf(buf) > 0 then
|
if name ~= "" and not util.is_uri(name) then
|
||||||
|
local rel = vim.fs.relpath(r.worktree, vim.fn.resolve(name))
|
||||||
|
if rel then
|
||||||
|
set_status(buf, r, rel)
|
||||||
|
if
|
||||||
|
not any_visible
|
||||||
|
and #vim.fn.win_findbuf(buf) > 0
|
||||||
|
then
|
||||||
any_visible = true
|
any_visible = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
if any_visible then
|
if any_visible then
|
||||||
vim.cmd.redrawstatus({ bang = true })
|
vim.cmd.redrawstatus({ bang = true })
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -93,6 +93,10 @@
|
|||||||
"rev": "73a3947324f6efddf9e17c0ea58d454843590cc0",
|
"rev": "73a3947324f6efddf9e17c0ea58d454843590cc0",
|
||||||
"src": "https://github.com/tree-sitter/tree-sitter-html"
|
"src": "https://github.com/tree-sitter/tree-sitter-html"
|
||||||
},
|
},
|
||||||
|
"tree-sitter-javascript": {
|
||||||
|
"rev": "58404d8cf191d69f2674a8fd507bd5776f46cb11",
|
||||||
|
"src": "https://github.com/tree-sitter/tree-sitter-javascript"
|
||||||
|
},
|
||||||
"tree-sitter-json": {
|
"tree-sitter-json": {
|
||||||
"rev": "001c28d7a29832b06b0e831ec77845553c89b56d",
|
"rev": "001c28d7a29832b06b0e831ec77845553c89b56d",
|
||||||
"src": "https://github.com/tree-sitter/tree-sitter-json"
|
"src": "https://github.com/tree-sitter/tree-sitter-json"
|
||||||
@@ -130,6 +134,10 @@
|
|||||||
"rev": "ae5199db47757f785e43a14b332118a5474de1a2",
|
"rev": "ae5199db47757f785e43a14b332118a5474de1a2",
|
||||||
"src": "https://github.com/tree-sitter-grammars/tree-sitter-svelte"
|
"src": "https://github.com/tree-sitter-grammars/tree-sitter-svelte"
|
||||||
},
|
},
|
||||||
|
"tree-sitter-tumblr": {
|
||||||
|
"rev": "45938c25e96351adf4140dce42795e61e944904e",
|
||||||
|
"src": "https://git.owall.dev/warg/tree-sitter-tumblr.git"
|
||||||
|
},
|
||||||
"tree-sitter-typescript": {
|
"tree-sitter-typescript": {
|
||||||
"rev": "75b3874edb2dc714fb1fd77a32013d0f8699989f",
|
"rev": "75b3874edb2dc714fb1fd77a32013d0f8699989f",
|
||||||
"src": "https://github.com/tree-sitter/tree-sitter-typescript"
|
"src": "https://github.com/tree-sitter/tree-sitter-typescript"
|
||||||
@@ -145,6 +153,14 @@
|
|||||||
"tree-sitter-zsh": {
|
"tree-sitter-zsh": {
|
||||||
"rev": "86b37f8d515a529722411bc7bf3c9e993a4743bf",
|
"rev": "86b37f8d515a529722411bc7bf3c9e993a4743bf",
|
||||||
"src": "https://github.com/georgeharker/tree-sitter-zsh"
|
"src": "https://github.com/georgeharker/tree-sitter-zsh"
|
||||||
|
},
|
||||||
|
"vim-flog": {
|
||||||
|
"rev": "665b16ac8915f746bc43c9572b4581a5e9047216",
|
||||||
|
"src": "https://github.com/rbong/vim-flog"
|
||||||
|
},
|
||||||
|
"vim-fugitive": {
|
||||||
|
"rev": "3b753cf8c6a4dcde6edee8827d464ba9b8c4a6f0",
|
||||||
|
"src": "https://github.com/tpope/vim-fugitive"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+97
-36
@@ -34,17 +34,59 @@ local DEFAULT_HIGHLIGHTS = {
|
|||||||
GitUnmergedBothModified = "GitUnmerged",
|
GitUnmergedBothModified = "GitUnmerged",
|
||||||
GitUnmergedDeletedByThem = "GitUnmerged",
|
GitUnmergedDeletedByThem = "GitUnmerged",
|
||||||
GitUnmergedDeletedByUs = "GitUnmerged",
|
GitUnmergedDeletedByUs = "GitUnmerged",
|
||||||
|
|
||||||
|
GitHunkAdded = "Added",
|
||||||
|
GitHunkChanged = "Changed",
|
||||||
|
GitHunkRemoved = "Removed",
|
||||||
|
GitHunkAddLine = "DiffAdd",
|
||||||
|
GitHunkDeleteLine = "DiffDelete",
|
||||||
}
|
}
|
||||||
|
local STAGED_HUNK_HL = {
|
||||||
|
GitHunkStagedAdded = "GitHunkAdded",
|
||||||
|
GitHunkStagedChanged = "GitHunkChanged",
|
||||||
|
GitHunkStagedRemoved = "GitHunkRemoved",
|
||||||
|
}
|
||||||
|
|
||||||
|
local function blend(a, b, t)
|
||||||
|
local function mix(shift)
|
||||||
|
local x = bit.band(bit.rshift(a, shift), 0xff)
|
||||||
|
local y = bit.band(bit.rshift(b, shift), 0xff)
|
||||||
|
return bit.lshift(math.floor(x + (y - x) * t + 0.5), shift)
|
||||||
|
end
|
||||||
|
return mix(16) + mix(8) + mix(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function apply_highlights()
|
||||||
for name, link in pairs(DEFAULT_HIGHLIGHTS) do
|
for name, link in pairs(DEFAULT_HIGHLIGHTS) do
|
||||||
vim.api.nvim_set_hl(0, name, { link = link, default = true })
|
vim.api.nvim_set_hl(0, name, { link = link, default = true })
|
||||||
end
|
end
|
||||||
|
local bg = vim.api.nvim_get_hl(0, { name = "Normal" }).bg or 0x000000
|
||||||
|
for name, base in pairs(STAGED_HUNK_HL) do
|
||||||
|
local src = vim.api.nvim_get_hl(0, { name = base, link = false })
|
||||||
|
local hl = {}
|
||||||
|
if src.fg then
|
||||||
|
hl.fg = blend(src.fg, bg, 0.45)
|
||||||
|
end
|
||||||
|
if src.bg then
|
||||||
|
hl.bg = blend(src.bg, bg, 0.45)
|
||||||
|
end
|
||||||
|
vim.api.nvim_set_hl(0, name, hl)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
apply_highlights()
|
||||||
|
|
||||||
local group = vim.api.nvim_create_augroup("ow.git", { clear = true })
|
local group = vim.api.nvim_create_augroup("ow.git", { clear = true })
|
||||||
|
|
||||||
|
vim.api.nvim_create_autocmd("ColorScheme", {
|
||||||
|
group = group,
|
||||||
|
callback = apply_highlights,
|
||||||
|
})
|
||||||
|
|
||||||
vim.api.nvim_create_autocmd({ "BufReadPost", "BufNewFile" }, {
|
vim.api.nvim_create_autocmd({ "BufReadPost", "BufNewFile" }, {
|
||||||
group = group,
|
group = group,
|
||||||
callback = function(args)
|
callback = function(args)
|
||||||
require("git.core.repo").track(args.buf)
|
require("git.core.repo").track(args.buf)
|
||||||
|
require("git.hunks").attach(args.buf)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
vim.api.nvim_create_autocmd({ "BufWritePost", "FileChangedShellPost" }, {
|
vim.api.nvim_create_autocmd({ "BufWritePost", "FileChangedShellPost" }, {
|
||||||
@@ -56,12 +98,15 @@ vim.api.nvim_create_autocmd({ "BufWritePost", "FileChangedShellPost" }, {
|
|||||||
vim.api.nvim_create_autocmd({ "ShellCmdPost", "TermLeave", "FocusGained" }, {
|
vim.api.nvim_create_autocmd({ "ShellCmdPost", "TermLeave", "FocusGained" }, {
|
||||||
group = group,
|
group = group,
|
||||||
callback = function()
|
callback = function()
|
||||||
require("git.core.repo").refresh_all()
|
for _, r in pairs(require("git.core.repo").all()) do
|
||||||
|
r:refresh({ invalidate = true })
|
||||||
|
end
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
vim.api.nvim_create_autocmd({ "BufDelete", "BufWipeout" }, {
|
vim.api.nvim_create_autocmd({ "BufDelete", "BufWipeout" }, {
|
||||||
group = group,
|
group = group,
|
||||||
callback = function(args)
|
callback = function(args)
|
||||||
|
require("git.hunks").detach(args.buf)
|
||||||
require("git.core.repo").unbind(args.buf)
|
require("git.core.repo").unbind(args.buf)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
@@ -94,21 +139,6 @@ vim.api.nvim_create_autocmd("BufReadCmd", {
|
|||||||
require("git.object").read_uri(args.buf)
|
require("git.object").read_uri(args.buf)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
vim.api.nvim_create_autocmd("BufReadCmd", {
|
|
||||||
pattern = "gitlog://*",
|
|
||||||
group = group,
|
|
||||||
callback = function(args)
|
|
||||||
require("git.log_view").read_uri(args.buf)
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
vim.api.nvim_create_autocmd("BufReadCmd", {
|
|
||||||
pattern = "gitstatus://*",
|
|
||||||
group = group,
|
|
||||||
callback = function(args)
|
|
||||||
require("git.status_view").read_uri(args.buf)
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
vim.api.nvim_create_user_command("G", function(opts)
|
vim.api.nvim_create_user_command("G", function(opts)
|
||||||
local cmd = require("git.cmd")
|
local cmd = require("git.cmd")
|
||||||
cmd.run(cmd.parse_args(opts.args), { bang = opts.bang })
|
cmd.run(cmd.parse_args(opts.args), { bang = opts.bang })
|
||||||
@@ -140,22 +170,18 @@ end
|
|||||||
|
|
||||||
local DIFF_DIRECTIONS = { "vertical", "horizontal" }
|
local DIFF_DIRECTIONS = { "vertical", "horizontal" }
|
||||||
|
|
||||||
local function default_vertical()
|
|
||||||
return vim.tbl_contains(vim.opt.diffopt:get(), "vertical")
|
|
||||||
end
|
|
||||||
|
|
||||||
vim.api.nvim_create_user_command("Gdiffsplit", function(opts)
|
vim.api.nvim_create_user_command("Gdiffsplit", function(opts)
|
||||||
local fargs = opts.fargs
|
local fargs = opts.fargs
|
||||||
local vertical = default_vertical()
|
local mods = nil
|
||||||
local rev_idx = 1
|
local rev_idx = 1
|
||||||
if fargs[1] == "vertical" then
|
if fargs[1] == "vertical" then
|
||||||
vertical = true
|
mods = { vertical = true }
|
||||||
rev_idx = 2
|
rev_idx = 2
|
||||||
elseif fargs[1] == "horizontal" then
|
elseif fargs[1] == "horizontal" then
|
||||||
vertical = false
|
mods = { vertical = false }
|
||||||
rev_idx = 2
|
rev_idx = 2
|
||||||
end
|
end
|
||||||
require("git.diff").split({ rev = fargs[rev_idx], vertical = vertical })
|
require("git.diffsplit").open({ target = fargs[rev_idx], mods = mods })
|
||||||
end, {
|
end, {
|
||||||
nargs = "*",
|
nargs = "*",
|
||||||
complete = function(arg_lead, cmd_line, _)
|
complete = function(arg_lead, cmd_line, _)
|
||||||
@@ -216,19 +242,28 @@ vim.keymap.set("n", "<Plug>(git-edit)", function()
|
|||||||
})
|
})
|
||||||
end, { silent = true, desc = "Edit a git object" })
|
end, { silent = true, desc = "Edit a git object" })
|
||||||
|
|
||||||
vim.keymap.set("n", "<Plug>(git-diff-vertical)", function()
|
vim.keymap.set("n", "<Plug>(git-diffsplit-vertical)", function()
|
||||||
require("git.diff").split({ vertical = true })
|
require("git.diffsplit").open({ mods = { vertical = true } })
|
||||||
end, { silent = true, desc = "Diff against index (vertical)" })
|
end, { silent = true, desc = "Open a diff split against index (vertical)" })
|
||||||
vim.keymap.set("n", "<Plug>(git-diff-horizontal)", function()
|
vim.keymap.set("n", "<Plug>(git-diffsplit-horizontal)", function()
|
||||||
require("git.diff").split({ vertical = false })
|
require("git.diffsplit").open({ mods = { vertical = false } })
|
||||||
end, { silent = true, desc = "Diff against index (horizontal)" })
|
end, { silent = true, desc = "Open a diff split against index (horizontal)" })
|
||||||
vim.keymap.set("n", "<Plug>(git-diff-vertical-head)", function()
|
vim.keymap.set("n", "<Plug>(git-diffsplit-vertical-head)", function()
|
||||||
require("git.diff").split({ rev = "HEAD", vertical = true })
|
require("git.diffsplit").open({
|
||||||
end, { silent = true, desc = "Diff against HEAD (vertical)" })
|
target = "HEAD",
|
||||||
vim.keymap.set("n", "<Plug>(git-diff-horizontal-head)", function()
|
mods = { vertical = true },
|
||||||
require("git.diff").split({ rev = "HEAD", vertical = false })
|
})
|
||||||
end, { silent = true, desc = "Diff against HEAD (horizontal)" })
|
end, { silent = true, desc = "Open a diff split against HEAD (vertical)" })
|
||||||
|
vim.keymap.set("n", "<Plug>(git-diffsplit-horizontal-head)", function()
|
||||||
|
require("git.diffsplit").open({
|
||||||
|
target = "HEAD",
|
||||||
|
mods = { vertical = false },
|
||||||
|
})
|
||||||
|
end, { silent = true, desc = "Open a diff split against HEAD (horizontal)" })
|
||||||
|
|
||||||
|
vim.keymap.set("n", "<Plug>(git-status-open)", function()
|
||||||
|
require("git.status_view").open()
|
||||||
|
end, { silent = true, desc = "Open git status sidebar" })
|
||||||
vim.keymap.set("n", "<Plug>(git-status-toggle)", function()
|
vim.keymap.set("n", "<Plug>(git-status-toggle)", function()
|
||||||
require("git.status_view").toggle()
|
require("git.status_view").toggle()
|
||||||
end, { silent = true, desc = "Toggle git status sidebar" })
|
end, { silent = true, desc = "Toggle git status sidebar" })
|
||||||
@@ -253,3 +288,29 @@ if vim.g.git_statusline ~= false then
|
|||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
vim.keymap.set({ "n", "x" }, "<Plug>(git-hunk-next)", function()
|
||||||
|
require("git.hunks").nav("next")
|
||||||
|
end, { silent = true, desc = "Jump to next git hunk" })
|
||||||
|
vim.keymap.set({ "n", "x" }, "<Plug>(git-hunk-prev)", function()
|
||||||
|
require("git.hunks").nav("prev")
|
||||||
|
end, { silent = true, desc = "Jump to previous git hunk" })
|
||||||
|
vim.keymap.set("n", "<Plug>(git-hunk-stage-toggle)", function()
|
||||||
|
require("git.hunks").toggle_stage()
|
||||||
|
end, { silent = true, desc = "Stage or unstage the hunk under cursor" })
|
||||||
|
vim.keymap.set("n", "<Plug>(git-hunk-reset)", function()
|
||||||
|
require("git.hunks").reset_hunk()
|
||||||
|
end, { silent = true, desc = "Reset hunk under cursor" })
|
||||||
|
vim.keymap.set("n", "<Plug>(git-hunk-preview)", function()
|
||||||
|
require("git.hunks").preview_hunk()
|
||||||
|
end, { silent = true, desc = "Preview hunk under cursor" })
|
||||||
|
vim.keymap.set("n", "<Plug>(git-hunk-select)", function()
|
||||||
|
require("git.hunks").select_hunk()
|
||||||
|
end, { silent = true, desc = "Select hunk under cursor" })
|
||||||
|
vim.keymap.set("n", "<Plug>(git-overlay-toggle)", function()
|
||||||
|
require("git.hunks").toggle_overlay()
|
||||||
|
end, { silent = true, desc = "Toggle the git diff overlay" })
|
||||||
|
|
||||||
|
vim.api.nvim_create_user_command("GitDiffOverlay", function()
|
||||||
|
require("git.hunks").toggle_overlay()
|
||||||
|
end, { desc = "Toggle the git diff overlay in the current buffer" })
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
require("gitsigns").setup({
|
|
||||||
preview_config = {
|
|
||||||
border = "single",
|
|
||||||
},
|
|
||||||
on_attach = function(bufnr)
|
|
||||||
local gs = require("gitsigns")
|
|
||||||
vim.keymap.set("n", "<leader>gv", gs.select_hunk, { buffer = bufnr })
|
|
||||||
vim.keymap.set("n", "<leader>gs", gs.stage_hunk, { buffer = bufnr })
|
|
||||||
vim.keymap.set("x", "<leader>gs", function()
|
|
||||||
gs.stage_hunk({ vim.fn.line("."), vim.fn.line("v") })
|
|
||||||
end, { buffer = bufnr })
|
|
||||||
vim.keymap.set("n", "<leader>gr", gs.reset_hunk, { buffer = bufnr })
|
|
||||||
vim.keymap.set(
|
|
||||||
"x",
|
|
||||||
"<leader>gr",
|
|
||||||
":Gitsigns reset_hunk<CR>",
|
|
||||||
{ buffer = bufnr }
|
|
||||||
)
|
|
||||||
vim.keymap.set("n", "<leader>g?", gs.preview_hunk, { buffer = bufnr })
|
|
||||||
vim.keymap.set("n", "<leader>gb", function()
|
|
||||||
gs.blame_line({ full = true, ignore_whitespace = true })
|
|
||||||
end, { buffer = bufnr })
|
|
||||||
vim.keymap.set({ "n", "x" }, "]g", function()
|
|
||||||
gs.nav_hunk("next", {
|
|
||||||
wrap = true,
|
|
||||||
foldopen = true,
|
|
||||||
navigation_message = true,
|
|
||||||
greedy = true,
|
|
||||||
preview = true,
|
|
||||||
count = 1,
|
|
||||||
target = "all",
|
|
||||||
})
|
|
||||||
end)
|
|
||||||
vim.keymap.set({ "n", "x" }, "[g", function()
|
|
||||||
gs.nav_hunk("prev", {
|
|
||||||
wrap = true,
|
|
||||||
foldopen = true,
|
|
||||||
navigation_message = true,
|
|
||||||
greedy = true,
|
|
||||||
preview = true,
|
|
||||||
count = 1,
|
|
||||||
target = "all",
|
|
||||||
})
|
|
||||||
end)
|
|
||||||
end,
|
|
||||||
attach_to_untracked = false,
|
|
||||||
sign_priority = 100,
|
|
||||||
})
|
|
||||||
@@ -7,7 +7,7 @@ local Decorator = require("nvim-tree.api").Decorator
|
|||||||
|
|
||||||
local repo = require("git.core.repo")
|
local repo = require("git.core.repo")
|
||||||
|
|
||||||
repo.on("refresh", function()
|
repo.on("change", function()
|
||||||
require("nvim-tree.api").tree.reload()
|
require("nvim-tree.api").tree.reload()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|||||||
+1
-3
@@ -45,10 +45,8 @@ local highlights = {
|
|||||||
TabLineFill = { bg = c.bg1 },
|
TabLineFill = { bg = c.bg1 },
|
||||||
EndOfBuffer = { fg = "NONE", bg = "NONE" },
|
EndOfBuffer = { fg = "NONE", bg = "NONE" },
|
||||||
DiffAdd = { bg = "#1a2f22" },
|
DiffAdd = { bg = "#1a2f22" },
|
||||||
DiffChange = { bg = "#15304a" },
|
|
||||||
DiffDelete = { bg = "#311c1e" },
|
|
||||||
Changed = { fg = c.yellow },
|
|
||||||
NvimTreeIndentMarker = { fg = c.bg3 },
|
NvimTreeIndentMarker = { fg = c.bg3 },
|
||||||
|
GitUnstaged = { fg = c.yellow },
|
||||||
}
|
}
|
||||||
for kind, color in pairs(completion_kind_colors) do
|
for kind, color in pairs(completion_kind_colors) do
|
||||||
highlights["LspKind" .. kind] = { fg = color }
|
highlights["LspKind" .. kind] = { fg = color }
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
; inherits: ecma,jsx
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
; inherits: ecma,jsx
|
||||||
|
|
||||||
|
; Parameters
|
||||||
|
(formal_parameters
|
||||||
|
(identifier) @variable.parameter)
|
||||||
|
|
||||||
|
(formal_parameters
|
||||||
|
(rest_pattern
|
||||||
|
(identifier) @variable.parameter))
|
||||||
|
|
||||||
|
; ({ a }) => null
|
||||||
|
(formal_parameters
|
||||||
|
(object_pattern
|
||||||
|
(shorthand_property_identifier_pattern) @variable.parameter))
|
||||||
|
|
||||||
|
; ({ a = b }) => null
|
||||||
|
(formal_parameters
|
||||||
|
(object_pattern
|
||||||
|
(object_assignment_pattern
|
||||||
|
(shorthand_property_identifier_pattern) @variable.parameter)))
|
||||||
|
|
||||||
|
; ({ a: b }) => null
|
||||||
|
(formal_parameters
|
||||||
|
(object_pattern
|
||||||
|
(pair_pattern
|
||||||
|
value: (identifier) @variable.parameter)))
|
||||||
|
|
||||||
|
; ([ a ]) => null
|
||||||
|
(formal_parameters
|
||||||
|
(array_pattern
|
||||||
|
(identifier) @variable.parameter))
|
||||||
|
|
||||||
|
; ({ a } = { a }) => null
|
||||||
|
(formal_parameters
|
||||||
|
(assignment_pattern
|
||||||
|
(object_pattern
|
||||||
|
(shorthand_property_identifier_pattern) @variable.parameter)))
|
||||||
|
|
||||||
|
; ({ a = b } = { a }) => null
|
||||||
|
(formal_parameters
|
||||||
|
(assignment_pattern
|
||||||
|
(object_pattern
|
||||||
|
(object_assignment_pattern
|
||||||
|
(shorthand_property_identifier_pattern) @variable.parameter))))
|
||||||
|
|
||||||
|
; a => null
|
||||||
|
(arrow_function
|
||||||
|
parameter: (identifier) @variable.parameter)
|
||||||
|
|
||||||
|
; optional parameters
|
||||||
|
(formal_parameters
|
||||||
|
(assignment_pattern
|
||||||
|
left: (identifier) @variable.parameter))
|
||||||
|
|
||||||
|
; punctuation
|
||||||
|
(optional_chain) @punctuation.delimiter
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
; inherits: ecma,jsx
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
if exists("b:current_syntax")
|
|
||||||
finish
|
|
||||||
endif
|
|
||||||
|
|
||||||
syntax match gitStatusLabel /\v^(Head|Push)\ze:/
|
|
||||||
syntax match gitStatusBranch /\v(^(Head|Push):\s+)@<=\S+/
|
|
||||||
syntax match gitStatusAhead /\v\+\d+/
|
|
||||||
syntax match gitStatusBehind /\v-\d+/
|
|
||||||
|
|
||||||
syntax region gitStatusUntrackedHeader start=/\v^Untracked>/ end=/\v^$/
|
|
||||||
syntax region gitStatusUnstagedHeader start=/\v^Unstaged>/ end=/\v^$/
|
|
||||||
syntax region gitStatusStagedHeader start=/\v^Staged>/ end=/\v^$/
|
|
||||||
syntax region gitStatusUnmergedHeader start=/\v^Unmerged>/ end=/\v^$/
|
|
||||||
syntax region gitStatusUnpushedHeader start=/\v^Unpushed>/ end=/\v^$/
|
|
||||||
syntax region gitStatusUnpulledHeader start=/\v^Unpulled>/ end=/\v^$/
|
|
||||||
|
|
||||||
syntax match gitStatusUntrackedLabel /\v^Untracked/ contained containedin=gitStatusUntrackedHeader
|
|
||||||
syntax match gitStatusUnstagedLabel /\v^Unstaged/ contained containedin=gitStatusUnstagedHeader
|
|
||||||
syntax match gitStatusStagedLabel /\v^Staged/ contained containedin=gitStatusStagedHeader
|
|
||||||
syntax match gitStatusUnmergedLabel /\v^Unmerged/ contained containedin=gitStatusUnmergedHeader
|
|
||||||
syntax match gitStatusUnpushedLabel /\v^Unpushed/ contained containedin=gitStatusUnpushedHeader
|
|
||||||
syntax match gitStatusUnpulledLabel /\v^Unpulled/ contained containedin=gitStatusUnpulledHeader
|
|
||||||
|
|
||||||
syntax match gitStatusHeaderCount /\v\(\zs\d+\ze\)/ contained containedin=gitStatusUntrackedHeader,
|
|
||||||
\ gitStatusUnstagedHeader,
|
|
||||||
\ gitStatusStagedHeader,
|
|
||||||
\ gitStatusUnmergedHeader,
|
|
||||||
\ gitStatusUnpushedHeader,
|
|
||||||
\ gitStatusUnpulledHeader
|
|
||||||
|
|
||||||
highlight default link gitStatusLabel Label
|
|
||||||
highlight default link gitStatusBranch None
|
|
||||||
highlight default link gitStatusAhead GitUnpushed
|
|
||||||
highlight default link gitStatusBehind GitUnpulled
|
|
||||||
highlight default link gitStatusHeaderCount Number
|
|
||||||
|
|
||||||
highlight default link gitStatusUntrackedLabel gitStatusLabel
|
|
||||||
highlight default link gitStatusUnstagedLabel gitStatusLabel
|
|
||||||
highlight default link gitStatusStagedLabel gitStatusLabel
|
|
||||||
highlight default link gitStatusUnmergedLabel gitStatusLabel
|
|
||||||
highlight default link gitStatusUnpushedLabel gitStatusLabel
|
|
||||||
highlight default link gitStatusUnpulledLabel gitStatusLabel
|
|
||||||
|
|
||||||
let b:current_syntax = "gitStatus"
|
|
||||||
@@ -338,10 +338,10 @@ t.test("<leader>gl log buffer refills after jumping back", function()
|
|||||||
h.git(dir, "commit", "-q", "-m", "second")
|
h.git(dir, "commit", "-q", "-m", "second")
|
||||||
|
|
||||||
require("git.log_view").open({ max_count = 1000 })
|
require("git.log_view").open({ max_count = 1000 })
|
||||||
wait_buf_populated("^gitlog://")
|
wait_buf_populated("/GitLog$")
|
||||||
local log_buf = vim.api.nvim_get_current_buf()
|
local log_buf = vim.api.nvim_get_current_buf()
|
||||||
local log_win = vim.api.nvim_get_current_win()
|
local log_win = vim.api.nvim_get_current_win()
|
||||||
t.truthy(vim.api.nvim_buf_get_name(log_buf):match("^gitlog://"))
|
t.truthy(vim.api.nvim_buf_get_name(log_buf):match("/GitLog$"))
|
||||||
local initial_lines = #vim.api.nvim_buf_get_lines(log_buf, 0, -1, false)
|
local initial_lines = #vim.api.nvim_buf_get_lines(log_buf, 0, -1, false)
|
||||||
t.truthy(initial_lines >= 2)
|
t.truthy(initial_lines >= 2)
|
||||||
|
|
||||||
|
|||||||
@@ -73,4 +73,25 @@ function M.make_repo(files, opts)
|
|||||||
return dir
|
return dir
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Build an outer repo with one nested submodule at `sub/`. Both the
|
||||||
|
---outer and inner repo are committed and registered for cleanup.
|
||||||
|
---@return string outer
|
||||||
|
---@return string inner
|
||||||
|
function M.make_submodule_repo()
|
||||||
|
local inner = M.make_repo({ a = "x\n" })
|
||||||
|
local outer = M.make_repo({ x = "x\n" })
|
||||||
|
vim.system({
|
||||||
|
"git",
|
||||||
|
"-c",
|
||||||
|
"protocol.file.allow=always",
|
||||||
|
"submodule",
|
||||||
|
"add",
|
||||||
|
"--quiet",
|
||||||
|
inner,
|
||||||
|
"sub",
|
||||||
|
}, { cwd = outer, text = true }):wait()
|
||||||
|
M.git(outer, "commit", "-q", "-m", "add submodule")
|
||||||
|
return outer, inner
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -0,0 +1,634 @@
|
|||||||
|
local h = require("test.git.helpers")
|
||||||
|
local hunks = require("git.hunks")
|
||||||
|
local t = require("test")
|
||||||
|
|
||||||
|
---@param committed string
|
||||||
|
---@param worktree string
|
||||||
|
---@param file string?
|
||||||
|
---@return string dir
|
||||||
|
---@return integer buf
|
||||||
|
---@return ow.Git.Hunks.BufState state
|
||||||
|
local function setup(committed, worktree, file)
|
||||||
|
file = file or "a.txt"
|
||||||
|
local dir = h.make_repo({ [file] = committed })
|
||||||
|
t.write(dir, file, worktree)
|
||||||
|
vim.cmd.edit(dir .. "/" .. file)
|
||||||
|
local buf = vim.api.nvim_get_current_buf()
|
||||||
|
hunks.attach(buf)
|
||||||
|
hunks._flush(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
local s = hunks.state(buf)
|
||||||
|
return s ~= nil and s.index ~= nil and s.head ~= nil
|
||||||
|
end, "hunks to load the index and HEAD snapshots")
|
||||||
|
local state = assert(hunks.state(buf), "buffer state should exist")
|
||||||
|
return dir, buf, state
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param buf integer
|
||||||
|
---@return { row: integer, sign: string, hl: string }[]
|
||||||
|
local function sign_marks(buf)
|
||||||
|
local ns = vim.api.nvim_get_namespaces()["ow.git.hunks"]
|
||||||
|
local out = {}
|
||||||
|
for _, m in ipairs(vim.api.nvim_buf_get_extmarks(buf, ns, 0, -1, {
|
||||||
|
details = true,
|
||||||
|
})) do
|
||||||
|
local d = assert(m[4])
|
||||||
|
table.insert(out, {
|
||||||
|
row = m[2],
|
||||||
|
sign = vim.trim(d.sign_text or ""),
|
||||||
|
hl = d.sign_hl_group,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
table.sort(out, function(a, b)
|
||||||
|
return a.row < b.row
|
||||||
|
end)
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param buf integer
|
||||||
|
---@param ns_name string
|
||||||
|
---@return vim.api.keyset.get_extmark_item[]
|
||||||
|
local function detailed_marks(buf, ns_name)
|
||||||
|
local ns = vim.api.nvim_get_namespaces()[ns_name]
|
||||||
|
return vim.api.nvim_buf_get_extmarks(buf, ns, 0, -1, { details = true })
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return integer?
|
||||||
|
local function find_float()
|
||||||
|
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
||||||
|
if vim.api.nvim_win_get_config(w).relative ~= "" then
|
||||||
|
return w
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
t.test("pure add: hunk shape and add signs", function()
|
||||||
|
local _, buf, state = setup("a\nd\n", "a\nb\nc\nd\n")
|
||||||
|
t.eq(#state.hunks, 1, "one hunk for a pure addition")
|
||||||
|
local hk = assert(state.hunks[1])
|
||||||
|
t.eq(hk.type, "add")
|
||||||
|
t.eq(hk.new_start, 2)
|
||||||
|
t.eq(hk.new_count, 2)
|
||||||
|
t.eq(sign_marks(buf), {
|
||||||
|
{ row = 1, sign = "┃", hl = "GitHunkAdded" },
|
||||||
|
{ row = 2, sign = "┃", hl = "GitHunkAdded" },
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("pure delete (middle): hunk shape and delete sign", function()
|
||||||
|
local _, buf, state = setup("a\nb\nc\n", "a\nc\n")
|
||||||
|
t.eq(#state.hunks, 1)
|
||||||
|
local hk = assert(state.hunks[1])
|
||||||
|
t.eq(hk.type, "delete")
|
||||||
|
t.eq(hk.new_count, 0)
|
||||||
|
t.eq(hk.old_lines, { "b" })
|
||||||
|
t.eq(sign_marks(buf), {
|
||||||
|
{ row = 0, sign = "▁", hl = "GitHunkRemoved" },
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("top-of-file delete: sign anchors on line 1", function()
|
||||||
|
local _, buf, state = setup("a\nb\nc\n", "b\nc\n")
|
||||||
|
t.eq(#state.hunks, 1)
|
||||||
|
local hk = assert(state.hunks[1])
|
||||||
|
t.eq(hk.type, "delete")
|
||||||
|
t.eq(hk.new_start, 0)
|
||||||
|
t.eq(hk.old_lines, { "a" })
|
||||||
|
t.eq(sign_marks(buf), {
|
||||||
|
{ row = 0, sign = "▁", hl = "GitHunkRemoved" },
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("change of N lines: hunk shape and change signs", function()
|
||||||
|
local _, buf, state = setup("a\nb\nc\nd\n", "a\nB\nC\nd\n")
|
||||||
|
t.eq(#state.hunks, 1)
|
||||||
|
local hk = assert(state.hunks[1])
|
||||||
|
t.eq(hk.type, "change")
|
||||||
|
t.eq(hk.old_start, 2)
|
||||||
|
t.eq(hk.old_count, 2)
|
||||||
|
t.eq(hk.new_start, 2)
|
||||||
|
t.eq(hk.new_count, 2)
|
||||||
|
t.eq(hk.old_lines, { "b", "c" })
|
||||||
|
t.eq(hk.new_lines, { "B", "C" })
|
||||||
|
t.eq(sign_marks(buf), {
|
||||||
|
{ row = 1, sign = "┃", hl = "GitHunkChanged" },
|
||||||
|
{ row = 2, sign = "┃", hl = "GitHunkChanged" },
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("multi-hunk file: two separate change hunks", function()
|
||||||
|
local _, buf, state = setup("a\nb\nc\nd\ne\n", "A\nb\nc\nd\nE\n")
|
||||||
|
t.eq(#state.hunks, 2, "two hunks for two disjoint changes")
|
||||||
|
t.eq(sign_marks(buf), {
|
||||||
|
{ row = 0, sign = "┃", hl = "GitHunkChanged" },
|
||||||
|
{ row = 4, sign = "┃", hl = "GitHunkChanged" },
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("clean file produces no hunks or signs", function()
|
||||||
|
local _, buf, state = setup("a\nb\nc\n", "a\nb\nc\n")
|
||||||
|
t.eq(#state.hunks, 0)
|
||||||
|
t.eq(sign_marks(buf), {})
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("editing the buffer refreshes signs", function()
|
||||||
|
local _, buf, state = setup("a\nb\nc\n", "a\nb\nc\n")
|
||||||
|
t.eq(#state.hunks, 0)
|
||||||
|
vim.api.nvim_buf_set_lines(buf, 1, 2, false, { "CHANGED" })
|
||||||
|
vim.api.nvim_exec_autocmds("TextChanged", { buffer = buf })
|
||||||
|
hunks._flush(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
local s = assert(hunks.state(buf))
|
||||||
|
return #s.hunks == 1
|
||||||
|
end, "hunks to pick up the in-buffer edit")
|
||||||
|
local hk = assert(assert(hunks.state(buf)).hunks[1])
|
||||||
|
t.eq(hk.type, "change")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("overlay: change hunk shows deletion and addition", function()
|
||||||
|
local _, buf = setup("a\nb\nc\n", "a\nB\nc\n")
|
||||||
|
hunks.toggle_overlay(buf)
|
||||||
|
---@type integer?
|
||||||
|
local add_row
|
||||||
|
---@type vim.api.keyset.extmark_details?
|
||||||
|
local add_d
|
||||||
|
---@type vim.api.keyset.extmark_details?
|
||||||
|
local virt_d
|
||||||
|
for _, m in ipairs(detailed_marks(buf, "ow.git.hunks.overlay")) do
|
||||||
|
local d = assert(m[4])
|
||||||
|
if d.line_hl_group then
|
||||||
|
add_row, add_d = m[2], d
|
||||||
|
elseif d.virt_lines then
|
||||||
|
virt_d = d
|
||||||
|
end
|
||||||
|
end
|
||||||
|
add_d = assert(add_d, "the added line should get a line highlight")
|
||||||
|
t.eq(add_row, 1, "addition highlighted on the changed line")
|
||||||
|
t.eq(add_d.line_hl_group, "GitHunkAddLine")
|
||||||
|
virt_d = assert(virt_d, "the deletion should render as virtual lines")
|
||||||
|
local piece = assert(assert(assert(virt_d.virt_lines)[1])[1])
|
||||||
|
t.truthy(vim.startswith(piece[1], "b"), "deleted line shows the old content")
|
||||||
|
t.eq(piece[2], "GitHunkDeleteLine")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("overlay: delete hunk shows only deletion lines", function()
|
||||||
|
local _, buf = setup("a\nb\nc\n", "a\nc\n")
|
||||||
|
hunks.toggle_overlay(buf)
|
||||||
|
local marks = detailed_marks(buf, "ow.git.hunks.overlay")
|
||||||
|
t.eq(#marks, 1, "a pure delete has no addition highlight")
|
||||||
|
local d = assert(assert(marks[1])[4])
|
||||||
|
local piece = assert(assert(assert(d.virt_lines)[1])[1])
|
||||||
|
t.truthy(vim.startswith(piece[1], "b"))
|
||||||
|
t.eq(piece[2], "GitHunkDeleteLine")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("overlay: add hunk highlights the added lines", function()
|
||||||
|
local _, buf = setup("a\nd\n", "a\nb\nc\nd\n")
|
||||||
|
hunks.toggle_overlay(buf)
|
||||||
|
local rows = {}
|
||||||
|
for _, m in ipairs(detailed_marks(buf, "ow.git.hunks.overlay")) do
|
||||||
|
local d = assert(m[4])
|
||||||
|
t.falsy(d.virt_lines, "a pure add has no deletion virtual lines")
|
||||||
|
t.eq(d.line_hl_group, "GitHunkAddLine")
|
||||||
|
table.insert(rows, m[2])
|
||||||
|
end
|
||||||
|
table.sort(rows)
|
||||||
|
t.eq(rows, { 1, 2 }, "both added lines highlighted")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("overlay: deleted lines are treesitter-highlighted", function()
|
||||||
|
local _, buf = setup(
|
||||||
|
"-- a note\nlocal x = 1\nlocal y = 2\n",
|
||||||
|
"local y = 2\n",
|
||||||
|
"a.lua"
|
||||||
|
)
|
||||||
|
t.truthy(
|
||||||
|
pcall(vim.treesitter.start, buf, "lua"),
|
||||||
|
"the lua parser should be available"
|
||||||
|
)
|
||||||
|
hunks.toggle_overlay(buf)
|
||||||
|
---@type table[]?
|
||||||
|
local virt
|
||||||
|
for _, m in ipairs(detailed_marks(buf, "ow.git.hunks.overlay")) do
|
||||||
|
local d = assert(m[4])
|
||||||
|
if d.virt_lines then
|
||||||
|
virt = d.virt_lines
|
||||||
|
end
|
||||||
|
end
|
||||||
|
virt = assert(virt, "a deletion virtual line should render")
|
||||||
|
---@type table<string, boolean>
|
||||||
|
local seen = {}
|
||||||
|
for _, line in ipairs(virt) do
|
||||||
|
for _, c in ipairs(line) do
|
||||||
|
local hl = c[2]
|
||||||
|
if
|
||||||
|
type(hl) == "table"
|
||||||
|
and hl[1] == "GitHunkDeleteLine"
|
||||||
|
and hl[2]
|
||||||
|
then
|
||||||
|
seen[hl[2]] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
t.truthy(seen["@comment"], "the deleted comment keeps its @comment group")
|
||||||
|
t.truthy(seen["@keyword"], "deleted code keeps its syntax groups")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("overlay: toggling swaps gutter signs for the overlay", function()
|
||||||
|
local _, buf = setup("a\nb\nc\n", "a\nB\nc\n")
|
||||||
|
t.truthy(
|
||||||
|
#detailed_marks(buf, "ow.git.hunks") > 0,
|
||||||
|
"gutter signs present while the overlay is off"
|
||||||
|
)
|
||||||
|
t.eq(#detailed_marks(buf, "ow.git.hunks.overlay"), 0)
|
||||||
|
|
||||||
|
hunks.toggle_overlay(buf)
|
||||||
|
t.truthy(
|
||||||
|
#detailed_marks(buf, "ow.git.hunks.overlay") > 0,
|
||||||
|
"overlay present once it is on"
|
||||||
|
)
|
||||||
|
t.eq(
|
||||||
|
#detailed_marks(buf, "ow.git.hunks"),
|
||||||
|
0,
|
||||||
|
"gutter signs replaced while the overlay is on"
|
||||||
|
)
|
||||||
|
|
||||||
|
hunks.toggle_overlay(buf)
|
||||||
|
t.eq(#detailed_marks(buf, "ow.git.hunks.overlay"), 0)
|
||||||
|
t.truthy(
|
||||||
|
#detailed_marks(buf, "ow.git.hunks") > 0,
|
||||||
|
"gutter signs restored after toggling the overlay off"
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("toggle_stage stages the change into the index", function()
|
||||||
|
local dir, buf = setup("a\nb\nc\n", "a\nB\nc\n")
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||||
|
hunks.toggle_stage(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
return h.git(dir, "diff", "--cached", "--name-only").stdout ~= ""
|
||||||
|
end, "stage to land in the index")
|
||||||
|
t.eq(h.git(dir, "diff", "--cached", "--name-only").stdout, "a.txt")
|
||||||
|
t.eq(
|
||||||
|
h.git(dir, "show", ":0:a.txt").stdout,
|
||||||
|
"a\nB\nc",
|
||||||
|
"index blob reflects the staged change"
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("toggle_stage stages a pure addition", function()
|
||||||
|
local dir, buf = setup("a\nb\n", "a\nb\nc\n")
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 3, 0 })
|
||||||
|
hunks.toggle_stage(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
return h.git(dir, "diff", "--cached", "--name-only").stdout ~= ""
|
||||||
|
end, "stage to land in the index")
|
||||||
|
t.eq(h.git(dir, "show", ":0:a.txt").stdout, "a\nb\nc")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("toggle_stage stages a deletion", function()
|
||||||
|
local dir, buf = setup("a\nb\nc\n", "a\nc\n")
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||||
|
hunks.toggle_stage(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
return h.git(dir, "diff", "--cached", "--name-only").stdout ~= ""
|
||||||
|
end, "stage to land in the index")
|
||||||
|
t.eq(h.git(dir, "show", ":0:a.txt").stdout, "a\nc")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("toggle_stage stages only the hunk under the cursor", function()
|
||||||
|
local committed = table.concat({
|
||||||
|
"local M = {}",
|
||||||
|
"",
|
||||||
|
"function M.first()",
|
||||||
|
" return 1",
|
||||||
|
"end",
|
||||||
|
"",
|
||||||
|
"function M.last()",
|
||||||
|
" return 9",
|
||||||
|
"end",
|
||||||
|
"",
|
||||||
|
"return M",
|
||||||
|
}, "\n") .. "\n"
|
||||||
|
local worktree = table.concat({
|
||||||
|
"local M = {}",
|
||||||
|
"",
|
||||||
|
"-- helpers",
|
||||||
|
"function M.first()",
|
||||||
|
" return 1",
|
||||||
|
"end",
|
||||||
|
"",
|
||||||
|
"function M.mid()",
|
||||||
|
" return 5",
|
||||||
|
"end",
|
||||||
|
"",
|
||||||
|
"function M.last()",
|
||||||
|
" return 9",
|
||||||
|
"end",
|
||||||
|
"",
|
||||||
|
"return M",
|
||||||
|
}, "\n") .. "\n"
|
||||||
|
local dir, buf = setup(committed, worktree)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 9, 0 })
|
||||||
|
hunks.toggle_stage(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
return h.git(dir, "diff", "--cached", "--name-only").stdout ~= ""
|
||||||
|
end, "the mid hunk to land in the index")
|
||||||
|
t.eq(
|
||||||
|
h.git(dir, "show", ":0:a.txt").stdout,
|
||||||
|
table.concat({
|
||||||
|
"local M = {}",
|
||||||
|
"",
|
||||||
|
"function M.first()",
|
||||||
|
" return 1",
|
||||||
|
"end",
|
||||||
|
"",
|
||||||
|
"function M.mid()",
|
||||||
|
" return 5",
|
||||||
|
"end",
|
||||||
|
"",
|
||||||
|
"function M.last()",
|
||||||
|
" return 9",
|
||||||
|
"end",
|
||||||
|
"",
|
||||||
|
"return M",
|
||||||
|
}, "\n"),
|
||||||
|
"only the cursor's hunk is staged, placed at the right line"
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("toggle_stage stages a whole-file change with no context", function()
|
||||||
|
local dir, buf = setup("a\nb\nc\n", "x\ny\nz\n")
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||||
|
hunks.toggle_stage(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
return h.git(dir, "diff", "--cached", "--name-only").stdout ~= ""
|
||||||
|
end, "the change to land in the index")
|
||||||
|
t.eq(h.git(dir, "show", ":0:a.txt").stdout, "x\ny\nz")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("toggle_stage stages a change at the start of the file", function()
|
||||||
|
local dir, buf = setup("a\nb\nc\nd\ne\n", "A\nb\nc\nd\ne\n")
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||||
|
hunks.toggle_stage(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
return h.git(dir, "diff", "--cached", "--name-only").stdout ~= ""
|
||||||
|
end, "the change to land in the index")
|
||||||
|
t.eq(h.git(dir, "show", ":0:a.txt").stdout, "A\nb\nc\nd\ne")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("toggle_stage stages a change at the end of the file", function()
|
||||||
|
local dir, buf = setup("a\nb\nc\nd\ne\n", "a\nb\nc\nd\nE\n")
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 5, 0 })
|
||||||
|
hunks.toggle_stage(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
return h.git(dir, "diff", "--cached", "--name-only").stdout ~= ""
|
||||||
|
end, "the change to land in the index")
|
||||||
|
t.eq(h.git(dir, "show", ":0:a.txt").stdout, "a\nb\nc\nd\nE")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("toggle_stage stages a deletion at the start of the file", function()
|
||||||
|
local dir, buf = setup("a\nb\nc\nd\n", "b\nc\nd\n")
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||||
|
hunks.toggle_stage(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
return h.git(dir, "diff", "--cached", "--name-only").stdout ~= ""
|
||||||
|
end, "the deletion to land in the index")
|
||||||
|
t.eq(h.git(dir, "show", ":0:a.txt").stdout, "b\nc\nd")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("toggle_stage leaves an adjacent unstaged hunk in place", function()
|
||||||
|
local dir, buf = setup("a\nb\nc\nd\ne\n", "A\nb\nC\nd\ne\n")
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 3, 0 })
|
||||||
|
hunks.toggle_stage(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
return h.git(dir, "diff", "--cached", "--name-only").stdout ~= ""
|
||||||
|
end, "the line-3 hunk to land in the index")
|
||||||
|
t.eq(
|
||||||
|
h.git(dir, "show", ":0:a.txt").stdout,
|
||||||
|
"a\nb\nC\nd\ne",
|
||||||
|
"only line 3 is staged; the adjacent line-1 hunk is untouched"
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("toggle_stage unstages one of two adjacent staged hunks", function()
|
||||||
|
local dir, buf = setup("a\nb\nc\nd\ne\n", "A\nb\nC\nd\ne\n")
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||||
|
hunks.toggle_stage(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
return #assert(hunks.state(buf)).staged == 1
|
||||||
|
end, "the line-1 hunk to be staged")
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 3, 0 })
|
||||||
|
hunks.toggle_stage(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
local s = assert(hunks.state(buf))
|
||||||
|
return #s.hunks == 0 and #s.staged == 2
|
||||||
|
end, "both hunks to be staged")
|
||||||
|
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 3, 0 })
|
||||||
|
hunks.toggle_stage(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
return #assert(hunks.state(buf)).staged == 1
|
||||||
|
end, "the line-3 hunk to be unstaged again")
|
||||||
|
t.eq(
|
||||||
|
h.git(dir, "show", ":0:a.txt").stdout,
|
||||||
|
"A\nb\nc\nd\ne",
|
||||||
|
"line 3 reverts to HEAD while the staged line-1 change remains"
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("toggle_stage refreshes the gutter when status stays modified", function()
|
||||||
|
local _, buf = setup("a\nb\nc\nd\ne\n", "A\nb\nC\nd\nE\n")
|
||||||
|
t.eq(#assert(hunks.state(buf)).hunks, 3)
|
||||||
|
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||||
|
hunks.toggle_stage(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
return #assert(hunks.state(buf)).hunks == 2
|
||||||
|
end, "gutter to drop the first staged hunk")
|
||||||
|
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 3, 0 })
|
||||||
|
hunks.toggle_stage(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
return #assert(hunks.state(buf)).hunks == 1
|
||||||
|
end, "gutter to drop the middle staged hunk")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("staged hunks show with the staged highlight", function()
|
||||||
|
local _, buf = setup("a\nb\nc\n", "a\nB\nc\n")
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||||
|
hunks.toggle_stage(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
local s = assert(hunks.state(buf))
|
||||||
|
return #s.hunks == 0 and #s.staged == 1
|
||||||
|
end, "the hunk to move from unstaged to staged")
|
||||||
|
t.eq(sign_marks(buf), {
|
||||||
|
{ row = 1, sign = "┃", hl = "GitHunkStagedChanged" },
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("the gutter shows staged and unstaged hunks together", function()
|
||||||
|
local _, buf = setup("a\nb\nc\nd\ne\n", "A\nb\nC\nd\nE\n")
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||||
|
hunks.toggle_stage(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
return #assert(hunks.state(buf)).hunks == 2
|
||||||
|
end, "the first hunk to leave the unstaged set")
|
||||||
|
t.eq(sign_marks(buf), {
|
||||||
|
{ row = 0, sign = "┃", hl = "GitHunkStagedChanged" },
|
||||||
|
{ row = 2, sign = "┃", hl = "GitHunkChanged" },
|
||||||
|
{ row = 4, sign = "┃", hl = "GitHunkChanged" },
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("toggle_stage toggles a staged hunk back to unstaged", function()
|
||||||
|
local _, buf = setup("a\nb\nc\n", "a\nB\nc\n")
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||||
|
hunks.toggle_stage(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
local s = assert(hunks.state(buf))
|
||||||
|
return #s.hunks == 0 and #s.staged == 1
|
||||||
|
end, "the hunk to become staged")
|
||||||
|
hunks.toggle_stage(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
local s = assert(hunks.state(buf))
|
||||||
|
return #s.hunks == 1 and #s.staged == 0
|
||||||
|
end, "the hunk to return to unstaged")
|
||||||
|
t.eq(sign_marks(buf), {
|
||||||
|
{ row = 1, sign = "┃", hl = "GitHunkChanged" },
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("toggle_stage unstages correctly when buffer lines are shifted", function()
|
||||||
|
local dir, buf = setup("a\nb\nc\n", "a\nb\nC\n")
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 3, 0 })
|
||||||
|
hunks.toggle_stage(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
return #assert(hunks.state(buf)).staged == 1
|
||||||
|
end, "the line-3 change to be staged")
|
||||||
|
|
||||||
|
vim.api.nvim_buf_set_lines(buf, 0, 0, false, { "NEW" })
|
||||||
|
vim.api.nvim_exec_autocmds("TextChanged", { buffer = buf })
|
||||||
|
hunks._flush(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
return #assert(hunks.state(buf)).hunks == 1
|
||||||
|
end, "the unstaged add at the top to register")
|
||||||
|
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 4, 0 })
|
||||||
|
hunks.toggle_stage(buf)
|
||||||
|
t.wait_for(function()
|
||||||
|
return #assert(hunks.state(buf)).staged == 0
|
||||||
|
end, "the shifted staged hunk to be unstaged")
|
||||||
|
t.eq(
|
||||||
|
h.git(dir, "show", ":0:a.txt").stdout,
|
||||||
|
"a\nb\nc",
|
||||||
|
"the index reverts to HEAD content for the unstaged hunk"
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("reset_hunk restores the index content for a change", function()
|
||||||
|
local _, buf, state = setup("a\nb\nc\n", "a\nB\nc\n")
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||||
|
hunks.reset_hunk(buf)
|
||||||
|
t.eq(
|
||||||
|
vim.api.nvim_buf_get_lines(buf, 0, -1, false),
|
||||||
|
state.index,
|
||||||
|
"buffer matches the index after reset"
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("reset_hunk re-inserts deleted lines", function()
|
||||||
|
local _, buf = setup("a\nb\nc\n", "a\nc\n")
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||||
|
hunks.reset_hunk(buf)
|
||||||
|
t.eq(vim.api.nvim_buf_get_lines(buf, 0, -1, false), { "a", "b", "c" })
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("reset_hunk removes a pure addition", function()
|
||||||
|
local _, buf = setup("a\nd\n", "a\nb\nc\nd\n")
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||||
|
hunks.reset_hunk(buf)
|
||||||
|
t.eq(vim.api.nvim_buf_get_lines(buf, 0, -1, false), { "a", "d" })
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("git_hunk_signs overrides the sign character per kind", function()
|
||||||
|
local prev = vim.g.git_hunk_signs
|
||||||
|
vim.g.git_hunk_signs = { change = "C" }
|
||||||
|
t.defer(function()
|
||||||
|
vim.g.git_hunk_signs = prev
|
||||||
|
end)
|
||||||
|
local _, buf = setup("a\nb\nc\n", "a\nB\nc\n")
|
||||||
|
t.eq(sign_marks(buf), {
|
||||||
|
{ row = 1, sign = "C", hl = "GitHunkChanged" },
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("git_hunk_signs falls back to the default for unset kinds", function()
|
||||||
|
local prev = vim.g.git_hunk_signs
|
||||||
|
vim.g.git_hunk_signs = { add = "A" }
|
||||||
|
t.defer(function()
|
||||||
|
vim.g.git_hunk_signs = prev
|
||||||
|
end)
|
||||||
|
local _, buf = setup("a\nb\nc\n", "a\nB\nc\n")
|
||||||
|
t.eq(sign_marks(buf), {
|
||||||
|
{ row = 1, sign = "┃", hl = "GitHunkChanged" },
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("preview_hunk shows the hunk body without file headers", function()
|
||||||
|
local _, buf = setup("a\nb\nc\n", "a\nB\nc\n")
|
||||||
|
vim.api.nvim_set_current_buf(buf)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||||
|
hunks.preview_hunk(buf)
|
||||||
|
local float = assert(find_float(), "preview float should open")
|
||||||
|
t.defer(function()
|
||||||
|
pcall(vim.api.nvim_win_close, float, true)
|
||||||
|
end)
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(
|
||||||
|
vim.api.nvim_win_get_buf(float),
|
||||||
|
0,
|
||||||
|
-1,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
t.truthy(
|
||||||
|
vim.startswith(lines[1] or "", "@@ "),
|
||||||
|
"first line is the @@ header"
|
||||||
|
)
|
||||||
|
for _, l in ipairs(lines) do
|
||||||
|
t.falsy(vim.startswith(l, "--- "), "no --- file header line")
|
||||||
|
t.falsy(vim.startswith(l, "+++ "), "no +++ file header line")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("preview_hunk re-invocation focuses the open float", function()
|
||||||
|
local _, buf = setup("a\nb\nc\n", "a\nB\nc\n")
|
||||||
|
vim.api.nvim_set_current_buf(buf)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||||
|
hunks.preview_hunk(buf)
|
||||||
|
local float = assert(find_float(), "preview float should open")
|
||||||
|
t.defer(function()
|
||||||
|
pcall(vim.api.nvim_win_close, float, true)
|
||||||
|
end)
|
||||||
|
t.truthy(
|
||||||
|
vim.api.nvim_get_current_win() ~= float,
|
||||||
|
"the float opens unfocused"
|
||||||
|
)
|
||||||
|
hunks.preview_hunk(buf)
|
||||||
|
t.eq(
|
||||||
|
vim.api.nvim_get_current_win(),
|
||||||
|
float,
|
||||||
|
"re-invoking focuses the existing float"
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("nav jumps to next and previous hunks with wrap", function()
|
||||||
|
local _, buf = setup("a\nb\nc\nd\ne\n", "A\nb\nc\nd\nE\n")
|
||||||
|
vim.api.nvim_set_current_buf(buf)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||||
|
hunks.nav("next")
|
||||||
|
t.eq(vim.api.nvim_win_get_cursor(0)[1], 5, "next hunk is line 5")
|
||||||
|
hunks.nav("next")
|
||||||
|
t.eq(vim.api.nvim_win_get_cursor(0)[1], 1, "next wraps back to line 1")
|
||||||
|
hunks.nav("prev")
|
||||||
|
t.eq(vim.api.nvim_win_get_cursor(0)[1], 5, "prev wraps back to line 5")
|
||||||
|
end)
|
||||||
@@ -51,7 +51,6 @@ end)
|
|||||||
t.test("parse_uri returns nil for non-git URIs", function()
|
t.test("parse_uri returns nil for non-git URIs", function()
|
||||||
t.falsy(object.parse_uri("file:///tmp/x"))
|
t.falsy(object.parse_uri("file:///tmp/x"))
|
||||||
t.falsy(object.parse_uri("/tmp/x"))
|
t.falsy(object.parse_uri("/tmp/x"))
|
||||||
t.falsy(object.parse_uri("gitlog:///tmp/x"))
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
t.test("M.open(HEAD) names buffer with full sha", function()
|
t.test("M.open(HEAD) names buffer with full sha", function()
|
||||||
@@ -87,6 +86,23 @@ t.test("M.open(HEAD:<path>) loads file content at HEAD", function()
|
|||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
t.test("M.open on a merge commit diffs against the first parent only", function()
|
||||||
|
local dir = h.make_repo({ ["a.txt"] = "one\n" })
|
||||||
|
t.write(dir, "a.txt", "two\n")
|
||||||
|
h.git(dir, "stash")
|
||||||
|
local stash = h.git(dir, "rev-parse", "stash@{0}").stdout
|
||||||
|
local r = assert(require("git.core.repo").resolve(dir))
|
||||||
|
|
||||||
|
object.open(r, stash, { split = false })
|
||||||
|
local count = 0
|
||||||
|
for _, l in ipairs(vim.api.nvim_buf_get_lines(0, 0, -1, false)) do
|
||||||
|
if l:match("^diff %-%-git ") then
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
t.eq(count, 1, "the stashed file's diff appears once, not per-parent")
|
||||||
|
end)
|
||||||
|
|
||||||
t.test("M.open errors on a bogus base, no buffer is opened", function()
|
t.test("M.open errors on a bogus base, no buffer is opened", function()
|
||||||
local dir = h.make_repo({ a = "first\n" })
|
local dir = h.make_repo({ a = "first\n" })
|
||||||
local r = assert(require("git.core.repo").resolve(dir))
|
local r = assert(require("git.core.repo").resolve(dir))
|
||||||
|
|||||||
@@ -71,6 +71,51 @@ t.test("get_cached memoizes by key", function()
|
|||||||
t.truthy(v1 == v2, "second call should return cached table")
|
t.truthy(v1 == v2, "second call should return cached table")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
t.test("index_sha returns the blob sha and caches it", function()
|
||||||
|
local dir = h.make_repo({ a = "x\n" })
|
||||||
|
local r = assert(require("git.core.repo").resolve(dir))
|
||||||
|
local sha = r:index_sha("a")
|
||||||
|
t.truthy(sha and #sha > 0, "index_sha returns the stage-0 blob sha")
|
||||||
|
t.truthy(r._cache["index:a"] ~= nil, "the result is cached")
|
||||||
|
t.eq(r:index_sha("a"), sha, "a cached call returns the same sha")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("index_sha caches a negative result for an untracked path", function()
|
||||||
|
local dir = h.make_repo({ a = "x\n" })
|
||||||
|
local r = assert(require("git.core.repo").resolve(dir))
|
||||||
|
t.eq(r:index_sha("nope"), nil, "an untracked path has no index sha")
|
||||||
|
t.eq(r._cache["index:nope"], false, "the negative result is cached")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("index_sha cache clears when the index is written", function()
|
||||||
|
local dir = h.make_repo({ a = "x\n" })
|
||||||
|
local r = assert(require("git.core.repo").resolve(dir))
|
||||||
|
r:index_sha("a")
|
||||||
|
t.truthy(r._cache["index:a"] ~= nil, "sha is cached before the stage")
|
||||||
|
t.write(dir, "a", "y\n")
|
||||||
|
h.git(dir, "add", "a")
|
||||||
|
wait_cleared(r, "index:a", 2000)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("head_sha returns the blob sha and caches it", function()
|
||||||
|
local dir = h.make_repo({ a = "x\n" })
|
||||||
|
local r = assert(require("git.core.repo").resolve(dir))
|
||||||
|
local sha = r:head_sha("a")
|
||||||
|
t.truthy(sha and #sha > 0, "head_sha returns the HEAD blob sha")
|
||||||
|
t.truthy(r._cache["head_blob:a"] ~= nil, "the result is cached")
|
||||||
|
t.eq(r:head_sha("a"), sha, "a cached call returns the same sha")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("head_sha cache clears when HEAD moves", function()
|
||||||
|
local dir = h.make_repo({ a = "x\n" })
|
||||||
|
local r = assert(require("git.core.repo").resolve(dir))
|
||||||
|
r:head_sha("a")
|
||||||
|
t.truthy(r._cache["head_blob:a"] ~= nil, "sha is cached before the commit")
|
||||||
|
t.write(dir, "a", "y\n")
|
||||||
|
h.git(dir, "commit", "-aqm", "change")
|
||||||
|
wait_cleared(r, "head_blob:a", 2000)
|
||||||
|
end)
|
||||||
|
|
||||||
t.test("cache clears after top-level .git change (commit)", function()
|
t.test("cache clears after top-level .git change (commit)", function()
|
||||||
local dir = h.make_repo({ a = "x" })
|
local dir = h.make_repo({ a = "x" })
|
||||||
local r = assert(require("git.core.repo").resolve(dir))
|
local r = assert(require("git.core.repo").resolve(dir))
|
||||||
@@ -134,6 +179,176 @@ t.test("resolve_sha caches by prefix", function()
|
|||||||
t.truthy(r._cache["resolve:" .. short], "result should be cached")
|
t.truthy(r._cache["resolve:" .. short], "result should be cached")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
---@param r ow.Git.Repo
|
||||||
|
local function wait_initial(r)
|
||||||
|
t.wait_for(function()
|
||||||
|
return r.status.branch.head ~= nil
|
||||||
|
end, "initial fetch to complete", 2000)
|
||||||
|
end
|
||||||
|
|
||||||
|
t.test("_invalidate clears only matching keys for HEAD", function()
|
||||||
|
local dir = h.make_repo({ a = "x" })
|
||||||
|
local r = assert(require("git.core.repo").resolve(dir))
|
||||||
|
r._cache.head = "h"
|
||||||
|
r._cache.refs = { "main" }
|
||||||
|
r._cache.pseudo_refs = { "HEAD" }
|
||||||
|
r._cache.stash_refs = {}
|
||||||
|
r._cache["resolve:abc"] = { "deadbeef", "ok" }
|
||||||
|
r:_invalidate("HEAD")
|
||||||
|
t.eq(r._cache.head, nil)
|
||||||
|
t.eq(r._cache.pseudo_refs, nil)
|
||||||
|
t.eq(r._cache["resolve:abc"], nil)
|
||||||
|
t.truthy(r._cache.refs)
|
||||||
|
t.truthy(r._cache.stash_refs)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("_invalidate clears refs/head/resolve for refs/heads/*", function()
|
||||||
|
local dir = h.make_repo({ a = "x" })
|
||||||
|
local r = assert(require("git.core.repo").resolve(dir))
|
||||||
|
r._cache.head = "h"
|
||||||
|
r._cache.refs = { "main" }
|
||||||
|
r._cache.pseudo_refs = { "HEAD" }
|
||||||
|
r._cache.stash_refs = {}
|
||||||
|
r._cache["resolve:abc"] = { "deadbeef", "ok" }
|
||||||
|
r:_invalidate("refs/heads/feature")
|
||||||
|
t.eq(r._cache.head, nil)
|
||||||
|
t.eq(r._cache.refs, nil)
|
||||||
|
t.eq(r._cache["resolve:abc"], nil)
|
||||||
|
t.truthy(r._cache.pseudo_refs)
|
||||||
|
t.truthy(r._cache.stash_refs)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("_invalidate clears config on .git/config change", function()
|
||||||
|
local dir = h.make_repo({ a = "x" })
|
||||||
|
local r = assert(require("git.core.repo").resolve(dir))
|
||||||
|
r._cache.config = { core = {} }
|
||||||
|
r:_invalidate("config")
|
||||||
|
t.eq(r._cache.config, nil)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("status_entry_for: exact match on case-sensitive repo", function()
|
||||||
|
local dir = h.make_repo({ Foo = "x" })
|
||||||
|
t.write(dir, "Foo", "modified")
|
||||||
|
local r = assert(require("git.core.repo").resolve(dir))
|
||||||
|
wait_initial(r)
|
||||||
|
t.truthy(r:status_entry_for("Foo"))
|
||||||
|
t.eq(r:status_entry_for("foo"), nil, "case mismatch returns nil")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("status_entry_for: case-insensitive fallback when core.ignorecase=true", function()
|
||||||
|
local dir = h.make_repo({ Foo = "x" })
|
||||||
|
h.git(dir, "config", "core.ignorecase", "true")
|
||||||
|
t.write(dir, "Foo", "modified")
|
||||||
|
local r = assert(require("git.core.repo").resolve(dir))
|
||||||
|
wait_initial(r)
|
||||||
|
t.truthy(r:status_entry_for("Foo"), "exact match")
|
||||||
|
t.truthy(r:status_entry_for("foo"), "lowercase finds Foo")
|
||||||
|
t.truthy(r:status_entry_for("FOO"), "uppercase finds Foo")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("_invalidate matches stash_refs on refs/stash and logs/refs/stash", function()
|
||||||
|
local dir = h.make_repo({ a = "x" })
|
||||||
|
local r = assert(require("git.core.repo").resolve(dir))
|
||||||
|
r._cache.stash_refs = {}
|
||||||
|
r:_invalidate("refs/stash")
|
||||||
|
t.eq(r._cache.stash_refs, nil)
|
||||||
|
r._cache.stash_refs = {}
|
||||||
|
r:_invalidate("logs/refs/stash")
|
||||||
|
t.eq(r._cache.stash_refs, nil)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("refresh with invalidate=true wipes cache on next fetch", function()
|
||||||
|
local dir = h.make_repo({ a = "x" })
|
||||||
|
local r = assert(require("git.core.repo").resolve(dir))
|
||||||
|
wait_initial(r)
|
||||||
|
r._cache.head = "stale"
|
||||||
|
r._cache["resolve:abc"] = { "x", "ok" }
|
||||||
|
r:refresh({ invalidate = true })
|
||||||
|
t.wait_for(function()
|
||||||
|
return r._cache.head == nil
|
||||||
|
end, "cache wiped after invalidating refresh completes", 2000)
|
||||||
|
t.eq(r._cache.head, nil)
|
||||||
|
t.eq(r._cache["resolve:abc"], nil)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("refresh emits change.paths listing structurally-changed paths", function()
|
||||||
|
local dir = h.make_repo({ a = "1", b = "1" })
|
||||||
|
local r = assert(require("git.core.repo").resolve(dir))
|
||||||
|
wait_initial(r)
|
||||||
|
t.write(dir, "a", "2")
|
||||||
|
---@type ow.Git.Repo.Change?
|
||||||
|
local change_seen
|
||||||
|
local unsub = r:on("change", function(change)
|
||||||
|
change_seen = change
|
||||||
|
end)
|
||||||
|
r:refresh()
|
||||||
|
t.wait_for(function()
|
||||||
|
return change_seen ~= nil
|
||||||
|
end, "refresh emit", 2000)
|
||||||
|
unsub()
|
||||||
|
local change = assert(change_seen)
|
||||||
|
t.truthy(change.paths["a"])
|
||||||
|
t.falsy(change.paths["b"], "b is unchanged structurally")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("submodule: parent enumerates initialized submodules by default", function()
|
||||||
|
local outer_path = h.make_submodule_repo()
|
||||||
|
local outer = assert(require("git.core.repo").resolve(outer_path))
|
||||||
|
t.truthy(outer._submodules["sub"], "sub recorded as submodule")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("submodule: eagerly creates child Repos and subscribes by default", function()
|
||||||
|
local outer_path = h.make_submodule_repo()
|
||||||
|
local outer = assert(require("git.core.repo").resolve(outer_path))
|
||||||
|
wait_initial(outer)
|
||||||
|
local inner = require("git.core.repo").all()[outer_path .. "/sub"]
|
||||||
|
t.truthy(inner, "inner Repo eagerly created")
|
||||||
|
t.truthy(outer._submodules["sub"] and outer._submodules["sub"].unsub, "inner subscribed by outer")
|
||||||
|
|
||||||
|
t.write(outer_path .. "/sub", "a", "modified\n")
|
||||||
|
---@type ow.Git.Repo.Change?
|
||||||
|
local outer_change
|
||||||
|
local unsub = outer:on("change", function(change)
|
||||||
|
outer_change = change
|
||||||
|
end)
|
||||||
|
inner:refresh()
|
||||||
|
t.wait_for(function()
|
||||||
|
return outer_change ~= nil
|
||||||
|
end, "outer notified by inner refresh", 2000)
|
||||||
|
unsub()
|
||||||
|
|
||||||
|
local entry = outer.status.entries["sub"]
|
||||||
|
t.truthy(entry, "outer sub entry now present")
|
||||||
|
t.eq(entry.kind, "changed")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("submodule: no eager creation when flag is explicitly disabled", function()
|
||||||
|
vim.g.git_submodule_recursion = false
|
||||||
|
t.defer(function()
|
||||||
|
vim.g.git_submodule_recursion = nil
|
||||||
|
end)
|
||||||
|
local outer_path = h.make_submodule_repo()
|
||||||
|
local outer = assert(require("git.core.repo").resolve(outer_path))
|
||||||
|
wait_initial(outer)
|
||||||
|
t.eq(
|
||||||
|
require("git.core.repo").all()[outer_path .. "/sub"],
|
||||||
|
nil,
|
||||||
|
"inner Repo not created when flag is false"
|
||||||
|
)
|
||||||
|
t.eq(next(outer._submodules), nil)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("submodule: outer created after inner picks up existing child", function()
|
||||||
|
local outer_path = h.make_submodule_repo()
|
||||||
|
local inner = assert(
|
||||||
|
require("git.core.repo").resolve(outer_path .. "/sub")
|
||||||
|
)
|
||||||
|
wait_initial(inner)
|
||||||
|
local outer = assert(require("git.core.repo").resolve(outer_path))
|
||||||
|
wait_initial(outer)
|
||||||
|
t.truthy(outer._submodules["sub"] and outer._submodules["sub"].unsub, "outer subscribed to pre-existing inner")
|
||||||
|
end)
|
||||||
|
|
||||||
t.test("watcher cleans up after a slash-branch dir is removed", function()
|
t.test("watcher cleans up after a slash-branch dir is removed", function()
|
||||||
local dir = h.make_repo({ a = "x" })
|
local dir = h.make_repo({ a = "x" })
|
||||||
local r = assert(require("git.core.repo").resolve(dir))
|
local r = assert(require("git.core.repo").resolve(dir))
|
||||||
|
|||||||
@@ -325,3 +325,63 @@ t.test("Status:aggregate_at with prefix '.' includes everything", function()
|
|||||||
}))
|
}))
|
||||||
t.eq(#s:aggregate_at("."), 2)
|
t.eq(#s:aggregate_at("."), 2)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
t.test("entry_equal: identical changed entries", function()
|
||||||
|
local a = { kind = "changed", path = "x", staged = "modified" }
|
||||||
|
local b = { kind = "changed", path = "x", staged = "modified" }
|
||||||
|
t.truthy(status.entry_equal(a, b))
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("entry_equal: differing staged side returns false", function()
|
||||||
|
local a = { kind = "changed", path = "x", staged = "modified" }
|
||||||
|
local b = { kind = "changed", path = "x", staged = "added" }
|
||||||
|
t.falsy(status.entry_equal(a, b))
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("entry_equal: differing orig returns false", function()
|
||||||
|
local a = { kind = "changed", path = "x", staged = "renamed", orig = "y" }
|
||||||
|
local b = { kind = "changed", path = "x", staged = "renamed", orig = "z" }
|
||||||
|
t.falsy(status.entry_equal(a, b))
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("entry_equal: nil vs nil is true", function()
|
||||||
|
t.truthy(status.entry_equal(nil, nil))
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("entry_equal: nil vs entry is false", function()
|
||||||
|
t.falsy(status.entry_equal(nil, { kind = "untracked", path = "x" }))
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("entry_equal: different kinds returns false", function()
|
||||||
|
local a = { kind = "untracked", path = "x" }
|
||||||
|
local b = { kind = "ignored", path = "x" }
|
||||||
|
t.falsy(status.entry_equal(a, b))
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("entry_equal: differing unmerged conflict returns false", function()
|
||||||
|
local a = { kind = "unmerged", path = "x", conflict = "both_added" }
|
||||||
|
local b = { kind = "unmerged", path = "x", conflict = "both_modified" }
|
||||||
|
t.falsy(status.entry_equal(a, b))
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("diff_entries: detects additions, removals, and modifications", function()
|
||||||
|
local prior = {
|
||||||
|
a = { kind = "changed", path = "a", staged = "modified" },
|
||||||
|
b = { kind = "untracked", path = "b" },
|
||||||
|
}
|
||||||
|
local next_ = {
|
||||||
|
a = { kind = "changed", path = "a", staged = "added" },
|
||||||
|
c = { kind = "untracked", path = "c" },
|
||||||
|
}
|
||||||
|
local changed = status.diff_entries(prior, next_)
|
||||||
|
t.truthy(changed.a, "a modified")
|
||||||
|
t.truthy(changed.b, "b removed")
|
||||||
|
t.truthy(changed.c, "c added")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("diff_entries: empty when entries match", function()
|
||||||
|
local prior = { a = { kind = "untracked", path = "a" } }
|
||||||
|
local next_ = { a = { kind = "untracked", path = "a" } }
|
||||||
|
t.eq(status.diff_entries(prior, next_), {})
|
||||||
|
end)
|
||||||
|
|
||||||
|
|||||||
@@ -39,16 +39,28 @@ local function find_sidebar()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---Find a diff-role window in the given tabpage (or current).
|
---Find a diff window in the given tabpage (or current). "left" / "right"
|
||||||
|
---is determined by column position: the layout is [sidebar | left | right],
|
||||||
|
---so the leftmost &diff window is the left pane and the rightmost is the
|
||||||
|
---right pane.
|
||||||
---@param role "left"|"right"
|
---@param role "left"|"right"
|
||||||
---@param tab integer?
|
---@param tab integer?
|
||||||
---@return integer?
|
---@return integer?
|
||||||
local function find_diff_win(role, tab)
|
local function find_diff_win(role, tab)
|
||||||
|
local diffs = {}
|
||||||
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(tab or 0)) do
|
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(tab or 0)) do
|
||||||
if vim.w[w].git_diff_role == role then
|
if vim.wo[w].diff then
|
||||||
return w
|
table.insert(diffs, w)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
table.sort(diffs, function(a, b)
|
||||||
|
return vim.api.nvim_win_get_position(a)[2]
|
||||||
|
< vim.api.nvim_win_get_position(b)[2]
|
||||||
|
end)
|
||||||
|
if role == "left" then
|
||||||
|
return diffs[1]
|
||||||
|
end
|
||||||
|
return diffs[#diffs]
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param file_path string
|
---@param file_path string
|
||||||
@@ -161,7 +173,6 @@ t.test(
|
|||||||
setup_sidebar_with_unstaged_file("foo.txt", "v1\n", "v2\n")
|
setup_sidebar_with_unstaged_file("foo.txt", "v1\n", "v2\n")
|
||||||
local tab1 = vim.api.nvim_get_current_tabpage()
|
local tab1 = vim.api.nvim_get_current_tabpage()
|
||||||
|
|
||||||
-- First show diff in tab1, so state.diff_*_win point at tab1.
|
|
||||||
vim.api.nvim_set_current_win(sidebar_win)
|
vim.api.nvim_set_current_win(sidebar_win)
|
||||||
vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 })
|
vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 })
|
||||||
t.press("<Tab>")
|
t.press("<Tab>")
|
||||||
@@ -223,3 +234,254 @@ t.test("refresh on stage updates the index URI buffer's content", function()
|
|||||||
"index pane should reflect staged content after refresh"
|
"index pane should reflect staged content after refresh"
|
||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
t.test(
|
||||||
|
"re-selecting same entry after close + diffsplit keeps fold state in sync",
|
||||||
|
function()
|
||||||
|
local committed, worktree = {}, {}
|
||||||
|
for i = 1, 30 do
|
||||||
|
committed[i] = "line " .. i
|
||||||
|
worktree[i] = i == 15 and "CHANGED" or ("line " .. i)
|
||||||
|
end
|
||||||
|
local sidebar_win, line = setup_sidebar_with_unstaged_file(
|
||||||
|
"foo.txt",
|
||||||
|
table.concat(committed, "\n") .. "\n",
|
||||||
|
table.concat(worktree, "\n") .. "\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
local prev_foldlevel = vim.o.foldlevel
|
||||||
|
vim.o.foldlevel = 99
|
||||||
|
t.defer(function()
|
||||||
|
vim.o.foldlevel = prev_foldlevel
|
||||||
|
end)
|
||||||
|
|
||||||
|
vim.api.nvim_set_current_win(sidebar_win)
|
||||||
|
vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 })
|
||||||
|
t.press("<Tab>")
|
||||||
|
t.wait_for(function()
|
||||||
|
return find_diff_win("left") ~= nil
|
||||||
|
and find_diff_win("right") ~= nil
|
||||||
|
end, "first diff pair to appear")
|
||||||
|
|
||||||
|
local first_left = assert(find_diff_win("left"))
|
||||||
|
vim.api.nvim_win_close(first_left, false)
|
||||||
|
|
||||||
|
local remaining
|
||||||
|
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
||||||
|
if w ~= sidebar_win then
|
||||||
|
remaining = w
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not remaining then
|
||||||
|
error("a non-sidebar window should remain after close")
|
||||||
|
end
|
||||||
|
vim.api.nvim_set_current_win(remaining)
|
||||||
|
require("git.diffsplit").open({ mods = { vertical = true } })
|
||||||
|
t.wait_for(function()
|
||||||
|
local count = 0
|
||||||
|
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
||||||
|
if vim.wo[w].diff then
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return count == 2
|
||||||
|
end, "diffsplit to produce a diff pair")
|
||||||
|
|
||||||
|
vim.api.nvim_set_current_win(sidebar_win)
|
||||||
|
vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 })
|
||||||
|
t.press("<Tab>")
|
||||||
|
t.wait_for(function()
|
||||||
|
local count = 0
|
||||||
|
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
||||||
|
if vim.wo[w].diff then
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return count == 2
|
||||||
|
end, "diff pair after re-selecting entry")
|
||||||
|
|
||||||
|
local left_win = assert(find_diff_win("left"))
|
||||||
|
local right_win = assert(find_diff_win("right"))
|
||||||
|
t.eq(
|
||||||
|
vim.wo[left_win].foldlevel,
|
||||||
|
0,
|
||||||
|
"left pane foldlevel should be 0 after re-select"
|
||||||
|
)
|
||||||
|
t.eq(
|
||||||
|
vim.wo[right_win].foldlevel,
|
||||||
|
0,
|
||||||
|
"right pane foldlevel should be 0 after re-select"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
t.test("sidebar buffer is named <worktree>/GitStatus", function()
|
||||||
|
local repo = h.make_repo({ ["foo.txt"] = "x\n" })
|
||||||
|
vim.cmd("cd " .. repo)
|
||||||
|
require("git.status_view").open({ placement = "sidebar" })
|
||||||
|
local r = assert(require("git.core.repo").find(vim.fn.getcwd()))
|
||||||
|
local buf = find_sidebar()
|
||||||
|
assert(buf, "sidebar buffer should exist")
|
||||||
|
t.eq(
|
||||||
|
vim.api.nvim_buf_get_name(buf),
|
||||||
|
r.worktree .. "/GitStatus",
|
||||||
|
"buffer name should be <worktree>/GitStatus"
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test(
|
||||||
|
"calling open twice without closing focuses the existing sidebar",
|
||||||
|
function()
|
||||||
|
local repo = h.make_repo({ ["foo.txt"] = "x\n" })
|
||||||
|
vim.cmd("cd " .. repo)
|
||||||
|
require("git.status_view").open({ placement = "sidebar" })
|
||||||
|
local first = find_sidebar()
|
||||||
|
assert(first, "first sidebar buffer should exist")
|
||||||
|
|
||||||
|
require("git.status_view").open({ placement = "sidebar" })
|
||||||
|
local second = find_sidebar()
|
||||||
|
assert(second, "second sidebar buffer should exist")
|
||||||
|
t.eq(
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
"consecutive opens should reuse the visible sidebar"
|
||||||
|
)
|
||||||
|
local count = 0
|
||||||
|
for _, b in ipairs(vim.api.nvim_list_bufs()) do
|
||||||
|
if vim.bo[b].filetype == "gitstatus" then
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
t.eq(count, 1, "only one gitstatus buffer should exist")
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
t.test("opening for different worktrees creates separate buffers", function()
|
||||||
|
local repo_a = h.make_repo({ ["a.txt"] = "x\n" })
|
||||||
|
local repo_b = h.make_repo({ ["b.txt"] = "y\n" })
|
||||||
|
|
||||||
|
vim.cmd("cd " .. repo_a)
|
||||||
|
require("git.status_view").open({ placement = "sidebar" })
|
||||||
|
local buf_a = find_sidebar()
|
||||||
|
require("git.status_view").toggle()
|
||||||
|
|
||||||
|
vim.cmd("cd " .. repo_b)
|
||||||
|
require("git.status_view").open({ placement = "sidebar" })
|
||||||
|
local buf_b = find_sidebar()
|
||||||
|
|
||||||
|
assert(buf_a and buf_b)
|
||||||
|
t.truthy(
|
||||||
|
buf_a ~= buf_b,
|
||||||
|
"different worktrees should produce different buffers"
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("sidebar buffer is buftype=nofile and not buflisted", function()
|
||||||
|
local repo = h.make_repo({ ["foo.txt"] = "x\n" })
|
||||||
|
vim.cmd("cd " .. repo)
|
||||||
|
require("git.status_view").open({ placement = "sidebar" })
|
||||||
|
local buf = find_sidebar()
|
||||||
|
assert(buf, "sidebar buffer should exist")
|
||||||
|
t.eq(vim.bo[buf].buftype, "nofile", "buftype should be nofile")
|
||||||
|
t.eq(vim.bo[buf].buflisted, false, "buflisted should be false")
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test("sidebar buffer name does not get written to disk", function()
|
||||||
|
local repo = h.make_repo({ ["foo.txt"] = "x\n" })
|
||||||
|
vim.cmd("cd " .. repo)
|
||||||
|
require("git.status_view").open({ placement = "sidebar" })
|
||||||
|
local buf = find_sidebar()
|
||||||
|
assert(buf, "sidebar buffer should exist")
|
||||||
|
local name = vim.api.nvim_buf_get_name(buf)
|
||||||
|
vim.api.nvim_buf_call(buf, function()
|
||||||
|
pcall(function()
|
||||||
|
vim.cmd("silent! write")
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
t.eq(
|
||||||
|
vim.uv.fs_stat(name),
|
||||||
|
nil,
|
||||||
|
"no real file should be created at the sidebar buffer's path"
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
t.test(
|
||||||
|
"diffsplit from sidebar resets cursor so panes stay in sync",
|
||||||
|
function()
|
||||||
|
local committed, worktree = {}, {}
|
||||||
|
for i = 1, 100 do
|
||||||
|
committed[i] = "line " .. i
|
||||||
|
worktree[i] = i == 10
|
||||||
|
and "CHANGED " .. i
|
||||||
|
or i == 40 and "CHANGED " .. i
|
||||||
|
or i == 70 and "CHANGED " .. i
|
||||||
|
or i == 90 and "CHANGED " .. i
|
||||||
|
or ("line " .. i)
|
||||||
|
end
|
||||||
|
local repo = h.make_repo({
|
||||||
|
["file.txt"] = table.concat(committed, "\n") .. "\n",
|
||||||
|
})
|
||||||
|
t.write(repo, "file.txt", table.concat(worktree, "\n") .. "\n")
|
||||||
|
vim.cmd("cd " .. repo)
|
||||||
|
|
||||||
|
-- Open the worktree file in a normal window and position cursor in
|
||||||
|
-- what becomes a folded section after diff is set up.
|
||||||
|
vim.cmd("edit file.txt")
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 50, 0 })
|
||||||
|
|
||||||
|
require("git.status_view").open({ placement = "sidebar" })
|
||||||
|
local sidebar_buf, sidebar_win = find_sidebar()
|
||||||
|
assert(sidebar_buf and sidebar_win)
|
||||||
|
local r = assert(require("git.core.repo").find(vim.fn.getcwd()))
|
||||||
|
r:refresh()
|
||||||
|
t.wait_for(function()
|
||||||
|
return r.status and #r.status:rows("unstaged") > 0
|
||||||
|
end, "git status to report unstaged changes")
|
||||||
|
|
||||||
|
local entry_line
|
||||||
|
for i, l in
|
||||||
|
ipairs(vim.api.nvim_buf_get_lines(sidebar_buf, 0, -1, false))
|
||||||
|
do
|
||||||
|
if l:match("file.txt$") then
|
||||||
|
entry_line = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not entry_line then
|
||||||
|
error("entry line should exist")
|
||||||
|
end
|
||||||
|
|
||||||
|
vim.api.nvim_set_current_win(sidebar_win)
|
||||||
|
vim.api.nvim_win_set_cursor(sidebar_win, { entry_line, 0 })
|
||||||
|
t.press("<Tab>")
|
||||||
|
t.wait_for(function()
|
||||||
|
return find_diff_win("left") ~= nil
|
||||||
|
and find_diff_win("right") ~= nil
|
||||||
|
end, "diff pair to appear")
|
||||||
|
|
||||||
|
local left_win = assert(find_diff_win("left"))
|
||||||
|
local right_win = assert(find_diff_win("right"))
|
||||||
|
local left_top =
|
||||||
|
vim.api.nvim_win_call(left_win, function() return vim.fn.line("w0") end)
|
||||||
|
local right_top = vim.api.nvim_win_call(
|
||||||
|
right_win,
|
||||||
|
function() return vim.fn.line("w0") end
|
||||||
|
)
|
||||||
|
t.eq(
|
||||||
|
left_top,
|
||||||
|
right_top,
|
||||||
|
"left and right panes should have the same topline after diffsplit"
|
||||||
|
)
|
||||||
|
t.eq(
|
||||||
|
vim.api.nvim_win_get_cursor(left_win),
|
||||||
|
{ 1, 0 },
|
||||||
|
"left pane should start at line 1"
|
||||||
|
)
|
||||||
|
t.eq(
|
||||||
|
vim.api.nvim_win_get_cursor(right_win),
|
||||||
|
{ 1, 0 },
|
||||||
|
"right pane should start at line 1"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user