Embercode
✕
Git and GitHubLesson 4 of 16

55 min read

Branches and merging

Why this matters

On a team, nobody works directly on the shared, working version of the project. Each person works on a branch, a private line of work, and brings it back only when it is ready. Branches let ten people change the same project at once without stepping on each other. They also let you experiment freely: if the idea fails, you throw the branch away and nothing is harmed.

Merging and merge conflicts are the part of Git that scares beginners most. It should not. By the end of this lesson you will have caused a conflict on purpose and fixed it, so the first real one will feel familiar.

The idea: parallel timelines

Think of a story with a choice point. In one timeline the hero opens the red door, in another the blue door. Both timelines start from the same moment and then diverge. At the end, an editor decides how to combine what is good in each.

A branch is exactly that: a second timeline that starts from the current point of your project. Your work on the branch does not touch the original line, which is usually called main.

Why this is cheap in Git: remember from lesson 3 that a branch is only a pointer, a small label that points to one commit. Creating a branch does not copy any files. It adds one more label. This is why Git developers create branches all day long.

A chain of commits, each pointing to its parent, with main and HEAD pointing at the newestA chain of commits, each pointing to its parent, with main and HEAD pointing at the newest

In the picture, main points to the newest commit and HEAD is a pointer that says "this is the branch you are standing on". When you commit, the branch you are standing on moves forward to the new commit. Keep that sentence in your head: committing moves the current branch's pointer forward.

Set up a project to practise on

You will use a small cafe website. Create it:

mkdir site
cd site
git init
printf '<h1>Corner Cafe</h1>\n<p>Open every day.</p>\n' > index.html
git add index.html
git commit -m "Add home page"

(printf with \n writes two lines. You can also create the file in an editor.) Now list your branches:

git branch
* main

The star marks the branch you are on. To print only the current branch name:

git branch --show-current
main

Create a branch and switch to it

You want to add a footer, but you are not sure it will look right. Make a branch:

git switch -c add-footer
Switched to a new branch 'add-footer'

git switch changes branches. The -c flag means "create it first". You could do the same in two steps: git branch add-footer creates the label, then git switch add-footer moves onto it. The older command git checkout does the same job and appears in many tutorials. You will see both. This course uses switch, since it has one clear purpose.

Branch names should be short, lower case, and say what the work is, with hyphens between words: add-footer, fix-login-redirect, update-readme. Many teams add a prefix such as feature/ or fix/.

Now make a change and commit. You are on add-footer, so the commit goes there:

echo "<footer>Contact us</footer>" >> index.html
git commit -am "Add footer"

(-a stages changes to files Git already tracks, then -m gives the message. It is a shortcut for git add index.html followed by git commit.)

See the picture of the history:

git log --oneline --graph --all
* 9934f81 Add footer
* 026fd54 Add home page

With --all, Git shows every branch, not only the current one. Here both commits are on one straight line, because add-footer simply moved one step ahead of main.

Switching changes your files

Now go back to main and look at the file:

git switch main
cat index.html
Switched to branch 'main'
<h1>Corner Cafe</h1>
<p>Open every day.</p>

The footer is gone. Not deleted: it lives on the add-footer branch. When you switch branches, Git rewrites the files in your working directory to match the commit that branch points to. Switch back to add-footer and the footer line returns. This is the magic and the thing to watch: the files on disk change when you switch. Always check which branch you are on with git status before you edit.

Merge: bring the work back

Happy with the footer? Bring it into main. First stand on the branch that should receive the work:

git switch main
git merge add-footer
Updating 026fd54..9934f81
Fast-forward
 index.html | 1 +
 1 file changed, 1 insertion(+)

git merge X brings branch X into the branch you are on. Always read it as "merge X into where I stand". The result Fast-forward tells you what kind of merge happened.

Fast-forward merge moves the main label forward without creating a new commitFast-forward merge moves the main label forward without creating a new commit

A fast-forward happens when main has not changed since you branched. Git does not need to combine anything: it just slides the main pointer forward to where add-footer is. No new commit is created. The history stays a straight line.

Clean up. A merged branch has done its job, and deleting the label is safe because the commits are now part of main:

