Git basics · Lesson 6 of 6 · about 25 minutes

When things go wrong

Set work aside with stash, back out of a merge, escape a detached HEAD, move a commit made on the wrong branch, and learn to read what Git is telling you. Plus what to do if you commit a secret.

By the end you'll know

  • How to set unfinished work aside with git stash, and get it back
  • How to back out of a merge that's going wrong
  • What "detached HEAD" means, and how to get out safely
  • How to move a commit you made on the wrong branch
  • What to do, and not do, if you commit a secret
  • How to read Git's messages, which usually tell you the fix

The most useful habit: read the message

Most Git "disasters" come with instructions. Git's error messages are unusually helpful: they say what went wrong, and lines starting with hint: or in brackets often give you the exact command to run next. You've already seen it: "Please commit your changes or stash them" in Lesson 4, and "use 'git pull' before pushing again" in Lesson 5.

So before searching the internet, read the whole message, slowly, and run git status. Between them, they explain most situations. Everything in this lesson starts from that.

Set work aside: git stash

In Lesson 4, Git refused to switch branches because your uncommitted changes were in the way, and suggested you "commit your changes or stash them". Committing isn't always right: the work might be half-finished. git stash sets your uncommitted changes aside on a shelf, leaves your folder clean, and lets you bring them back later.

Think of clearing your desk for a visitor. You're in the middle of something when someone needs the desk for a different job. You don't throw your papers away, and you don't file them as finished. You sweep them into a drawer, help with the other job, then pull them back out exactly as they were. git stash is the drawer.

In your git-practice folder, make a branch with a change to ideas.txt, then go back to main and make a different, uncommitted change to the same file:

BASH
git switch -c more-notes
echo "- learn git stash" >> ideas.txt
git commit -am "Add git stash to the ideas"
git switch main
echo "- learn git log" >> ideas.txt
git switch more-notes
TEXT
error: Your local changes to the following files would be overwritten by checkout:
	ideas.txt
Please commit your changes or stash them before you switch branches.

Stash the change instead:

BASH
git stash
git status --short
TEXT
Saved working directory and index state WIP on main: 07b7b15 ...

git status --short shows nothing: your folder is clean, and the change is on the shelf. (WIP means "work in progress".) Now you can switch freely:

BASH
git switch more-notes
git switch main

Back on main, take your change off the shelf:

BASH
git stash pop
git status --short
TEXT
 M ideas.txt

Your edit is back, exactly as it was. pop reapplies the most recent stash and removes it from the shelf. A few more you'll use:

  • git stash list shows everything on the shelf. You can stash several times; each gets a number, like stash@{0}.
  • git stash -u also stashes untracked files. Plain git stash only takes changes to files Git already tracks, so a brand-new file is left behind. If git stash says "No local changes to save" but you know you've made a new file, that's why.
  • git stash drop throws away the latest stash, when you've decided you don't need it.

Throw away the practice edit, and delete the branch:

BASH
git restore ideas.txt
git branch -D more-notes

Back out of a merge

Lesson 4 showed how to finish a merge conflict. Sometimes you'd rather not: the conflict is bigger than you expected, or you merged the wrong branch. Set up a conflict:

BASH
git switch -c clash
echo "- learn git stash" >> todo.txt
git commit -am "Add git stash to the to-do list"
git switch main
echo "- learn git log" >> todo.txt
git commit -am "Add git log to the to-do list"
git merge clash
TEXT
CONFLICT (content): Merge conflict in todo.txt
Automatic merge failed; fix conflicts and then commit the result.

Now ask Git where you stand:

BASH
git status
TEXT
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

The answer is right there in brackets. git merge --abort puts everything back exactly as it was before you typed git merge:

BASH
git merge --abort
git status --short

Nothing listed: no conflict, no half-merged file, as if the merge never happened. When a merge gets confusing, aborting and starting again calmly is always an option. Delete the practice branch, and push the to-do list commit you made on main, so main matches GitHub again before the next part:

BASH
git branch -D clash
git push

Detached HEAD

This message alarms almost everyone the first time. Look at an old commit directly, by going back three commits:

BASH
git checkout HEAD~3
TEXT
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.

Nothing is broken. In Lesson 3, HEAD was the "you are here" pin, normally attached to a branch: it points at main, and main points at a commit. Now HEAD points straight at an old commit, with no branch in between. That's all "detached" means. It's a perfectly good way to look at an old version: your files now look exactly as they did three commits ago (cat todo.txt to check).

The one catch is making commits here. A commit always moves the branch HEAD is attached to, but HEAD isn't attached to any branch, so a new commit has nothing holding on to it:

Detached HEADCommits A, B, C and D are on main, and the main label points at D. HEAD has been moved to point straight at the older commit B, with no branch in between. A commit made now, X, hangs off B with no branch label pointing at it. Switching back to main leaves X behind, unless you first give it a branch with git switch -c.ABCDmainHEADHEAD points straight at an old commitXa commit made while detached:no branch points at it, so switching away leaves it behindLooking around is safe. To keep a commit you made here, give it a branch with git switch -c.
In detached HEAD, HEAD points at a commit instead of a branch. Commits you make there have no branch holding on to them until you create one.

Try it. Make a commit while detached, then go back to main:

