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
mainto 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
monitoringfrommainis "branching offmain". The new branch starts at the latest commit ofmain. - 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
*ingit branch, or the first line ofgit status(On branch ...), before committing. - Expecting
git branch <name>to switch. It only creates the branch. Usegit switch -c <name>to create and switch. - Branching off the wrong commit. A new branch starts where you are. Run
git switch mainbeforegit switch -c <name>if the branch must start frommain. - 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:
- List the branches and identify the current one.
- Create a branch
printerwithout switching to it, then check that you are still onmain. - Switch to
printer, add the lineprinter,192.168.1.80,printtoinventory.csv, and commit with the messageAdd printer. - Go back to
mainand displayinventory.csv. - From
main, create and switch to a branchcamerasin a single command. Add the linecam01,192.168.1.90,cameraand commit with the messageAdd camera.
Expected result and verification:
git branchlistscameras(current),main, andprinter.- At step 4,
inventory.csvonmainhas three lines and no printer. git log --oneline --allshows three commits:Add camera,Add printer, andCreate inventory.- On
cameras,inventory.csvcontains the camera but not the printer: both branches started frommain.
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
cameraswas created frommain, which never received the printer commit, so the printer line is not oncameras.git commit -amstages and commits tracked files in one step; it works here becauseinventory.csvis 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:
- From
main, create and switch to a branchdocs. CreateREADME.mdcontaining# Homelab, commit it with the messageAdd README, and list the files. - Switch to
mainand list the files again. Where didREADME.mdgo? Switch back todocsand check. - Display the content of
.git/HEADondocs, then onmain. - On
main, create a filenotes.txtwithout committing it, and switch todocs. Doesnotes.txtfollow you? - Still on
docs, add the linedrafttoinventory.csvwithout committing, and try to switch tonas. Read the message. - Get out of this situation by discarding the draft, then switch to
nas.
Expected result and verification:
- Task 1 lists
README.mdandinventory.csv; task 2 lists onlyinventory.csvonmain. - Task 3 prints
ref: refs/heads/docs, thenref: refs/heads/main. - Task 4:
notes.txtis still there ondocs, andgit statuslists it as untracked. - Task 5 fails with
error: Your local changes to the following files would be overwritten by checkout:andinventory.csv. - After task 6,
git branchshows* nas, andinventory.csvcontains the NAS but nodraft.
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/HEADis 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.txtfollowed you tonasas 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.
- Create and switch to
monitoringin one command. Addmon01,192.168.1.50,monitoringto the inventory and commit with the messageAdd monitoring server. - Without going back to
main, create and switch tobackups. Addbackup01,192.168.1.60,backupand commit with the messageAdd backup server. - Display the inventory on
backups. Is that what you expected for a branch about backups only? - Display the history of all branches as a graph, and explain the mistake.
- Create a branch
backups-okthat starts frommainthis time, without switching tomainfirst. Hint:git checkout -baccepts a starting point after the branch name. - Display the graph again.
Expected result and verification:
- Task 3: the inventory on
backupsalso contains the monitoring server. - Task 4: the graph is a straight line:
Add backup serversits on top ofAdd monitoring server, becausebackupswas created frommonitoring. - After task 5,
git branchshows* backups-ok, and its inventory has only two lines. - Task 6:
backups-ok,main,monitoring, andbackupsall appear in the graph;backups-okandmainpoint toCreate 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>andgit checkout -b <branch>do the same asgit switchandgit switch -c.- A branch starts at the current commit unless you give a starting point:
git checkout -b backups-ok main(orgit switch -c backups-ok main). - The wrong
backupsbranch can be deleted, as shown on the next page.
Clean up when you are done: rm -rf ~/git-practice/checkout.