Demystifying Git, making sense of what we write daily.
/ 10 min read
The Problem
Most developers use Git daily but only know a handful of commands. When something breaks, we panic. This article covers the concepts that will give you actual control over your repository: hashes, merge types, rebasing, conflict resolution, and reflog.
What Is a Hash?
Every commit has a unique 40-character identifier (SHA-1 hash). Git generates it by hashing the commit’s contents, author, timestamp, and parent commit(s).
git log --oneline# a3f2d1e Fix navbar alignment# 8b4c2a1 Add user authentication# 1d5e3f2 Initial commitThe short version is just the first 7 characters — enough to uniquely identify a commit in most repos.
Key insight: commits are snapshots, not diffs. Each commit stores the complete state of your project. Git uses content-addressable storage to avoid duplicating unchanged files.
Merge Types
Fast-Forward Merge
Happens when your feature branch is ahead of main with no divergence. Git simply moves the main pointer forward.
gitGraph commit id: "A" commit id: "B" branch feature commit id: "C" commit id: "D" checkout main merge feature
git checkout maingit merge --ff-only feature# Fast-forward (no merge commit)Three-Way Merge
Happens when both branches have new commits. Git creates a merge commit with two parents.
gitGraph commit id: "A" commit id: "B" branch feature commit id: "C" checkout main commit id: "D" checkout feature commit id: "E" checkout main merge feature id: "M"
git checkout maingit merge feature# Creates merge commitSquash Merge
Combines all feature branch commits into a single commit on main.
git checkout maingit merge --squash featuregit commit -m "Add user authentication feature"When to use each:
- Fast-forward: Linear history, branch is up-to-date
- Three-way merge: Preserve full parallel development history
- Squash: Clean main history, messy feature commits
Rebase Main Before Opening PRs
Before opening a pull request, always rebase your feature branch onto the latest main. This ensures your PR won’t conflict with recent changes.
git checkout maingit pull origin main
git checkout feature/my-thinggit rebase mainGit temporarily removes your commits, updates your branch to match main, then replays your commits on top.
Before rebase — feature branched from B, but main now has E:
gitGraph commit id: "A" commit id: "B" branch feature commit id: "C" commit id: "D" checkout main commit id: "E" tag: "new"
After git rebase main — feature commits replayed on top of E:
gitGraph commit id: "A" commit id: "B" commit id: "E" branch feature commit id: "C'" type: HIGHLIGHT commit id: "D'" type: HIGHLIGHT
The replayed commits (C', D') have new hashes because their parent changed from B to E.
Interactive Rebase
Interactive rebase (git rebase -i) gives you full control over your commit history. You can combine commits, delete them, reorder them, or edit their content.
How It Works
When you run interactive rebase, Git:
- Pauses and opens a text editor with a list of commits
- Waits for you to edit that list (change commands, reorder lines, delete lines)
- After you save and close, Git replays the commits according to your instructions
Starting an Interactive Rebase
git rebase -i HEAD~4 # Edit last 4 commitsgit rebase -i abc123 # Edit all commits after abc123This opens your editor with something like:
pick a1b2c3d Add login formpick e4f5g6h Fix validation bugpick i7j8k9l WIPpick m0n1o2p Fix typo in login formNote: Commits are listed oldest-first (top = oldest). This is the order they’ll be replayed.
Available Commands
| Command | Short | What it does |
|---|---|---|
pick | p | Keep the commit as-is |
reword | r | Keep commit, but edit the message |
edit | e | Pause after this commit so you can amend it |
squash | s | Combine with previous commit, keep both messages |
fixup | f | Combine with previous commit, discard this message |
drop | d | Delete the commit entirely |
You can also reorder commits by moving lines up or down.
Example: Squashing Commits
You have these commits:
pick a1b2c3d Add login formpick e4f5g6h Add validationpick i7j8k9l Fix validation typopick m0n1o2p Fix another typoYou want to combine the typo fixes into the validation commit. Edit to:
pick a1b2c3d Add login formpick e4f5g6h Add validationfixup i7j8k9l Fix validation typofixup m0n1o2p Fix another typoSave and close. Git will combine commits i7j8k9l and m0n1o2p into e4f5g6h. Result: 2 clean commits instead of 4.
Example: Reordering Commits
Original order:
pick a1b2c3d Add login formpick e4f5g6h Fix unrelated bugpick i7j8k9l Add login validationYou want the login commits together. Move the lines:
pick a1b2c3d Add login formpick i7j8k9l Add login validationpick e4f5g6h Fix unrelated bugExample: Editing a Commit
You need to change the actual code in an old commit (not just the message).
pick a1b2c3d Add login formedit e4f5g6h Add validation # <- change pick to editpick i7j8k9l WIPSave and close. Git will pause after applying e4f5g6h:
# Git pauses here. Make your changes to the files, then:git add .git commit --amend # Updates the paused commitgit rebase --continue # Continues with remaining commitsExample: Dropping a Commit
That “WIP” commit shouldn’t exist. Delete the line or change pick to drop:
pick a1b2c3d Add login formpick e4f5g6h Add validationdrop i7j8k9l WIPThe WIP commit is removed from history.
Handling Conflicts
If Git encounters conflicts while replaying commits:
- It pauses and shows which files have conflicts
- Fix the conflicts manually
- Run
git add .to mark them resolved - Run
git rebase --continueto proceed
To cancel the entire rebase: git rebase --abort
The Golden Rule
Never rebase commits that have been pushed and shared with others.
Interactive rebase rewrites history — it creates new commits with new hashes. If someone else has pulled your original commits, their history will diverge from yours. Only rebase your local, unpushed work.
Conflict Resolution
Conflicts happen when Git can’t automatically merge changes to the same lines.
<<<<<<< HEADconst timeout = 5000;=======const timeout = 10000;>>>>>>> feature/my-changes- Top section (
HEAD): current branch - Bottom section: incoming changes
Fix by choosing one, combining both, or writing something new. Then:
git add .git rebase --continue # or git merge --continue—ours vs —theirs
During merge:
--ours= branch you’re on--theirs= branch being merged in
During rebase:
--ours= branch you’re rebasing onto--theirs= your commits being replayed
git checkout --theirs path/to/file.jsgit add path/to/file.jsTo abort and start over:
git rebase --abortgit merge --abortRevert vs Reset
Two ways to undo changes in Git — but they work very differently.
Git Revert
git revert creates a new commit that undoes the changes from a previous commit. History is preserved.
git revert abc123Before:
A -- B -- C -- D (HEAD)After:
A -- B -- C -- D -- D' (HEAD)Commit D' contains the inverse of D’s changes. The original commit D still exists in history.
When to use: When you’ve already pushed commits and others are working with them. Revert is safe for shared branches because it doesn’t rewrite history.
Git Reset
git reset moves the HEAD pointer backward, effectively removing commits from the current branch. There are three modes:
—soft
Moves HEAD back but keeps all changes staged (ready to commit).
git reset --soft HEAD~2Before:
A -- B -- C -- D (HEAD) staged: nothing working dir: cleanAfter:
A -- B (HEAD) staged: changes from C and D working dir: cleanUse case: You want to redo the last few commits as a single commit.
—mixed (default)
Moves HEAD back and unstages changes, but keeps them in your working directory.
git reset HEAD~2 # --mixed is the defaultgit reset --mixed HEAD~2After:
A -- B (HEAD) staged: nothing working dir: changes from C and DUse case: You want to reorganize changes into different commits.
—hard
Moves HEAD back and discards all changes. Working directory is reset to match the target commit.
git reset --hard HEAD~2After:
A -- B (HEAD) staged: nothing working dir: clean (matches B)Warning: This permanently deletes uncommitted work. Use with caution.
Quick Comparison
| Command | History | Staged | Working Dir |
|---|---|---|---|
revert | Adds new commit | — | — |
reset --soft | Removes commits | Keeps changes | Unchanged |
reset --mixed | Removes commits | Clears | Keeps changes |
reset --hard | Removes commits | Clears | Clears |
When to Use Which
- revert: Undo a commit on a shared branch (safe, preserves history)
- reset —soft: Redo recent commits differently
- reset —mixed: Unstage changes and reorganize
- reset —hard: Completely discard recent work (can recover with reflog)
Comparing Changes: diff, difftool, and log
Git Diff
git diff shows line-by-line changes between commits, branches, or your working directory.
Common Use Cases
# Unstaged changes (working dir vs staged)git diff
# Staged changes (staged vs last commit)git diff --stagedgit diff --cached # same as --staged
# All changes since last commit (working dir vs last commit)git diff HEAD
# Changes between two commitsgit diff abc123 def456
# Changes between two branchesgit diff main featuregit diff main..feature # same thingUseful Options
# Show only file names that changedgit diff --name-only main feature
# Show file names with status (added, modified, deleted)git diff --name-status main feature
# Show condensed stats (files changed, insertions, deletions)git diff --stat main feature
# Ignore whitespace changesgit diff -w main featuregit diff --ignore-all-space main feature
# Show word-level diff instead of line-levelgit diff --word-diff main feature
# Limit to specific file or directorygit diff main feature -- src/components/git diff main feature -- src/utils/date.tsReading Diff Output
diff --git a/src/utils.ts b/src/utils.tsindex abc123..def456 100644--- a/src/utils.ts+++ b/src/utils.ts@@ -10,7 +10,7 @@ export function calculate() { const value = 100;- return value * 2;+ return value * 3; }---and+++: old file (a) and new file (b)@@: line numbers context (line 10, 7 lines shown)-: line removed (red in terminal)+: line added (green in terminal)
Git Difftool
git difftool opens an external diff viewer instead of showing diff in the terminal. Useful for complex changes.
# Open difftool for all changesgit difftool main feature
# Configure default difftool (one-time setup)git config --global diff.tool vscodegit config --global difftool.vscode.cmd 'code --wait --diff $LOCAL $REMOTE'
# Other popular difftoolsgit config --global diff.tool meldgit config --global diff.tool kdiff3git config --global diff.tool vimdiffAfter configuration:
git difftool # Compare working dir with stagedgit difftool main feature # Compare branchesgit difftool HEAD~3 # Compare with 3 commits agoComparing Branches with Git Log
git log with range syntax shows commits that exist in one branch but not another.
Double Dot Syntax (..)
# Commits in feature that are NOT in maingit log main..feature
# Commits in main that are NOT in featuregit log feature..mainThink of it as: “show me what’s new in the second branch compared to the first.”
# Concise outputgit log --oneline main..feature
# With graphgit log --oneline --graph main..featureTriple Dot Syntax (…)
# Commits in either branch, but not in both (symmetric difference)git log main...featuregit log --oneline --graph --left-right main...featureThe --left-right flag marks commits with < (left branch) or > (right branch).
Practical Examples
# What will this PR add to main?git log --oneline main..feature
# What's been merged to main since I branched?git log --oneline feature..main
# Count commits in feature not yet in maingit log --oneline main..feature | wc -l
# Show actual changes (patches) for each commitgit log -p main..featureReflog: Your Recovery Tool
git reflog logs every HEAD movement — commits, checkouts, rebases, resets. Almost nothing is truly lost.
git refloga3f2d1e HEAD@{0}: rebase finished8b4c2a1 HEAD@{1}: rebase: checkout main1d5e3f2 HEAD@{2}: commit: WIPf4e3d2c HEAD@{3}: checkout: moving from main to featureRecovering from mistakes
Accidentally ran git reset --hard or a rebase went wrong:
git refloggit reset --hard HEAD@{2}This restores your repo to that exact state. Reflog entries are kept for ~90 days by default.
Summary
- Hashes are unique commit fingerprints based on content + metadata
- Merge types: fast-forward (linear), three-way (merge commit), squash (single commit)
- Rebase before PR to keep your branch current and avoid conflicts
- Interactive rebase to clean up commit history before sharing
- Revert creates a new commit that undoes changes (safe for shared branches)
- Reset moves HEAD backward:
--soft(keep staged),--mixed(keep unstaged),--hard(discard all) - Diff shows line-by-line changes; use
--stat,--name-only,-wfor different views - Log ranges:
main..featureshows commits in feature not in main - Reflog to recover from almost any mistake