In this episode we bring Git to life directly inside Neovim: change indicators in the gutter, inline blame, stage/reset hunks with gitsigns.nvim, plus diffs and merge conflict resolution with diffview.nvim.

After ensuring code quality with automatic formatting and asynchronous linting in episode 17, in this episode we discuss the true collaboration tool: Git. We will bring Git into Neovim with two main plugins — gitsigns.nvim for visualizing changes in the gutter and diffview.nvim for diffs and merge conflict resolution.
In the real world, almost no software engineering happens alone. Every day we do the same things: see what changed since last time, inspect hunks changed by colleagues, revert a single wrong line, or resolve conflicts after a pull. For developers who jump between editor and terminal, this kind of work breaks focus: open the terminal, run git diff, read raw output, return to the editor, find the line... In this episode, you will learn to run that entire workflow without leaving the editor — and this is what makes Neovim feel like a true collaborative IDE.
Git itself is a great CLI tool, but its output is static text. What we need is a connection between the git diff results and the buffer we are editing. This is where these two plugins play their roles:
| Plugin | Role | Analogy |
|---|---|---|
gitsigns.nvim | Visualizing changes inside the buffer | Indicator lights at the edge of the file |
diffview.nvim | Diff between commits/branches & merge tool | A GUI Git comparison window |
neogit / fugitive | Complete Git client in the editor | A GUI Git alternative without leaving Neovim |
The most fitting analogy: gitsigns is the car dashboard (showing current changes without opening another panel), while diffview and neogit are the garage where you do thorough comparison and maintenance on the vehicle.
gitsigns.nvim displays +, ~, - markers in the sign column (gutter) for lines added, changed, or deleted since the last commit. They are computed asynchronously by git diff, so they do not burden the editor. Here is the configuration:
return {
{
"lewis6991/gitsigns.nvim",
event = { "BufReadPre" },
opts = {
signs = {
add = { text = "+" },
change = { text = "~" },
delete = { text = "_" },
topdelete = { text = "‾" },
changedelete = { text = "~" },
},
on_attach = function(bufnr)
local gitsigns = require("gitsigns")
local opts = { buffer = bufnr, silent = true }
vim.keymap.set("n", "<leader>gp", gitsigns.preview_hunk, opts)
vim.keymap.set("n", "<leader>gs", gitsigns.stage_hunk, opts)
vim.keymap.set("n", "<leader>gr", gitsigns.reset_hunk, opts)
vim.keymap.set("n", "<leader>gS", gitsigns.stage_buffer, opts)
vim.keymap.set("n", "<leader>gu", gitsigns.undo_stage_hunk, opts)
vim.keymap.set("n", "<leader>gb", function()
gitsigns.blame_line({ full = true })
end, opts)
vim.keymap.set("n", "[h", gitsigns.prev_hunk, { buffer = bufnr, silent = true })
vim.keymap.set("n", "]h", gitsigns.next_hunk, { buffer = bufnr, silent = true })
end,
},
},
}Dissecting the keymaps we just defined:
<leader>gs — stage the hunk under the cursor (mark changes for commit). This is a far more visual replacement for git add -p.<leader>gr — reset the hunk, discarding the changes in that hunk and returning it to the last commit state. Be careful — this operation permanently deletes changes.<leader>gp — preview the hunk: opens a small window showing the exact diff of that hunk, without switching buffers.<leader>gu — undo stage: pulls a staged hunk back into the working directory (the opposite of staging).<leader>gb — inline blame: shows sha1 author date on the line where the cursor is. This function triggers a single-line blame with blame_line({ full = true }) for complete detail.[h / ]h — jump to the previous / next hunk. Useful before doing sequential previews or staging.The most common real-world scenario — working with two files mixed in one working tree:
1. Buka file yang sudah diubah → gutter menampilkan + ~ - sesuai perubahan.
2. Tekan ]h untuk lompat ke hunk pertama.
3. Tekan <leader>gp untuk preview diff hunk tersebut.
4. Jika sudah yakin, tekan <leader>gs untuk men-stage hunk.
5. Ulangi untuk hunk berikutnya hingga semua perubahan yang diinginkan ter-stage.
6. Tekan <leader>gS untuk men-stage seluruh buffer bila ingin semua sekaligus.
7. Tekan <leader>gb untuk mengecek siapa yang menulis baris tertentu (blame).Tip
The power of stage-hunk over git add .: you can separate unrelated changes in the same file. For example one file contains both a bug fix and a feature addition — stage only the relevant hunk for the first commit, then commit the rest of the hunks. This keeps every commit focused and the Git history clean — a practice highly valued by reviewers.
To see comparisons between commits, between branches, or to resolve conflicts, diffview.nvim is the answer. It provides a dedicated diff file explorer plus split windows for comparison.
return {
{
"sindrets/diffview.nvim",
cmd = { "DiffviewOpen", "DiffviewFileHistory", "DiffviewClose" },
keys = {
{ "<leader>gd", "<cmd>DiffviewOpen<CR>", desc = "Buka diff view" },
{ "<leader>gh", "<cmd>DiffviewFileHistory<CR>", desc = "Riwayat file" },
},
opts = {
enhanced_diff_hl = true,
view = {
merge_tool = {
layout = "diff3_mixed",
},
},
},
},
}The commands you will often use:
| Command | Function |
|---|---|
:DiffviewOpen | Diff between the working tree and HEAD |
:DiffviewOpen <branch> | Diff the working tree with another branch |
:DiffviewOpen HEAD~2..HEAD | Diff between a commit range |
:DiffviewFileHistory % | Commit history for the active file |
:DiffviewClose | Close the diff panel |
:DiffviewToggleFiles | Toggle the panel listing changed files |
When there is a conflict, run :DiffviewOpen on the conflict state and use the merge_tool with the diff3_mixed layout — a three-panel view: BASE (left), ours (top right), theirs (bottom right), and the merge result in the middle. With this configuration, resolving conflicts becomes visual, not blindly reading <<<<<<< and >>>>>>> in a raw file.
Caution
During conflict resolution in :DiffviewOpen, do not forget that the merge result must be saved to the correct file. Diffview shows the merged file as an editable buffer — make sure you resolve all markers (<<<<<<<, =======, >>>>>>>) before closing the view, and verify with :diffoff / :DiffviewClose that no markers remain. Git will refuse the commit if conflict markers still exist.
After understanding diffs, the next step is reading history. There are three different ways to trace a file's past — each answers a different question:
1. <leader>gb → blame 1 baris : "siapa yang menulis BARIS ini?"
2. <leader>gh → :DiffviewFileHistory % : "commit apa saja yang MENYENTUH file ini?"
3. :Git log → fugitive/neogit log : "cerita lengkap branch ini?"A classic production team scenario: a timeout = 30 line in a config file suddenly causes problems. You place the cursor on that line, press <leader>gb, and immediately know — a1b2c3d — John Doe — 2026-07-12 — fix: raise api timeout. If you need broader context, open :DiffviewFileHistory % to see the series of commits touching the file, or use :Git log -- <file> for its commit list. From within the history, press Enter in diffview to open the full diff of that commit. The whole investigation happens without switching applications.
Note
Inside the diffview panel, navigation between hunks and files also uses the Vim keymaps you already know: ]c / [c to move between hunks in a diff (these are Vim's built-in change navigation keymaps, not conflicting with gitsigns' [h/]h — the two live in different buffers). Use :DiffviewToggleFiles to see the list of changed files, then Enter to jump to that file in the diff.
For larger Git operations (commit, branch, log, push), we need a git client. The two most popular options:
Neogit — a Magit-style (from Emacs) TUI git client, with a paneled interface showing status, diff, log, and staging in one window:
return {
{
"NeogitOrg/neogit",
dependencies = {
"nvim-lua/plenary.nvim",
"sindrets/diffview.nvim",
},
keys = {
{ "<leader>gg", "<cmd>Neogit<CR>", desc = "Buka Neogit" },
{ "<leader>gc", "<cmd>Neogit commit<CR>", desc = "Commit" },
},
},
}Inside Neogit: s to stage a file/hunk, c to commit, p to push, l for log, ? for the full keymap list.
Fugitive — Tim Pope's legendary plugin using Ex commands (:Git), the most popular in the Vim/Neovim ecosystem. Basic keymaps to memorize:
:Git status
:Git commit
:Git log
:Gdiffsplit " diff file aktif vs index
:Gblame " blame visual per barisNote
Choosing between neogit and fugitive is a matter of taste: fugitive feels "Vim-native" (Ex commands, rarely needs the mouse), while neogit feels like a GUI Git because it shows a status list with panel navigation. Many developers use both: fugitive for quick operations, neogit for visually browsing status. You can also start with both — the configurations above do not interfere with each other.
| Shortcut | Action | Plugin |
|---|---|---|
<leader>gs | Stage hunk | gitsigns |
<leader>gr | Reset hunk (discard) | gitsigns |
<leader>gp | Preview hunk | gitsigns |
<leader>gS | Stage the whole buffer | gitsigns |
<leader>gu | Undo stage hunk | gitsigns |
<leader>gb | Inline blame of the cursor line | gitsigns |
[h / ]h | Previous / next hunk | gitsigns |
<leader>gd | Open diff view | diffview |
<leader>gh | File history | diffview |
<leader>gg | Open Neogit | neogit |
<leader>gc | Open the commit panel | neogit |
| Mistake | Symptom | Solution |
|---|---|---|
| Blame does not show | <leader>gb displays nothing | Blame needs an active line — make sure the cursor is on a line that actually has changes or code |
| Empty gutter | No +/~ markers even though the file changed | Make sure the buffer is a file actually tracked by Git; check git status; try reopening the buffer |
| Confusing merge conflict | Many <<<<<<< with no hints | Use :DiffviewOpen during a conflict with the diff3_mixed layout to see BASE/ours/theirs |
| Neogit does not open | git not found error | Install Git on the system (sudo apt install git); neogit needs the git binary on PATH |
| Diffview shows the wrong files | The comparison is not as expected | Understand the arguments: :DiffviewOpen <range>; without arguments, it compares against HEAD |
| Stage hunk erases changes | Changes disappear after reset | reset_hunk permanently deletes changes — make sure the hunk being reset is really meant to be discarded, or do an undo (u) if you immediately regret it |
| Undo cannot restore a hunk | u does not restore the file | Reset hunk rewrites the buffer; if undofile (episode 7) is active, Vim undo can save you — always enable it |
| Weird diff line highlighting | Diff colors are unclear | Enable enhanced_diff_hl = true in diffview (already in our config) |
| Performance drops on big repos | The gutter feels slow on giant files | Limit gitsigns scope (diff_opts) or disable it for certain very large buffers |
Warning
The most dangerous and most frequent mistake: pressing <leader>gr (reset hunk) on the wrong line. Reset hunk has no confirmation button — the change is immediately gone. Always preview with <leader>gp first to make sure it is the right hunk, and make sure undofile is active (episode 7 configuration) as the last safety net.
In episode 18 we brought Git fully into Neovim: gitsigns.nvim with add/change/delete indicators in the gutter, the stage-preview-reset hunk workflow, inline blame, and hunk navigation ([h, ]h). We also mastered diffview.nvim for branch diffs and merge conflict resolution with the diff3_mixed layout, plus two git client choices — neogit with its paneled UI and fugitive with Ex commands.
With this, you have completed Phase 4: MODERN IDE CAPABILITIES. Your Neovim is no longer just a text editor — it understands code grammatically (Treesitter), semantically (LSP), completes contextually (nvim-cmp), maintains consistency (conform & nvim-lint), and coexists with Git (gitsigns & diffview).
In episode 19, we will enter Phase 5: Advanced Workflow, Productivity & AI, starting with the Integrated Terminal & Task Management using toggleterm.nvim — running a terminal, lazygit, and task runners directly inside Neovim. The journey is getting more exciting — stay motivated!