How Bat's Diff Highlighting Works: A Technical Deep Dive into the Source Code
Bat's diff highlighting works by using libgit2 to compute changes between the working directory and Git index, storing results in a HashMap<u32, LineChange>, and decorating output lines with colored symbols via the LineChangesDecoration struct.
When you pass the --diff flag to bat, the syntax-highlighting cat clone transforms into a powerful diff viewer. This feature, implemented in the sharkdp/bat repository, leverages Rust's git2 crate to analyze modifications and render per-line change indicators directly in your terminal.
The Architecture of Bat's Diff Highlighting
The diff highlighting pipeline spans six distinct modules, moving from CLI argument parsing through Git integration to terminal output decoration. At each stage, specific structs and functions transform raw Git data into visual indicators.
The core flow follows this path: CLI flags in clap_app.rs set diff_mode → controller.rs triggers get_git_diff → src/diff.rs builds a line-change map → printer.rs passes the map to InteractivePrinter → decorations.rs renders symbols.
Step 1: CLI Flag Parsing and Configuration
Defining the --diff Option in clap_app.rs
Bat defines the diff functionality through Clap arguments in src/bin/bat/clap_app.rs. Lines 155-171 declare both the --diff (-d) flag and the --diff-context option for controlling surrounding line display.
When users invoke bat --diff, the argument parser sets internal configuration values that propagate through the application. The Config::visible_lines.diff_mode() method returns true when this flag is present, signaling downstream components to activate diff processing.
Configuring Diff Mode in config.rs
The Config struct in src/config.rs maintains the diff_mode state throughout the application lifecycle. This boolean flag determines whether the controller should attempt Git diff computation or skip directly to standard file printing.
Step 2: Triggering the Diff in the Controller
The Controller struct in src/controller.rs orchestrates the diff detection logic. Between lines 163-180, the code checks diff_mode() and conditionally calls get_git_diff(path) to retrieve change information for the target file.
If the file resides in a Git repository with uncommitted changes, get_git_diff returns Some(HashMap<u32, LineChange>) mapping line numbers to change types. The controller passes this map into the printer initialization, ensuring the decoration layer receives the diff data.
The optional diff_context value controls how many unchanged lines appear around modifications, though the core highlighting logic functions independently of context length.
Step 3: Computing the Diff with libgit2
The diff.rs Module and HashMap Storage
The src/diff.rs module contains the core Git integration logic, utilizing the git2 crate (Rust bindings for libgit2) to compute differences without shelling out to the git binary. Lines 19-84 handle the repository discovery and diff generation.
The module computes a diff between the Git index and the working tree (or HEAD for staged changes). It iterates through diff hunks, recording specific line numbers for additions, deletions, and modifications. These results populate a HashMap<u32, LineChange> where keys are 1-based line numbers and values indicate the change type.
LineChange Enum and Symbol Mapping
The LineChange enum defines four distinct states that map to specific visual symbols:
- Added lines: Displayed with a green
+symbol - Removed lines above: Displayed with a red
‾(overline) symbol - Removed lines below: Displayed with a red
_(underscore) symbol - Modified lines: Displayed with a yellow
~symbol
This mapping occurs in src/decorations.rs, where the LineChangesDecoration struct translates the raw LineChange values into terminal-ready strings with appropriate ANSI color codes.
Step 4: Decorating Output with LineChangesDecoration
InteractivePrinter Setup in printer.rs
The InteractivePrinter struct in src/printer.rs receives the diff data through its constructor (lines 95-100). It accepts an Option<LineChanges> reference—the HashMap produced by the diff module—and stores it for use during the printing lifecycle.
The printer also defines the color scheme for diff symbols in the Colors::colored method (lines 332-342), assigning green to additions, red to removals, and yellow to modifications. Users can override these colors via custom themes or the BAT_THEME environment variable.
Rendering Symbols in decorations.rs
The actual symbol generation happens in src/decorations.rs through the LineChangesDecoration struct. Lines 72-99 handle the decoration creation, which only occurs when both config.style_components.changes() is enabled and the diff map contains data.
During line printing, InteractivePrinter::print_line invokes LineChangesDecoration::generate (lines 101-124). This method looks up the current line number in the diff map:
- If the line is added, it returns a green
+ - If the line is removed above, it returns a red
‾ - If the line is removed below, it returns a red
_ - If the line is modified, it returns a yellow
~ - If unchanged, it returns a blank space
The symbol is printed immediately before the line number and content, creating the side-by-side diff visualization that bat is known for.
Usage Examples and Implementation Details
You can trigger bat's diff highlighting from the command line using the --diff or -d flag. This mode works particularly well when reviewing changes before committing or when browsing modified files in a Git repository.
# Basic usage – show a git diff with bat’s pretty colours
bat --diff README.md
# Show 3 lines of context around each change (default is 0)
bat --diff --diff-context=3 src/main.rs
# Disable the diff symbols while keeping the rest of the styling
BAT_THEME="OneHalfDark" bat --diff --style=plain README.md
Under the hood, bat avoids shelling out to the git binary by using the git2 crate, which provides direct bindings to libgit2. This approach ensures cross-platform consistency and eliminates the performance overhead of spawning external processes.
The following Rust snippet demonstrates the core logic that bat uses in src/diff.rs to build the line-change map:
// Minimal snippet that mimics bat’s diff handling
use git2::{Repository, DiffOptions};
use std::collections::HashMap;
#[derive(Copy, Clone)]
enum LineChange { Added, RemovedAbove, RemovedBelow, Modified }
fn get_git_diff(path: &std::path::Path) -> Option<HashMap<u32, LineChange>> {
let repo = Repository::discover(path).ok()?;
let workdir = repo.workdir()?;
let diff_opts = {
let mut o = DiffOptions::new();
o.pathspec(path.strip_prefix(workdir).ok()?.into_c_string().ok()?);
o.context_lines(0);
o
};
let diff = repo.diff_index_to_workdir(None, Some(&mut diff_opts)).ok()?;
let mut map = HashMap::new();
diff.foreach(
&mut |_, _| true,
None,
Some(&mut |_, hunk| {
// Simplified: mark every line in the hunk as Modified
let start = hunk.new_start();
let end = start + hunk.new_lines() - 1;
for l in start..=end { map.insert(l, LineChange::Modified); }
true
}),
None,
).ok()?;
Some(map)
}
Summary
Bat's diff highlighting combines Git integration with terminal decoration to provide immediate visual feedback on file modifications. The implementation relies on several key architectural decisions:
- CLI Integration: The
--diffflag insrc/bin/bat/clap_app.rsactivatesdiff_modein the configuration, triggering the entire pipeline. - Git Analysis: The
src/diff.rsmodule uses libgit2 via thegit2crate to compute differences without external process calls, storing results in aHashMap<u32, LineChange>. - Symbol Decoration: The
LineChangesDecorationstruct insrc/decorations.rstranslates change types into colored symbols (+,‾,_,~) that appear beside line numbers. - Color Management: The
InteractivePrinterinsrc/printer.rsapplies green for additions, red for removals, and yellow for modifications, respecting user themes viaBAT_THEME.
Frequently Asked Questions
How do I enable diff highlighting in bat?
Pass the --diff or -d flag when invoking bat. For example, bat --diff src/main.rs displays the file with colored symbols indicating which lines differ from the Git index. You can combine this with --diff-context=N to show N lines of surrounding context.
What do the symbols in bat's diff output mean?
Bat uses four distinct symbols to represent change states: a green + indicates added lines, a red ‾ (overline) marks removed lines appearing above the current hunk, a red _ (underscore) marks removed lines below, and a yellow ~ denotes modified lines. Unchanged lines display a blank space in the symbol column.
How does bat compute diffs without calling the git binary?
Instead of spawning a git subprocess, bat uses the git2 crate, which provides Rust bindings to libgit2. In src/diff.rs, the code calls Repository::discover() to find the Git root, then repo.diff_index_to_workdir() to generate the diff programmatically. This approach ensures cross-platform consistency and better performance by avoiding process overhead.
Can I customize the colors used for diff highlighting?
Yes, bat respects the BAT_THEME environment variable and custom theme files defined in your configuration directory. The default colors are defined in src/printer.rs within the Colors::colored method: green for additions, red for removals, and yellow for modifications. You can override these by creating a custom theme or selecting a built-in theme that provides different color mappings for diff components.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →