worktree: update is_bare heuristics
diff mbox series

Message ID 20190417212128.52475-1-jonathantanmy@google.com
State New
Headers show
Series
  • worktree: update is_bare heuristics
Related show

Commit Message

Jonathan Tan April 17, 2019, 9:21 p.m. UTC
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 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
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 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
   core.bare=1, trust it, and otherwise, use is_bare_repository().

Signed-off-by: Jonathan Tan <jonathantanmy@google.com>
---
 t/t3200-branch.sh | 14 ++++++++++++++
 worktree.c        |  7 +++----
 2 files changed, 17 insertions(+), 4 deletions(-)

Comments

Junio C Hamano April 18, 2019, 2:16 a.m. UTC | #1
Jonathan Tan <jonathantanmy@google.com> writes:

[jc: pinging the current area expert for the multiple worktree setup
for sanity checking]

> 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 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
> 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 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
>    core.bare=1, trust it, and otherwise, use is_bare_repository().

I am not sure if the latter is worth keeping, but the former
definitely is a huge improvement, I would imagine.  Somebody
did "git branch -D <that-branch>" by accident in a submodule
checkout, or something?

> +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
> +'

That is a quite straightforward reproduction recipe.  Very good.

> diff --git a/worktree.c b/worktree.c
> index b45bfeb9d3..5d52b2ba53 100644
> --- a/worktree.c
> +++ b/worktree.c
> @@ -49,18 +49,17 @@ 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, "/.");

So, we'd drop /.git or /. from the end, without any behaviour
change.  But we no longer take the fact that it was not the ".git"
subdirectory into account, as that is unreliable?

>  	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;
> +	worktree->is_bare = (is_bare_repository_cfg == 1) ||
> +		is_bare_repository();

And instead core.bare being '1' is used as the definite sign
that we are dealing with a bare repository.

It all makes sense to me.

Thanks for working on it.

>  	add_head_info(worktree);
>  
>  	strbuf_release(&path);
Johannes Schindelin April 18, 2019, 9:59 a.m. UTC | #2
Hi,

On Thu, 18 Apr 2019, Junio C Hamano wrote:

> Jonathan Tan <jonathantanmy@google.com> writes:
>
> > 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 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
> > 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 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
> >    core.bare=1, trust it, and otherwise, use is_bare_repository().
>
> I am not sure if the latter is worth keeping, but the former
> definitely is a huge improvement, I would imagine.

I think that both are improvements, with the former definitely being the
more impactful one.

> Somebody did "git branch -D <that-branch>" by accident in a submodule
> checkout, or something?

/me shudders at the thought of it

The patch makes tons of sense to me.

Thank you,
Dscho
Duy Nguyen April 18, 2019, 10:11 a.m. UTC | #3
On Thu, Apr 18, 2019 at 4:22 AM Jonathan Tan <jonathantanmy@google.com> wrote:
>
> 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 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
> 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...

You actually didn't spell out the problem with "git branch -D", or at
least the consequence (i.e. the submodule branch is deleted even if
it's checked out).

>  - 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
>    core.bare=1, trust it, and otherwise, use is_bare_repository().
>
> Signed-off-by: Jonathan Tan <jonathantanmy@google.com>
> ---
>  t/t3200-branch.sh | 14 ++++++++++++++
>  worktree.c        |  7 +++----
>  2 files changed, 17 insertions(+), 4 deletions(-)
>
> diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
> index 478b82cf9b..db5c411e76 100755
> --- a/t/t3200-branch.sh
> +++ b/t/t3200-branch.sh
> @@ -264,6 +264,20 @@ 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 '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..5d52b2ba53 100644
> --- a/worktree.c
> +++ b/worktree.c
> @@ -49,18 +49,17 @@ 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, "/.");

We can just call these two calls unconditionally, right? No harm done
if we don't strip.

>
>         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;
> +       worktree->is_bare = (is_bare_repository_cfg == 1) ||

core.bare and core.worktree are special. When you access them standing
from the main worktree, you'll see them. But when you stand from a
secondary worktree, they are ignored. It's more obvious with
core.worktree because if that affects all worktrees, what's the point
of having multiple worktrees. Git will always go to the place
core.worktree points out.

So if this function is called from a secondary worktree, I'm not sure
if it still works as expected because is_bare_repo may be false then.
For the submodule case, you always stand at the submodule's main
worktree, so it still works.

I don't think multiple-worktrees-on-submodules will be coming soon, so
it's probably ok. But maybe leave a note here.

