926 lines
23 KiB
Lua
926 lines
23 KiB
Lua
local status = require("git.core.status")
|
|
local util = require("git.core.util")
|
|
|
|
local M = {}
|
|
|
|
---@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
|
|
return buf
|
|
end
|
|
|
|
---@class ow.Git.Repo.BufState
|
|
---@field repo ow.Git.Repo
|
|
---@field sha string?
|
|
---@field initialized boolean?
|
|
---@field immutable boolean?
|
|
---@field index_writer boolean?
|
|
---@field index_mode string?
|
|
|
|
---@alias ow.Git.Repo.Event
|
|
---| "change"
|
|
|
|
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
|
|
---@field gitdir string
|
|
---@field worktree string
|
|
---@field buffers table<integer, ow.Git.Repo.BufState>
|
|
---@field tabs table<integer, true>
|
|
---@field status ow.Git.Status
|
|
---@field private _events ow.Git.Util.Emitter<ow.Git.Repo.Event>
|
|
---@field private _watchers table<string, uv.uv_fs_event_t>
|
|
---@field private _schedule_refresh fun(self: ow.Git.Repo)
|
|
---@field private _refresh_handle ow.Git.Util.DebounceHandle
|
|
---@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 = {}
|
|
Repo.__index = Repo
|
|
|
|
local STATUS_ARGS = {
|
|
"--no-optional-locks",
|
|
"-c",
|
|
"core.quotePath=false",
|
|
"status",
|
|
"--porcelain=v2",
|
|
"--branch",
|
|
"--ignored",
|
|
"--untracked-files=all",
|
|
"-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
|
|
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, {
|
|
cwd = self.worktree,
|
|
on_exit = function(result)
|
|
if epoch ~= self._fetch_epoch then
|
|
return
|
|
end
|
|
if result.code ~= 0 then
|
|
util.error(
|
|
"git status failed: %s",
|
|
vim.trim(result.stderr or "")
|
|
)
|
|
return
|
|
end
|
|
self.status = status.parse(result.stdout or "")
|
|
local change = {
|
|
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
|
|
|
|
---@param opts ow.Git.Repo.RefreshOpts?
|
|
function Repo:refresh(opts)
|
|
if opts and opts.invalidate then
|
|
self._pending_invalidate = true
|
|
end
|
|
self:_schedule_refresh()
|
|
end
|
|
|
|
---@param gitdir string
|
|
---@param worktree string
|
|
---@return ow.Git.Repo
|
|
function Repo.new(gitdir, worktree)
|
|
local self = setmetatable({
|
|
gitdir = gitdir,
|
|
worktree = worktree,
|
|
buffers = {},
|
|
tabs = {},
|
|
status = status.parse(""),
|
|
_events = util.Emitter.new(),
|
|
_cache = {},
|
|
_fetch_epoch = 0,
|
|
_pending_invalidate = false,
|
|
_submodules = {},
|
|
}, Repo)
|
|
self._schedule_refresh, self._refresh_handle =
|
|
util.debounce(Repo._fetch_status, 50)
|
|
self:start_watcher()
|
|
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
|
|
end
|
|
|
|
---@generic T
|
|
---@param key string
|
|
---@param compute fun(self: ow.Git.Repo): T
|
|
---@return T
|
|
function Repo:get_cached(key, compute)
|
|
local hit = self._cache[key]
|
|
if hit ~= nil then
|
|
return hit
|
|
end
|
|
local value = compute(self)
|
|
self._cache[key] = value
|
|
return value
|
|
end
|
|
|
|
---@param path string
|
|
---@param on_event fun(filename: string?)
|
|
---@return uv.uv_fs_event_t?
|
|
local function start_fs_event(path, on_event)
|
|
local watcher = vim.uv.new_fs_event()
|
|
if not watcher then
|
|
return nil
|
|
end
|
|
local ok = watcher:start(path, {}, function(err, filename)
|
|
if err then
|
|
return
|
|
end
|
|
on_event(filename)
|
|
end)
|
|
if not ok then
|
|
watcher:close()
|
|
return nil
|
|
end
|
|
return watcher
|
|
end
|
|
|
|
---@private
|
|
---@param name string
|
|
function Repo:_unregister_submodule(name)
|
|
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
|
|
return
|
|
end
|
|
local stat = vim.uv.fs_stat(path)
|
|
if not stat or stat.type ~= "directory" then
|
|
return
|
|
end
|
|
local watcher = start_fs_event(path, function(filename)
|
|
if not vim.uv.fs_stat(path) then
|
|
local w = self._watchers[path] --[[@as uv.uv_fs_event_t?]]
|
|
if w then
|
|
w:stop()
|
|
w:close()
|
|
self._watchers[path] = nil
|
|
end
|
|
return
|
|
end
|
|
if filename then
|
|
local child = vim.fs.joinpath(relpath, filename)
|
|
self:_handle_fs_event(child)
|
|
vim.schedule(function()
|
|
self:_watch_tree(child)
|
|
end)
|
|
else
|
|
self:refresh({ invalidate = true })
|
|
end
|
|
end)
|
|
if not watcher then
|
|
return
|
|
end
|
|
self._watchers[path] = watcher
|
|
local handle = vim.uv.fs_scandir(path)
|
|
if not handle then
|
|
return
|
|
end
|
|
while true do
|
|
local name, typ = vim.uv.fs_scandir_next(handle)
|
|
if not name then
|
|
break
|
|
end
|
|
if typ == "directory" then
|
|
self:_watch_tree(vim.fs.joinpath(relpath, name))
|
|
end
|
|
end
|
|
end
|
|
|
|
function Repo:start_watcher()
|
|
self._watchers = {}
|
|
local top = start_fs_event(self.gitdir, function(filename)
|
|
if not filename then
|
|
self:refresh({ invalidate = true })
|
|
return
|
|
end
|
|
self:_handle_fs_event(filename)
|
|
end)
|
|
if not top then
|
|
util.error("git: failed to watch %s", self.gitdir)
|
|
return
|
|
end
|
|
self._watchers[self.gitdir] = top
|
|
self:_watch_tree("refs")
|
|
end
|
|
|
|
function Repo:close()
|
|
for _, watcher in pairs(self._watchers) do
|
|
watcher:stop()
|
|
watcher:close()
|
|
end
|
|
self._watchers = {}
|
|
self:_stop_modules_watcher()
|
|
self._refresh_handle.close()
|
|
self._events:clear()
|
|
end
|
|
|
|
---@overload fun(event: "change", fn: fun(change: ow.Git.Repo.Change, status: ow.Git.Status)): fun()
|
|
function Repo:on(event, fn)
|
|
return self._events:on(event, fn)
|
|
end
|
|
|
|
---@param buf? integer
|
|
---@return ow.Git.Repo.BufState?
|
|
function Repo:state(buf)
|
|
return self.buffers[expand_buf(buf)]
|
|
end
|
|
|
|
---@return string?
|
|
function Repo:head()
|
|
return self:get_cached("head", function(self)
|
|
local f = io.open(vim.fs.joinpath(self.gitdir, "HEAD"), "r")
|
|
if not f then
|
|
return nil
|
|
end
|
|
local first = f:read("*l")
|
|
f:close()
|
|
if not first then
|
|
return nil
|
|
end
|
|
local branch = first:match("^ref:%s*refs/heads/(%S+)")
|
|
if branch then
|
|
return branch
|
|
end
|
|
local sha = first:match("^(%x+)")
|
|
if sha then
|
|
return sha:sub(1, 7)
|
|
end
|
|
return nil
|
|
end)
|
|
end
|
|
|
|
---@return string[]
|
|
function Repo:list_refs()
|
|
return self:get_cached("refs", function(self)
|
|
local out = util.git({
|
|
"for-each-ref",
|
|
"--format=%(refname:short)",
|
|
"refs/heads",
|
|
"refs/tags",
|
|
"refs/remotes",
|
|
}, { cwd = self.worktree, silent = true })
|
|
if not out then
|
|
return {}
|
|
end
|
|
return util.split_lines(out)
|
|
end)
|
|
end
|
|
|
|
---@return string[]
|
|
function Repo:list_pseudo_refs()
|
|
return self:get_cached("pseudo_refs", function(self)
|
|
local refs = {}
|
|
for _, name in ipairs(PSEUDO_REFS) do
|
|
if name == "HEAD" or vim.uv.fs_stat(self.gitdir .. "/" .. name) then
|
|
table.insert(refs, name)
|
|
end
|
|
end
|
|
return refs
|
|
end)
|
|
end
|
|
|
|
---@return string[]
|
|
function Repo:list_stash_refs()
|
|
return self:get_cached("stash_refs", function(self)
|
|
if not vim.uv.fs_stat(self.gitdir .. "/refs/stash") then
|
|
return {}
|
|
end
|
|
local refs = { "stash" }
|
|
local out = util.git(
|
|
{ "stash", "list", "--pretty=format:%gd" },
|
|
{ cwd = self.worktree, silent = true }
|
|
)
|
|
if out then
|
|
for _, entry in ipairs(util.split_lines(out)) do
|
|
table.insert(refs, entry)
|
|
end
|
|
end
|
|
return refs
|
|
end)
|
|
end
|
|
|
|
---@param rev string
|
|
---@param short boolean
|
|
---@return string?
|
|
function Repo:rev_parse(rev, short)
|
|
local args = { "rev-parse", "--verify", "--quiet" }
|
|
if short then
|
|
table.insert(args, "--short")
|
|
end
|
|
table.insert(args, rev)
|
|
local stdout = util.git(args, { cwd = self.worktree, silent = true })
|
|
local trimmed = stdout and vim.trim(stdout) or ""
|
|
return trimmed ~= "" and trimmed or nil
|
|
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"
|
|
|
|
---@param abbrev string
|
|
---@return string? full_sha
|
|
---@return ow.Git.Repo.ResolveStatus
|
|
function Repo:resolve_sha(abbrev)
|
|
local result = self:get_cached("resolve:" .. abbrev, function(self)
|
|
local out = util.git(
|
|
{ "rev-parse", "--disambiguate=" .. abbrev },
|
|
{ cwd = self.worktree, silent = true }
|
|
)
|
|
local trimmed = out and vim.trim(out) or ""
|
|
if trimmed == "" then
|
|
return { nil, "missing" }
|
|
end
|
|
local lines = util.split_lines(trimmed)
|
|
if #lines == 1 then
|
|
return { lines[1], "ok" }
|
|
end
|
|
return { nil, "ambiguous" }
|
|
end)
|
|
return result[1], result[2]
|
|
end
|
|
|
|
---@private
|
|
---@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>
|
|
local no_repo_dirs = {}
|
|
|
|
---@overload fun(event: "change", fn: fun(r: ow.Git.Repo, change: ow.Git.Repo.Change, status: ow.Git.Status)): fun()
|
|
function M.on(event, fn)
|
|
return global:on(event, fn)
|
|
end
|
|
|
|
---@param prefix string
|
|
---@param fn fun(buf: integer, r: ow.Git.Repo)
|
|
---@return fun() unsubscribe
|
|
function M.on_uri_change(prefix, fn)
|
|
return M.on("change", function(r)
|
|
for buf in pairs(r.buffers) do
|
|
if vim.api.nvim_buf_is_loaded(buf) then
|
|
local name = vim.api.nvim_buf_get_name(buf)
|
|
if name:sub(1, #prefix) == prefix then
|
|
fn(buf, r)
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
---@return table<string, ow.Git.Repo>
|
|
function M.all()
|
|
return repos
|
|
end
|
|
|
|
---@param buf integer
|
|
---@return ow.Git.Repo?
|
|
local function find_by_buf(buf)
|
|
for _, r in pairs(repos) do
|
|
if r.buffers[buf] then
|
|
return r
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
---@param path string
|
|
---@return ow.Git.Repo?
|
|
local function find_by_path(path)
|
|
if path == "" then
|
|
return nil
|
|
end
|
|
if repos[path] then
|
|
return repos[path]
|
|
end
|
|
local best
|
|
for wt in pairs(repos) do
|
|
if path:sub(1, #wt + 1) == wt .. "/" then
|
|
if not best or #wt > #best then
|
|
best = wt
|
|
end
|
|
end
|
|
end
|
|
return best and repos[best] or nil
|
|
end
|
|
|
|
---@param buf integer
|
|
---@return string
|
|
local function path_for_buf(buf)
|
|
local path = vim.api.nvim_buf_get_name(buf)
|
|
if path == "" or util.is_uri(path) then
|
|
return vim.fn.getcwd()
|
|
end
|
|
return vim.fn.resolve(path)
|
|
end
|
|
|
|
---@param arg? integer | string bufnr (default current) or worktree path
|
|
---@return ow.Git.Repo?
|
|
function M.find(arg)
|
|
if type(arg) == "string" then
|
|
return find_by_path(arg)
|
|
end
|
|
local buf = expand_buf(arg)
|
|
return find_by_buf(buf) or find_by_path(path_for_buf(buf))
|
|
end
|
|
|
|
---@param arg? integer | string bufnr (default current) or worktree path
|
|
---@return ow.Git.Repo?
|
|
function M.resolve(arg)
|
|
if type(arg) ~= "string" then
|
|
local existing = find_by_buf(expand_buf(arg))
|
|
if existing then
|
|
return existing
|
|
end
|
|
end
|
|
local path
|
|
if type(arg) == "string" then
|
|
path = vim.fn.resolve(arg)
|
|
else
|
|
path = path_for_buf(expand_buf(arg))
|
|
end
|
|
local dir = vim.fs.dirname(path)
|
|
if no_repo_dirs[dir] then
|
|
return nil
|
|
end
|
|
local found = vim.fs.find(".git", { upward = true, path = path })[1]
|
|
if not found then
|
|
no_repo_dirs[dir] = true
|
|
return nil
|
|
end
|
|
local worktree = vim.fs.dirname(found)
|
|
if repos[worktree] then
|
|
return repos[worktree]
|
|
end
|
|
local stat = vim.uv.fs_stat(found)
|
|
if not stat then
|
|
return nil
|
|
end
|
|
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.error(".git file at %s has no `gitdir:` line", found)
|
|
return nil
|
|
end
|
|
gitdir = vim.fs.normalize(
|
|
rel:match("^/") and rel or vim.fs.joinpath(worktree, rel)
|
|
)
|
|
end
|
|
local r = Repo.new(gitdir, worktree)
|
|
repos[worktree] = r
|
|
for d in pairs(no_repo_dirs) do
|
|
if d == worktree or vim.startswith(d, worktree .. "/") then
|
|
no_repo_dirs[d] = nil
|
|
end
|
|
end
|
|
return r
|
|
end
|
|
|
|
---@param buf? integer
|
|
---@return ow.Git.Repo.BufState?
|
|
function M.state(buf)
|
|
buf = expand_buf(buf)
|
|
local r = find_by_buf(buf)
|
|
return r and r.buffers[buf]
|
|
end
|
|
|
|
---@param buf? integer
|
|
---@param r ow.Git.Repo
|
|
function M.bind(buf, r)
|
|
buf = expand_buf(buf)
|
|
local prev = find_by_buf(buf)
|
|
if prev == r then
|
|
return
|
|
end
|
|
if prev then
|
|
prev.buffers[buf] = nil
|
|
release_if_unused(prev)
|
|
end
|
|
r.buffers[buf] = { repo = r }
|
|
end
|
|
|
|
---@param buf? integer
|
|
function M.unbind(buf)
|
|
buf = expand_buf(buf)
|
|
local r = find_by_buf(buf)
|
|
if not r then
|
|
return
|
|
end
|
|
r.buffers[buf] = nil
|
|
release_if_unused(r)
|
|
end
|
|
|
|
---@param buf integer
|
|
---@return boolean
|
|
function M.is_worktree_buf(buf)
|
|
if not vim.api.nvim_buf_is_valid(buf) or vim.bo[buf].buftype ~= "" then
|
|
return false
|
|
end
|
|
local path = vim.api.nvim_buf_get_name(buf)
|
|
return path ~= "" and not util.is_uri(path)
|
|
end
|
|
|
|
---@param buf? integer
|
|
function M.track(buf)
|
|
buf = expand_buf(buf)
|
|
if not M.is_worktree_buf(buf) then
|
|
return
|
|
end
|
|
local r = M.resolve(buf)
|
|
if r and not r.buffers[buf] then
|
|
M.bind(buf, r)
|
|
end
|
|
end
|
|
|
|
---@param buf? integer
|
|
function M.refresh(buf)
|
|
local r = find_by_buf(expand_buf(buf))
|
|
if r then
|
|
r:refresh()
|
|
end
|
|
end
|
|
|
|
function M.refresh_all()
|
|
for _, r in pairs(repos) do
|
|
r:refresh()
|
|
end
|
|
end
|
|
|
|
function M.update_cwd_repo()
|
|
no_repo_dirs = {}
|
|
local tab = vim.api.nvim_get_current_tabpage()
|
|
local new = M.resolve(vim.fn.getcwd())
|
|
local old
|
|
for _, r in pairs(repos) do
|
|
if r.tabs[tab] then
|
|
old = r
|
|
break
|
|
end
|
|
end
|
|
if new == old then
|
|
return
|
|
end
|
|
if old then
|
|
old.tabs[tab] = nil
|
|
release_if_unused(old)
|
|
end
|
|
if new then
|
|
new.tabs[tab] = true
|
|
new:refresh()
|
|
end
|
|
end
|
|
|
|
---@param tab integer
|
|
function M.release_tab(tab)
|
|
for _, r in pairs(repos) do
|
|
if r.tabs[tab] then
|
|
r.tabs[tab] = nil
|
|
release_if_unused(r)
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
function M.stop_all()
|
|
for _, r in pairs(repos) do
|
|
r:close()
|
|
end
|
|
end
|
|
|
|
return M
|