The git history command
The git history command — spanning the workhorse git log and its rich flag ecosystem, dedicated GUI tools, and the powerful-but-underused git rebase -i
Researched and drafted with AI assistance, then screened by automated editorial checks before publishing. How we work.

The git history command — spanning the workhorse git log and its rich flag ecosystem, dedicated GUI tools, and the powerful-but-underused git rebase -i --update-refs rewriting workflow — is central to how developers understand and maintain a codebase over time. Whether you need to explore who changed a line and why, trace the full git history of a file across renames, browse commits visually inside VS Code, or surgically amend a commit buried deep in a stack, every one of those tasks has a precise, well-supported solution in Git today.
This guide covers the complete landscape: the essential git log flags for the git history command line, the most effective way to view the git history of a file, the Git History extension for VS Code, and the mature-but-underappreciated interactive rebase techniques for git history rewrite operations — including rewording commit messages, splitting bloated commits, and absorbing fixups into past work.
What "Git History" Actually Means: Two Distinct Layers
Before diving into syntax, it is worth separating two things that the phrase "git history" conflates in everyday speech:
- Inspection: Reading the commit history — who changed what, when, and why. This is the domain of
git logand its many flags, plus GUI overlays like the VS Code Git History extension. - Rewriting: Mutating the commit graph — amending messages, folding in fixes, splitting commits. This has traditionally required composing
git rebase -i,git commit --fixup, and autosquash, though third-party tools likegit absorband Jujutsu (jj) provide higher-level interfaces to the same operations.
Both layers matter. A developer who only knows git log without flags is flying blind; a developer who rewrites history without understanding how branches follow (or don't) along will corrupt teammates' stacks. The sections below address both layers in full.
The Git History Command Line: Mastering git log
The primary git history command in terminal is git log. Out of the box it dumps a verbose, paginated stream of commits. The real power comes from composing its flags and format strings.
Essential Formatting Flags
| Flag | What it does | Typical use |
|---|---|---|
--oneline |
Compact single-line output per commit (abbreviated hash + subject); shorthand for --pretty=oneline --abbrev-commit |
Quick overview of recent history |
--graph |
Draws an ASCII branch/merge graph alongside the log | Visualising branching topology in the terminal |
--decorate |
Appends branch/tag ref names to the relevant commit lines (defaults to auto in modern Git, so refs already appear on a terminal) |
Identifying where HEAD and branches currently point |
--pretty=<format> |
Custom output: short, full, fuller, oneline, format:<string> |
Scripting or machine-readable output |
--all |
Includes every ref under refs/ plus HEAD |
Seeing all branches and tags at once |
--no-merges |
Excludes merge commits from output | Cleaner linear view of feature work |
--first-parent |
Follows only the first parent at merge commits | Reading the mainline history of a long-lived branch |
Combining flags gives you the canonical "pretty graph" alias that almost every senior engineer has in their ~/.gitconfig:
git log --oneline --graph --decorate --all
Because --decorate now defaults to auto, ref names typically appear on terminal output even when you omit the flag; passing it explicitly is still useful when redirecting output or scripting.
Custom Format Strings
The --pretty=format:<string> option accepts placeholder tokens that give you precise control over output columns — essential for piping into scripts or generating changelogs. Common placeholders include:
%h— abbreviated commit hash%H— full commit hash%an— author name%ae— author email%ad— author date (format controlled by--date=<format>)%s— commit subject (first line of message)%b— commit body (remainder of message after the subject)%D— ref names without the surrounding parentheses
A practical example that outputs a tab-separated log suitable for spreadsheet import:
git log --pretty=format:"%h%x09%an%x09%ad%x09%s" --date=short
Here %x09 is the hex escape for a tab character. The --date=short flag produces YYYY-MM-DD dates. Wrap the entire format in single quotes on Unix systems to avoid shell interpolation.
Filtering the Git History of Commits
Raw output is rarely what you need. git log's filtering flags let you slice the git history of commits by author, time window, or commit message content:
--author=<pattern>— Limits to commits by a matching author name or email (regex accepted).--since=<date>/--until=<date>— Bounds the time range; accepts ISO 8601 dates or human-readable strings like"2 weeks ago"or"last Monday".--grep=<pattern>— Matches against commit messages; combine with--all-matchto require multiple--greppatterns simultaneously, or--invert-grepto exclude matching messages.-n <number>/--max-count=<number>— Caps output at the N most recent qualifying commits.-S <string>(the "pickaxe") — Finds commits that changed the number of occurrences of a specific string; invaluable for tracking exactly when a function was introduced or removed.-G <regex>— A more powerful variant of-Sthat matches commits where the diff contains an added or removed line matching the given regular expression.
Combining filters: show all commits by a specific author in the last month that mention "refactor" in the message:
git log --author="Alice" --since="1 month ago" --grep="refactor" --oneline
Viewing the Git History of a File
One of the most common daily operations is examining the git history of a file — tracing exactly which commits touched a given path. The canonical git history command for a file is:
git log -- path/to/file.py
The -- separator is not always required but prevents ambiguity when a filename might be interpreted as a branch or revision. For the git history of a file that has been renamed, add --follow:
git log --follow -- path/to/file.py
--follow walks through renames, stitching together a coherent history that would otherwise be truncated at the rename point. It works only for a single file at a time — you cannot pass a directory glob to --follow. Pair it with -p to see the full diff introduced by each commit:

git log --follow -p -- path/to/file.py
To restrict to a summary of changed lines rather than full diffs, substitute --stat for -p:
git log --follow --stat -- path/to/file.py
For an even tighter view — just the commit hashes and subjects that touched the file — combine with --oneline:
git log --follow --oneline -- path/to/file.py
Git History in VS Code: The Git History Extension
For engineers who prefer a graphical view without leaving the editor, the Git History extension for VS Code — authored by Don Jayamanne — is a de facto standard. It ranks among the most-installed source-control extensions on the VS Code Marketplace (install counts are shown on its Marketplace listing). It is free and MIT-licensed.
What the Git History VS Code Extension Provides
- Visual git log: A searchable, graphical view of the full commit history, equivalent to
git log --graphbut rendered as an interactive UI panel with colour-coded branches. - File history: Right-click any file in the Explorer and select Git: View File History to see every commit that touched it — the visual analogue of
git log --follow -- <file>. - Line history: View the history of a specific line within a file (Git Blame surfaced as a browsable timeline, not just a single-commit annotation).
- Branch and commit comparison: Diff any two commits or branches side by side directly in the VS Code diff viewer.
- Action shortcuts: Cherry-pick commits, create branches from commits, create tags, perform soft/hard resets, and revert commits — all from the same panel without opening a terminal.
Trigger any of these views via the Command Palette (F1 or Ctrl+Shift+P) and typing Git: View History, Git: View File History, or Git: View Line History. The file you want history for must be open in the active editor tab before invoking file or line history commands.
Why it matters: The git history extension for VS Code surfaces information that requires composing half a dozen git log flags at the command line — in a single click. For teams onboarding junior engineers or conducting code reviews, the visual graph and blame integration dramatically reduce the cognitive overhead of understanding why a line exists.
Alternatives to the Git History Extension in VS Code
VS Code's built-in Timeline panel (visible in the Explorer sidebar when a file is open) provides a lightweight file-history view using data from VS Code's native Git extension. It shows commits that touched the current file and allows single-click diffing. It does not require any additional extension, making it a good baseline for teams with restricted Marketplace access. The Git History extension adds branch visualisation, line-level history, and action shortcuts that the built-in Timeline does not offer.
The GitLens extension is a more fully-featured alternative that includes an interactive rebase editor, worktree management, and a richer blame annotation system alongside history views. GitLens offers a free tier with core features and a paid "Pro" tier for advanced capabilities.
Git History Rewrite: The Complete Toolkit
Rewriting history in Git is both routine and, done carelessly, risky. The established tools for a git history rewrite — git rebase -i, git commit --fixup, and autosquash — are powerful but require careful orchestration, especially in stacked-branch workflows where multiple local branch pointers need to follow a rewritten commit. The sections below cover each technique with the detail required to use it safely.
Interactive Rebase: The Foundation of History Rewriting
git rebase -i <base> opens a text editor listing every commit between <base> and HEAD, one per line. Each line begins with an action keyword that controls what Git does with that commit:
| Action | Shorthand | Effect |
|---|---|---|
pick |
p |
Include the commit unchanged |
reword |
r |
Include the commit but pause to edit its message |
edit |
e |
Pause after applying the commit so you can amend it or add more commits |
squash |
s |
Meld this commit into the previous one; combine both messages |
fixup |
f |
Meld this commit into the previous one; discard this commit's message |
drop |
d |
Remove the commit entirely |
exec |
x |
Run a shell command at this point in the sequence |
break |
b |
Pause the rebase without applying a commit (useful for inspection) |
Reorder lines to reorder commits. Delete a line to drop a commit entirely. The sequence is executed top to bottom.
git history reword: Correcting a Commit Message
To reword a commit message buried in history without touching its content, use the reword action in an interactive rebase. Find the commit hash first with git log --oneline, then:
git rebase -i <hash>^
The ^ suffix means "the parent of this commit" — which sets the rebase base just before the target commit. In the editor, change pick to reword (or just r) on the target line, save and close the todo list. Git immediately opens a second editor window pre-populated with the existing commit message; edit it, save, and close. Git completes the rebase automatically.
For a single-commit reword, an even faster route if the commit is the most recent:
git commit --amend
This opens the editor on the last commit's message. If you have no staged changes, only the message is updated; if you have staged changes, those are folded in too.
git history split: Splitting a Bloated Commit
Splitting a commit that bundled multiple unrelated changes requires the edit action in an interactive rebase:

- Run
git rebase -i <hash>^and changepicktoediton the target commit. - Git applies the commit and pauses. At this point, undo the commit while keeping its changes in the working tree.
git reset HEAD^performs a mixed reset (the default): it moves HEAD back to the parent and unstages the commit's changes, but leaves them in your working tree ready to be re-staged in pieces:git reset HEAD^ - Use
git add -p(interactive patch staging) to selectively stage the first logical chunk of changes:git add -p - Commit the staged chunk with an appropriate message:
git commit -m "First logical change" - Stage and commit the remaining changes:
git add -p git commit -m "Second logical change" - Resume the rebase:
git rebase --continue
Git rebuilds every descendant commit on top of the two new commits. The result is a clean linear history where both changes are independently revertable:
Before: A - B - C (B bundles two unrelated changes)
After: A - B1 - B2 - C* (C rebuilt on top of the pair)
git history fixup: Absorbing a Fix into a Past Commit
When you discover a bug in a commit that is no longer at HEAD, the --fixup workflow keeps your history clean. Stage the fix, then create a fixup commit that is semantically linked to the target:
git add <changed files>
git commit --fixup=<hash>
This creates a commit whose message is fixup! <original subject>. Git's autosquash mechanism recognises this prefix. When you next run an interactive rebase with --autosquash, Git automatically places the fixup commit immediately after its target and sets its action to fixup, ready to be collapsed:
git rebase -i --autosquash <base>
To make autosquash the default so you never have to pass the flag manually, set it in your global config:
git config --global rebase.autoSquash true
A related variant, git commit --squash=<hash>, works identically but uses the squash! prefix — which means the editor pauses to let you merge both commit messages rather than silently discarding the fixup's message.
Updating Downstream Branch Refs with --update-refs
One of the most painful aspects of interactive rebase in a stacked-branch workflow is that local branch pointers on descendant branches are not automatically moved to point at the rebased commits. Before Git 2.38, you had to manually run git rebase (or reset each ref) on each downstream branch in turn.
Git 2.38 (released October 2022) introduced --update-refs, which instructs the rebase to automatically update any local branch ref that points to a commit in the rebase range:
git rebase -i --update-refs <base>
To always enable this behaviour, set it globally:
git config --global rebase.updateRefs true
With rebase.updateRefs true in your config, a single interactive rebase session correctly moves all local branch pointers that fell within the rewritten range — making stacked-branch workflows in plain Git substantially more manageable without third-party tooling.
Third-Party Tools: git absorb and Jujutsu
For teams that want even more automation, two tools extend the fixup workflow further:
git absorb— A standalone binary (written in Rust) that inspects your staged changes, automatically determines which past commits they "belong to" based on line-level blame information, and creates appropriately targetedfixup!commits. Runninggit absorbstages those fixups; you then complete the absorption with an autosquash rebase (or pass--and-rebaseto have it run the rebase for you). It is the closest analogue in the Git ecosystem to a single-command "absorb staged changes into the right past commit" workflow.- Jujutsu (
jj) — A VCS with a Git-compatible backend that replaces the working tree with an always-committed model, maintains a first-class operation log (enabling true undo of any rewrite viajj undo/jj op restore), and makes amending arbitrary past commits and splitting commits first-class operations without entering a rebase mode. For teams willing to adopt a new tool, it offers a notably ergonomic history-rewriting experience while still being backed by a Git repository.
Design Constraints: What Interactive Rebase Won't Do Cleanly
Understanding the hard limits of git rebase -i is as important as understanding its capabilities.
- Conflicts pause the operation: Unlike an atomic abort, a conflict during rebase leaves the repository mid-rebase. You must either resolve the conflict and run
git rebase --continue, or abandon the entire operation withgit rebase --abort. This is the primary risk in rewriting deep, heavily-diverged stacks. - Merge commits require care: By default,
git rebase -iflattens a range that contains merge commits unless you pass--rebase-merges(which superseded the now-removed--preserve-merges). Even with that flag, octopus merges and complex merge topologies can produce unexpected results. - No built-in undo: The reflog is your safety net. Before any significant rewrite, note the current HEAD hash (
git rev-parse HEAD) so you can recover withgit reset --hard <saved-hash>if the result is not what you intended. - Shared history must not be rewritten: Rewriting commits that have already been pushed to a remote and pulled by teammates will force-push divergent histories. Coordinate explicitly, or limit rewrites to local branches and commits not yet shared.
Comparing the Git History Rewrite Approaches
| Method | Use case | Branches auto-updated? | Touches index/working tree? | Handles merges? | Conflict behaviour |
|---|---|---|---|---|---|
git rebase -i |
General-purpose interactive rewrite | Only with --update-refs (Git 2.38+) |
Yes (during edit steps) | With --rebase-merges |
Pauses; requires manual resolution or --abort |
git commit --amend |
Fix the most recent commit | No (HEAD branch only) | Yes | N/A (only applies to HEAD) | N/A |
git commit --fixup + rebase --autosquash |
Fold a fix into a past commit | Only with --update-refs |
Yes | No | Pauses; requires manual resolution |
git absorb (third-party) |
Auto-target fixups by blame | Only with subsequent --update-refs rebase |
Yes | No | Pauses; requires manual resolution |
Jujutsu (jj) |
Any rewrite with undo support | Yes (operation log + bookmarks) | Model differs (always committed) | Yes, with first-class conflict objects | Records conflicts; undo via op log |
Key Takeaways
- The git history command has two distinct meanings: the mature
git logecosystem for reading history, and thegit rebase -i/git commit --fixupecosystem for rewriting it — both are essential knowledge for any engineer working on a shared codebase. - For viewing the git history of a file at the command line,
git log --follow -p -- <file>is the most complete form; the--followflag is required to trace history across renames and is limited to one file at a time. - The
--pretty=format:<string>option with placeholders like%h,%an,%ad, and%senables fully custom, script-friendly output fromgit log— use%x09for tab-separated columns. - The Git History extension for VS Code provides file, line, and branch history in a graphical panel. For richer rebase tooling inside VS Code, GitLens adds an interactive rebase editor and worktree management.
git rebase -i --update-refs(available since Git 2.38, October 2022) is the key flag for stacked-branch workflows — it automatically moves all local branch refs that fall within the rewritten range. Setgit config --global rebase.updateRefs trueto enable it permanently.git commit --fixup=<hash>combined withgit rebase -i --autosquashis the idiomatic Git workflow for absorbing a fix into a past commit. Setrebase.autoSquash trueglobally to remove the need to remember the flag.- For conflict-tolerant history rewriting or a first-class undo log, Jujutsu (
jj) is among the most capable alternatives — though it requires a separate tool installation and a shift in mental model. - Never rewrite shared history without coordinating with your team. Even with the cleanest tooling, force-pushing a rewritten branch that teammates have already based work on creates divergence that is painful to reconcile.
What Comes Next for Git History Tooling
The direction of the Git ecosystem has trended toward making history rewriting as deliberate and safe as branching has long been. On the Git core side, the introduction of --update-refs in Git 2.38 removed a significant friction point in stacked-branch workflows. The --rebase-merges machinery continues to narrow the gap between linear and non-linear topologies. Community tools like git absorb automate the blame-based fixup targeting that was previously entirely manual.
One of the most ambitious visions for ergonomic history rewriting today comes from Jujutsu: by making every state of the working tree a first-class commit and maintaining a recoverable operation log, it aims to eliminate the "lost in a half-rebase" failure mode. Whether those ideas influence Git's own roadmap directly — or whether Git grows its own higher-level rewrite interface over time — the design ideas circulating in projects like Jujutsu are part of a broader community conversation about Git's interactive rebase UX. For now, teams running Git 2.38 or later should consider setting rebase.updateRefs globally, experimenting with --autosquash for fixup workflows, and evaluating git absorb or Jujutsu for any workflow where interactive rebase currently feels like too much ceremony.
Topics
Sources
Comments(0)
No comments yet. Be the first to share your thoughts.
Join the conversation
Your email stays private and comments are reviewed before appearing.


