In the previous tutorial, we learned to create branches and work on them. But branches are only useful if you can bring the work back together.
Git gives you two ways to combine branches: merge and rebase. In this tutorial, you will learn both, understand the difference, and know when to use each one. We will also cover merge conflicts and git stash.
What Is Merging?
Merging takes two branches and combines them. You switch to the branch you want to update, then merge the other branch into it.
# Switch to main
git switch main
# Merge feature branch into main
git merge feature/login
That is it. Git takes the changes from feature/login and adds them to main.
But there are two types of merges, and they behave differently.
Fast-Forward Merge
A fast-forward merge happens when the target branch has no new commits since the feature branch was created.
Before merge:
main: A --- B
\
feature: C --- D
After fast-forward merge:
main: A --- B --- C --- D
Git does not create a new commit. It just moves the main pointer forward to where feature is. This is the simplest kind of merge.
git switch main
git merge feature/greeting
Updating b2c3d4e..e5f6g7h
Fast-forward
welcome.txt | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 welcome.txt
The word “Fast-forward” in the output tells you this was a fast-forward merge.
Merge Commit
A merge commit happens when both branches have new commits. Git cannot just move the pointer — it needs to create a new commit that combines both sets of changes.
Before merge:
main: A --- B --- E
\
feature: C --- D
After merge commit:
main: A --- B --- E --- F (merge commit)
\ /
feature: C --- D -
git switch main
git merge feature/navigation
Merge made by the 'ort' strategy.
nav.html | 1 +
1 file changed, 1 insertion(+)
create mode 100644 nav.html
Git opens your text editor to write a merge commit message. The default message is usually fine:
Merge branch 'feature/navigation'
If you want to always create a merge commit (even when fast-forward is possible), use:
git merge --no-ff feature/login
Some teams prefer this because it keeps a clear record of when each feature was merged.
Merge Conflicts
A merge conflict happens when two branches change the same part of the same file. Git does not know which version to keep, so it asks you to decide.
Creating a Conflict
Let’s create a conflict on purpose:
# On main, edit line 1 of hello.txt
git switch main
echo "Hello from main!" > hello.txt
git add hello.txt
git commit -m "Update greeting on main"
# On feature branch, edit the same line
git switch -c feature/new-greeting
echo "Hello from feature!" > hello.txt
git add hello.txt
git commit -m "Update greeting on feature"
# Now try to merge
git switch main
git merge feature/new-greeting
Auto-merging hello.txt
CONFLICT (content): Merge conflict in hello.txt
Automatic merge failed; fix conflicts and then commit the result.
What a Conflict Looks Like
Open the conflicted file:
<<<<<<< HEAD
Hello from main!
=======
Hello from feature!
>>>>>>> feature/new-greeting
Git marks the conflict with special markers:
<<<<<<< HEAD— the start of your current branch’s version=======— the separator between the two versions>>>>>>> feature/new-greeting— the end of the incoming branch’s version
Resolving the Conflict
Edit the file and keep what you want. Remove the conflict markers:
Hello from main and feature!
Then stage and commit:
git add hello.txt
git commit -m "Merge feature/new-greeting, combine greetings"
The conflict is resolved.
Aborting a Merge
If you do not want to deal with conflicts right now, you can cancel the merge:
git merge --abort
This puts everything back to how it was before the merge.
What Is Rebasing?
Rebasing is another way to combine branches. Instead of creating a merge commit, it replays your commits on top of another branch.
Before rebase:
main: A --- B --- E
\
feature: C --- D
After rebasing feature onto main:
main: A --- B --- E
\
feature: C' --- D'
The commits C and D get replayed on top of E, creating new commits C’ and D’. They have the same changes but different hashes.
git switch feature/footer
git rebase main
Successfully rebased and updated refs/heads/feature/footer.
After rebasing, you can merge with a clean fast-forward:
git switch main
git merge feature/footer
The result is a clean, linear history with no merge commit.
Merge vs Rebase
Both combine branches. Here is when to use each:
| Merge | Rebase | |
|---|---|---|
| Creates | Merge commit | No extra commit |
| History | Shows branch structure | Linear, clean |
| Safe for shared branches? | Yes | No |
| Best for | Merging to main | Updating your feature branch |
The golden rule: Never rebase a branch that other people are working on. Rebase changes commit hashes. If someone else has the old commits, things will break.
When to rebase:
# Your feature branch is behind main
# Rebase to get the latest main changes
git switch feature/my-work
git rebase main
When to merge:
# Your feature is done, merge it into main
git switch main
git merge feature/my-work
A common workflow: rebase your feature branch onto main to keep it up to date, then merge into main when the feature is done.
Rebase Conflicts
Rebase can also have conflicts. The process is similar:
git rebase main
CONFLICT (content): Merge conflict in hello.txt
error: could not apply c3d4e5f... Update greeting
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add <pathspec>...", then run "git rebase --continue".
Fix the conflict, then:
git add hello.txt
git rebase --continue
To cancel:
git rebase --abort
Git Stash
Sometimes you are in the middle of work and need to switch branches. But you are not ready to commit. This is where git stash helps.
Stash saves your uncommitted changes and gives you a clean working directory.
# You are working on a feature
echo "Work in progress..." > wip.txt
git add wip.txt
# Something urgent comes up — stash your work
git stash
Saved working directory and index state WIP on feature/login: a1b2c3d Add hello.txt
Your working directory is now clean. You can switch branches, fix the urgent issue, and come back.
Get Your Work Back
git stash pop
On branch feature/login
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: wip.txt
Dropped refs/stash@{0} (abc123...)
pop applies the stash and removes it from the stash list.
Stash with a Message
git stash -m "Login form half done"
This makes it easier to find the right stash later.
List All Stashes
git stash list
stash@{0}: On feature/login: Login form half done
stash@{1}: WIP on main: Update config
Apply a Specific Stash
git stash pop "stash@{1}"
Note the quotes around stash@{1}. In Zsh (the default macOS shell), the curly braces need quoting.
Apply Without Removing
git stash apply
This applies the stash but keeps it in the stash list. Useful if you want to apply the same changes to multiple branches.
Drop a Stash
git stash drop
This removes the latest stash without applying it.
Interactive Rebase
Interactive rebase lets you modify commits before replaying them. This is powerful for cleaning up your commit history.
git rebase -i HEAD~3
This opens your editor with the last 3 commits:
pick a1b2c3d Add login form
pick b2c3d4e Fix typo in login
pick c3d4e5f Add login validation
You can change pick to:
- squash (or s) — combine this commit with the one above it
- reword (or r) — change the commit message
- drop (or d) — remove this commit
- edit (or e) — pause here so you can modify the commit
Squashing Commits
A common use case: you made several small commits and want to combine them into one clean commit.
Change the file to:
pick a1b2c3d Add login form
squash b2c3d4e Fix typo in login
squash c3d4e5f Add login validation
Git combines all three into one commit and lets you write a new message:
Add login form with validation
This gives you a cleaner history. Instead of three messy commits, you have one meaningful commit.
Important: Only squash commits on your local branch. If you already pushed the commits and others are working on them, squashing will cause problems.
Putting It All Together
Here is a typical workflow using branches, merge, and stash:
# Start a new feature
git switch -c feature/search
# Work on it
echo "<input type='search'>" > search.html
git add search.html
git commit -m "Add search input"
# Meanwhile, main got updated by a teammate
# Update your branch with rebase
git fetch origin
git rebase origin/main
# Keep working
echo "function search() {}" > search.js
git add search.js
git commit -m "Add search function"
# Feature is done — merge into main
git switch main
git merge feature/search
# Clean up
git branch -d feature/search
Common Mistakes
Rebasing shared branches — Never rebase a branch that others are using. Rebase rewrites commit hashes. If someone else has the old commits, they will get duplicate commits and a messy history. Only rebase your own local branches.
Panicking during merge conflicts — Conflicts are normal. They happen when two people edit the same code. Read the conflict markers carefully, decide what to keep, remove the markers, and commit. If you are confused,
git merge --aborttakes you back to safety.Forgetting to delete merged branches — After merging, delete the feature branch with
git branch -d. Stale branches pile up and make it hard to find active work. The-dflag is safe — it only deletes fully merged branches.
What’s Next?
You can now combine branches with merge and rebase. In the next tutorial, we will learn how to share your code with others using GitHub, create pull requests, and set up basic CI/CD with GitHub Actions.
Git Tutorial #4: GitHub Workflow — Collaborating with Others
For a quick reference of all Git commands, check the Git Commands Cheat Sheet.
This is part 3 of the DevTools Tutorial series.