Branches are cheap, and that is their virtue. It is also their problem: when creating one costs nothing, people create a lot of them, and after a few months Ana's repository holds branches for features that are already integrated, branches from experiments nobody remembers, branches with names like test or fix2 and one that has not seen a commit in six weeks.
A repository with forty branches of which only three are alive is not a technical problem — Git handles thousands without breaking a sweat — but it is a human one: nobody knows which is which, nobody dares delete anything for fear of losing work, and autocompletion stops being useful.
This lesson is the toolkit for keeping that under control: listing branches with the information you actually need, knowing which ones can be deleted without losing anything, renaming, deleting safely and giving them names that mean something. It is the least spectacular lesson of the module and probably the one you will apply most often.
Contents
- Listing branches:
git branchand its variants --mergedand--no-merged: what can be deleted without losing anything- Custom listings with
--sortand--format - Renaming branches with
-m - Deleting branches:
-dversus-D - When the safe delete fails
- Naming conventions
- Which names Git accepts:
git check-ref-format - Hygiene: spotting stale branches
- Wrapping up the module
- Listing branches:
git branch and its variants
git branch and its variantsYou already know the base command:
cleanup/remove-notes docs/update-notes experiment/indexeddb-storage feature/alphabetical-order feature/csv-export feature/pending-filter feature/task-counter fix/empty-list-message * main test
Ten branches. The asterisk marks the current one. Sorted alphabetically, with no other information. It is a poor starting point: you cannot tell which ones are integrated, nor which have been idle for months.
-v: what sits at each tip
cleanup/remove-notes 7f3c9a2 Remove the internal notes, now obsolete docs/update-notes 3f7a9c1 Update the internal notes with the new flow experiment/indexeddb-storage 5e8b1d4 Store tasks in IndexedDB feature/alphabetical-order a1e5c93 Show tasks in alphabetical order feature/csv-export e9a2c5f Now it works feature/pending-filter b2e6d3f Restore field focus after adding a task feature/task-counter 9d1e4b7 Mark tasks as done on click fix/empty-list-message 6d3f8b2 Show a message when the list is empty * main c2a8f1e Merge the empty-list message test 8b6d3c2 Add base styles for the list
Now you can start reasoning. Look at test: it points at 8b6d3c2, an ancient commit from the very beginning of the project. It is a dead branch that was created and never used.
There is a doubled-up variant:
It adds, in square brackets, each branch's tracking branch: which branch of the remote repository it is paired with and how many commits ahead or behind it is. Since everything in this module happens in a single local repository, there is nothing to show yet; -vv will come into its own in module 4, once work starts travelling between machines.
Other listing options
# Only branches that contain a given commit
git branch --contains a1e5c93
# Only branches that do NOT contain it
git branch --no-contains a1e5c93
# Filter by pattern (wildcards allowed)
git branch --list 'feature/*'
# Include remote branches too (module 4)
git branch -a
# Only the remote ones
git branch -r--contains is especially useful for answering "which branches is this fix in?":
The empty-list message fix is on its original branch and on main. Nowhere else.
--merged and --no-merged: what can be deleted without losing anything
--merged and --no-merged: what can be deleted without losing anythingThese two options are the heart of branch hygiene.
cleanup/remove-notes experiment/indexeddb-storage feature/alphabetical-order feature/pending-filter feature/task-counter fix/empty-list-message * main test
The exact meaning, and it is worth being precise because it causes confusion: --merged lists the branches whose tip is reachable from the current branch. Put another way: all of their commits are already in main's history.
In practical terms: these branches can be deleted without losing a single commit.
Two cases deserve a comment:
testshows up here because its tip is8b6d3c2, a commit from the beginning of the project that is obviously inmain. It contributed nothing, which is why it counts as merged.experiment/indexeddb-storageshows up too, even though its code never reached the project: we closed it in lesson 03-04 withgit merge -s ours, which creates the merge commit without bringing in the content. That was precisely the point of that strategy: to stop the branch showing up as outstanding.
And now the other side:
These branches have commits that main does not have. Deleting them would leave those commits with no reference reaching them. Why each one is here:
docs/update-notes: we resolved itsmodify/deleteconflict in favour of deleting the file, so its commit never made it intomain. Quite right that it appears here.feature/csv-export: we integrated it with squash. Its content is inmain, but its six original commits are not. Git is telling the truth: from the graph's point of view, it is not merged.
That last case is the most common trap: a branch integrated with squash always appears under --no-merged. If your team uses squash systematically, --merged stops being a reliable criterion for deleting and you have to keep track some other way.
Comparing against another branch
By default, both options compare against HEAD. You can name a different reference:
# Branches merged into main, even if I am not on main
git branch --merged main
# Branches whose work is already in version 1.2
git branch --merged v1.2.0
# Branches not yet integrated into main
git branch --no-merged mainThat is the correct way to use them in scripts, because it does not depend on where you happen to be standing.
The cleanup pattern
This is the idiom you will see everywhere, and it is worth understanding piece by piece:
git branch --merged main: the candidates.grep -v '^\*': drops the current branch's line (the one with the asterisk).grep -v ' main$': dropsmainfrom the list, just in case it tries to delete itself.xargs -r git branch -d: deletes each one with the safe delete (-rstops anything running if the list comes back empty).
Always review the list before running it, by chopping off the last stage:
One important piece of advice: use -d and never -D in an automated command like this. -d refuses if something does not add up; -D deletes without asking.
- Custom listings with
--sort and --format
--sort and --formatAlphabetical order is not the most useful one. What you almost always want to know is which branches are alive, and that is a question about dates.
* main fix/empty-list-message feature/alphabetical-order docs/update-notes cleanup/remove-notes feature/csv-export feature/pending-filter feature/task-counter experiment/indexeddb-storage test
The dash in front of committerdate means descending order: the most recent at the top. The ones at the bottom are the candidates for disappearing.
Common sort fields:
| Field | Sorts by |
|---|---|
committerdate |
Date of the branch's last commit |
authordate |
Author date of the last commit |
refname |
Name (the default order) |
-committerdate |
Date, most recent to oldest |
And --format builds a listing to your own taste, using the same placeholders as git for-each-ref:
git branch --sort=-committerdate \
--format='%(HEAD) %(color:yellow)%(refname:short)%(color:reset) | %(committerdate:relative) | %(authorname) | %(contents:subject)'* main | 2 hours ago | Ana Ferrer | Merge the empty-list message fix/empty-list-message | 3 hours ago | Bruno Salas | Show a message when the list is empty feature/alphabetical-order | 5 hours ago | Ana Ferrer | Show tasks in alphabetical order docs/update-notes | 2 days ago | Bruno Salas | Update the internal notes with the new flow cleanup/remove-notes | 2 days ago | Ana Ferrer | Remove the internal notes, now obsolete feature/csv-export | 6 days ago | Bruno Salas | Now it works feature/pending-filter | 3 weeks ago | Bruno Salas | Restore field focus after adding a task feature/task-counter | 3 weeks ago | Ana Ferrer | Mark tasks as done on click experiment/indexeddb-storage | 2 months ago | Ana Ferrer | Store tasks in IndexedDB test | 4 months ago | Ana Ferrer | Add base styles for the list
Now that is a useful view. At a glance you can see what is alive, who is driving it and what was last done.
The most practical placeholders:
| Placeholder | Content |
|---|---|
%(HEAD) |
A * if it is the current branch, a space if not |
%(refname:short) |
The branch name without refs/heads/ |
%(committerdate:relative) |
"3 weeks ago" |
%(committerdate:short) |
2026-07-10 |
%(authorname) |
Author of the last commit |
%(contents:subject) |
First line of the message |
%(objectname:short) |
Abbreviated hash |
%(color:...) / %(color:reset) |
Colour |
Since that line is impossible to remember, you save it as an alias:
git config --global alias.branches "branch --sort=-committerdate --format='%(HEAD) %(color:yellow)%(refname:short)%(color:reset) | %(committerdate:relative) | %(authorname) | %(contents:subject)'"Aliases get their own lesson in module 6; this one is a strong candidate for your collection.
- Renaming branches with
-m
-mA few days ago Bruno opened a branch called fix2 and now nobody knows what is in it. After looking through it, Ana gives it a name that says something:
If you want to rename the branch you are on, one argument is enough:
There is an uppercase variant, -M, which forces the rename even if a branch with the target name already exists, overwriting it. The usual warning applies: -M can orphan commits without warning you. Use -m unless you know exactly what you are doing.
What it actually does
Nothing surprising, this far into the module:
Renaming a branch means renaming the 41-byte file and, if it was the current branch, updating .git/HEAD to point at the new name. The commits are untouched: they are immutable and completely unaware.
That is why renaming is a perfectly safe operation locally. Once the branch has been shared with the team the picture changes, because everyone else still sees the old name; that belongs to module 4.
- Deleting branches:
-d versus -D
-d versus -DThe safe one, on an already integrated branch:
Notice that Git gives you the hash of where it was. That is not politeness: it is the piece of information that lets you recreate the branch if you got it wrong (git branch <name> 9d1e4b7). Copy it if you have the slightest doubt.
You can delete several at once:
Deleted branch feature/pending-filter (was b2e6d3f). Deleted branch feature/alphabetical-order (was a1e5c93). Deleted branch cleanup/remove-notes (was 7f3c9a2).
What exactly gets deleted
The 41-byte file. Nothing else.
The commits remain intact in .git/objects/. The only thing that has disappeared is the name that pointed at them. If those commits are still reachable from main (which is the case when the branch was merged), absolutely nothing changes in the repository.
This is why deleting a merged branch is a trivial, risk-free operation, and why it deserves no reverence whatsoever.
Two limitations
You cannot delete the branch you are on:
And, since Git 2.5, you cannot delete a branch that is checked out in another working copy either (git worktree, lesson 06-06). In both cases the fix is the same: git switch to another branch and try again.
- When the safe delete fails
error: The branch 'feature/export-to-csv' is not fully merged. If you are sure you want to delete it, run 'git branch -D feature/export-to-csv'.
What that message means exactly: that branch's tip is not reachable from the current branch (nor from its tracking branch, if it had one). In other words, it holds commits that exist nowhere else in the graph.
And what it does NOT mean: it does not mean the work is lost, nor that the content is missing from main. Remember the squash case: the content of feature/export-to-csv is entirely in main, in commit 3b9e7d1, but its six original commits are not. Git reasons about the graph, not about the content.
Check before you force. This is the reflex to acquire:
e9a2c5f Now it works 7e2a9c4 Remove the console.log 1d6b4f8 wip 2 5a8e2b9 Fix the separator 9f3a2c1 wip 4a1c7e3 First attempt at exporting
No differences: the content is identical. It went in via squash and nothing is lost. Now, with full knowledge of the facts:
-D is recoverable (for a while)
A -D delete is not the end of the world, and it is worth knowing that so you can stop being afraid of it. The commits are still in the object database, and Git keeps a log called the reflog with every movement of every reference. As long as the hash can be recovered — which is why git branch -D prints it as it deletes — recreating the branch is all it takes:
Commits with no reference at all are eventually removed by garbage collection (git gc), but not straight away: the default window is 30 days for unreachable objects and 90 days for reflog entries. There is plenty of margin.
The whole rescue procedure — finding the hash with git reflog and git fsck --lost-found when you no longer have it written down — is covered in lesson 09-04. Take away the reassuring idea: -D is almost never irreversible.
-d |
-D |
|
|---|---|---|
| Checks whether it is merged | Yes | No |
| Fails if there are unintegrated commits | Yes | No |
| Prints the hash on deletion | Yes | Yes |
| Recoverable afterwards | Yes (nothing is lost) | Yes, via the reflog, for weeks |
| When to use it | Always, by default | Once you have checked and know what you are doing |
- Naming conventions
Git imposes no structure on branch names beyond a handful of forbidden characters. What teams do is adopt slash-separated prefixes that group branches by purpose, exactly as we have been doing throughout the module.
| Prefix | What for | Example |
|---|---|---|
feature/ (or feat/) |
New functionality | feature/csv-export |
fix/ (or bugfix/) |
Fixing a defect | fix/empty-list-message |
hotfix/ |
Urgent fix on the version in production | hotfix/save-error |
experiment/ (or spike/) |
A trial that may end up dropped | experiment/indexeddb-storage |
docs/ (or doc/) |
Documentation only | docs/installation-guide |
refactor/ |
Reorganisation with no functional change | refactor/split-tasks-module |
chore/ (or cleanup/) |
Maintenance, dependencies, configuration | cleanup/remove-notes |
release/ |
Preparing a version | release/1.3.0 |
Many teams add the issue tracker's ticket identifier, which lets you go from the branch to the specification and back:
And some include the name of whoever is driving it, which helps in large teams:
The practical advantage of the slash
It is not just cosmetic. The slash lets you filter:
And it makes autocompletion usable: you type git switch fea<TAB> and the shell offers you only that group.
It is also worth knowing why the slash has a limit: since names become paths inside .git/refs/heads/, a branch called feature and another called feature/order cannot both exist. The first would be a file and the second would require feature to be a directory. Git rejects it:
fatal: cannot lock ref 'refs/heads/feature': 'refs/heads/feature/alphabetical-order' exists; cannot create 'refs/heads/feature'
A cryptic error with a very simple explanation, and now you know it.
Style rules that help
- Lower case and hyphens:
feature/csv-export, notFeature/CSVExport. It avoids surprises between case-sensitive and case-insensitive file systems (remember that Ana is on Ubuntu, Bruno on macOS and Carla on Windows). - Plain ASCII, no accents.
fix/cafe-list, notfix/café-list. Git accepts them, but they end up in URLs, in file names and in third-party tool output, where they cause trouble. - Descriptive but short:
fix/focus-after-adding, notfix/fix-the-focus-problem-the-client-reported-on-tuesday. - No
test,temp,new,fix2. In two weeks' time you will not know what they were, and that is why nobody will dare delete them.
The named workflows
There are whole methodologies that define which branches exist, what they are called, where they come from and where they get integrated. The best known:
- Git Flow:
develop,release/*,hotfix/*andfeature/*branches over amainthat only ever holds released versions. - GitHub Flow: a single long-lived branch (
main) and short-lived working branches integrated through change proposals. - Trunk Based Development: everybody integrates into the trunk daily, with branches measured in hours rather than days.
Each has its own assumptions about team size, release frequency and level of automation. We will study them in module 7, where we will already have remotes and change proposals to understand them properly. For now, the point that matters: the specific convention matters far less than the whole team following the same one.
- Which names Git accepts:
git check-ref-format
git check-ref-formatThe rules are formally defined and you can check against them:
If the name is valid, it prints it and returns 0. If not:
The main rules, with the reason behind each:
| Forbidden | Invalid example | Why |
|---|---|---|
| Spaces and control characters | my branch |
They break command-line use |
Two consecutive dots .. |
branch..old |
.. is the range operator |
The characters ~ ^ : ? * [ \ |
branch^2, branch:x |
They are reference syntax or wildcards |
Starting or ending in /, or // |
/branch, branch//x |
They are not valid paths |
Ending in . or in .lock |
branch., branch.lock |
.lock is Git's locking mechanism |
The component @{ |
branch@{1} |
It is the reflog syntax |
A lone @ |
@ |
It is an alias for HEAD |
Starting a component with . |
.hidden |
It would be a hidden file |
A curious detail, consistent with everything you have learnt: most of these rules exist because a branch name is simultaneously a file path and an expression Git has to be able to parse. Forbidding ^ is not a whim: if a branch called my^branch existed, git log my^branch would be ambiguous.
Checking it in a script:
NAME="feature/new-thing"
if git check-ref-format --branch "$NAME" > /dev/null 2>&1; then
git switch -c "$NAME"
else
echo "Invalid branch name: $NAME"
fiThis kind of validation is exactly what gets automated with a hook, to stop names that do not follow the team's convention getting in. Hooks are the subject of lesson 06-01.
- Hygiene: spotting stale branches
Let us pull it all together into a maintenance routine. Doing it once a month is enough.
Step 1: see the picture sorted by age.
2026-03-12 test Ana Ferrer 2026-05-28 experiment/indexeddb-storage Ana Ferrer 2026-07-10 feature/task-counter Ana Ferrer 2026-07-12 feature/pending-filter Bruno Salas ...
Without the dash the order is ascending: oldest at the top, which is what you want when you are hunting for deletion candidates.
Step 2: separate the integrated ones from the rest.
echo "=== Merged into main (safe to delete) ==="
git branch --merged main | grep -v ' main$'
echo "=== Not merged (check first) ==="
git branch --no-merged mainStep 3: go through the unmerged ones one by one. For each of them:
BRANCH=experiment/indexeddb-storage
# Which commits does it have that main does not?
git log --oneline main..$BRANCH
# How long since the last one?
git log -1 --format='%ar by %an' $BRANCH
# Is its content in main some other way (squash)?
git diff main..$BRANCH --statWith those three pieces of information the decision is easy: if the content is already in main, use -D without fear; if it holds real, recent work, leave it; if it holds real work from months ago, talk to whoever opened it before touching anything.
Step 4: delete the integrated ones.
Signs of a stale branch
| Sign | What it usually means |
|---|---|
Merged into main |
It did its job; delete |
| No commits for over a month | Abandoned work; ask |
Generic name (test, temp, new) |
Nobody remembers what it was for |
Its content is in main but it is not merged |
It went in via squash; delete with -D |
Hundreds of commits behind main |
Merging it will be hell; rethink |
And a deeper thought
The best branch management is not needing any: if branches live two or three days and get deleted on integration, the repository never accumulates rubbish. The monthly clean-up is a patch for a problem you avoid by working with short branches.
It is the same conclusion we reached about conflicts in the previous lesson, and that is no coincidence: short-lived branches solve divergence, conflicts and clutter all at once. It is the idea that underpins trunk-based development and a good part of module 7.
- Wrapping up the module
Let us recap what we have built across these six lessons.
We started from a promise made in module 2 — "a branch is a file with a hash inside" — and kept it right down to the byte: 41 of them, exactly. We saw that HEAD is another file that points at a branch, not at a commit, and that this indirection is what lets committing move the right pointer. We understood why Git's branches are free compared with Subversion's, and what it means for two branches to diverge from a common ancestor.
Then we created them and moved between them with git branch, git switch and git switch -c, understood why Git split checkout into two commands, what happens to uncommitted changes when you switch branch, and what detached HEAD is.
We put them back together with git merge, distinguishing the fast-forward — a pointer moving along — from the three-way merge that creates a commit with two parents, and we learnt to force or forbid each behaviour with --no-ff and --ff-only. We studied the strategies (ort, resolve, octopus, ours, subtree), the -X options, the critical difference between -s ours and -X ours, and the squash merge.
We resolved a real conflict: the markers, the zdiff3 style that shows the ancestor, the three index stages, git add as the way of saying "resolved" and git merge --abort as the escape key.
And we have finished by tidying up: listing, filtering, renaming, deleting with judgement and naming well.
The problem still open
And yet everything we have done in this module has happened inside a single laptop. Every branch lives in Ana's .git/. When we said "Bruno opened a branch", we were really reasoning as if his work were already there.
In the real world it is not. Ana works on Ubuntu, Bruno on macOS, and their repositories are two completely independent object databases that know nothing about each other. Bruno's commits are on his MacBook and Ana's on her Ubuntu machine, and no git merge in the world can merge something that is not in your own .git/objects/.
Some very concrete questions are still unanswered:
- How do objects get from one repository to another?
- Where does the "official" copy of the project live?
- What exactly is that
origin/mainthat flashed past in lesson 02-06, and why does its name contain a slash? - How does Bruno authenticate in order to send his work?
- What happens if two people push changes to the same branch at once?
- And above all: Carla still has not joined. She is waiting on her Windows 11 machine for someone to tell her where to download the project from.
Module 4: Working with Remote Repositories closes that circle. We will see what a remote is and why it is nothing more than a short name for a URL, how to register one with git remote add, how authentication works with SSH and with tokens, the crucial difference between git fetch and git pull, how to send work with git push, and what the tracking branches are that make git status tell you "your branch is 3 commits ahead of origin/main".
And the good news is that everything you have learnt in this module stays true, unchanged. Merging someone else's work is exactly the same git merge you have used here. Conflicts are resolved the same way. The strategies are the same. All that gets added is transport: how objects manage to travel from one .git/ to another. Once they have arrived, everything works the way you already know.
Common Mistakes and Tips
Mistake 1: reading --no-merged as "there is lost work here". It only means that the branch's tip is not reachable from where you are standing. A branch integrated with squash will always appear there even though its content is entirely in main. Check with git diff main..<branch> before drawing conclusions.
Mistake 2: reaching for -D out of habit because -d "keeps nagging". That nagging is the safety net. Every time -d fails it is telling you something worth thirty seconds of checking. Save -D for after you have looked.
Mistake 3: not noting down the hash the delete prints. Deleted branch X (was 9d1e4b7) is your return ticket. If you realise a minute later that you got it wrong, git branch X 9d1e4b7 fixes it. Without the hash, you have to go to the reflog.
Mistake 4: creating a branch named after an existing branch directory. If you have feature/alphabetical-order, you cannot create feature. The error Git gives is cryptic, but the cause is that names are file paths.
Mistake 5: hoarding branches "just in case". The cost is not the space (they are 41 bytes) but the confusion: nobody knows which are alive, autocompletion becomes useless and the listings stop being readable. If the branch is merged, delete it; the commits are not going anywhere.
Tip 1: save the pretty listing as an alias. git branches with a relative date, an author and the last message is the view you will genuinely use. The full line is in section 3.
Tip 2: do the monthly clean-up, and do it in two steps. List first, review with your own eyes, then delete. Never in a single chained command without looking.
Tip 3: name branches with the person who will see them in a month in mind. Include the type prefix and, if the team uses an issue tracker, its identifier. It is the difference between a branch list that makes sense and one nobody dares touch.
Tip 4: delete the branch as soon as you integrate it. Making that the last step of the merge ritual — merge, check, delete — means the monthly clean-up never becomes necessary at all.
Exercises
Exercise 1: auditing a repository
In a repository with several branches (create them if you need to), answer with commands:
- Which branches can be deleted right now without losing any commit?
- Which branches hold unintegrated work, and how many commits is that in each case?
- Which branch has been idle the longest?
- Which branches contain a given commit?
Exercise 2: the squash case
Reproduce the situation that confuses --merged: integrate a branch with --squash and show with commands that:
- Its content is in
main. - Git considers it not merged.
-drefuses to delete it and-Ddoes not.- After deleting it with
-D, it can be recreated exactly where it was.
Exercise 3: validating branch names
Write a small script that takes a branch name and decides whether it is valid according to Git and according to a team convention requiring one of these prefixes: feature/, fix/, hotfix/ or docs/. Try it with at least four names, valid and invalid.
Solutions
Solution 1:
Their tips are reachable from main: deleting them leaves no commit without a reference.
# 2. Branches with unintegrated work, with the count
for r in $(git branch --no-merged main --format='%(refname:short)'); do
n=$(git rev-list --count main..$r)
echo "$r: $n unintegrated commits"
donegit rev-list --count main..<branch> counts the commits in the range, with the same semantics you learnt in lesson 02-06.
# 3. The most idle branch: ascending order, first line
git branch --sort=committerdate --format='%(committerdate:short) %(refname:short)' | head -1Solution 2:
mkdir /tmp/practice-management && cd /tmp/practice-management
git init -b main
echo "base" > f.txt && git add . && git commit -m "Base"
git switch -c feature/something
echo "step 1" >> f.txt && git commit -am "Step 1"
echo "step 2" >> f.txt && git commit -am "Step 2"
echo "step 3" >> f.txt && git commit -am "Step 3"
git switch main
git merge --squash feature/something
git commit -m "Add the complete feature"Zero differences: the files are identical.
Three commits that main does not have, even though their effect is there, condensed into a single different commit.
error: The branch 'feature/something' is not fully merged. If you are sure you want to delete it, run 'git branch -D feature/something'.
# 4. Exact recreation with the hash Git gave us
git branch feature/something 5f8b2e1
git log --oneline -1 feature/somethingIdentical to how it was. The commits never went anywhere: only the name pointing at them was deleted.
Solution 3:
#!/bin/bash
# validate-branch.sh — checks a name is valid for Git and for the team
NAME="$1"
if [ -z "$NAME" ]; then
echo "Usage: $0 <branch-name>"
exit 2
fi
# 1. Does Git accept it?
if ! git check-ref-format --branch "$NAME" > /dev/null 2>&1; then
echo "REJECTED: '$NAME' is not a valid branch name for Git."
exit 1
fi
# 2. Does it follow the team convention?
case "$NAME" in
feature/*|fix/*|hotfix/*|docs/*)
;;
*)
echo "REJECTED: '$NAME' must start with feature/, fix/, hotfix/ or docs/."
exit 1
;;
esac
# 3. Extra style rule: no capitals
if echo "$NAME" | grep -q '[A-Z]'; then
echo "REJECTED: '$NAME' must not contain capital letters."
exit 1
fi
echo "ACCEPTED: $NAME"
exit 0Tests:
chmod +x validate-branch.sh
./validate-branch.sh 'feature/csv-export'
./validate-branch.sh 'fix/empty-list-message'
./validate-branch.sh 'fix2'
./validate-branch.sh 'feature/Export CSV'
./validate-branch.sh 'fix/branch..old'ACCEPTED: feature/csv-export ACCEPTED: fix/empty-list-message REJECTED: 'fix2' must start with feature/, fix/, hotfix/ or docs/. REJECTED: 'feature/Export CSV' is not a valid branch name for Git. REJECTED: 'fix/branch..old' is not a valid branch name for Git.
Look at the fourth case: the space gets it rejected by git check-ref-format already, before we ever reach the capitals check. And the fifth is rejected for the .., which Git reserves for commit ranges.
This script is exactly what turns into a pre-commit or pre-push hook so that the convention enforces itself, without depending on anybody's discipline. We will see that in lesson 06-01.
Conclusion
This lesson closes module 3. What you have learnt here:
git branch -vshows each branch's tip and last message;-vvwill add the tracking information once we have remotes (module 4).--mergedand--no-mergedanswer the key maintenance question: which branches can be deleted without losing any commit. Watch out for the ones integrated via squash, which always show up as unmerged even though their content is inmain.--sort=-committerdatewith--formatturns a useless alphabetical listing into a real view of what is alive, who is driving it and since when. Save it as an alias.git branch -mrenames: it renames the 41-byte file and, if it was the current branch, updatesHEAD. Completely safe locally.-dversus-D: the first checks that nothing gets lost and refuses if that is not the case; the second forces it through. When-dfails, it means the tip is not reachable from where you are, not necessarily that work will be lost. Check withgit log main..<branch>andgit diff main <branch>before forcing.-Dis recoverable for weeks thanks to the reflog (module 9).- Deleting a branch deletes 41 bytes. The commits stay intact in the object database.
- Naming conventions (
feature/,fix/,hotfix/…) group, allow filtering and make autocompletion useful. Git's formal rules can be checked withgit check-ref-format --branch. - Hygiene is a four-step routine: list by age, separate merged from unmerged, review the latter and delete the former. And the best hygiene of all is short-lived branches that never pile up.
What comes next
You have branches under control now: what they are, how to create them, how to merge them, how to resolve the clashes and how to keep the repository tidy. But all of it inside a single .git/.
In module 4: Working with Remote Repositories the project finally leaves Ana's laptop. We will see what a remote is, how to register one, how access is authenticated, the difference between git fetch and git pull, how to send work with git push and what tracking branches are. And Carla will finally join the team from her Windows 11 machine, cloning the project just as Bruno did in lesson 02-02, but this time into a repository that already has history, branches and a way of working.
Mastering Git: From Beginner to Advanced
Module 1: Introduction to Git
- What Is Git?
- Installing Git
- Basic Git Terminology
- The Git Data Model
- Configuring Git
- Initial Configuration
Module 2: Basic Git Operations
- Creating a Repository
- Cloning a Repository
- The Basic Git Workflow
- Staging and Committing Changes
- Inspecting Changes with git diff
- Viewing Commit History
Module 3: Branching and Merging
- Understanding Branches
- Creating and Switching Branches
- Merging Branches
- Merge Strategies
- Resolving Merge Conflicts
- Branch Management
Module 4: Working with Remote Repositories
- Understanding Remote Repositories
- Adding a Remote Repository
- Authenticating with Remote Repositories
- Fetching and Pulling Changes
- Pushing Changes
- Tracking Branches
Module 5: Advanced Git Operations
Module 6: Git Tools and Techniques
- Using Git Hooks
- Git Bisect
- Git Blame
- Git Log and Aliases
- Git Submodules
- Multiple Working Copies with git worktree
Module 7: Collaboration and Workflow Strategies
- Forks and Pull Requests
- Code Reviews with Git
- The Git Flow Workflow
- GitHub Flow
- Trunk Based Development
- Continuous Integration with Git
Module 8: Git Best Practices and Tips
- Writing Good Commit Messages
- Keeping a Clean History
- Ignoring Files with .gitignore
- File Attributes with .gitattributes
- Security Best Practices
- Performance Tips
Module 9: Troubleshooting and Debugging
- Common Git Problems
- Undoing Changes
- Resolving Divergence with the Remote
- Recovering Lost Commits
- Dealing with Corrupted Repositories
- Advanced Debugging Techniques
