Git development
 help / color / mirror / Atom feed
From: "Julia Evans via GitGitGadget" <gitgitgadget@gmail.com>
To: git@vger.kernel.org
Cc: Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com>,
	Julia Evans <julia@jvns.ca>, Julia Evans <julia@jvns.ca>
Subject: [PATCH v2] doc: checkout: rewrite detached HEAD state explanation
Date: Tue, 13 Oct 2026 13:01:26 +0000	[thread overview]
Message-ID: <pull.2250.v2.git.1791896486100.gitgitgadget@gmail.com> (raw)
In-Reply-To: <pull.2250.git.1791384721919.gitgitgadget@gmail.com>

From: Julia Evans <julia@jvns.ca>

The current explanation has the following issues:

- Says detached HEAD state is useful but doesn't explain why
- Takes many paragraphs before explaining what detached HEAD state is
- It's common for users to accidentally end up in detached HEAD state,
  but it doesn't explain why that might happen
- One of the UI improvements in `git switch` is to make it harder
  to detach accidentally, but that isn't advertised.
  See 7968bef06b (switch: only allow explicit detached HEAD, 2019-03-29)
- Too many confusing diagrams

Write a new explanation addressing these issues, and put it in a
standalone guide so that we can easily reference it from advice
("see `git help detachedhead`").
The result is a shorter guide that covers more material.

The framing that "Git considers commits that aren't on a branch/other
reference to be garbage" is taken from Steve Klabnik's tutorial
https://steveklabnik.github.io/jujutsu-tutorial/branching-merging-and-conflicts/anonymous-branches.html.
It's funny and it's consistent with the way Git uses the term
"garbage collection".

Co-authored-by: Marie Claire LeBlanc Flanagan <hello@marieflanagan.com>
Signed-off-by: Julia Evans <julia@jvns.ca>
---
    doc: checkout: rewrite detached HEAD state explanation
    
    This is a pretty quick rewrite where we didn't get any user feedback. If
    we really want to make sure it's clear to users as possible, we should
    IMO do a second stage later where we ask users which parts are unclear.
    
    Changes in v2:
    
     * Make the meson.build and Makefile changes (thanks to Kristoffer)
     * Rename to gitdata-model (thanks to Kristoffer)
     * Mention commits will be deleted after 90 days to make it seem less
       scary (thanks to Kristoffer)
     * Add an "easily" qualifier to "it lets you look at an old version of
       your code", to avoid implying that this is the only way (thanks to
       Junio)
     * Mention that Git will warn you when switching branches if you're
       leaving commits behind (thanks to Junio)
     * Remove "unintentionally" (thanks to Junio)
     * Mention git help datamodel (thanks to Junio)
    
    Left the style comments alone (ideas for how to make it more clear) to
    be potentially addressed in v3, after we finalize the information
    content.

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-2250%2Fjvns%2Fdetached-head-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-2250/jvns/detached-head-v2
Pull-Request: https://github.com/gitgitgadget/git/pull/2250

