Step 03 of 10

Branching & merging

Create, switch and delete branches confidently, and predict whether a merge will fast-forward or create a merge commit.

17 min 8 sections

Branches are what make Git feel fast and safe: you can try an idea, fix an urgent bug, or review someone's work without disturbing anything else. Because a branch is just a pointer to a commit, creating one is instant. In this lesson you will create and switch branches, see exactly what happens to your files when you do, and learn the two ways Git combines work: fast-forward merges and merge commits.

What a branch really is

Recall from the first lesson that a branch is a file containing one commit ID, and that HEAD names the branch you are on. Picture a repository with three commits on main:

text
A ── B ── C   ← main ← HEAD

Creating a branch adds a second pointer to the same commit. Nothing is copied:

text
A ── B ── C   ← main ← HEAD
            ↖ feature/cart

When you commit, only the branch that HEAD points at moves forward. That is the entire mechanism. Everything else in this lesson follows from it.

Creating and switching branches

List branches with git branch. The asterisk marks the current one:

bash
git branch
text
* main

Create a branch and switch to it in one step with git switch -c (short for --create):

bash
git switch -c feature/cart
text
Switched to a new branch 'feature/cart'

Now commit something on it:

bash
echo "cart" > cart.html
git add cart.html
git commit -m "Add cart page"
git branch -v
text
* feature/cart 4287e39 Add cart page
  main         a902197 Add nav

HEAD moved with feature/cart; main stayed where it was:

text
A ── B ── C   ← main
           \
            D   ← feature/cart ← HEAD

Switch back with git switch main (or git switch - to jump to the previous branch, like cd -). Git rewrites your working tree to match the commit you switch to, so cart.html disappears from disk. It is not lost; it lives in commit D and returns when you switch back.

Other handy commands:

bash
git branch feature/search        # create without switching
git switch -c hotfix a902197     # create starting from a specific commit
git branch -m feature/cart feature/basket   # rename

What happens to uncommitted changes

Uncommitted edits belong to your working tree and index, not to any branch. When you switch:

  • If your edits do not touch files that differ between the two branches, Git carries them along unchanged. This is useful: you can start editing, realise you are on the wrong branch, and simply git switch -c a new one.
  • If an edited file differs between the branches, Git refuses rather than overwrite your work:
text
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

Commit the work (a work-in-progress commit is fine on your own branch) or set it aside with git stash, covered in the power tools lesson.

Fast-forward merges

Merging integrates another branch's commits into the branch you are on. You always merge into the current branch, so first switch to the receiving branch:

bash
git switch main
git merge feature/cart
text
Updating a902197..4287e39
Fast-forward
 cart.html | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 cart.html

Before the merge, main had not moved since feature/cart branched off. Every commit on main was already in feature/cart's history, so there was nothing to combine. Git simply slid the main pointer forward:

text
Before:  A ── B ── C   ← main
                    \
                     D   ← feature/cart

After:   A ── B ── C ── D   ← main, feature/cart

No new commit was created. This is a fast-forward, and it happens whenever the current branch is a direct ancestor of the branch being merged.

Merge commits

Usually both branches move. Say you start feature/search and commit on it, and meanwhile someone adds a footer on main:

bash
git log --oneline --graph --all
text
* c420dc2 Add search page
| * d90a3eb Add footer
|/  
* 4287e39 Add cart page
* a902197 Add nav
* a7098f8 Initial page

The histories have diverged: each branch has a commit the other lacks. A fast-forward is impossible, so Git performs a three-way merge. It finds the merge base (the most recent commit both branches share, here 4287e39), compares each branch tip against it, and combines the two sets of changes into a new snapshot. It then records that snapshot as a merge commit with two parents:

bash
git switch main
git merge feature/search
text
Merge made by the 'ort' strategy.
 search.html | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 search.html
text
*   7789053 Merge branch 'feature/search'
|\  
| * c420dc2 Add search page
* | d90a3eb Add footer
|/  
* 4287e39 Add cart page

(Without -m, Git opens your editor with the default message Merge branch 'feature/search'; save and close to accept it.) Inspect the merge commit and you will see two parent lines:

bash
git cat-file -p HEAD
text
tree e5ce5e7d4de89a311f40e34677a75a1a100eb996
parent d90a3eb42373d199803766948324ca13b63c3c31
parent c420dc29d142dcf1c4e429920d4ac1efd922bb12
author Ada Lovelace <[email protected]> 1791705827 +0000
committer Ada Lovelace <[email protected]> 1791705827 +0000

Merge branch 'feature/search'

The first parent is the branch you were on (main), the second is the branch you merged in. That order matters later when you inspect or undo merges. ort is simply the name of Git's default merge algorithm.

If both branches changed the same lines of a file, Git cannot decide which version wins and stops with a conflict for you to resolve. That gets its own lesson; for now, git merge --abort returns you to the state before the merge.

