diff mbox series

[v3,4/7] sequencer: treat error reading HEAD as unborn branch

Message ID 20240310184602.539656-5-brianmlyles@gmail.com (mailing list archive)
State New
Headers show
Series None | expand

Commit Message

Brian Lyles March 10, 2024, 6:42 p.m. UTC
When using git-cherry-pick(1) with `--allow-empty` while on an unborn
branch, an error is thrown. This is inconsistent with the same
cherry-pick when `--allow-empty` is not specified.

Treat a failure reading HEAD as an unborn branch in
`is_index_unchanged`. This is consistent with other sequencer logic such
as `do_pick_commit`. When on an unborn branch, use the `empty_tree` as
the tree to compare against.

Add a new test to cover this scenario. While modelled off of the
existing 'cherry-pick on unborn branch' test, some improvements can be
made:

- Use `git switch --orphan unborn` instead of `git checkout --orphan
  unborn` to avoid the need for a separate `rm -rf *` call
- Avoid using `--quiet` in the `git diff` call to make debugging easier
  in the event of a failure

Make these improvements to the existing test as well as the new test.

Helped-by: Phillip Wood <phillip.wood@dunelm.org.uk>
Signed-off-by: Brian Lyles <brianmlyles@gmail.com>
---

Differences from v2:

- Minor code and test cleanup per [1] and the other replies in that
  thread

[1]: https://lore.kernel.org/git/62247a1c-0249-4ce1-8626-fe97b89c23dc@gmail.com/

 sequencer.c                   | 35 +++++++++++++++++++++--------------
 t/t3501-revert-cherry-pick.sh | 14 +++++++++++---
 2 files changed, 32 insertions(+), 17 deletions(-)

Comments

Junio C Hamano March 11, 2024, 12:07 a.m. UTC | #1
Brian Lyles <brianmlyles@gmail.com> writes:

> When using git-cherry-pick(1) with `--allow-empty` while on an unborn
> branch, an error is thrown. This is inconsistent with the same
> cherry-pick when `--allow-empty` is not specified.

When cherry-picking on top of an unborn branch without the option,
how does the code figure out that we are on an unborn branch?  We
must be doing the detection, as we'd need to at least know the fact
that we are on an unborn in order to make the resulting commit a
root commit and also in order to apply the cherry-picked changes
against an empty tree.

> Treat a failure reading HEAD as an unborn branch in
> `is_index_unchanged`. This is consistent with other sequencer logic such
> as `do_pick_commit`. When on an unborn branch, use the `empty_tree` as
> the tree to compare against.

It is not a good code hygiene to assume that a failure to read HEAD
always means we are on an unborn branch, even if that is the most
likely cause of the failure.  We may instead want to positively
determine that we are on an unborn state, by seeing if the HEAD is a
symbolic ref that points at a ref in refs/heads/* hierarchy, and
that ref does not exist.

But if the existing sequencer code is littered with such loose logic
everywhere, perhaps imitating the looseness for now with a NEEDSWORK
comment to later clean them all up would be the least we could do
right now.  If we want to be more ambitious, a few clean-up patches
that refactors existing code paths that detect that we are on an
unborn branch into a call to a single helper function, and then
tightens its implementation, before doing this step would be even
better (but I offhand do not know how bad the current code is, so I
would understand it if it may turn out to be a lot more work than
you would want to invest in).
Junio C Hamano March 11, 2024, 4:54 p.m. UTC | #2
Junio C Hamano <gitster@pobox.com> writes:

> It is not a good code hygiene to assume that a failure to read HEAD
> always means we are on an unborn branch, even if that is the most
> likely cause of the failure.  We may instead want to positively
> determine that we are on an unborn state, by seeing if the HEAD is a
> symbolic ref that points at a ref in refs/heads/* hierarchy, and
> that ref does not exist.

I suspect that you are almost there.

+	if (!resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, &head_oid, NULL)) {
+		/*
+		 * Treat an error reading HEAD as an unborn branch.
+		 */

