Git Merging
Fast-forward vs three-way merges, resolving conflict markers step by step, --no-ff and squash merges, and aborting a merge that went wrong.
What Merging Does
Merging combines changes from one branch into another. It is how feature work eventually becomes part of main or another shared branch.
A common example is merging feature/login into main after the feature is complete and reviewed.
git switch main
git merge feature/login
Git tries to combine history automatically. When histories fit together cleanly, merging is easy. When the same lines changed in different ways, you may get a conflict that needs manual resolution.
Fast-Forward Merge
A fast-forward merge happens when the target branch has not moved since the feature branch was created. Git can simply move the branch pointer forward.
Imagine this sequence:
mainat commit A- create
feature/docs - add commits B and C on
feature/docs mainstill points at A
Merging can look like this:
git switch main
git merge feature/docs
Git may respond with a fast-forward update because there is no need to create a separate merge commit.
Why It Happens
The histories form a straight line. main can just “catch up” to the feature branch.
Benefits:
- simple history
- fewer merge commits
- easy to understand for small projects
Three-Way Merge
A three-way merge happens when both branches moved forward independently.
For example:
mainhas commit Dfeature/loginhas commits B and C- both branches share an earlier common ancestor A
Now Git must combine two lines of development. It creates a new merge commit that has two parents.
git switch main
git merge feature/login
This is called a three-way merge because Git compares:
- the common ancestor
- the current branch tip
- the branch being merged
When Teams Use --no-ff
By default, Git may fast-forward when possible. Some teams prefer to always keep a visible merge commit for feature branches.
Use:
git switch main
git merge --no-ff feature/login
This forces a merge commit even if a fast-forward would work.
Why Use --no-ff?
It can make history easier to read because the branch integration remains visible as a single merge event. That can help when reviewing past work or reverting an entire feature.
Tip: Teams that rely heavily on pull requests often like explicit merge commits because they preserve the story of how a feature entered the main branch.
What a Merge Conflict Is
A merge conflict happens when Git cannot decide how to combine changes automatically.
This usually occurs when:
- both branches changed the same lines
- one branch deleted a file the other edited
- file renames and edits overlap in confusing ways
Git will pause the merge and ask you to resolve the conflict manually.
Step-by-Step Conflict Resolution
Assume both branches edited the same line in app.conf.
Start the merge:
git switch main
git merge feature/config-update
Git may report a conflict. Check the status:
git status
Open the conflicted file. You may see markers like this:
<<<<<<< HEAD
port=8080
=======
port=9090
>>>>>>> feature/config-update
What the Markers Mean
<<<<<<< HEADshows the version from the current branch=======separates the two versions>>>>>>> feature/config-updateshows the incoming version
Resolve the File
Choose the correct final content, for example:
port=9090
Or combine both ideas if needed. Then stage the resolved file:
git add app.conf
Complete the merge:
git commit
Git usually opens your editor with a default merge message.
Verifying the Merge
After merging, inspect the history:
git log --oneline --graph --decorate -10
This helps you confirm whether Git created a fast-forward result or a merge commit.
You can also test the application after resolving conflicts:
npm test
Or if it is a shell-based project:
./run-checks.sh
Validation matters because a “clean” merge is not automatically a correct merge.
Abort a Problematic Merge
If you start a merge and realize you are not ready, you can often stop and return to the pre-merge state:
git merge --abort
This is helpful if you accidentally merged the wrong branch or want to review changes before trying again.
Note:
git merge --abortworks best before you make extra manual changes beyond the merge conflict state.
Merge vs Rebase at a High Level
Merging preserves the true branch structure. Rebasing rewrites commits onto a new base to create a straighter history. Both are useful, but merge is often the safer beginner choice because it does not rewrite existing commits.
What You Should Remember
Merging is how Git combines branches. A fast-forward merge simply moves a pointer. A three-way merge creates a merge commit when histories diverged. Conflicts happen when Git cannot safely decide between competing changes, and resolving them means editing the affected files, staging the results, and finishing the merge.
Use --no-ff when your team wants feature integrations to remain clearly visible in history.
Test Your Understanding
Exercise 1: Recognizing a fast-forward
Scenario: main is still at commit A, while feature/docs moved from A to B to C and nothing new was added to main.
Question: What kind of merge should Git usually be able to perform when you merge feature/docs into main?
Exercise 1: Recognizing a fast-forward
Identify the merge type when the target branch has not moved since branching.
Exercise 2: Preserving feature history
Question: Why might a team choose git merge --no-ff feature/login even when a fast-forward merge is possible?
Exercise 2: Preserving feature history
Pick the reason teams force a merge commit with --no-ff.
Exercise 3: Finishing conflict resolution
Scenario: A merge stopped with conflict markers in app.conf. You edited the file so the final content is correct.
Question: According to the tutorial, what should you do next to complete the merge workflow?
Exercise 3: Finishing conflict resolution
Choose the next step after manually resolving merge conflict markers.
Frequently Asked Questions
How do I resolve a Git merge conflict?
Open each file Git lists as conflicted and find the <<<<<<<, ======= and >>>>>>> markers. The section above ======= is the branch you are on, the section below is the branch you are merging in. Edit the file into the state you actually want, often a combination of both, not a choice between them, delete all three markers, then git add the file and git commit. git status tracks which files are still outstanding, and git diff --name-only --diff-filter=U lists just the unresolved ones.
What is the difference between a fast-forward and a three-way merge?
A fast-forward happens when the target branch has no commits since you branched off, so Git simply moves the pointer forward to your latest commit, no merge commit, and history stays linear. A three-way merge happens when both branches have moved on, so Git combines them using the common ancestor plus each tip, and records the result as a merge commit with two parents. You cannot choose: Git fast-forwards whenever it can, unless you pass --no-ff to force a merge commit.
When should I use git merge --no-ff?
When you want the branch to remain visible in history. --no-ff forces a merge commit even where a fast-forward was possible, so the graph shows that a group of commits belonged to one feature and git log --graph reads as a series of merged branches. That makes reverting a whole feature easy, one git revert -m 1 on the merge commit, and it is why many teams enable it for release branches. The cost is a busier graph, which is why other teams prefer squash merges instead.
How do I abort a merge and start over?
git merge --abort returns you to the state before the merge began, discarding partial resolutions. It works while the merge is in progress, that is, before you commit, which covers the case where the conflicts turn out to be more than you expected. If you have already committed the merge, use git reset --hard HEAD~1 to drop it locally, and git revert -m 1 <merge-sha> if it has already been pushed, since resetting a shared branch strands everyone else.
What does "Already up to date" mean when merging?
Every commit on the branch you are merging is already reachable from your current branch, so there is nothing to bring in. The usual cause is not having fetched: your local copy of the remote branch is stale, and Git is telling you the truth about what it knows. Run git fetch first, then merge. The other cause is merging the wrong direction. Merging main into your feature branch when you meant to merge your feature into main.
Should my team use merge commits, squash merges, or rebase merges?
Squash merges suit most teams: each pull request becomes one commit on main, so history reads as a list of shipped changes and reverting is trivial, at the cost of losing the individual commits. Merge commits preserve everything and keep the branch visible, which matters when you need the full record for audit or bisecting inside a feature. Rebase merges give a linear history with every commit kept, which is tidy but rewrites hashes. Pick one and enforce it in the repository settings rather than leaving it per-developer.