Branches
How branches let you work on several things at once: creating and switching branches, what HEAD points to, the default branch, and the detached HEAD state.
What a branch really is
A
branch
sounds like a copy of the project, but in Git it is far smaller: a
name that points to one commit. The commit itself knows its parent,
that one knows its parent, and so on, so the pointer is enough to
describe the whole line of work. You can see this directly inside
the .git folder:
git init -q netops && cd netops
printf 'hostname edge-01\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
printf 'ntp server 192.0.2.10\n' >> router.cfg
git commit -q -am "Add NTP server"
git log --oneline
cat .git/refs/heads/main
git rev-parse main
cat .git/HEAD
5a75fd4 Add NTP server aedb4dc Add edge-01 base config 5a75fd41be199730678ccec34815f03d40a37c7b 5a75fd41be199730678ccec34815f03d40a37c7b ref: refs/heads/main
The branch main is a file,
.git/refs/heads/main, that contains the 40-character
hash of the newest commit. git rev-parse main asks Git
the same question in a supported way (in some repositories branch
files are packed together in .git/packed-refs, so
rev-parse is the reliable method). The file
.git/HEAD does not contain a hash but a reference to a
branch: "I am on main". How these references and
objects fit together is explained on
How Git Stores Your Work.
Because a branch is only a 41-byte file, creating one is instant, whatever the size of the project. Nothing is copied. That is why Git users create branches for even small pieces of work.
Creating, listing and switching
Creating and listing branches
git branch <name> creates a new branch at the
current commit, without switching to it. git branch
on its own lists local branches and marks the current one with
*; -v adds the commit each one points to.
git init -q netops && cd netops
printf 'hostname edge-01\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
printf 'ntp server 192.0.2.10\n' >> router.cfg
git commit -q -am "Add NTP server"
git branch syslog # create a branch: a new pointer to the current commit
git branch -v # list branches; * marks the one HEAD points to
git switch syslog # move HEAD to the other branch
printf 'logging host 192.0.2.20\n' >> router.cfg
git commit -q -am "Add syslog server"
git branch -v
cat .git/HEAD
* main 5a75fd4 Add NTP server syslog 5a75fd4 Add NTP server Switched to branch 'syslog' main 5a75fd4 Add NTP server * syslog c8dae00 Add syslog server ref: refs/heads/syslog
Right after git branch syslog, both branches point to
the same commit 5a75fd4. After switching and
committing, syslog has moved to the new commit
c8dae00, while main stayed where it was.
Other useful listings: git branch -a includes
remote-tracking branches, git branch --merged shows
branches whose work is already contained in the current branch, and
git branch --show-current prints only the current
branch name (handy in scripts).
Switching branches
Branch switching moves
HEAD to another branch and updates the working tree and
staging area to that branch's latest commit: files that differ are
rewritten, files that only exist on the other branch appear, and
files that do not exist there disappear (they are safe in their own
branch's commits). The commands:
| Task | git switch |
Older equivalent |
|---|---|---|
| Switch to an existing branch | git switch syslog |
git checkout syslog |
| Create a branch and switch to it | git switch -c syslog |
git checkout -b syslog |
| Create it from another starting point | git switch -c hotfix v1.4 |
git checkout -b hotfix v1.4 |
| Go back to the previous branch | git switch - |
git checkout - |
Look at an old commit (detached HEAD) |
git switch --detach HEAD~3 |
git checkout HEAD~3 |
git checkout is the older command and does many
different jobs: switching branches, but also restoring files (git checkout -- file). Because one command with two very different effects caused
mistakes, Git 2.23 (2019) split the work into
git switch (branches) and
git restore (files). Both were labeled experimental
until Git 2.51 (2025); since then their behavior is stable.
git checkout is not being removed, so you will keep
seeing it in scripts and older documentation.
HEAD follows the branch
HEAD answers the question "where am I?". Normally it
points to a branch, and that branch points to a commit. When you
commit, Git creates the new commit with the current commit as its
parent and then moves the branch that HEAD points to.
HEAD itself does not change: it still says
ref: refs/heads/syslog, and so it follows along.
HEAD points to a branch. The next commit moves
syslog forward, and HEAD comes along.
This also explains why commits are never "on" a branch in the way
files are in a folder. A commit belongs to every branch from which
it can be reached by following parents: aedb4dc and
5a75fd4 are on both main and
syslog.
git init -q netops && cd netops
printf 'hostname edge-01\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
printf 'ntp server 192.0.2.10\n' >> router.cfg
git commit -q -am "Add NTP server"
printf 'ntp server 192.0.2.11\n' >> router.cfg
git commit -q -am "Add second NTP server" # oops: meant for a topic branch
git switch -c ntp-fix # new branch at the current commit, and switch to it
git branch -f main HEAD~1 # move main back one commit
git branch -v
Switched to a new branch 'ntp-fix' main 5a75fd4 Add NTP server * ntp-fix 6d5c9c2 Add second NTP server
git branch -f main HEAD~1 forces the existing branch
main to point one commit back (it refuses to move the
branch you are currently on, which is why you switch first). If the
commit was already pushed, others may have it too; then use
git revert instead (see
Undoing Things).
The default branch
The default branch is the branch a new repository starts with, and on a hosting service the branch that clones check out and pull requests target unless you choose otherwise. It is an ordinary branch; only convention and settings make it special. Teams usually keep it in a working, deployable state.
For most of Git's history, the first branch was called
master. Around 2020 many projects and hosting services
moved to main, and Git 2.28 (2020) added the setting
init.defaultBranch to choose the name. When the setting
is missing, Git 2.39 still uses master and prints a
hint:
git init netops
hint: Using 'master' as the name for the initial branch. This default branch name hint: is subject to change. To configure the initial branch name to use in all hint: of your new repositories, which will suppress this warning, call: hint: hint: git config --global init.defaultBranch <name> hint: hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and hint: 'development'. The just-created branch can be renamed via this command: hint: hint: git branch -m <name> Initialized empty Git repository in /home/sam/netops/.git/
(This is Git 2.39's wording. Since Git 2.52 the hint says the
default will change to main in Git 3.0.)
Set it once with
git config --global init.defaultBranch main, or choose
per repository with git init -b main. An existing
repository's branch can be renamed with git branch -m:
git init -q --initial-branch=master old-repo && cd old-repo
git commit -q --allow-empty -m "First commit"
git branch
git branch -m master main # rename the current branch
git branch
* master * main
GitHub names the default branch of new repositories
main, and you can change it per repository in its
settings. Git itself plans to make main the built-in
default in Git 3.0, which has no release date yet. Renaming the
default branch of a shared repository affects everyone's clones and
any automation that names the branch, so it is a change to plan, not
to do casually.
Topic branches
A topic branch (also called a feature branch) is a short-lived branch for one piece of work: a feature, a fix, a config change. You create it from the default branch, commit on it, get it reviewed and merged, and then delete it. Meanwhile the default branch stays stable, and several people can work on separate topics without disturbing each other.
Good topic branch names say what the work is, often with a prefix
and a ticket number: fix/ntp-edge-01,
feature/acl-noc-subnet,
chg-1042-syslog-collector. Slashes are allowed and some
tools group branches by them. Spaces and a few special characters
are not allowed. Short lifetimes keep merges easy: a branch that
lives for two days rarely conflicts, one that lives for two months
almost always does. How teams organize branches at scale is the
subject of
Branching Strategies and Releases.
Detached HEAD
In the detached HEAD state,
HEAD points directly at a commit instead of at a
branch. You get there by switching to something that is not a
branch: an older commit, a tag, or a remote-tracking branch. It is
perfectly normal, for example to test an old version, and
git bisect uses it all the time. With
git switch you have to ask for it explicitly:
git init -q netops && cd netops
printf 'hostname edge-01\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
printf 'ntp server 192.0.2.10\n' >> router.cfg
git commit -q -am "Add NTP server"
git switch --detach HEAD~1
git status | head -1
HEAD is now at aedb4dc Add edge-01 base config HEAD detached at aedb4dc
Without --detach,
git switch HEAD~1 refuses, because
switch expects a branch.
git checkout detaches without asking but prints a long
explanation instead — the message that alarms many people:
git init -q netops && cd netops
printf 'hostname edge-01\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
printf 'ntp server 192.0.2.10\n' >> router.cfg
git commit -q -am "Add NTP server"
git checkout HEAD~1
Note: switching to 'HEAD~1'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by switching back to a branch. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -c with the switch command. Example: git switch -c <new-branch-name> Or undo this operation with: git switch - Turn off this advice by setting config variable advice.detachedHead to false HEAD is now at aedb4dc Add edge-01 base config
The message says it all: you can look around and even commit, but no
branch moves along with your new commits. The risk comes when you
leave. Commits that no branch points to are only reachable through
HEAD, so when HEAD moves elsewhere, they
become invisible in the normal history. Git warns you:
git init -q netops && cd netops
printf 'hostname edge-01\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
printf 'ntp server 192.0.2.10\n' >> router.cfg
git commit -q -am "Add NTP server"
git switch -q --detach HEAD~1
printf 'snmp-server location lab\n' >> router.cfg
git commit -q -am "Try SNMP location"
git switch main
Warning: you are leaving 1 commit behind, not connected to any of your branches: 7563f39 Try SNMP location If you want to keep it by creating a new branch, this may be a good time to do so with: git branch <new-branch-name> 7563f39 Switched to branch 'main'
To keep the work, create a branch before leaving, with
git switch -c. The commit then has a name and is part
of the normal history:
git init -q netops && cd netops
printf 'hostname edge-01\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
printf 'ntp server 192.0.2.10\n' >> router.cfg
git commit -q -am "Add NTP server"
git switch -q --detach HEAD~1
printf 'snmp-server location lab\n' >> router.cfg
git commit -q -am "Try SNMP location"
git switch -c snmp-test # give the commit a branch before leaving
git log --oneline --all --graph
Switched to a new branch 'snmp-test' * 5a75fd4 Add NTP server | * 7563f39 Try SNMP location |/ * aedb4dc Add edge-01 base config
Deleting branches
Deleting a branch deletes the pointer, not the commits. What matters
is whether those commits are still reachable from another branch.
git branch -d (lowercase) only deletes a branch when
its commits are merged: into the branch's upstream if it has one,
otherwise into the current branch. That way no work becomes
unreachable. Otherwise it refuses:
git init -q netops && cd netops
printf 'hostname edge-01\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
printf 'ntp server 192.0.2.10\n' >> router.cfg
git commit -q -am "Add NTP server"
git switch -q -c ntp-fix
printf 'ntp server 192.0.2.11\n' >> router.cfg
git commit -q -am "Add second NTP server"
git switch -q main
git branch -d ntp-fix
error: The branch 'ntp-fix' is not fully merged. If you are sure you want to delete it, run 'git branch -D ntp-fix'.
git branch -D (uppercase, short for
--delete --force) deletes it anyway, and prints the
hash the branch pointed to — your note for a recovery with
git branch ntp-fix 6d5c9c2 if you change your mind:
git init -q netops && cd netops
printf 'hostname edge-01\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
printf 'ntp server 192.0.2.10\n' >> router.cfg
git commit -q -am "Add NTP server"
git switch -q -c ntp-fix
printf 'ntp server 192.0.2.11\n' >> router.cfg
git commit -q -am "Add second NTP server"
git switch -q main
git branch -D ntp-fix
git branch -q topic; git branch -d topic
Deleted branch ntp-fix (was 6d5c9c2). Deleted branch topic (was 5a75fd4).
The second deletion, of topic, succeeded with
-d because that branch pointed to a commit that is
already on main. You cannot delete the branch you are
on; switch to another one first. Deleting a branch on the server is
a separate step (git push origin --delete ntp-fix, see
Remotes and Collaboration); GitHub can also delete the branch automatically after a pull
request is merged.
Switching with uncommitted changes
Uncommitted changes in the working tree and staging area do not
belong to any branch. When you switch, Git tries to keep them. If
the files you changed are identical in both branches' commits,
switching is safe and the changes simply come along. Git lists them
with M (modified):
git init -q netops && cd netops
printf 'hostname edge-01\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
printf 'ntp server 192.0.2.10\n' >> router.cfg
git commit -q -am "Add NTP server"
git branch acl-update # same commit as main
printf 'banner motd "Authorized access only"\n' >> router.cfg
git switch acl-update # the uncommitted change comes along
git status --short
Switched to branch 'acl-update' M router.cfg M router.cfg
If switching would overwrite a file you changed — because the target branch has a different version of that file — Git refuses and changes nothing:
git init -q netops && cd netops
printf 'hostname edge-01\n' > router.cfg
git add router.cfg && git commit -q -m "Add edge-01 base config"
printf 'ntp server 192.0.2.10\n' >> router.cfg
git commit -q -am "Add NTP server"
git switch -q -c syslog
printf 'logging host 192.0.2.20\n' >> router.cfg
git commit -q -am "Add syslog server"
git switch -q main
printf 'banner motd "Authorized access only"\n' >> router.cfg # not committed
git switch syslog
error: Your local changes to the following files would be overwritten by checkout: router.cfg Please commit your changes or stash them before you switch branches. Aborting
You then have three good options: commit the change (on the current
branch, or on a new one with git switch -c, which never
conflicts because it starts at the same commit), stash it
temporarily with git stash (see
Undoing Things), or discard it
with git restore if you do not need it.