Git diff is the Swiss Army knife for understanding changes in your codebase, but most people only use it to see what they’ve staged. It’s actually way more powerful, letting you compare literally any two points in your Git history, and the most surprising thing is how seamlessly it bridges the gap between your working directory, your staging area, and your commit history.
Let’s see it in action. Imagine you’ve been working on a feature, made some commits, and now want to see what’s changed between your current work and the main branch.
git diff main
This command shows you everything in your working directory that differs from the main branch, including unstaged changes.
Now, let’s say you’ve staged some of those changes using git add. To see what you’ve staged, you’d run:
git diff --staged
This shows you precisely what will be included in your next commit.
But git diff isn’t limited to just your working directory or staging area. It shines when comparing actual commits. To compare the commit before the last one (HEAD~1) with the very last commit (HEAD), you’d do:
git diff HEAD~1 HEAD
This shows you the exact changes introduced by your most recent commit.
The real power comes from comparing branches. Let’s say you have a branch called feature/new-login and you want to see all the differences between it and main:
git diff feature/new-login main
This command gives you a unified view of all the lines added, removed, or modified in feature/new-login compared to main. It’s your tool for understanding the scope of your feature branch before merging.
You can also compare specific commits on different branches. If you know the commit hash for a particular point on main, say a1b2c3d, and you want to compare it to the latest commit on your feature branch:
git diff a1b2c3d feature/new-login
This is incredibly useful for pinpointing when a specific change was introduced or for reviewing changes made by someone else on a different branch.
The output of git diff uses a standard format. Lines starting with - are deletions, and lines starting with + are additions. The @@ ... @@ lines are hunk headers, indicating the line numbers and context of the changes.
Here’s a common scenario: you’re working on a bug fix, and you want to compare your current working directory including staged changes against the main branch. This is different from git diff main (which ignores staged changes) and git diff --staged (which only shows staged changes). To see everything that’s different from main, whether staged or not, you’d use:
git diff main
If you want to see what you’ve staged relative to main, you’d combine --staged with the branch:
git diff --staged main
This shows you what will be committed if you were to commit right now, but specifically highlights the differences from main.
The mental model to build around git diff is that it’s a comparison tool for states. Your working directory is one state. Your staged changes (the index) are another state. Any commit in your history is a state. git diff lets you pick any two of these states and shows you the deltas. The default behavior of git diff without arguments is to compare your working directory state against your staged index state. git diff --staged compares your staged index state against the HEAD commit state. git diff <commit> compares your working directory state against the specified commit. git diff <commit1> <commit2> compares two specific commit states.
One subtlety often missed is how git diff handles renames and copies. By default, git diff might show a file as deleted and another as added, even if it was just renamed. To get Git to try and detect these, you can use the --find-renames flag:
git diff --find-renames --name-status main
This flag tells git diff to look for files that have been moved or renamed, and it will report them as such (e.g., R100 old_name.txt new_name.txt for a 100% confident rename). This makes understanding the actual structural changes to your repository much clearer than just seeing a file disappear and a new one appear.
Understanding these variations of git diff allows you to precisely track changes, review code effectively, and prevent unintended consequences before they enter your commit history.
The next step is often understanding how to apply these differences using git apply or git checkout.