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.
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:
A ── B ── C ← main ← HEADCreating a branch adds a second pointer to the same commit. Nothing is copied:
A ── B ── C ← main ← HEAD
↖ feature/cartWhen 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:
git branch* mainCreate a branch and switch to it in one step with git switch -c (short for --create):
git switch -c feature/cartSwitched to a new branch 'feature/cart'Now commit something on it:
echo "cart" > cart.html
git add cart.html
git commit -m "Add cart page"
git branch -v* feature/cart 4287e39 Add cart page
main a902197 Add navHEAD moved with feature/cart; main stayed where it was:
A ── B ── C ← main
\
D ← feature/cart ← HEADSwitch 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:
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 # renameWhat 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 -ca new one. - If an edited file differs between the branches, Git refuses rather than overwrite your work:
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.
AbortingCommit 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:
git switch main
git merge feature/cartUpdating a902197..4287e39
Fast-forward
cart.html | 1 +
1 file changed, 1 insertion(+)
create mode 100644 cart.htmlBefore 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:
Before: A ── B ── C ← main
\
D ← feature/cart
After: A ── B ── C ── D ← main, feature/cartNo 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:
git log --oneline --graph --all* c420dc2 Add search page
| * d90a3eb Add footer
|/
* 4287e39 Add cart page
* a902197 Add nav
* a7098f8 Initial pageThe 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:
git switch main
git merge feature/searchMerge made by the 'ort' strategy.
search.html | 1 +
1 file changed, 1 insertion(+)
create mode 100644 search.html* 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:
git cat-file -p HEADtree 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.
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:
| 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:
* 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:
git branch --merged
git branch -d feature/search 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:
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:
- In a new repository, commit
index.htmlonmain. - Create
feature/about, addabout.htmlin one commit, switch back tomainand merge it. Was it a fast-forward? - Create
feature/contactfrommainand addcontact.html. Before merging, switch tomainand commit a change toindex.html. - Merge
feature/contactintomain. Draw the graph withgit log --oneline --graph --all, and identify the merge base. - Delete both feature branches.
Show solution
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* 5e1c2a9 Merge branch 'feature/contact'
|\
| * 8b04f17 Add contact page
* | 31d9c40 Add welcome text
|/
* 0c7a6d2 Add about page
* 9f3e115 Add home pageThe 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.
git branch -d feature/about feature/contactKey takeaways
- A branch is a movable pointer; creating one copies nothing, and committing moves only the current branch.
git switchandgit switch -cchange and create branches;git checkoutis 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-ffalways creates a merge commit and--ff-onlyrefuses 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.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.In a merge commit created by running git merge feature while on main, which is the first parent?
3.You have unsaved edits to app.js and run git switch other. app.js is identical on both branches. What happens?
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.
- Branches, refs and HEADA branch is a file holding one commit hash; HEAD says which branch you’re on.
- Fast-forward vs merge commitsIf the target hasn’t moved, Git just slides the pointer; otherwise it records a commit with two parents.
- Three-way merges and conflictsGit compares both sides with their common ancestor; a conflict is a line both sides changed.
Finished reading?
Mark it complete to fill in your roadmap.