After we see this error, if we make a call to resolve_ref_unsafe()
with RESOLVE_REF_NO_RECURSE, the call should return the branch that
we are on but is not yet born, and &oid will get the null_oid.  I am
not sure if there is a way to combine the two calls into one, but
because the failure case (i.e. doing anything on an unborn branch)
is a rare case that happens only once before actually giving birth
to a new branch, it probably is not worth spending extra brain
cycles on it and just use a simple and stupid "when resolving fails,
see if we are in a rare salvageable case with extra code" approach.
Brian Lyles March 12, 2024, 2:04 a.m. UTC | #3
Hi Junio,

On Mon, Mar 11, 2024 at 11:54 AM Junio C Hamano <gitster@pobox.com> wrote:

> Junio C Hamano <gitster@pobox.com> writes:
> 
>> It is not a good code hygiene to assume that a failure to read HEAD
>> always means we are on an unborn branch, even if that is the most
>> likely cause of the failure.  We may instead want to positively
>> determine that we are on an unborn state, by seeing if the HEAD is a
>> symbolic ref that points at a ref in refs/heads/* hierarchy, and
>> that ref does not exist.
> 
> I suspect that you are almost there.
> 
> +	if (!resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, &head_oid, NULL)) {
> +		/*
> +		 * Treat an error reading HEAD as an unborn branch.
> +		 */
> 
> After we see this error, if we make a call to resolve_ref_unsafe()
> with RESOLVE_REF_NO_RECURSE, the call should return the branch that
> we are on but is not yet born, and &oid will get the null_oid.  I am
> not sure if there is a way to combine the two calls into one, but
> because the failure case (i.e. doing anything on an unborn branch)
> is a rare case that happens only once before actually giving birth
> to a new branch, it probably is not worth spending extra brain
> cycles on it and just use a simple and stupid "when resolving fails,
> see if we are in a rare salvageable case with extra code" approach.

If I'm understanding you correctly, it sounds like you're hinting at
something like this:

	if (!resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, &head_oid, NULL)) {
		/*
		 * Check to see if this is an unborn branch
		 */
		if (resolve_ref_unsafe("HEAD", RESOLVE_REF_NO_RECURSE, &head_oid, NULL)
			&& is_null_oid(&head_oid)) {
			head_tree_oid = the_hash_algo->empty_tree;
		} else {
			return error(_("could not resolve HEAD commit"));
		}
	} else {
		...
	}

This does in fact seem to have the same effect as the original patch in
this case. If this is in line with what you are looking for, I can
include this in v4. The commit message would be adjusted slightly to be:

	sequencer: handle unborn branch with `--allow-empty`

	When using git-cherry-pick(1) with `--allow-empty` while on an
	unborn branch, an error is thrown. This is inconsistent with the
	same cherry-pick when `--allow-empty` is not specified.

	Detect unborn branches in `is_index_unchanged`. When on an unborn
	branch, use the `empty_tree` as the tree to compare against.

	...

Do these changes look good?
Junio C Hamano March 12, 2024, 10:25 p.m. UTC | #4
"Brian Lyles" <brianmlyles@gmail.com> writes:

> If I'm understanding you correctly, it sounds like you're hinting at
> something like this:

