Branching
A branch in git is a lightweight movable pointer to a commit. Creating a branch does not copy files. Git simply writes a 41-byte file containing the SHA-1 hash the branch points to. This makes branching nearly instant, regardless of repository size.
Branches let you isolate work. You can develop a feature, fix a bug, or run
an experiment on a branch without touching the code on main. When the work
is ready, you bring it back by merging.
How branches work
Every repository starts with one branch. By convention it is named main
(or master in older repositories). HEAD is a special pointer that tracks
which branch you are currently on:
A---B---C (main, HEAD)
When you create a branch, git adds a new pointer at the same commit. HEAD
stays on the original branch:
A---B---C (main, HEAD)
^
(feature)
When you switch to the new branch and commit, the feature pointer advances.
main stays where it was:
A---B---C (main)
\
D---E (feature, HEAD)
Managing branches
git branch
List all local branches. The branch with * is the one HEAD points to:
$ git branch
* main
feature/login
fix/timeout
Pass -a to include remote-tracking branches:
$ git branch -a
* main
feature/login
remotes/origin/main
remotes/origin/feature/login
Pass -v to see the last commit on each branch:
$ git branch -v
* main ca82a6d update readme
feature/login 2b1c673 add login form
Create a branch
Create a branch at the current commit without switching to it:
git branch feature/user-auth
git switch and git checkout
git switch is the modern command for changing branches. git checkout does
the same and is still widely used:
# switch to an existing branch
git switch feature/user-auth
git checkout feature/user-auth # equivalent
# create a branch and switch to it in one step
git switch -c feature/user-auth
git checkout -b feature/user-auth # equivalent
Delete a branch
Delete a branch after its work has been merged. Git refuses to delete a branch with unmerged commits unless you force it:
git branch -d feature/user-auth # safe: refuses if unmerged
git branch -D feature/user-auth # force: deletes regardless
Merging
Merging integrates the history of one branch into another. Switch to the
branch you want to merge into (usually main), then run git merge:
git checkout main
git merge feature/user-auth
Git uses one of two strategies depending on the state of the branches.
Fast-forward merge
A fast-forward merge happens when the target branch has not diverged from the
source. main simply moves its pointer forward to the tip of the feature
branch. No new commit is created:
Before: After git merge feature:
A---B---C (main) A---B---C---D---E (main, HEAD)
\
D---E (feature)
$ git merge feature/login
Updating ca82a6d..2b1c673
Fast-forward
login.go | 42 +++++++++++++++++++++
1 file changed, 42 insertions(+)
Three-way merge
A three-way merge happens when both branches have diverged from a common ancestor. Git identifies three commits: the common ancestor, the tip of the target branch, and the tip of the source branch. It combines them into a new merge commit that has two parents:
Before: After git merge feature:
A---B---C---F (main) A---B---C---F---G (main, HEAD)
\ \ /
D---E (feature) D---E
$ git merge feature/login
Merge made by the 'ort' strategy.
login.go | 42 +++++++++++++++++++++
1 file changed, 42 insertions(+)
Resolving conflicts
A conflict occurs when two branches modify the same lines in the same file. Git cannot decide automatically which version to keep, so it pauses the merge and asks you to resolve it.
$ git merge feature/login
Auto-merging auth.go
CONFLICT (content): Merge conflict in auth.go
Automatic merge failed; fix conflicts and then commit the result.
Git marks the conflicting region in the file:
<<<<<<< HEAD
func authenticate(user string) bool {
return checkPassword(user)
=======
func authenticate(user, token string) bool {
return checkToken(user, token)
>>>>>>> feature/login
The section between <<<<<<< HEAD and ======= is the version on your
current branch. The section between ======= and >>>>>>> is the incoming
version. Edit the file to produce the result you want, removing all three
marker lines:
func authenticate(user, token string) bool {
return checkToken(user, token)
}
After editing every conflicted file, stage them and complete the merge:
git add auth.go
git commit
To abandon a merge in progress and return to the state before you ran
git merge:
git merge --abort
Keeping a branch up to date
While you work on a feature branch, main may receive new commits from other
contributors. Two approaches bring those commits into your branch.
Merge main into your branch
This preserves the exact history of both branches but adds a merge commit:
git checkout feature/login
git merge main
Rebase onto main
Rebasing replays your commits on top of the latest main, producing a linear
history. See Rebasing for a full explanation. For a long-running
feature branch, rebasing is usually the cleaner choice:
git fetch origin
git rebase origin/main
Viewing branch relationships
git log --oneline --graph --all --decorate shows the full branch and merge
history as an ASCII graph:
$ git log --oneline --graph --all --decorate
* g7f3a1b (HEAD -> main) Merge branch 'feature/login'
|\
| * 2b1c673 (feature/login) add token validation
| * 4e3af98 add login handler
* | ca82a6d update readme
|/
* a11bef0 initial commit