git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v2] worktree: update is_bare heuristics

From
JTJonathan Tan <jonathantanmy@google.com>
Date
Apr 19, 2019, 17:21 UTC
Message-ID
<20190419172128.130170-1-jonathantanmy@google.com>
In-Reply-To
<20190417212128.52475-1-jonathantanmy@google.com>

When "git branch -D <name>" is run, Git usually first checks if that branch is currently checked out. But this check is not performed if the Git directory of that repository is not at "<repo>/.git", which is the case if that repository is a submodule that has its Git directory stored as "super/.git/modules/<repo>", for example. This results in the branch being deleted even though it is checked out.

This is because get_main_worktree() in worktree.c sets is_bare on a worktree only using the heuristic that a repo is bare if the worktree's path does not end in "/.git", and not bare otherwise. This is_bare code was introduced in 92718b7438 ("worktree: add details to the worktree struct", 2015-10-08), following a pre-core.bare heuristic. This patch does 2 things:

 - Teach get_main_worktree() to use is_bare_repository() instead,
   introduced in 7d1864ce67 ("Introduce is_bare_repository() and
   core.bare configuration variable", 2007-01-07) and updated in
   e90fdc39b6 ("Clean up work-tree handling", 2007-08-01). This solves
   the "git branch -D <name>" problem described above. However...
 - If a repository has core.bare=1 but the "git" command is being run
   from one of its secondary worktrees, is_bare_repository() returns
   false (which is fine, since there is a worktree available). However,
   treating the main worktree as non-bare when it is bare causes issues:
   for example, failure to delete a branch from a secondary worktree
   that is referred to by a main worktree's HEAD, even if that main
   worktree is bare.
   In order to avoid that, also check core.bare when setting is_bare. If
   core.bare=1, trust it, and otherwise, use is_bare_repository().
Signed-off-by: Jonathan Tan <jonathantanmy@google.com>
---
Thanks, everyone, for your reviews.
Changes:
 - s/ends/does not end/ in commit message (thanks, Duy)
 - better rationale for using (is_bare_repository_cfg == 1) in commit
   message, and added test to check that
 - added NEEDSWORK
Range-diff against v1:
1:  d0934b13ae ! 1:  62be513269 worktree: update is_bare heuristics
    @@ -6,12 +6,13 @@
         branch is currently checked out. But this check is not performed if the
         Git directory of that repository is not at "<repo>/.git", which is the
         case if that repository is a submodule that has its Git directory stored
    -    as "super/.git/modules/<repo>", for example.
    +    as "super/.git/modules/<repo>", for example. This results in the branch
    +    being deleted even though it is checked out.
     
         This is because get_main_worktree() in worktree.c sets is_bare on a
         worktree only using the heuristic that a repo is bare if the worktree's
    -    path ends in "/.git", and not bare otherwise. This is_bare code was
    -    introduced in 92718b7438 ("worktree: add details to the worktree
    +    path does not end in "/.git", and not bare otherwise. This is_bare code
    +    was introduced in 92718b7438 ("worktree: add details to the worktree
         struct", 2015-10-08), following a pre-core.bare heuristic. This patch
         does 2 things:
     
    @@ -22,12 +23,14 @@
            the "git branch -D <name>" problem described above. However...
     
          - If a repository has core.bare=1 but the "git" command is being run
    -       from one of its added worktrees, is_bare_repository() returns false
    -       (which is fine, since there is a worktree available). However,
    -       commands like "git worktree list" currently distinguish between a
    -       repository that has core.bare=1 but has a worktree available, and a
    -       repository that is truly non-bare (core.bare=0). To preserve this
    -       distinction, also check core.bare when setting is_bare. If
    +       from one of its secondary worktrees, is_bare_repository() returns
    +       false (which is fine, since there is a worktree available). However,
    +       treating the main worktree as non-bare when it is bare causes issues:
    +       for example, failure to delete a branch from a secondary worktree
    +       that is referred to by a main worktree's HEAD, even if that main
    +       worktree is bare.
    +
    +       In order to avoid that, also check core.bare when setting is_bare. If
            core.bare=1, trust it, and otherwise, use is_bare_repository().
     
         Signed-off-by: Jonathan Tan <jonathantanmy@google.com>
    @@ -52,6 +55,16 @@
     +	git -C repo2/sub checkout -b work &&
     +	test_must_fail git -C repo2/sub branch -D work
     +'
    ++
    ++test_expect_success 'bare main worktree has HEAD at branch deleted by secondary worktree' '
    ++	test_when_finished "rm -rf nonbare base secondary" &&
    ++
    ++	git init nonbare &&
    ++	test_commit -C nonbare x &&
    ++	git clone --bare nonbare bare &&
    ++	git -C bare worktree add --detach ../secondary master &&
    ++	git -C secondary branch -D master
    ++'
     +
      test_expect_success 'git branch --list -v with --abbrev' '
      	test_when_finished "git branch -D t" &&
    @@ -77,6 +90,13 @@
      	worktree = xcalloc(1, sizeof(*worktree));
      	worktree->path = strbuf_detach(&worktree_path, NULL);
     -	worktree->is_bare = is_bare;
    ++	/*
    ++	 * NEEDSWORK: If this function is called from a secondary worktree and
    ++	 * config.worktree is present, is_bare_repository_cfg will reflect the
    ++	 * contents of config.worktree, not the contents of the main worktree.
    ++	 * This means that worktree->is_bare may be set to 0 even if the main
    ++	 * worktree is configured to be bare.
    ++	 */
     +	worktree->is_bare = (is_bare_repository_cfg == 1) ||
     +		is_bare_repository();
      	add_head_info(worktree);
 t/t3200-branch.sh | 24 ++++++++++++++++++++++++
 worktree.c        | 14 ++++++++++----
 2 files changed, 34 insertions(+), 4 deletions(-)
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index 478b82cf9b..e9ad50b66d 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -264,6 +264,30 @@ test_expect_success 'git branch --list -d t should fail' '
 	test_must_fail git rev-parse refs/heads/t
 '
 
+test_expect_success 'deleting checked-out branch from repo that is a submodule' '
+	test_when_finished "rm -rf repo1 repo2" &&
+
+	git init repo1 &&
+	git init repo1/sub &&
+	test_commit -C repo1/sub x &&
+	git -C repo1 submodule add ./sub &&
+	git -C repo1 commit -m "adding sub" &&
+
+	git clone --recurse-submodules repo1 repo2 &&
+	git -C repo2/sub checkout -b work &&
+	test_must_fail git -C repo2/sub branch -D work
+'
+
+test_expect_success 'bare main worktree has HEAD at branch deleted by secondary worktree' '
+	test_when_finished "rm -rf nonbare base secondary" &&
+
+	git init nonbare &&
+	test_commit -C nonbare x &&
+	git clone --bare nonbare bare &&
+	git -C bare worktree add --detach ../secondary master &&
+	git -C secondary branch -D master
+'
+
 test_expect_success 'git branch --list -v with --abbrev' '
 	test_when_finished "git branch -D t" &&
 	git branch t &&
diff --git a/worktree.c b/worktree.c
index b45bfeb9d3..4f66cd9ce1 100644
--- a/worktree.c
+++ b/worktree.c
@@ -49,18 +49,24 @@ static struct worktree *get_main_worktree(void)
 	struct worktree *worktree = NULL;
 	struct strbuf path = STRBUF_INIT;
 	struct strbuf worktree_path = STRBUF_INIT;
-	int is_bare = 0;
 
 	strbuf_add_absolute_path(&worktree_path, get_git_common_dir());
-	is_bare = !strbuf_strip_suffix(&worktree_path, "/.git");
-	if (is_bare)
+	if (!strbuf_strip_suffix(&worktree_path, "/.git"))
 		strbuf_strip_suffix(&worktree_path, "/.");
 
 	strbuf_addf(&path, "%s/HEAD", get_git_common_dir());
 
 	worktree = xcalloc(1, sizeof(*worktree));
 	worktree->path = strbuf_detach(&worktree_path, NULL);
-	worktree->is_bare = is_bare;
+	/*
+	 * NEEDSWORK: If this function is called from a secondary worktree and
+	 * config.worktree is present, is_bare_repository_cfg will reflect the
+	 * contents of config.worktree, not the contents of the main worktree.
+	 * This means that worktree->is_bare may be set to 0 even if the main
+	 * worktree is configured to be bare.
+	 */
+	worktree->is_bare = (is_bare_repository_cfg == 1) ||
+		is_bare_repository();
 	add_head_info(worktree);
 
 	strbuf_release(&path);
-- 
2.21.0.392.gf8f6787159e-goog
Previous: Duy Nguyen
Message 10 of 10 in “worktree: update is_bare heuristics”
  1. worktree: update is_bare heuristicsJonathan Tan, Apr 17, 2019
  2. Junio C HamanoApr 18, 2019
  3. Johannes SchindelinApr 18, 2019
  4. Jonathan TanApr 18, 2019
  5. Duy NguyenApr 18, 2019
  6. Jonathan TanApr 18, 2019
  7. Jeff KingApr 18, 2019
  8. Duy NguyenApr 19, 2019
  9. Duy NguyenApr 19, 2019
  10. worktree: update is_bare heuristicsJonathan Tan, Apr 19, 2019

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.