Skip to main content
RunBook Academy

Git, CI/CD & GitOpsVIII · BranchingBranching

Branch creation and switching — git switch, git checkout, and the three-tree update

Intermediate⏱ ~18 mingit

What you'll learn

  • Create a branch with git switch -c or git checkout -b and explain the two-phase operation
  • Describe what git switch does to HEAD, the index, and the working tree
  • Recognise why a clean working tree is required for a safe branch switch and what git status reports otherwise
  • Use git switch - to toggle between the previous branch and the current one
  • Distinguish a branch switch from a detached-HEAD checkout with git switch --detach

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

Creating a branch and switching to it are two operations that should feel like one. They are not. The branch is a ref write — a single file under .git/refs/heads/ is created or updated. The switch is a three-tree update — HEAD moves, the index is rewritten from the new branch’s tree, and the working tree files are updated to match. The distinction is not academic. The first operation is free; the second operation can refuse to run if your working tree is dirty. A production engineer who has never been bitten by the difference has not yet worked on a long-running repository.

The two-phase operation

The most common incantation for starting a new line of work is git switch -c <name>. This is one command in the shell but two operations in Git:

  1. Create the branch ref under refs/heads/<name>, pointing at the current HEAD.
  2. Move HEAD to point at the new branch, rewrite the index from the new branch’s tree object, and update the working tree.

The -c flag is short for --create. Without it, git switch <name> performs only phase 2 — the branch must already exist.

# Phase 1 + Phase 2: create and switch in one command
git switch -c feature/iam-rotation

# Equivalent legacy spelling (still common in older scripts)
git checkout -b feature/iam-rotation

# Phase 2 only: switch to an existing branch
git switch main

# Equivalent legacy spelling
git checkout main

The two spellings are functionally identical. git switch was introduced in Git 2.23 specifically to disambiguate “switch to a branch” from “restore a file from the index”, which git checkout had overloaded into one command. New work should use git switch; older scripts and muscle memory will keep git checkout alive for years.

sequenceDiagram
    participant Dev as Engineer
    participant Refs as refs/heads/
    participant HEAD as HEAD
    participant IDX as Index
    participant WT as Working tree

    Dev->>Refs: git switch -c feature/x
    Note over Refs: write file with current OID
    Refs->>HEAD: HEAD -> refs/heads/feature/x
    HEAD->>IDX: rewrite index from feature/x tree
    IDX->>WT: update working tree to match
flowchart LR
    A["HEAD\n(current branch)"] --> B["refs/heads/feature/x\n(new branch ref)"]
    B --> C[".git/refs/heads/feature/x\n(40-char SHA)"]
    C --> D["working tree\n(feature/x files)"]

What happens to the three trees

When git switch (or git checkout) runs successfully, three things happen in order:

  • HEAD is moved. HEAD is a file under .git/ whose contents are the textual name of the current branch (ref: refs/heads/main followed by a newline). HEAD is rewritten to point at the new branch. The branch ref itself is unchanged — you have not committed anything, so the tip stays where it was.
  • The index is rewritten. The binary manifest at .git/index is replaced with a fresh serialisation of the tree object that the new branch points at. Every path gets a new blob OID; the cache of stat() information is reset. This is fast because Git reads the tree object directly; it does not scan the working tree.
  • The working tree is updated. For each path whose blob OID changed between the old HEAD’s tree and the new branch’s tree, Git writes the new bytes to disk. Unchanged paths are not touched. A path that is clean in the index but dirty in the working tree (edited but not staged) blocks the switch — Git refuses, because writing the new bytes would silently destroy the uncommitted edits.
git switch main
# Switched to branch 'main'

git switch feature/iam-rotation
# Switched to branch 'feature/iam-rotation'

The two-line “Switched to branch ’…’” output is the only thing the user sees; behind the scenes, three trees have been rewired and zero or more working-tree files have been rewritten.

Why a clean working tree is required

The most common reason a branch switch fails is an unclean working tree. git status exposes this; git switch enforces it:

git status
# On branch feature/iam-rotation
# Changes not staged for commit:
#   modified:   terraform/main.tf
# Untracked files:
#   scripts/scratch.sh

