Learning Git: The Practical Guide I Wish I Had
When I first started using Git, I could git add, git commit, and git push. That was it. Everything else felt like black magic — rebasing, squashing, cherry-picking. I'd Google the same commands over and over.
This is the guide I wish I had from day one. No theory-heavy explanations — just the mental models and commands you actually need.
The Commit Tree
Before anything else, understand this: Git is a tree of snapshots. Every commit is a node that points to its parent. A branch is just a pointer to a specific commit.
main: A --- B --- C
\
feature: D --- E --- F
When you're on feature, you're at commit F. When you merge into main, you're telling Git to combine the history of both branches. That's it. Everything else in Git is just different ways of manipulating this tree.
HEAD is where you currently are. It usually points to a branch name, which points to a commit. When people say "detached HEAD," it means you're pointing directly at a commit instead of a branch.
Git Fetch vs Git Pull
This trips everyone up early on.
git fetch downloads new commits from the remote but doesn't touch your working directory. It updates your remote-tracking branches (origin/main, origin/feature, etc.) so you can see what's changed.
git pull is git fetch + git merge. It downloads and integrates the changes into your current branch.
# Safe way: fetch first, then decide what to do
git fetch origin
git log origin/main..main # See what's different
# Quick way: pull and merge
git pull origin main
When to use which:
- Use
fetchwhen you want to see what changed before integrating - Use
pullwhen you're confident you just want the latest changes - Use
pull --rebasewhen you want to replay your commits on top of the latest remote (cleaner history)
Rebasing: Rewriting History
Rebasing moves your branch's commits to start from a different point. Instead of a merge commit, you get a linear history.
# You're on your feature branch
git rebase main
Before rebase:
main: A --- B --- C
\
feature: D --- E
After rebase:
main: A --- B --- C
\
feature: D' --- E'
Your commits D and E get replayed on top of C. They're technically new commits (hence D' and E') because their parent changed.
The golden rule: Never rebase commits that other people have based work on. Rebase your own feature branches before merging — don't rebase main.
Squashing Commits with Interactive Rebase
This is the one I use constantly. You've been working on a feature and you have 5 commits like:
fix typo
wip
actually fix the thing
forgot to add file
final final version
Nobody wants to see that in the git log. Here's how to squash them into one clean commit.
Step-by-Step
1. Start the interactive rebase
From your feature branch (not main):
git rebase -i HEAD~N
Where N is the number of commits you want to combine. You can check your PR/MR to see how many commits are in it.
2. The Vim editor opens
You'll see something like this:
pick abc1234 fix typo
pick def5678 wip
pick ghi9012 actually fix the thing
pick jkl3456 forgot to add file
pick mno7890 final final version
Press I to enter insert mode. Change pick to squash (or just s) for every commit except the top one:
pick abc1234 fix typo
squash def5678 wip
squash ghi9012 actually fix the thing
squash jkl3456 forgot to add file
squash mno7890 final final version
Press Esc, then type :wq and hit Enter to save.
3. Edit the commit message
Another Vim window opens with all the commit messages combined. Press I to enter insert mode. Put # in front of the messages you don't want — lines starting with # are comments and get ignored:
Add user authentication feature
# fix typo
# wip
# actually fix the thing
# forgot to add file
# final final version
Press Esc, then :wq to save.
4. Push
Since you rewrote history, you'll need to force push:
git push --force-with-lease
Use --force-with-lease instead of --force — it's safer because it checks that nobody else has pushed to the branch since your last fetch.
Resolving Merge Conflicts
Merge conflicts happen when two branches modify the same lines. Git doesn't know which version to keep, so it asks you.
# You're trying to merge or rebase and see:
# CONFLICT (content): Merge conflict in src/app.tsx
Open the file and you'll see conflict markers:
<<<<<<< HEAD
const title = "My App";
=======
const title = "Our App";
>>>>>>> feature-branch
- Everything between
<<<<<<< HEADand=======is your current branch's version - Everything between
=======and>>>>>>>is the incoming branch's version
To resolve:
- Delete the conflict markers (
<<<<<<<,=======,>>>>>>>) - Keep the code you want (or combine both)
- Save the file
- Stage and continue
# After fixing the file
git add src/app.tsx
# If you were merging:
git merge --continue
# If you were rebasing:
git rebase --continue
Tips for fewer conflicts:
- Rebase your branch on
mainfrequently so you're never too far behind - Keep PRs small and focused — fewer changed files means fewer conflicts
- Communicate with your team about who's working on what
Useful Commands I Actually Use
# See commit history (compact)
git log --oneline --graph
# See what changed in the last commit
git show
# Undo the last commit but keep the changes
git reset --soft HEAD~1
# Stash changes temporarily
git stash
git stash pop
# See which branches exist and where they point
git branch -vv
# Delete a local branch
git branch -d feature-branch
# See the diff of staged changes
git diff --cached
# Blame a file (who changed what)
git blame src/app.tsx
# Cherry-pick a specific commit onto your branch
git cherry-pick abc1234
The Mental Model
Here's how I think about Git now:
- Commits are snapshots, not diffs (Git stores the full state)
- Branches are just movable pointers to commits — they're cheap
- Merge creates a new commit with two parents
- Rebase replays commits onto a new base (rewrites history)
- HEAD is where you are right now
- Remote is just another copy of the repo with its own branches
Once you internalize the tree model, everything clicks. You stop memorizing commands and start understanding why they work.
Git is not hard. It's just poorly explained most of the time.
The best way to learn is to break things in a throwaway repo. Create a test repo, make some branches, practice rebasing and squashing. You'll have it down in an afternoon.