local WIN = ya.target_family() == "windows" local PATS = { { "[MT]", 6 }, -- Modified { "[AC]", 5 }, -- Added { "?$", 4 }, -- Untracked { "!$", 3 }, -- Ignored { "D", 2 }, -- Deleted { "U", 1 }, -- Updated { "[AD][AD]", 1 }, -- Updated } local function match(line) local signs = line:sub(1, 2) for _, p in ipairs(PATS) do local path if signs:find(p[1]) then path = line:sub(4, 4) == '"' and line:sub(5, -2) or line:sub(4) path = WIN and path:gsub("/", "\\") or path end if not path then elseif path:find("[/\\]$") then return p[2] == 3 and 30 or p[2], path:sub(1, -2) else return p[2], 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 k, v in pairs(changed) do if v ~= 3 and v ~= 30 then local url = Url(k):parent() while url and url ~= empty do local s = tostring(url) new[s] = (new[s] or 0) > v and new[s] or v url = url:parent() end end end return new end local function propagate_down(ignored, cwd, repo) local new, rel = {}, cwd:strip_prefix(repo) for k, v in pairs(ignored) do if v == 30 then if rel:starts_with(k) then new[tostring(repo:join(rel))] = 30 elseif cwd == repo:join(k):parent() then new[k] = 3 end 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 k, v in pairs(changed) do if v == 0 then st.repos[repo][k] = nil elseif v == 30 then st.dirs[k] = "" else st.repos[repo][k] = v end end ya.render() end) local remove = ya.sync(function(st, cwd) local dir = st.dirs[cwd] if not dir then return end ya.render() st.dirs[cwd] = nil if not st.repos[dir] then return end for _, r in pairs(st.dirs) do if r == dir then return end end st.repos[dir] = nil end) local function setup(st, opts) st.dirs = {} st.repos = {} opts = opts or {} opts.order = opts.order or 1500 -- Chosen by ChatGPT fairly, PRs are welcome to adjust them local t = THEME.git or {} local styles = { [6] = t.modified and ui.Style(t.modified) or ui.Style():fg("#ffa500"), [5] = t.added and ui.Style(t.added) or ui.Style():fg("#32cd32"), [4] = t.untracked and ui.Style(t.untracked) or ui.Style():fg("#a9a9a9"), [3] = t.ignored and ui.Style(t.ignored) or ui.Style():fg("#696969"), [2] = t.deleted and ui.Style(t.deleted) or ui.Style():fg("#ff4500"), [1] = t.updated and ui.Style(t.updated) or ui.Style():fg("#1e90ff"), } local signs = { [6] = t.modified_sign and t.modified_sign or "", [5] = t.added_sign and t.added_sign or "", [4] = t.untracked_sign and t.untracked_sign or "", [3] = t.ignored_sign and t.ignored_sign or "", [2] = t.deleted_sign and t.deleted_sign or "", [1] = t.updated_sign and t.updated_sign or "U", } Linemode:children_add(function(self) local url = self._file.url local dir = st.dirs[tostring(url:parent())] local change if dir then change = dir == "" and 3 or st.repos[dir][tostring(url):sub(#dir + 2)] end if not change or signs[change] == "" then return ui.Line("") elseif self._file:is_hovered() then return ui.Line { ui.Span(" "), ui.Span(signs[change]) } else return ui.Line { ui.Span(" "), ui.Span(signs[change]):style(styles[change]) } end end, opts.order) end local function fetch(self) local cwd = self.files[1].url:parent() local repo = root(cwd) if not repo then remove(tostring(cwd)) return 1 end local paths = {} for _, f in ipairs(self.files) do paths[#paths + 1] = tostring(f.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 ya.err("Cannot spawn git command, error code " .. tostring(err)) return 0 end local changed, ignored = {}, {} for line in output.stdout:gmatch("[^\r\n]+") do local sign, path = match(line) if sign == 30 then ignored[path] = sign else changed[path] = sign end end if self.files[1].cha.is_dir then ya.dict_merge(changed, bubble_up(changed)) ya.dict_merge(changed, propagate_down(ignored, cwd, Url(repo))) else ya.dict_merge(changed, propagate_down(ignored, cwd, Url(repo))) end for _, p in ipairs(paths) do local s = p:sub(#repo + 2) changed[s] = changed[s] or 0 end add(tostring(cwd), repo, changed) return 3 end return { setup = setup, fetch = fetch }