In the previous lesson we took the mystery apart: a branch is a 41-byte file with a hash inside, and HEAD is another file that records which branch you are on. Now we are going to put that machinery to work. Ana will finally open feature/task-counter, Bruno will do the same with feature/pending-filter, and the repository will stop having a single line of work.
Creating branches in Git is trivial — you will see that in a second — but moving between them has subtleties worth mastering from the start: what happens to the work you have half finished, why Git sometimes lets you switch and sometimes refuses, and what that detached HEAD state is, the one that turns up when you least expect it and frightens everybody the first time.
We will also settle a question that follows anyone who learned Git before 2019: the difference between git checkout and git switch, and why Git decided to split one command into two.
Contents
git branch: creating without movinggit switch: movinggit switch -c: create and move in one stepgit switchversusgit checkout- Creating a branch from a specific point in history
git switch -: toggling between two branches- Uncommitted changes when you switch branches
- Detached HEAD
- The team's repository, now with branches
git branch: creating without moving
git branch: creating without movingAna is on main, with the repository exactly as we left it:
c5d9b1e (HEAD -> main) Document installation in the README 4e7f2a9 Add task deletion to the list 8b6d3c2 Add base styles for the list 1a4c8d6 Add initial task manager structure
She wants a branch for the task counter:
Complete silence. In Git, silence usually means success. Let us see exactly what happened:
A new file, holding the hash of the commit Ana was sitting on. (An interesting aside: because the branch name contains a slash, Git has created a directory called feature/ inside refs/heads/. Branch names with slashes are simply file paths; we will come back to naming conventions in lesson 03-06.)
Now the important part:
HEAD has not moved. Ana is still on main. She has created a new pointer but has not placed herself on it:
The asterisk marks the current branch. There are two branches and both point at the same commit.
gitGraph commit id: "1a4c8d6" commit id: "8b6d3c2" commit id: "4e7f2a9" commit id: "c5d9b1e" branch task-counter
Rule:
git branch <name>creates the pointer and touches nothing else. NotHEAD, not the working tree, not the staging area.
If Ana committed now, the commit would land on main, because that is still where HEAD points. It is a classic mistake: create the branch and forget to switch to it. That is why, in practice, plain git branch is almost never used to start work; the variant in section 3 is.
git switch: moving
git switch: movingTo place herself on the branch she has just created:
And now:
That is all Git did in this case: rewrite the file .git/HEAD. Because both branches pointed at the same commit, the working tree did not change by a single byte.
When the branches point at different commits, git switch does more: as well as rewriting HEAD, it updates the working tree and the staging area so they match the tree of the target commit. In other words, it changes the files on your disk. That is what happens in the three internal operations of a branch switch:
| Step | What Git does |
|---|---|
| 1 | Checks that the switch is safe (see section 7) |
| 2 | Updates the working tree and the index to the tree of the target commit |
| 3 | Rewrites .git/HEAD with the new branch |
Ana writes the task counter in app.js and commits:
Look at the name in square brackets: [feature/task-counter 6f2b9d4]. Git is telling you which branch the commit landed on. It is a free check, and one worth reading every time.
Ana then adds the "mark as done" behaviour:
[feature/task-counter 9d1e4b7] Mark tasks as done on click 2 files changed, 18 insertions(+), 2 deletions(-)
Current state:
* 9d1e4b7 (HEAD -> feature/task-counter) Mark tasks as done on click * 6f2b9d4 Add the pending task counter * c5d9b1e (main) Document installation in the README * 4e7f2a9 Add task deletion to the list * 8b6d3c2 Add base styles for the list * 1a4c8d6 Add initial task manager structure
main has stayed put at c5d9b1e, exactly where it was. Ana's work is isolated: she can commit as much as she likes without touching the project's main line.
git switch -c: create and move in one step
git switch -c: create and move in one stepCreating a branch and not switching to it is unusual. Hence the shortcut:
The -c stands for create. It is exactly equivalent to:
This is the form you will use 95% of the time. Plain git branch <name> is reserved for the cases where you want to mark a point in history without abandoning what you are doing (for instance, leaving a bookmark before a risky operation).
One important detail that often gets overlooked: the new branch is created from wherever you are right now. If Ana ran that command while on feature/task-counter, the new branch would start at 9d1e4b7 and drag the task counter along with it, which is not what she wants. For Bruno's work, the correct starting point is main:
Or, better still, by stating it explicitly in a single command — the form we will see in section 5.
There is a capital-letter variant, -C, which creates the branch or resets it if it already exists:
Use it carefully: if the branch already existed pointing somewhere else, -C moves the pointer without asking and you can leave commits orphaned. -c, by contrast, fails with fatal: a branch named '…' already exists, which is almost always what you want.
git switch versus git checkout
git switch versus git checkoutIf you have seen Git in any tutorial written before 2019, or in most of the ones on the internet today, you will have seen this:
git checkout feature/task-counter # same as git switch
git checkout -b feature/pending-filter # same as git switch -cIt works. git checkout is not deprecated and is not going away. But since Git 2.23 (August 2019) we have git switch and git restore, and for new work they are the recommended option. The reason is simple: git checkout did far too many different things.
| Need | Old command | Modern command |
|---|---|---|
| Switch branch | git checkout <branch> |
git switch <branch> |
| Create a branch and switch | git checkout -b <branch> |
git switch -c <branch> |
| Create a branch from a commit | git checkout -b <branch> <commit> |
git switch -c <branch> <commit> |
| Go to a loose commit | git checkout <commit> |
git switch --detach <commit> |
| Return to the previous branch | git checkout - |
git switch - |
| Discard a file's changes | git checkout -- <file> |
git restore <file> |
| Remove from the staging area | git reset HEAD <file> |
git restore --staged <file> |
| Recover a file from another commit | git checkout <commit> -- <file> |
git restore --source=<commit> <file> |
Look at the last three rows, which are the important ones. git checkout served both to navigate between branches (a harmless, reversible operation) and to destroy working-tree changes (an irreversible one). The same verb for two things of opposite risk.
The potential disaster lay in the ambiguity. Imagine there is a file called report and also a branch called report:
Does it switch branch or discard the file's changes? Git resolved in favour of the branch, and you had to write git checkout -- report (with the double-dash separator) to force the file interpretation. Forget the -- and you could lose work with no warning.
The split introduced in Git 2.23 removes the ambiguity at the root:
git switchonly works with branches. It cannot touch files.git restoreonly works with files. It cannot switch branches.
You already used git restore in lesson 02-04, so you know half of the reform already. This lesson gives you the other half.
What to do in practice:
- Type
git switchandgit restorein your everyday work. - Learn to read
git checkout, because you will see it everywhere: old documentation, Stack Overflow answers, continuous integration scripts and colleagues with ingrained habits. - Do not convert existing scripts that work purely for the sake of fashion;
checkoutremains perfectly valid.
- Creating a branch from a specific point in history
By default, a new branch starts from HEAD. But you can give it another starting point as a second argument, and that works for both forms of the command:
The starting point can be anything Git knows how to resolve to a commit, with all the syntax you learned in lesson 02-06:
# From another branch, without having to switch first
git switch -c feature/pending-filter main
# From a specific commit, by its hash
git switch -c experiment/redesign 8b6d3c2
# From a relative position
git switch -c fix/regression HEAD~2
# From the parent of a specific commit
git switch -c test 4e7f2a9^The first is Bruno's case and it is the right way to do it: it creates the branch from main without needing to switch to main first. One command instead of two, and no risk of forgetting.
A real case where this saves your afternoon: you have been coding on main for half an hour by mistake, you have made three commits, and you realise they should have gone on a branch. The fix:
# 1. Create the branch right here, with the three commits inside
git branch feature/whatever
# 2. Put main back where it belonged
git switch main
git reset --hard HEAD~3
# 3. Go to the branch, where all the work is intact
git switch feature/whateverNotice that step 1 uses git branch without switching: it creates the bookmark before moving anything. (git reset --hard is powerful and dangerous; we will study it thoroughly in lesson 09-02. It is safe here because the commits are still reachable from the new branch.)
git switch -: toggling between two branches
git switch -: toggling between two branchesWhen you are reviewing a colleague's work and hopping constantly between two branches, typing the full name gets tiring. A lone dash means "the previous branch":
It is the same idea as cd - in the shell. It works with branch names as long as feature/export-list-to-csv and saves an enormous number of keystrokes.
Under the bonnet, the dash is an alias for @{-1}, meaning "the branch I was on one switch ago". And there is more:
This information comes from the reflog, the record Git keeps of every movement of HEAD. It is the same tool that lets you recover apparently lost work, and we will study it in lesson 09-04.
- Uncommitted changes when you switch branches
Here is the subtlety that causes the most confusion. What happens to your half-finished work when you switch branches?
The short answer: Git tries to bring it with you, and if it cannot do so without losing anything, it refuses.
The long answer depends on whether the changes affect files that differ between the two branches.
Case A: Git lets you through and brings the changes along
Ana is on feature/task-counter and has started reworking README.md, a file that is identical on both branches:
It worked, and that M README.md line is Git's way of saying that the file arrived modified and is still modified. The change has travelled with Ana to the other branch. It is still uncommitted, in her working tree:
This is deliberately useful: it lets you start writing something, realise you are on the wrong branch and put it right without losing anything.
Case B: Git refuses
Now Ana is on main and modifies app.js, which does differ between main and the counter branch (that is where the counter changed it):
error: Your local changes to the following files would be overwritten by checkout: app.js Please commit your changes or stash them before you switch branches. Aborting
Git has refused, and rightly so. To place itself on the other branch it would have to overwrite app.js with the version from there, and that would destroy Ana's changes beyond recovery, because they have never been in the object database.
The important thing is that the operation was aborted entirely. You are still where you were and your changes are untouched.
The rule, summarised
| Situation | Behaviour |
|---|---|
| The modified file is the same on both branches | Switches branch and brings the modifications along |
| The modified file differs between the two branches | Refuses the switch and aborts |
| There are staged changes to files that do not differ | Switches branch and keeps the staging area |
| There are new untracked files that do not exist on the target | Switches branch and leaves them where they are |
| There are untracked files that do exist on the target | Refuses, so as not to overwrite them |
The three ways out when Git refuses
-
Commit. If the work makes sense on the current branch, commit it. If it is half finished, you can make a provisional commit and tidy it up later with
git commit --amend(lesson 02-04) or with an interactive rebase (lesson 05-02). -
Discard. If the change is worthless,
git restore app.jsremoves it and the branch switch becomes possible. -
Stash it. For the realistic case — half-finished work you want to keep but that does not yet deserve a commit — Git has a dedicated tool:
git stash, which sets the changes aside in a temporary store, leaves the working tree clean and lets you get them back later withgit stash pop:
git stash # sets the changes aside
git switch feature/task-counter # now it can switch
# … whatever you came to do …
git switch main
git stash pop # brings the changes backIt is a command with a great deal more depth than this example suggests (several stacked entries, including untracked files, applying on another branch…). We will study it in full in lesson 05-04; for now, just remember it exists and that it is the standard answer to this problem.
Forcing the branch switch (carefully)
There is an option to discard the changes and switch branch anyway:
(The old equivalent was git checkout -f.) It is destructive and irreversible: uncommitted changes are not in any Git object, so there is no reflog and no possible recovery. Use it only when you are absolutely certain you want to throw that work away.
- Detached HEAD
Remember the previous lesson: HEAD normally contains ref: refs/heads/<branch>. But it can contain a hash directly. That state is called a detached HEAD.
How you get there
The deliberate way is to ask for it:
Note: switching to '4e7f2a9'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by switching back to a branch. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -c with the switch command. Example: git switch -c <new-branch-name> HEAD is now at 4e7f2a9 Add task deletion to the list
Git gives you a whole speech because the state is unusual, not because it is dangerous. In fact that text already explains almost everything you need to know.
The accidental ways of getting there are the frightening ones:
git checkout <hash>(without-b), the most common with the old command.git checkout v1.2.0, when landing on a tag — tags point at commits, they are not branches.- During a
git rebaseor agit bisect, which work internally in this state (modules 5 and 6). - Landing on
origin/maininstead ofmain, a very common mistake we will see in module 4.
What you see
A hash directly, with no ref:. HEAD points at the commit, not at a branch.
The working tree holds the files exactly as they were at that commit. It is a perfectly legitimate state: you can build, run the tests, look at old code or even commit.
What it is genuinely for
It is not an accident to be avoided; it is a tool:
- Reviewing how the project looked at a given moment. Ana wants to see the application as it was on the day of the first commit:
git switch --detach 1a4c8d6, openindex.htmlin the browser, done. - Testing whether a bug existed in an older version, by landing on different points in history.
- Experimenting without committing yourself. You can commit in this state, and if the result is no good you go back to a branch and the commits are left orphaned.
The real danger
The danger is not being in this state. It is committing in it and then leaving without a trace:
# In detached HEAD
echo "// performance test" >> app.js
git commit -am "Try a rendering optimisation"That commit 7d2f9a1 is real and is in the object database, but no branch reaches it. If Ana now runs git switch main:
Warning: you are leaving 1 commit behind, not connected to any of your branches: 7d2f9a1 Try a rendering optimisation If you want to keep it by creating a new branch, this may be a good time to do so with: git branch <new-branch-name> 7d2f9a1 Switched to branch 'main'
Git warns you, and even hands you the exact command. But if you do not read that warning and close the terminal, the hash disappears from view and the commit is orphaned. It stays recoverable for a while through the reflog (module 9), but it is not a situation you want.
How to get out without losing anything
If you have committed nothing (you were only looking):
Or git switch main, or any branch. There is nothing to save.
If you have committed and want to keep it, create a branch before you leave:
Now HEAD points at a branch again, that branch points at 7d2f9a1, and the commit is reachable. Nothing is orphaned any more.
If you have already left and the commit stayed behind, you can still create the branch after the fact, as long as you have the hash (it appears in the warning, and if you lost it, in git reflog):
graph TD
A["On a branch<br/>HEAD → ref: refs/heads/main"] -->|"git switch --detach abc123"| B["Detached HEAD<br/>HEAD → abc123"]
B -->|"git switch -<br/>(nothing committed)"| A
B -->|"commit here"| C["Orphaned commits<br/>no branch reaches them"]
C -->|"git switch -c my-branch"| D["Work saved<br/>on a new branch"]
C -->|"leave without creating a branch"| E["Recoverable only<br/>via the reflog (module 9)"]
- The team's repository, now with branches
Let us recap where we are. Ana has her branch with two commits. Bruno has been working on his, with the three commits we saw at the end of module 2 — the pending filter, the styling of completed tasks and the focus fix — now on feature/pending-filter rather than directly on main.
For the rest of the module we will reason as though the three branches lived in one and the same local repository, Ana's. How the team gets work to travel between Ana's laptop and Bruno's MacBook is precisely the subject of module 4; here we concentrate on branch mechanics, which are identical wherever the work comes from.
feature/pending-filter b2e6d3f Restore field focus after adding a task feature/task-counter 9d1e4b7 Mark tasks as done on click * main c5d9b1e Document installation in the README
* b2e6d3f (feature/pending-filter) Restore field focus after adding a task * 7c1f4a9 Apply style to completed tasks * 3d5b8e1 Add a pending tasks filter | * 9d1e4b7 (feature/task-counter) Mark tasks as done on click | * 6f2b9d4 Add the pending task counter |/ * c5d9b1e (HEAD -> main) Document installation in the README * 4e7f2a9 Add task deletion to the list * 8b6d3c2 Add base styles for the list * 1a4c8d6 Add initial task manager structure
gitGraph commit id: "1a4c8d6" commit id: "8b6d3c2" commit id: "4e7f2a9" commit id: "c5d9b1e" branch task-counter checkout task-counter commit id: "6f2b9d4" commit id: "9d1e4b7" checkout main branch pending-filter checkout pending-filter commit id: "3d5b8e1" commit id: "7c1f4a9" commit id: "b2e6d3f"
Three lines of work, three pointers, 123 bytes of total cost. The four base commits sit once in the object database and all three branches share them.
And here is the problem we will solve in the next lesson: both features are finished and tested, but the real project — main — is still exactly where it was a week ago. Nothing Ana and Bruno have done is on the main line.
Common Mistakes and Tips
Mistake 1: git branch <name> and then getting to work. You create the branch, forget the git switch, make five commits and every one lands on main. Two-part prevention: use git switch -c instead of git branch, and read the line in square brackets that git commit prints, which tells you the target branch.
Mistake 2: creating the branch from the wrong place. Running git switch -c fix/urgent while on a half-finished feature branch drags all that unfinished work into the urgent fix. Before creating a branch, check where you are with git branch --show-current, or state the starting point explicitly: git switch -c fix/urgent main.
Mistake 3: panicking about detached HEAD. It is neither an error nor corruption: it is a normal state. The message is long because it explains, not because it is warning you of a disaster. If you were only looking, git switch - and that is that. If you committed, git switch -c <name> before you leave.
Mistake 4: using git checkout -f or --discard-changes to "get unstuck". That command destroys uncommitted changes irreversibly: they are in no Git object and no reflog will help. When Git refuses to switch branch, the right answer is to commit, to discard deliberately with git restore, or to set aside with git stash.
Mistake 5: git switch -C instead of -c. The capital letter resets an existing branch without asking. If the branch already had work on it, the pointer moves and that work is left orphaned. -c fails with a clear error, which in 99% of cases is exactly what you want to happen.
Tip 1: one branch per task, and short-lived. Branches are free, so do not pile work up in them. A branch that lives three weeks accumulates divergence and guarantees conflicts; one that lives two days almost never has any.
Tip 2: turn on Git's autocompletion. With names like feature/pending-filter, typing git switch fea<TAB> is the difference between working comfortably and landing on the wrong branch. The git-completion.bash script ships with Git and many distributions enable it by default.
Tip 3: check where you are before committing. It costs one second:
Tip 4: do not reuse old branches for new tasks. It is tempting (I already have the tests branch, I will reuse it), but it ends in histories nobody can follow and merges that drag in changes nobody asked for. Create a new branch and delete the old one once it is no longer useful; tidying up is the subject of lesson 03-06.
Exercises
Exercise 1: creating branches without moving
In a test repository with at least three commits, create two branches — experiment/one and experiment/two — without switching to either of them, such that:
experiment/onestarts from the current commit.experiment/twostarts from two commits back.
Then prove with commands that HEAD has not moved and that both branches point where they should.
Exercise 2: triggering and resolving a refused branch switch
Design the minimal sequence of commands that triggers the error Your local changes to the following files would be overwritten by checkout, then resolve it in three different ways, explaining when you would use each.
Exercise 3: getting out of a detached HEAD without losing work
Deliberately land in a detached HEAD, make a commit there and keep it on a new branch. Then repeat the process but abandon the state without creating the branch, and observe what Git tells you. Which piece of information in the message is essential to note down?
Solutions
Solution 1:
# Both branches, with git branch (which does NOT move HEAD)
git branch experiment/one
git branch experiment/two HEAD~2Checking:
experiment/one c5d9b1e Document installation in the README experiment/two 8b6d3c2 Add base styles for the list * main c5d9b1e Document installation in the README
The asterisk is still on main: HEAD has not moved. experiment/one matches main and experiment/two is two commits back, as the exercise asked.
Byte-by-byte verification:
Solution 2:
Two conditions are needed to trigger the error: a file that differs between the two branches, and an uncommitted modification to that same file.
# 1. A branch with a committed change to app.js
git switch -c branch-x
echo "// change from branch X" >> app.js
git commit -am "Change app.js on branch X"
# 2. Back to main, and touch the SAME file without committing
git switch main
echo "// uncommitted change on main" >> app.js
# 3. Try to go back
git switch branch-xerror: Your local changes to the following files would be overwritten by checkout: app.js Please commit your changes or stash them before you switch branches. Aborting
The three resolutions:
# A) Commit: the work belongs on main and is ready
git commit -am "Add a note in app.js"
git switch branch-xWhen: when the change is valid and makes sense on the current branch.
When: experiments, debugging traces or anything you do not want to keep. It is irreversible.
# C) Stash: half-finished work I want to keep
git stash
git switch branch-x
# … work on branch-x …
git switch main
git stash popWhen: the most common case in daily work, and the reason git stash exists (lesson 05-04).
Solution 3:
# Commit something here
echo "/* test from a detached HEAD */" >> styles.css
git commit -am "Try a style tweak"The commit is now reachable from a branch: nothing is orphaned.
Second part, leaving without creating a branch:
git switch --detach HEAD~2
echo "/* another test */" >> styles.css
git commit -am "Another test I am about to abandon"
git switch mainWarning: you are leaving 1 commit behind, not connected to any of your branches: 3c9d4a2 Another test I am about to abandon If you want to keep it by creating a new branch, this may be a good time to do so with: git branch <new-branch-name> 3c9d4a2 Switched to branch 'main'
The essential piece of information is the hash: 3c9d4a2. As long as you have it, recovering the work is a single command:
And if you lose the hash as well, the situation is not hopeless: git reflog stores every movement of HEAD for 90 days by default and lets you find it. That is the subject of lesson 09-04.
Conclusion
You can now create branches and move between them with judgement:
git branch <name>creates the pointer and touches nothing else: notHEAD, not the working tree, not the index. Useful for marking a point without abandoning what you are doing.git switch <branch>places you on a branch: it rewrites.git/HEADand updates the working tree to the target tree.git switch -c <name> [start-point]is the usual way to begin a task: it creates and switches in one step, and accepts any reference as its origin (another branch, a hash,HEAD~2…).git switchandgit restorereplacegit checkoutas of Git 2.23, splitting up what used to be a single ambiguous command: navigating between branches and destroying changes no longer share a verb.checkoutstill works and you will keep seeing it in documentation and scripts.git switch -toggles back to the previous branch, just likecd -.- When you switch branches, Git brings uncommitted changes with you if it can do so without losing anything, and aborts cleanly if the file differs between the two branches. The ways out are to commit, to discard with
git restoreor to set aside withgit stash(lesson 05-04). - A detached HEAD is
HEADholding a hash instead ofref: refs/heads/…. It is not an error: it is there for inspecting and experimenting. The only precaution is to create a branch withgit switch -cbefore leaving it if you have committed anything.
What comes next
The team now has three branches: main sitting where it was, feature/task-counter with two commits from Ana and feature/pending-filter with three from Bruno. Two finished features that are not in the project.
Isolating the work was half the problem; the other half is bringing it back together. In the next lesson, Merging Branches, Ana will finally integrate both features into main. We will see the two scenarios Git distinguishes — the fast-forward, where the pointer simply moves along, and the three-way merge, which creates a special commit with two parents — how Git finds the common ancestor we studied in the previous lesson, and why it is sometimes worth forcing a merge commit even when one is not needed.
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
