Git Basics You Actually Need to Know - Part 2

In Part 1, we covered commits, branches, and basic Git workflows. You know how to create branches and make commits. Now comes the hard part: bringing those branches back together.
This is where most people get stuck. Merge conflicts, rebasing gone wrong, accidentally deleted commits. Let's demystify all of it.
This is Part 2 of the Git Basics series. Read Part 1 here for fundamentals like commits, branches, and reset.
Merging: Bringing Branches Together
You've been working on a feature branch. It's done. Now you need to integrate it back into main. That's merging.
# Switch to the branch you want to merge INTO
git checkout main
# Merge the feature branch
git merge feature/login
Git looks at three commits:
The common ancestor (where the branches split)
The latest commit on
mainThe latest commit on
feature/login
Then it creates a new commit that combines both branches.
Fast-Forward Merge
If main hasn't changed since you created the branch, Git does a "fast-forward" merge:
# Before merge
main: A - B - C
\
feature: D - E
# After fast-forward merge
main: A - B - C - D - E
No merge commit needed. Git just moves the main pointer forward. Clean and simple.
git merge feature/login
# Output: Fast-forward
Three-Way Merge
If both branches have new commits, Git creates a merge commit:
# Before merge
main: A - B - C - F
\
feature: D - E
# After three-way merge
main: A - B - C - F - M
\ /
feature: D - E
The merge commit M has two parents: F from main and E from feature.
git merge feature/login
# Output: Merge made by the 'recursive' strategy
This creates a visible merge in the history. Some teams like this because it shows where features were integrated. Others find it noisy.
Merge Conflicts: When Git Can't Decide
Conflicts happen when both branches modified the same lines of the same file. Git doesn't know which version to keep.
git merge feature/login
# Output: CONFLICT (content): Merge conflict in src/app.js
# Automatic merge failed; fix conflicts and then commit the result.
Open the conflicted file. You'll see markers:
function authenticate(user) {
<<<<<<< HEAD
return jwt.sign({ id: user.id }, SECRET);
=======
return jwt.sign({ userId: user.id, role: user.role }, SECRET);
>>>>>>> feature/login
}
The section between <<<<<<< HEAD and ======= is your current branch. The section between ======= and >>>>>>> feature/login is the incoming branch.
Fix it manually:
function authenticate(user) {
return jwt.sign({ userId: user.id, role: user.role }, SECRET);
}
Remove the conflict markers, keep what you want, then:
git add src/app.js
git commit -m "Merge feature/login"
Abort a merge:
If you want to give up and start over:
git merge --abort
This puts everything back to before you started the merge.
Rebasing: The Alternative to Merging
Rebasing rewrites history. Instead of creating a merge commit, it replays your commits on top of another branch.
# You're on feature/login
git rebase main
What happens:
Git finds the common ancestor
Saves your commits (D, E) temporarily
Resets your branch to match
mainReplays your commits one by one on top of
main
# Before rebase
main: A - B - C - F
\
feature: D - E
# After rebase
main: A - B - C - F
\
feature: D' - E'
Notice D' and E' — these are new commits with different SHAs. Same changes, different history.
Why Rebase?
Linear history: No merge commits cluttering the log. Clean, straight line.
Easier to read: git log --oneline shows a simple sequence of changes.
Better for code review: Each commit stands alone, no merge noise.
The Golden Rule of Rebasing
Never rebase commits that you've pushed to a shared branch.
If other people have based work on your commits, rebasing rewrites history under them. Their branches break. People get angry. Projects get messy.
Rebase private branches only. Once you push to a shared branch, merge instead.
Interactive Rebase
Want to clean up your commits before merging? Interactive rebase lets you rewrite history:
git rebase -i HEAD~3
This opens an editor showing your last 3 commits:
pick a3f2d91 WIP
pick b8e4a12 Fix typo
pick c9d5b23 Add authentication
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like squash, but discard commit message
# d, drop = remove commit
Common operations:
Squash commits together:
pick a3f2d91 Add authentication
squash b8e4a12 Fix typo
squash c9d5b23 Add tests
Combines all three into one commit.
Reword a commit message:
pick a3f2d91 WIP
reword b8e4a12 Add user authentication
Changes the commit message without changing the code.
Drop a commit:
pick a3f2d91 Add authentication
drop b8e4a12 Debug logging
pick c9d5b23 Add tests
Removes that commit entirely.
Merge vs Rebase: When to Use Which
Use merge when:
Working on a shared branch
You want to preserve the full history of when features were integrated
You're merging a long-lived feature branch into main
Use rebase when:
Updating your feature branch with latest main changes
Cleaning up local commits before pushing
You want a linear history
Common workflow:
# While working on feature branch, keep it updated
git checkout feature/login
git pull origin main --rebase
# When ready to merge, use merge for the final integration
git checkout main
git merge feature/login
This keeps your feature branch clean (rebased) but preserves merge history in main.
Reflog: The Git Time Machine
You messed up. You ran git reset --hard and deleted commits. Or rebased wrong and lost work. Don't panic.
git reflog records every time HEAD moves. Every commit, reset, rebase, checkout — it's all there.
git reflog
Output:
c9d5b23 HEAD@{0}: reset: moving to HEAD~2
e2b7d45 HEAD@{1}: commit: Add tests
d1a6c34 HEAD@{2}: commit: Fix authentication
b8e4a12 HEAD@{3}: commit: Add user model
a3f2d91 HEAD@{4}: commit (initial): Initial commit
Each entry has:
The commit SHA
What action was taken
The commit message
Recover a deleted commit:
You accidentally deleted commits d1a6c34 and e2b7d45. They're in reflog:
# Create a branch at the lost commit
git branch recovery e2b7d45
# Or reset to it
git reset --hard e2b7d45
Your "deleted" commits are back.
Reflog expires after 90 days by default. After that, commits are truly gone. But 90 days is usually enough to realize you made a mistake.
Recovering from a Bad Rebase
# You rebased and everything went wrong
git reflog
# Find the commit before the rebase
# HEAD@{1}: rebase: checkout main
# HEAD@{2}: commit: Your last good commit
# Reset to before the rebase
git reset --hard HEAD@{2}
The bad rebase is undone. You're back to before you started.
Recovering Uncommitted Work
Reflog only tracks committed work. If you lost uncommitted changes with git reset --hard, they're gone forever.
Exception: If you used git stash, stashed changes are tracked:
git reflog show stash
But we'll cover stashing in Part 3.
Cherry-Pick: Copying Commits
Sometimes you want just one commit from another branch, not the whole branch.
# You're on main, want commit abc123 from feature branch
git cherry-pick abc123
This creates a new commit on main with the same changes as abc123. The original commit stays on the feature branch.
Use case: A bug fix was committed to the wrong branch:
# Bug fix is on feature/login but needed on main
git checkout main
git cherry-pick bug-fix-commit
git checkout feature/login
git rebase main # Now feature has the fix too
Cherry-pick a range:
git cherry-pick abc123..def456
Applies all commits from abc123 to def456.
Handling Complex Merges
Merge Strategies
Git has different merge strategies for different situations:
Recursive (default): Works for most cases. Handles renames well.
Ours: In conflicts, always keep our version:
git merge -X ours feature/login
Theirs: In conflicts, always keep their version:
git merge -X theirs feature/login
⚠️ These still create a merge commit. They just resolve conflicts automatically.
Viewing Merge History
See where branches were merged:
git log --graph --oneline --all
Output:
* c9d5b23 (HEAD -> main) Merge feature/login
|\
| * e2b7d45 (feature/login) Add tests
| * d1a6c34 Fix authentication
* | b8e4a12 Update dependencies
|/
* a3f2d91 Initial commit
The graph shows exactly how branches diverged and merged.
Real-World Scenarios
Scenario 1: Update Feature Branch with Latest Main
# Option 1: Rebase (cleaner history)
git checkout feature/login
git pull origin main --rebase
# Option 2: Merge (preserves history)
git checkout feature/login
git merge main
Scenario 2: Squash Feature Branch Before Merging
git checkout feature/login
git rebase -i main
# In the editor, squash all commits into one
# Then merge into main
git checkout main
git merge feature/login
Scenario 3: Undo a Merge
If you haven't pushed:
git reset --hard HEAD~1
If you have pushed:
# Create a new commit that undoes the merge
git revert -m 1 HEAD
The -m 1 flag tells Git which parent to revert to (the first parent, which is main).
Scenario 4: Recover Deleted Branch
# Find the last commit on the deleted branch
git reflog
# Create a new branch at that commit
git branch recovered-branch abc123
What We Covered
You now understand:
Merging: fast-forward vs three-way merges
Handling merge conflicts step by step
Rebasing: rewriting history for cleaner logs
When to merge vs when to rebase
Interactive rebase for cleaning up commits
Reflog: recovering "deleted" commits
Cherry-picking specific commits
Real-world merge and rebase workflows
In Part 3, we'll cover worktrees for working on multiple branches simultaneously, and stashing for saving uncommitted work.
Common Mistakes to Avoid
Rebasing pushed commits: Rewrites history, breaks other people's branches. Merge instead.
Not pulling before rebasing: You'll rebase onto old code. Always git pull first.
Force pushing to shared branches: git push -f overwrites remote history. Only force push to your own feature branches.
Ignoring conflicts: The conflict markers must be removed. Git won't let you commit with <<<<<<< in your files.
Rebasing main onto feature: Rebase feature onto main, not the other way around.
TLDR;
Merging combines branches by creating a merge commit. Fast-forward merges happen when one branch is ahead. Three-way merges happen when both branches have new commits. Conflicts occur when both branches modify the same lines — fix them manually, remove markers, then commit. Rebasing replays your commits on top of another branch for linear history. Never rebase pushed commits. Use git rebase -i to squash, reword, or drop commits. Reflog tracks every HEAD movement for 90 days — use it to recover "deleted" commits with git reset --hard commit-sha. Cherry-pick copies specific commits between branches. Merge for final integration, rebase for keeping feature branches clean. Part 3 covers worktrees and stashing.