BASH
echo "an experiment" >> ideas.txt
git commit -am "Experiment on an old commit"
git switch main
TEXT
Warning: you are leaving 1 commit behind, not connected to
any of your branches:

  88be1c0 Experiment on an old commit

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> 88be1c0

Git warns you, and even gives you the fix. So there are two ways out of detached HEAD:

  • Just looking? git switch main (or any branch), and you're back.
  • Made commits you want to keep? Before leaving, give them a branch: git switch -c experiment. The new branch points at your commits, and you're attached again.

And if you've already left, like you just did, the warning shows the commit's ID, and the reflog from Lesson 3 remembers it too. git branch experiment 88be1c0, with your own ID, rescues it. (This one was only practice, so you can leave it.)

Committed on the wrong branch

A very common one: you meant to start a branch, forgot, and committed straight onto main.

BASH
echo "- learn branches properly" >> todo.txt
git commit -am "Start a new idea"
git status
TEXT
On branch main
Your branch is ahead of 'origin/main' by 1 commit.

You haven't pushed yet, so this is easy to fix. First, put a branch label on the commit, so it's safe:

BASH
git branch new-idea

git branch without switch creates the branch but keeps you on main. Now move main back one commit, to where it was:

BASH
git reset --hard HEAD~1
git status
TEXT
On branch main
Your branch is up to date with 'origin/main'.

main is back to matching GitHub, and the commit lives on new-idea, right where it belongs:

BASH
git switch new-idea
git log --oneline -1

The order matters: branch first, then reset. The branch is what keeps the commit safe while you move main. And this only applies before you push: once a commit on main is on GitHub, revert it instead (Lesson 3). Clean up with git switch main and git branch -D new-idea.

If you commit a secret

Lesson 1 warned that a committed password stays in the history even after you delete the file. If it happens:

  1. Treat the secret as leaked, and change it. Make a new password or API key, and turn off the old one. This is the step that actually protects you, and it's the one people skip.
  2. If you haven't pushed yet, remove it from the commit: take it out of the file, add the file to .gitignore (Lesson 2), and git commit --amend (Lesson 3).
  3. If you have pushed, removing it from the history takes special tools, like git filter-repo, and rewrites shared history, with all the trouble from Lesson 3. That's why step 1 comes first: once a secret has been pushed, assume someone has it.

GitHub also scans pushes for common kinds of secrets, like API keys, and can block a push that contains one. Treat that as a safety net, not a plan.

In my real setup

Writing this track, I ran into several of these on my own site. I hit "would be overwritten by checkout" while publishing a lesson, and stashing got me out; Git also stashes by itself during git pull when it needs to, which is the Created autostash line you may have noticed. Git said four of my old branches weren't merged when they were, because they'd been squashed (Lessons 4 and 5).

And once, I ran my whole publish sequence a second time by accident. Every step reported that there was nothing to do: no changes added to commit, then could not find any commits between origin/main from the pull request step, then already merged. Nothing broke, and nothing was published twice. Git's commands mostly check before they act, which makes running something twice far less scary than it looks.

The most instructive problem wasn't a Git problem at all. Several lessons on this site went live marked as drafts, so they didn't show up. Git did exactly what I told it to: it committed the file as it was. The fix wasn't a Git command, it was changing my process, so the step that marks a lesson as published is part of the same commands that commit it. When something keeps going wrong, it's worth asking whether the tool is the problem, or the steps around it.

Break it on purpose

Stash a new file the wrong way. Create a new file, and stash without -u:

BASH
echo "a brand-new note" > notes.txt
git stash
TEXT
No local changes to save

Git saved nothing, because notes.txt is untracked: plain git stash ignores it. The file is still in your folder (ls), but anyone who expected it to be safely stashed would be surprised. Use -u:

BASH
git stash -u
ls
git stash pop
ls

The first ls shows notes.txt is gone, safely on the shelf. After pop, it's back. Delete it with rm notes.txt (rm removes a file, permanently, with no undo).

You've finished Git basics

That's the end of the track. You can save history, undo mistakes, work on branches, collaborate through GitHub with pull requests, and get yourself out of the usual messes. For the next step, the CI/CD track covers what happens automatically when you push: tests, builds and deploys.

Check yourself

  1. Git won't switch branches because your uncommitted changes are in the way, but the work isn't ready to commit. What do you run?

  2. You made a new file, ran git stash, and Git said No local changes to save. Why?

  3. A merge stops with a conflict, and you decide you'd rather not merge right now. What's the safest way out?

  4. Git says you're in detached HEAD state. What does that mean?

  5. You committed to main by mistake and haven't pushed. What's the right order to fix it?

  6. You pushed a commit containing a real API key. What should you do first?

Key terms

  • git stash / git stash pop: set uncommitted changes aside / bring the latest set back. -u includes untracked files.
  • git merge --abort: cancels a merge in progress, putting everything back as it was.
  • Detached HEAD: HEAD pointing straight at a commit instead of a branch.
  • git checkout commit: an older command that, given a commit, moves you into detached HEAD to look at it.
  • git branch name: creates a branch at the current commit without switching to it.
  • Autostash: Git stashing and restoring your changes by itself, for example during git pull.
  • Rotate a secret: replace a password or key with a new one, and turn off the old one.

Go deeper