> +               is_bare_repository();
>         add_head_info(worktree);
>
>         strbuf_release(&path);
> --
> 2.21.0.392.gf8f6787159e-goog
>
Jonathan Tan April 18, 2019, 6:30 p.m. UTC | #4
> You actually didn't spell out the problem with "git branch -D", or at
> least the consequence (i.e. the submodule branch is deleted even if
> it's checked out).

Thanks - I'll do that in the commit message.

> >         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, "/.");
> 
> We can just call these two calls unconditionally, right? No harm done
> if we don't strip.

We can, and no harm done. But this if/then pattern is also repeated in
other parts of the file (e.g. get_linked_worktree()) so I'll leave it in
for consistency. (Also, for what it's worth, it's slightly faster if
only one strip is done.)

> >         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;
> > +       worktree->is_bare = (is_bare_repository_cfg == 1) ||
> 
> core.bare and core.worktree are special. When you access them standing
> from the main worktree, you'll see them. But when you stand from a
> secondary worktree, they are ignored.

Just checking: I think that is_bare_repository_cfg ignores core.bare
only if the config.worktree file is present? In the t2402 test '"list"
all worktrees from linked with a bare main', "git worktree list" still
observes the main worktree as bare. But in any case, you are right that
core.bare is sometimes ignored.

> It's more obvious with
> core.worktree because if that affects all worktrees, what's the point
> of having multiple worktrees. Git will always go to the place
> core.worktree points out.

That's true.

> So if this function is called from a secondary worktree, I'm not sure
> if it still works as expected because is_bare_repo may be false then.

I think you're right that is_bare_repository() will always return false
here. So let's look at the cases where, running from a secondary
worktree, we think that the main worktree should be observed as bare:

 - main worktree did not define core.bare
   - I don't know if this is possible (remember that we're running from
     a secondary worktree). But if it is, it seems that
     is_bare_repository_cfg will be -1, and worktree->is_bare will be
     set to 0 regardless of whether or not it is bare.

 - main worktree defines core.bare as 1; no config.worktree
   - is_bare_repository_cfg is 1, so we see the main worktree as bare.
     (This case is tested in t2402 '"list" all worktrees from linked
     with a bare main'.)

 - main worktree defines core.bare as 1, and secondary worktree defines
   core.bare as 0
   - I think that we'll see is_bare_repository_cfg as 0, so we won't see
     the main worktree as bare.

The only potentially problematic case seems to be the 3rd one.

> For the submodule case, you always stand at the submodule's main
> worktree, so it still works.

Yes.

> I don't think multiple-worktrees-on-submodules will be coming soon, so
> it's probably ok. But maybe leave a note here.

Observing that the problematic case is the 3rd one above, would this
note work:

  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.
Jeff King April 18, 2019, 6:42 p.m. UTC | #5
On Thu, Apr 18, 2019 at 11:30:00AM -0700, Jonathan Tan wrote:

> > >         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, "/.");
> > 
> > We can just call these two calls unconditionally, right? No harm done
> > if we don't strip.
> 
> We can, and no harm done. But this if/then pattern is also repeated in
> other parts of the file (e.g. get_linked_worktree()) so I'll leave it in
> for consistency. (Also, for what it's worth, it's slightly faster if
> only one strip is done.)

I also think your version expresses the intent more clearly. We expect
to see one or the other, but not "foo/./.git". And so (just as the code
prior to your patch) we would not convert that to "foo".

I am not sure of exactly what the "/." is trying to accomplish, so maybe
that double-strip _would_ be desirable, but if so it is definitely
worthy of its own commit explaining why that is so.

Interestingly, the case in get_linked_worktree() makes a lot more sense
because it has added "." as an absolute path itself, and is just
cleaning up the results of its strbuf_add_absolute_path()[1]. Which
makes me wonder if the "/." stripping in get_main_worktree() is actually
cargo-culted and simply unnecessary.

-Peff

[1] It seems like it would be simpler to just use strbuf_getcwd() for
    this, but maybe I am missing something.
Jonathan Tan April 18, 2019, 6:59 p.m. UTC | #6
> > >  - 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 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
> > >    core.bare=1, trust it, and otherwise, use is_bare_repository().
> >
> > I am not sure if the latter is worth keeping, but the former
> > definitely is a huge improvement, I would imagine.
> 
> I think that both are improvements, with the former definitely being the
> more impactful one.

Thanks.

These comments spurred me to look at it a bit closer, and omitting the
latter not only results in "git worktree list" changes, but means that
the following test now fails:

    test_expect_success 'bare main worktree has HEAD at branch deleted by secondary worktree' '
    	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
    '

(At current master, it succeeds.)

In the next reroll, I'll include this test and update the commit message
to use this as a rationale instead.

> > Somebody did "git branch -D <that-branch>" by accident in a submodule
> > checkout, or something?
> 
> /me shudders at the thought of it