git switch main
# error: Your local changes to the following files would be overwritten by checkout:
#   terraform/main.tf
# Please commit your changes or stash them before you switch branches.
# Aborting

The error message names exactly the path that would be clobbered. The reasoning is straightforward: switching branches is going to write the bytes that the new branch’s tree contains at that path. If your working tree has different bytes there, the switch would silently destroy your edits. Git refuses to do that and asks you to commit, stash, or discard the local changes first.

# Three options when the switch is blocked by a dirty working tree

# 1. Commit the change on the current branch first
git add terraform/main.tf
git commit -m "rotate iam keys on feature branch"

# 2. Stash the change, switch, then unstash
git stash push -m "wip on rotation"
git switch main
git stash pop

# 3. Discard the change (use only when the work is genuinely throwaway)
git restore terraform/main.tf

Switching to the previous branch

git switch - is a shortcut for “go back to wherever I was before the last switch”. The previous branch is recorded in the reflog as @{-1}, and - is the readable spelling.

git switch feature/iam-rotation
# Switched to branch 'feature/iam-rotation'

git switch main
# Switched to branch 'main'

git switch -
# Switched to branch 'feature/iam-rotation'

This is the right command for “I just peeked at main, give me back my feature branch”. It is also the safest spelling because it never names the destination explicitly — you cannot typo a branch name that you do not have to type.

Detached HEAD: switching to a commit, not a branch

git switch --detach <commit> checks out a commit directly, bypassing the branch machinery. HEAD is set to the commit’s OID, not to a branch name; the working tree and index are still updated, but no branch has moved.

git switch --detach 8a3f9d2
# HEAD is now at 8a3f9d2 rotate iam keys

The legacy spelling is git checkout 8a3f9d2. A detached HEAD is the right state for inspecting an old commit (bisecting, reviewing a tag, recovering a lost branch tip) and the wrong state for doing work — commits made on a detached HEAD are only reachable through the reflog and become orphaned when the reflog expires. The lesson git-cicd-gitops-v-04-detached-head-state covers this in depth.

Production discipline

  1. Use git switch -c for new work; use git switch - for “go back”. The two-flag spellings match the common cases and remove the temptation to type a branch name you might typo.
  2. Verify HEAD after every programmatic switch. A CI script that calls git switch should capture the exit code and re-read git rev-parse --abbrev-ref HEAD before doing anything that depends on being on the right branch.
  3. Commit or stash before switching. A working tree with uncommitted edits will block the switch. Treat the block as a reminder, not an obstacle: the uncommitted edits belong to the current branch, and the switch is asking you to make that explicit.
  4. Never git checkout a remote-tracking ref and start committing. git checkout origin/main puts you in detached HEAD at the remote’s tip. Always create a local branch first: git switch -c main origin/main (or git switch main if a local branch already exists).

Cross-course references

  • Ansible for Production Sysadmins - Part XXXVIII (Review) uses git switch -c feature/x as the standard opening of a change branch. The dirty-working-tree check is what prevents the common “committed my fix on the wrong branch” mistake.
  • GitOps with Argo CD - Part IV (AppSources) discusses branch-per-environment and uses git switch for the rare manual operations against the config repo. The discipline of “verify HEAD after every switch” applies directly.
  • Terraform for Production Sysadmins - Part XII (State) draws the analogy between a branch switch and a Terraform workspace switch: both move the user to a different configuration with a strict consistency check, and both refuse to run when the local state is dirty.

Quiz

Knowledge check · 4 questions

  1. Q1. What does `git switch -c feature/iam-rotation` actually do, in two phases?

  2. Q2. When `git switch main` fails with 'Your local changes to the following files would be overwritten', HEAD has already been moved to main and the index has been partially rewritten.

  3. Q3. What does `git switch -` do, and why is it the safest spelling for toggling between two branches?

  4. Q4. Diagnose a 'committed on the wrong branch' incident and recommend a fix.

    An engineer edits terraform/main.tf on a feature branch, runs `git switch main` to pull a recent change from upstream, sees the switch succeed, edits the same file, and commits. The commit lands on main, not on the feature branch. The engineer insists they ran `git switch feature/iam-rotation` first. The team's branch protection requires a PR for main, but the commit was direct-pushed because the engineer is a maintainer.

Passing score: 75%. Answers are checked in this browser.