git@vger.kernel.org mailing list mirror (one of many)
 help / color / mirror / code / Atom feed
* Bug in `git branch --delete main` when on other orphan branch
@ 2022-10-29  5:46 Martin von Zweigbergk
  2022-11-01 10:15 ` Jeff King
  0 siblings, 1 reply; 12+ messages in thread
From: Martin von Zweigbergk @ 2022-10-29  5:46 UTC (permalink / raw)
  To: Git Mailing List

Hi,

I did this:
git init test
cd test
echo a > file
git add file
git commit -m a
git checkout --orphan other
git branch --delete main

The last command fails with:
fatal: Couldn't look up commit object for HEAD

That's a bug, right? I can of course work around it with `rm
.git/refs/heads/main`.

^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: Bug in `git branch --delete main` when on other orphan branch
  2022-10-29  5:46 Bug in `git branch --delete main` when on other orphan branch Martin von Zweigbergk
@ 2022-11-01 10:15 ` Jeff King
  2022-11-01 15:31   ` Martin von Zweigbergk
  2022-11-01 20:12   ` Taylor Blau
  0 siblings, 2 replies; 12+ messages in thread
From: Jeff King @ 2022-11-01 10:15 UTC (permalink / raw)
  To: Martin von Zweigbergk; +Cc: Junio C Hamano, Git Mailing List

On Fri, Oct 28, 2022 at 10:46:37PM -0700, Martin von Zweigbergk wrote:

> I did this:
> git init test
> cd test
> echo a > file
> git add file
> git commit -m a
> git checkout --orphan other
> git branch --delete main
> 
> The last command fails with:
> fatal: Couldn't look up commit object for HEAD
> 
> That's a bug, right? I can of course work around it with `rm
> .git/refs/heads/main`.

Sort of. This is part of the "is the thing we are deleting merged into
HEAD" check. It tries to look up the HEAD and calls die() when it can't.
The more correct thing, I think, would be for it to just return "nope,
there is no HEAD so nothing is merged into it".

But that probably won't make your command succeed; you'll just get:

  error: The branch 'main' is not fully merged.

At which point you'd retry with "-f" (or "-D"). And then it succeeds,
because the force path is smart enough to skip loading HEAD, from
67affd5173 (git-branch -D: make it work even when on a yet-to-be-born
branch, 2006-11-24).

At the time, I suspect that logic was "good enough". You'd need "-f"
either way, so it is really just a question of producing a lousy error
message.

But since then, I think there are more cases. For example, 99c419c915
(branch -d: base the "already-merged" safety on the branch it merges
with, 2009-12-29) makes it OK to delete the branch if it's merged to
HEAD _or_ to its upstream. You don't have an upstream in your example,
but it's not hard to imagine one (just start the repo via "clone" rather
than from scratch).

And in that case I think the HEAD check calling die() is actively doing
the wrong thing, and would prevent an otherwise successful deletion.

The fix might be as simple as:

diff --git a/builtin/branch.c b/builtin/branch.c
index 15be0c03ef..f6ff9084c8 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -235,11 +235,8 @@ static int delete_branches(int argc, const char **argv, int force, int kinds,
 	}
 	branch_name_pos = strcspn(fmt, "%");
 
-	if (!force) {
+	if (!force)
 		head_rev = lookup_commit_reference(the_repository, &head_oid);
-		if (!head_rev)
-			die(_("Couldn't look up commit object for HEAD"));
-	}
 
 	for (i = 0; i < argc; i++, strbuf_reset(&bname)) {
 		char *target = NULL;

as the later code seems to do the right thing with the NULL head_rev. It
would definitely need more careful investigation (and tests!) to confirm
that, though.

And in the meantime, hopefully you noticed that "-f" is a better
workaround than manually deleting the refs file. :)

-Peff


^ permalink raw reply related	[flat|nested] 12+ messages in thread

* Re: Bug in `git branch --delete main` when on other orphan branch
  2022-11-01 10:15 ` Jeff King
@ 2022-11-01 15:31   ` Martin von Zweigbergk
  2022-11-01 20:12   ` Taylor Blau
  1 sibling, 0 replies; 12+ messages in thread
From: Martin von Zweigbergk @ 2022-11-01 15:31 UTC (permalink / raw)
  To: Jeff King; +Cc: Junio C Hamano, Git Mailing List

