mirror of
https://github.com/hydroxycarbamide/dotfiles.git
synced 2025-05-04 20:09:08 -04:00
228 lines
6 KiB
Lua
228 lines
6 KiB
Lua
--- @since 25.4.4
|
|
|
|
local WINDOWS = ya.target_family() == "windows"
|
|
|
|
-- The code of supported git status,
|
|
-- also used to determine which status to show for directories when they contain different statuses
|
|
-- see `bubble_up`
|
|
local CODES = {
|
|
excluded = 100, -- ignored directory
|
|
ignored = 6, -- ignored file
|
|
untracked = 5,
|
|
modified = 4,
|
|
added = 3,
|
|
deleted = 2,
|
|
updated = 1,
|
|
unknown = 0,
|
|
}
|
|
|
|
local PATTERNS = {
|
|
{ "!$", CODES.ignored },
|
|
{ "?$", CODES.untracked },
|
|
{ "[MT]", CODES.modified },
|
|
{ "[AC]", CODES.added },
|
|
{ "D", CODES.deleted },
|
|
{ "U", CODES.updated },
|
|
{ "[AD][AD]", CODES.updated },
|
|
}
|
|
|
|
local function match(line)
|
|
local signs = line:sub(1, 2)
|
|
for _, p in ipairs(PATTERNS) do
|
|
local path, pattern, code = nil, p[1], p[2]
|
|
if signs:find(pattern) then
|
|
path = line:sub(4, 4) == '"' and line:sub(5, -2) or line:sub(4)
|
|
path = WINDOWS and path:gsub("/", "\\") or path
|
|
end
|
|
if not path then
|
|
elseif path:find("[/\\]$") then
|
|
-- Mark the ignored directory as `excluded`, so we can process it further within `propagate_down`
|
|
return code == CODES.ignored and CODES.excluded or code, path:sub(1, -2)
|
|
else
|
|
return code, path
|
|
end
|
|
end
|
|
end
|
|
|
|
local function root(cwd)
|
|
local is_worktree = function(url)
|
|
local file, head = io.open(tostring(url)), nil
|
|
if file then
|
|
head = file:read(8)
|
|
file:close()
|
|
end
|
|
return head == "gitdir: "
|
|
end
|
|
|
|
repeat
|
|
local next = cwd:join(".git")
|
|
local cha = fs.cha(next)
|
|
if cha and (cha.is_dir or is_worktree(next)) then
|
|
return tostring(cwd)
|
|
end
|
|
cwd = cwd.parent
|
|
until not cwd
|
|
end
|
|
|
|
local function bubble_up(changed)
|
|
local new, empty = {}, Url("")
|
|
for path, code in pairs(changed) do
|
|
if code ~= CODES.ignored then
|
|
local url = Url(path).parent
|
|
while url and url ~= empty do
|
|
local s = tostring(url)
|
|
new[s] = (new[s] or CODES.unknown) > code and new[s] or code
|
|
url = url.parent
|
|
end
|
|
end
|
|
end
|
|
return new
|
|
end
|
|
|
|
local function propagate_down(excluded, cwd, repo)
|
|
local new, rel = {}, cwd:strip_prefix(repo)
|
|
for _, path in ipairs(excluded) do
|
|
if rel:starts_with(path) then
|
|
-- If `cwd` is a subdirectory of an excluded directory, also mark it as `excluded`
|
|
new[tostring(cwd)] = CODES.excluded
|
|
elseif cwd == repo:join(path).parent then
|
|
-- If `path` is a direct subdirectory of `cwd`, mark it as `ignored`
|
|
new[path] = CODES.ignored
|
|
else
|
|
-- Skipping, we only care about `cwd` itself and its direct subdirectories for maximum performance
|
|
end
|
|
end
|
|
return new
|
|
end
|
|
|
|
local add = ya.sync(function(st, cwd, repo, changed)
|
|
st.dirs[cwd] = repo
|
|
st.repos[repo] = st.repos[repo] or {}
|
|
for path, code in pairs(changed) do
|
|
if code == CODES.unknown then
|
|
st.repos[repo][path] = nil
|
|
elseif code == CODES.excluded then
|
|
-- Mark the directory with a special value `excluded` so that it can be distinguished during UI rendering
|
|
st.dirs[path] = CODES.excluded
|
|
else
|
|
st.repos[repo][path] = code
|
|
end
|
|
end
|
|
ya.render()
|
|
end)
|
|
|
|
local remove = ya.sync(function(st, cwd)
|
|
local repo = st.dirs[cwd]
|
|
if not repo then
|
|
return
|
|
end
|
|
|
|
ya.render()
|
|
st.dirs[cwd] = nil
|
|
if not st.repos[repo] then
|
|
return
|
|
end
|
|
|
|
for _, r in pairs(st.dirs) do
|
|
if r == repo then
|
|
return
|
|
end
|
|
end
|
|
st.repos[repo] = nil
|
|
end)
|
|
|
|
local function setup(st, opts)
|
|
st.dirs = {} -- Mapping between a directory and its corresponding repository
|
|
st.repos = {} -- Mapping between a repository and the status of each of its files
|
|
|
|
opts = opts or {}
|
|
opts.order = opts.order or 1500
|
|
|
|
local t = th.git or {}
|
|
local styles = {
|
|
[CODES.ignored] = t.ignored and ui.Style(t.ignored) or ui.Style():fg("darkgray"),
|
|
[CODES.untracked] = t.untracked and ui.Style(t.untracked) or ui.Style():fg("magenta"),
|
|
[CODES.modified] = t.modified and ui.Style(t.modified) or ui.Style():fg("yellow"),
|
|
[CODES.added] = t.added and ui.Style(t.added) or ui.Style():fg("green"),
|
|
[CODES.deleted] = t.deleted and ui.Style(t.deleted) or ui.Style():fg("red"),
|
|
[CODES.updated] = t.updated and ui.Style(t.updated) or ui.Style():fg("yellow"),
|
|
}
|
|
local signs = {
|
|
[CODES.ignored] = t.ignored_sign or "",
|
|
[CODES.untracked] = t.untracked_sign or "?",
|
|
[CODES.modified] = t.modified_sign or "",
|
|
[CODES.added] = t.added_sign or "",
|
|
[CODES.deleted] = t.deleted_sign or "",
|
|
[CODES.updated] = t.updated_sign or "",
|
|
}
|
|
|
|
Linemode:children_add(function(self)
|
|
local url = self._file.url
|
|
local repo = st.dirs[tostring(url.base)]
|
|
local code
|
|
if repo then
|
|
code = repo == CODES.excluded and CODES.ignored or st.repos[repo][tostring(url):sub(#repo + 2)]
|
|
end
|
|
|
|
if not code or signs[code] == "" then
|
|
return ""
|
|
elseif self._file.is_hovered then
|
|
return ui.Line { " ", signs[code] }
|
|
else
|
|
return ui.Line { " ", ui.Span(signs[code]):style(styles[code]) }
|
|
end
|
|
end, opts.order)
|
|
end
|
|
|
|
local function fetch(_, job)
|
|
local cwd = job.files[1].url.base
|
|
local repo = root(cwd)
|
|
if not repo then
|
|
remove(tostring(cwd))
|
|
return true
|
|
end
|
|
|
|
local paths = {}
|
|
for _, file in ipairs(job.files) do
|
|
paths[#paths + 1] = tostring(file.url)
|
|
end
|
|
|
|
-- stylua: ignore
|
|
local output, err = Command("git")
|
|
:cwd(tostring(cwd))
|
|
:args({ "--no-optional-locks", "-c", "core.quotePath=", "status", "--porcelain", "-unormal", "--no-renames", "--ignored=matching" })
|
|
:args(paths)
|
|
:stdout(Command.PIPED)
|
|
:output()
|
|
if not output then
|
|
return true, Err("Cannot spawn `git` command, error: %s", err)
|
|
end
|
|
|
|
local changed, excluded = {}, {}
|
|
for line in output.stdout:gmatch("[^\r\n]+") do
|
|
local code, path = match(line)
|
|
if code == CODES.excluded then
|
|
excluded[#excluded + 1] = path
|
|
else
|
|
changed[path] = code
|
|
end
|
|
end
|
|
|
|
if job.files[1].cha.is_dir then
|
|
ya.dict_merge(changed, bubble_up(changed))
|
|
end
|
|
ya.dict_merge(changed, propagate_down(excluded, cwd, Url(repo)))
|
|
|
|
-- Reset the status of any files that don't appear in the output of `git status` to `unknown`,
|
|
-- so that cleaning up outdated statuses from `st.repos`
|
|
for _, path in ipairs(paths) do
|
|
local s = path:sub(#repo + 2)
|
|
changed[s] = changed[s] or CODES.unknown
|
|
end
|
|
|
|
add(tostring(cwd), repo, changed)
|
|
|
|
return false
|
|
end
|
|
|
|
return { setup = setup, fetch = fetch }
|