Skip to content

Commit

Permalink
feat(blame): add custom git blame functionality of whole file with co…
Browse files Browse the repository at this point in the history
…mmit checking
  • Loading branch information
ecosse3 committed Feb 9, 2022
1 parent 50095ae commit 9841fe5
Showing 1 changed file with 289 additions and 0 deletions.
289 changes: 289 additions & 0 deletions lua/blame.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
local utils = require('utils')
local git = require('utils.git')

local M = {}

local config = {
keymaps = {
quit_blame = "q",
blame_commit = "<CR>"
}
}

local blame_state = {
file = "",
temp_file = "",
starting_win = "",
relative_path = "",
git_root = "",
}

local function blameLinechars()
return vim.fn.strlen(vim.fn.getline ".")
end

local function create_blame_win()
vim.api.nvim_command "topleft vnew"
local win = vim.api.nvim_get_current_win()
local buf = vim.api.nvim_get_current_buf()

vim.api.nvim_buf_set_option(buf, "buftype", "nofile")
vim.api.nvim_buf_set_option(buf, "swapfile", false)
vim.api.nvim_buf_set_option(buf, "bufhidden", "wipe")
vim.api.nvim_buf_set_option(buf, "filetype", "git")
vim.api.nvim_buf_set_option(buf, "buflisted", false)

vim.api.nvim_win_set_option(win, "number", true)
vim.api.nvim_win_set_option(win, "foldcolumn", "0")
vim.api.nvim_win_set_option(win, "foldenable", false)
vim.api.nvim_win_set_option(win, "foldenable", false)
vim.api.nvim_win_set_option(win, "winfixwidth", true)
vim.api.nvim_win_set_option(win, "signcolumn", "no")
vim.api.nvim_win_set_option(win, "wrap", false)

return win, buf
end

local function blame_syntax()
local seen = {}
local hash_colors = {}
for lnum = 1, vim.fn.line "$" do
local orig_hash = vim.fn.matchstr(vim.fn.getline(lnum), [[^\^\=[*?]*\zs\x\{6\}]])
local hash = orig_hash
hash = vim.fn.substitute(hash, [[\(\x\)\x]], [[\=submatch(1).printf("%x", 15-str2nr(submatch(1),16))]], "g")
hash = vim.fn.substitute(hash, [[\(\x\x\)]], [[\=printf("%02x", str2nr(submatch(1),16)*3/4+32)]], "g")
if hash ~= "" and orig_hash ~= "000000" and seen[hash] == nil then
seen[hash] = 1
if vim.wo.t_Co == "256" then
local colors = vim.fn.map(vim.fn.matchlist(orig_hash, [[\(\x\)\x\(\x\)\x\(\x\)\x]]), "str2nr(v:val,16)")
local r = colors[2]
local g = colors[3]
local b = colors[4]
local color = 16 + (r + 1) / 3 * 36 + (g + 1) / 3 * 6 + (b + 1) / 3
if color == 16 then
color = 235
elseif color == 231 then
color = 255
end
hash_colors[hash] = " ctermfg=" .. tostring(color)
else
hash_colors[hash] = ""
end
local pattern = vim.fn.substitute(orig_hash, [[^\(\x\)\x\(\x\)\x\(\x\)\x$]], [[\1\\x\2\\x\3\\x]], "") .. [[*\>]]
vim.cmd("syn match GitNvimBlameHash" .. hash .. [[ "\%(^\^\=[*?]*\)\@<=]] .. pattern .. [[" skipwhite]])
end

for hash_value, cterm in pairs(hash_colors) do
if cterm ~= nil or vim.fn.has "gui_running" or vim.fn.hash "termguicolors" and vim.wo.termguicolors then
vim.cmd("hi GitNvimBlameHash" .. hash_value .. " guifg=#" .. hash_value .. cterm)
else
vim.cmd("hi link GitNvimBlameHash" .. hash_value .. " Identifier")
end
end
end
end

local function on_blame_done(lines)
local starting_win = vim.api.nvim_get_current_win()
local current_top = vim.fn.line "w0" + vim.wo.scrolloff
local current_pos = vim.fn.line "."

-- Save the state
blame_state.file = vim.api.nvim_buf_get_name(0)
blame_state.starting_win = starting_win

local blame_win, blame_buf = create_blame_win()

vim.api.nvim_buf_set_lines(blame_buf, 0, -1, true, lines)
vim.api.nvim_buf_set_option(blame_buf, "modifiable", false)
vim.api.nvim_win_set_width(blame_win, blameLinechars() + 1)

vim.cmd("execute " .. tostring(current_top))
vim.cmd "normal! zt"
vim.cmd("execute " .. tostring(current_pos))

-- We should call cursorbind, scrollbind here to avoid unexpected behavior
vim.api.nvim_win_set_option(blame_win, "cursorbind", true)
vim.api.nvim_win_set_option(blame_win, "scrollbind", true)

vim.api.nvim_win_set_option(starting_win, "scrollbind", true)
vim.api.nvim_win_set_option(starting_win, "cursorbind", true)