Yes, that happened.

> The patch makes tons of sense to me.

Thanks!
Duy Nguyen April 19, 2019, 1:33 a.m. UTC | #7
On Fri, Apr 19, 2019 at 1:42 AM Jeff King <peff@peff.net> wrote:
>
> On Thu, Apr 18, 2019 at 11:30:00AM -0700, Jonathan Tan wrote:
>
> > > >         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, "/.");
> > >
> > > We can just call these two calls unconditionally, right? No harm done
> > > if we don't strip.
> >
> > We can, and no harm done. But this if/then pattern is also repeated in
> > other parts of the file (e.g. get_linked_worktree()) so I'll leave it in
> > for consistency. (Also, for what it's worth, it's slightly faster if
> > only one strip is done.)
>
> I also think your version expresses the intent more clearly. We expect
> to see one or the other, but not "foo/./.git". And so (just as the code
> prior to your patch) we would not convert that to "foo".
>
> I am not sure of exactly what the "/." is trying to accomplish, so maybe
> that double-strip _would_ be desirable, but if so it is definitely
> worthy of its own commit explaining why that is so.
>
> Interestingly, the case in get_linked_worktree() makes a lot more sense
> because it has added "." as an absolute path itself, and is just
> cleaning up the results of its strbuf_add_absolute_path()[1]. Which
> makes me wonder if the "/." stripping in get_main_worktree() is actually
> cargo-culted and simply unnecessary.

Yeah. It's added the same time get_linked_worktree() adds absolute
paths and trims "/." in 5193490442 (worktree: add a function to get
worktree details - 2015-10-08). Maybe it's because he wasn't sure if
get_git_common_dir() could return ".", which makes it exactly the same
as get_linked_worktree(). It's probably very unlikely that
git_git_common_dir() could return ".", but I can't be sure either.
Duy Nguyen April 19, 2019, 10:50 a.m. UTC | #8
On Fri, Apr 19, 2019 at 1:30 AM Jonathan Tan <jonathantanmy@google.com> wrote:
>
> > You actually didn't spell out the problem with "git branch -D", or at
> > least the consequence (i.e. the submodule branch is deleted even if
> > it's checked out).
>
> Thanks - I'll do that in the commit message.

Another minor nit (because I was still puzzled why a submodule was
considered bare/not bare)

> 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.

s/ends/does not end/. Now it makes more sense because the submodule's
main worktree is accidentally considered "bare" (i.e. no worktree),
its HEAD is ignored.

> > >         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;
> > > +       worktree->is_bare = (is_bare_repository_cfg == 1) ||
> >
> > core.bare and core.worktree are special. When you access them standing
> > from the main worktree, you'll see them. But when you stand from a
> > secondary worktree, they are ignored.
>
> Just checking: I think that is_bare_repository_cfg ignores core.bare
> only if the config.worktree file is present?

No, both are ignored independently if you read them from a secondary
worktree. Tested too with rev-parse, just to be sure.

> In the t2402 test '"list"
> all worktrees from linked with a bare main', "git worktree list" still
> observes the main worktree as bare.

Yes, because of bug :(

When git_config() is called again by cmd_worktree(), it does not treat
core.worktree and core.bare special anymore. So is_bare_repository_cfg
is reset from 0 to 1, even though I think its value at that point
plays no role anymore. What matters is the value at
setup_git_directory().

So yeah your code works in all cases by luck ;)

Fixing that one will not be easy (to avoid traps). I did try to delete
is_bare_repository_cfg (on the ground that global vars are hard to
manage, as seen here) only to discover that I could not simply delete
the core.bare parsing code in git_default_core_config() without
changing behaviour in some case [1]. I should probably revive that
branch (cleaning up gitdir setup code a bit) and submit.

[1] https://gitlab.com/pclouds/git/commit/2cc46d698ebe7af295e0d91f68103675077df68e#db04685516ffb491eb4239291b892fc0784e1aea

> > I don't think multiple-worktrees-on-submodules will be coming soon, so
> > it's probably ok. But maybe leave a note here.
>
> Observing that the problematic case is the 3rd one above, would this
> note work:
>
>   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.

Even though your code works perfectly now, I still think it's good to
have some comment like this.

Patch
diff mbox series

diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index 478b82cf9b..db5c411e76 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -264,6 +264,20 @@  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 '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..5d52b2ba53 100644
--- a/worktree.c
+++ b/worktree.c
@@ -49,18 +49,17 @@  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;
+	worktree->is_bare = (is_bare_repository_cfg == 1) ||
+		is_bare_repository();
 	add_head_info(worktree);
 
 	strbuf_release(&path);