On Tue, Nov 1, 2022 at 3:15 AM Jeff King <peff@peff.net> wrote:
>
> On Fri, Oct 28, 2022 at 10:46:37PM -0700, Martin von Zweigbergk wrote:
>
> > I did this:
> > git init test
> > cd test
> > echo a > file
> > git add file
> > git commit -m a
> > git checkout --orphan other
> > git branch --delete main
> >
> > The last command fails with:
> > fatal: Couldn't look up commit object for HEAD
> >
> > That's a bug, right? I can of course work around it with `rm
> > .git/refs/heads/main`.
>
> Sort of. This is part of the "is the thing we are deleting merged into
> HEAD" check. It tries to look up the HEAD and calls die() when it can't.
> The more correct thing, I think, would be for it to just return "nope,
> there is no HEAD so nothing is merged into it".

Ah, so that's what it was about. Thanks for looking into it!

> And in the meantime, hopefully you noticed that "-f" is a better
> workaround than manually deleting the refs file. :)

Nope, because I had no idea it was something that could be first.
Also, this was just in a script to reproduce an unrelated (non-Git)
bug, so my hacky workaround was okay :)

Thanks!

^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: Bug in `git branch --delete main` when on other orphan branch
  2022-11-01 10:15 ` Jeff King
  2022-11-01 15:31   ` Martin von Zweigbergk
@ 2022-11-01 20:12   ` Taylor Blau
  2022-11-01 20:14     ` Bug in `git branch --delete main` when on other orphan brancht Taylor Blau
  2022-11-01 20:32     ` [PATCH] branch: gracefully handle '-d' on detached HEAD Taylor Blau
  1 sibling, 2 replies; 12+ messages in thread
From: Taylor Blau @ 2022-11-01 20:12 UTC (permalink / raw)
  To: Jeff King; +Cc: Martin von Zweigbergk, Junio C Hamano, Git Mailing List

On Tue, Nov 01, 2022 at 06:15:33AM -0400, Jeff King wrote:
> On Fri, Oct 28, 2022 at 10:46:37PM -0700, Martin von Zweigbergk wrote:
>
> > I did this:
> > git init test
> > cd test
> > echo a > file
> > git add file
> > git commit -m a
> > git checkout --orphan other
> > git branch --delete main
> >
> > The last command fails with:
> > fatal: Couldn't look up commit object for HEAD
> >
> > That's a bug, right? I can of course work around it with `rm
> > .git/refs/heads/main`.
>
> Sort of. This is part of the "is the thing we are deleting merged into
> HEAD" check. It tries to look up the HEAD and calls die() when it can't.
> The more correct thing, I think, would be for it to just return "nope,
> there is no HEAD so nothing is merged into it".

Yeah, I think that it's fair to call being unable to find HEAD in 'git
branch -d' when we are detached a bug. Indeed, if we can't find a HEAD,
then that's fine (there is just nothing merged into it, as you note).

> And in that case I think the HEAD check calling die() is actively doing
> the wrong thing, and would prevent an otherwise successful deletion.
>
> The fix might be as simple as:
>
> diff --git a/builtin/branch.c b/builtin/branch.c
> index 15be0c03ef..f6ff9084c8 100644
> --- a/builtin/branch.c
> +++ b/builtin/branch.c
> @@ -235,11 +235,8 @@ static int delete_branches(int argc, const char **argv, int force, int kinds,
>  	}
>  	branch_name_pos = strcspn(fmt, "%");
>
> -	if (!force) {
> +	if (!force)
>  		head_rev = lookup_commit_reference(the_repository, &head_oid);
> -		if (!head_rev)
> -			die(_("Couldn't look up commit object for HEAD"));
> -	}
>
>  	for (i = 0; i < argc; i++, strbuf_reset(&bname)) {
>  		char *target = NULL;
>
> as the later code seems to do the right thing with the NULL head_rev. It
> would definitely need more careful investigation (and tests!) to confirm
> that, though.

Yeah, that looks reasonable to me. Presumably we want a small test, as
well, but I doubt that is any more complicated than:

--- 8< ---
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index 7f605f865b..6ace22f7ce 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -279,6 +279,14 @@ test_expect_success 'git branch -M and -C fail on detached HEAD' '
 	test_cmp expect err
 '

+test_expect_success 'git branch -d on detached HEAD' '
+	test_when_finished "git checkout main && git branch -D other" &&
+	git branch other &&
+	git checkout --orphan orphan &&
+	test_must_fail git branch -d other 2>err &&
+	grep "not fully merged" err
+'
+
 test_expect_success 'git branch -v -d t should work' '
 	git branch t &&
 	git rev-parse --verify refs/heads/t &&
--- >8 ---

I'm happy to wrap all of that up into a patch, and equally happy for you
to do so (feel free to forge my S-o-b here if you do).

Thanks,
Taylor

^ permalink raw reply related	[flat|nested] 12+ messages in thread

* Re: Bug in `git branch --delete main` when on other orphan brancht
  2022-11-01 20:12   ` Taylor Blau
