Skip to main content
Published / updated

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
  • HEAD and detached HEAD
  • 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
RuleReason
Lowercase, hyphensSome filesystems are case-insensitive; Feature/X and feature/X collide
A type prefixSorting and filtering
The ticket numberLinks branch to the issue
Descriptivefix-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-forwardmain 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 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

SymptomCause
cannot switch, you have local changesUncommitted work — commit or stash
Detached HEADChecked out a commit or tag, not a branch
-d refuses to deleteUnmerged commits — check before -D
Branch deleted, work missinggit reflog, then branch from the hash
Remote branch still listedDeleted locally only, or not pruned
merge did nothingAlready merged, or already up to date
Merge produced no merge commitFast-forward — use --no-ff

Errors you will hit

MessageCauseFix
cannot switch branch, you have local changesUncommitted workCommit or stash
You are in 'detached HEAD' stateChecked out a commit, not a branchNormal for inspection; branch to keep work
-d refuses to deleteUnmerged commitsCheck before using -D
A deleted branch's work is goneBranch pointer removedgit reflog recovers it
A remote branch still appearsNot prunedgit fetch --prune
Merge created no merge commitFast-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 main instead of a branch
  • Branch names that describe nothing
  • Committing in detached HEAD then switching away
  • -D to 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-ff where the team requires it
  • Confusing .. and ...

Practice

The course exercise is create branches and merge them.

  1. Inspect .git/refs/heads/main and confirm it holds a commit hash.
  2. Create a branch, commit, and check both refs to see one pointer move.
  3. Create a branch, commit, switch to main with no commits, and merge. Confirm it fast-forwards.
  4. Force the three-way case: commit on main and on the branch, then merge.
  5. Merge with --no-ff and compare the graph.
  6. Merge with --squash and observe that the branch still shows as unmerged.
  7. Run git branch --merged main and --no-merged main, and delete only what the first lists.
  8. Try -d on an unmerged branch. Read the message rather than reaching for -D.
  9. Check out a commit hash, commit in detached HEAD, switch to main, then recover with git reflog.
  10. Delete a branch with -D, then recover it from reflog.
  11. Compare git diff main feature with git diff main...feature after committing on both.
  12. Use git branch --contains <hash> to find which branches hold a commit.
  13. 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 --merged to delete safely
  • Recover a deleted branch from reflog

Review questions

  1. What does Git create when you make a branch?
  2. When does a merge fast-forward, and why might a team forbid it?
  3. Why is committing in detached HEAD risky, and how do you keep the work?
  4. What can git reflog recover, and what can it not?

Next: Merge conflicts