Skip to main content

Command Palette

Search for a command to run...

Git Basics You Actually Need to Know - Part 2

Published
10 min read
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:

  1. The common ancestor (where the branches split)

  2. The latest commit on main

  3. The 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:

  1. Git finds the common ancestor

  2. Saves your commits (D, E) temporarily

  3. Resets your branch to match main

  4. Replays 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.