-- Keymaps
local options = {
noremap = true,
silent = true,
expr = false,
}

vim.api.nvim_buf_set_keymap(0, "n", config.keymaps.quit_blame, "<CMD>q<CR>", options)
vim.api.nvim_buf_set_keymap(
0,
"n",
config.keymaps.blame_commit,
"<CMD>lua require('blame').blame_commit()<CR>",
options
)
vim.api.nvim_command "autocmd BufWinLeave <buffer> lua require('blame').blame_quit()"

blame_syntax()
end

local function on_blame_commit_done(commit_hash, lines)
-- TODO: Find a better way to handle this case
local idx = 1
while idx <= #lines and not utils.starts_with(lines[idx], "diff") do
idx = idx + 1
end
table.insert(lines, idx, "")

local temp_file = vim.fn.tempname()
blame_state.temp_file = temp_file
vim.fn.writefile(lines, temp_file)

-- Close blame window
local win = vim.api.nvim_get_current_win()
vim.api.nvim_win_close(win, true)

vim.api.nvim_command("silent! e" .. temp_file)

local buf = vim.api.nvim_get_current_buf()
vim.api.nvim_buf_set_name(buf, commit_hash)
vim.api.nvim_buf_set_option(buf, "buftype", "nofile")
vim.api.nvim_buf_set_option(buf, "bufhidden", "delete")
vim.api.nvim_buf_set_option(buf, "filetype", "git")
vim.api.nvim_command "autocmd BufLeave <buffer> lua require('blame').blame_commit_quit()"

vim.fn.search([[^diff .* b/\M]] .. vim.fn.escape(blame_state.relative_path, "\\") .. "$", "W")
end

function M.blame_commit_quit()
local buf = vim.api.nvim_get_current_buf()
vim.api.nvim_command(buf .. "bdelete")
vim.fn.delete(blame_state.temp_file)
end

function M.blame_commit()
local line = vim.fn.getline "."
local commit = vim.fn.matchstr(line, [[^\^\=[?*]*\zs\x\+]])
if string.match(commit, "^0+$") then
utils.warnlog("Not Committed Yet", "Git")
return
end

local commit_hash = git.run_git_cmd(
"git -C " .. blame_state.git_root .. " --literal-pathspecs rev-parse --verify " .. commit .. " --"
)
if commit_hash == nil then
utils.warnlog("Commit hash not found", "Git")
return
end

commit_hash = string.gsub(commit_hash, "\n", "")
local diff_cmd = "git -C "
.. blame_state.git_root
.. " --literal-pathspecs --no-pager show --no-color "
.. commit_hash
.. " -- "
.. blame_state.file

local lines = {}
local function on_event(_, data, event)
-- TODO: Handle error data
if event == "stdout" or event == "stderr" then
data = utils.handle_job_data(data)
if not data then
return
end

for i = 1, #data do
if data[i] ~= "" then
table.insert(lines, data[i])
end
end
end

if event == "exit" then
on_blame_commit_done(commit_hash, lines)
end
end

vim.fn.jobstart(diff_cmd, {
on_stderr = on_event,
on_stdout = on_event,
on_exit = on_event,
stdout_buffered = true,
stderr_buffered = true,
})
end

function M.blame_quit()
vim.api.nvim_win_set_option(blame_state.starting_win, "scrollbind", false)
vim.api.nvim_win_set_option(blame_state.starting_win, "cursorbind", false)
end

function M.open()
local fpath = vim.api.nvim_buf_get_name(0)
if fpath == "" or fpath == nil then
return
end

local git_root = git.get_git_repo()
if git_root == "" then
return
end
blame_state.git_root = git_root
blame_state.relative_path = vim.fn.fnamemodify(vim.fn.expand "%", ":~:.")

local blame_cmd = "git -C "
.. git_root
.. " --literal-pathspecs --no-pager -c blame.coloring=none -c blame.blankBoundary=false blame --show-number -- "
.. fpath

local lines = {}
local has_error = false

local function on_event(_, data, event)
if event == "stdout" then
data = utils.handle_job_data(data)
if not data then
return
end

for i = 1, #data do
if data[i] ~= "" then
local commit = vim.fn.matchstr(data[i], [[^\^\=[?*]*\zs\x\+]])
local commit_info = data[i]:match "%((.-)%)"
commit_info = string.match(commit_info, "(.-)%s(%S+)$")
table.insert(lines, commit .. " " .. commit_info)
end
end
elseif event == "stderr" then
data = utils.handle_job_data(data)
if not data then
return
end

has_error = true
local error_message = ""
for _, line in ipairs(data) do
error_message = error_message .. line
end
utils.warnlog("Failed to open git blame window: " .. error_message, "Git")
elseif event == "exit" then
if not has_error then
on_blame_done(lines)
end
end
end

vim.fn.jobstart(blame_cmd, {
on_stderr = on_event,
on_stdout = on_event,
on_exit = on_event,
stdout_buffered = true,
stderr_buffered = true,
})
end

return M

0 comments on commit 9841fe5

Please sign in to comment.