You may also want to add _READING (I do not know offhand).  Also
you'd want to make sure resolve_ref_unsafe() returned a plausible
looking refname (i.e. passes starts_with("refs/heads/").

But other than that, yeah, doing these as "extra checks" only after
we see the primary resolve_ref("HEAD") fails was what I had in mind.

Thanks.
Phillip Wood March 13, 2024, 3:10 p.m. UTC | #5
Hi Brian

On 10/03/2024 18:42, Brian Lyles wrote:
> - Avoid using `--quiet` in the `git diff` call to make debugging easier
>    in the event of a failure

Sorry, I forgot when I was reviewing v2 that we need to replace --quiet 
with --exit-code otherwise the diff will never fail. Apart from that I 
don't have anything to add to Junio's comments on this patch.

Best Wishes

Phillip
Brian Lyles March 16, 2024, 3:05 a.m. UTC | #6
On Tue, Mar 12, 2024 at 5:25 PM Junio C Hamano <gitster@pobox.com> wrote:

> "Brian Lyles" <brianmlyles@gmail.com> writes:
> 
>> If I'm understanding you correctly, it sounds like you're hinting at
>> something like this:
> 
> You may also want to add _READING (I do not know offhand).  Also
> you'd want to make sure resolve_ref_unsafe() returned a plausible
> looking refname (i.e. passes starts_with("refs/heads/").
> 
> But other than that, yeah, doing these as "extra checks" only after
> we see the primary resolve_ref("HEAD") fails was what I had in mind.
> 
> Thanks.

Makes sense to me. From reading the documentation for
RESOLVE_REF_READING, I think we do want that as well, and the
starts_with("refs/heads/") check works as expected too. I'll incorporate
those into v4.
Brian Lyles March 16, 2024, 3:07 a.m. UTC | #7
Hi Phillip

On Wed, Mar 13, 2024 at 10:10 AM <phillip.wood123@gmail.com> wrote:

> Hi Brian
> 
> On 10/03/2024 18:42, Brian Lyles wrote:
>> - Avoid using `--quiet` in the `git diff` call to make debugging easier
>>    in the event of a failure
> 
> Sorry, I forgot when I was reviewing v2 that we need to replace --quiet 
> with --exit-code otherwise the diff will never fail. Apart from that I 
> don't have anything to add to Junio's comments on this patch.

Ah, of course -- thank you for catching that. I'll include that in v4.
diff mbox series

Patch

diff --git a/sequencer.c b/sequencer.c
index f49a871ac0..a62ce244c1 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -770,29 +770,36 @@  static struct object_id *get_cache_tree_oid(struct index_state *istate)
 static int is_index_unchanged(struct repository *r)
 {
 	struct object_id head_oid, *cache_tree_oid;
+	const struct object_id *head_tree_oid;
 	struct commit *head_commit;
 	struct index_state *istate = r->index;
 
-	if (!resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, &head_oid, NULL))
-		return error(_("could not resolve HEAD commit"));
+	if (!resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, &head_oid, NULL)) {
+		/*
+		 * Treat an error reading HEAD as an unborn branch.
+		 */
+		head_tree_oid = the_hash_algo->empty_tree;
+	} else {
+		head_commit = lookup_commit(r, &head_oid);
 
-	head_commit = lookup_commit(r, &head_oid);
+		/*
+		 * If head_commit is NULL, check_commit, called from
+		 * lookup_commit, would have indicated that head_commit is not
+		 * a commit object already.  repo_parse_commit() will return failure
+		 * without further complaints in such a case.  Otherwise, if
+		 * the commit is invalid, repo_parse_commit() will complain.  So
+		 * there is nothing for us to say here.  Just return failure.
+		 */
+		if (repo_parse_commit(r, head_commit))
+			return -1;
 
-	/*
-	 * If head_commit is NULL, check_commit, called from
-	 * lookup_commit, would have indicated that head_commit is not
-	 * a commit object already.  repo_parse_commit() will return failure
-	 * without further complaints in such a case.  Otherwise, if
-	 * the commit is invalid, repo_parse_commit() will complain.  So
-	 * there is nothing for us to say here.  Just return failure.
-	 */
-	if (repo_parse_commit(r, head_commit))
-		return -1;
+		head_tree_oid = get_commit_tree_oid(head_commit);
+	}
 
 	if (!(cache_tree_oid = get_cache_tree_oid(istate)))
 		return -1;
 
-	return oideq(cache_tree_oid, get_commit_tree_oid(head_commit));
+	return oideq(cache_tree_oid, head_tree_oid);
 }
 
 static int write_author_script(const char *message)
diff --git a/t/t3501-revert-cherry-pick.sh b/t/t3501-revert-cherry-pick.sh
index aeab689a98..8a1d154ca6 100755
--- a/t/t3501-revert-cherry-pick.sh
+++ b/t/t3501-revert-cherry-pick.sh
@@ -104,11 +104,19 @@  test_expect_success 'revert forbidden on dirty working tree' '
 '
 
 test_expect_success 'cherry-pick on unborn branch' '
-	git checkout --orphan unborn &&
+	git switch --orphan unborn &&
 	git rm --cached -r . &&
-	rm -rf * &&
 	git cherry-pick initial &&
-	git diff --quiet initial &&
+	git diff initial &&
+	test_cmp_rev ! initial HEAD
+'
+
+test_expect_success 'cherry-pick on unborn branch with --allow-empty' '
+	git checkout --detach &&
+	git branch -D unborn &&
+	git switch --orphan unborn &&
+	git cherry-pick initial --allow-empty &&
+	git diff initial &&
 	test_cmp_rev ! initial HEAD
 '