git branch -d add-footer
Deleted branch add-footer (was 9934f81).

When both sides moved: a real merge

A fast-forward is the easy case. In a team, main usually moves while you work. Reproduce this. Create a branch with a new menu page, then go back and add a different file on main:

git switch -c add-menu
echo "<a href=menu.html>Menu</a>" > menu.html
git add menu.html
git commit -m "Add menu page"
 
git switch main
echo "<p>Closed on Mondays.</p>" > hours.html
git add hours.html
git commit -m "Add hours page"
 
git log --oneline --graph --all
* 50b4873 Add menu page
| * 45fc890 Add hours page
|/  
* 9934f81 Add footer
* 026fd54 Add home page

The graph shows the fork. Both branches grew from Add footer, and they went different ways. Reading it: the |/ joins the two lines back to their shared ancestor, called the common ancestor or merge base. Now merge:

git merge add-menu -m "Merge add-menu"
Merge made by the 'ort' strategy.
 menu.html | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 menu.html

("ort" is the name of Git's current merging method, short for "Ostensibly Recursive's Twin". You can ignore it.) Because the two branches changed different files, Git combined them automatically. See the result:

git log --oneline --graph --all
*   35dbfcc Merge add-menu
|\  
| * 50b4873 Add menu page
* | 45fc890 Add hours page
|/  
* 9934f81 Add footer
* 026fd54 Add home page

Two branches that each added commits are joined by a merge commit with two parentsTwo branches that each added commits are joined by a merge commit with two parents

Git created a new commit, called a merge commit, with two parents: one from each branch. This is a three-way merge, so named because Git looks at three commits: the tip of your branch, the tip of the other branch, and their common ancestor. Delete the finished branch with git branch -d add-menu.

How Git decides, line by line

How does Git combine two edits to the same file? It compares each side against the common ancestor.

  • If only one side changed a line, take that side's version.
  • If both sides made the identical change, take it once.
  • If both sides changed the same line to different things, Git cannot choose. That is a conflict.

You can see this logic in a toy that treats each line as a numbered slot:

Python · editable

The output:

1 The Corner Cafe
2 Open daily.
3 Menu

Trace line 1: mine equals the base, so only "theirs" changed it, and the result is theirs. Line 2: theirs equals the base, so only mine changed it, and the result is mine. Line 3: nobody changed anything. The merge is clean because each line was edited by at most one side.

Now make both sides edit the same line:

Python · editable
1 Corner Cafe
2 CONFLICT: Open daily. | Open 7 days a week.

Both sides moved line 2 away from the base, in different directions. No rule can pick the winner, because the right answer depends on what the business wants. Only a human can decide. That is all a merge conflict is: a question Git cannot answer.

Causing and fixing a merge conflict

Practise on purpose. In a fresh repository, create the same file and two competing edits to the title line:

mkdir site2
cd site2
git init
printf '<h1>Corner Cafe</h1>\n<p>Open every day.</p>\n' > index.html
git add index.html
git commit -m "Add home page"
 
git switch -c fix-title
printf '<h1>The Corner Cafe</h1>\n<p>Open every day.</p>\n' > index.html
git commit -am "Rename title to The Corner Cafe"
 
git switch main
printf '<h1>Corner Cafe and Bakery</h1>\n<p>Open every day.</p>\n' > index.html
git commit -am "Add Bakery to title"

Both branches rewrote line 1 of index.html. Now merge:

git merge fix-title
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

Do not panic. Git did not break anything. It stopped halfway and is waiting for you. The three lines of output say: it tried to merge index.html, found a conflict in the content, and the merge is unfinished. Ask where things stand:

git status
On branch main
You 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:   index.html
 
no changes added to commit (use "git add" and/or "git commit -a")

both modified is the clue: both branches changed this file. Note the line use "git merge --abort" to abort the merge. You always have an escape hatch, which we will use in a moment. First, look at the file:

cat index.html
<<<<<<< HEAD
<h1>Corner Cafe and Bakery</h1>
=======
<h1>The Corner Cafe</h1>
>>>>>>> fix-title
<p>Open every day.</p>

Git wrote conflict markers into the file. They look scary and are simple.

A conflicted file showing the three marker lines and the two competing versionsA conflicted file showing the three marker lines and the two competing versions

  • <<<<<<< HEAD begins the conflict. Everything until ======= is your side, the branch you are on (main).
  • ======= is the divider.
  • Everything from the divider to >>>>>>> fix-title is the incoming side, from the branch you merged.
  • >>>>>>> fix-title ends the conflict.
  • Lines outside the markers (<p>Open every day.</p>) merged fine and need no action.

To resolve it, you edit the file so that it contains the text you want, and delete all three marker lines. You can keep one side, keep the other, or combine both. Here the sensible result keeps both ideas:

<h1>The Corner Cafe and Bakery</h1>
<p>Open every day.</p>

Do this in your editor. VS Code highlights conflicts and offers buttons such as "Accept Current Change", "Accept Incoming Change" and "Accept Both Changes". Those buttons just edit the markers for you. Whichever way, the finished file must contain no <<<<<<<, ======= or >>>>>>> lines.

Then tell Git the conflict is resolved by staging the file and committing:

git add index.html
git status -s
git commit -m "Merge fix-title"
M  index.html
[main 0d2c37f] Merge fix-title
git log --oneline --graph
*   0d2c37f Merge fix-title
|\  
| * 7005a53 Rename title to The Corner Cafe
* | 441e469 Add Bakery to title
|/  
* 026fd54 Add home page

The merge commit has two parents, and the conflict is part of the permanent record: anyone can see both original edits and your decision.

The recipe, to memorise:

  1. git merge other-branch and read the CONFLICT lines.
  2. git status to see which files are unmerged.
  3. Open each file, find the markers, decide, and edit.
  4. Delete every marker line.
  5. git add <file> for each fixed file.
  6. git commit to finish the merge.

Walking away: git merge --abort

If you open the file and are not sure what to do, you can undo the whole merge and return to how things were before you ran it:

git merge --abort
git status
On branch main
nothing to commit, working tree clean

Nothing is lost on either branch. You can ask the other person what they meant, then try again.

What happens if you try to commit too early

Suppose you forget to resolve and type git commit. Git refuses:

error: Committing is not possible because you have unmerged files.
hint: Fix them up in the work tree, and then use 'git add/rm <file>'
hint: as appropriate to mark resolution and make a commit.
fatal: Exiting because of an unresolved conflict.

Read the hint. It says the same thing as the recipe: fix the files, then add, then commit.

A conflict in a Python merge: see the shape in a program

The conflicted text above is just a file, so a program could even detect it. This one reports whether a text still contains markers:

Python · editable

It prints still conflicted ['<<<<<<<', '=======', '>>>>>>>']. A common beginner disaster is to commit a file while a marker is still inside it, which breaks the code. Some teams add a check that rejects commits containing markers. Before you commit a resolution, search the file for <<<<<<<.

How Git finds the common ancestor

Every three-way merge starts by finding the merge base: the newest commit that both branches share. Because each commit points to its parent (or parents), Git can walk backwards from each branch tip and see where the two walks meet.

This program does the same on a small made-up history. Each commit lists its parents. m1 and m2 are on main, f1 and f2 on the feature branch, and both forked from c2:

Python · editable
main sees   : ['m2', 'm1', 'c2', 'c1']
feature sees: ['f2', 'f1', 'c2', 'c1']
merge base  : c2

Trace it: ancestors("m2") starts at m2, then visits its parent m1, then c2, then c1. The feature walk starts at f2 and reaches c2 too. The first entry on the main list that also appears on the feature list is c2, the merge base. Git then compares c2 to m2 (what did main change?) and c2 to f2 (what did the feature change?), and combines the two sets of changes. You can ask Git for the merge base of any two branches with git merge-base main feature, which prints the full ID.

This also explains why a fast-forward is possible: if the merge base is already the tip of the branch you stand on, then your branch has nothing of its own to combine, and the pointer can simply slide forward.

Other kinds of conflict

Text conflicts, with markers, are the most common. A merge can also stop for other reasons.

One side deleted a file the other side edited. Git cannot decide whether the file should stay. The message reads:

CONFLICT (modify/delete): a.txt deleted in del and modified in HEAD.  Version HEAD of a.txt left in tree.
Automatic merge failed; fix conflicts and then commit the result.

git status -s shows UD a.txt (unmerged, deleted by them). To keep the file, run git add a.txt. To accept the deletion, run git rm a.txt. Then commit.

Both sides added a file with the same name but different content. Git reports CONFLICT (add/add) and writes markers into the file, just like an ordinary text conflict.

A rename on one side and an edit on the other. Git usually handles this alone, by noticing the rename and applying the edit to the renamed file. If it cannot, it reports a conflict and tells you which file.

Whichever it is, the recipe is the same: read the message, look at git status, decide, stage, commit. If the situation becomes confusing, git merge --abort always gets you back to a clean start.

Habits that make conflicts small

Conflicts are normal, but you can keep them tiny.

  1. Keep branches short-lived. A branch that lives for a day or two has a small gap to bridge. One that lives for a month does not.
  2. Bring main into your branch often. While you work on add-footer, run git switch add-footer, then git merge main once a day. Each time, you resolve a few small conflicts instead of one huge one.
  3. Make focused changes. If you reformat a whole file while fixing one bug, you touch every line and conflict with everyone. Change what the task needs.
  4. Talk to your team. Two people planning to edit the same function should know about each other. A message in the team chat prevents an hour of untangling.
  5. Commit before merging. Merge only from a clean working tree, so the only differences in play are the ones from the branches.

Resolving conflicts in VS Code

VS Code recognises the markers and shows buttons above each conflict: Accept Current Change (your side, HEAD), Accept Incoming Change (the other branch), Accept Both Changes (keeps both, one after the other), and Compare Changes. Pressing one rewrites the lines for you and removes the markers. Afterwards, read the result as plain text: "Accept Both" often gives two lines that contradict each other, and you must edit them into one sensible line. When the file looks right, stage it with the plus button (or git add) and commit.

The buttons save typing but not thinking. The question is always the same: "What should this text say now?"

Tools for working with branches

A cheat sheet of the commands you will use daily:

CommandWhat it does
git branchList local branches; the star marks the current one
git branch -vvList branches with the latest commit and tracking info
git switch nameMove to an existing branch
git switch -c nameCreate a branch and move to it
git switch -Jump back to the previous branch
git merge nameMerge branch name into the current branch
git merge --no-ff nameAlways create a merge commit, even when a fast-forward is possible
git branch -d nameDelete a branch that is already merged
git branch -D nameForce-delete a branch, even with unmerged work (dangerous)
git branch -m old newRename a branch
git branch --mergedList branches already merged into the current one
git log main..featureCommits on feature that are not yet in main

You can test git log main..feature. In the earlier example (after fixing the title), git log --oneline main..fix-title prints nothing, because everything on fix-title is already in main. Reverse it with fix-title..main and you see the commits main has that fix-title lacks.

Switching with unsaved work

Suppose you are partway through an edit on main and need to look at another branch. Git protects you:

error: Your local changes to the following files would be overwritten by checkout:
	index.html
Please commit your changes or stash them before you switch branches.
Aborting

Git refuses because switching would overwrite your uncommitted edit with the other branch's version. You have three choices.

  1. Commit the work-in-progress, even with a message like "WIP: footer styling". You can clean up later.
  2. Stash it. A stash is a shelf where Git puts your uncommitted changes aside and gives you a clean working directory:
git stash
git switch other-branch
# ... do what you needed, then come back ...
git switch main
git stash pop

git stash saves the edits and restores a clean tree. git stash pop brings them back. git stash list shows the shelf.

  1. Discard the edits with git restore index.html, if you no longer want them.

Note that Git does not complain when the edit is in a file the other branch has not changed. It switches and carries your edit along. The error only appears when the switch would overwrite your change.

Deleting branches: two warnings you will see

If a branch has commits that are not merged anywhere, Git refuses a plain delete:

error: the branch 'wip' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D wip'
hint: Disable this message with "git config set advice.forceDeleteBranch false"

Git is protecting work that exists only on that branch. If you really do not need it, -D forces deletion. If you are unsure, merge it first or keep the branch.

Switching to a branch that does not exist gives:

fatal: invalid reference: nothere

Check the spelling with git branch. Likewise, git merge nothere fails with merge: nothere - not something we can merge.

Fast-forward versus merge commit: which is better?

Both are correct. Teams choose a style.

Fast-forwardMerge commit
History shapeOne straight lineVisible fork and join
Extra commitNoYes, the merge commit
Shows that a feature was a unit of workNoYes
When it happensmain did not movemain moved, or you pass --no-ff

On GitHub, the Merge button offers three variants (merge commit, squash and rebase), covered in lesson 5. Do not worry about choosing yet. A merge commit is the safe default.

Worked examples

Example 1: Experiment and discard. You try a dark theme on a branch called dark-theme. It looks ugly. You switch to main and run git branch -D dark-theme. Nothing on main was touched. The cost of the experiment: zero.

Example 2: Two people, different files. Asha adds menu.html on one branch and Ravi adds hours.html on another. Merge both into main: the first merge fast-forwards, and the second creates a merge commit. No conflict, because no file was touched by both.

Example 3: Same file, different lines. Asha changes line 1 of index.html, Ravi changes line 10. Git merges automatically. Conflicts are about overlapping lines, not about the same file. You can see that by editing different lines in your practice repo.

Example 4: Same line, same change. Both change the title to exactly The Corner Cafe. Git sees identical changes and takes it once. No conflict.

Example 5: Resolving in favour of one side. In a conflict you decide your side is right. You edit the file down to your version, remove the markers, and commit. The incoming change is not lost from history, only from the final text.

Example 6: Merge conflict while testing. You fix a conflict and start the test suite, which fails. The cause is a marker you missed halfway down the file. Search the file for <<<<<<< and >>>>>>>, fix it, and make a new commit such as Remove leftover conflict marker.

Example 7: Stash and switch. You edit index.html on main, and your teammate asks for a quick look at the fix-title branch. git switch fix-title fails with the "local changes" error. You run git stash, switch, look, switch back and run git stash pop. Your edit returns, M index.html.

Common mistakes

1. Merging in the wrong direction. git merge main while standing on add-footer brings main into your branch, which is sometimes right (to update your branch) but is not what you want when you aim to finish the feature. Always check git branch --show-current first.

2. Editing on the wrong branch. You make changes while on main that belonged on a feature branch. Fix before committing: git stash, git switch feature, git stash pop. After committing, ask a teammate. It is solvable but needs more advanced commands.

3. Leaving conflict markers in the file. The merge is "finished", but the page shows <<<<<<< HEAD in the browser. Search before committing.

4. Resolving by deleting everything. You delete the marker blocks and both versions. The conflict disappears and so does the code. Look at the final text and ask whether the result makes sense.

5. Thinking a conflict is an error. It is a normal event when people work in parallel. Reading it calmly is a core skill.

6. Forgetting to git add after resolving. git status still says both modified. Stage the file to say it is resolved.

7. Long-lived branches. The longer a branch lives without merging, the larger and more painful the conflicts. Merge small, merge often, and bring main into your branch regularly.

A debugging walkthrough

Scenario: "After my merge, the site looks broken." Work through it.

  1. git status. If it says "You have unmerged paths", the merge is not finished. Resolve and commit.
  2. git log --oneline --graph -5. Confirm the merge commit exists and has two parents.
  3. Open the broken file and search for <<<<<<<. If found, you committed a conflict. Fix it, and commit again with "Remove leftover conflict markers".
  4. Compare against the branch that was good: git diff main~1 main. Compare what changed in the merge.
  5. If it is a mess and you have not shared it, you can step back with an advanced command you will learn later. For now, remember that a plain git revert -m 1 HEAD can cancel a shared merge, and ask for help when needed.

Interview-style questions

  1. What is a branch in Git? A lightweight movable pointer to a commit. Creating one copies no files.
  2. What is the difference between a fast-forward and a three-way merge? Fast-forward just moves the pointer when the target has not diverged. A three-way merge combines two diverged lines using their common ancestor and makes a merge commit.
  3. When does a merge conflict happen? When both branches change the same lines of a file differently, so Git cannot choose.
  4. How do you resolve one? Edit the file to the desired text, remove the markers, git add it, then git commit.
  5. What does git merge --abort do? It cancels an in-progress merge and restores the state from before it began.
  6. What is git stash for? It shelves uncommitted changes so you can switch branches with a clean tree, and git stash pop brings them back.

Practice

1. (Easy) In a repo with one commit, create a branch called practice and switch to it in one command. Then show the current branch name.

Solution
git switch -c practice
git branch --show-current

The second command prints practice.

2. (Easy) On that branch, add a file and commit. Switch back to main. Is the file there? Why?

Solution

No. The commit exists only on practice. When you switch, Git rewrites the working directory to match main, which has no such file. Switch back and it returns.

3. (Easy) Merge practice into main and say which kind of merge occurred.

Solution
git switch main
git merge practice

It prints Fast-forward, because main did not change while practice grew. The pointer slid forward and no merge commit was created.

4. (Easy) Delete the merged branch. Why is -d allowed here?

Solution

git branch -d practice. -d works because the branch is fully merged, so no commits would be lost.

5. (Medium) Create a fork in history: one branch changes a.txt, main changes b.txt. Merge and read the graph. How many parents does the merge commit have?

Solution

Two. The graph from git log --oneline --graph --all shows a diamond: the fork, one commit on each side, and a merge commit joining them. No conflict occurs because different files changed.

6. (Medium) Cause a conflict on purpose using one file with one line, and resolve it by keeping both ideas. List the commands in order.

Solution
  1. Commit line: base on main.
  2. git switch -c other, change the line to line: other, commit.
  3. git switch main, change it to line: main, commit.
  4. git merge other and read the CONFLICT output.
  5. Edit the file to a combined line such as line: main and other, deleting all three marker lines.
  6. git add file, git commit -m "Merge other".

7. (Medium) After step 4 above, you change your mind and want the repository back to normal. What command, and what does git status show?

Solution

git merge --abort. Then git status shows nothing to commit, working tree clean, and both branches are exactly as they were.

8. (Medium) You type git commit during a conflict without resolving. What error appears, and what are its hints?

Solution

error: Committing is not possible because you have unmerged files. followed by hints to fix the files in the work tree, then git add or git rm them to mark resolution, and commit.

9. (Hard) You are halfway through editing index.html on main when a teammate needs you to check fix-title. Show the safe sequence, and what you would see if you skipped the first step.

Solution
git stash
git switch fix-title
git switch main
git stash pop

Without git stash, git switch fix-title fails with error: Your local changes to the following files would be overwritten by checkout: index.html and Aborting, because the other branch has a different version of the file.

10. (Hard) Explain, using the three versions (base, mine, theirs), why Git can merge "mine changes line 2, theirs changes line 1" but not "both change line 2". Use the Python toy as a guide.

Solution

For each line Git compares both sides against the base. If only one side differs from the base, that side's version is taken, which is unambiguous. If both differ from the base and from each other, taking either would throw away the other side's intent, so Git needs a human. In the first case each line has at most one author of change; in the second, line 2 has two.

11. (Hard) A teammate has a branch that is five weeks old and main has moved a lot. Suggest two habits that would have prevented a huge conflict.

Solution

Merge small pieces of work often instead of keeping long-lived branches, and regularly bring main into the feature branch (git switch feature, git merge main) so conflicts arrive in small, fresh batches while the author still remembers the code. Talking to the team about who edits which files also helps.

What you now know

  • A branch is a lightweight pointer to a commit. Committing moves the current branch forward, and switching branches rewrites your files to match.
  • git switch -c name creates and enters a branch; git merge name brings it into the branch you are on; git branch -d name removes a merged branch.
  • A fast-forward just moves a pointer; a three-way merge makes a merge commit with two parents, using the common ancestor.
  • A conflict happens when both sides changed the same lines differently. Markers show your side (HEAD) and the incoming side; resolve by editing, git add, git commit.
  • git merge --abort is the escape hatch; git stash shelves uncommitted work before switching.
  • git log --oneline --graph --all draws the history so you can see forks and merges.
Check yourself: Quick quiz: branches and merging (6 questions) →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.