@ 2022-11-01 20:14     ` Taylor Blau
  2022-11-01 21:45       ` Jeff King
  2022-11-01 20:32     ` [PATCH] branch: gracefully handle '-d' on detached HEAD Taylor Blau
  1 sibling, 1 reply; 12+ messages in thread
From: Taylor Blau @ 2022-11-01 20:14 UTC (permalink / raw)
  To: Taylor Blau
  Cc: Jeff King, Martin von Zweigbergk, Junio C Hamano,
	Git Mailing List

On Tue, Nov 01, 2022 at 04:12:06PM -0400, Taylor Blau wrote:
> Yeah, that looks reasonable to me. Presumably we want a small test, as
> well, but I doubt that is any more complicated than:
>
> --- 8< ---
> diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
> index 7f605f865b..6ace22f7ce 100755
> --- a/t/t3200-branch.sh
> +++ b/t/t3200-branch.sh
> @@ -279,6 +279,14 @@ test_expect_success 'git branch -M and -C fail on detached HEAD' '
>  	test_cmp expect err
>  '
>
> +test_expect_success 'git branch -d on detached HEAD' '
> +	test_when_finished "git checkout main && git branch -D other" &&
> +	git branch other &&
> +	git checkout --orphan orphan &&
> +	test_must_fail git branch -d other 2>err &&
> +	grep "not fully merged" err
> +'
> +
>  test_expect_success 'git branch -v -d t should work' '
>  	git branch t &&
>  	git rev-parse --verify refs/heads/t &&
> --- >8 ---

Actually, the test doesn't need "other" here, since we aren't actually
going to delete the target branch (for the exact same reason that you
pointed out to Martin earlier in the thread).

So it could actually be as small as something like this:

--- >8 ---
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index 7f605f865b..464d3f610b 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -279,6 +279,13 @@ test_expect_success 'git branch -M and -C fail on detached HEAD' '
 	test_cmp expect err
 '

+test_expect_success 'git branch -d on detached HEAD' '
+	test_when_finished git checkout main &&
+	git checkout --orphan orphan &&
+	test_must_fail git branch -d main 2>err &&
+	grep "not fully merged" err
+'
+
 test_expect_success 'git branch -v -d t should work' '
 	git branch t &&
 	git rev-parse --verify refs/heads/t &&
--- 8< ---

Thanks,
Taylor

^ permalink raw reply related	[flat|nested] 12+ messages in thread

* [PATCH] branch: gracefully handle '-d' on detached HEAD
  2022-11-01 20:12   ` Taylor Blau
  2022-11-01 20:14     ` Bug in `git branch --delete main` when on other orphan brancht Taylor Blau
@ 2022-11-01 20:32     ` Taylor Blau
  2022-11-02  5:27       ` [PATCH v2] branch: gracefully handle '-d' on orphan HEAD Jeff King
  1 sibling, 1 reply; 12+ messages in thread
From: Taylor Blau @ 2022-11-01 20:32 UTC (permalink / raw)
  To: git, git; +Cc: Jeff King, Martin von Zweigbergk

From: Jeff King <peff@peff.net>

Since 67affd5173 (git-branch -D: make it work even when on a
yet-to-be-born branch, 2006-11-24), 'git branch -d' refuses to work when
orphaned, since there is no HEAD to resolve.

But since 67affd5173, there have been other checks, like 99c419c915
(branch -d: base the "already-merged" safety on the branch it merges
with, 2009-12-29), which makes it OK to delete a branch if it is merged
to HEAD or its upstream.

99c419c915 makes the check in 67affd5173 wrong, since it's OK to delete
a branch if it is merged to its upstream.

Since the code in delete_branches() tolerates a NULL head_rev perfectly
fine, make it non-fatal to fail to resolve a commit object at HEAD.

Signed-off-by: Jeff King <peff@peff.net>
Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
OK, here's that patch. Since Peff wrote the main portion of it, he gets
authorship credit. My version comes with a test, which we should
consider picking up.

Even though it doesn't resolve Martin's original scenario (i.e., he'd
still have to use -D or -f to actually delete 'main' there), I still
think the bugfix is worth pursuing in its own right.

 builtin/branch.c  | 5 +----
 t/t3200-branch.sh | 7 +++++++
 2 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/builtin/branch.c b/builtin/branch.c
index 15be0c03ef..f6ff9084c8 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -235,11 +235,8 @@ static int delete_branches(int argc, const char **argv, int force, int kinds,
 	}
 	branch_name_pos = strcspn(fmt, "%");

-	if (!force) {
+	if (!force)
 		head_rev = lookup_commit_reference(the_repository, &head_oid);
-		if (!head_rev)
-			die(_("Couldn't look up commit object for HEAD"));
-	}

 	for (i = 0; i < argc; i++, strbuf_reset(&bname)) {
 		char *target = NULL;
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index 7f605f865b..464d3f610b 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -279,6 +279,13 @@ test_expect_success 'git branch -M and -C fail on detached HEAD' '
 	test_cmp expect err
 '

+test_expect_success 'git branch -d on detached HEAD' '
+	test_when_finished git checkout main &&
+	git checkout --orphan orphan &&
+	test_must_fail git branch -d main 2>err &&
+	grep "not fully merged" err
+'
+
 test_expect_success 'git branch -v -d t should work' '
 	git branch t &&
 	git rev-parse --verify refs/heads/t &&
--
2.38.0.16.g393fd4c6db

^ permalink raw reply related	[flat|nested] 12+ messages in thread

* Re: Bug in `git branch --delete main` when on other orphan brancht
  2022-11-01 20:14     ` Bug in `git branch --delete main` when on other orphan brancht Taylor Blau
@ 2022-11-01 21:45       ` Jeff King
  2022-11-02  0:59         ` Taylor Blau
  0 siblings, 1 reply; 12+ messages in thread
