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 ---@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 local repo_by_gitdir = {} ---@type table 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