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.

Three commits, aedb4dc, 5a75fd4 and c8dae00, each pointing to its parent. The branch main points to 5a75fd4, the branch syslog points to c8dae00, and HEAD points to the branch syslog. aedb4dc 5a75fd4 c8dae00 parent main syslog HEAD current branch
The state at the end of the example above. Arrows between commits point from child to parent. Branches point to commits; 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.