Git checkout: switch branches without losing open work

git checkout is two unrelated commands wearing one name. The manual page says so in its summary line, which describes it as switching branches or restoring working tree files. One form moves HEAD and leaves edits alone. Another overwrites files on disk with no recovery path. The difference between them is whether a pathspec was given, which is a single argument at the end of the line.

That is the whole reason this command has a reputation. Nothing about the syntax signals that a file name turns a navigation command into a destructive one, and the safe form and the dangerous form differ by a few characters.

The rule that separates the two behaviours

The manual page states the condition plainly:

Updates files in the working tree to match the version in the index or the specified tree. If no pathspec was given, git checkout will also update HEAD to set the specified branch as the current branch. Source: git-scm.com

With no pathspec, HEAD moves. With a pathspec, HEAD stays exactly where it is and the named files get overwritten instead. The first is a move between states of the project. The second is a local overwrite of specific files, and there is no reflog entry for the contents it replaced.

The reassuring half of the first behaviour is documented too. When switching to a branch, local modifications to files in the working tree are kept, so that they can be committed to that branch. Uncommitted work is not lost by switching. It travels along.

Why a switch sometimes refuses

The refusal happens under one specific condition: local modifications to one or more files that differ between the current branch and the target branch. The manual page explains the reasoning, which is that the command refuses in order to preserve those modifications in context. Carrying an edit onto a version of the file that has changed underneath it would silently produce something nobody wrote.

-m or --merge is the documented way through. With it, a three way merge is performed between the current branch, the working tree contents and the new branch, and the switch completes. When a conflict happens, the index entries for the conflicting paths are left unmerged and have to be resolved and marked with git add, or with git rm if the merge should result in a deletion.

One line in that section deserves attention before using it: when switching branches with --merge, staged changes may be lost. Anything carefully staged and not yet committed is at risk in that path, so committing first, even to a throwaway commit, costs nothing by comparison. --conflict=<style> behaves the same way while changing how the conflicting hunks are presented, overriding merge.conflictStyle, with merge, diff3 and zdiff3 as the values.

-f or --force is the other way past the refusal and it is not a variation on --merge. When switching branches it proceeds even if the index or the working tree differs from HEAD, and even if untracked files are in the way, and the manual page describes its purpose as throwing away local changes and any untracked files or directories that are in the way. Untracked files have never been in Git, so nothing can bring them back.

Creating a branch while switching

-b <new-branch> creates a branch and checks it out in one step, behaving as if git branch had been called and then the result checked out, which means --track and --no-track can be passed and are handed to git branch.

-B is the version that does not fail on an existing name: the branch is created if it does not exist, and otherwise reset to the start point. The manual page frames it as the transactional equivalent of git branch -f followed by a checkout, with the emphasis on transactional. The branch is not reset or created unless the checkout succeeds, so when the branch is in use in another worktree, not only does the current branch stay the same, the branch is not reset to the start point either.

--orphan <new-branch> creates an unborn branch whose first commit will have no parents, making it the root of a history disconnected from everything else. The index and working tree are adjusted as if the start point had been checked out, which the manual page presents as a way to publish a tree without exposing its full history. For a genuinely empty start, it suggests clearing the index and working tree with git rm -rf . from the top level immediately after creating the branch.

The guess that creates branches out of remote names

git checkout feature-x on a branch that does not exist locally often works anyway, and the mechanism behind that is worth knowing because it writes configuration. If the branch is not found but a tracking branch with a matching name exists in exactly one remote, the command is treated as equivalent to creating the branch with --track against that remote branch.

When the name exists on several remotes, checkout.defaultRemote decides which one is used, even if the branch is not unique across all remotes. Setting it to origin makes that the consistent source. The behaviour is on by default as --guess, can be turned off per command with --no-guess, and has checkout.guess as its configuration equivalent.

A related shortcut is easy to miss. -t or --track without -b implies branch creation, and the new branch name is derived from the remote-tracking branch by taking the local part of the configured refspec. Branching from origin/hack therefore produces a local branch called hack. If the derived name would be empty, or the given name has no slash, the guessing is aborted.

The form with no undo

Giving a pathspec changes the meaning completely. The manual page describes it as overwriting the contents of the files that match the pathspec, and the source depends on what else is given.

Command shape Source of the content What it overwrites
git checkout -- <paths> The index The working tree
git checkout <commit> -- <paths> That commit The index and the working tree
git checkout -p -- <paths> The index, hunk by hunk Selected hunks in the working tree
git checkout <branch> Nothing overwritten HEAD moves, local edits kept

The first row is the one that catches people. Uncommitted edits to a tracked file are not recorded anywhere, so replacing the working tree copy with the index copy discards them permanently. There is no reflog for file contents and no stash entry created on the way.

Unmerged entries have their own rules here. By default, checking out such an entry from the index fails and nothing is checked out at all, which is deliberate. -f ignores the unmerged entries, --ours and --theirs check out stage 2 or stage 3 for the unmerged paths, and -m discards changes made to the working tree file in order to re-create the original conflicted merge result, which is the way back after a botched conflict resolution.

--ours and --theirs carry a caution that has cost a lot of people an afternoon. During git rebase and git pull --rebase they may appear swapped, because --ours gives the version from the branch the changes are being rebased onto, while --theirs gives the version from the branch holding the work being rebased. The manual page explains the logic: during a rebase the remote history is treated as the shared canonical one, so it is the one referred to as ours.

