Git & GitHub
Branches, Merging and Conflicts
Fast-forward versus merge commits, and a real merge conflict resolved step by step.
On this page
In one line: a branch is a cheap label, so you can try an idea on its own line of work and join it back later with git merge. Sometimes Git cannot join two changes by itself. That is a merge conflict, and it is not an error: it is Git asking you to decide.
In lesson 0 you saw that a branch is a file with one hash. This lesson uses that fact. We create branches, merge them in the two ways Git has (fast-forward and merge commit), then make a real conflict on purpose and fix it step by step. You will also learn the one command that gets you out when you panic: git merge --abort.
What I ran / checked. Every command was run in a throwaway repository with git version 2.43.0 (Linux). I ignored my own Git config, used the identity "Ada Lovelace" and fixed the date, so your hashes will differ. The merge strategy name in the output (ort) is what this version printed. The conflict, the conflict markers and the --abort result are real pasted output. I also read the Pro Git book source for branching (progit/progit2, book/03-git-branching/sections/workflows.asc) for the idea of short-lived topic branches. Nothing here uses GitHub.
Words used in this lesson
- A branch is a label that points to a commit. It moves forward when you commit.
mainis the usual name of the main branch. Some old projects usemaster. The name is only a convention.- To merge is to join the work of another branch into the one you are on.
- A fast-forward is a merge where the current branch just moves forward to the other branch's tip, because nothing happened on the current branch in the meantime. No new commit is made.
- A merge commit is a new commit with two parents. It records that two lines of work were joined.
- A conflict happens when both branches changed the same lines (or one deleted what the other changed) and Git cannot choose.
- Conflict markers are the lines
<<<<<<<,=======and>>>>>>>that Git writes into the file to show both versions. - The merge base is the latest commit that both branches have in common. Git compares each side with it.
- A topic branch is a short-lived branch for one feature or fix.
Create and switch branches
git -c init.defaultBranch=main init -q site && cd siteprintf '<h1>My Site</h1>\n<p>Welcome</p>\n' > index.htmlprintf 'Title: My Site\nColor: blue\nLanguage: en\n' > config.txtgit add . && git commit -q -m "Initial site"git branch* mainThe star marks the branch you are on. Make a new branch and switch to it in one step:
git switch -c add-footerprintf '<footer>Contact us</footer>\n' >> index.htmlgit commit -qam "Add footer"git log --oneline --graph --decorate --allSwitched to a new branch 'add-footer'* 539ccc5 (HEAD -> add-footer) Add footer* b4f8188 (main) Initial sitegit switch is the modern command for changing branch. You will also meet git checkout <branch>, which does the same and much more (it is older and more confusing, which is why switch and restore were added). git switch -c name creates the branch. Use git switch - to jump back to the previous branch.
main still points to b4f8188. The footer exists only on add-footer. If you switch back, the footer line disappears from index.html, because Git rewrites your files to match the branch.
If you have uncommitted changes, git switch may refuse (if the files clash) or carry the changes with you (if they do not). To park them safely, use git stash (lesson 4).
Merge 1: fast-forward
Switch to main and merge:
git switch maingit merge add-footergit log --oneline --graph --decorate --allSwitched to branch 'main'Updating b4f8188..539ccc5Fast-forward index.html | 1 + 1 file changed, 1 insertion(+)* 539ccc5 (HEAD -> main, add-footer) Add footer* b4f8188 (main) Initial siteThe last two lines show that main and add-footer now point to the same commit. Git said Fast-forward. Nobody had committed on main, so Git only moved the main label forward. No new commit was created, and the history is a straight line:
before: b4f8188 <- 539ccc5 after: b4f8188 <- 539ccc5 ^ ^ ^ main add-footer main, add-footerMerge 2: a merge commit
Now both branches get new work. I branch off for an about page, then also commit on main:
git switch -c about-pageecho "<p>About us</p>" > about.html && git add about.html && git commit -qm "Add about page"git switch mainprintf 'Contact: hi@example.com\n' >> config.txt && git commit -qam "Add contact to config"git log --oneline --graph --decorate --all* 5f295d8 (about-page) Add about page| * bd2a10a (HEAD -> main) Add contact to config|/ * 539ccc5 (add-footer) Add footer* b4f8188 Initial siteThe history has forked. The two lines share 539ccc5 (the merge base). A fast-forward is impossible, because main has a commit that about-page does not. Merge:
git merge about-page -m "Merge branch 'about-page'"git log --oneline --graph --decorate --allMerge made by the 'ort' strategy. about.html | 1 + 1 file changed, 1 insertion(+) create mode 100644 about.html* ab4a3b0 (HEAD -> main) Merge branch 'about-page'|\ | * 5f295d8 (about-page) Add about page* | bd2a10a Add contact to config|/ * 539ccc5 (add-footer) Add footer* b4f8188 Initial siteGit made a new commit, ab4a3b0, with two parents. It joined the changes automatically, because they touched different files. The word ort is the name of Git's default merge algorithm in this version. You do not need to choose it.
If you want a merge commit even when a fast-forward is possible (some teams like to keep a record that a feature branch existed), use git merge --no-ff. If you want to refuse anything but a fast-forward, use --ff-only. I ran --no-ff on a small branch nf; Git printed Merge made by the 'ort' strategy. and the log showed the diamond shape.
| Fast-forward | Merge commit | |
|---|---|---|
| New commit? | No | Yes, with two parents |
| History shape | Straight line | Diamond |
| When | Current branch did not move | Both branches have new commits (or --no-ff) |
Clean up merged branches
A branch label is cheap, but old ones create clutter. Check which are merged and delete them:
git branch --mergedgit branch -d about-page add-footergit branch about-page add-footer* mainDeleted branch about-page (was 5f295d8).Deleted branch add-footer (was 539ccc5).* main-d (lowercase) deletes only if the branch is fully merged. This is a safety check. I tried it on an unmerged branch experiment:
error: the branch 'experiment' is not fully merged.If you are sure you want to delete it, run 'git branch -D experiment'-D (capital) forces it. Deleting a branch deletes the label, not the commits. The commits stay in the repository for a while and can be recovered with the reflog (lesson 4).
A real conflict, step by step
A conflict needs two branches that change the same line differently. In config.txt the line is Color: blue. One branch will make it green. main will make it red.
git switch -c blue-to-greensed -i 's/Color: blue/Color: green/' config.txtgit commit -qam "Change color to green"git switch mainsed -i 's/Color: blue/Color: red/' config.txtgit commit -qam "Change color to red"Both branches now changed line 2 of config.txt away from blue, to different values. Merge:
git merge blue-to-greenAuto-merging config.txtCONFLICT (content): Merge conflict in config.txtAutomatic merge failed; fix conflicts and then commit the result.Git stopped half-way. Do not panic. Nothing is lost. Ask what state we are in:
git statusOn branch mainYou have unmerged paths. (fix conflicts and run "git commit") (use "git merge --abort" to abort the merge)Unmerged paths: (use "git add <file>..." to mark resolution) both modified: config.txtno changes added to commit (use "git add" and/or "git commit -a")Git tells you exactly what to do: fix the file, git add it, then git commit, or abort. Open the file:
cat config.txtTitle: My Site<<<<<<< HEADColor: red=======Color: green>>>>>>> blue-to-greenLanguage: enContact: hi@example.comHow to read it:
- Everything outside the markers merged fine (
Title,Language,Contact). - Between
<<<<<<< HEADand=======is our side: the branch we are on (main) saysColor: red. - Between
=======and>>>>>>> blue-to-greenis their side: the other branch saysColor: green.
Git cannot know which colour you want. Maybe red, maybe green, maybe orange. You decide. A few helpers:
git diff # a special 3-way diff of the conflictgit log --oneline --merge # commits from both sides that touch the conflict7382a1e Change color to redfb24269 Change color to greenOption A: abort and think
If you are not ready, go back to the state before the merge:
git merge --abortgit statuscat config.txtOn branch mainnothing to commit, working tree cleanTitle: My SiteColor: redLanguage: enContact: hi@example.comEverything is as it was before git merge. This is the safest way out. Use it freely.
Option B: resolve it
I run the same merge again, then edit the file. Resolving means producing the final text you want, with all the marker lines removed. I decide on green:
git merge blue-to-green # conflicts again, same as before# edit config.txt so that it reads:cat config.txtTitle: My SiteColor: greenLanguage: enContact: hi@example.comI wrote the file with an editor (in the run I replaced the whole file with printf). Then tell Git the conflict is resolved by adding the file, and check:
git add config.txtgit statusOn branch mainAll conflicts fixed but you are still merging. (use "git commit" to conclude merge)Changes to be committed: modified: config.txtFinish the merge. If you run git commit without -m, Git opens an editor with a ready message. I give one directly:
git commit -m "Merge blue-to-green: use green"git log --oneline --graph --decorate -6[main f154fa8] Merge blue-to-green: use green* f154fa8 (HEAD -> main) Merge blue-to-green: use green|\ | * fb24269 (blue-to-green) Change color to green* | 7382a1e Change color to red|/ * ab4a3b0 Merge branch 'about-page'|\ | * 5f295d8 Add about page* | bd2a10a Add contact to config|/ Done. The merge commit f154fa8 has parents 7382a1e (red) and fb24269 (green), and its content is your decision.
The checklist for any conflict
- Read
git status. It lists the files withboth modified. - Open each file. Search for
<<<<<<<. - Decide the final text. Sometimes it is one side, sometimes the other, sometimes a mix. Delete all three marker lines.
- Run the project's tests or at least start the app. A conflict can be resolved into code that does not work.
git add file, thengit commit.- If it goes wrong:
git merge --abort.
Do not "resolve" by deleting the other side without reading it. A conflict means someone changed that line on purpose. If you do not understand the other change, ask that person.
See the original too: diff3
By default Git shows only the two sides. With git config merge.conflictStyle diff3 it also shows the common ancestor between ||||||| and =======. I tried it on a new conflict:
<<<<<<< HEADColor: gray||||||| f154fa8Color: green=======Color: pink>>>>>>> a1Now you can see that both sides changed green (the original), one to gray and one to pink. That often makes the right answer obvious. It is a good setting to turn on once for all repositories.
Keep conflicts small
- Merge often. A branch that lives for two days has few conflicts. One that lives for two months has many.
- Pull the latest
maininto your branch regularly (lesson 3). - Keep commits and files focused. If everyone edits one giant file, everyone conflicts.
- Format code with a shared formatter so that "conflicts" are not just whitespace.
- Tell your team before you rename or move many files.
Git can also record how you solved a conflict and replay it (git rerere), but that is an advanced topic and I did not run it here.
Try it
Use a new empty repository.
- Make a commit with a file
menu.txtcontaining three lines:Soup,Salad,Pasta. Create a branchspicy, changeSouptoHot soup, commit. Switch tomainand mergespicy. Is it a fast-forward? Check withgit log --oneline --graph. - From
main, createdessert, add a lineCake, commit. Onmain, changePastatoPizza, commit. Mergedessert. Do you get a merge commit? Why? - Make a conflict: on branch
achangeSaladtoGreen salad; on branchb(from the same starting commit) changeSaladtoCaesar salad. Mergeaintomain, thenbintomain. What do you see? - Abort that merge. Check that
git statusis clean. - Do the merge again and resolve it by keeping
Caesar salad. Commit. - Try
git branch -don a branch with a commit that was never merged. What happens, and what is the option to force it?
Answers
- Yes.
mainhad no new commits, so Git only moves the label: you seeFast-forwardand a straight line. - Yes. Both branches have a new commit since they split, so a fast-forward is impossible. Git creates a commit with two parents. The two edits touched different lines, so there is no conflict.
- The first merge (
a) is a fast-forward or clean merge. The second (b) stops withCONFLICT (content): Merge conflict in menu.txt, because both changed theSaladline from the same base. git merge --abortreturns to the state before the merge.git statusshowsnothing to commit, working tree clean.- Run the merge again, edit
menu.txtto the one lineCaesar salad, remove the markers, thengit add menu.txtandgit commit. error: the branch '...' is not fully merged. Usegit branch -D nameto force it. The commits remain recoverable through the reflog for a time.
Interview questions
What is the difference between a fast-forward merge and a merge commit? (Must know)
A fast-forward happens when the current branch has no new commits since the other branch split, so Git only moves the branch label forward; no new commit is made, and the history stays a straight line. A merge commit happens when both branches have new work. Git makes a new commit with two parents. I can force a merge commit with --no-ff, or forbid anything else with --ff-only.
What is a merge conflict and why does it happen? (Must know)
It happens when two branches changed the same lines in different ways, or one deleted a file the other changed. Git compares each side with the merge base. If only one side changed a region, Git takes it. If both changed it differently, Git cannot decide and asks me. It is not an error in Git. It is a decision only a human can make.
Walk me through resolving a conflict. (Must know)
git status lists the files. I open each one, find the markers, and write the final text. I remove all marker lines. I run the tests or the app. Then git add the file and git commit. If it looks wrong I use git merge --abort and start again. I also try to understand the other person's change. I do not just take my side.
Follow-up: "What do the markers mean?" Between <<<<<<< HEAD and ======= is the branch I am on. Between ======= and >>>>>>> name is the branch I am merging in. With diff3 there is a third part, the common ancestor.
How do you back out of a merge that went wrong? (Must know)
While the merge is still in progress, git merge --abort returns everything to how it was before. If I already committed the merge, I can undo it with git reset --hard ORIG_HEAD when it was not pushed, or git revert -m 1 <merge> when it was pushed. I cover both in lesson 4. Before a risky merge I make sure the working tree is clean.
Why are branches cheap in Git? (Senior signal)
A branch is just a small file holding one commit hash. Creating it writes 41 bytes. No files are copied. Because of this, the normal way to work is a new branch for each task. Older systems copied the whole directory for a branch, which made people avoid branches.
What is the difference between git switch, git checkout and git restore? (Senior signal)
checkout was one command with two jobs: changing branches and restoring files. That was confusing, so Git 2.23 added switch (change branches) and restore (restore files). They are clearer, and they are what I use. checkout still works, and you will see it in old tutorials and scripts.
Why does git branch -d refuse sometimes? (Trap)
-d checks that the branch is fully merged into the current branch (or its upstream). If not, deleting the label would leave the commits without any name, so Git warns me. I check with git log main..branch what would be lost. If I really want to drop the work, I use -D. The commits can often be recovered with the reflog.
Can a merge succeed without conflicts and still be wrong? (Senior signal)
Yes. Git merges text lines. It does not understand the code. Branch A can rename a function while branch B adds a call to the old name in a different file. Git merges cleanly, and the build breaks. That is why teams run tests on the merged result (in a pull request, CI tests the merge), and why "up to date with main" checks exist (lesson 5).
How do you reduce conflicts in a team? (Senior signal)
Short-lived branches, merging main into my branch often, small pull requests, focused files and a shared formatter. Also talking first when two people must change the same area, and avoiding mass renames while others have open branches.
In a conflict, which side is "ours" and which is "theirs"? (Trap)
In a merge, "ours" is the branch I am on (HEAD) and "theirs" is the branch I am merging. In a rebase (lesson 5) it flips: "ours" is the branch being rebased onto, and "theirs" is my own commit being replayed. This surprises many people. With git checkout --ours file or --theirs file I must remember which operation I am in. I have seen this in the markers during rebase: the HEAD side is the upstream and my commit is on the other side.
Key takeaways
- A branch is a label.
git switch -c namecreates one and moves you onto it. - If the current branch did not move, merge is a fast-forward (just a label move). Otherwise Git makes a merge commit with two parents.
- A conflict means both sides changed the same lines. Git writes markers and waits for you.
- Fix: edit the file to the final text, remove the markers,
git add,git commit. Escape:git merge --abort. git branch -dis the safe delete,-Dforces it.- Turn on
merge.conflictStyle diff3to see the original as well. - A merge with no conflict can still break the build. Run the tests.
What's next
Next: Remotes, pull requests and code review, Lesson 3. Everything so far was on one computer. Next you share work: clone, push, fetch, pull, forks and the pull request flow on GitHub. Practise now: make a conflict on purpose three times until the markers feel boring.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Related lessons
- Git, Team Workflow and Monorepos · Full-Stack Questions
Part of the study guide
Related series