Now run both kinds of merge yourself. Click the suggested commands in order (or type them) and watch the pointers: the first merge only slides main forward, the second creates a new commit with two parents. Click any commit to see its parents.

InteractiveFast-forward, then a merge commitOpen in Labs
Loading interactive…

The first merge prints Fast-forward and creates nothing: main was an ancestor of feature/cart. After main gains its own commit the histories diverge, so the second merge records a merge commit whose first parent is main's old tip. Try git merge --no-ff on a fresh branch to force a merge commit anyway.

Choosing the merge style

You can force either behaviour with a flag:

CommandDiverged historyCurrent branch is behind (fast-forward possible)
git merge XMerge commitFast-forward
git merge --no-ff XMerge commitMerge commit anyway
git merge --ff-only XRefuses with an errorFast-forward

--no-ff is used by teams who want every feature to appear as a visible "bubble" in history, so it is clear which commits arrived together:

text
*   1024d74 Merge branch 'feature/reviews'
|\  
| * a861a63 Add reviews widget
|/  
* 7789053 ...

--ff-only is a safety switch: it only proceeds when no merge commit is needed, which is handy when you expect your branch to simply catch up.

Neither style is wrong. What matters is that a team picks one and applies it consistently; the workflows lesson discusses the trade-offs.

Cleaning up, and detached HEAD

Once a branch is merged, delete it. The commits stay in history; only the label goes:

bash
git branch --merged
git branch -d feature/search
text
  feature/search
* main
Deleted branch feature/search (was c420dc2).

-d is safe: it refuses to delete a branch whose commits are not merged into its upstream (or into the current branch, if it has no upstream), printing error: the branch 'spike' is not fully merged. Use -D only when you really mean to throw that work away. You cannot delete the branch you are currently on.

Here feature has two commits that main lacks. Try deleting it before and after merging, then make a commit in detached HEAD and leave it behind:

InteractiveSafe deletes and detached HEADOpen in Labs
Loading interactive…

Deleting the label after the merge loses nothing: the commits stay in main's history. The experiment commit is different. Once HEAD leaves it, no branch leads to it, so it fades out: only the reflog still remembers it.

Practice

Simulate two lines of work and merge them both ways:

  1. In a new repository, commit index.html on main.
  2. Create feature/about, add about.html in one commit, switch back to main and merge it. Was it a fast-forward?
  3. Create feature/contact from main and add contact.html. Before merging, switch to main and commit a change to index.html.
  4. Merge feature/contact into main. Draw the graph with git log --oneline --graph --all, and identify the merge base.
  5. Delete both feature branches.
Show solution
bash
git init site && cd site
echo "<h1>Home</h1>" > index.html
git add index.html && git commit -m "Add home page"

git switch -c feature/about
echo "<h1>About</h1>" > about.html
git add about.html && git commit -m "Add about page"
git switch main
git merge feature/about        # prints "Fast-forward": main had not moved

git switch -c feature/contact
echo "<h1>Contact</h1>" > contact.html
git add contact.html && git commit -m "Add contact page"

git switch main
echo "<p>Welcome</p>" >> index.html
git commit -am "Add welcome text"

git merge feature/contact      # editor opens; accept the message
git log --oneline --graph --all
text
*   5e1c2a9 Merge branch 'feature/contact'
|\  
| * 8b04f17 Add contact page
* | 31d9c40 Add welcome text
|/  
* 0c7a6d2 Add about page
* 9f3e115 Add home page

The merge base is "Add about page": the last commit both branches shared. Step 2 fast-forwarded because main was an ancestor of feature/about; step 4 needed a merge commit because both branches had new commits. Your IDs will differ.

bash
git branch -d feature/about feature/contact

Key takeaways

  • A branch is a movable pointer; creating one copies nothing, and committing moves only the current branch.
  • git switch and git switch -c change and create branches; git checkout is the older equivalent you will still see.
  • Uncommitted changes travel with you when you switch unless they would be overwritten, in which case Git refuses.
  • A fast-forward just moves the pointer; diverged branches get a three-way merge and a merge commit with two parents.
  • --no-ff always creates a merge commit and --ff-only refuses to create one.
  • Delete merged branches with git branch -d; in detached HEAD, create a branch before leaving to keep new commits.

Check yourself

Quiz

0/4 correct · 4 left

  1. 1.main is at commit C. You create feature from main, add commits D and E on feature, and nobody touches main. What does git merge feature do while on main?

  2. 2.In a merge commit created by running git merge feature while on main, which is the first parent?

  3. 3.You have unsaved edits to app.js and run git switch other. app.js is identical on both branches. What happens?

  4. 4.Which command fails instead of creating a merge commit when the branches have diverged?

Concepts in this lesson

One-page notes on each idea, for review or a quick lookup later.

Finished reading?

Mark it complete to fill in your roadmap.