On this page
The bug report names a release: v1.3.0 writes export timestamps in
local time, so the same CSV reads differently on every machine that
opens it. Your main has moved on since that tag went out three
weeks ago. To see exactly what customers are running, you decide to
check out the tag itself. Here is the repository the morning this starts:
$ git log --oneline --decorate -4
f56f091 (HEAD -> main, origin/main, origin/HEAD) feat: json export target
d6d32d7 (tag: v1.3.0) feat: timestamp column for exported rows
6af6ae3 fix: quote fields containing the delimiter
05cafe3 feat: csv export with column mapping
One checkout from here, Git is going to print the most quoted paragraph it ships. To read that paragraph calmly, start with the thing it is about.
Where HEAD points on a normal day
Git keeps your position in a file. On a branch, that file holds a branch name, and the branch holds the commit:
$ git symbolic-ref HEAD
refs/heads/main
$ cat .git/HEAD
ref: refs/heads/main
When you commit, main advances and HEAD rides along by name.
(cat .git/HEAD assumes the main checkout; a linked
worktree keeps its own HEAD
elsewhere, at the path git rev-parse --git-path HEAD prints.)
Everything Git does with the word “branch” runs through that one
level of indirection, and that indirection is exactly what you are
about to remove.
What the paragraph actually says
$ git checkout v1.3.0
Note: switching to 'v1.3.0'.
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>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at d6d32d7 feat: timestamp column for exported rows
Read it in the order Git wrote it: permission first. You can look
around, you can make experimental changes, you can commit them.
Nothing here is damaged and nothing needs rescuing; the paragraph
even tells you how to turn it off. It exists because of one
mechanical change, and git status names where that change happened:
$ git status
HEAD detached at v1.3.0
nothing to commit, working tree clean
Status keeps naming the thing you checked out, a tag here, a short hash or a remote-tracking ref when that is what you typed. The mechanical change itself is in the file from the previous section:
$ cat .git/HEAD
d6d32d74f99e436d40b659ba94715a5688a1fa0d
$ git symbolic-ref HEAD
fatal: ref HEAD is not a symbolic ref
HEAD no longer holds a branch name. It holds a commit directly,
and git symbolic-ref states the definition in the negative: there
is no symbolic ref to resolve. That is all “detached” means. The
file that normally points at a name now points at a hash.
The first consequence shows up in commands that orient themselves by your current branch. A pull merges into the branch you are on; with no branch, it refuses to guess:
$ git pull
You are not currently on a branch.
Please specify which branch you want to merge with.
See git-pull(1) for details.
git pull <remote> <branch>
(git push refuses in the same spirit.)
Committing with no branch under you
The timestamp bug turns out to be one line in exporter.js, and you
fix it right there, standing on the release. Git takes the commit
without complaint; look at the label it prints where a branch
name usually goes:
$ git commit -am "fix: write export timestamps in UTC"
[detached HEAD 86c9c4d] fix: write export timestamps in UTC
1 file changed, 1 insertion(+), 1 deletion(-)
[detached HEAD 86c9c4d] where [main 86c9c4d] would normally be.
The commit is real and stored like any other. What changed is the
bookkeeping: no branch advanced to include it.
$ git status
HEAD detached from v1.3.0
nothing to commit, working tree clean
Status flips its preposition. “At v1.3.0” meant you stood exactly where you detached; “from v1.3.0” means you have moved since. The anchor stays put so you can always see where you came in. And the log shows the situation plainly:
$ git log --oneline --decorate -3
86c9c4d (HEAD) fix: write export timestamps in UTC
d6d32d7 (tag: v1.3.0) feat: timestamp column for exported rows
6af6ae3 fix: quote fields containing the delimiter
HEAD sits on your fix with no branch name beside it. The tag still
points where it always did. Nothing is following you.
The warning on the way out
The fix verifies, and you switch back to main to turn it into a
proper branch. This is the moment Git gets loud:
$ git switch main
Warning: you are leaving 1 commit behind, not connected to
any of your branches:
86c9c4d fix: write export timestamps in UTC
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> 86c9c4d
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
Look at what the warning actually does: it counts the commits, lists
them by hash and subject, and closes with the git branch command
that keeps them, waiting only on a branch name. Nothing was deleted
in this moment. The commit simply stopped being listed; no branch
or tag names it, so git log on any branch will not show it. And
suppose the terminal is gone, warning and hash with it. HEAD’s reflog
kept a line for each move it made, the fix commit included; this
is the same machinery that undoes a hard reset:
$ git reflog -3
f56f091 HEAD@{0}: checkout: moving from 86c9c4dcfba8685a14af31dda9600d5ac7660c5e to main
86c9c4d HEAD@{1}: commit: fix: write export timestamps in UTC
d6d32d7 HEAD@{2}: checkout: moving from main to v1.3.0
Three entries is enough here because the exit just happened. Every
move HEAD makes afterward pushes the entry deeper, so on a day-old
mess drop the -3 and look for checkout: moving from followed by
a full hash rather than a branch name: that hash is the commit
you left behind. Either way, the rescue is the command Git
suggested: a branch created at the hash.
$ git branch fix-utc-timestamps 86c9c4d
$ git log --oneline --decorate -1 fix-utc-timestamps
86c9c4d (fix-utc-timestamps) fix: write export timestamps in UTC
The commit did not move and did not change. It gained a name, and the logs that walk that name now reach it.
The commands that ask first
git checkout detaches without asking because it has two jobs,
switching branches and restoring files, and pointing it at anything
that is not a branch slides into the detached state as a side
effect. Git later split those jobs between git switch and
git restore, and git switch treats detaching as something you
have to ask for:
$ git switch v1.3.0
fatal: a branch is expected, got tag 'v1.3.0'
hint: If you want to detach HEAD at the commit, try again with the --detach option.
$ git switch origin/main
fatal: a branch is expected, got remote branch 'origin/main'
hint: If you want to detach HEAD at the commit, try again with the --detach option.
Refusal plus hint is the famous paragraph compressed to two lines:
if you wanted a branch, name one; if you want the detached state,
say --detach. The classic accident this catches is
git checkout origin/main. A remote-tracking ref is not a branch
you can be on, so checkout detaches you there and prints the same
paragraph, while switch stops the typo at the door.
And when the plan was always to come back and fix something, take the name with you in one move:
$ git switch -c hotfix-1.3.1 v1.3.0
Switched to a new branch 'hotfix-1.3.1'
A branch born at the tag, no detached state at any point, and every
commit you make lands somewhere listed from the first one. The
git switch - the paragraph advertises means wherever you stood
last; from the new branch it hops back to main:
$ git switch -
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
Or don’t do any of this
GitDesktop’s rule for detached HEAD is to say the name everywhere. Checking out a commit or a tag asks first: “Check out tag v1.3.0?”, with the move spelled out underneath: “Your files move to that tag and HEAD detaches, so you won’t be on a branch. Switching back to a branch returns everything to normal, and commits you make while detached need a new branch to keep them.” Git prints its paragraph after you have arrived; a client gets to ask before you go.
Confirm it and the toast answers in the same vocabulary: “Checked
out v1.3.0 — HEAD is detached”. The branch switcher in the header
drops its branch name for “detached @ d6d32d7” and pins a “detached”
badge beside it, so the state stays on screen for as long as you are
in it. Under the hood the checkout runs git switch --detach on the
resolved commit, the same opt-in form the terminal uses. And the
branch actions that need a current branch do not disappear while
you are detached; rename, delete, and update each stay visible and
say why they are held: “HEAD is detached — there’s no current branch
to rename.” The state is fine to work in. The product’s job is to
make sure you always know you are in it.
Git will take your commits without a branch. It asks for a name only when you turn to leave.
Written while building GitDesktop.
A desktop Git client for GitHub, GitLab and Bitbucket — free and open source, with AI you control or hide entirely.