Aller au contenu

Working with branches

So far, every commit of the homelab repository followed the previous one in a single line. Branches let the history split into several lines that evolve independently, and come back together later.

What is a branch?

A branch is an individual line of development in a repository: a version of the project that can evolve on its own. Git uses branches to track several versions of the files at the same time. Between two branches:

  • some files are identical,
  • some files are different,
  • some files exist in one branch only.

Technically, a branch is just a movable label that points to a commit. Each new commit made on a branch moves its label forward. Creating a branch is therefore instant and cheap: Git writes a small file containing a commit hash, it does not copy the project (Pro Git, Branches in a Nutshell).

Why use branches?

The default branch, usually called main, holds the version that works: the live system, the "ground truth" of the project. New work happens on separate branches, so it can be developed and tested without breaking main.

Branches make it possible to:

  • work in parallel: several people, or several tasks, each on their own branch;
  • compare the state of the repository between two branches;
  • combine the work: merge a finished branch back into main to publish the new feature.

Each branch should have one specific purpose: a new feature, a bug fix, an experiment. When the purpose is reached, the branch is merged and deleted.

Visualizing branches

In the homelab repository, Sam wants to add a monitoring server without touching the working inventory. The work happens on a branch monitoring, which is then merged back into main. Later, a bug is found on main, and fixed on its own short-lived branch:

%%{init: {"gitGraph": {"showBranches": true, "rotateCommitLabel": false, "mainBranchName": "main"}, "themeVariables": {"git0": "#43a047", "git1": "#1e88e5", "git2": "#fb8c00", "gitBranchLabel0": "#ffffff", "gitBranchLabel1": "#ffffff", "gitBranchLabel2": "#ffffff", "gitInv0": "#ffffff", "commitLabelFontSize": "13px"}}}%%
gitGraph TB:
    commit id: "Create homelab inventory"
    branch monitoring
    commit id: "Add monitoring server"
    commit id: "Add Grafana dashboards"
    checkout main
    merge monitoring id: "Merge monitoring"
    branch fix-ip
    commit id: "Fix web01 IP address"
    checkout main
    merge fix-ip id: "Merge fix-ip"
  • Branching off: creating a branch from another one. Creating monitoring from main is "branching off main". The new branch starts at the latest commit of main.
  • Merging: bringing the commits of one branch into another (see Merging branches).

Listing branches: git branch

git branch lists the local branches. The current branch is marked with an asterisk *:

$ git branch
* main

A brand new repository has only one branch. Its name comes from git init -b main or from the init.defaultBranch setting (Creating repositories).

Creating a branch: git branch <name>

Give git branch a name to create a branch at the current commit:

$ git branch monitoring
$ git branch
* main
  monitoring

The branch exists, but you are still on main: git branch <name> creates the branch without switching to it.

Switching branches: git switch

git switch moves to another branch:

$ git switch monitoring
Switched to branch 'monitoring'
$ git branch
  main
* monitoring

Commits are now recorded on monitoring. Sam adds the monitoring server to the inventory and the services list:

$ echo "mon01,192.168.1.50,monitoring" >> inventory.csv
$ echo "- mon01: Prometheus and Grafana" >> services.md
$ git commit -am "Add monitoring server"
[monitoring eeb2e47] Add monitoring server
 2 files changed, 2 insertions(+)

The commit output starts with the branch name: [monitoring eeb2e47].

Switching changes your files

Switching branches replaces the files in the working directory with their version on the target branch. Back on main, the monitoring server is not in the inventory, because it was only committed on monitoring:

$ git switch main
Switched to branch 'main'
$ cat inventory.csv
hostname,ip,role
pi-dns,192.168.1.10,dns
nas,192.168.1.20,storage
web01,192.168.1.30,web

Nothing is lost: git switch monitoring brings the line back. Git keeps both versions in the repository, and only shows one at a time in the working directory.

Uncommitted changes follow you

Edits that are not committed belong to the working directory, not to a branch. If they do not conflict with the target branch, git switch keeps them and they appear on the new branch too. If the switch would overwrite them, Git refuses:

error: Your local changes to the following files would be overwritten by checkout:
    inventory.csv
Please commit your changes or stash them before you switch branches.
Aborting

Commit your work before switching, so that each change stays on the branch it belongs to.

HEAD: where you are

HEAD is the reference to the current branch, which itself points to its latest commit. git switch moves HEAD from one branch to another; git commit moves the current branch, and HEAD with it. This is why HEAD~1 always means "the commit before the latest one on the current branch".

flowchart LR
    head(["HEAD"]) --> mon["monitoring"]
    mon --> c2["eeb2e47<br/>Add monitoring server"]
    main["main"] --> c1["112c587<br/>Create homelab inventory"]
    c2 --> c1

Creating and switching at once: git switch -c