-p or --patch interactively selects hunks in the difference between the tree-ish, or the index if none is given, and the working tree. The chosen hunks are then applied in reverse, which is what makes it a way to selectively discard edits rather than a way to apply them.

When a name is both a branch and a file

If a repository contains a branch called abc and a file called abc, the command is ambiguous, and Git resolves it rather than asking. The manual page states that because checking out a branch is so common an operation, git checkout abc takes abc as a tree-ish in that situation, and that git checkout -- <pathspec> is the form to use when the paths are what is meant.

This is the reason experienced users type -- even when nothing is ambiguous. The separator turns an implicit resolution into an explicit statement, and in the pathspec form the consequence of being wrong is an overwritten file.

Detached HEAD, and how the commits get lost

Checking out a commit rather than a branch detaches HEAD, and the manual page's walkthrough of what follows is the clearest explanation of a state people usually meet by accident. HEAD normally refers to a named branch, and the branch refers to a commit. Committing in the normal case updates the branch, so HEAD keeps pointing at the branch and indirectly at the new commit.

Detached, HEAD refers to a commit directly. Commits made in that state are referenced only by HEAD, and the manual page states the consequence without softening it: nothing refers to the last commit made there, and it will eventually be deleted by the routine garbage collection process unless a reference is created before that happens.

Three commands create that reference while still sitting on the commit. git checkout -b <name>, or git switch -c <name>, creates a branch pointing at it and moves HEAD to the branch, ending the detached state. git branch <name> creates the branch but leaves HEAD detached. git tag <name> creates a tag and also leaves HEAD detached.

If the working tree has already moved away, the object name has to be recovered first, and the manual page names git reflog for that, with git reflog -2 HEAD or git log -g -2 HEAD showing the last two commits HEAD referred to. The window for this is the garbage collection interval, not forever, which is the practical reason to create the branch as soon as the state is noticed rather than later.

The two commands that replaced it

Git now ships git switch and git restore, which split the two jobs apart. Both carried a notice in their documentation for years saying the command was experimental and the behaviour might change, and the current documentation for both no longer carries it. Anything pinned to an older Git is worth checking against the manual page shipped with that version rather than the one online.

git switch does the navigation half. Switching does not require a clean index and working tree, and the operation is aborted if it would lead to loss of local changes, unless --discard-changes or --merge says otherwise. -c creates a branch and switches to it, -C resets it if it already exists, and -f is documented as an alias for --discard-changes, which is a clearer name for what the flag does.

git restore does the file half, and its defaults are the improvement. Restoring the working tree is the default location, --staged restores the index instead, and both together restore both. With --staged the content comes from HEAD, otherwise from the index, and --source names a different commit. Nothing about git restore can move HEAD, which removes the entire class of mistake where a file name was meant and a branch was switched.

One shortcut survives in both. @{-N} refers to the N-th last branch or commit switched to, and - is a synonym for @{-1}, which is how a branch switch made by mistake gets undone in one keystroke.

What to change first

Start typing -- before file paths and use git restore when the intention is a file, because those two habits remove the only form of this command that destroys work with no way back. Seeing which files are actually modified in a folder view while running the command in the same window is what a file manager with a built-in terminal is for, which is the idea behind Atriens, and how it compares with the alternatives is set out on the comparison page.

Frequently asked questions

Does switching branches lose uncommitted changes?

No. The manual page states that local modifications to files in the working tree are kept when switching, so they can be committed to the branch being switched to. What can lose work is the pathspec form, git checkout -- <file>, which overwrites the working tree copy with the index copy and leaves no way back.

Why does git refuse to switch branches and say it would overwrite local changes?

Because the modified files differ between the current branch and the target branch, and the command refuses in order to preserve those modifications in context. --merge performs a three way merge and completes the switch, but the manual page warns that staged changes may be lost in that path, so committing first is the cautious route.

What is the difference between `git checkout -f` and `git checkout --merge`?

--merge keeps the local changes by merging them into the target branch. -f discards them, proceeding even if the index or working tree differs from HEAD and even if untracked files are in the way. Untracked files removed that way have never been recorded in Git, so nothing can restore them.

How do you recover commits made in detached HEAD state?

Create a reference to them. While still on the commit, git checkout -b <name> or git branch <name> or git tag <name> will do it. After moving away, git reflog -2 HEAD shows the commits HEAD recently pointed at so the object name can be recovered, and the manual page notes that unreferenced commits are eventually removed by routine garbage collection.

Should `git switch` and `git restore` be used instead of `git checkout`?

For interactive use they are the clearer pair, because they separate the two jobs: a file name can never be mistaken for a branch name, and git restore has no way to move HEAD at all. The experimental notice both commands once carried is gone from the current documentation, so the remaining caution is only about scripts that have to run against older Git versions.

Why does `--ours` give the wrong side during a rebase?

It does not, although it reads that way. During git rebase and git pull --rebase, --ours gives the version from the branch the changes are being rebased onto, and --theirs gives the version from the branch holding the work being rebased. The manual page explains that a rebase treats the remote history as the shared canonical one, so that is the side called ours.

Back to all posts