skip to content
Mohamed Refaat

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).

Terminal window
git log --oneline
# a3f2d1e Fix navbar alignment
# 8b4c2a1 Add user authentication
# 1d5e3f2 Initial commit

The 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
Terminal window
git checkout main
git 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"
Terminal window
git checkout main
git merge feature
# Creates merge commit

Squash Merge

Combines all feature branch commits into a single commit on main.

Terminal window
git checkout main
git merge --squash feature
git 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.

Terminal window
git checkout main
git pull origin main
git checkout feature/my-thing
git rebase main

Git 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:

  1. Pauses and opens a text editor with a list of commits
  2. Waits for you to edit that list (change commands, reorder lines, delete lines)
  3. After you save and close, Git replays the commits according to your instructions

Starting an Interactive Rebase

Terminal window
git rebase -i HEAD~4 # Edit last 4 commits
git rebase -i abc123 # Edit all commits after abc123

This opens your editor with something like:

pick a1b2c3d Add login form
pick e4f5g6h Fix validation bug
pick i7j8k9l WIP
pick m0n1o2p Fix typo in login form

Note: Commits are listed oldest-first (top = oldest). This is the order they’ll be replayed.

Available Commands

CommandShortWhat it does
pickpKeep the commit as-is
rewordrKeep commit, but edit the message
editePause after this commit so you can amend it
squashsCombine with previous commit, keep both messages
fixupfCombine with previous commit, discard this message
dropdDelete 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 form
pick e4f5g6h Add validation
pick i7j8k9l Fix validation typo
pick m0n1o2p Fix another typo

You want to combine the typo fixes into the validation commit. Edit to:

pick a1b2c3d Add login form
pick e4f5g6h Add validation
fixup i7j8k9l Fix validation typo
fixup m0n1o2p Fix another typo

Save 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 form
pick e4f5g6h Fix unrelated bug
pick i7j8k9l Add login validation

You want the login commits together. Move the lines:

pick a1b2c3d Add login form
pick i7j8k9l Add login validation
pick e4f5g6h Fix unrelated bug

Example: Editing a Commit

You need to change the actual code in an old commit (not just the message).

pick a1b2c3d Add login form
edit e4f5g6h Add validation # <- change pick to edit
pick i7j8k9l WIP

Save and close. Git will pause after applying e4f5g6h:

Terminal window
# Git pauses here. Make your changes to the files, then:
git add .
git commit --amend # Updates the paused commit
git rebase --continue # Continues with remaining commits

Example: Dropping a Commit

That “WIP” commit shouldn’t exist. Delete the line or change pick to drop:

pick a1b2c3d Add login form
pick e4f5g6h Add validation
drop i7j8k9l WIP

The WIP commit is removed from history.

Handling Conflicts

If Git encounters conflicts while replaying commits:

  1. It pauses and shows which files have conflicts
  2. Fix the conflicts manually
  3. Run git add . to mark them resolved
  4. Run git rebase --continue to 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.

<<<<<<< HEAD
const 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:

Terminal window
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
Terminal window
git checkout --theirs path/to/file.js
git add path/to/file.js

To abort and start over:

Terminal window
git rebase --abort
git merge --abort

Revert 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.

Terminal window
git revert abc123

Before:

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).

Terminal window
git reset --soft HEAD~2

Before:

A -- B -- C -- D (HEAD)
staged: nothing
working dir: clean

After:

A -- B (HEAD)
staged: changes from C and D
working dir: clean

Use 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.

Terminal window
git reset HEAD~2 # --mixed is the default
git reset --mixed HEAD~2

After:

A -- B (HEAD)
staged: nothing
working dir: changes from C and D

Use 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.

Terminal window
git reset --hard HEAD~2

After:

A -- B (HEAD)
staged: nothing
working dir: clean (matches B)

Warning: This permanently deletes uncommitted work. Use with caution.

Quick Comparison

CommandHistoryStagedWorking Dir
revertAdds new commit
reset --softRemoves commitsKeeps changesUnchanged
reset --mixedRemoves commitsClearsKeeps changes
reset --hardRemoves commitsClearsClears

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

Terminal window
# Unstaged changes (working dir vs staged)
git diff
# Staged changes (staged vs last commit)
git diff --staged
git diff --cached # same as --staged
# All changes since last commit (working dir vs last commit)
git diff HEAD
# Changes between two commits
git diff abc123 def456
# Changes between two branches
git diff main feature
git diff main..feature # same thing

Useful Options

Terminal window
# Show only file names that changed
git 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 changes
git diff -w main feature
git diff --ignore-all-space main feature
# Show word-level diff instead of line-level
git diff --word-diff main feature
# Limit to specific file or directory
git diff main feature -- src/components/
git diff main feature -- src/utils/date.ts

Reading Diff Output

diff --git a/src/utils.ts b/src/utils.ts
index 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.

Terminal window
# Open difftool for all changes
git difftool main feature
# Configure default difftool (one-time setup)
git config --global diff.tool vscode
git config --global difftool.vscode.cmd 'code --wait --diff $LOCAL $REMOTE'
# Other popular difftools
git config --global diff.tool meld
git config --global diff.tool kdiff3
git config --global diff.tool vimdiff

After configuration:

Terminal window
git difftool # Compare working dir with staged
git difftool main feature # Compare branches
git difftool HEAD~3 # Compare with 3 commits ago

Comparing Branches with Git Log

git log with range syntax shows commits that exist in one branch but not another.

Double Dot Syntax (..)

Terminal window
# Commits in feature that are NOT in main
git log main..feature
# Commits in main that are NOT in feature
git log feature..main

Think of it as: “show me what’s new in the second branch compared to the first.”

Terminal window
# Concise output
git log --oneline main..feature
# With graph
git log --oneline --graph main..feature

Triple Dot Syntax (…)

Terminal window
# Commits in either branch, but not in both (symmetric difference)
git log main...feature
git log --oneline --graph --left-right main...feature

The --left-right flag marks commits with < (left branch) or > (right branch).

Practical Examples

Terminal window
# 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 main
git log --oneline main..feature | wc -l
# Show actual changes (patches) for each commit
git log -p main..feature

Reflog: Your Recovery Tool

git reflog logs every HEAD movement — commits, checkouts, rebases, resets. Almost nothing is truly lost.

Terminal window
git reflog
a3f2d1e HEAD@{0}: rebase finished
8b4c2a1 HEAD@{1}: rebase: checkout main
1d5e3f2 HEAD@{2}: commit: WIP
f4e3d2c HEAD@{3}: checkout: moving from main to feature

Recovering from mistakes

Accidentally ran git reset --hard or a rebase went wrong:

Terminal window
git reflog
git 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, -w for different views
  • Log ranges: main..feature shows commits in feature not in main
  • Reflog to recover from almost any mistake
Table of Contents