From: Jeff King @ 2022-11-01 21:45 UTC (permalink / raw)
  To: Taylor Blau; +Cc: Martin von Zweigbergk, Junio C Hamano, Git Mailing List

On Tue, Nov 01, 2022 at 04:14:40PM -0400, Taylor Blau wrote:

> Actually, the test doesn't need "other" here, since we aren't actually
> going to delete the target branch (for the exact same reason that you
> pointed out to Martin earlier in the thread).
> 
> So it could actually be as small as something like this:
> 
> --- >8 ---
> diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
> index 7f605f865b..464d3f610b 100755
> --- a/t/t3200-branch.sh
> +++ b/t/t3200-branch.sh
> @@ -279,6 +279,13 @@ test_expect_success 'git branch -M and -C fail on detached HEAD' '
>  	test_cmp expect err
>  '
> 
> +test_expect_success 'git branch -d on detached HEAD' '
> +	test_when_finished git checkout main &&
> +	git checkout --orphan orphan &&
> +	test_must_fail git branch -d main 2>err &&
> +	grep "not fully merged" err
> +'

I think there's a more interesting case, which is when "main" has a
configured upstream to which it's fully merged. And then the deletion
actually succeeds (and the bug is not just giving us a crappy message,
but actually doing causing the wrong outcome).

And for that we'd probably not want to use "main", since a successful
test will actually delete it. ;)

I'll try later tonight to integrate that test into the patch you've
written.

-Peff

^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: Bug in `git branch --delete main` when on other orphan brancht
  2022-11-01 21:45       ` Jeff King
@ 2022-11-02  0:59         ` Taylor Blau
  0 siblings, 0 replies; 12+ messages in thread
From: Taylor Blau @ 2022-11-02  0:59 UTC (permalink / raw)
  To: Jeff King
  Cc: Taylor Blau, Martin von Zweigbergk, Junio C Hamano,
	Git Mailing List

On Tue, Nov 01, 2022 at 05:45:36PM -0400, Jeff King wrote:
> I'll try later tonight to integrate that test into the patch you've
> written.

Very much appreciated, thanks.

Thanks,
Taylor

^ permalink raw reply	[flat|nested] 12+ messages in thread