Creating a branch and switching to it is so common that git switch does both with -c (create):

$ git switch -c backups
Switched to a new branch 'backups'

This is equivalent to git branch backups followed by git switch backups. The new branch starts from the current commit, so switch to main first if the branch should start from main.

The older syntax: git checkout

git switch was added in Git 2.23 (2019) to take over the branch-related half of git checkout, which also restores files. You will still meet the older form in documentation and scripts:

Modern command Older equivalent
git switch monitoring git checkout monitoring
git switch -c backups git checkout -b backups

Both forms work and do the same thing (git switch documentation).

Summary

Command Result
git branch List all local branches; * marks the current one
git branch monitoring Create the branch monitoring at the current commit, without switching
git switch monitoring Switch to the existing branch monitoring
git switch -c backups Create the branch backups and switch to it
git checkout -b backups Older equivalent of git switch -c backups

Common mistakes

  • Committing on the wrong branch. Check the * in git branch, or the first line of git status (On branch ...), before committing.
  • Expecting git branch <name> to switch. It only creates the branch. Use git switch -c <name> to create and switch.
  • Branching off the wrong commit. A new branch starts where you are. Run git switch main before git switch -c <name> if the branch must start from main.
  • Thinking work disappeared after a switch. Files committed on another branch are hidden, not deleted. Switch back to see them.

Hands-on labs

Three labs, from guided to more autonomous. Each one has its own setup, so you can do them in any order and repeat them as often as you like. Type the commands rather than pasting them, and read every output before moving on: the goal is to build reflexes, not to reach the end.

Lab 1: develop on a branch

Objective: create branches, commit on them, and observe how switching changes the working directory.

Prerequisites and initial state: Git 2.23 or later. The setup creates ~/git-practice/branches with one commit on main.

Setup:

mkdir -p ~/git-practice/branches && cd ~/git-practice/branches
git init -q -b main
git config user.name "Practice User"
git config user.email "[email protected]"
printf 'hostname,ip,role\npi-dns,192.168.1.10,dns\nnas,192.168.1.20,storage\n' > inventory.csv
git add . && git commit -q -m "Create inventory"

Tasks:

  1. List the branches and identify the current one.
  2. Create a branch printer without switching to it, then check that you are still on main.
  3. Switch to printer, add the line printer,192.168.1.80,print to inventory.csv, and commit with the message Add printer.
  4. Go back to main and display inventory.csv.
  5. From main, create and switch to a branch cameras in a single command. Add the line cam01,192.168.1.90,camera and commit with the message Add camera.

Expected result and verification:

  • git branch lists cameras (current), main, and printer.
  • At step 4, inventory.csv on main has three lines and no printer.
  • git log --oneline --all shows three commits: Add camera, Add printer, and Create inventory.
  • On cameras, inventory.csv contains the camera but not the printer: both branches started from main.
Solution
# 1. List branches
git branch                     # * main

# 2. Create without switching
git branch printer
git branch                     # * main, printer: still on main

# 3. Commit on the new branch
git switch printer
echo "printer,192.168.1.80,print" >> inventory.csv
git commit -am "Add printer"

# 4. Back to main: the printer line is not there
git switch main
cat inventory.csv              # 3 lines, no printer

# 5. Create and switch at once
git switch -c cameras          # older syntax: git checkout -b cameras
echo "cam01,192.168.1.90,camera" >> inventory.csv
git commit -am "Add camera"

git branch                     # * cameras, main, printer
git log --oneline --all        # Add camera, Add printer, Create inventory
cat inventory.csv              # camera, but no printer
  • cameras was created from main, which never received the printer commit, so the printer line is not on cameras.
  • git commit -am stages and commits tracked files in one step; it works here because inventory.csv is already tracked.

Clean up when you are done: rm -rf ~/git-practice/branches.

Lab 2: what switching does to your files

Objective: watch files appear and disappear when switching branches, follow HEAD, and see when Git refuses to switch.

Prerequisites and initial state: Git 2.23 or later. The setup creates ~/git-practice/switching with a commit on main, and a branch nas where the inventory has one more line.

Setup:

mkdir -p ~/git-practice/switching && cd ~/git-practice/switching
git init -q -b main
git config user.name "Practice User"
git config user.email "[email protected]"
printf 'hostname,ip,role\npi-dns,192.168.1.10,dns\n' > inventory.csv
git add . && git commit -q -m "Create inventory"
git branch nas
git switch -q nas
echo "nas,192.168.1.20,storage" >> inventory.csv
git commit -q -am "Add NAS"
git switch -q main

