# Branching & merging

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

Git & Collaboration roadmap · Core concepts · 17 min read · https://levelcode.org/roadmaps/git/branching-and-merging

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
```

> [!NOTE]
> Older tutorials and many colleagues use `git checkout <branch>` and `git checkout -b <branch>`. These do the same as `git switch` and `git switch -c`. `git checkout` is overloaded — it also restores files — which is why Git added the clearer `git switch` and `git restore`. You will meet both, so recognise both.

### 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 <ada@example.com> 1791705827 +0000
committer Ada Lovelace <ada@example.com> 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.

> [!TIP]
> Before merging, preview what is coming with `git log --oneline main..feature/search`. The `A..B` syntax means "commits reachable from B but not from A" — exactly the commits the merge will bring in.

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.

> **Interactive: Fast-forward, then a merge commit.** 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. Try it: https://levelcode.org/labs/git-graph

## Choosing the merge style

You can force either behaviour with a flag:

| Command | Diverged history | Current branch is behind (fast-forward possible) |
| --- | --- | --- |
| `git merge X` | Merge commit | Fast-forward |
| `git merge --no-ff X` | Merge commit | Merge commit anyway |
| `git merge --ff-only X` | Refuses with an error | Fast-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:

> **Interactive: Safe deletes and detached HEAD.** 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. Try it: https://levelcode.org/labs/git-graph

> [!WARNING]
> If you switch to a commit instead of a branch — `git switch --detach a7098f8`, or `git checkout a7098f8` — you enter **detached HEAD**: `HEAD` points straight at a commit, with no branch to move. Looking around is harmless. But commits you make there belong to no branch, and once you switch away they become hard to find. If you want to keep them, run `git switch -c new-branch-name` before leaving.

## 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.

**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.

## Quiz

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?

   A. Creates a merge commit with parents C and E
   B. Moves main forward to E without creating a new commit
   C. Copies D and E as new commits onto main
   D. Refuses because feature has commits main lacks

   **Answer: B.** main is an ancestor of feature, so there is nothing to combine. Git performs a fast-forward and simply moves the main pointer to E.

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

   A. The merge base
   B. The tip of feature
   C. Whichever commit is older
   D. The commit main pointed to before the merge

   **Answer: D.** The first parent is always the branch you were on when you merged; the merged-in branch tip becomes the second parent.

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

   A. Git switches and your edits to app.js come with you
   B. Git refuses to switch
   C. Git discards your edits
   D. Git commits your edits to the current branch first

   **Answer: A.** Uncommitted changes belong to the working tree, not a branch. Since switching does not need to rewrite app.js, Git keeps your edits. It only refuses when a modified file differs between the branches.

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

   A. git merge --no-ff feature
   B. git merge feature
   C. git merge --ff-only feature
   D. git switch --merge feature

   **Answer: C.** --ff-only allows only fast-forwards. With diverged history a fast-forward is impossible, so it aborts. --no-ff does the opposite: it always creates a merge commit.
