Comparing and managing branches¶
The homelab repository now has two branches: main, and monitoring with one extra commit. This page compares them, then renames and deletes branches.
Comparing branches: git diff¶
Recap of git diff¶
Comparing versions introduced the forms of git diff:
| Command | Compares |
|---|---|
git diff |
Unstaged changes, for all files |
git diff README.md |
Unstaged changes, for one file |
git diff --staged |
Staged changes and the latest commit, for all files |
git diff --staged README.md |
Same, for one file |
git diff 2553c7d dd032b5 |
Two commits, by hash |
git diff HEAD~2 HEAD |
Two commits, relative to HEAD |
Comparing two branches¶
A branch name stands for the latest commit of that branch, so it can be used anywhere a commit hash is accepted. To compare the state of main and monitoring:
$ git diff main monitoring
diff --git a/inventory.csv b/inventory.csv
index fb08d5e..b3112a3 100644
--- a/inventory.csv
+++ b/inventory.csv
@@ -2,3 +2,4 @@ hostname,ip,role
pi-dns,192.168.1.10,dns
nas,192.168.1.20,storage
web01,192.168.1.30,web
+mon01,192.168.1.50,monitoring
diff --git a/services.md b/services.md
index df0ab76..a6c5662 100644
--- a/services.md
+++ b/services.md
@@ -2,3 +2,4 @@
- pi-dns: Pi-hole
- nas: Samba shares
- web01: Nginx
+- mon01: Prometheus and Grafana
The output reads exactly like a diff between two commits:
- Version a is the first branch given (
main), version b the second (monitoring). - Each file that differs gets its own
diff --gitsection. +lines exist onmonitoringbut not onmain. Swapping the arguments (git diff monitoring main) swaps the signs.
The diff compares the files, not the history. To list the commits that are on monitoring but not on main, use git log with two dots:
$ git log --oneline main..monitoring
eeb2e47 Add monitoring server
For a quick overview, --stat summarises the changes per file:
$ git diff --stat main monitoring
inventory.csv | 1 +
services.md | 1 +
2 files changed, 2 insertions(+)
Navigating long outputs
When an output is longer than the terminal, Git shows it in a pager (usually less). Press Space to go down one page, b to go back, / to search, and q to quit and return to the prompt.
Renaming a branch: git branch -m¶
Sam started a branch with a vague name, new-server, and added a VPN server on it:
$ git switch -c new-server
Switched to a new branch 'new-server'
$ echo "vpn01,192.168.1.60,vpn" >> inventory.csv
$ git commit -am "Add VPN server"
[new-server 0caa26a] Add VPN server
1 file changed, 1 insertion(+)
Another new server is planned, so new-server is no longer a clear name. git branch -m (move) renames it:
$ git branch -m new-server vpn
$ git branch
main
monitoring
* vpn
| Command | Effect |
|---|---|
git branch -m <old> <new> |
Rename the branch <old> to <new>, from any branch |
git branch -m <new> |
Rename the current branch |
The commits do not change: only the label moves to a new name. -M forces the rename even if a branch called <new> already exists, which overwrites it.
Deleting a branch: git branch -d¶
Large projects can accumulate many branches. Once a branch has been merged, delete it to keep the list readable. The commits stay in the history of the branch they were merged into.
git branch -d deletes a branch only if it has been merged (into its upstream or into the current branch). You cannot delete the branch you are on, so switch away first:
$ git switch main
Switched to branch 'main'
$ git branch -d vpn
error: the branch 'vpn' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D vpn'
hint: Disable this message with "git config set advice.forceDeleteBranch false"
The vpn commit exists only on vpn, so deleting the branch would leave that commit unreachable: Git refuses. This safety check is the whole point of -d. An example of a successful -d is on the Merging branches page.
Force-deleting: git branch -D¶
The VPN idea is dropped. To delete a branch whether or not it has been merged, use the capital -D (shorthand for --delete --force):
$ git branch -D vpn
Deleted branch vpn (was 0caa26a).
$ git branch
* main
monitoring
Unmerged commits become hard to find
After -D, the commits of the deleted branch are no longer listed by git log or git branch. They are still in the repository for a while, so the branch can be recreated from the hash in the message: git branch vpn 0caa26a. If the message is gone, git reflog lists the recent positions of HEAD, which usually include the lost commit. Do not count on it: check that you no longer need a branch before force-deleting it.
Summary¶
| Command | Result |
|---|---|
git diff main monitoring |
Compare the latest commits of main and monitoring |
git log --oneline main..monitoring |
List the commits on monitoring that are not on main |
git branch |
List all branches |
git branch -m old_name new_name |
Rename the branch old_name to new_name |
git branch -d monitoring |
Delete monitoring, which has been merged |
git branch -D vpn |
Delete vpn, even though it has not been merged |
Common mistakes¶
- Reading a branch diff backwards.
git diff A Bshows what changes fromAtoB:+lines are inB. - Trying to delete the current branch. Git refuses with
cannot delete branch ... used by worktree. Switch to another branch first. - Reaching for
-Dby reflex. When-drefuses, it is protecting unmerged work. Check withgit log main..<branch>before forcing. - Confusing
-dand-D, or-mand-M. Lower case is the safe version, upper case forces.
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: compare, rename, delete¶
Objective: compare two branches, rename a branch, and delete branches with and without unmerged work.
Prerequisites and initial state: Git installed. The setup creates ~/git-practice/manage with main and two branches, tmp and test, each with one commit.
Setup:
mkdir -p ~/git-practice/manage && cd ~/git-practice/manage
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 switch -q -c tmp
echo "nas,192.168.1.20,storage" >> inventory.csv
git commit -q -am "Add NAS"
git switch -q -c test main
echo "lab01,192.168.1.99,test" >> inventory.csv
git commit -q -am "Add lab machine"
git switch -q main
Tasks:
- Show the differences between
mainandtmp. Which line doestmpadd? - List the commits that are on
testbut not onmain. - Rename
tmptonas, without switching to it. - Try to delete
testwith the safe option. Read the message, then delete it anyway. - Create a branch
scratchfrommain, then delete it with the safe option.
Expected result and verification:
- Task 1 shows
+nas,192.168.1.20,storage. - Task 2 shows a single commit,
Add lab machine. git branchfinally lists* mainandnasonly.- At task 4,
git branch -d testfails withthe branch 'test' is not fully merged. - At task 5,
git branch -d scratchsucceeds: a branch with no commits of its own is already fully merged.
Solution
# 1. Compare two branches (a = main, b = tmp)
git diff main tmp # +nas,192.168.1.20,storage
# 2. Commits on test that are not on main
git log --oneline main..test # Add lab machine
# 3. Rename another branch
git branch -m tmp nas
# 4. Safe delete refuses, forced delete works
git branch -d test # error: the branch 'test' is not fully merged
git branch -D test # Deleted branch test (was ...)
# 5. A branch with nothing of its own is "merged"
git branch scratch
git branch -d scratch # Deleted branch scratch (was ...)
git branch # * main, nas
git branch -m <old> <new>works from any branch;git branch -m <new>renames only the current one.scratchpoints to the same commit asmain, so everything on it is already onmain, and-daccepts.- The hash printed by
git branch -Dis your last chance to recover the branch easily:git branch test <hash>.
Clean up when you are done: rm -rf ~/git-practice/manage.
Lab 2: rename master, resurrect a branch¶
Objective: rename the current branch, then bring back a branch deleted by mistake.
Prerequisites and initial state: Git installed. The setup creates ~/git-practice/resurrect, a repository whose first branch is still called master, with a branch grafana holding one commit of its own.
Setup:
mkdir -p ~/git-practice/resurrect && cd ~/git-practice/resurrect
git init -q -b master
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 switch -q -c grafana
printf '# Services\n- mon01: Grafana\n' > services.md
git add . && git commit -q -m "Document Grafana"
git switch -q master
Tasks:
- You are on
master. Rename it tomain, and check. - Force-delete
grafana. Copy the hash printed by Git. - Change your mind: that commit was useful. Recreate
grafanaat that hash, and check thatDocument Grafanais back. - Rename
grafanatomonitoringwithout switching to it. - Delete
monitoringagain, this time with the safe option. What happens, and why?
Expected result and verification:
- Task 1:
git branchshowsgrafanaand* main;git statussaysOn branch main. - Task 2 prints
Deleted branch grafana (was 1a2b3c4)., with your own hash. - Task 3:
git log --oneline grafanashowsDocument GrafanaandCreate inventory. - Task 5 fails with
error: the branch 'monitoring' is not fully merged: its commit is not onmain. The branch is kept.
Solution
# 1. Rename the current branch
git branch -m main
git branch # grafana, * main
# 2. Force-delete: note the hash
git branch -D grafana # Deleted branch grafana (was 1a2b3c4).
# 3. A branch is only a label: put it back on the commit
git branch grafana 1a2b3c4 # use your own hash
git log --oneline grafana # Document Grafana, Create inventory
# 4. Rename another branch
git branch -m grafana monitoring
# 5. The safe option protects unmerged work
git branch -d monitoring # error: the branch 'monitoring' is not fully merged
git branch # * main, monitoring
- Deleting a branch deletes the label, not the commits. As long as you know the hash,
git branch <name> <hash>brings the branch back. - Renaming
mastertomainis exactly what you do on an older repository; the rename is local until you push. -drefuses to drop commits that exist only on that branch: it is the option to use by default.
Clean up when you are done: rm -rf ~/git-practice/resurrect.
Lab 3: spring cleaning¶
Objective: decide, for each branch of a cluttered repository, whether it can be deleted, using comparisons before any deletion.
Prerequisites and initial state: Git installed. The setup creates ~/git-practice/cleaning with six branches: one misspelled, one already included in main, two throwaway tests, and two real work branches.
Setup (copy the whole block):
mkdir -p ~/git-practice/cleaning && cd ~/git-practice/cleaning
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 old-docs
echo "nas,192.168.1.20,storage" >> inventory.csv
git commit -q -am "Add NAS"
for b in moniotring backups test-1 test-2; do git branch "$b"; done
git switch -q moniotring && echo "mon01,192.168.1.50,monitoring" >> inventory.csv && git commit -q -am "Add monitoring server"
git switch -q backups && echo "backup01,192.168.1.60,backup" >> inventory.csv && git commit -q -am "Add backup server"
git switch -q test-1 && echo "test" >> inventory.csv && git commit -q -am "Test 1"
git switch -q test-2 && echo "test again" >> inventory.csv && git commit -q -am "Test 2"
git switch -q main
Tasks:
- List the branches. For each one, list the commits it has that
maindoes not have. - Delete
old-docswith the safe option. Why does Git accept? - Check what
test-1andtest-2changed compared tomain, then delete both in a single command. - Fix the typo:
moniotringshould bemonitoring. - Check the final list of branches.
Expected result and verification:
- Task 1:
old-docshas no commit of its own;moniotring,backups,test-1, andtest-2have one each. - Task 2 prints
Deleted branch old-docs (was ...): its only commit is already part ofmain. - Task 3: the diffs only add
testortest again;git branch -drefuses them,git branch -Ddeletes both. - Task 5:
git branchshowsbackups,* main, andmonitoring.
Solution
# 1. What would be lost?
git branch
for b in old-docs moniotring backups test-1 test-2; do echo "== $b"; git log --oneline main..$b; done
# 2. Nothing of its own: the safe delete works
git branch -d old-docs
# 3. Inspect, then force-delete both
git diff main test-1 # +test
git diff main test-2 # +test again
git branch -d test-1 test-2 # error: ... not fully merged (twice)
git branch -D test-1 test-2
# 4. Fix the name
git branch -m moniotring monitoring
# 5. Result
git branch # backups, * main, monitoring
git log --oneline main..<branch>answers the only question that matters before deleting: "which commits exist only on this branch?". An empty answer means-dis safe.old-docswas created beforeAdd NAS, so it points to an older commit ofmain: it is behind, not diverged, and fully merged.- Like
git branch -d,git branch -Daccepts several names at once.
Clean up when you are done: rm -rf ~/git-practice/cleaning.