Part 8: Neovim git integration

Dec 2, 2025
1273 words - 6 min read

This is part 8 of my Neovim series. Today we setup git tools. See git changes in your editor.

Git integration shows you what changed. Which lines you added. Which you deleted. Who wrote this line. When it was changed. All without leaving Neovim.

We already setup snacks.nvim in part 4. That gives us many Git utilities:

  • <leader>gb - Show git branches
  • <leader>gl - Show git logs
  • <leader>gs - Show git status
  • <leader>gd - Show git diff ...etc

Snacks.nvim already wraps lazygit. That handles a lot of Git work. But it still misses some features:

  • Show changes directly in the buffer
  • Blame individual lines
  • Show diffs in splits
  • Resolve conflicts

To get these features, I use two plugins: gitsigns.nvim and diffview.nvim

gitsigns.nvim - Shows git changes in the sign column. Blame lines. Stage hunks. Navigate changes.

diffview.nvim - View diffs in a split. Compare branches. See file history. Review changes before commit. Resolve conflicts

Setup git tools

Here's my complete setup:

File: lua/plugins/git.lua

vim.pack.add({
	"https://github.com/lewis6991/gitsigns.nvim",
	"https://github.com/sindrets/diffview.nvim",
})

-- Setup gitsigns.nvim
require("gitsigns").setup({
	current_line_blame = true,
	signs = {
		add = { text = "▎" },
		change = { text = "▎" },
		delete = { text = "" },
		topdelete = { text = "" },
		changedelete = { text = "▎" },
		untracked = { text = "▎" },
	},
	signs_staged = {
		add = { text = "▎" },
		change = { text = "▎" },
		delete = { text = "" },
		topdelete = { text = "" },
		changedelete = { text = "▎" },
	},
	on_attach = function(buffer)
		local gs = package.loaded.gitsigns

		local function map(mode, lhs, rhs, desc)
			vim.keymap.set(mode, lhs, rhs, { buffer = buffer, desc = desc })
		end

		map("n", "]h", function()
			if vim.wo.diff then
				vim.cmd.normal({ "]c", bang = true })
			else
				gs.nav_hunk("next")
			end
		end, "Next Hunk")

		map("n", "[h", function()
			if vim.wo.diff then
				vim.cmd.normal({ "[c", bang = true })
			else
				gs.nav_hunk("prev")
			end
		end, "Prev Hunk")

		map("n", "]H", function()
			gs.nav_hunk("last")
		end, "Last Hunk")
		map("n", "[H", function()
			gs.nav_hunk("first")
		end, "First Hunk")

		map({ "n", "v" }, "<leader>ghs", ":Gitsigns stage_hunk<CR>", "Stage Hunk")
		map({ "n", "v" }, "<leader>ghr", ":Gitsigns reset_hunk<CR>", "Reset Hunk")

		map("n", "<leader>ghS", gs.stage_buffer, "Stage Buffer")
		map("n", "<leader>ghu", gs.undo_stage_hunk, "Undo Stage Hunk")
		map("n", "<leader>ghR", gs.reset_buffer, "Reset Buffer")
		map("n", "<leader>ghp", gs.preview_hunk_inline, "Preview Hunk Inline")
		map("n", "<leader>ghb", function()
			gs.blame_line({ full = true })
		end, "Blame Line")
		map("n", "<leader>ghB", function()
			gs.blame()
		end, "Blame Buffer")
		map("n", "<leader>ghd", gs.diffthis, "Diff This")
		map("n", "<leader>ghD", function()
			gs.diffthis("~")
		end, "Diff This ~")

		map({ "o", "x" }, "ih", ":<C-U>Gitsigns select_hunk<CR>", "GitSigns Select Hunk")
	end,
})

-- Setup diffview.nvim
require("diffview").setup({
	use_icons = true,
	icons = { folder_closed = "", folder_open = "" },
	view = {
		default = { winbar_info = true },
	},
	file_panel = {
		win_config = { height = 20 },
	},
})

-- diffview keymaps
vim.keymap.set("n", "<leader>Do", function()
	vim.ui.input({ prompt = "Diff refs (ex. main..feature): " }, function(refs)
		if refs and refs:match("%S") then
			local safe = vim.fn.shellescape(refs, true)
			vim.cmd(("DiffviewOpen %s"):format(safe))
		else
			vim.cmd("DiffviewOpen")
		end
	end)
end, { desc = "Diffview: open (prompt for refs or default)" })

vim.keymap.set("n", "<leader>Dc", "<cmd>DiffviewClose<cr>", { desc = "Diffview: Close" })
vim.keymap.set("n", "<leader>Dt", "<cmd>DiffviewToggleFiles<cr>", { desc = "Diffview: Toggle file list" })
vim.keymap.set("n", "<leader>Dh", "<cmd>DiffviewFileHistory %<cr>", { desc = "Diffview: File history" })
vim.keymap.set("n", "<leader>DH", "<cmd>DiffviewFileHistory<cr>", { desc = "Diffview: Repo history" })