* [PATCH v2] branch: gracefully handle '-d' on orphan HEAD
  2022-11-01 20:32     ` [PATCH] branch: gracefully handle '-d' on detached HEAD Taylor Blau
@ 2022-11-02  5:27       ` Jeff King
  2022-11-04  1:26         ` Rubén Justo
  0 siblings, 1 reply; 12+ messages in thread
From: Jeff King @ 2022-11-02  5:27 UTC (permalink / raw)
  To: Taylor Blau; +Cc: git, Martin von Zweigbergk

On Tue, Nov 01, 2022 at 04:32:18PM -0400, Taylor Blau wrote:

> Subject: Re: [PATCH] branch: gracefully handle '-d' on detached HEAD

OK, here's my version of the patch (which I called v2 for the sake of
confusion). I ended up rewriting the tests and commit message, as there
were a few inaccuracies, and some subtle things our earlier conversation
didn't turn up.

This should really be s/detached/orphan/ here and elsewhere (including
the branch name you used). All of this works fine on a detached HEAD.
It's only a problem when it can't be resolved to a commit at all (one
could perhaps argue that a branch being merged to a detached HEAD isn't
any real kind of safety valve, but that's how it has always worked and
is outside the scope of this fix).

> Since 67affd5173 (git-branch -D: make it work even when on a
> yet-to-be-born branch, 2006-11-24), 'git branch -d' refuses to work when
> orphaned, since there is no HEAD to resolve.

It was true even before that commit that "git branch -d" would refuse to
work. That commit just made it so that "branch -D" worked as a safety
hatch (rather than also dying!).

> But since 67affd5173, there have been other checks, like 99c419c915
> (branch -d: base the "already-merged" safety on the branch it merges
> with, 2009-12-29), which makes it OK to delete a branch if it is merged
> to HEAD or its upstream.

I always thought of this as "either-or", but it is a little different.
If we are merged to HEAD and there is an upstream but we are not merged
to it, we'll still refuse the deletion. Not super relevant for our
purposes here, except that there's extra code to produce a custom
message for this case, which we need to fix to handle NULL. ;)

> Since the code in delete_branches() tolerates a NULL head_rev perfectly
> fine, make it non-fatal to fail to resolve a commit object at HEAD.

And alas, this "perfectly fine" turns out to not be true. :) See below
for the gory details.

-- >8 --
Subject: [PATCH] branch: gracefully handle '-d' on orphan HEAD

When deleting a branch, "git branch -d" has a safety check that ensures
the branch is merged to its upstream (if any), or to HEAD. To do that,
naturally we try to resolve HEAD to a commit object. If we're on an
orphan branch (i.e., HEAD points to a branch that does not yet exist),
that will fail, and we'll bail with an error:

  $ git branch -d to-delete
  fatal: Couldn't look up commit object for HEAD

This usually isn't that big of a deal. The deletion would fail anyway,
since the branch isn't merged to HEAD, and you'd need to use "-D" (or
"-f"). And doing so skips the HEAD resolution, courtesy of 67affd5173
(git-branch -D: make it work even when on a yet-to-be-born branch,
2006-11-24).

But there are still two problems:

  1. The error message isn't very helpful. We should give the usual "not
     fully merged" message, which points the user at "branch -D". That
     was a problem even back in 67affd5173.

  2. Even without a HEAD, these days it's still possible for the
     deletion to succeed. After 67affd5173, commit 99c419c915 (branch
     -d: base the "already-merged" safety on the branch it merges with,
     2009-12-29) made it OK to delete a branch if it is merged to its
     upstream.

We can fix both by removing the die() in delete_branches() completely,
leaving head_rev NULL in this case. It's tempting to stop there, as it
appears at first glance that the rest of the code does the right thing
with a NULL. But sadly, it's not quite true.

We end up feeding the NULL to repo_is_descendant_of(). In the
traditional code path there, we call repo_in_merge_bases_many(). It
feeds the NULL to repo_parse_commit(), which is smart enough to return
an error, and we immediately return "no, it's not a descendant".

But there's an alternate code path: if we have a commit graph with
generation numbers, we end up in can_all_from_reach(), which does
eventually try to set a flag on the NULL commit and segfaults.

So instead, we'll teach the local branch_merged() helper to treat a NULL
as "not merged". This would be a little more elegant in in_merge_bases()
itself, but that function is called in a lot of places, and it's not
clear that quietly returning "not merged" is the right thing everywhere
(I'd expect in many cases, feeding a NULL is a sign of a bug).

There are four tests here:

  a. The first one confirms that deletion succeeds with an orphaned HEAD
     when the branch is merged to its upstream. This is case (2) above.

  b. Same, but with commit graphs enabled. Even if it is merged to
     upstream, we still check head_rev so that we can say "deleting
     because it's merged to upstream, even though it's not merged to
     HEAD". Without the second hunk in branch_merged(), this test would
     segfault in can_all_from_reach().

  c. The third one confirms that we correctly say "not merged to HEAD"
     when we can't resolve HEAD, and reject the deletion.

  d. Same, but with commit graphs enabled. Without the first hunk in
     branch_merged(), this one would segfault.

Reported-by: Martin von Zweigbergk <martinvonz@google.com>
Signed-off-by: Jeff King <peff@peff.net>
---
 builtin/branch.c  |  9 +++------
 t/t3200-branch.sh | 36 ++++++++++++++++++++++++++++++++++++
 2 files changed, 39 insertions(+), 6 deletions(-)

diff --git a/builtin/branch.c b/builtin/branch.c
index 15be0c03ef..9470c980c1 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -150,7 +150,7 @@ static int branch_merged(int kind, const char *name,
 	if (!reference_rev)
 		reference_rev = head_rev;
 
-	merged = in_merge_bases(rev, reference_rev);
+	merged = reference_rev ? in_merge_bases(rev, reference_rev) : 0;
 
 	/*
 	 * After the safety valve is fully redefined to "check with
@@ -160,7 +160,7 @@ static int branch_merged(int kind, const char *name,
 	 * a gentle reminder is in order.
 	 */
 	if ((head_rev != reference_rev) &&
-	    in_merge_bases(rev, head_rev) != merged) {
+	    (head_rev ? in_merge_bases(rev, head_rev) : 0) != merged) {
 		if (merged)
 			warning(_("deleting branch '%s' that has been merged to\n"
 				"         '%s', but not yet merged to HEAD."),
@@ -235,11 +235,8 @@ static int delete_branches(int argc, const char **argv, int force, int kinds,
 	}
 	branch_name_pos = strcspn(fmt, "%");
 
-	if (!force) {
+	if (!force)
 		head_rev = lookup_commit_reference(the_repository, &head_oid);
-		if (!head_rev)
-			die(_("Couldn't look up commit object for HEAD"));
-	}
 
 	for (i = 0; i < argc; i++, strbuf_reset(&bname)) {
 		char *target = NULL;
diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index 7f605f865b..5a169b68d6 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -279,6 +279,42 @@ test_expect_success 'git branch -M and -C fail on detached HEAD' '
 	test_cmp expect err
 '
 
+test_expect_success 'git branch -d on orphan HEAD (merged)' '
+	test_when_finished git checkout main &&
+	git checkout --orphan orphan &&
+	test_when_finished "rm -rf .git/objects/commit-graph*" &&
+	git commit-graph write --reachable &&
+	git branch --track to-delete main &&
+	git branch -d to-delete
+'
+
+test_expect_success 'git branch -d on orphan HEAD (merged, graph)' '
+	test_when_finished git checkout main &&
+	git checkout --orphan orphan &&
+	git branch --track to-delete main &&
+	git branch -d to-delete
+'
+
+test_expect_success 'git branch -d on orphan HEAD (unmerged)' '
+	test_when_finished git checkout main &&
+	git checkout --orphan orphan &&
+	test_when_finished "git branch -D to-delete" &&
+	git branch to-delete main &&
+	test_must_fail git branch -d to-delete 2>err &&
+	grep "not fully merged" err
+'
+
+test_expect_success 'git branch -d on orphan HEAD (unmerged, graph)' '
+	test_when_finished git checkout main &&
+	git checkout --orphan orphan &&
+	test_when_finished "git branch -D to-delete" &&
+	git branch to-delete main &&
+	test_when_finished "rm -rf .git/objects/commit-graph*" &&
+	git commit-graph write --reachable &&
+	test_must_fail git branch -d to-delete 2>err &&
+	grep "not fully merged" err
+'
+
 test_expect_success 'git branch -v -d t should work' '
 	git branch t &&
 	git rev-parse --verify refs/heads/t &&
-- 
2.38.1.669.g2ee9a5b0e3


^ permalink raw reply related	[flat|nested] 12+ messages in thread

* Re: [PATCH v2] branch: gracefully handle '-d' on orphan HEAD
  2022-11-02  5:27       ` [PATCH v2] branch: gracefully handle '-d' on orphan HEAD Jeff King