Range-diff vs v1:

 1:  efe0f9db36 ! 1:  83139be46f doc: checkout: rewrite detached HEAD state explanation
     @@ Commit message
          Co-authored-by: Marie Claire LeBlanc Flanagan <hello@marieflanagan.com>
          Signed-off-by: Julia Evans <julia@jvns.ca>
      
     + ## Documentation/Makefile ##
     +@@ Documentation/Makefile: MAN7_TXT += gitcore-tutorial.adoc
     + MAN7_TXT += gitcredentials.adoc
     + MAN7_TXT += gitcvs-migration.adoc
     + MAN7_TXT += gitdatamodel.adoc
     ++MAN7_TXT += gitdetached-head.adoc
     + MAN7_TXT += gitdiffcore.adoc
     + MAN7_TXT += giteveryday.adoc
     + MAN7_TXT += gitfaq.adoc
     +
       ## Documentation/detached-head.adoc (new) ##
      @@
      +`HEAD` is where Git stores your current branch. `HEAD` can either be:
     @@ Documentation/detached-head.adoc (new)
      +   This is called "detached HEAD state".
      +
      +It can sometimes be useful for `HEAD` to be a commit ID.
     -+For example, it lets you look at an old version of your code
     -+(with `git checkout COMMIT_ID`).
     ++For example, it lets you easily look at an old version of your code
     ++(with `git checkout COMMIT_ID` or `git switch --detach COMMIT_ID`).
      +
      +The only problem is that if you create new commits while in detached
      +HEAD state, those commits won't be on a branch. This makes those new
      +commits much harder to find later. Also, Git considers commits that
      +aren't on any branch (or a tag or other reference) to be garbage.
      +Git will eventually permanently delete those "garbage" commits during
     -+garbage collection.
     ++garbage collection. Those commits will be deleted after 90 days by
     ++default, see linkgit:git-gc[1] for more.
      +
     -+There are 3 main ways you can end up in detached HEAD state
     -+unintentionally:
     ++There are 3 main ways you can end up in detached HEAD state:
      +
      +1. `git checkout COMMIT_ID`, where `COMMIT_ID` is a commit ID
      +2. `git checkout v1.3`, where v1.3 is a tag name
     @@ Documentation/detached-head.adoc (new)
      +   You might want to do this if you've created new commits, so that you can
      +   find the commit later and so that it won't be garbage collected.
      +
     -+If you create commits in detached HEAD state that aren't on a branch,
     -+you can find them later using linkgit:git-reflog[1].
     ++If you switch branches after making commits in detached HEAD state, Git
     ++will warn you that you might be about to "lose" those commits and print
     ++out the commit ID. You can also always find those commits later using
     ++linkgit:git-reflog[1].
     ++
     ++Here's what that warning looks like:
     ++
     ++-------------------------
     ++Warning: you are leaving 1 commit behind, not connected to
     ++any of your branches:
     ++
     ++  48c09f6 3
     ++-------------------------
     ++
     ++For a more in-depth explanation of how `HEAD` works in Git,
     ++see linkgit:gitdatamodel[7] (`git help datamodel`).
      
       ## Documentation/git-checkout.adoc ##
      @@ Documentation/git-checkout.adoc: For more details, see the 'pathspec' entry in linkgit:gitglossary[7].
     @@ Documentation/git-checkout.adoc: For more details, see the 'pathspec' entry in l
       [[ARGUMENT_DISAMBIGUATION]]
       ARGUMENT DISAMBIGUATION
      
     - ## Documentation/gitdetachedhead.adoc (new) ##
     + ## Documentation/gitdetached-head.adoc (new) ##
      @@
     -+gitdetachedhead(7)
     -+===============
     ++gitdetached-head(7)
     ++===================
      +
      +NAME
      +----
     -+gitdetachedhead - How detached HEAD state works
     ++gitdetached-head - How detached HEAD state works
      +
      +DESCRIPTION
      +-----------
     @@ Documentation/gitdetachedhead.adoc (new)
      +---
      +Part of the linkgit:git[1] suite
      
     + ## Documentation/meson.build ##
     +@@ Documentation/meson.build: manpages = {
     +   'gitcredentials.adoc' : 7,
     +   'gitcvs-migration.adoc' : 7,
     +   'gitdatamodel.adoc' : 7,
     ++  'gitdetached-head.adoc' : 7,
     +   'gitdiffcore.adoc' : 7,
     +   'giteveryday.adoc' : 7,
     +   'gitfaq.adoc' : 7,
     +
       ## advice.c ##
      @@ advice.c: void detach_advice(const char *new_name)
       	"\n"
     @@ command-list.txt: gitcore-tutorial                        guide
       gitcredentials                          guide
       gitcvs-migration                        guide
       gitdatamodel                            guide
     -+gitdetachedhead                         guide
     ++gitdetached-head                         guide
       gitdiffcore                             guide
       giteveryday                             guide
       gitfaq                                  guide


 Documentation/Makefile              |   1 +
 Documentation/detached-head.adoc    |  57 ++++++++++++
 Documentation/git-checkout.adoc     | 129 +---------------------------
 Documentation/gitdetached-head.adoc |  14 +++
 Documentation/meson.build           |   1 +
 advice.c                            |   1 +
 command-list.txt                    |   1 +
 7 files changed, 76 insertions(+), 128 deletions(-)
 create mode 100644 Documentation/detached-head.adoc
 create mode 100644 Documentation/gitdetached-head.adoc

diff --git a/Documentation/Makefile b/Documentation/Makefile
index f8dea4b395..5892f48b06 100644
--- a/Documentation/Makefile
+++ b/Documentation/Makefile
@@ -54,6 +54,7 @@ MAN7_TXT += gitcore-tutorial.adoc
 MAN7_TXT += gitcredentials.adoc
 MAN7_TXT += gitcvs-migration.adoc
 MAN7_TXT += gitdatamodel.adoc
+MAN7_TXT += gitdetached-head.adoc
 MAN7_TXT += gitdiffcore.adoc
 MAN7_TXT += giteveryday.adoc
 MAN7_TXT += gitfaq.adoc
diff --git a/Documentation/detached-head.adoc b/Documentation/detached-head.adoc
new file mode 100644
index 0000000000..712074c0ef
--- /dev/null
+++ b/Documentation/detached-head.adoc
@@ -0,0 +1,57 @@
+`HEAD` is where Git stores your current branch. `HEAD` can either be:
+
+1. A branch, which is your current branch.
+2. A commit ID, when you don't have a current branch.
+   This is called "detached HEAD state".
+
+It can sometimes be useful for `HEAD` to be a commit ID.
+For example, it lets you easily look at an old version of your code
+(with `git checkout COMMIT_ID` or `git switch --detach COMMIT_ID`).
+
+The only problem is that if you create new commits while in detached
+HEAD state, those commits won't be on a branch. This makes those new
+commits much harder to find later. Also, Git considers commits that
+aren't on any branch (or a tag or other reference) to be garbage.
+Git will eventually permanently delete those "garbage" commits during
+garbage collection. Those commits will be deleted after 90 days by
+default, see linkgit:git-gc[1] for more.
+
+There are 3 main ways you can end up in detached HEAD state:
+
+1. `git checkout COMMIT_ID`, where `COMMIT_ID` is a commit ID
+2. `git checkout v1.3`, where v1.3 is a tag name
+3. `git checkout origin/main`, where `origin/main` is
+   a remote-tracking branch
+
+Checking out a tag puts you in detached HEAD state because `HEAD` can
+only be a branch or a commit, not a tag or any other reference.
+So `git checkout TAG` will set HEAD to the commit for that tag.
+
+The easiest way to avoid accidentally ending up in detached HEAD state
+is to use linkgit:git-switch[1] instead of linkgit:git-checkout[1] to
+switch branches. `git switch` won't let you detach unless you explicitly
+pass the `--detach` argument.
+
+To get back onto a branch, you can:
+
+1. Switch to the branch you want to be on, with `git switch BRANCHNAME`.
+2. Create a new branch at the current commit, with `git switch -c BRANCHNAME`.
+   You might want to do this if you've created new commits, so that you can
+   find the commit later and so that it won't be garbage collected.
+
+If you switch branches after making commits in detached HEAD state, Git
+will warn you that you might be about to "lose" those commits and print
+out the commit ID. You can also always find those commits later using
+linkgit:git-reflog[1].
+
+Here's what that warning looks like:
+
+-------------------------
+Warning: you are leaving 1 commit behind, not connected to
+any of your branches:
+
+  48c09f6 3
+-------------------------
+
+For a more in-depth explanation of how `HEAD` works in Git,
+see linkgit:gitdatamodel[7] (`git help datamodel`).
diff --git a/Documentation/git-checkout.adoc b/Documentation/git-checkout.adoc
index 2aefea0228..4e9e94e24d 100644
--- a/Documentation/git-checkout.adoc
+++ b/Documentation/git-checkout.adoc
@@ -376,135 +376,8 @@ For more details, see the 'pathspec' entry in linkgit:gitglossary[7].
 [[DETACHED_HEAD]]
 DETACHED HEAD
 -------------
-`HEAD` normally refers to a named branch (e.g. `master`). Meanwhile, each
-branch refers to a specific commit. Let's look at a repo with three
-commits, one of them tagged, and with branch `master` checked out:
 
-------------
-           HEAD (refers to branch 'master')
-            |
-            v
-a---b---c  branch 'master' (refers to commit 'c')
-    ^
-    |
-  tag 'v2.0' (refers to commit 'b')
-------------
-
-When a commit is created in this state, the branch is updated to refer to
-the new commit. Specifically, `git commit` creates a new commit `d`, whose
-parent is commit `c`, and then updates branch `master` to refer to new
-commit `d`. `HEAD` still refers to branch `master` and so indirectly now refers
-to commit `d`:
-
-------------
-$ edit; git add; git commit
-
-               HEAD (refers to branch 'master')
-                |
-                v
-a---b---c---d  branch 'master' (refers to commit 'd')
-    ^
-    |
-  tag 'v2.0' (refers to commit 'b')
-------------
-
-It is sometimes useful to be able to checkout a commit that is not at
-the tip of any named branch, or even to create a new commit that is not
-referenced by a named branch. Let's look at what happens when we
-checkout commit `b` (here we show two ways this may be done):
-
-------------
-$ git checkout v2.0  # or
-$ git checkout master^^
-
-   HEAD (refers to commit 'b')
-    |
-    v
-a---b---c---d  branch 'master' (refers to commit 'd')
-    ^
-    |
-  tag 'v2.0' (refers to commit 'b')
-------------
-
-Notice that regardless of which checkout command we use, `HEAD` now refers
-directly to commit `b`. This is known as being in detached `HEAD` state.
-It means simply that `HEAD` refers to a specific commit, as opposed to
-referring to a named branch. Let's see what happens when we create a commit:
-
-------------
-$ edit; git add; git commit
-
-     HEAD (refers to commit 'e')
-      |
-      v
-      e
-     /
-a---b---c---d  branch 'master' (refers to commit 'd')
-    ^
-    |
-  tag 'v2.0' (refers to commit 'b')
-------------
-
-There is now a new commit `e`, but it is referenced only by `HEAD`. We can
-of course add yet another commit in this state:
-
-------------
-$ edit; git add; git commit
-
-	 HEAD (refers to commit 'f')
-	  |
-	  v
-      e---f
-     /
-a---b---c---d  branch 'master' (refers to commit 'd')
-    ^
-    |
-  tag 'v2.0' (refers to commit 'b')
-------------
-
-In fact, we can perform all the normal Git operations. But, let's look
-at what happens when we then checkout `master`:
-
-------------
-$ git checkout master
-
-               HEAD (refers to branch 'master')
-      e---f     |
-     /          v
-a---b---c---d  branch 'master' (refers to commit 'd')
-    ^
-    |
-  tag 'v2.0' (refers to commit 'b')
-------------
-
-It is important to realize that at this point nothing refers to commit
-`f`. Eventually commit `f` (and by extension commit `e`) will be deleted
-by the routine Git garbage collection process, unless we create a reference
-before that happens. If we have not yet moved away from commit `f`,
-any of these will create a reference to it:
-
-------------
-$ git checkout -b foo  # or "git switch -c foo"  <1>
-$ git branch foo                                 <2>
-$ git tag foo                                    <3>
-------------
-<1> creates a new branch `foo`, which refers to commit `f`, and then
-    updates `HEAD` to refer to branch `foo`. In other words, we'll no longer
-    be in detached `HEAD` state after this command.
-<2> similarly creates a new branch `foo`, which refers to commit `f`,
-    but leaves `HEAD` detached.
-<3> creates a new tag `foo`, which refers to commit `f`,
-    leaving `HEAD` detached.
-
-If we have moved away from commit `f`, then we must first recover its object
-name (typically by using git reflog), and then we can create a reference to
-it. For example, to see the last two commits to which `HEAD` referred, we
-can use either of these commands:
-
-------------
-$ git reflog -2 HEAD # or
-$ git log -g -2 HEAD
-------------
+include::detached-head.adoc[]
 
 [[ARGUMENT_DISAMBIGUATION]]
 ARGUMENT DISAMBIGUATION
diff --git a/Documentation/gitdetached-head.adoc b/Documentation/gitdetached-head.adoc
new file mode 100644
index 0000000000..a477060db2
--- /dev/null
+++ b/Documentation/gitdetached-head.adoc
@@ -0,0 +1,14 @@
+gitdetached-head(7)
+===================
+
+NAME
+----
+gitdetached-head - How detached HEAD state works
+
+DESCRIPTION
+-----------
+include::detached-head.adoc[]
+
+GIT
+---
+Part of the linkgit:git[1] suite
diff --git a/Documentation/meson.build b/Documentation/meson.build
index cfa9c67609..6b3141647f 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -197,6 +197,7 @@ manpages = {
   'gitcredentials.adoc' : 7,
   'gitcvs-migration.adoc' : 7,
   'gitdatamodel.adoc' : 7,
+  'gitdetached-head.adoc' : 7,
   'gitdiffcore.adoc' : 7,
   'giteveryday.adoc' : 7,
   'gitfaq.adoc' : 7,
diff --git a/advice.c b/advice.c
index 401d047391..43f86c2eaf 100644
--- a/advice.c
+++ b/advice.c
@@ -291,6 +291,7 @@ void detach_advice(const char *new_name)
 	"\n"
 	"  git switch -\n"
 	"\n"
+	"Run `git help detachedhead` to learn more.\n"
 	"Turn off this advice by setting config variable advice.detachedHead to false\n\n");
 
 	fprintf(stderr, fmt, new_name);
diff --git a/command-list.txt b/command-list.txt
index 63ae2a67c9..922a1dbd84 100644
--- a/command-list.txt
+++ b/command-list.txt
@@ -218,6 +218,7 @@ gitcore-tutorial                        guide
 gitcredentials                          guide
 gitcvs-migration                        guide
 gitdatamodel                            guide
+gitdetached-head                         guide
 gitdiffcore                             guide
 giteveryday                             guide
 gitfaq                                  guide

base-commit: 5a7d1e8045ce66c908f62598e26cbb8df7b39a90
-- 
gitgitgadget

      parent reply	other threads:[~2026-10-13 13:01 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-07 14:52 [PATCH] doc: checkout: rewrite detached HEAD state explanation Julia Evans via GitGitGadget
2026-10-07 16:43 ` Junio C Hamano
2026-10-13 12:54   ` Julia Evans
2026-10-07 19:02 ` Kristoffer Haugsbakk
2026-10-13 11:48   ` Julia Evans
2026-10-13 12:20     ` Kristoffer Haugsbakk
2026-10-13 12:27       ` Julia Evans
2026-10-13 12:42         ` Julia Evans
2026-10-13 13:01 ` Julia Evans via GitGitGadget [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=pull.2250.v2.git.1791896486100.gitgitgadget@gmail.com \
    --to=gitgitgadget@gmail.com \
    --cc=git@vger.kernel.org \
    --cc=julia@jvns.ca \
    --cc=kristofferhaugsbakk@fastmail.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox