Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 101 additions & 10 deletions lua/lualine/components/branch/git_branch.lua
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
local M = {}

local require = require('lualine_require').require
local utils = require('lualine.utils.utils')
local lualine_require = require('lualine_require')
local modules = lualine_require.lazy_require {
utils = 'lualine.utils.utils',
Job = 'lualine.utils.job',
}

-- vars
local current_git_branch = ''
local current_git_dir = ''
local current_git_dir_is_reftable = false
local branch_cache = {} -- stores last known branch for a buffer
local active_bufnr = '0'
-- os specific path separator
Expand All @@ -15,6 +19,17 @@ local sep = package.config:sub(1, 1)
-- Windows doesn't like file watch for some reason.
local file_changed = sep ~= '\\' and vim.loop.new_fs_event() or vim.loop.new_fs_poll()
local git_dir_cache = {} -- Stores git paths that we already know of
---job handle for async git commands (reftable repos)
local branch_job = nil

---checks if git directory uses reftable format
---@param git_dir string full path to .git directory
---@return boolean
local function is_reftable_repo(git_dir)
local reftable_dir = git_dir .. sep .. 'reftable'
local stat = vim.loop.fs_stat(reftable_dir)
return stat ~= nil and stat.type == 'directory'
end

---sets git_branch variable to branch name or commit hash if not on branch
---@param head_file string full path of .git/HEAD file
Expand All @@ -33,16 +48,86 @@ local function get_git_head(head_file)
return nil
end

---gets short commit hash for detached HEAD in reftable repos (async)
---@param git_dir string full path to .git directory
---@param bufnr number buffer number to update cache for
local function get_branch_reftable_hash(git_dir, bufnr)
if branch_job then
branch_job:stop()
end

local output = {}
branch_job = modules.Job {
cmd = { 'git', '--git-dir=' .. git_dir, 'rev-parse', '--short', 'HEAD' },
on_stdout = function(_, data)
if data then
output = vim.list_extend(output, data)
end
end,
on_exit = function(_, code)
if code == 0 and #output > 0 then
current_git_branch = vim.trim(table.concat(output, ''))
else
current_git_branch = ''
end
branch_cache[bufnr] = current_git_branch
end,
}
branch_job:start()
end

---gets branch name for reftable repos using git command (async)
---@param git_dir string full path to .git directory
---@param bufnr number buffer number to update cache for
local function get_branch_reftable(git_dir, bufnr)
if branch_job then
branch_job:stop()
end

local output = {}
branch_job = modules.Job {
cmd = { 'git', '--git-dir=' .. git_dir, 'symbolic-ref', '--short', 'HEAD' },
on_stdout = function(_, data)
if data then
output = vim.list_extend(output, data)
end
end,
on_exit = function(_, code)
if code == 0 and #output > 0 then
local branch = vim.trim(table.concat(output, ''))
if #branch > 0 then
current_git_branch = branch
branch_cache[bufnr] = current_git_branch
return
end
end
-- Detached HEAD or error - try rev-parse for short commit hash
get_branch_reftable_hash(git_dir, bufnr)
end,
}
branch_job:start()
end

---updates the current value of git_branch and sets up file watch on HEAD file
local function update_branch()
active_bufnr = tostring(vim.api.nvim_get_current_buf())
local bufnr = vim.api.nvim_get_current_buf()
active_bufnr = tostring(bufnr)
file_changed:stop()
local git_dir = current_git_dir
if git_dir and #git_dir > 0 then
local head_file = git_dir .. sep .. 'HEAD'
get_git_head(head_file)
local watch_file
if current_git_dir_is_reftable then
-- Reftable format: use git command to get branch name
get_branch_reftable(git_dir, bufnr)
watch_file = git_dir .. sep .. 'reftable' .. sep .. 'tables.list'
else
-- Normal git format: read HEAD file directly
local head_file = git_dir .. sep .. 'HEAD'
get_git_head(head_file)
watch_file = head_file
end
file_changed:start(
head_file,
watch_file,
sep ~= '\\' and {} or 1000,
vim.schedule_wrap(function()
-- reset file-watch
Expand All @@ -53,13 +138,14 @@ local function update_branch()
-- set to '' when git dir was not found
current_git_branch = ''
end
branch_cache[vim.api.nvim_get_current_buf()] = current_git_branch
branch_cache[bufnr] = current_git_branch
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: addresses slight race condition potential

end

---updates the current value of current_git_branch and sets up file watch on HEAD file if value changed
local function update_current_git_dir(git_dir)
if current_git_dir ~= git_dir then
current_git_dir = git_dir
current_git_dir_is_reftable = git_dir and is_reftable_repo(git_dir) or false
update_branch()
end
end
Expand Down Expand Up @@ -116,12 +202,17 @@ function M.find_git_dir(dir_path)
end
end
if git_dir then
-- Check for traditional HEAD file or reftable format
local head_file_stat = vim.loop.fs_stat(git_dir .. sep .. 'HEAD')
if head_file_stat and head_file_stat.type == 'file' then
break
else
git_dir = nil
end
-- Also accept reftable repos (they may not have a traditional HEAD file)
local reftable_stat = vim.loop.fs_stat(git_dir .. sep .. 'reftable' .. sep .. 'tables.list')
if reftable_stat and reftable_stat.type == 'file' then
break
end
git_dir = nil
end
end
root_dir = root_dir:match('(.*)' .. sep .. '.-')
Expand All @@ -139,7 +230,7 @@ function M.init()
-- run watch head on load so branch is present when component is loaded
M.find_git_dir()
-- update branch state of BufEnter as different Buffer may be on different repos
utils.define_autocmd('BufEnter', "lua require'lualine.components.branch.git_branch'.find_git_dir()")
modules.utils.define_autocmd('BufEnter', "lua require'lualine.components.branch.git_branch'.find_git_dir()")
end
function M.get_branch(bufnr)
if vim.g.actual_curbuf ~= nil and active_bufnr ~= vim.g.actual_curbuf then
Expand Down