Skip to content

Instantly share code, notes, and snippets.

@bassamsdata
Last active May 24, 2024 10:06
Show Gist options
  • Save bassamsdata/eec0a3065152226581f8d4244cce9051 to your computer and use it in GitHub Desktop.
Save bassamsdata/eec0a3065152226581f8d4244cce9051 to your computer and use it in GitHub Desktop.
MiniFiles Git integration

Below is a code for Minifiles Git integration code snippet.

How to use it

Just insert the code below into this function in your Minifiles config:

config = function()
-- add the git code here
end

Screenshot:

Screenshot 2024-04-16 at 9 53 57 PM

Some Notes:

  • It requires the latest version of mini.files.
  • it requires neovim v0.10.0 or later, for previous versions please check the revison of this gist for function(fetchGitStatus) specifically.
  • it works on mac, linux or windows.
  • The shell command git status is executed once per Minifiles session for performance reasons, leveraging simple cache integration.
  • the code is efficient and shell command executes asyncronously for performance optimization.
  • You have the option to change symbols and highlight groups to GitSigns if preferred. Currently, it's using Mini.Diff.
  • If you prefer symbols on the right, they're commented out. Refer to the NOTE in the code.

TODOs and some limitation:

  • Git ignore support isn't implemented yet, but it's feasible and might be added in the future.
  • It doesn't check for Git outside of the current working directory (cwd) due to caching considerations. This might be revisited in the future.
  • currently, it doesn't work if preview was on
  • The code will be simpler and more efficient when this issue echasnovski/mini.nvim#817 is resolved.

NOTE:

  • I'm open to feedback, suggestions, or even criticism.
  • If you have a better idea for implementation, please share!

Thanks:

local nsMiniFiles = vim.api.nvim_create_namespace("mini_files_git")
local autocmd = vim.api.nvim_create_autocmd
local _, MiniFiles = pcall(require, "mini.files")
-- Cache for git status
local gitStatusCache = {}
local cacheTimeout = 2000 -- Cache timeout in milliseconds
local function mapSymbols(status)
local statusMap = {
-- stylua: ignore start
[" M"] = { symbol = "•", hlGroup = "MiniDiffSignChange"}, -- Modified in the working directory
["M "] = { symbol = "✹", hlGroup = "MiniDiffSignChange"}, -- modified in index
["MM"] = { symbol = "≠", hlGroup = "MiniDiffSignChange"}, -- modified in both working tree and index
["A "] = { symbol = "+", hlGroup = "MiniDiffSignAdd" }, -- Added to the staging area, new file
["AA"] = { symbol = "≈", hlGroup = "MiniDiffSignAdd" }, -- file is added in both working tree and index
["D "] = { symbol = "-", hlGroup = "MiniDiffSignDelete"}, -- Deleted from the staging area
["AM"] = { symbol = "⊕", hlGroup = "MiniDiffSignChange"}, -- added in working tree, modified in index
["AD"] = { symbol = "-•", hlGroup = "MiniDiffSignChange"}, -- Added in the index and deleted in the working directory
["R "] = { symbol = "→", hlGroup = "MiniDiffSignChange"}, -- Renamed in the index
["U "] = { symbol = "‖", hlGroup = "MiniDiffSignChange"}, -- Unmerged path
["UU"] = { symbol = "⇄", hlGroup = "MiniDiffSignAdd" }, -- file is unmerged
["UA"] = { symbol = "⊕", hlGroup = "MiniDiffSignAdd" }, -- file is unmerged and added in working tree
["??"] = { symbol = "?", hlGroup = "MiniDiffSignDelete"}, -- Untracked files
["!!"] = { symbol = "!", hlGroup = "MiniDiffSignChange"}, -- Ignored files
-- stylua: ignore end
}
local result = statusMap[status]
or { symbol = "?", hlGroup = "NonText" }
return result.symbol, result.hlGroup
end
---@param cwd string
---@param callback function
local function fetchGitStatus(cwd, callback)
local function on_exit(content)
if content.code == 0 then
callback(content.stdout)
vim.g.content = content.stdout
end
end
vim.system(
{ "git", "status", "--ignored", "--porcelain" },
{ text = true, cwd = cwd },
on_exit
)
end
local function escapePattern(str)
return str:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1")
end
local function updateMiniWithGit(buf_id, gitStatusMap)
vim.schedule(function()
local nlines = vim.api.nvim_buf_line_count(buf_id)
local cwd = vim.fn.getcwd() -- vim.fn.expand("%:p:h")
local escapedcwd = escapePattern(cwd)
if vim.fn.has("win32") == 1 then
escapedcwd = escapedcwd:gsub("\\", "/")
end
for i = 1, nlines do
local entry = MiniFiles.get_fs_entry(buf_id, i)
if not entry then
break
end
local relativePath = entry.path:gsub("^" .. escapedcwd .. "/", "")
local status = gitStatusMap[relativePath]
if status then
local symbol, hlGroup = mapSymbols(status)
vim.api.nvim_buf_set_extmark(buf_id, nsMiniFiles, i - 1, 0, {
-- NOTE: if you want the signs on the right uncomment those and comment
-- the 3 lines after
-- virt_text = { { symbol, hlGroup } },
-- virt_text_pos = "right_align",
sign_text = symbol,
sign_hl_group = hlGroup,
priority = 2,
})
else
end
end
end)
end
local function is_valid_git_repo()
if vim.fn.isdirectory(".git") == 0 then
return false
end
return true
end
-- Thanks for the idea of gettings https://github.com/refractalize/oil-git-status.nvim signs for dirs
local function parseGitStatus(content)
local gitStatusMap = {}
-- lua match is faster than vim.split (in my experience )
for line in content:gmatch("[^\r\n]+") do
local status, filePath = string.match(line, "^(..)%s+(.*)")
-- Split the file path into parts
local parts = {}
for part in filePath:gmatch("[^/]+") do
table.insert(parts, part)
end
-- Start with the root directory
local currentKey = ""
for i, part in ipairs(parts) do
if i > 1 then
-- Concatenate parts with a separator to create a unique key
currentKey = currentKey .. "/" .. part
else
currentKey = part
end
-- If it's the last part, it's a file, so add it with its status
if i == #parts then
gitStatusMap[currentKey] = status
else
-- If it's not the last part, it's a directory. Check if it exists, if not, add it.
if not gitStatusMap[currentKey] then
gitStatusMap[currentKey] = status
end
end
end
end
return gitStatusMap
end
local function updateGitStatus(buf_id)
if not is_valid_git_repo() then
return
end
local cwd = vim.fn.expand("%:p:h")
local currentTime = os.time()
if
gitStatusCache[cwd]
and currentTime - gitStatusCache[cwd].time < cacheTimeout
then
updateMiniWithGit(buf_id, gitStatusCache[cwd].statusMap)
else
fetchGitStatus(cwd, function(content)
local gitStatusMap = parseGitStatus(content)
gitStatusCache[cwd] = {
time = currentTime,
statusMap = gitStatusMap,
}
updateMiniWithGit(buf_id, gitStatusMap)
end)
end
end
local function clearCache()
gitStatusCache = {}
end
local function augroup(name)
return vim.api.nvim_create_augroup(
"MiniFiles_" .. name,
{ clear = true }
)
end
autocmd("User", {
group = augroup("start"),
pattern = "MiniFilesExplorerOpen",
-- pattern = { "minifiles" },
callback = function()
local bufnr = vim.api.nvim_get_current_buf()
updateGitStatus(bufnr)
end,
})
autocmd("User", {
group = augroup("close"),
pattern = "MiniFilesExplorerClose",
callback = function()
clearCache()
end,
})
autocmd("User", {
group = augroup("update"),
pattern = "MiniFilesBufferUpdate",
callback = function(sii)
local bufnr = sii.data.buf_id
local cwd = vim.fn.expand("%:p:h")
if gitStatusCache[cwd] then
updateMiniWithGit(bufnr, gitStatusCache[cwd].statusMap)
end
end,
})
@WizardStark
Copy link

