Branching
Before you start
You need: remotes (Article 02).
Time: about 45 minutes, at the keyboard.
Learning objective
Work on branches confidently — create, switch, merge and delete them — and explain what a branch actually is.
Topics
- What a branch is
- Creating and switching
- Naming conventions
- Merging, fast-forward and three-way
- Deleting branches
HEADand detachedHEAD- Comparing branches
- Recovering a deleted branch
What a branch is
A branch is a movable pointer to a commit. That is the whole implementation — a file in .git/refs/heads/ containing a 40-character hash.
cat .git/refs/heads/main
a3f9c1d8e2b4f6a0c9d3e7b1f5a8c2d4e6b0f9a1
Nothing is copied when you branch. Creating one writes a file with a hash in it, which is why branching in Git is instant and why teams create them freely.
A---B---C (main)
\
D---E (feature/student-search)
main points at C, the feature branch at E. Both share A, B and C — one copy of each.
Committing moves the pointer. Commit on the feature branch and it advances to F; main stays at C.
Understanding this makes the rest of Git tractable: merging moves a pointer, deleting a branch removes a pointer, and a "lost" branch is a commit whose pointer is gone but whose objects remain.
Creating and switching
git switch -c feature/student-search # create and switch
git switch main # switch
git switch - # previous branch
git switch -c hotfix/fee-rounding main # branch from a specific point
git switch --detach a3f9c1d # detached HEAD, deliberately
Older syntax, still everywhere:
git checkout -b feature/student-search
git checkout main
switch and restore were introduced because checkout did too many unrelated things — switching branches, restoring files, detaching HEAD. Prefer switch; recognise checkout.
git branch # local
git branch -a # including remote-tracking
git branch -vv # with tracking and last commit
git branch --merged main # merged into main — safe to delete
git branch --no-merged main # not merged — deleting loses work
git branch --merged main is the command that makes branch cleanup safe. Anything it lists can be deleted without losing anything.
Naming
feature/student-search
feature/142-fee-report
bugfix/absent-marks-counted-as-zero
hotfix/login-500
release/2.4.0
chore/update-dapper
| Rule | Reason |
|---|---|
| Lowercase, hyphens | Some filesystems are case-insensitive; Feature/X and feature/X collide |
| A type prefix | Sorting and filtering |
| The ticket number | Links branch to the issue |
| Descriptive | fix-bug tells the next person nothing |
Avoid spaces, ~, ^, :, ?, *, [ — Git rejects some and shells mangle others.
Merging
git switch main
git merge feature/student-search
Fast-forward — main has no new commits, so Git just moves the pointer:
Before: A---B---C (main)
\
D---E (feature)
After: A---B---C---D---E (main, feature)
Three-way — both branches moved, so Git creates a merge commit:
Before: A---B---C---F (main)
\
D---E (feature)
After: A---B---C---F---M (main)
\ /
D---E
git merge --no-ff feature/x # always create a merge commit
git merge --ff-only feature/x # fail unless fast-forward
git merge --squash feature/x # combine into one staged change
git merge --abort # back out of a conflicted merge
--no-ff keeps the feature visible as a unit. Without it, a fast-forwarded branch is indistinguishable from commits made directly on main, and reverting the whole feature means reverting each commit individually. Many teams require it.
--squash turns a branch into one commit — useful when a feature branch has fifteen "wip" commits nobody needs. It does not record the merge, so the branch shows as unmerged afterwards.
Deleting
git branch -d feature/student-search # refuses if unmerged
git branch -D feature/student-search # force
git push origin --delete feature/student-search
git fetch --prune
-d is a safety check. It refuses to delete a branch whose commits are not reachable from anywhere else. -D overrides it — use it for a genuinely abandoned experiment, never to escape a warning you have not read.
Deleting a local branch does not delete the remote one. Both are separate steps, and forgetting the second is why repositories accumulate hundreds of stale remote branches.
HEAD
HEAD is a pointer to your current branch, which points to a commit.
HEAD → main → a3f9c1d
cat .git/HEAD # ref: refs/heads/main
git rev-parse HEAD # the commit hash
Useful notation:
HEAD # current commit
HEAD~1 # one before
HEAD~3 # three before
HEAD^ # first parent
HEAD^2 # second parent, on a merge commit
Detached HEAD — checking out a commit directly points HEAD at the commit rather than a branch:
HEAD → a3f9c1d
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them...
It is not an error. It is normal when inspecting an old commit or a tag.
The danger is committing there. Those commits belong to no branch, so switching away leaves them unreachable and eventually garbage-collected.
git switch -c experiment/caching # keep the work
git switch main # abandon it
If you already switched away:
git reflog # find the hash
git branch rescue a3f9c1d
Comparing branches
git diff main feature/student-search
git diff main...feature/student-search # since they diverged
git log main..feature/student-search # commits on feature, not main
git log feature/student-search..main # the reverse
git log --oneline --graph --all
git branch --contains a3f9c1d # which branches hold this commit
Two dots and three dots differ. main..feature is "commits in feature not in main". main...feature in a diff is "changes on feature since the branch point" — which is what a pull request shows, and usually what you want.
Recovering
git reflog
a3f9c1d HEAD@{0}: checkout: moving from feature/search to main
8c2e4f1 HEAD@{1}: commit: Add roll number filter
1a9b3c5 HEAD@{2}: commit: Add search endpoint
git reflog records every position HEAD has held, including commits on branches you deleted. It is local, and entries expire after about 90 days.
git branch recovered 8c2e4f1
git switch recovered
Almost nothing committed is truly lost. Deleted branch, bad reset, abandoned detached-HEAD work — reflog finds it. What reflog cannot recover is a change that was never committed.
Diagnosing
| Symptom | Cause |
|---|---|
cannot switch, you have local changes | Uncommitted work — commit or stash |
| Detached HEAD | Checked out a commit or tag, not a branch |
-d refuses to delete | Unmerged commits — check before -D |
| Branch deleted, work missing | git reflog, then branch from the hash |
| Remote branch still listed | Deleted locally only, or not pruned |
merge did nothing | Already merged, or already up to date |
| Merge produced no merge commit | Fast-forward — use --no-ff |
Errors you will hit
| Message | Cause | Fix |
|---|---|---|
cannot switch branch, you have local changes | Uncommitted work | Commit or stash |
You are in 'detached HEAD' state | Checked out a commit, not a branch | Normal for inspection; branch to keep work |
-d refuses to delete | Unmerged commits | Check before using -D |
| A deleted branch's work is gone | Branch pointer removed | git reflog recovers it |
| A remote branch still appears | Not pruned | git fetch --prune |
| Merge created no merge commit | Fast-forward | --no-ff if the team requires it |
Committing in detached HEAD then switching away leaves the work unreachable. git reflog finds it, but only for about 90 days.
Common mistakes
- Working on
maininstead of a branch - Branch names that describe nothing
- Committing in detached
HEADthen switching away -Dto silence a warning about unmerged work- Deleting locally and assuming the remote followed
- Never pruning
- Long-lived branches that drift for weeks
- Merging without
--no-ffwhere the team requires it - Confusing
..and...
Practice
The course exercise is create branches and merge them.
- Inspect
.git/refs/heads/mainand confirm it holds a commit hash. - Create a branch, commit, and check both refs to see one pointer move.
- Create a branch, commit, switch to
mainwith no commits, and merge. Confirm it fast-forwards. - Force the three-way case: commit on
mainand on the branch, then merge. - Merge with
--no-ffand compare the graph. - Merge with
--squashand observe that the branch still shows as unmerged. - Run
git branch --merged mainand--no-merged main, and delete only what the first lists. - Try
-don an unmerged branch. Read the message rather than reaching for-D. - Check out a commit hash, commit in detached HEAD, switch to
main, then recover withgit reflog. - Delete a branch with
-D, then recover it from reflog. - Compare
git diff main featurewithgit diff main...featureafter committing on both. - Use
git branch --contains <hash>to find which branches hold a commit. - Delete a branch locally, confirm it still exists on the remote, then delete it there.
Exercises 9 and 10 are the ones that make reflog part of your toolkit rather than something you read about.
You can now
- Create, switch, merge and delete branches
- Say what a branch actually is
- Recognise and recover from detached HEAD
- Use
--mergedto delete safely - Recover a deleted branch from reflog
Review questions
- What does Git create when you make a branch?
- When does a merge fast-forward, and why might a team forbid it?
- Why is committing in detached
HEADrisky, and how do you keep the work? - What can
git reflogrecover, and what can it not?
Next: Merge conflicts