Tasks:

  1. From main, create and switch to a branch docs. Create README.md containing # Homelab, commit it with the message Add README, and list the files.
  2. Switch to main and list the files again. Where did README.md go? Switch back to docs and check.
  3. Display the content of .git/HEAD on docs, then on main.
  4. On main, create a file notes.txt without committing it, and switch to docs. Does notes.txt follow you?
  5. Still on docs, add the line draft to inventory.csv without committing, and try to switch to nas. Read the message.
  6. Get out of this situation by discarding the draft, then switch to nas.

Expected result and verification:

  • Task 1 lists README.md and inventory.csv; task 2 lists only inventory.csv on main.
  • Task 3 prints ref: refs/heads/docs, then ref: refs/heads/main.
  • Task 4: notes.txt is still there on docs, and git status lists it as untracked.
  • Task 5 fails with error: Your local changes to the following files would be overwritten by checkout: and inventory.csv.
  • After task 6, git branch shows * nas, and inventory.csv contains the NAS but no draft.
Solution
# 1. A file that only exists on docs
git switch -c docs
echo "# Homelab" > README.md
git add README.md
git commit -m "Add README"
ls                              # README.md  inventory.csv

# 2. Switching replaces the files
git switch main
ls                              # inventory.csv
git switch docs
ls                              # README.md is back

# 3. HEAD points to the current branch
cat .git/HEAD                   # ref: refs/heads/docs
git switch main
cat .git/HEAD                   # ref: refs/heads/main

# 4. Untracked files belong to no branch
echo "draft" > notes.txt
git switch docs
ls                              # notes.txt is still there
git status                      # notes.txt untracked

# 5. A change that the switch would overwrite
echo "draft" >> inventory.csv
git switch nas                  # error: Your local changes ... would be overwritten by checkout

# 6. Discard, then switch
git checkout -- inventory.csv
git switch nas
cat inventory.csv               # NAS, no draft
  • .git/HEAD is a small text file: switching branches rewrites it.
  • Uncommitted files and changes live in the working directory, not on a branch. Git carries them along when it can, and refuses when the target branch has a different version of the same file.
  • notes.txt followed you to nas as well: delete it or commit it on the branch where it belongs.

Clean up when you are done: rm -rf ~/git-practice/switching.

Lab 3: same moves, older syntax

Objective: repeat the branch workflow with git checkout, branch off the wrong commit on purpose, and read the result in a graph.

Prerequisites and initial state: Git installed. The setup creates ~/git-practice/checkout with one commit on main.

Setup:

mkdir -p ~/git-practice/checkout && cd ~/git-practice/checkout
git init -q -b main
git config user.name "Practice User"
git config user.email "[email protected]"
printf 'hostname,ip,role\npi-dns,192.168.1.10,dns\n' > inventory.csv
git add . && git commit -q -m "Create inventory"

Tasks: use only git checkout and git branch to create and switch branches in this lab.

  1. Create and switch to monitoring in one command. Add mon01,192.168.1.50,monitoring to the inventory and commit with the message Add monitoring server.
  2. Without going back to main, create and switch to backups. Add backup01,192.168.1.60,backup and commit with the message Add backup server.
  3. Display the inventory on backups. Is that what you expected for a branch about backups only?
  4. Display the history of all branches as a graph, and explain the mistake.
  5. Create a branch backups-ok that starts from main this time, without switching to main first. Hint: git checkout -b accepts a starting point after the branch name.
  6. Display the graph again.

Expected result and verification:

  • Task 3: the inventory on backups also contains the monitoring server.
  • Task 4: the graph is a straight line: Add backup server sits on top of Add monitoring server, because backups was created from monitoring.
  • After task 5, git branch shows * backups-ok, and its inventory has only two lines.
  • Task 6: backups-ok, main, monitoring, and backups all appear in the graph; backups-ok and main point to Create inventory.
Solution
# 1. Older syntax for "git switch -c"
git checkout -b monitoring
echo "mon01,192.168.1.50,monitoring" >> inventory.csv
git commit -am "Add monitoring server"

# 2. The new branch starts where you are: on monitoring
git checkout -b backups
echo "backup01,192.168.1.60,backup" >> inventory.csv
git commit -am "Add backup server"

# 3. Surprise
cat inventory.csv                 # monitoring AND backup servers

# 4. The graph shows the chain
git log --oneline --graph --all
# * 1c2d3e4 (HEAD -> backups) Add backup server
# * 5f6a7b8 (monitoring) Add monitoring server
# * 9c0d1e2 (main) Create inventory

# 5. Name a starting point
git checkout -b backups-ok main
cat inventory.csv                 # only the header and pi-dns

# 6. Check
git log --oneline --graph --all
  • git checkout <branch> and git checkout -b <branch> do the same as git switch and git switch -c.
  • A branch starts at the current commit unless you give a starting point: git checkout -b backups-ok main (or git switch -c backups-ok main).
  • The wrong backups branch can be deleted, as shown on the next page.

Clean up when you are done: rm -rf ~/git-practice/checkout.