-- toggles
vim.keymap.set("n", "<leader>uG", function()
	local gs = require("gitsigns")
	local cfg = require("gitsigns.config").config
	local current = cfg.signcolumn
	gs.toggle_signs(not current)
end, { desc = "Toggle Git Signs" })

Restart Neovim. Open a git repo. You see git signs in the gutter.

Breaking down the setup

Gitsigns config

current_line_blame = true,

Shows git blame on current line. See who wrote it and when.

signs = {
	add = { text = "▎" },
	change = { text = "▎" },
	delete = { text = "" },
	topdelete = { text = "" },
	changedelete = { text = "▎" },
	untracked = { text = "▎" },
},

Icons in the sign column. Shows what changed:

  • add - New lines
  • change - Modified lines
  • delete - Deleted lines
  • untracked - New files not in git
signs_staged = {
	add = { text = "▎" },
	change = { text = "▎" },
	-- ...
},

Different signs for staged changes. You see what's staged vs unstaged.

Some more keymaps:

map("n", "]h", function()
	if vim.wo.diff then
		vim.cmd.normal({ "]c", bang = true })
	else
		gs.nav_hunk("next")
	end
end, "Next Hunk")

]h - Jump to next change (hunk). [h - Jump to previous change.

Works in diff mode too. Smart navigation.

map("n", "]H", function()
	gs.nav_hunk("last")
end, "Last Hunk")

]H - Jump to last change. [H - Jump to first change.

Stage and reset keymaps

map({ "n", "v" }, "<leader>ghs", ":Gitsigns stage_hunk<CR>", "Stage Hunk")
map({ "n", "v" }, "<leader>ghr", ":Gitsigns reset_hunk<CR>", "Reset Hunk")

<leader>ghs - Stage current hunk. Works in visual mode too (stage selection).

<leader>ghr - Reset current hunk (discard changes).

map("n", "<leader>ghS", gs.stage_buffer, "Stage Buffer")
map("n", "<leader>ghR", gs.reset_buffer, "Reset Buffer")

<leader>ghS - Stage entire file.

<leader>ghu - Undo stage hunk (unstage last staged hunk).

<leader>ghR - Reset entire file (discard all changes).

<leader>ghp - Preview hunk inline (show changes without opening a split).

Blame keymaps

map("n", "<leader>ghb", function()
	gs.blame_line({ full = true })
end, "Blame Line")

<leader>ghb - Show full blame for current line. See commit message and author.

<leader>ghB - Show blame for entire file.

Diff keymaps

map("n", "<leader>ghd", gs.diffthis, "Diff This")

<leader>ghd - Open diff view for current file. See all changes in a split.

<leader>ghD - Diff against HEAD~.

Text object

map({ "o", "x" }, "ih", ":<C-U>Gitsigns select_hunk<CR>", "GitSigns Select Hunk")

ih - Text object for hunk. Use like vih (select hunk), dih (delete hunk), yih (yank hunk).

Diffview config

require("diffview").setup({
	use_icons = true,
	icons = { folder_closed = "", folder_open = "" },
	view = {
		default = { winbar_info = true },
	},
	file_panel = {
		win_config = { height = 20 },
	},
})

Diffview shows changes in a nice split layout. File panel shows changed files. Click to see diff.

Diffview keymaps

vim.keymap.set("n", "<leader>Do", function()
	vim.ui.input({ prompt = "Diff refs (ex. main..feature): " }, function(refs)
		if refs and refs:match("%S") then
			local safe = vim.fn.shellescape(refs, true)
			vim.cmd(("DiffviewOpen %s"):format(safe))
		else
			vim.cmd("DiffviewOpen")
		end
	end)
end, { desc = "Diffview: open (prompt for refs or default)" })

<leader>Do - Open diffview. Prompts for refs (like main..feature). Leave empty to see uncommitted changes.

<leader>Dc - Close diffview.

<leader>Dt - Toggle file list.

<leader>Dh - Show history for current file.

<leader>DH - Show history for entire repo.

Toggle signs

vim.keymap.set("n", "<leader>uG", function()
	local gs = require("gitsigns")
	local cfg = require("gitsigns.config").config
	local current = cfg.signcolumn
	gs.toggle_signs(not current)
end, { desc = "Toggle Git Signs" })

<leader>uG - Toggle git signs on/off. Clean view when you don't need them.

What's next

Now you have:

  • LSP (part 3)
  • Setup vim.pack with snacks.nvim (part 4)
  • Tree-sitter syntax highlighting (part 5)
  • Auto-completion (part 6)
  • Code formatter (part 7)
  • Git tools (this article)

Next article, we will setup debug tools

Thanks for reading! If you notice anything outdated, feel free to comment or send me a message.