From 959cec7051988583e8a7686fc3fbf0bce7364b37 Mon Sep 17 00:00:00 2001 From: Oscar Wallberg Date: Mon, 13 Apr 2026 21:40:52 +0200 Subject: [PATCH] feat(git): add git module and replace vim-flog with :Glog --- after/syntax/git.vim | 20 +++ init.lua | 1 - lua/core/options.lua | 19 +-- lua/git.lua | 298 +++++++++++++++++++++++++++++++++++ lua/plugins/onedark.lua | 3 + lua/plugins/vim-flog.lua | 1 - lua/plugins/vim-fugitive.lua | 15 +- nvim-pack-lock.json | 4 - 8 files changed, 337 insertions(+), 24 deletions(-) create mode 100644 after/syntax/git.vim create mode 100644 lua/git.lua delete mode 100644 lua/plugins/vim-flog.lua diff --git a/after/syntax/git.vim b/after/syntax/git.vim new file mode 100644 index 0000000..442d338 --- /dev/null +++ b/after/syntax/git.vim @@ -0,0 +1,20 @@ +syntax match gitlogGraph contained /^[*|\\\/_ ]*/ + \ nextgroup=gitlogHash +syntax match gitlogHash contained /\<\x\{7,40\}\>/ + \ nextgroup=gitlogDate skipwhite +syntax match gitlogDate contained /\<\d\{4}-\d\{2}-\d\{2}\>/ + \ nextgroup=gitlogAuthor skipwhite +syntax match gitlogAuthor contained /{[^}]\+}/ + \ nextgroup=gitlogRef skipwhite +syntax match gitlogRef contained /([^)]\+)/ +syntax match gitlogLine + \ /^[*|\\\/_ ]*\x\{7,40}\s\+\d\{4}-\d\{2}-\d\{2}\s\+{[^}]\+}.*/ + \ contains=gitlogGraph +syntax match gitlogGraphLine /^[*|\\\/_ ]\+$/ + \ contains=gitlogGraph + +highlight default link gitlogGraph Comment +highlight default link gitlogHash Identifier +highlight default link gitlogDate Number +highlight default link gitlogAuthor String +highlight default link gitlogRef Constant diff --git a/init.lua b/init.lua index 2d780f5..b2f5189 100644 --- a/init.lua +++ b/init.lua @@ -41,7 +41,6 @@ require("pack").setup({ "https://github.com/owallb/mason-auto-install.nvim", "https://github.com/mfussenegger/nvim-dap", "https://github.com/numToStr/Comment.nvim", - "https://github.com/rbong/vim-flog", "https://github.com/tpope/vim-fugitive", "https://github.com/lewis6991/gitsigns.nvim", "https://github.com/MagicDuck/grug-far.nvim", diff --git a/lua/core/options.lua b/lua/core/options.lua index 0bed757..1035d3c 100644 --- a/lua/core/options.lua +++ b/lua/core/options.lua @@ -78,23 +78,8 @@ vim.opt.guicursor:append("a:Cursor") vim.opt.inccommand = "split" vim.opt.winborder = "rounded" -function _G._status_line_git() - local status = vim.b.gitsigns_status_dict - - if not status then - return "" - end - - local added = status.added or 0 - local changed = status.changed or 0 - local removed = status.removed or 0 - - return (added + changed + removed) > 0 - and "%#NvimTreeGitDirty#M%*" - or "" -end - -vim.opt.statusline = " %{expand('%:.')} %{%v:lua._status_line_git()%} %3(%m%)" +require("git").setup() +vim.opt.statusline = "%{expand('%:.')} %{%v:lua.require('git').status()%} %3(%m%)" .. " %=" .. " %{%v:lua.vim.diagnostic.status()%}" .. " %{&filetype} %{&fileencoding} %{&fileformat}" diff --git a/lua/git.lua b/lua/git.lua new file mode 100644 index 0000000..373db9e --- /dev/null +++ b/lua/git.lua @@ -0,0 +1,298 @@ +local log = require("log") +local util = require("util") + +local HIGHLIGHTS = { + GitDeleted = "Statement", + GitDirty = "Statement", + GitIgnored = "Comment", + GitMerge = "Constant", + GitNew = "PreProc", + GitRenamed = "PreProc", + GitStaged = "Constant", +} + +local UNMERGED = { + DD = true, + AU = true, + UD = true, + UA = true, + DU = true, + AA = true, + UU = true, +} + +---@param code string +---@return string? +local function format(code) + if code == "" then + return nil + end + local char, hl + if code == "??" then + char, hl = "?", "GitNew" + elseif code == "!!" then + char, hl = "!", "GitIgnored" + elseif UNMERGED[code] then + char, hl = "U", "GitMerge" + else + local x, y = code:sub(1, 1), code:sub(2, 2) + if x == "R" or y == "R" then + char, hl = "R", "GitRenamed" + elseif y == "M" or y == "T" then + char, hl = "M", "GitDirty" + elseif y == "D" then + char, hl = "D", "GitDeleted" + elseif y == " " and x == "D" then + char, hl = "D", "GitStaged" + elseif y == " " and x == "A" then + char, hl = "A", "GitStaged" + elseif y == " " and x ~= " " then + char, hl = "M", "GitStaged" + else + char, hl = "M", "GitDirty" + end + end + return string.format("%%#%s#%s%%*", hl, char) +end + +---@param path string +---@return string? gitdir +---@return string? worktree +local function resolve(path) + local found = vim.fs.find(".git", { upward = true, path = path })[1] + if not found then + return nil + 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 + 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 + return nil + end + if not gitdir:match("^/") then + gitdir = vim.fs.joinpath(worktree, gitdir) + end + return vim.fs.normalize(gitdir), worktree +end + +---@class ow.Git.Repo +---@field gitdir string +---@field worktree string +---@field buffers integer[] +---@field watcher? uv.uv_fs_event_t +---@field refresh ow.Util.Debouncer +local Repo = {} +Repo.__index = Repo + +function Repo:start_watcher() + local watcher, err_msg, err_name = vim.uv.new_fs_event() + if not watcher then + log.error( + "Failed to create fs event watcher: %s (%s)", + err_msg, + err_name + ) + return + end + watcher:start(self.gitdir, {}, function(err, filename) + if err or (filename ~= "index" and filename ~= "HEAD") then + return + end + self.refresh:call() + end) + self.watcher = watcher +end + +function Repo:stop_watcher() + self.refresh:cancel() + if self.watcher then + self.watcher:stop() + self.watcher:close() + self.watcher = nil + end +end + +---@param buf integer +function Repo:add_buffer(buf) + table.insert(self.buffers, buf) +end + +---@param buf integer +function Repo:remove_buffer(buf) + for i, b in ipairs(self.buffers) do + if b == buf then + table.remove(self.buffers, i) + break + end + end +end + +---@param repo ow.Git.Repo +local function do_refresh(repo) + vim.system({ + "git", + "-c", + "core.quotePath=false", + "status", + "--porcelain=v1", + }, { cwd = repo.worktree, text = true }, function(obj) + vim.schedule(function() + local statuses = {} + if obj.code == 0 then + for line in (obj.stdout or ""):gmatch("[^\r\n]+") do + local code = line:sub(1, 2) + local path_part = line:sub(4) + local arrow = path_part:find(" -> ", 1, true) + if arrow then + path_part = path_part:sub(arrow + 4) + end + statuses[vim.fs.joinpath(repo.worktree, path_part)] = + format(code) + end + end + for _, buf in ipairs(repo.buffers) do + if vim.api.nvim_buf_is_valid(buf) then + local status = statuses[vim.api.nvim_buf_get_name(buf)] + if vim.b[buf].git_status ~= status then + vim.b[buf].git_status = status + vim.cmd.redrawstatus({ bang = true }) + end + end + end + vim.api.nvim_exec_autocmds("User", { + pattern = "GitRefresh", + data = { gitdir = repo.gitdir, worktree = repo.worktree }, + }) + end) + 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 = util.debounce(function() + do_refresh(self) + end, 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 = resolve(path) + if not gitdir or not worktree then + return nil + end + local repo = repo_by_gitdir[gitdir] + if not repo then + repo = Repo.new(gitdir, worktree) + repo_by_gitdir[gitdir] = repo + end + repo:add_buffer(buf) + repo_by_buf[buf] = repo + return repo +end + +---@param buf integer +local function unregister(buf) + local repo = repo_by_buf[buf] + if not repo then + return + end + repo_by_buf[buf] = nil + repo:remove_buffer(buf) + if #repo.buffers == 0 then + repo:stop_watcher() + repo_by_gitdir[repo.gitdir] = nil + end +end + +---@param buf integer +local function refresh(buf) + if not vim.api.nvim_buf_is_valid(buf) or vim.bo[buf].buftype ~= "" then + return + end + local repo = register(buf) + if not repo then + vim.b[buf].git_status = nil + return + end + repo.refresh:call() +end + +local M = {} + +function M.status() + return vim.b.git_status or "" +end + +function M.setup() + for name, link in pairs(HIGHLIGHTS) do + vim.api.nvim_set_hl(0, name, { link = link, default = true }) + end + local group = vim.api.nvim_create_augroup("ow.git", { clear = true }) + vim.api.nvim_create_autocmd( + { "BufReadPost", "BufNewFile", "BufWritePost", "FileChangedShellPost" }, + { + group = group, + callback = function(args) + refresh(args.buf) + end, + } + ) + vim.api.nvim_create_autocmd({ "BufDelete", "BufWipeout" }, { + group = group, + callback = function(args) + unregister(args.buf) + end, + }) + vim.api.nvim_create_autocmd("FocusGained", { + group = group, + callback = function() + refresh(vim.api.nvim_get_current_buf()) + end, + }) + vim.api.nvim_create_autocmd("VimLeavePre", { + group = group, + callback = function() + for _, repo in pairs(repo_by_gitdir) do + repo:stop_watcher() + end + end, + }) +end + +return M diff --git a/lua/plugins/onedark.lua b/lua/plugins/onedark.lua index 216112c..120b7ab 100644 --- a/lua/plugins/onedark.lua +++ b/lua/plugins/onedark.lua @@ -21,6 +21,9 @@ local highlights = { DiffAdd = { bg = "#1a2f22" }, DiffChange = { bg = "#15304a" }, DiffDelete = { bg = "#311c1e" }, + GitDeleted = { fg = c.red }, + GitDirty = { fg = c.yellow }, + GitNew = { fg = c.green }, } require("onedark").set_options("highlights", highlights) require("onedark").load() diff --git a/lua/plugins/vim-flog.lua b/lua/plugins/vim-flog.lua deleted file mode 100644 index ee643db..0000000 --- a/lua/plugins/vim-flog.lua +++ /dev/null @@ -1 +0,0 @@ -vim.keymap.set("n", "gl", vim.cmd.Flog) diff --git a/lua/plugins/vim-fugitive.lua b/lua/plugins/vim-fugitive.lua index 759c5b1..81a061d 100644 --- a/lua/plugins/vim-fugitive.lua +++ b/lua/plugins/vim-fugitive.lua @@ -30,6 +30,17 @@ local function toggle_git_status() open_git_status() end +vim.api.nvim_create_user_command("Glog", function(opts) + local mods = opts.mods ~= "" and (opts.mods .. " ") or "" + vim.cmd( + mods + .. "Git log --graph --all --decorate --date=short " + .. "--format=format:'%h %ad {%an}%d %s' " + .. opts.args + ) +end, { nargs = "*", desc = "Pretty git log via fugitive" }) + +vim.keymap.set("n", "gl", vim.cmd.Glog) vim.keymap.set("n", "gd", vim.cmd.Gvdiffsplit) vim.keymap.set("n", "gD", function() vim.cmd.Gvdiffsplit("HEAD") end) vim.keymap.set("n", "gh", vim.cmd.Ghdiffsplit) @@ -39,7 +50,9 @@ vim.keymap.set("n", "ga", function() vim.cmd.G("commit --amend") end) vim.keymap.set("n", "gp", function() vim.cmd.G("push") end) vim.keymap.set("n", "gg", toggle_git_status) -vim.api.nvim_create_autocmd("BufWritePost", { +vim.api.nvim_create_autocmd("User", { + pattern = "GitRefresh", + group = vim.api.nvim_create_augroup("ow.fugitive", { clear = true }), callback = function() vim.fn["fugitive#ReloadStatus"]() end, diff --git a/nvim-pack-lock.json b/nvim-pack-lock.json index ab427a0..fbc52a8 100644 --- a/nvim-pack-lock.json +++ b/nvim-pack-lock.json @@ -154,10 +154,6 @@ "rev": "484e2889f3619b9da90c9b73a6f216a71947c09f", "src": "https://github.com/georgeharker/tree-sitter-zsh" }, - "vim-flog": { - "rev": "665b16ac8915f746bc43c9572b4581a5e9047216", - "src": "https://github.com/rbong/vim-flog" - }, "vim-fugitive": { "rev": "3b753cf8c6a4dcde6edee8827d464ba9b8c4a6f0", "src": "https://github.com/tpope/vim-fugitive"