refactor(git): unify around the Repo abstraction
This commit is contained in:
@@ -1,17 +0,0 @@
|
||||
-- The built-in `git` filetype is reused for our `:G show` / `:G cat-file -p`
|
||||
-- output buffers and for commits opened from the log window. We set
|
||||
-- `vim.b.git_worktree` before assigning the filetype on those buffers; the
|
||||
-- guard below keeps the <CR> dispatcher off any unrelated git buffer (a
|
||||
-- real `.git/HEAD` file, etc.) so the default normal-mode <CR> still works.
|
||||
if not vim.b.git_worktree then
|
||||
return
|
||||
end
|
||||
|
||||
local cr = vim.api.nvim_replace_termcodes("<CR>", true, false, true)
|
||||
|
||||
vim.keymap.set("n", "<CR>", function()
|
||||
if not require("git.object").open_under_cursor() then
|
||||
-- "n" mode = no remap, so this doesn't recurse into our mapping.
|
||||
vim.api.nvim_feedkeys(cr, "n", false)
|
||||
end
|
||||
end, { buffer = 0, silent = true, desc = "Open file at commit" })
|
||||
@@ -1,17 +0,0 @@
|
||||
local cr = vim.api.nvim_replace_termcodes("<CR>", true, false, true)
|
||||
|
||||
vim.keymap.set("n", "<CR>", function()
|
||||
local worktree = vim.b.git_worktree
|
||||
-- Anchor past the leading graph chars (matches the leading sha column,
|
||||
-- not any hex word that happens to appear later in the subject).
|
||||
local sha = worktree
|
||||
and vim.api
|
||||
.nvim_get_current_line()
|
||||
:match("^[*|/\\_ ]*(%x%x%x%x%x%x%x+)")
|
||||
if sha then
|
||||
require("git.object").open_object(worktree, sha, { split = false })
|
||||
else
|
||||
-- "n" mode = no remap, so this doesn't recurse into our mapping.
|
||||
vim.api.nvim_feedkeys(cr, "n", false)
|
||||
end
|
||||
end, { buffer = 0, silent = true, desc = "Open commit" })
|
||||
+122
-23
@@ -23,8 +23,10 @@ local function git_cmds()
|
||||
if cached_cmds then
|
||||
return cached_cmds
|
||||
end
|
||||
local result = vim
|
||||
.system({ "git", "--list-cmds=main,others,alias" }, { text = true })
|
||||
local result = vim.system(
|
||||
{ "git", "--list-cmds=main,others,alias" },
|
||||
{ text = true }
|
||||
)
|
||||
:wait()
|
||||
if result.code ~= 0 then
|
||||
util.error("git --list-cmds failed: %s", vim.trim(result.stderr or ""))
|
||||
@@ -70,30 +72,31 @@ local function place_split(name)
|
||||
return buf
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param r ow.Git.Repo
|
||||
---@param args string[]
|
||||
---@param conf ow.Git.SplitHandler
|
||||
local function run_in_split(worktree, args, conf)
|
||||
local function run_in_split(r, args, conf)
|
||||
local cmd = { "git" }
|
||||
vim.list_extend(cmd, args)
|
||||
util.exec(cmd, {
|
||||
cwd = worktree,
|
||||
cwd = r.worktree,
|
||||
on_done = function(stdout)
|
||||
if not stdout then
|
||||
return
|
||||
end
|
||||
local name = "[git " .. table.concat(args, " ") .. "]"
|
||||
local buf = place_split(name)
|
||||
vim.b[buf].git_worktree = worktree
|
||||
vim.b[buf].git_sha = nil
|
||||
vim.b[buf].git_parent_sha = nil
|
||||
repo.attach(buf, r)
|
||||
object.attach_dispatch(buf)
|
||||
local state = r:state(buf) --[[@as -nil]]
|
||||
state.sha = nil
|
||||
state.parent_sha = nil
|
||||
if conf.needs_rev then
|
||||
local user_rev = first_positional(args, 2) or "HEAD"
|
||||
local sha = repo.rev_parse(worktree, user_rev, true)
|
||||
local sha = r:rev_parse(user_rev, true)
|
||||
if sha then
|
||||
vim.b[buf].git_sha = sha
|
||||
vim.b[buf].git_parent_sha =
|
||||
repo.rev_parse(worktree, user_rev .. "^", true)
|
||||
state.sha = sha
|
||||
state.parent_sha = r:rev_parse(user_rev .. "^", true)
|
||||
end
|
||||
end
|
||||
vim.bo[buf].filetype = conf.ft
|
||||
@@ -111,14 +114,14 @@ local function run_in_split(worktree, args, conf)
|
||||
})
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param r ow.Git.Repo
|
||||
---@param args string[]
|
||||
local function run_to_messages(worktree, args)
|
||||
local function run_to_messages(r, args)
|
||||
local cmd = { "git" }
|
||||
vim.list_extend(cmd, args)
|
||||
vim.system(
|
||||
cmd,
|
||||
{ cwd = worktree, text = true },
|
||||
{ cwd = r.worktree, text = true },
|
||||
vim.schedule_wrap(function(obj)
|
||||
local out = vim.trim(obj.stdout or "")
|
||||
local err = vim.trim(obj.stderr or "")
|
||||
@@ -175,8 +178,8 @@ end
|
||||
|
||||
---@param args string[]
|
||||
function M.run(args)
|
||||
local _, worktree = repo.current_repo()
|
||||
if not worktree then
|
||||
local r = repo.find()
|
||||
if not r then
|
||||
util.warning("not in a git repository")
|
||||
return
|
||||
end
|
||||
@@ -190,10 +193,10 @@ function M.run(args)
|
||||
if sub == "show" then
|
||||
local arg = first_positional(args, 2)
|
||||
if arg and arg:find(":", 1, true) then
|
||||
object.open_object(worktree, arg)
|
||||
object.open_object(r, arg)
|
||||
return
|
||||
end
|
||||
run_in_split(worktree, args, { ft = "git", needs_rev = true })
|
||||
run_in_split(r, args, { ft = "git", needs_rev = true })
|
||||
return
|
||||
end
|
||||
|
||||
@@ -201,22 +204,118 @@ function M.run(args)
|
||||
if vim.list_contains(args, "-p") then
|
||||
local rev = first_positional(args, 2)
|
||||
if rev then
|
||||
object.open_object(worktree, rev)
|
||||
object.open_object(r, rev)
|
||||
return
|
||||
end
|
||||
end
|
||||
run_in_split(worktree, args, { ft = "git", needs_rev = true })
|
||||
run_in_split(r, args, { ft = "git", needs_rev = true })
|
||||
return
|
||||
end
|
||||
|
||||
local conf = sub and SPLIT_HANDLERS[sub]
|
||||
if conf then
|
||||
run_in_split(worktree, args, conf)
|
||||
run_in_split(r, args, conf)
|
||||
else
|
||||
run_to_messages(worktree, args)
|
||||
run_to_messages(r, args)
|
||||
end
|
||||
end
|
||||
|
||||
---@param arg_lead string
|
||||
---@return string[]
|
||||
function M.complete_rev(arg_lead)
|
||||
local r = repo.find()
|
||||
if not r then
|
||||
return {}
|
||||
end
|
||||
|
||||
local stage, stage_path_lead = arg_lead:match("^:([0-3]):(.*)$")
|
||||
if stage then
|
||||
local out = util.exec(
|
||||
{ "git", "ls-files", "--stage" },
|
||||
{ cwd = r.worktree, silent = true }
|
||||
)
|
||||
if not out then
|
||||
return {}
|
||||
end
|
||||
local matches = {}
|
||||
for _, line in ipairs(util.split_lines(out)) do
|
||||
local row_stage, row_path = line:match("^%S+ %S+ (%d)\t(.*)$")
|
||||
if
|
||||
row_stage == stage
|
||||
and row_path
|
||||
and row_path:sub(1, #stage_path_lead) == stage_path_lead
|
||||
then
|
||||
table.insert(matches, ":" .. stage .. ":" .. row_path)
|
||||
end
|
||||
end
|
||||
return matches
|
||||
end
|
||||
|
||||
local colon = arg_lead:find(":", 1, true)
|
||||
if not colon then
|
||||
local matches = {}
|
||||
for _, ref in ipairs(r:list_refs()) do
|
||||
if ref:sub(1, #arg_lead) == arg_lead then
|
||||
table.insert(matches, ref)
|
||||
end
|
||||
end
|
||||
return matches
|
||||
end
|
||||
|
||||
local rev = arg_lead:sub(1, colon - 1)
|
||||
local path_lead = arg_lead:sub(colon + 1)
|
||||
local dir, name_lead = path_lead:match("^(.*/)([^/]*)$")
|
||||
dir = dir or ""
|
||||
name_lead = name_lead or path_lead
|
||||
|
||||
if rev ~= "" then
|
||||
local cmd = { "git", "ls-tree", rev }
|
||||
if dir ~= "" then
|
||||
table.insert(cmd, dir)
|
||||
end
|
||||
local out = util.exec(cmd, { cwd = r.worktree, silent = true })
|
||||
if not out then
|
||||
return {}
|
||||
end
|
||||
local matches = {}
|
||||
for _, line in ipairs(util.split_lines(out)) do
|
||||
local typ, full_path = line:match("^%S+ (%S+) %S+\t(.*)$")
|
||||
if typ and full_path then
|
||||
local basename = dir == "" and full_path
|
||||
or full_path:sub(#dir + 1)
|
||||
if typ == "tree" then
|
||||
basename = basename .. "/"
|
||||
end
|
||||
if basename:sub(1, #name_lead) == name_lead then
|
||||
table.insert(matches, rev .. ":" .. dir .. basename)
|
||||
end
|
||||
end
|
||||
end
|
||||
return matches
|
||||
end
|
||||
|
||||
local cmd = { "git", "ls-files" }
|
||||
if dir ~= "" then
|
||||
table.insert(cmd, dir)
|
||||
end
|
||||
local out = util.exec(cmd, { cwd = r.worktree, silent = true })
|
||||
if not out then
|
||||
return {}
|
||||
end
|
||||
local matches = {}
|
||||
local seen = {}
|
||||
for _, full_path in ipairs(util.split_lines(out)) do
|
||||
local rel = dir == "" and full_path or full_path:sub(#dir + 1)
|
||||
local slash = rel:find("/", 1, true)
|
||||
local segment = slash and rel:sub(1, slash) or rel
|
||||
if not seen[segment] and segment:sub(1, #name_lead) == name_lead then
|
||||
seen[segment] = true
|
||||
table.insert(matches, ":" .. dir .. segment)
|
||||
end
|
||||
end
|
||||
return matches
|
||||
end
|
||||
|
||||
---@param arg_lead string
|
||||
---@param cmd_line string
|
||||
---@return string[]
|
||||
|
||||
+3
-3
@@ -7,8 +7,8 @@ local M = {}
|
||||
---@param opts { amend: boolean? }?
|
||||
function M.commit(opts)
|
||||
local amend = opts and opts.amend or false
|
||||
local _, worktree = repo.current_repo()
|
||||
if not worktree then
|
||||
local r = repo.find()
|
||||
if not r then
|
||||
util.warning("not in a git repository")
|
||||
return
|
||||
end
|
||||
@@ -19,7 +19,7 @@ function M.commit(opts)
|
||||
end
|
||||
|
||||
local proxy_buf, proxy_win
|
||||
editor.run(cmd, { cwd = worktree }, function(file_path, done)
|
||||
editor.run(cmd, { cwd = r.worktree }, function(file_path, done)
|
||||
local lines = {}
|
||||
local f = io.open(file_path, "r")
|
||||
if f then
|
||||
|
||||
+14
-19
@@ -62,8 +62,8 @@ end
|
||||
---@param buf integer
|
||||
---@param rev ow.Git.Revision
|
||||
local function uri_split(opts, buf, rev)
|
||||
local worktree = vim.b[buf].git_worktree or select(2, repo.current_repo())
|
||||
if not worktree then
|
||||
local r = repo.find(buf)
|
||||
if not r then
|
||||
util.warning("git URI buffer has no worktree")
|
||||
return
|
||||
end
|
||||
@@ -76,7 +76,7 @@ local function uri_split(opts, buf, rev)
|
||||
if opts.rev and opts.rev:find(":", 1, true) then
|
||||
local content = util.exec(
|
||||
{ "git", "cat-file", "-p", opts.rev },
|
||||
{ cwd = worktree, silent = true }
|
||||
{ cwd = r.worktree, silent = true }
|
||||
)
|
||||
if not content then
|
||||
util.warning("invalid rev: %s", opts.rev)
|
||||
@@ -84,7 +84,7 @@ local function uri_split(opts, buf, rev)
|
||||
end
|
||||
place_pair(
|
||||
buf,
|
||||
object.buf_for(worktree, Revision.parse(opts.rev), content),
|
||||
object.buf_for(r, Revision.parse(opts.rev), content),
|
||||
false,
|
||||
opts.vertical
|
||||
)
|
||||
@@ -92,7 +92,7 @@ local function uri_split(opts, buf, rev)
|
||||
end
|
||||
|
||||
if not opts.rev then
|
||||
local worktree_path = vim.fs.joinpath(worktree, rev.path)
|
||||
local worktree_path = vim.fs.joinpath(r.worktree, rev.path)
|
||||
if not vim.uv.fs_stat(worktree_path) then
|
||||
util.warning("worktree file does not exist: %s", rev.path)
|
||||
return
|
||||
@@ -118,18 +118,13 @@ local function uri_split(opts, buf, rev)
|
||||
local other_rev, left = m[1], m[2]
|
||||
local content = util.exec(
|
||||
{ "git", "cat-file", "-p", other_rev:format() },
|
||||
{ cwd = worktree, silent = true }
|
||||
{ cwd = r.worktree, silent = true }
|
||||
)
|
||||
if not content then
|
||||
util.warning("invalid rev: %s", other_rev:format())
|
||||
return
|
||||
end
|
||||
place_pair(
|
||||
buf,
|
||||
object.buf_for(worktree, other_rev, content),
|
||||
left,
|
||||
opts.vertical
|
||||
)
|
||||
place_pair(buf, object.buf_for(r, other_rev, content), left, opts.vertical)
|
||||
end
|
||||
|
||||
---@class ow.Git.SplitOpts
|
||||
@@ -141,7 +136,7 @@ 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 = Revision.from_uri(cur_path)
|
||||
local cur_rev = require("git.object").parse_uri(cur_path)
|
||||
if cur_rev then
|
||||
return uri_split(opts, cur_buf, cur_rev)
|
||||
end
|
||||
@@ -154,13 +149,13 @@ function M.split(opts)
|
||||
util.warning("cannot diff this buffer (not a worktree file)")
|
||||
return
|
||||
end
|
||||
local _, worktree, cur_path = repo.resolve(cur_path)
|
||||
if not worktree then
|
||||
cur_path = vim.fn.resolve(cur_path)
|
||||
local r = repo.resolve(cur_path)
|
||||
if not r then
|
||||
util.warning("not in a git repository")
|
||||
return
|
||||
end
|
||||
---@cast cur_path -nil
|
||||
local rel = vim.fs.relpath(worktree, cur_path)
|
||||
local rel = vim.fs.relpath(r.worktree, cur_path)
|
||||
|
||||
local rev
|
||||
if not opts.rev then
|
||||
@@ -172,13 +167,13 @@ function M.split(opts)
|
||||
end
|
||||
local content = util.exec(
|
||||
{ "git", "cat-file", "-p", rev:format() },
|
||||
{ cwd = worktree, silent = true }
|
||||
{ cwd = r.worktree, silent = true }
|
||||
)
|
||||
if not content then
|
||||
util.warning("invalid rev: %s", rev:format())
|
||||
return
|
||||
end
|
||||
local buf = require("git.object").buf_for(worktree, rev, content)
|
||||
local buf = require("git.object").buf_for(r, rev, content)
|
||||
place_pair(buf, cur_buf, true, opts.vertical)
|
||||
end
|
||||
|
||||
|
||||
+6
-6
@@ -25,26 +25,26 @@ function M.init()
|
||||
{
|
||||
group = group,
|
||||
callback = function(args)
|
||||
require("git.watcher").refresh_buf(args.buf)
|
||||
require("git.repo").refresh(args.buf)
|
||||
end,
|
||||
}
|
||||
)
|
||||
vim.api.nvim_create_autocmd({ "BufDelete", "BufWipeout" }, {
|
||||
group = group,
|
||||
callback = function(args)
|
||||
require("git.watcher").unregister(args.buf)
|
||||
require("git.repo").unregister(args.buf)
|
||||
end,
|
||||
})
|
||||
vim.api.nvim_create_autocmd("FocusGained", {
|
||||
group = group,
|
||||
callback = function(args)
|
||||
require("git.watcher").refresh_buf(args.buf)
|
||||
require("git.repo").refresh(args.buf)
|
||||
end,
|
||||
})
|
||||
vim.api.nvim_create_autocmd("VimLeavePre", {
|
||||
group = group,
|
||||
callback = function()
|
||||
require("git.watcher").stop_all()
|
||||
require("git.repo").stop_all()
|
||||
end,
|
||||
})
|
||||
|
||||
@@ -82,7 +82,7 @@ function M.init()
|
||||
end
|
||||
end
|
||||
local function complete_rev(...)
|
||||
return require("git.repo").complete_rev(...)
|
||||
return require("git.cmd").complete_rev(...)
|
||||
end
|
||||
vim.api.nvim_create_user_command("Gdiffsplit", diff_split_cmd(true), {
|
||||
nargs = "?",
|
||||
@@ -101,7 +101,7 @@ function M.init()
|
||||
})
|
||||
vim.api.nvim_create_user_command("Gedit", function(opts)
|
||||
vim.cmd.edit({
|
||||
args = { "git://" .. opts.args },
|
||||
args = { require("git.object").URI_PREFIX .. opts.args },
|
||||
magic = { file = false },
|
||||
})
|
||||
end, {
|
||||
|
||||
+43
-10
@@ -3,8 +3,31 @@ local util = require("git.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
M.URI_PREFIX = "gitlog://"
|
||||
|
||||
local LOG_FORMAT = "%h %ad {%an}%d %s"
|
||||
local URI_PREFIX = "gitlog://"
|
||||
|
||||
local cr = vim.api.nvim_replace_termcodes("<CR>", true, false, true)
|
||||
|
||||
---@param buf integer
|
||||
local function attach_dispatch(buf)
|
||||
vim.keymap.set("n", "<CR>", function()
|
||||
local r = repo.find(buf)
|
||||
-- Anchor past the leading graph chars (matches the leading sha
|
||||
-- column, not any hex word that happens to appear later in the
|
||||
-- subject).
|
||||
local sha = r
|
||||
and vim.api
|
||||
.nvim_get_current_line()
|
||||
:match("^[*|/\\_ ]*(%x%x%x%x%x%x%x+)")
|
||||
if sha then
|
||||
---@cast r -nil
|
||||
require("git.object").open_object(r, sha, { split = false })
|
||||
else
|
||||
vim.api.nvim_feedkeys(cr, "n", false)
|
||||
end
|
||||
end, { buffer = buf, silent = true, desc = "Open commit" })
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param max_count integer?
|
||||
@@ -27,8 +50,12 @@ end
|
||||
|
||||
---@param buf integer
|
||||
local function populate(buf)
|
||||
local worktree = vim.b[buf].git_worktree
|
||||
local stdout = fetch(worktree, vim.b[buf].git_log_max_count)
|
||||
local r = repo.find(buf)
|
||||
local state = r and r:state(buf)
|
||||
if not r or not state then
|
||||
return
|
||||
end
|
||||
local stdout = fetch(r.worktree, state.log_max_count)
|
||||
if not stdout then
|
||||
return
|
||||
end
|
||||
@@ -65,12 +92,16 @@ end
|
||||
---@param buf integer
|
||||
function M.read_uri(buf)
|
||||
local name = vim.api.nvim_buf_get_name(buf)
|
||||
local worktree = name:sub(#URI_PREFIX + 1)
|
||||
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.attach(buf, r)
|
||||
|
||||
vim.b[buf].git_worktree = worktree
|
||||
vim.bo[buf].swapfile = false
|
||||
vim.bo[buf].bufhidden = "hide"
|
||||
vim.bo[buf].buftype = "nofile"
|
||||
@@ -78,6 +109,7 @@ function M.read_uri(buf)
|
||||
vim.bo[buf].filetype = "gitlog"
|
||||
end
|
||||
|
||||
attach_dispatch(buf)
|
||||
populate(buf)
|
||||
end
|
||||
|
||||
@@ -92,15 +124,16 @@ M.opt_parsers = {
|
||||
---@param opts ow.Git.LogOpts?
|
||||
function M.open(opts)
|
||||
opts = opts or {}
|
||||
local _, worktree = repo.current_repo()
|
||||
if not worktree then
|
||||
local r = repo.find()
|
||||
if not r then
|
||||
util.warning("not in a git repository")
|
||||
return
|
||||
end
|
||||
|
||||
local buf = vim.fn.bufadd(URI_PREFIX .. worktree)
|
||||
vim.b[buf].git_worktree = worktree
|
||||
vim.b[buf].git_log_max_count = opts.max_count
|
||||
local buf = vim.fn.bufadd(M.URI_PREFIX .. r.worktree)
|
||||
repo.attach(buf, r)
|
||||
local state = r:state(buf) --[[@as -nil]]
|
||||
state.log_max_count = opts.max_count
|
||||
local was_loaded = vim.api.nvim_buf_is_loaded(buf)
|
||||
|
||||
local win = vim.fn.bufwinid(buf)
|
||||
|
||||
+96
-75
@@ -4,27 +4,29 @@ local util = require("git.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
M.URI_PREFIX = "git://"
|
||||
|
||||
---@param rev ow.Git.Revision
|
||||
---@return string
|
||||
function M.format_uri(rev)
|
||||
return M.URI_PREFIX .. rev:format()
|
||||
end
|
||||
|
||||
---@param str string
|
||||
---@return ow.Git.Revision?
|
||||
function M.parse_uri(str)
|
||||
local raw = str:match("^" .. M.URI_PREFIX .. "(.+)$")
|
||||
if raw then
|
||||
return Revision.parse(raw)
|
||||
end
|
||||
end
|
||||
|
||||
---@class ow.Git.DiffSection
|
||||
---@field path_a string
|
||||
---@field path_b string
|
||||
---@field blob_a string?
|
||||
---@field blob_b string?
|
||||
|
||||
---@class ow.Git.BufContext
|
||||
---@field worktree string
|
||||
---@field sha string
|
||||
---@field parent_sha string?
|
||||
|
||||
---@return ow.Git.BufContext?
|
||||
local function context()
|
||||
local worktree = vim.b.git_worktree
|
||||
local sha = vim.b.git_sha
|
||||
if not worktree or not sha then
|
||||
return nil
|
||||
end
|
||||
return { worktree = worktree, sha = sha, parent_sha = vim.b.git_parent_sha }
|
||||
end
|
||||
|
||||
---@return ow.Git.DiffSection?
|
||||
local function diff_section()
|
||||
local diff_lnum = vim.fn.search("^diff --git ", "bcnW")
|
||||
@@ -70,9 +72,9 @@ local function is_zero(sha)
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
---@param worktree string
|
||||
---@param r ow.Git.Repo
|
||||
---@param path string
|
||||
local function attach_index_writer(buf, worktree, path)
|
||||
local function attach_index_writer(buf, r, path)
|
||||
vim.api.nvim_create_autocmd("BufWriteCmd", {
|
||||
buffer = buf,
|
||||
callback = function()
|
||||
@@ -82,18 +84,19 @@ local function attach_index_writer(buf, worktree, path)
|
||||
) .. "\n"
|
||||
local hash_stdout = util.exec(
|
||||
{ "git", "hash-object", "-w", "--stdin" },
|
||||
{ cwd = worktree, stdin = body }
|
||||
{ cwd = r.worktree, stdin = body }
|
||||
)
|
||||
if not hash_stdout then
|
||||
return
|
||||
end
|
||||
local sha = vim.trim(hash_stdout)
|
||||
local mode = vim.b[buf].git_index_mode
|
||||
local state = r:state(buf)
|
||||
local mode = state and state.index_mode
|
||||
if not mode then
|
||||
mode = "100644"
|
||||
local ls = util.exec(
|
||||
{ "git", "ls-files", "-s", "--", path },
|
||||
{ cwd = worktree, silent = true }
|
||||
{ cwd = r.worktree, silent = true }
|
||||
)
|
||||
if ls then
|
||||
local m = ls:match("^(%d+)")
|
||||
@@ -101,7 +104,9 @@ local function attach_index_writer(buf, worktree, path)
|
||||
mode = m
|
||||
end
|
||||
end
|
||||
vim.b[buf].git_index_mode = mode
|
||||
if state then
|
||||
state.index_mode = mode
|
||||
end
|
||||
end
|
||||
-- Use the 3-arg form (mode sha path) instead of the comma
|
||||
-- form (mode,sha,path), which doesn't survive paths
|
||||
@@ -114,7 +119,7 @@ local function attach_index_writer(buf, worktree, path)
|
||||
mode,
|
||||
sha,
|
||||
path,
|
||||
}, { cwd = worktree })
|
||||
}, { cwd = r.worktree })
|
||||
then
|
||||
return
|
||||
end
|
||||
@@ -123,18 +128,27 @@ local function attach_index_writer(buf, worktree, path)
|
||||
})
|
||||
end
|
||||
|
||||
---@type table<integer, string>
|
||||
local pending_content = {}
|
||||
local cr = vim.api.nvim_replace_termcodes("<CR>", true, false, true)
|
||||
|
||||
---@param worktree string
|
||||
---@param buf integer
|
||||
function M.attach_dispatch(buf)
|
||||
vim.keymap.set("n", "<CR>", function()
|
||||
if not M.open_under_cursor() then
|
||||
vim.api.nvim_feedkeys(cr, "n", false)
|
||||
end
|
||||
end, { buffer = buf, silent = true, desc = "Open file at commit" })
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param rev ow.Git.Revision
|
||||
---@param content string?
|
||||
---@return integer
|
||||
function M.buf_for(worktree, rev, content)
|
||||
local buf = vim.fn.bufadd(rev:uri())
|
||||
vim.b[buf].git_worktree = worktree
|
||||
function M.buf_for(r, rev, content)
|
||||
local buf = vim.fn.bufadd(M.format_uri(rev))
|
||||
repo.attach(buf, r)
|
||||
if content then
|
||||
pending_content[buf] = content
|
||||
local state = r:state(buf) --[[@as -nil]]
|
||||
state.pending_content = content
|
||||
end
|
||||
vim.fn.bufload(buf)
|
||||
return buf
|
||||
@@ -143,35 +157,43 @@ end
|
||||
---@param buf integer
|
||||
function M.read_uri(buf)
|
||||
local name = vim.api.nvim_buf_get_name(buf)
|
||||
local rev = Revision.from_uri(name)
|
||||
local rev = M.parse_uri(name)
|
||||
if not rev then
|
||||
return
|
||||
end
|
||||
local rev_str = rev:format()
|
||||
|
||||
local worktree = vim.b[buf].git_worktree or select(2, repo.current_repo())
|
||||
if not worktree then
|
||||
local r = repo.find(buf)
|
||||
if not r then
|
||||
util.error("git BufReadCmd %s: cannot resolve worktree", name)
|
||||
return
|
||||
end
|
||||
vim.b[buf].git_worktree = worktree
|
||||
repo.attach(buf, r)
|
||||
local state = r:state(buf) --[[@as -nil]]
|
||||
|
||||
vim.bo[buf].swapfile = false
|
||||
vim.bo[buf].bufhidden = "hide"
|
||||
|
||||
---@type string?
|
||||
local stdout = pending_content[buf]
|
||||
pending_content[buf] = nil
|
||||
local stdout = state.pending_content
|
||||
state.pending_content = nil
|
||||
-- On a refresh tick (no caller-provided content), skip the re-read
|
||||
-- when the rev still resolves to the same sha. Avoids re-firing
|
||||
-- BufReadPost (and the LSP/treesitter re-attach storm) on every
|
||||
-- fs-event for buffers whose content can't have changed.
|
||||
if stdout == nil then
|
||||
local rev_sha = r:rev_parse(rev_str, true)
|
||||
if rev_sha and rev_sha == state.sha then
|
||||
return
|
||||
end
|
||||
stdout = util.exec(
|
||||
{ "git", "cat-file", "-p", rev_str },
|
||||
{ cwd = worktree }
|
||||
{ cwd = r.worktree }
|
||||
)
|
||||
end
|
||||
|
||||
if stdout and rev.path == nil then
|
||||
local commit_sha =
|
||||
repo.rev_parse(worktree, rev_str .. "^{commit}", true)
|
||||
local commit_sha = r:rev_parse(rev_str .. "^{commit}", true)
|
||||
if commit_sha then
|
||||
local patch = util.exec({
|
||||
"git",
|
||||
@@ -182,12 +204,11 @@ function M.read_uri(buf)
|
||||
"--root",
|
||||
"--no-commit-id",
|
||||
commit_sha,
|
||||
}, { cwd = worktree })
|
||||
}, { cwd = r.worktree })
|
||||
if patch then
|
||||
stdout = (stdout:gsub("\n*$", "\n\n")) .. patch
|
||||
end
|
||||
vim.b[buf].git_parent_sha =
|
||||
repo.rev_parse(worktree, commit_sha .. "^", true)
|
||||
state.parent_sha = r:rev_parse(commit_sha .. "^", true)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -196,16 +217,13 @@ function M.read_uri(buf)
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, util.split_lines(stdout))
|
||||
end
|
||||
|
||||
local rev_sha = repo.rev_parse(worktree, rev_str, true)
|
||||
if rev_sha then
|
||||
vim.b[buf].git_sha = rev_sha
|
||||
end
|
||||
state.sha = r:rev_parse(rev_str, true)
|
||||
|
||||
if rev.stage == 0 and rev.path then
|
||||
vim.bo[buf].buftype = "acwrite"
|
||||
if not vim.b[buf].git_index_writer then
|
||||
attach_index_writer(buf, worktree, rev.path)
|
||||
vim.b[buf].git_index_writer = true
|
||||
if not state.index_writer then
|
||||
attach_index_writer(buf, r, rev.path)
|
||||
state.index_writer = true
|
||||
end
|
||||
else
|
||||
vim.bo[buf].buftype = "nofile"
|
||||
@@ -226,27 +244,29 @@ function M.read_uri(buf)
|
||||
vim.bo[buf].filetype = "git"
|
||||
end
|
||||
|
||||
M.attach_dispatch(buf)
|
||||
|
||||
vim.api.nvim_exec_autocmds("BufReadPost", { buffer = buf })
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param r ow.Git.Repo
|
||||
---@param blob string?
|
||||
---@param path string
|
||||
---@param sha string
|
||||
---@return integer?
|
||||
local function blob_buf(worktree, blob, path, sha)
|
||||
local function blob_buf(r, blob, path, sha)
|
||||
if is_zero(blob) then
|
||||
return nil
|
||||
end
|
||||
return M.buf_for(worktree, Revision.new({ base = sha, path = path }))
|
||||
return M.buf_for(r, Revision.new({ base = sha, path = path }))
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param r ow.Git.Repo
|
||||
---@param blob string?
|
||||
---@param path string
|
||||
---@param sha string
|
||||
local function load_blob(worktree, blob, path, sha)
|
||||
local buf = blob_buf(worktree, blob, path, sha)
|
||||
local function load_blob(r, blob, path, sha)
|
||||
local buf = blob_buf(r, blob, path, sha)
|
||||
if not buf then
|
||||
util.warning("no content for %s at %s", path, sha)
|
||||
return
|
||||
@@ -255,17 +275,17 @@ local function load_blob(worktree, blob, path, sha)
|
||||
vim.api.nvim_set_current_buf(buf)
|
||||
end
|
||||
|
||||
---@param ctx ow.Git.BufContext
|
||||
---@param s ow.Git.BufState
|
||||
---@param section ow.Git.DiffSection
|
||||
local function open_section(ctx, section)
|
||||
local function open_section(s, section)
|
||||
if not section.blob_a or not section.blob_b then
|
||||
util.warning("no index line, cannot determine blob SHAs")
|
||||
return
|
||||
end
|
||||
local parent = ctx.parent_sha or "0"
|
||||
local left = blob_buf(ctx.worktree, section.blob_a, section.path_a, parent)
|
||||
local parent = s.parent_sha or "0"
|
||||
local left = blob_buf(s.repo, section.blob_a, section.path_a, parent)
|
||||
local right =
|
||||
blob_buf(ctx.worktree, section.blob_b, section.path_b, ctx.sha)
|
||||
blob_buf(s.repo, section.blob_b, section.path_b, s.sha --[[@as string]])
|
||||
if left and right then
|
||||
require("git.diff").open(left, right, true)
|
||||
return
|
||||
@@ -283,44 +303,45 @@ end
|
||||
---@class ow.Git.OpenObjectOpts
|
||||
---@field split (false|"above"|"below"|"left"|"right")?
|
||||
|
||||
---@param worktree string
|
||||
---@param r ow.Git.Repo
|
||||
---@param rev string
|
||||
---@param opts ow.Git.OpenObjectOpts?
|
||||
function M.open_object(worktree, rev, opts)
|
||||
function M.open_object(r, rev, opts)
|
||||
local parsed = Revision.parse(rev)
|
||||
if parsed.base then
|
||||
local sha = repo.rev_parse(worktree, parsed.base, true)
|
||||
local sha = r:rev_parse(parsed.base, true)
|
||||
if sha then
|
||||
parsed.base = sha
|
||||
end
|
||||
end
|
||||
local content = util.exec(
|
||||
{ "git", "cat-file", "-p", parsed:format() },
|
||||
{ cwd = worktree, silent = true }
|
||||
{ cwd = r.worktree, silent = true }
|
||||
)
|
||||
if not content then
|
||||
util.warning("not a git object: %s", rev)
|
||||
return
|
||||
end
|
||||
local buf = M.buf_for(worktree, parsed, content)
|
||||
local buf = M.buf_for(r, parsed, content)
|
||||
util.place_buf(buf, opts and opts.split)
|
||||
end
|
||||
|
||||
---@return boolean dispatched
|
||||
function M.open_under_cursor()
|
||||
local ctx = context()
|
||||
if not ctx then
|
||||
local s = repo.state()
|
||||
if not s or not s.sha then
|
||||
return false
|
||||
end
|
||||
|
||||
local line = vim.api.nvim_get_current_line()
|
||||
local r = s.repo
|
||||
|
||||
local sha = line:match("^commit (%x+)$")
|
||||
or line:match("^parent (%x+)$")
|
||||
or line:match("^tree (%x+)$")
|
||||
or line:match("^object (%x+)$")
|
||||
if sha then
|
||||
M.open_object(ctx.worktree, sha, { split = false })
|
||||
M.open_object(r, sha, { split = false })
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -328,9 +349,9 @@ function M.open_under_cursor()
|
||||
line:match("^%d+ (%w+) (%x+)\t(.+)$")
|
||||
if entry_sha then
|
||||
local nav_rev = entry_type == "blob"
|
||||
and Revision.new({ base = ctx.sha, path = entry_name }):format()
|
||||
and Revision.new({ base = s.sha, path = entry_name }):format()
|
||||
or entry_sha
|
||||
M.open_object(ctx.worktree, nav_rev, { split = false })
|
||||
M.open_object(r, nav_rev, { split = false })
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -338,26 +359,26 @@ function M.open_under_cursor()
|
||||
if not section then
|
||||
return false
|
||||
end
|
||||
local parent = ctx.parent_sha or "0"
|
||||
local parent = s.parent_sha or "0"
|
||||
|
||||
if line:match("^diff %-%-git ") then
|
||||
open_section(ctx, section)
|
||||
open_section(s, section)
|
||||
return true
|
||||
end
|
||||
if line:match("^%-%-%- ") then
|
||||
load_blob(ctx.worktree, section.blob_a, section.path_a, parent)
|
||||
load_blob(r, section.blob_a, section.path_a, parent)
|
||||
return true
|
||||
end
|
||||
if line:match("^%+%+%+ ") then
|
||||
load_blob(ctx.worktree, section.blob_b, section.path_b, ctx.sha)
|
||||
load_blob(r, section.blob_b, section.path_b, s.sha --[[@as string]])
|
||||
return true
|
||||
end
|
||||
local prefix = line:sub(1, 1)
|
||||
if prefix == "+" then
|
||||
load_blob(ctx.worktree, section.blob_b, section.path_b, ctx.sha)
|
||||
load_blob(r, section.blob_b, section.path_b, s.sha --[[@as string]])
|
||||
return true
|
||||
elseif prefix == "-" then
|
||||
load_blob(ctx.worktree, section.blob_a, section.path_a, parent)
|
||||
load_blob(r, section.blob_a, section.path_a, parent)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
|
||||
+338
-154
@@ -1,61 +1,205 @@
|
||||
local status = require("git.status")
|
||||
local util = require("git.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param path string
|
||||
---@return string? gitdir
|
||||
---@return string? worktree
|
||||
---@return string? path
|
||||
function M.resolve(path)
|
||||
path = vim.fn.resolve(path)
|
||||
local found = vim.fs.find(".git", { upward = true, path = path })[1]
|
||||
if not found then
|
||||
return nil
|
||||
---@param buf integer?
|
||||
---@return integer
|
||||
local function expand_buf(buf)
|
||||
if not buf or buf == 0 then
|
||||
return vim.api.nvim_get_current_buf()
|
||||
end
|
||||
local worktree = vim.fs.dirname(found)
|
||||
local stat = vim.uv.fs_stat(found)
|
||||
if not stat then
|
||||
return nil
|
||||
end
|
||||
if stat.type == "directory" then
|
||||
return found, worktree, path
|
||||
end
|
||||
local f = io.open(found, "r")
|
||||
if not f then
|
||||
return nil
|
||||
end
|
||||
local content = f:read("*a")
|
||||
f:close()
|
||||
local gitdir = content:match("gitdir:%s*(%S+)")
|
||||
if not gitdir then
|
||||
util.warning(".git file at %s has no `gitdir:` line", found)
|
||||
return nil
|
||||
end
|
||||
if not gitdir:match("^/") then
|
||||
gitdir = vim.fs.joinpath(worktree, gitdir)
|
||||
end
|
||||
return vim.fs.normalize(gitdir), worktree, path
|
||||
return buf
|
||||
end
|
||||
|
||||
---@return string? gitdir
|
||||
---@return string? worktree
|
||||
function M.current_repo()
|
||||
local path = vim.api.nvim_buf_get_name(0)
|
||||
if path == "" or path:match("^%a+://") then
|
||||
path = vim.fn.getcwd()
|
||||
---@class ow.Git.BufState
|
||||
---@field repo ow.Git.Repo
|
||||
---@field sha string?
|
||||
---@field parent_sha string?
|
||||
---@field index_writer boolean?
|
||||
---@field index_mode string?
|
||||
---@field log_max_count integer?
|
||||
---@field pending_content string?
|
||||
|
||||
---@class ow.Git.Repo
|
||||
---@field gitdir string
|
||||
---@field worktree string
|
||||
---@field buffers table<integer, ow.Git.BufState>
|
||||
---@field watcher? uv.uv_fs_event_t
|
||||
---@field refresh fun(self: ow.Git.Repo)
|
||||
---@field refresh_handle ow.Git.Util.DebounceHandle
|
||||
---@field private refresh_listeners (fun(r: ow.Git.Repo, porcelain_stdout: string?))[]
|
||||
local Repo = {}
|
||||
Repo.__index = Repo
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
local function do_refresh(r)
|
||||
vim.system(
|
||||
{
|
||||
"git",
|
||||
"-c",
|
||||
"core.quotePath=false",
|
||||
"status",
|
||||
"--porcelain=v1",
|
||||
"--branch",
|
||||
},
|
||||
{ cwd = r.worktree, text = true },
|
||||
vim.schedule_wrap(function(obj)
|
||||
local statuses = {}
|
||||
if obj.code == 0 then
|
||||
for line in (obj.stdout or ""):gmatch("[^\r\n]+") do
|
||||
if line:sub(1, 2) ~= "##" then
|
||||
local code = line:sub(1, 2)
|
||||
local x = code:sub(1, 1)
|
||||
local y = code:sub(2, 2)
|
||||
local path_part = line:sub(4)
|
||||
if x == "R" or x == "C" or y == "R" or y == "C" then
|
||||
local arrow = path_part:find(" -> ", 1, true)
|
||||
if arrow then
|
||||
path_part = path_part:sub(arrow + 4)
|
||||
end
|
||||
end
|
||||
statuses[vim.fs.joinpath(r.worktree, path_part)] =
|
||||
status.format(code)
|
||||
end
|
||||
end
|
||||
else
|
||||
util.warning(
|
||||
"git status failed: %s",
|
||||
vim.trim(obj.stderr or "")
|
||||
)
|
||||
end
|
||||
local dirty = false
|
||||
for buf in pairs(r.buffers) do
|
||||
if not vim.api.nvim_buf_is_valid(buf) then
|
||||
r.buffers[buf] = nil
|
||||
else
|
||||
local name = vim.api.nvim_buf_get_name(buf)
|
||||
local object = require("git.object")
|
||||
local log = require("git.log")
|
||||
if name:sub(1, #object.URI_PREFIX) == object.URI_PREFIX then
|
||||
object.read_uri(buf)
|
||||
elseif name:sub(1, #log.URI_PREFIX) == log.URI_PREFIX then
|
||||
log.read_uri(buf)
|
||||
else
|
||||
local s = statuses[vim.fn.resolve(name)]
|
||||
if vim.b[buf].git_status ~= s then
|
||||
vim.b[buf].git_status = s
|
||||
dirty = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if dirty then
|
||||
vim.cmd.redrawstatus({ bang = true })
|
||||
end
|
||||
r:notify_refresh(obj.code == 0 and obj.stdout or nil)
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
---@param gitdir string
|
||||
---@param worktree string
|
||||
---@return ow.Git.Repo
|
||||
function Repo.new(gitdir, worktree)
|
||||
local self = setmetatable({
|
||||
gitdir = gitdir,
|
||||
worktree = worktree,
|
||||
buffers = {},
|
||||
refresh_listeners = {},
|
||||
}, Repo)
|
||||
self.refresh, self.refresh_handle = util.debounce(do_refresh, 50)
|
||||
self:start_watcher()
|
||||
return self
|
||||
end
|
||||
|
||||
function Repo:start_watcher()
|
||||
local watcher, err = vim.uv.new_fs_event()
|
||||
if not watcher then
|
||||
util.warning(
|
||||
"git: failed to create fs_event for %s: %s",
|
||||
self.gitdir,
|
||||
err
|
||||
)
|
||||
return
|
||||
end
|
||||
local ok, err = watcher:start(
|
||||
self.gitdir,
|
||||
{ recursive = true },
|
||||
function(err_, filename)
|
||||
if
|
||||
err_
|
||||
or filename:match("^objects/")
|
||||
or filename:match("^logs/")
|
||||
then
|
||||
return
|
||||
end
|
||||
self:refresh()
|
||||
end
|
||||
)
|
||||
if not ok then
|
||||
util.warning("failed to watch %s: %s", self.gitdir, tostring(err))
|
||||
watcher:close()
|
||||
return
|
||||
end
|
||||
self.watcher = watcher
|
||||
end
|
||||
|
||||
function Repo:stop_watcher()
|
||||
if self.watcher then
|
||||
self.watcher:stop()
|
||||
self.watcher:close()
|
||||
self.watcher = nil
|
||||
end
|
||||
self.refresh_handle.close()
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
function Repo:add_buffer(buf)
|
||||
buf = expand_buf(buf)
|
||||
if not self.buffers[buf] then
|
||||
self.buffers[buf] = { repo = self }
|
||||
end
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
function Repo:remove_buffer(buf)
|
||||
self.buffers[expand_buf(buf)] = nil
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
---@return ow.Git.BufState?
|
||||
function Repo:state(buf)
|
||||
return self.buffers[expand_buf(buf)]
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function Repo:has_buffers()
|
||||
return next(self.buffers) ~= nil
|
||||
end
|
||||
|
||||
---@param fn fun(r: ow.Git.Repo, porcelain_stdout: string?)
|
||||
---@return fun() unsubscribe
|
||||
function Repo:on_refresh(fn)
|
||||
table.insert(self.refresh_listeners, fn)
|
||||
return function()
|
||||
for i, f in ipairs(self.refresh_listeners) do
|
||||
if f == fn then
|
||||
table.remove(self.refresh_listeners, i)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param porcelain_stdout string?
|
||||
function Repo:notify_refresh(porcelain_stdout)
|
||||
for _, fn in ipairs(self.refresh_listeners) do
|
||||
fn(self, porcelain_stdout)
|
||||
end
|
||||
local gitdir, worktree, _ = M.resolve(path)
|
||||
return gitdir, worktree
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@return string?
|
||||
function M.head(path)
|
||||
local gitdir = M.resolve(path)
|
||||
if not gitdir then
|
||||
return nil
|
||||
end
|
||||
local f = io.open(vim.fs.joinpath(gitdir, "HEAD"), "r")
|
||||
function Repo:head()
|
||||
local f = io.open(vim.fs.joinpath(self.gitdir, "HEAD"), "r")
|
||||
if not f then
|
||||
return nil
|
||||
end
|
||||
@@ -75,9 +219,8 @@ function M.head(path)
|
||||
return nil
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@return string[]
|
||||
function M.list_refs(worktree)
|
||||
function Repo:list_refs()
|
||||
local out = util.exec({
|
||||
"git",
|
||||
"for-each-ref",
|
||||
@@ -85,7 +228,7 @@ function M.list_refs(worktree)
|
||||
"refs/heads",
|
||||
"refs/tags",
|
||||
"refs/remotes",
|
||||
}, { cwd = worktree, silent = true })
|
||||
}, { cwd = self.worktree, silent = true })
|
||||
if not out then
|
||||
return {}
|
||||
end
|
||||
@@ -94,118 +237,159 @@ function M.list_refs(worktree)
|
||||
return refs
|
||||
end
|
||||
|
||||
---@param arg_lead string
|
||||
---@return string[]
|
||||
function M.complete_rev(arg_lead)
|
||||
local _, worktree = M.current_repo()
|
||||
if not worktree then
|
||||
return {}
|
||||
end
|
||||
|
||||
local stage, stage_path_lead = arg_lead:match("^:([0-3]):(.*)$")
|
||||
if stage then
|
||||
local out = util.exec(
|
||||
{ "git", "ls-files", "--stage" },
|
||||
{ cwd = worktree, silent = true }
|
||||
)
|
||||
if not out then
|
||||
return {}
|
||||
end
|
||||
local matches = {}
|
||||
for _, line in ipairs(util.split_lines(out)) do
|
||||
local row_stage, row_path = line:match("^%S+ %S+ (%d)\t(.*)$")
|
||||
if
|
||||
row_stage == stage
|
||||
and row_path
|
||||
and row_path:sub(1, #stage_path_lead) == stage_path_lead
|
||||
then
|
||||
table.insert(matches, ":" .. stage .. ":" .. row_path)
|
||||
end
|
||||
end
|
||||
return matches
|
||||
end
|
||||
|
||||
local colon = arg_lead:find(":", 1, true)
|
||||
if not colon then
|
||||
local matches = {}
|
||||
for _, ref in ipairs(M.list_refs(worktree)) do
|
||||
if ref:sub(1, #arg_lead) == arg_lead then
|
||||
table.insert(matches, ref)
|
||||
end
|
||||
end
|
||||
return matches
|
||||
end
|
||||
|
||||
local rev = arg_lead:sub(1, colon - 1)
|
||||
local path_lead = arg_lead:sub(colon + 1)
|
||||
local dir, name_lead = path_lead:match("^(.*/)([^/]*)$")
|
||||
dir = dir or ""
|
||||
name_lead = name_lead or path_lead
|
||||
|
||||
if rev ~= "" then
|
||||
local cmd = { "git", "ls-tree", rev }
|
||||
if dir ~= "" then
|
||||
table.insert(cmd, dir)
|
||||
end
|
||||
local out = util.exec(cmd, { cwd = worktree, silent = true })
|
||||
if not out then
|
||||
return {}
|
||||
end
|
||||
local matches = {}
|
||||
for _, line in ipairs(util.split_lines(out)) do
|
||||
local typ, full_path = line:match("^%S+ (%S+) %S+\t(.*)$")
|
||||
if typ and full_path then
|
||||
local basename = dir == "" and full_path
|
||||
or full_path:sub(#dir + 1)
|
||||
if typ == "tree" then
|
||||
basename = basename .. "/"
|
||||
end
|
||||
if basename:sub(1, #name_lead) == name_lead then
|
||||
table.insert(matches, rev .. ":" .. dir .. basename)
|
||||
end
|
||||
end
|
||||
end
|
||||
return matches
|
||||
end
|
||||
|
||||
local cmd = { "git", "ls-files" }
|
||||
if dir ~= "" then
|
||||
table.insert(cmd, dir)
|
||||
end
|
||||
local out = util.exec(cmd, { cwd = worktree, silent = true })
|
||||
if not out then
|
||||
return {}
|
||||
end
|
||||
local matches = {}
|
||||
local seen = {}
|
||||
for _, full_path in ipairs(util.split_lines(out)) do
|
||||
local rel = dir == "" and full_path or full_path:sub(#dir + 1)
|
||||
local slash = rel:find("/", 1, true)
|
||||
local segment = slash and rel:sub(1, slash) or rel
|
||||
if
|
||||
not seen[segment]
|
||||
and segment:sub(1, #name_lead) == name_lead
|
||||
then
|
||||
seen[segment] = true
|
||||
table.insert(matches, ":" .. dir .. segment)
|
||||
end
|
||||
end
|
||||
return matches
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param rev string
|
||||
---@param short boolean
|
||||
---@return string?
|
||||
function M.rev_parse(worktree, rev, short)
|
||||
function Repo:rev_parse(rev, short)
|
||||
local cmd = { "git", "rev-parse", "--verify", "--quiet" }
|
||||
if short then
|
||||
table.insert(cmd, "--short")
|
||||
end
|
||||
table.insert(cmd, rev)
|
||||
local stdout = util.exec(cmd, { cwd = worktree, silent = true })
|
||||
local stdout = util.exec(cmd, { cwd = self.worktree, silent = true })
|
||||
local trimmed = stdout and vim.trim(stdout) or ""
|
||||
return trimmed ~= "" and trimmed or nil
|
||||
end
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type table<string, ow.Git.Repo>
|
||||
local repo_by_gitdir = {}
|
||||
|
||||
---@type table<integer, ow.Git.Repo>
|
||||
local repo_by_buf = {}
|
||||
|
||||
---@param path string
|
||||
---@return ow.Git.Repo?
|
||||
function M.resolve(path)
|
||||
path = vim.fn.resolve(path)
|
||||
local found = vim.fs.find(".git", { upward = true, path = path })[1]
|
||||
if not found then
|
||||
return nil
|
||||
end
|
||||
local stat = vim.uv.fs_stat(found)
|
||||
if not stat then
|
||||
return nil
|
||||
end
|
||||
local worktree = vim.fs.dirname(found)
|
||||
local gitdir
|
||||
if stat.type == "directory" then
|
||||
gitdir = found
|
||||
else
|
||||
local f = io.open(found, "r")
|
||||
if not f then
|
||||
return nil
|
||||
end
|
||||
local content = f:read("*a")
|
||||
f:close()
|
||||
local rel = content:match("gitdir:%s*(%S+)")
|
||||
if not rel then
|
||||
util.warning(".git file at %s has no `gitdir:` line", found)
|
||||
return nil
|
||||
end
|
||||
if rel:match("^/") then
|
||||
gitdir = rel
|
||||
else
|
||||
gitdir = vim.fs.joinpath(worktree, rel)
|
||||
end
|
||||
gitdir = vim.fs.normalize(gitdir)
|
||||
end
|
||||
local r = repo_by_gitdir[gitdir]
|
||||
if not r then
|
||||
r = Repo.new(gitdir, worktree)
|
||||
repo_by_gitdir[gitdir] = r
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
---@param buf integer?
|
||||
---@return ow.Git.Repo?
|
||||
function M.find(buf)
|
||||
buf = expand_buf(buf)
|
||||
local existing = repo_by_buf[buf]
|
||||
if existing then
|
||||
return existing
|
||||
end
|
||||
local path = vim.api.nvim_buf_get_name(buf)
|
||||
if path == "" or path:match("^%a+://") then
|
||||
path = vim.fn.getcwd()
|
||||
end
|
||||
return M.resolve(path)
|
||||
end
|
||||
|
||||
---@param buf integer?
|
||||
---@return ow.Git.BufState?
|
||||
function M.state(buf)
|
||||
buf = expand_buf(buf)
|
||||
local r = repo_by_buf[buf]
|
||||
return r and r.buffers[buf]
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
---@param r ow.Git.Repo
|
||||
function M.attach(buf, r)
|
||||
buf = expand_buf(buf)
|
||||
if repo_by_buf[buf] == r then
|
||||
return
|
||||
end
|
||||
if repo_by_buf[buf] then
|
||||
repo_by_buf[buf]:remove_buffer(buf)
|
||||
end
|
||||
r:add_buffer(buf)
|
||||
repo_by_buf[buf] = r
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
---@return ow.Git.Repo?
|
||||
function M.register(buf)
|
||||
buf = expand_buf(buf)
|
||||
local r = M.find(buf)
|
||||
if not r then
|
||||
return nil
|
||||
end
|
||||
if repo_by_buf[buf] ~= r then
|
||||
M.attach(buf, r)
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
function M.unregister(buf)
|
||||
buf = expand_buf(buf)
|
||||
local r = repo_by_buf[buf]
|
||||
if not r then
|
||||
return
|
||||
end
|
||||
repo_by_buf[buf] = nil
|
||||
r:remove_buffer(buf)
|
||||
if not r:has_buffers() then
|
||||
r:stop_watcher()
|
||||
repo_by_gitdir[r.gitdir] = nil
|
||||
end
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
function M.refresh(buf)
|
||||
buf = expand_buf(buf)
|
||||
if not vim.api.nvim_buf_is_valid(buf) or vim.bo[buf].buftype ~= "" then
|
||||
return
|
||||
end
|
||||
local path = vim.api.nvim_buf_get_name(buf)
|
||||
if path == "" or path:match("^%a+://") then
|
||||
return
|
||||
end
|
||||
local r = M.register(buf)
|
||||
if not r then
|
||||
vim.b[buf].git_status = nil
|
||||
return
|
||||
end
|
||||
r:refresh()
|
||||
end
|
||||
|
||||
function M.stop_all()
|
||||
for _, r in pairs(repo_by_gitdir) do
|
||||
r:stop_watcher()
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
local Revision = {}
|
||||
Revision.__index = Revision
|
||||
|
||||
local URI_PREFIX = "git://"
|
||||
|
||||
---@return string
|
||||
function Revision:format()
|
||||
if self.stage then
|
||||
@@ -17,11 +15,6 @@ function Revision:format()
|
||||
return self.base or error("Revision:format: empty Revision")
|
||||
end
|
||||
|
||||
---@return string
|
||||
function Revision:uri()
|
||||
return URI_PREFIX .. self:format()
|
||||
end
|
||||
|
||||
---@param parts { stage?: integer, base?: string, path?: string }
|
||||
---@return ow.Git.Revision
|
||||
function Revision.new(parts)
|
||||
@@ -49,13 +42,4 @@ function Revision.parse(str)
|
||||
return Revision.new({ base = str })
|
||||
end
|
||||
|
||||
---@param str string
|
||||
---@return ow.Git.Revision?
|
||||
function Revision.from_uri(str)
|
||||
local raw = str:match("^" .. URI_PREFIX .. "(.+)$")
|
||||
if raw then
|
||||
return Revision.parse(raw)
|
||||
end
|
||||
end
|
||||
|
||||
return Revision
|
||||
|
||||
+32
-45
@@ -32,14 +32,13 @@ local SIDEBAR_WIDTH = 50
|
||||
---@alias ow.Git.SidebarEntry ow.Git.FileEntry | ow.Git.CommitEntry
|
||||
|
||||
---@class ow.Git.SidebarState
|
||||
---@field gitdir string
|
||||
---@field worktree string
|
||||
---@field repo ow.Git.Repo
|
||||
---@field lines table<integer, ow.Git.SidebarEntry>
|
||||
---@field sidebar_win integer?
|
||||
---@field invocation_win integer?
|
||||
---@field diff_left_win integer?
|
||||
---@field diff_right_win integer?
|
||||
---@field user_aucmd integer?
|
||||
---@field unsubscribe fun()?
|
||||
---@field last_shown_key string?
|
||||
---@field last_render_key string?
|
||||
|
||||
@@ -407,7 +406,7 @@ local function refresh(bufnr, prefetched_stdout)
|
||||
end
|
||||
end
|
||||
|
||||
fetch_status(s.worktree, prefetched_stdout, function(branch, groups)
|
||||
fetch_status(s.repo.worktree, prefetched_stdout, function(branch, groups)
|
||||
if not vim.api.nvim_buf_is_valid(bufnr) then
|
||||
return
|
||||
end
|
||||
@@ -456,22 +455,22 @@ end
|
||||
---@field left ow.Git.DiffSide
|
||||
---@field right ow.Git.DiffSide
|
||||
|
||||
---@param worktree string
|
||||
---@param r ow.Git.Repo
|
||||
---@param path string
|
||||
---@return ow.Git.DiffSide
|
||||
local function head_pane(worktree, path)
|
||||
local function head_pane(r, path)
|
||||
local rev = Revision.new({ base = "HEAD", path = path })
|
||||
return {
|
||||
buf = object.buf_for(worktree, rev),
|
||||
name = rev:uri(),
|
||||
buf = object.buf_for(r, rev),
|
||||
name = object.format_uri(rev),
|
||||
}
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param r ow.Git.Repo
|
||||
---@param path string
|
||||
---@return ow.Git.DiffSide
|
||||
local function worktree_pane(worktree, path)
|
||||
local buf = vim.fn.bufadd(vim.fs.joinpath(worktree, path))
|
||||
local function worktree_pane(r, path)
|
||||
local buf = vim.fn.bufadd(vim.fs.joinpath(r.worktree, path))
|
||||
vim.fn.bufload(buf)
|
||||
return { buf = buf, name = nil }
|
||||
end
|
||||
@@ -482,8 +481,8 @@ end
|
||||
local function index_pane(s, entry)
|
||||
local rev = Revision.new({ stage = 0, path = entry.path })
|
||||
return {
|
||||
buf = object.buf_for(s.worktree, rev),
|
||||
name = rev:uri(),
|
||||
buf = object.buf_for(s.repo, rev),
|
||||
name = object.format_uri(rev),
|
||||
}
|
||||
end
|
||||
|
||||
@@ -495,7 +494,7 @@ local function older_pane(s, entry)
|
||||
if entry.x == "A" then
|
||||
return nil
|
||||
end
|
||||
return head_pane(s.worktree, entry.orig or entry.path)
|
||||
return head_pane(s.repo, entry.orig or entry.path)
|
||||
end
|
||||
if entry.section == "Unstaged" then
|
||||
return index_pane(s, entry)
|
||||
@@ -517,10 +516,10 @@ local function newer_pane(s, entry)
|
||||
if entry.y == "D" then
|
||||
return nil
|
||||
end
|
||||
return worktree_pane(s.worktree, entry.path)
|
||||
return worktree_pane(s.repo, entry.path)
|
||||
end
|
||||
if entry.section == "Untracked" then
|
||||
return worktree_pane(s.worktree, entry.path)
|
||||
return worktree_pane(s.repo, entry.path)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
@@ -695,8 +694,9 @@ local function view_entry(s, entry, focus_left)
|
||||
+ vim.api.nvim_win_get_width(right_win)
|
||||
vim.api.nvim_win_set_width(left_win, math.floor(combined / 2))
|
||||
end
|
||||
---@cast left_win -nil
|
||||
---@cast right_win -nil
|
||||
|
||||
assert(left_win and right_win, "diff windows must be set")
|
||||
vim.w[left_win].git_diff_role = "left"
|
||||
vim.w[right_win].git_diff_role = "right"
|
||||
s.diff_left_win = left_win
|
||||
@@ -728,7 +728,7 @@ local function action_stage()
|
||||
end
|
||||
vim.system(
|
||||
{ "git", "add", "--", entry.path },
|
||||
{ cwd = s.worktree },
|
||||
{ cwd = s.repo.worktree },
|
||||
vim.schedule_wrap(function(obj)
|
||||
if obj.code ~= 0 then
|
||||
util.error("git add failed: %s", vim.trim(obj.stderr or ""))
|
||||
@@ -753,7 +753,7 @@ local function action_unstage()
|
||||
table.insert(cmd, entry.path)
|
||||
vim.system(
|
||||
cmd,
|
||||
{ cwd = s.worktree },
|
||||
{ cwd = s.repo.worktree },
|
||||
vim.schedule_wrap(function(obj)
|
||||
if obj.code ~= 0 then
|
||||
util.error(
|
||||
@@ -785,7 +785,7 @@ local function action_discard()
|
||||
entry.path
|
||||
)
|
||||
action = function()
|
||||
local target = vim.fs.joinpath(s.worktree, entry.path)
|
||||
local target = vim.fs.joinpath(s.repo.worktree, entry.path)
|
||||
local rc = vim.fn.delete(target, is_dir and "rf" or "")
|
||||
if rc ~= 0 then
|
||||
util.error("failed to delete %s", entry.path)
|
||||
@@ -797,7 +797,7 @@ local function action_discard()
|
||||
action = function()
|
||||
vim.system(
|
||||
{ "git", "checkout", "--", entry.path },
|
||||
{ cwd = s.worktree },
|
||||
{ cwd = s.repo.worktree },
|
||||
vim.schedule_wrap(function(obj)
|
||||
if obj.code ~= 0 then
|
||||
util.error(
|
||||
@@ -829,20 +829,14 @@ local function action_help()
|
||||
}, "\n"))
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
local function open(worktree)
|
||||
---@param r ow.Git.Repo
|
||||
local function open(r)
|
||||
local existing = find_sidebar()
|
||||
if existing then
|
||||
vim.api.nvim_set_current_win(existing)
|
||||
return
|
||||
end
|
||||
|
||||
local gitdir, worktree = repo.resolve(worktree)
|
||||
if not gitdir then
|
||||
return
|
||||
end
|
||||
---@cast worktree -nil
|
||||
|
||||
local previous_win = vim.api.nvim_get_current_win()
|
||||
local bufnr, win = util.new_scratch({ split = "left" })
|
||||
vim.bo[bufnr].filetype = "gitsidebar"
|
||||
@@ -856,8 +850,7 @@ local function open(worktree)
|
||||
vim.api.nvim_win_set_width(win, SIDEBAR_WIDTH)
|
||||
|
||||
state[bufnr] = {
|
||||
gitdir = gitdir,
|
||||
worktree = worktree,
|
||||
repo = r,
|
||||
lines = {},
|
||||
sidebar_win = win,
|
||||
invocation_win = previous_win,
|
||||
@@ -882,15 +875,9 @@ local function open(worktree)
|
||||
k("X", action_discard, "Discard worktree changes")
|
||||
k("g?", action_help, "Help")
|
||||
|
||||
state[bufnr].user_aucmd = vim.api.nvim_create_autocmd("User", {
|
||||
pattern = "GitRefresh",
|
||||
group = group,
|
||||
callback = function(args)
|
||||
if args.data and args.data.gitdir == gitdir then
|
||||
refresh(bufnr, args.data.porcelain_stdout)
|
||||
end
|
||||
end,
|
||||
})
|
||||
state[bufnr].unsubscribe = r:on_refresh(function(_, porcelain_stdout)
|
||||
refresh(bufnr, porcelain_stdout)
|
||||
end)
|
||||
vim.api.nvim_create_autocmd({ "BufWipeout", "BufDelete" }, {
|
||||
buffer = bufnr,
|
||||
group = group,
|
||||
@@ -899,8 +886,8 @@ local function open(worktree)
|
||||
if not s then
|
||||
return
|
||||
end
|
||||
if s.user_aucmd then
|
||||
pcall(vim.api.nvim_del_autocmd, s.user_aucmd)
|
||||
if s.unsubscribe then
|
||||
s.unsubscribe()
|
||||
end
|
||||
state[bufnr] = nil
|
||||
end,
|
||||
@@ -916,12 +903,12 @@ function M.toggle()
|
||||
vim.api.nvim_win_close(sidebar_win, false)
|
||||
return
|
||||
end
|
||||
local _, worktree = repo.current_repo()
|
||||
if not worktree then
|
||||
local r = repo.find()
|
||||
if not r then
|
||||
util.warning("not in a git repository")
|
||||
return
|
||||
end
|
||||
open(worktree)
|
||||
open(r)
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
+14
-1
@@ -101,7 +101,20 @@ end
|
||||
---@param delay integer
|
||||
---@return F, ow.Git.Util.DebounceHandle
|
||||
function M.debounce(fn, delay)
|
||||
local timer = assert(vim.uv.new_timer())
|
||||
local timer, err = vim.uv.new_timer()
|
||||
if not timer then
|
||||
M.warning("git: failed to create timer: %s", err)
|
||||
local noop = function() end
|
||||
return fn,
|
||||
{
|
||||
cancel = noop,
|
||||
flush = noop,
|
||||
pending = function()
|
||||
return false
|
||||
end,
|
||||
close = noop,
|
||||
}
|
||||
end
|
||||
local args ---@type table?
|
||||
local gen = 0
|
||||
local fired_gen = 0
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
local repo = require("git.repo")
|
||||
local status = require("git.status")
|
||||
local util = require("git.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class ow.Git.Repo
|
||||
---@field gitdir string
|
||||
---@field worktree string
|
||||
---@field buffers table<integer, true>
|
||||
---@field watcher? uv.uv_fs_event_t
|
||||
---@field refresh fun(self: ow.Git.Repo)
|
||||
---@field refresh_handle ow.Git.Util.DebounceHandle
|
||||
local Repo = {}
|
||||
Repo.__index = Repo
|
||||
|
||||
function Repo:start_watcher()
|
||||
local watcher = assert(vim.uv.new_fs_event())
|
||||
assert(watcher:start(self.gitdir, {}, function(err, filename)
|
||||
if err or (filename ~= "index" and filename ~= "HEAD") then
|
||||
return
|
||||
end
|
||||
self:refresh()
|
||||
end))
|
||||
self.watcher = watcher
|
||||
end
|
||||
|
||||
function Repo:stop_watcher()
|
||||
-- Stop the libuv watcher first so no further fs-events can trigger
|
||||
-- self:refresh(). Tearing down the debounce handle first leaves a
|
||||
-- window where an in-flight watcher callback hits a closed timer.
|
||||
if self.watcher then
|
||||
self.watcher:stop()
|
||||
self.watcher:close()
|
||||
self.watcher = nil
|
||||
end
|
||||
self.refresh_handle.close()
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
function Repo:add_buffer(buf)
|
||||
self.buffers[buf] = true
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
function Repo:remove_buffer(buf)
|
||||
self.buffers[buf] = nil
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function Repo:has_buffers()
|
||||
return next(self.buffers) ~= nil
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
local function do_refresh(r)
|
||||
vim.system(
|
||||
{
|
||||
"git",
|
||||
"-c",
|
||||
"core.quotePath=false",
|
||||
"status",
|
||||
"--porcelain=v1",
|
||||
"--branch",
|
||||
},
|
||||
{ cwd = r.worktree, text = true },
|
||||
vim.schedule_wrap(function(obj)
|
||||
local statuses = {}
|
||||
if obj.code == 0 then
|
||||
for line in (obj.stdout or ""):gmatch("[^\r\n]+") do
|
||||
if line:sub(1, 2) ~= "##" then
|
||||
local code = line:sub(1, 2)
|
||||
local x = code:sub(1, 1)
|
||||
local y = code:sub(2, 2)
|
||||
local path_part = line:sub(4)
|
||||
if x == "R" or x == "C" or y == "R" or y == "C" then
|
||||
local arrow = path_part:find(" -> ", 1, true)
|
||||
if arrow then
|
||||
path_part = path_part:sub(arrow + 4)
|
||||
end
|
||||
end
|
||||
statuses[vim.fs.joinpath(r.worktree, path_part)] =
|
||||
status.format(code)
|
||||
end
|
||||
end
|
||||
else
|
||||
util.warning(
|
||||
"git status failed: %s",
|
||||
vim.trim(obj.stderr or "")
|
||||
)
|
||||
end
|
||||
local dirty = false
|
||||
for buf in pairs(r.buffers) do
|
||||
if not vim.api.nvim_buf_is_valid(buf) then
|
||||
r.buffers[buf] = nil
|
||||
else
|
||||
local status =
|
||||
statuses[vim.fn.resolve(vim.api.nvim_buf_get_name(buf))]
|
||||
if vim.b[buf].git_status ~= status then
|
||||
vim.b[buf].git_status = status
|
||||
dirty = true
|
||||
end
|
||||
end
|
||||
end
|
||||
if dirty then
|
||||
vim.cmd.redrawstatus({ bang = true })
|
||||
end
|
||||
vim.api.nvim_exec_autocmds("User", {
|
||||
pattern = "GitRefresh",
|
||||
data = {
|
||||
gitdir = r.gitdir,
|
||||
worktree = r.worktree,
|
||||
porcelain_stdout = obj.code == 0 and obj.stdout or nil,
|
||||
},
|
||||
})
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
---@param gitdir string
|
||||
---@param worktree string
|
||||
---@return ow.Git.Repo
|
||||
function Repo.new(gitdir, worktree)
|
||||
local self = setmetatable({
|
||||
gitdir = gitdir,
|
||||
worktree = worktree,
|
||||
buffers = {},
|
||||
}, Repo)
|
||||
self.refresh, self.refresh_handle = util.debounce(do_refresh, 50)
|
||||
self:start_watcher()
|
||||
return self
|
||||
end
|
||||
|
||||
---@type table<string, ow.Git.Repo>
|
||||
local repo_by_gitdir = {}
|
||||
|
||||
---@type table<integer, ow.Git.Repo>
|
||||
local repo_by_buf = {}
|
||||
|
||||
---@param buf integer
|
||||
---@return ow.Git.Repo?
|
||||
local function register(buf)
|
||||
local existing = repo_by_buf[buf]
|
||||
if existing then
|
||||
return existing
|
||||
end
|
||||
local path = vim.api.nvim_buf_get_name(buf)
|
||||
if path == "" then
|
||||
return nil
|
||||
end
|
||||
local gitdir, worktree = repo.resolve(path)
|
||||
if not gitdir or not worktree then
|
||||
return nil
|
||||
end
|
||||
local r = repo_by_gitdir[gitdir]
|
||||
if not r then
|
||||
r = Repo.new(gitdir, worktree)
|
||||
repo_by_gitdir[gitdir] = r
|
||||
end
|
||||
r:add_buffer(buf)
|
||||
repo_by_buf[buf] = r
|
||||
return r
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
function M.unregister(buf)
|
||||
local r = repo_by_buf[buf]
|
||||
if not r then
|
||||
return
|
||||
end
|
||||
repo_by_buf[buf] = nil
|
||||
r:remove_buffer(buf)
|
||||
if not r:has_buffers() then
|
||||
r:stop_watcher()
|
||||
repo_by_gitdir[r.gitdir] = nil
|
||||
end
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
function M.refresh_buf(buf)
|
||||
if not vim.api.nvim_buf_is_valid(buf) or vim.bo[buf].buftype ~= "" then
|
||||
return
|
||||
end
|
||||
local r = register(buf)
|
||||
if not r then
|
||||
vim.b[buf].git_status = nil
|
||||
return
|
||||
end
|
||||
r:refresh()
|
||||
end
|
||||
|
||||
function M.stop_all()
|
||||
for _, r in pairs(repo_by_gitdir) do
|
||||
r:stop_watcher()
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -111,7 +111,8 @@ require("nvim-tree").setup({
|
||||
full_name = true,
|
||||
root_folder_label = function(path)
|
||||
local label = vim.fn.fnamemodify(path, ":~")
|
||||
local git_head = require("git.repo").head(path)
|
||||
local r = require("git.repo").resolve(path)
|
||||
local git_head = r and r:head()
|
||||
if git_head then
|
||||
label = label .. (" %s"):format(git_head)
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user