@ 2022-11-04  1:26         ` Rubén Justo
  2022-11-04  5:36           ` Jeff King
  0 siblings, 1 reply; 12+ messages in thread
From: Rubén Justo @ 2022-11-04  1:26 UTC (permalink / raw)
  To: Jeff King, Taylor Blau; +Cc: git, Martin von Zweigbergk, Derrick Stolee

On 2/11/22 6:27, Jeff King wrote:

> We can fix both by removing the die() in delete_branches() completely,
> leaving head_rev NULL in this case. It's tempting to stop there, as it
> appears at first glance that the rest of the code does the right thing
> with a NULL. But sadly, it's not quite true.
> 
> We end up feeding the NULL to repo_is_descendant_of(). In the
> traditional code path there, we call repo_in_merge_bases_many(). It
> feeds the NULL to repo_parse_commit(), which is smart enough to return
> an error, and we immediately return "no, it's not a descendant".
> 
> But there's an alternate code path: if we have a commit graph with
> generation numbers, we end up in can_all_from_reach(), which does
> eventually try to set a flag on the NULL commit and segfaults.
> 
> So instead, we'll teach the local branch_merged() helper to treat a NULL
> as "not merged". This would be a little more elegant in in_merge_bases()
> itself, but that function is called in a lot of places, and it's not
> clear that quietly returning "not merged" is the right thing everywhere
> (I'd expect in many cases, feeding a NULL is a sign of a bug).
> 
> There are four tests here:
> ...
I've reviewed the change and looks fine to me.  Fixes the issue with the
deletion and avoids the segfault you discovered.

But the last paragraph in your message, before describing the tests, makes me
scratch my head.

Certainly there are a few dozen places where we have direct calls to
in_merge_bases.  I haven't found any (beyond the modified in this patch) where
a NULL commit can be used in the call.  All I have reviewed have a
check_for_null protection before calling, mainly to show an error message.  And
this makes me think about, what almost happened here, leaving that uncovered
left us open to a change where the error condition (NULL commit) doesn't matter
(just the not_merged), and/or does not have a proper test with generation
numbers.

The segfault possibility was introduced in 6cc017431 (commit-reach: use
can_all_from_reach, 2018-07-20).  Before that, NULL was tolerated by
is_descendant_of (and indirectly by in_merge_bases) and returned, still today
(as you described in your message) as 1.  So IMHO we can safely put a check for
NULL there and return 1, as a fix (or protection) for this segfault.  Something
like:

diff --git a/commit-reach.c b/commit-reach.c
index c226ee3da4..246eaf093d 100644
--- a/commit-reach.c
+++ b/commit-reach.c
@@ -445,7 +445,7 @@ int repo_is_descendant_of(struct repository *r,
                          struct commit *commit,
                          struct commit_list *with_commit)
 {
-       if (!with_commit)
+       if (!with_commit || !commit)
                return 1;
 
        if (generation_numbers_enabled(the_repository)) {

and leave the checks for NULL in branch.c, as optimizations.

I've cc Derrick, maybe he can give us an opinion on this.


This patch also /fixes/ the error message when:

	$ git init -b initial
	$ git branch -d initial
	fatal: Couldn't look up commit object for HEAD

Now we get the much clear:

	error: Cannot delete branch 'initial' checked out at ...

A nice patch.
Thank you.

^ permalink raw reply related	[flat|nested] 12+ messages in thread

* Re: [PATCH v2] branch: gracefully handle '-d' on orphan HEAD
  2022-11-04  1:26         ` Rubén Justo
@ 2022-11-04  5:36           ` Jeff King
  2022-11-06 22:22             ` Rubén Justo
  0 siblings, 1 reply; 12+ messages in thread
From: Jeff King @ 2022-11-04  5:36 UTC (permalink / raw)
  To: Rubén Justo; +Cc: Taylor Blau, git, Martin von Zweigbergk, Derrick Stolee

On Fri, Nov 04, 2022 at 02:26:00AM +0100, Rubén Justo wrote:

> > So instead, we'll teach the local branch_merged() helper to treat a NULL
> > as "not merged". This would be a little more elegant in in_merge_bases()
> > itself, but that function is called in a lot of places, and it's not
> > clear that quietly returning "not merged" is the right thing everywhere
> > (I'd expect in many cases, feeding a NULL is a sign of a bug).
> > 
> > There are four tests here:
> > ...
> I've reviewed the change and looks fine to me.  Fixes the issue with the
> deletion and avoids the segfault you discovered.

Just to make things clear, the segfault doesn't exist before my patch.
It's only once we remove the die() call that we need to make sure the
downstream code does the right thing with the resulting NULL, and does
not segfault.

> But the last paragraph in your message, before describing the tests, makes me
> scratch my head.
> 
> Certainly there are a few dozen places where we have direct calls to
> in_merge_bases.  I haven't found any (beyond the modified in this patch) where
> a NULL commit can be used in the call.  All I have reviewed have a
> check_for_null protection before calling, mainly to show an error message.  And
> this makes me think about, what almost happened here, leaving that uncovered
> left us open to a change where the error condition (NULL commit) doesn't matter
> (just the not_merged), and/or does not have a proper test with generation
> numbers.

I didn't find any instances, either, but I also didn't look. My
reasoning was mostly that by making the change to this code in
isolation, we could be sure not to have accidental effects in other
code. Now it _could_ be useful to handle NULL in those other call-sites,
but I didn't want making a judgement on that to hold up this fix.

> The segfault possibility was introduced in 6cc017431 (commit-reach: use
> can_all_from_reach, 2018-07-20).  Before that, NULL was tolerated by
> is_descendant_of (and indirectly by in_merge_bases) and returned, still today
> (as you described in your message) as 1.  So IMHO we can safely put a check for
> NULL there and return 1, as a fix (or protection) for this segfault.  Something
> like:

Yes, the segfault possibility was introduced there. But that doesn't
mean the code intended to handle a NULL commit in that case. I think it
ends up doing the right thing, but the behavior is a little
questionable. It actually sees an error from repo_parse_commit(), and
then aborts the whole in_merge_bases_many() operation (not even looking
at the other entries in the "reference" array, although in this caller
it will always be the only element of the array).

So I find it too hard to blame 6cc017431 here; I don't think
is_descendant_of() ever intended to handle NULL, and it was just luck
that it did before then.

So a fix there might be OK, but...

> diff --git a/commit-reach.c b/commit-reach.c
> index c226ee3da4..246eaf093d 100644
> --- a/commit-reach.c
> +++ b/commit-reach.c
> @@ -445,7 +445,7 @@ int repo_is_descendant_of(struct repository *r,
>                           struct commit *commit,
>                           struct commit_list *with_commit)
>  {
> -       if (!with_commit)
> +       if (!with_commit || !commit)
>                 return 1;
>  
>         if (generation_numbers_enabled(the_repository)) {
> 
> and leave the checks for NULL in branch.c, as optimizations.

I don't think that does the right thing. We are asking if "commit" is a
descendant of any element in "with_commit". If "with_commit" is empty,
we say "yes" by returning 1.  But if there is no "commit", is the answer
also "yes"? It seems like it should be "no", returning 0.

TBH, I find the existing "return 1" questionable. It comes originally
from 694a577519 (git-branch --contains=commit, 2007-11-07). Back then
the function was used only for checking --contains, where a NULL list
meant "the user did not ask to constrain the list at all".

I think it may be luck that no other caller has relied on that in the
intervening years.

> This patch also /fixes/ the error message when:
> 
> 	$ git init -b initial
> 	$ git branch -d initial
> 	fatal: Couldn't look up commit object for HEAD
> 
> Now we get the much clear:
> 
> 	error: Cannot delete branch 'initial' checked out at ...

OK, good. That surprised me at first, because the check in
branch_checked_out() doesn't use the same head_rev variable. But it is
just the case that the die() I removed was aborting much earlier, and
now we get far enough to do the right message. The distinction is
relevant because it means that I didn't miss a spot where I should have
checked the behavior of NULL head_rev; the head_rev value is not used
directly here.

-Peff

^ permalink raw reply	[flat|nested] 12+ messages in thread

* Re: [PATCH v2] branch: gracefully handle '-d' on orphan HEAD
  2022-11-04  5:36           ` Jeff King
@ 2022-11-06 22:22             ` Rubén Justo
  0 siblings, 0 replies; 12+ messages in thread
From: Rubén Justo @ 2022-11-06 22:22 UTC (permalink / raw)
  To: Jeff King; +Cc: Taylor Blau, git, Martin von Zweigbergk, Derrick Stolee

On 4/11/22 6:36, Jeff King wrote:

> I didn't find any instances, either, but I also didn't look. My
> reasoning was mostly that by making the change to this code in
> isolation, we could be sure not to have accidental effects in other
> code. Now it _could_ be useful to handle NULL in those other call-sites,
> but I didn't want making a judgement on that to hold up this fix.
> 
>> The segfault possibility was introduced in 6cc017431 (commit-reach: use
>> can_all_from_reach, 2018-07-20).  Before that, NULL was tolerated by
>> is_descendant_of (and indirectly by in_merge_bases) and returned, still today
>> (as you described in your message) as 1.  So IMHO we can safely put a check for
>> NULL there and return 1, as a fix (or protection) for this segfault.  Something
>> like:
> 
> Yes, the segfault possibility was introduced there. But that doesn't
> mean the code intended to handle a NULL commit in that case. I think it
> ends up doing the right thing, but the behavior is a little
> questionable. It actually sees an error from repo_parse_commit(), and
> then aborts the whole in_merge_bases_many() operation (not even looking
> at the other entries in the "reference" array, although in this caller
> it will always be the only element of the array).
> 
> So I find it too hard to blame 6cc017431 here; I don't think
> is_descendant_of() ever intended to handle NULL, and it was just luck
> that it did before then.
> 
> So a fix there might be OK, but...
> 
>> diff --git a/commit-reach.c b/commit-reach.c
>> index c226ee3da4..246eaf093d 100644
>> --- a/commit-reach.c
>> +++ b/commit-reach.c
>> @@ -445,7 +445,7 @@ int repo_is_descendant_of(struct repository *r,
>>                           struct commit *commit,
>>                           struct commit_list *with_commit)
>>  {
>> -       if (!with_commit)
>> +       if (!with_commit || !commit)
>>                 return 1;
>>  
>>         if (generation_numbers_enabled(the_repository)) {
>>
>> and leave the checks for NULL in branch.c, as optimizations.
> 
> I don't think that does the right thing. We are asking if "commit" is a
> descendant of any element in "with_commit". If "with_commit" is empty,
> we say "yes" by returning 1.  But if there is no "commit", is the answer
> also "yes"? It seems like it should be "no", returning 0.

Correct.  I'm sorry :-/, I meant 0.  My reasoning was to maintain, for NULL
commit, the same result with or without generation_numbers_enabled.  Nothing to
blame on 6cc017431, as nothing states that repo_is_descendant_of needs to
support a NULL commit; but as generation_numbers is not enabled by default, it
is easy to leave that aside if only checked with defaults.  But of course, your
change and 0cc017431 are correct, and your tests cover both execution paths, so
nothing needs to be changed.

>> This patch also /fixes/ the error message when:
>>
>> 	$ git init -b initial
>> 	$ git branch -d initial
>> 	fatal: Couldn't look up commit object for HEAD
>>
>> Now we get the much clear:
>>
>> 	error: Cannot delete branch 'initial' checked out at ...
> 
> OK, good. That surprised me at first, because the check in
> branch_checked_out() doesn't use the same head_rev variable. But it is
> just the case that the die() I removed was aborting much earlier, and
> now we get far enough to do the right message. The distinction is
> relevant because it means that I didn't miss a spot where I should have
> checked the behavior of NULL head_rev; the head_rev value is not used
> directly here.

My comment was just to suggest that maybe it is worth adding a test for this :-)
If you think it is, maybe this can be useful:

----- 8< -----

diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh
index 5a169b68d6..2c1c16cc17 100755
--- a/t/t3200-branch.sh
+++ b/t/t3200-branch.sh
@@ -315,6 +315,13 @@ test_expect_success 'git branch -d on orphan HEAD (unmerged, graph)' '
 	grep "not fully merged" err
 '
 
+test_expect_success 'git branch -d orphan error message' '
+	test_when_finished git checkout main &&
+	git checkout --orphan orphan &&
+	test_must_fail git branch -d orphan 2>err &&
+	grep "checked out at" err
+'
+
 test_expect_success 'git branch -v -d t should work' '
 	git branch t &&
 	git rev-parse --verify refs/heads/t &&
----- >8 -----

> 
> -Peff
> 

Sorry for the noise.

Un saludo.
Rubén.

^ permalink raw reply related	[flat|nested] 12+ messages in thread

end of thread, other threads:[~2022-11-06 22:22 UTC | newest]

Thread overview: 12+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-10-29  5:46 Bug in `git branch --delete main` when on other orphan branch Martin von Zweigbergk
2022-11-01 10:15 ` Jeff King
2022-11-01 15:31   ` Martin von Zweigbergk
2022-11-01 20:12   ` Taylor Blau
2022-11-01 20:14     ` Bug in `git branch --delete main` when on other orphan brancht Taylor Blau
2022-11-01 21:45       ` Jeff King
2022-11-02  0:59         ` Taylor Blau
2022-11-01 20:32     ` [PATCH] branch: gracefully handle '-d' on detached HEAD Taylor Blau
2022-11-02  5:27       ` [PATCH v2] branch: gracefully handle '-d' on orphan HEAD Jeff King
2022-11-04  1:26         ` Rubén Justo
2022-11-04  5:36           ` Jeff King
2022-11-06 22:22             ` Rubén Justo

Code repositories for project(s) associated with this public inbox

	https://80x24.org/mirrors/git.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).