Thanks for this, its something I've been missing. Some updates that I made to suite my usecase - I often work in large repos and then my cwd != git root, but I would still like to see the git status in this case, so just tweaking updateMiniWithGit and updateGitStatus as follows, solved this:

local function updateMiniWithGit(buf_id, gitStatusMap)
	local MiniFiles = require("mini.files")
	vim.schedule(function()
		local nlines = vim.api.nvim_buf_line_count(buf_id)
		local git_root = vim.trim(vim.fn.system("git rev-parse --show-toplevel"))
		local escapedcwd = escapePattern(git_root)
		if vim.fn.has("win32") == 1 then
			escapedcwd = escapedcwd:gsub("\\", "/")
		end

		for i = 1, nlines do
			local entry = MiniFiles.get_fs_entry(buf_id, i)
			if not entry then
				break
			end
			local relativePath = entry.path:gsub("^" .. escapedcwd .. "/", "")
			local status = gitStatusMap[relativePath]

			if status then
				local symbol, hlGroup = mapSymbols(status)
				vim.api.nvim_buf_set_extmark(buf_id, nsMiniFiles, i - 1, 0, {
					-- NOTE: if you want the signs on the right uncomment those and comment
					-- the 3 lines after
					-- virt_text = { { symbol, hlGroup } },
					-- virt_text_pos = "right_align",
					sign_text = symbol,
					sign_hl_group = hlGroup,
					priority = 2,
				})
			else
			end
		end
	end)
end

local function updateGitStatus(buf_id)
	if vim.fn.system("git rev-parse --show-toplevel 2> /dev/null") == "" then
		vim.notify("Not a valid git repo")
		return
	end
	local cwd = vim.fn.expand("%:p:h")
	local currentTime = os.time()
	if gitStatusCache[cwd] and currentTime - gitStatusCache[cwd].time < cacheTimeout then
		updateMiniWithGit(buf_id, gitStatusCache[cwd].statusMap)
	else
		fetchGitStatus(cwd, function(content)
			local gitStatusMap = parseGitStatus(content)
			gitStatusCache[cwd] = {
				time = currentTime,
				statusMap = gitStatusMap,
			}
			updateMiniWithGit(buf_id, gitStatusMap)
		end)
	end
end

@bassamsdata
Copy link
Author

Thanks @WizardStark
That's a good point, and I considered it when I implemented it. However, I opted for my solution because I thought most people wouldn't start Neovim from a subdirectory inside a Git repo. But I understand that people have different use cases.

One suggestion, if you're using Neovim nightly, is to utilize the Neovim official API instead of directly calling Git.
For instance, instead of:

local git_root = vim.trim(vim.fn.system("git rev-parse --show-toplevel")) 

You can use:

local root_dir = vim.fs.root(vim.uv.cwd(), ".git")

Similarly, instead of:

if vim.fn.system("git rev-parse --show-toplevel 2> /dev/null") == "" then

You can use:

if not vim.fs.root(vim.uv.cwd(), ".git") then

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment