Skip to content

Instantly share code, notes, and snippets.

@bassamsdata
Last active April 21, 2025 07:46
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 2025-04-09 at 3 53 38 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!

Last Update:

29/03/2025

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 -- in milliseconds
local uv = vim.uv or vim.loop
local function isSymlink(path)
local stat = uv.fs_lstat(path)
return stat and stat.type == "link"
end
---@type table<string, {symbol: string, hlGroup: string}>
---@param status string
---@return string symbol, string hlGroup
local function mapSymbols(status, is_symlink)
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" }
local gitSymbol = result.symbol
local gitHlGroup = result.hlGroup
local symlinkSymbol = is_symlink and "" or ""
-- Combine symlink symbol with Git status if both exist
local combinedSymbol = (symlinkSymbol .. gitSymbol)
:gsub("^%s+", "")
:gsub("%s+$", "")
-- Change the color of the symlink icon from "MiniDiffSignDelete" to something else
local combinedHlGroup = is_symlink and "MiniDiffSignDelete" or gitHlGroup
return combinedSymbol, combinedHlGroup
end
---@param cwd string
---@param callback function
---@return nil
local function fetchGitStatus(cwd, callback)
local clean_cwd = cwd:gsub("^minifiles://%d+/", "")
---@param content table
local function on_exit(content)
if content.code == 0 then
callback(content.stdout)
-- vim.g.content = content.stdout
end
end
---@see vim.system
vim.system(
{ "git", "status", "--ignored", "--porcelain" },
{ text = true, cwd = clean_cwd },
on_exit
)
end
---@param buf_id integer
---@param gitStatusMap table
---@return nil
local function updateMiniWithGit(buf_id, gitStatusMap)
vim.schedule(function()
local nlines = vim.api.nvim_buf_line_count(buf_id)
local cwd = vim.fs.root(buf_id, ".git")
local escapedcwd = cwd and vim.pesc(cwd)
escapedcwd = vim.fs.normalize(escapedcwd)
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, isSymlink(entry.path))
vim.api.nvim_buf_set_extmark(buf_id, nsMiniFiles, i - 1, 0, {
sign_text = symbol,
sign_hl_group = hlGroup,
priority = 2,
})
-- This below code is responsible for coloring the text of the items. comment it out if you don't want that
local line = vim.api.nvim_buf_get_lines(buf_id, i - 1, i, false)[1]
-- Find the name position accounting for potential icons
local nameStartCol = line:find(vim.pesc(entry.name)) or 0
if nameStartCol > 0 then
vim.api.nvim_buf_set_extmark(
buf_id,
nsMiniFiles,
i - 1,
nameStartCol - 1,
{
end_col = nameStartCol + #entry.name - 1,
hl_group = hlGroup,
}
)
end
else
end
end
end)
end
-- Thanks for the idea of gettings https://github.com/refractalize/oil-git-status.nvim signs for dirs
---@param content string
---@return table
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
---@param buf_id integer
---@return nil
local function updateGitStatus(buf_id)
if not vim.fs.root(buf_id, ".git") then
return
end
local cwd = vim.fs.root(buf_id, ".git")
-- 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
---@return nil
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",
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(args)
local bufnr = args.data.buf_id
local cwd = vim.fs.root(bufnr, ".git")
if gitStatusCache[cwd] then
updateMiniWithGit(bufnr, gitStatusCache[cwd].statusMap)
end
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

@theammir
Copy link

theammir commented Aug 17, 2024

Hi! How do I properly swap mini.diff with gitsigns?
Just tried changing hlgroups to GitSigns... (found them here) (ok, there's gitsigns-highlight-groups help entry, i simply used Add, Change, Delete ones), and it doesn't seem to work after that.

@theammir
Copy link

theammir commented Aug 17, 2024

Hi! How do I properly swap mini.diff with gitsigns? Just tried changing hlgroups to GitSigns... (found them here) (ok, there's gitsigns-highlight-groups help entry, i simply used Add, Change, Delete ones), and it doesn't seem to work after that.

After further investigation, it does work with a different repo, just not ~/.config/nvim. Does it matter that the latter one is a renamed submodule?
Went with @WizardStark's approach for now, it resolves the issue.

@bassamsdata
Copy link
Author

Hey @theammir

just not ~/.config/nvim. Does it matter that the latter one is a renamed submodule?

I updated the module to work with async functions and the latest vim.fs.root(). I believe it should work better now and be more performant than WizardStark's version since it doesn't call Git multiple times.

Just tried changing hlgroups to GitSigns...

I updated the module to use GitSigns highlights since everyone is using GitSigns.

Please note this module requires nvim version 0.10.

@linkarzu
Copy link

I sometimes need to know if the file or directory is a symlink when in mini.files, not sure if you're interested in that, or if anyone else is, but added it, notice that symlinks show in red, and if they're ignored the ignored symbol also shows. I'm using the latest gist revision

    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 isSymlink(path)
      local stat = vim.loop.fs_lstat(path)
      return stat and stat.type == "link"
    end

    ---@type table<string, {symbol: string, hlGroup: string}>
    ---@param status string
    ---@return string symbol, string hlGroup
    local function mapSymbols(status, is_symlink)
      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" }
      local gitSymbol = result.symbol
      local gitHlGroup = result.hlGroup

      local symlinkSymbol = is_symlink and "" or ""

      -- Combine symlink symbol with Git status if both exist
      local combinedSymbol = (symlinkSymbol .. gitSymbol):gsub("^%s+", ""):gsub("%s+$", "")
      -- Change the color of the symlink icon from "MiniDiffSignDelete" to something else
      local combinedHlGroup = is_symlink and "MiniDiffSignDelete" or gitHlGroup

      return combinedSymbol, combinedHlGroup
    end

    ---@param cwd string
    ---@param callback function
    ---@return nil
    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

    ---@param str string|nil
    ---@return string
    local function escapePattern(str)
      if not str then
        return ""
      end
      return (str:gsub("([%^%$%(%)%%%.%[%]%*%+%-%?])", "%%%1"))
    end

    ---@param buf_id integer
    ---@param gitStatusMap table
    ---@return nil
    local function updateMiniWithGit(buf_id, gitStatusMap)
      vim.schedule(function()
        local nlines = vim.api.nvim_buf_line_count(buf_id)
        local cwd = vim.fs.root(buf_id, ".git")
        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 is_symlink = isSymlink(entry.path)
            local symbol, hlGroup = mapSymbols(status, is_symlink)
            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

    -- Thanks for the idea of gettings https://github.com/refractalize/oil-git-status.nvim signs for dirs
    ---@param content string
    ---@return table
    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

    ---@param buf_id integer
    ---@return nil
    local function updateGitStatus(buf_id)
      local cwd = vim.uv.cwd()
      if not cwd or not vim.fs.root(cwd, ".git") then
        return
      end

      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

    ---@return nil
    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,
    })

@matt-schrader
Copy link

I am trying to use this and it is only showing the git status on the first buffer, not the previews or drilled in buffers.

@bassamsdata
Copy link
Author

@matt-schrader I didn't quiet understand what you mean? it shouldn't be in previews, but when there is a git status , there should be a sign in buffers. would you please share more infos, like neovim version, OS, screenshot.
Screenshot 2025-01-05 at 12 47 26 PM

@matt-schrader
Copy link

matt-schrader commented Jan 5, 2025

Sorry, yea I didn't explain it very well last night :D. It is likely that I am doing something wrong, though I kind of thought if I was doing it wrong it wouldnt work at all. Below you can see that I have git statuses for the root pane, but not for lua and plugins folders. It makes sense preview doesnt have any. You can see how I have the plugin configured with your status code. Thank you for your help!

Screenshot 2025-01-05 at 12 54 18 PM

@matt-schrader
Copy link

Missed info -

  • OS - MacOS
  • NVim - 0.10.2

@matt-schrader
Copy link

This was my mistake. I completely blanked on the fact I had never added the folder I am looking at to git. Weekend brain fog :D.

@bassamsdata
Copy link
Author

great, no problem glad it's working

@inogai
Copy link

inogai commented Mar 27, 2025

recent commit echasnovski/mini.files@3007632 just broke the cwd finding mechanism

I was able to make it work by changing line 140 to

local cwd = vim.fn.expand('%:p'):gsub('^minifiles://%d+/', '')

@bassamsdata
Copy link
Author

Thank you @inogai for the solution. I haven't tested it fully but I think it behaves differently now with vim.fn.expand()
but basically you could simply use this:

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

@Ryan-W31
Copy link

Ryan-W31 commented Apr 4, 2025

Hi, this is great! Something I've been trying to implement but I can't seem to figure out.. any ideas how I might change the color of the text itself based on the git status? Similar to how Neo-tree colors its text

@bassamsdata
Copy link
Author

@Ryan-W31 Hey, thank you.
yes, you can add those after line 94

              local line = vim.api.nvim_buf_get_lines(buf_id, i - 1, i, false)[1]
              -- Find the name position accounting for potential icons
              local nameStartCol = line:find(vim.pesc(entry.name)) or 0

              if nameStartCol > 0 then
                vim.api.nvim_buf_add_highlight(
                  buf_id,
                  nsMiniFiles,
                  hlGroup,
                  i - 1,
                  nameStartCol - 1,
                  nameStartCol + #entry.name - 1
                )
              end

I opted for vim.api.nvim_buf_add_highlight because it give better result than the set_extmark even though it's depreciated.

Screenshot 2025-04-04 at 1 23 45 PM

@Ryan-W31
Copy link

Ryan-W31 commented Apr 4, 2025

Ah, I see. Thanks @bassamsdata !

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