Git Branches and Merging: Fast-Forward, Merge Commits and Conflicts
DevOpsLesson 2 of 46 min read
What you'll learn
- Explain a branch as a movable pointer to a commit
- Produce and recognise a fast-forward merge and a merge commit
- Resolve a merge conflict and abort a merge safely
- Choose --no-ff or --squash when you want a different history
On this page
Two merges, same command, different results: one leaves no trace in the history, the other adds a commit with two parents, and a third stops halfway and asks you a question. Which one you get depends on whether both sides of the merge have moved. This lesson uses a trail-guide repository with three branches to produce each case on purpose, so you recognise them when they happen by surprise.
A branch is a movable label
A branch is a name that points at one commit. When you commit, the label moves forward to the new commit. HEAD records which branch you are on, and git switch <branch> moves HEAD and rewrites your working folder to match that branch's latest snapshot. Creating a branch copies nothing; it writes a short pointer, so branching is cheap enough to do for every small change. git switch -c <name> creates and switches in one step. (Older tutorials use git checkout for both jobs; switch and restore split that command into clearer pieces.)
Fast-forward: main did not move
The repository starts with one commit listing three routes. We branch off, add a fourth route, go back to main and merge. Since main has no new commits of its own, Git has nothing to combine. It just slides the main label forward. Lines starting with # in the transcript are edits made in a text editor.
$ git switch -c add-night-route
Switched to a new branch 'add-night-route'
# edit routes.txt: add the line "Night walk, 3 km"
$ git commit -am "Add the night walk"
[add-night-route 5d04e08] Add the night walk
1 file changed, 1 insertion(+)
$ git switch main
Switched to branch 'main'
$ git merge add-night-route
Updating 27e8197..5d04e08
Fast-forward
routes.txt | 1 +
1 file changed, 1 insertion(+)
$ git branch -d add-night-route
Deleted branch add-night-route (was 5d04e08).
$ git log --oneline --graph
* 5d04e08 Add the night walk
* 27e8197 List the three starter routesFast-forward in the output is the whole story. The graph is a straight line, and afterwards nobody can tell a branch ever existed. Deleting the merged branch with -d is safe; Git refuses -d on a branch with unmerged work and prints error: the branch '...' is not fully merged, which is the cue to merge it or consciously use -D.
A merge commit: both sides moved
Now two people work in parallel. One branch corrects a distance in routes.txt; meanwhile main gains a new file, safety.txt. The histories have diverged, so sliding a label is impossible. Git builds a new commit whose content combines both and which has two parents.
$ git switch -c fix-lake-distance
Switched to a new branch 'fix-lake-distance'
# edit routes.txt: change "Lake path, 4 km" to "Lake path, 4.5 km"
$ git commit -am "Correct the lake path distance"
[fix-lake-distance 3276570] Correct the lake path distance
1 file changed, 1 insertion(+), 1 deletion(-)
$ git switch main
Switched to branch 'main'
$ echo 'Bring water on the ridge loop.' > safety.txt
$ git add safety.txt
$ git commit -m "Add safety note"
[main 26065c6] Add safety note
1 file changed, 1 insertion(+)
create mode 100644 safety.txt
$ git merge fix-lake-distance -m "Merge fix-lake-distance"
Merge made by the 'ort' strategy.
routes.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline --graph
* 5d668a3 Merge fix-lake-distance
|\
| * 3276570 Correct the lake path distance
* | 26065c6 Add safety note
|/
* 5d04e08 Add the night walk
* 27e8197 List the three starter routesThe graph shows the diamond: two lines that left a common commit and joined at the merge commit. The strategy name ort is just Git's default algorithm. Git merged automatically because the two edits touched different files. Two variations are worth knowing. git merge --no-ff <branch> forces a merge commit even when a fast-forward is possible, keeping the branch visible in history. git merge --squash <branch> stages the combined changes without a merge commit, so you record them as one commit of your own.
A conflict: both sides touched the same line
Last scenario. One branch marks the ridge loop as steep; main re-measures the same line as 8.4 km. Git cannot choose between two edits of one line, so it stops, marks the file and waits.
$ git switch -c rename-ridge
Switched to a new branch 'rename-ridge'
# edit routes.txt: change "Ridge loop, 8 km" to "Ridge loop (steep), 8 km"
$ git commit -am "Flag the ridge loop as steep"
[rename-ridge e9e1b5f] Flag the ridge loop as steep
1 file changed, 1 insertion(+), 1 deletion(-)
$ git switch main
Switched to branch 'main'
# edit routes.txt: change "Ridge loop, 8 km" to "Ridge loop, 8.4 km"
$ git commit -am "Re-measure the ridge loop"
[main 7a3a653] Re-measure the ridge loop
1 file changed, 1 insertion(+), 1 deletion(-)
$ git merge rename-ridge
Auto-merging routes.txt
CONFLICT (content): Merge conflict in routes.txt
Automatic merge failed; fix conflicts and then commit the result.
$ git status -s
UU routes.txt
$ cat routes.txt
Lake path, 4.5 km
<<<<<<< HEAD
Ridge loop, 8.4 km
=======
Ridge loop (steep), 8 km
>>>>>>> rename-ridge
Quarry steps, 2 km
Night walk, 3 km
$ git merge --abort
$ git status
On branch main
nothing to commit, working tree clean
$ git merge rename-ridge
Auto-merging routes.txt
CONFLICT (content): Merge conflict in routes.txt
Automatic merge failed; fix conflicts and then commit the result.
# edit routes.txt: resolve it: delete the markers and keep one combined Ridge line
$ cat routes.txt
Lake path, 4.5 km
Ridge loop (steep), 8.4 km
Quarry steps, 2 km
Night walk, 3 km
$ git add routes.txt
$ git commit --no-edit
[main 31912ac] Merge branch 'rename-ridge'
$ git log --oneline --graph
* 31912ac Merge branch 'rename-ridge'
|\
| * e9e1b5f Flag the ridge loop as steep
* | 7a3a653 Re-measure the ridge loop
|/
* 5d668a3 Merge fix-lake-distance
|\
| * 3276570 Correct the lake path distance
* | 26065c6 Add safety note
|/
* 5d04e08 Add the night walk
* 27e8197 List the three starter routes
$ git branch --merged
fix-lake-distance
* main
rename-ridge
$ git branch -d rename-ridge fix-lake-distance
Deleted branch rename-ridge (was e9e1b5f).
Deleted branch fix-lake-distance (was 3276570).Everything between <<<<<<< HEAD and ======= is your side, the branch you are on. Everything between ======= and >>>>>>> rename-ridge is the incoming side. Status UU means both modified. Notice the safe exit in the middle: git merge --abort put everything back exactly as before, so you can retreat at any point. To resolve:
- Open the conflicted file and decide the final text for each marked region.
- Delete all three marker lines (
<<<<<<<,=======,>>>>>>>). Git does not remove them for you. - Run
git add <file>to tell Git that conflict is resolved. - Run
git commit(here--no-editaccepts the prepared message) to finish the merge.
| Merge result | When it happens | History looks like |
|---|---|---|
| Fast-forward | The target branch has no new commits | A straight line, no merge commit |
| Merge commit | Both branches have new commits, changes do not overlap | A diamond, one commit with two parents |
| Conflict | Both changed the same lines | You edit, git add, then commit the merge |
Common mistakes
- Committing the conflict markers. Symptom: the build fails with a syntax error pointing at
<<<<<<< HEAD, or the site shows the markers as text. Search the project for<<<<<<<before finishing a merge. - Switching branches with uncommitted edits to the same file. Symptom:
error: Your local changes to the following files would be overwritten by checkoutfollowed byAborting. Commit the work, or usegit stashandgit stash pop. - Committing on the wrong branch. Symptom: your feature commit shows up in
git logonmain. Create a branch at that point withgit switch -c featureand then fixmainusing the reset techniques in the last lesson. - Long-lived branches. Symptom: the eventual merge produces dozens of conflicts in files you never touched. Merge
maininto your branch regularly, or keep branches to a day or two of work.
How this shows up in projects and interviews
Merge conflicts are a normal part of team work, and how you handle one says a lot: you read both sides, resolve deliberately, run the tests and say what you did in the commit. Common interview questions are fast-forward versus merge commit, merge versus rebase (a rebase replays your commits on top of the other branch, producing a straight history, and the next lesson shows it on a real push), and what HEAD is. Teams also differ on whether to keep merge commits; your job at first is to follow the team's rule. See Collaborating in a Team for the human side of sharing a codebase.
Try it yourself
Make a repository with a file playlist.txt of four tracks. Create two branches from main: one changes track 2, the other also changes track 2 to something different. Merge both into main and resolve the conflict so the final line keeps both ideas. Finish by running git log --oneline --graph and drawing the result on paper.
Show a hint
Merge the first branch (it fast-forwards), then merge the second; that is where the conflict appears. Remember to delete every marker line.
Key takeaways
- A branch is a label on a commit; creating one is instant and costs almost nothing.
- A fast-forward just moves the label; a merge commit appears only when both branches have new commits.
- A conflict means both sides edited the same lines; edit, remove markers, git add, then commit.
- git merge --abort returns to the state before the merge, so conflicts are never a point of no return.
Test yourself
A trick from this lesson's ride: A branch is a sticky note on one commit, not a copy of the project. Four more tricks like it wait along the route, each followed by a question.
Frequently asked questions
What is the difference between fast-forward and a merge commit in Git?
A fast-forward happens when the branch you are merging into has no new commits, so Git simply moves its label forward and no new commit is created. A merge commit is created when both branches have new commits; it combines them and has two parents. You can force a merge commit with git merge --no-ff.
How do I resolve a merge conflict in Git?
Open the conflicted file, decide the final text for each region between the <<<<<<<, ======= and >>>>>>> markers, and delete the marker lines. Then run git add on the file and git commit to complete the merge. If you want to start over, git merge --abort restores the state before the merge.
How do I delete a Git branch after merging?
Use git branch -d <name> to delete a local branch that is fully merged; Git refuses if it has unmerged commits. Use -D to force deletion. To delete the branch on the remote as well, run git push origin --delete <name>.