* [PATCH 0/2] replay: die descriptively when invalid commit-ish
@ 2025-12-22 22:04 kristofferhaugsbakk
2025-12-22 22:04 ` [PATCH 1/2] " kristofferhaugsbakk
` (4 more replies)
0 siblings, 5 replies; 24+ messages in thread
From: kristofferhaugsbakk @ 2025-12-22 22:04 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, christian.couder, newren, Siddharth Asthana
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
You get this error when you for example mistype the argument to
`--onto`:
fatal: Replaying down to root commit is not supported yet!
Consider that you might not know yourself that you have mistyped
something; then this looks even more puzzling. You might have given a
range like `main..topic` but the command says that it would need to
replay down to the root commit.
The only thing that’s happened though is that `NULL` has been
interpreted in the wrong way.
Let’s instead die immediately when the real error happens, in other
words when we can’t find the commit for the given commit-ish.
Also add more regression tests.
Somewhat unrelated to this change—and caveat not a C programmer—I was
confused by `determine_replay_mode`. Most of the function deals with
three cases:
if (onto_name) {
...
} else if (*advance_name) {
...
} else {
...
}
But I don’t get the `else` since you now need to provide either `--onto`
or `--advance`. And both require an argument. So when can the
fallthrough `else` happen?
I thought that maybe the `else` was old code that predated `--onto` and
`--advance` being mandatory. But that whole method and this check was
added in the same commit, in 22d99f01 (replay: add --advance or
'cherry-pick' mode, 2023-11-24):
if (!onto_name && !advance_name) {
error(_("option --onto or --advance is mandatory"));
But just ignore this section if I’m simply confused.
Kristoffer Haugsbakk (2):
replay: die descriptively when invalid commit-ish
t3650: add more regression tests for failure conditions
builtin/replay.c | 2 +-
t/t3650-replay-basics.sh | 43 ++++++++++++++++++++++++++++++++++++++++
2 files changed, 44 insertions(+), 1 deletion(-)
base-commit: 9a2fb147f2c61d0cab52c883e7e26f5b7948e3ed
--
2.52.0.10.g08704017180
^ permalink raw reply [flat|nested] 24+ messages in thread
* [PATCH 1/2] replay: die descriptively when invalid commit-ish
2025-12-22 22:04 [PATCH 0/2] replay: die descriptively when invalid commit-ish kristofferhaugsbakk
@ 2025-12-22 22:04 ` kristofferhaugsbakk
2025-12-23 3:12 ` Junio C Hamano
2025-12-22 22:04 ` [PATCH 2/2] t3650: add more regression tests for failure conditions kristofferhaugsbakk
` (3 subsequent siblings)
4 siblings, 1 reply; 24+ messages in thread
From: kristofferhaugsbakk @ 2025-12-22 22:04 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, christian.couder, newren, Siddharth Asthana
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
Giving an invalid commit-ish to `--onto` or `--advance` makes
git-replay(1) fail with:
fatal: Replaying down to root commit is not supported yet!
Going backwards from this point:
1. `onto` is `NULL` from `determine_replay_mode`;
2. that function in turn calls `peel_committish`; and
3. here we return `NULL` if `repo_get_oid` fails.
Let’s die immediately with a descriptive error message instead.
Doing this also provides us with a descriptive error if we “forget” to
provide an argument to `--onto` (but we really do unintentionally):[1]
$ git replay --onto ^main topic1
fatal: '^main' is not a valid commit-ish
† 1: The argument to `--onto` is mandatory and the option parser accepts
both `--onto=<name>` (stuck form) and `--onto name`. The latter
form makes it easy to unintentionally pass something to the option
when you really meant to pass a positional argument.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
builtin/replay.c | 2 +-
t/t3650-replay-basics.sh | 16 ++++++++++++++++
2 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/builtin/replay.c b/builtin/replay.c
index 6172c8aacc9..175b64c5335 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -33,7 +33,7 @@ static struct commit *peel_committish(struct repository *repo, const char *name)
struct object_id oid;
if (repo_get_oid(repo, name, &oid))
- return NULL;
+ die(_("'%s' is not a valid commit-ish"), name);
obj = parse_object(repo, &oid);
return (struct commit *)repo_peel_to_type(repo, name, 0, obj,
OBJ_COMMIT);
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 58b37599357..bfe8e01da49 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -51,6 +51,22 @@ test_expect_success 'setup bare' '
git clone --bare . bare
'
+test_expect_success '--onto with invalid commit-ish' '
+ cat >expect <<-EOF &&
+ fatal: ${SQ}refs/not-valid${SQ} is not a valid commit-ish
+ EOF
+ test_must_fail git replay --onto=refs/not-valid topic1..topic2 2>actual &&
+ test_cmp expect actual
+'
+
+test_expect_success '--advance with invalid commit-ish' '
+ cat >expect <<-EOF &&
+ fatal: ${SQ}refs/not-valid${SQ} is not a valid commit-ish
+ EOF
+ test_must_fail git replay --advance=refs/not-valid topic1..topic2 2>actual &&
+ test_cmp expect actual
+'
+
test_expect_success 'using replay to rebase two branches, one on top of other' '
git replay --onto main topic1..topic2 >result &&
--
2.52.0.10.g08704017180
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH 2/2] t3650: add more regression tests for failure conditions
2025-12-22 22:04 [PATCH 0/2] replay: die descriptively when invalid commit-ish kristofferhaugsbakk
2025-12-22 22:04 ` [PATCH 1/2] " kristofferhaugsbakk
@ 2025-12-22 22:04 ` kristofferhaugsbakk
2025-12-23 10:58 ` Phillip Wood
2025-12-23 3:16 ` [PATCH 0/2] replay: die descriptively when invalid commit-ish Junio C Hamano
` (2 subsequent siblings)
4 siblings, 1 reply; 24+ messages in thread
From: kristofferhaugsbakk @ 2025-12-22 22:04 UTC (permalink / raw)
To: git; +Cc: Kristoffer Haugsbakk, christian.couder, newren, Siddharth Asthana
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
There isn’t much test coverage for basic failure conditions. Let’s add
a few more since these are simple to write and remove if they become
obsolete.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
t/t3650-replay-basics.sh | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index bfe8e01da49..c543d55857b 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -67,6 +67,33 @@ test_expect_success '--advance with invalid commit-ish' '
test_cmp expect actual
'
+test_expect_success 'option --onto or --advance is mandatory' '
+ cat >expect <<-\EOF &&
+ error: option --onto or --advance is mandatory
+ EOF
+ # First line is the error; rest is Usage
+ test_must_fail git replay topic1..topic2 >&1 2>&1 |
+ head -1 >actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'no base or negative ref gives no-replaying down to root error' '
+ cat >expect <<-\EOF &&
+ fatal: replaying down to root commit is not supported yet!
+ EOF
+ test_must_fail git replay --onto=topic1 topic2 2>actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'options --advance and --contained cannot be used together' '
+ cat >expect <<-EOF &&
+ fatal: options ${SQ}--advance${SQ} and ${SQ}--contained${SQ} cannot be used together
+ EOF
+ test_must_fail git replay --advance=main --contained \
+ topic1..topic2 2>actual &&
+ test_cmp expect actual
+'
+
test_expect_success 'using replay to rebase two branches, one on top of other' '
git replay --onto main topic1..topic2 >result &&
--
2.52.0.10.g08704017180
^ permalink raw reply related [flat|nested] 24+ messages in thread
* Re: [PATCH 1/2] replay: die descriptively when invalid commit-ish
2025-12-22 22:04 ` [PATCH 1/2] " kristofferhaugsbakk
@ 2025-12-23 3:12 ` Junio C Hamano
2025-12-23 10:52 ` Phillip Wood
0 siblings, 1 reply; 24+ messages in thread
From: Junio C Hamano @ 2025-12-23 3:12 UTC (permalink / raw)
To: kristofferhaugsbakk
Cc: git, Kristoffer Haugsbakk, christian.couder, newren,
Siddharth Asthana
kristofferhaugsbakk@fastmail.com writes:
> diff --git a/builtin/replay.c b/builtin/replay.c
> index 6172c8aacc9..175b64c5335 100644
> --- a/builtin/replay.c
> +++ b/builtin/replay.c
> @@ -33,7 +33,7 @@ static struct commit *peel_committish(struct repository *repo, const char *name)
> struct object_id oid;
>
> if (repo_get_oid(repo, name, &oid))
> - return NULL;
> + die(_("'%s' is not a valid commit-ish"), name);
This is after repo_get_oid() fails to turn the "name" into an oid.
The only thing we know about "name" is that it does not name an
object, but we want to get a commit-ish and the new message sounds
like a reasonable way to tell both of these two facts.
> obj = parse_object(repo, &oid);
> return (struct commit *)repo_peel_to_type(repo, name, 0, obj,
> OBJ_COMMIT);
The previous parse_object() can return NULL, in which case
repo_peel_to_type() would also silently return NULL.
If obj is not NULL, repo_peel_to_type() would die with a descriptive
message when the thing does not peel to an object of the expected
type.
So the caller of this function still needs to be prepared for
receiving a NULL from here.
How many callers use this function? I am wondering if it is better
to give a better message at the caller(s), rather than here, where
we lack context to tell something like "You gave string 'ource' as
the argument to the '--onto' option, but 'ource' does not name any
commit" (in other words, "for what our caller is trying to peel
<name> to a commit").
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH 0/2] replay: die descriptively when invalid commit-ish
2025-12-22 22:04 [PATCH 0/2] replay: die descriptively when invalid commit-ish kristofferhaugsbakk
2025-12-22 22:04 ` [PATCH 1/2] " kristofferhaugsbakk
2025-12-22 22:04 ` [PATCH 2/2] t3650: add more regression tests for failure conditions kristofferhaugsbakk
@ 2025-12-23 3:16 ` Junio C Hamano
2025-12-30 14:33 ` Kristoffer Haugsbakk
2025-12-24 3:03 ` Elijah Newren
2025-12-30 15:01 ` [PATCH v2 0/5] " kristofferhaugsbakk
4 siblings, 1 reply; 24+ messages in thread
From: Junio C Hamano @ 2025-12-23 3:16 UTC (permalink / raw)
To: kristofferhaugsbakk
Cc: git, Kristoffer Haugsbakk, christian.couder, newren,
Siddharth Asthana
kristofferhaugsbakk@fastmail.com writes:
> Subject: Re: [PATCH 0/2] replay: die descriptively when invalid commit-ish
"... is given" or something?
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH 1/2] replay: die descriptively when invalid commit-ish
2025-12-23 3:12 ` Junio C Hamano
@ 2025-12-23 10:52 ` Phillip Wood
2025-12-23 13:41 ` Junio C Hamano
2025-12-30 14:30 ` Kristoffer Haugsbakk
0 siblings, 2 replies; 24+ messages in thread
From: Phillip Wood @ 2025-12-23 10:52 UTC (permalink / raw)
To: Junio C Hamano, kristofferhaugsbakk
Cc: git, Kristoffer Haugsbakk, christian.couder, newren,
Siddharth Asthana
On 23/12/2025 03:12, Junio C Hamano wrote:
> kristofferhaugsbakk@fastmail.com writes:
>
> How many callers use this function? I am wondering if it is better
> to give a better message at the caller(s), rather than here, where
> we lack context to tell something like "You gave string 'ource' as
> the argument to the '--onto' option, but 'ource' does not name any
> commit" (in other words, "for what our caller is trying to peel
> <name> to a commit").
There are only two callers so I think that is a good idea. If you give
an invalid commit name to "--advance" then it dies with
fatal: argument to --advance must be a reference
so arguably we only need to check the return value when parsing "--onto"
Thanks
Phillip
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH 2/2] t3650: add more regression tests for failure conditions
2025-12-22 22:04 ` [PATCH 2/2] t3650: add more regression tests for failure conditions kristofferhaugsbakk
@ 2025-12-23 10:58 ` Phillip Wood
2025-12-30 14:33 ` Kristoffer Haugsbakk
0 siblings, 1 reply; 24+ messages in thread
From: Phillip Wood @ 2025-12-23 10:58 UTC (permalink / raw)
To: kristofferhaugsbakk, git
Cc: Kristoffer Haugsbakk, christian.couder, newren, Siddharth Asthana
On 22/12/2025 22:04, kristofferhaugsbakk@fastmail.com wrote:
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>
> There isn’t much test coverage for basic failure conditions. Let’s add
> a few more since these are simple to write and remove if they become
> obsolete.
Sounds like a good idea
>
> +test_expect_success 'option --onto or --advance is mandatory' '
> + cat >expect <<-\EOF &&
> + error: option --onto or --advance is mandatory
> + EOF
> + # First line is the error; rest is Usage
> + test_must_fail git replay topic1..topic2 >&1 2>&1 |
> + head -1 >actual &&
Using a pipe means we lose the return value of test_must_fail here so
the test wont fail if the command succeeds. Everything else looks good
Thanks
Phillip
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'no base or negative ref gives no-replaying down to root error' '
> + cat >expect <<-\EOF &&
> + fatal: replaying down to root commit is not supported yet!
> + EOF
> + test_must_fail git replay --onto=topic1 topic2 2>actual &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'options --advance and --contained cannot be used together' '
> + cat >expect <<-EOF &&
> + fatal: options ${SQ}--advance${SQ} and ${SQ}--contained${SQ} cannot be used together
> + EOF
> + test_must_fail git replay --advance=main --contained \
> + topic1..topic2 2>actual &&
> + test_cmp expect actual
> +'
> +
> test_expect_success 'using replay to rebase two branches, one on top of other' '
> git replay --onto main topic1..topic2 >result &&
>
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH 1/2] replay: die descriptively when invalid commit-ish
2025-12-23 10:52 ` Phillip Wood
@ 2025-12-23 13:41 ` Junio C Hamano
2025-12-30 14:30 ` Kristoffer Haugsbakk
1 sibling, 0 replies; 24+ messages in thread
From: Junio C Hamano @ 2025-12-23 13:41 UTC (permalink / raw)
To: Phillip Wood
Cc: kristofferhaugsbakk, git, Kristoffer Haugsbakk, christian.couder,
newren, Siddharth Asthana
Phillip Wood <phillip.wood123@gmail.com> writes:
> There are only two callers so I think that is a good idea. If you give
> an invalid commit name to "--advance" then it dies with
>
> fatal: argument to --advance must be a reference
>
> so arguably we only need to check the return value when parsing "--onto"
So, in determine_replay_mode(), ...
if (onto_name) {
*onto = peel_committish(repo, onto_name);
... here is where we must see *onto is NULL and barf and then ...
if (rinfo.positive_refexprs <
strset_get_size(&rinfo.positive_refs))
die(_("all positive revisions given must be references"));
} else if (*advance_name) {
struct object_id oid;
char *fullname = NULL;
*onto = peel_committish(repo, *advance_name);
... for symmetry, we would want to do the same.
In addition, we probably would want to let the following code that
uses the same *advance_name (and requires that it just not names a
commit-ish object, but is actually a ref, which is a different
requirement that is probably a bit tighter) ...
if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),
&oid, &fullname, 0) == 1) {
free(*advance_name);
*advance_name = fullname;
} else {
die(_("argument to --advance must be a reference"));
}
... first, and then compute *onto after that by moving code a bit,
perhaps?
Thanks.
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH 0/2] replay: die descriptively when invalid commit-ish
2025-12-22 22:04 [PATCH 0/2] replay: die descriptively when invalid commit-ish kristofferhaugsbakk
` (2 preceding siblings ...)
2025-12-23 3:16 ` [PATCH 0/2] replay: die descriptively when invalid commit-ish Junio C Hamano
@ 2025-12-24 3:03 ` Elijah Newren
2025-12-30 14:31 ` Kristoffer Haugsbakk
2025-12-30 15:01 ` [PATCH v2 0/5] " kristofferhaugsbakk
4 siblings, 1 reply; 24+ messages in thread
From: Elijah Newren @ 2025-12-24 3:03 UTC (permalink / raw)
To: kristofferhaugsbakk
Cc: git, Kristoffer Haugsbakk, christian.couder, Siddharth Asthana
On Mon, Dec 22, 2025 at 2:04 PM <kristofferhaugsbakk@fastmail.com> wrote:
>
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>
> You get this error when you for example mistype the argument to
> `--onto`:
>
> fatal: Replaying down to root commit is not supported yet!
>
> Consider that you might not know yourself that you have mistyped
> something; then this looks even more puzzling. You might have given a
> range like `main..topic` but the command says that it would need to
> replay down to the root commit.
>
> The only thing that’s happened though is that `NULL` has been
> interpreted in the wrong way.
>
> Let’s instead die immediately when the real error happens, in other
> words when we can’t find the commit for the given commit-ish.
>
> Also add more regression tests.
Thanks for working on this. I have nothing to add beyond what others
have already commented on the series, except that I can maybe answer
one question...
> Somewhat unrelated to this change—and caveat not a C programmer—I was
> confused by `determine_replay_mode`. Most of the function deals with
> three cases:
>
> if (onto_name) {
> ...
> } else if (*advance_name) {
> ...
> } else {
> ...
> }
>
> But I don’t get the `else` since you now need to provide either `--onto`
> or `--advance`. And both require an argument. So when can the
> fallthrough `else` happen?
>
> I thought that maybe the `else` was old code that predated `--onto` and
> `--advance` being mandatory. But that whole method and this check was
> added in the same commit, in 22d99f01 (replay: add --advance or
> 'cherry-pick' mode, 2023-11-24):
>
> if (!onto_name && !advance_name) {
> error(_("option --onto or --advance is mandatory"));
>
> But just ignore this section if I’m simply confused.
Your assumption was right -- that the final `else` was old code that
predated `--onto` and `--advance` being mandatory. git-replay was
originally developed in my private branch, but I let other folks know
I was experimenting with it. When life pulled me away from git,
Christian and Johannes expressed interest in the command, and
Christian cleaned up my rough patches, stripping out the clearly
half-baked stuff, and also removing some additional things that didn't
need to be in the initial version, and sent it upstream. None of us
noticed this `else` had turned into dead code in the review.
The original had the idea that it would be able to sometimes guess the
mode and allow for shorter command lines, but I was still playing
around with it and haven't particularly felt the need to push that
idea.
Even if we eventually decide we want that other handling, removing
this `else` clause for now and allowing it to be reintroduced later
would probably make for a more coherent patch anyway.
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH 1/2] replay: die descriptively when invalid commit-ish
2025-12-23 10:52 ` Phillip Wood
2025-12-23 13:41 ` Junio C Hamano
@ 2025-12-30 14:30 ` Kristoffer Haugsbakk
1 sibling, 0 replies; 24+ messages in thread
From: Kristoffer Haugsbakk @ 2025-12-30 14:30 UTC (permalink / raw)
To: Phillip Wood, Junio C Hamano
Cc: git, Kristoffer Haugsbakk, Christian Couder, Elijah Newren,
Siddharth Asthana
On Tue, Dec 23, 2025, at 11:52, Phillip Wood wrote:
> On 23/12/2025 03:12, Junio C Hamano wrote:
>> kristofferhaugsbakk@fastmail.com writes:
>>
>> How many callers use this function? I am wondering if it is better
>> to give a better message at the caller(s), rather than here, where
>> we lack context to tell something like "You gave string 'ource' as
>> the argument to the '--onto' option, but 'ource' does not name any
>> commit" (in other words, "for what our caller is trying to peel
>> <name> to a commit").
>
> There are only two callers so I think that is a good idea. If you give
> an invalid commit name to "--advance" then it dies with
>
> fatal: argument to --advance must be a reference
>
> so arguably we only need to check the return value when parsing "--onto"
Well spotted. My change would give a worse error message
for `--advance`.
I’ve made the move-line change that Junio suggested in version 2.
Thanks.
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH 0/2] replay: die descriptively when invalid commit-ish
2025-12-24 3:03 ` Elijah Newren
@ 2025-12-30 14:31 ` Kristoffer Haugsbakk
0 siblings, 0 replies; 24+ messages in thread
From: Kristoffer Haugsbakk @ 2025-12-30 14:31 UTC (permalink / raw)
To: Elijah Newren, Kristoffer Haugsbakk
Cc: git, Christian Couder, Siddharth Asthana
On Wed, Dec 24, 2025, at 04:03, Elijah Newren wrote:
> On Mon, Dec 22, 2025 at 2:04 PM <kristofferhaugsbakk@fastmail.com> wrote:
>>[snip]
>
> Thanks for working on this. I have nothing to add beyond what others
> have already commented on the series, except that I can maybe answer
> one question...
>
>[snip]
Thanks for explaining! I’ve removed this code in version 2.
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH 0/2] replay: die descriptively when invalid commit-ish
2025-12-23 3:16 ` [PATCH 0/2] replay: die descriptively when invalid commit-ish Junio C Hamano
@ 2025-12-30 14:33 ` Kristoffer Haugsbakk
0 siblings, 0 replies; 24+ messages in thread
From: Kristoffer Haugsbakk @ 2025-12-30 14:33 UTC (permalink / raw)
To: Junio C Hamano, Kristoffer Haugsbakk
Cc: git, Christian Couder, Elijah Newren, Siddharth Asthana
On Tue, Dec 23, 2025, at 04:16, Junio C Hamano wrote:
> kristofferhaugsbakk@fastmail.com writes:
>
>> Subject: Re: [PATCH 0/2] replay: die descriptively when invalid commit-ish
>
> "... is given" or something?
Sure thing.
I’m a bit paranoid of the soft 50 character max that git-commit(1)
recommends (and that might practically matter for email-based projects).
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH 2/2] t3650: add more regression tests for failure conditions
2025-12-23 10:58 ` Phillip Wood
@ 2025-12-30 14:33 ` Kristoffer Haugsbakk
0 siblings, 0 replies; 24+ messages in thread
From: Kristoffer Haugsbakk @ 2025-12-30 14:33 UTC (permalink / raw)
To: Phillip Wood, Kristoffer Haugsbakk, git
Cc: Christian Couder, Elijah Newren, Siddharth Asthana
On Tue, Dec 23, 2025, at 11:58, Phillip Wood wrote:
>>[snip]
>> +test_expect_success 'option --onto or --advance is mandatory' '
>> + cat >expect <<-\EOF &&
>> + error: option --onto or --advance is mandatory
>> + EOF
>> + # First line is the error; rest is Usage
>> + test_must_fail git replay topic1..topic2 >&1 2>&1 |
>> + head -1 >actual &&
>
> Using a pipe means we lose the return value of test_must_fail here so
> the test wont fail if the command succeeds. Everything else looks good
Thanks. That’s kind of subtle for me. I’ll fix.
I’ll just test for the full output instead of the first line.
^ permalink raw reply [flat|nested] 24+ messages in thread
* [PATCH v2 0/5] replay: die descriptively when invalid commit-ish
2025-12-22 22:04 [PATCH 0/2] replay: die descriptively when invalid commit-ish kristofferhaugsbakk
` (3 preceding siblings ...)
2025-12-24 3:03 ` Elijah Newren
@ 2025-12-30 15:01 ` kristofferhaugsbakk
2025-12-30 15:01 ` [PATCH v2 1/5] replay: remove dead code and rearrange kristofferhaugsbakk
` (5 more replies)
4 siblings, 6 replies; 24+ messages in thread
From: kristofferhaugsbakk @ 2025-12-30 15:01 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, christian.couder, newren, Siddharth Asthana,
Phillip Wood
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
You get this error when you for example mistype the argument to
`--onto`:
fatal: Replaying down to root commit is not supported yet!
Consider that you might not know yourself that you have mistyped
something; then this looks even more puzzling. You might have given a
range like `main..topic` but the command says that it would need to
replay down to the root commit.
The only thing that’s happened though is that `NULL` has been
interpreted in the wrong way.
Let’s instead die immediately when the real error happens, in other
words when we can’t find the commit for the given commit-ish.
Also:
• Add more regression tests
• Remove dead code
• Slightly more robust object parsing (parse_object_or_die)
§ Dropped section (see previous)
Somewhat unrelated to this change ...
§ Changes in v2
In this version we remove some dead code (see previous section), improve
tests and add a couple more, and improve the main “die descriptively”
part, with two additional patches/commits to that end:
• replay: find *onto only after testing for ref name
• replay: die if we cannot parse object
See the notes on each patch/commit for change details.
§ Link to v1
https://lore.kernel.org/git/CV_replay_die_descr.13f@msgid.xyz/
❦ ❦ ❦ ❦ ❦
(this means you can stop reading here)
§ How I Present Patch Series
Version: 2
The cover letter:
• Problem statement and solution
• (optional) Summary of what each commit/patch does
• Changes compared to the previous version
• Note that all versions are not included
• (optional) “CC”/“Cc” which explains all or parts of the CC list
• But some things are not noteworthy, like the standard practice of
including previous-round respondents
• Link to the previous version
• This is new. I thought it was redundant but some people (like me!)
use webapp email clients which are not that great for navigating
among tree-like email threads. So I was inspired by this practice
which many others already use.
Then each commit/patch might have Git notes with a `series` namespace.
These contain:
• Comments/questions to reviewers about my approach or statements made
in the commit message.
• May quote the commit message or emails (with links)
• Lines starting with `v<version>:` which introduce a changelog for that
version. Might have `v<num>: [new]` if this version is 2+ and the
patch/commit is new. Now, lately (Sep 2025) these have been written in
the “imperative mood”, like what is done for a commit message in this
(Git) project. A bit strange, given that most others seem to use the
more immediately natural past-tense. But on the other hand: how many
new contributors to this project use the wrong tense/mood in their
commit messages? The Git project rule is not “natural”. But I think
it’s better nonetheless and worth the effort.
• ... But this kind of changelog might also be conducive to a bullet
list of changes. So I might either skip the previous point, do only a
bullet point, or do both: a presentation and then the bullet points
summarizing the presentation.
• Note that all `v<version>:` are kept between versions, which is not
consistent with how I only have the “Changes” part for the previous
version in the cover letter.
• `v<version>:` are ordered newest-to-oldest.
Kristoffer Haugsbakk (5):
replay: remove dead code and rearrange
replay: find *onto only after testing for ref name
replay: die descriptively when invalid commit-ish is given
replay: die if we cannot parse object
t3650: add more regression tests for failure conditions
builtin/replay.c | 87 +++++++++++-----------------------------
t/t3650-replay-basics.sh | 54 +++++++++++++++++++++++++
2 files changed, 78 insertions(+), 63 deletions(-)
Interdiff against v1:
diff --git a/builtin/replay.c b/builtin/replay.c
index 175b64c5335..ca5a14de4c7 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -25,18 +25,20 @@ static const char *short_commit_name(struct repository *repo,
{
return repo_find_unique_abbrev(repo, &commit->object.oid,
DEFAULT_ABBREV);
}
-static struct commit *peel_committish(struct repository *repo, const char *name)
+static struct commit *peel_committish(struct repository *repo,
+ const char *name,
+ const char *mode)
{
struct object *obj;
struct object_id oid;
if (repo_get_oid(repo, name, &oid))
- die(_("'%s' is not a valid commit-ish"), name);
- obj = parse_object(repo, &oid);
+ die(_("'%s' is not a valid commit-ish for %s"), name, mode);
+ obj = parse_object_or_die(repo, &oid, name);
return (struct commit *)repo_peel_to_type(repo, name, 0, obj,
OBJ_COMMIT);
}
static char *get_author(const char *message)
@@ -154,89 +156,50 @@ static void get_ref_information(struct repository *repo,
free(fullname);
}
}
-static void determine_replay_mode(struct repository *repo,
- struct rev_cmdline_info *cmd_info,
- const char *onto_name,
- char **advance_name,
- struct commit **onto,
- struct strset **update_refs)
+static void populate_for_onto_or_advance_mode(struct repository *repo,
+ struct rev_cmdline_info *cmd_info,
+ const char *onto_name,
+ char **advance_name,
+ struct commit **onto,
+ struct strset **update_refs)
{
struct ref_info rinfo;
get_ref_information(repo, cmd_info, &rinfo);
if (!rinfo.positive_refexprs)
die(_("need some commits to replay"));
die_for_incompatible_opt2(!!onto_name, "--onto",
!!*advance_name, "--advance");
if (onto_name) {
- *onto = peel_committish(repo, onto_name);
+ *onto = peel_committish(repo, onto_name, "--onto");
if (rinfo.positive_refexprs <
strset_get_size(&rinfo.positive_refs))
die(_("all positive revisions given must be references"));
- } else if (*advance_name) {
+ *update_refs = xcalloc(1, sizeof(**update_refs));
+ **update_refs = rinfo.positive_refs;
+ memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
+ } else {
struct object_id oid;
char *fullname = NULL;
- *onto = peel_committish(repo, *advance_name);
+ if (!*advance_name)
+ BUG("expected either onto_name or *advance_name in this function");
+
if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),
&oid, &fullname, 0) == 1) {
free(*advance_name);
*advance_name = fullname;
} else {
die(_("argument to --advance must be a reference"));
}
+ *onto = peel_committish(repo, *advance_name, "--advance");
if (rinfo.positive_refexprs > 1)
die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
- } else {
- int positive_refs_complete = (
- rinfo.positive_refexprs ==
- strset_get_size(&rinfo.positive_refs));
- int negative_refs_complete = (
- rinfo.negative_refexprs ==
- strset_get_size(&rinfo.negative_refs));
- /*
- * We need either positive_refs_complete or
- * negative_refs_complete, but not both.
- */
- if (rinfo.negative_refexprs > 0 &&
- positive_refs_complete == negative_refs_complete)
- die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
- if (negative_refs_complete) {
- struct hashmap_iter iter;
- struct strmap_entry *entry;
- const char *last_key = NULL;
-
- if (rinfo.negative_refexprs == 0)
- die(_("all positive revisions given must be references"));
- else if (rinfo.negative_refexprs > 1)
- die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
- else if (rinfo.positive_refexprs > 1)
- die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
-
- /* Only one entry, but we have to loop to get it */
- strset_for_each_entry(&rinfo.negative_refs,
- &iter, entry) {
- last_key = entry->key;
- }
-
- free(*advance_name);
- *advance_name = xstrdup_or_null(last_key);
- } else { /* positive_refs_complete */
- if (rinfo.negative_refexprs > 1)
- die(_("cannot implicitly determine correct base for --onto"));
- if (rinfo.negative_refexprs == 1)
- *onto = rinfo.onto;
- }
- }
- if (!*advance_name) {
- *update_refs = xcalloc(1, sizeof(**update_refs));
- **update_refs = rinfo.positive_refs;
- memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
}
strset_clear(&rinfo.negative_refs);
strset_clear(&rinfo.positive_refs);
}
@@ -384,15 +347,13 @@ int cmd_replay(int argc,
"'%s' bit in 'struct rev_info' will be forced"),
"simplify_history");
revs.simplify_history = 0;
}
- determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
- &onto, &update_refs);
-
- if (!onto) /* FIXME: Should handle replaying down to root commit */
- die("Replaying down to root commit is not supported yet!");
+ populate_for_onto_or_advance_mode(repo, &revs.cmdline,
+ onto_name, &advance_name,
+ &onto, &update_refs);
if (prepare_revision_walk(&revs) < 0) {
ret = error(_("error preparing revisions"));
goto cleanup;
}
@@ -405,11 +366,11 @@ int cmd_replay(int argc,
while ((commit = get_revision(&revs))) {
const struct name_decoration *decoration;
khint_t pos;
int hr;
- if (!commit->parents)
+ if (!commit->parents) /* FIXME: Should handle replaying down to root commit */
die(_("replaying down to root commit is not supported yet!"));
if (commit->parents->next)
die(_("replaying merge commits is not supported yet!"));
last_commit = pick_regular_commit(repo, commit, replayed_commits,
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index c543d55857b..c0c59ae6938 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -41,61 +41,72 @@ test_expect_success 'setup' '
git switch main &&
test_commit L &&
test_commit M &&
+ git switch --detach topic4 &&
+ test_commit N &&
+ test_commit O &&
+ git switch -c topic-with-merge topic4 &&
+ test_merge P O --no-ff &&
+ git switch main &&
+
git switch -c conflict B &&
test_commit C.conflict C.t conflict
'
test_expect_success 'setup bare' '
git clone --bare . bare
'
-test_expect_success '--onto with invalid commit-ish' '
- cat >expect <<-EOF &&
- fatal: ${SQ}refs/not-valid${SQ} is not a valid commit-ish
- EOF
- test_must_fail git replay --onto=refs/not-valid topic1..topic2 2>actual &&
+test_expect_success 'argument to --advance must be a reference' '
+ echo "fatal: argument to --advance must be a reference" >expect &&
+ oid=$(git rev-parse main) &&
+ test_must_fail git replay --advance=$oid topic1..topic2 2>actual &&
test_cmp expect actual
'
-test_expect_success '--advance with invalid commit-ish' '
- cat >expect <<-EOF &&
- fatal: ${SQ}refs/not-valid${SQ} is not a valid commit-ish
- EOF
- test_must_fail git replay --advance=refs/not-valid topic1..topic2 2>actual &&
+test_expect_success '--onto with invalid commit-ish' '
+ printf "fatal: ${SQ}refs/not-valid${SQ} is not " >expect &&
+ printf "a valid commit-ish for --onto\n" >>expect &&
+ test_must_fail git replay --onto=refs/not-valid topic1..topic2 2>actual &&
test_cmp expect actual
'
test_expect_success 'option --onto or --advance is mandatory' '
- cat >expect <<-\EOF &&
- error: option --onto or --advance is mandatory
- EOF
- # First line is the error; rest is Usage
- test_must_fail git replay topic1..topic2 >&1 2>&1 |
- head -1 >actual &&
+ echo "error: option --onto or --advance is mandatory" >expect &&
+ test_might_fail git replay -h >>expect &&
+ test_must_fail git replay topic1..topic2 2>actual &&
test_cmp expect actual
'
test_expect_success 'no base or negative ref gives no-replaying down to root error' '
- cat >expect <<-\EOF &&
- fatal: replaying down to root commit is not supported yet!
- EOF
+ echo "fatal: replaying down to root commit is not supported yet!" >expect &&
test_must_fail git replay --onto=topic1 topic2 2>actual &&
test_cmp expect actual
'
test_expect_success 'options --advance and --contained cannot be used together' '
- cat >expect <<-EOF &&
- fatal: options ${SQ}--advance${SQ} and ${SQ}--contained${SQ} cannot be used together
- EOF
+ printf "fatal: options ${SQ}--advance${SQ} " >expect &&
+ printf "and ${SQ}--contained${SQ} cannot be used together\n" >>expect &&
test_must_fail git replay --advance=main --contained \
topic1..topic2 2>actual &&
test_cmp expect actual
'
+test_expect_success 'cannot advance target ... ordering would be ill-defined' '
+ echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect &&
+ test_must_fail git replay --advance=main main topic1 topic2 2>actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'replaying merge commits is not supported yet' '
+ echo "fatal: replaying merge commits is not supported yet!" >expect &&
+ test_must_fail git replay --advance=main main..topic-with-merge 2>actual &&
+ test_cmp expect actual
+'
+
test_expect_success 'using replay to rebase two branches, one on top of other' '
git replay --onto main topic1..topic2 >result &&
test_line_count = 1 result &&
Range-diff against v1:
-: ----------- > 1: 314ba49dd2f replay: remove dead code and rearrange
-: ----------- > 2: 976d336adef replay: find *onto only after testing for ref name
1: d49923de7c5 ! 3: a7a7e1e720e replay: die descriptively when invalid commit-ish
@@ Metadata
Author: Kristoffer Haugsbakk <code@khaugsbakk.name>
## Commit message ##
- replay: die descriptively when invalid commit-ish
+ replay: die descriptively when invalid commit-ish is given
- Giving an invalid commit-ish to `--onto` or `--advance` makes
- git-replay(1) fail with:
+ Giving an invalid commit-ish to `--onto` makes git-replay(1) fail with:
fatal: Replaying down to root commit is not supported yet!
@@ Commit message
$ git replay --onto ^main topic1
fatal: '^main' is not a valid commit-ish
+ Note that the `--advance` case won’t be triggered in practice because
+ of the “argument to --advance must be a reference” check (see the
+ previous test, and commit).
+
† 1: The argument to `--onto` is mandatory and the option parser accepts
both `--onto=<name>` (stuck form) and `--onto name`. The latter
form makes it easy to unintentionally pass something to the option
@@ Commit message
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
+
+ ## Notes (series) ##
+ v2:
+
+ Let’s use a slightly longer subject line in the commit message so that it
+ looks more like a full sentence (no dropped/implied words).[1]
+
+ Also remove the test for `--advance` which is now wrong because of the
+ previous commit/patch. And reword the commit message now that only `--onto`
+ is relevant in practice.
+
+ There was also feedback about *where* to give this error:[2]
+
+ > How many callers use this function? I am wondering if it is better
+ > to give a better message at the caller(s), rather than here, where
+ > we lack context to tell something like "You gave string 'ource' as
+ > the argument to the '--onto' option, but 'ource' does not name any
+ > commit" (in other words, "for what our caller is trying to peel
+ > <name> to a commit").
+
+ But I opted to keep the check here by using the new `mode` parameter to
+ provide the context; it is either `--onto` or `--advance`.
+
+ Also remove the “not supported yet” now that `*onto` cannot be `NULL` at
+ this point. I wasn’t confident enough to pull the trigger on that in the
+ first round. But after Elijah’s comment[3] I feel like I understand the code
+ well enough.
+
+ Also change the test to use printf since it’s only one line. That will be
+ in line with the later commits/patches here.
+
+ 🔗 1: https://lore.kernel.org/git/xmqqecolrip7.fsf@gitster.g/
+ 🔗 2: https://lore.kernel.org/git/xmqqikdxriw3.fsf@gitster.g/
+ 🔗 3: https://lore.kernel.org/git/CABPp-BEcJqjD4ztsZo2FTZgWT5ZOADKYEyiZtda+d0mSd1quPQ@mail.gmail.com/
+
## builtin/replay.c ##
-@@ builtin/replay.c: static struct commit *peel_committish(struct repository *repo, const char *name)
+@@ builtin/replay.c: static const char *short_commit_name(struct repository *repo,
+ DEFAULT_ABBREV);
+ }
+
+-static struct commit *peel_committish(struct repository *repo, const char *name)
++static struct commit *peel_committish(struct repository *repo,
++ const char *name,
++ const char *mode)
+ {
+ struct object *obj;
struct object_id oid;
if (repo_get_oid(repo, name, &oid))
- return NULL;
-+ die(_("'%s' is not a valid commit-ish"), name);
++ die(_("'%s' is not a valid commit-ish for %s"), name, mode);
obj = parse_object(repo, &oid);
return (struct commit *)repo_peel_to_type(repo, name, 0, obj,
OBJ_COMMIT);
+@@ builtin/replay.c: static void populate_for_onto_or_advance_mode(struct repository *repo,
+ die_for_incompatible_opt2(!!onto_name, "--onto",
+ !!*advance_name, "--advance");
+ if (onto_name) {
+- *onto = peel_committish(repo, onto_name);
++ *onto = peel_committish(repo, onto_name, "--onto");
+ if (rinfo.positive_refexprs <
+ strset_get_size(&rinfo.positive_refs))
+ die(_("all positive revisions given must be references"));
+@@ builtin/replay.c: static void populate_for_onto_or_advance_mode(struct repository *repo,
+ } else {
+ die(_("argument to --advance must be a reference"));
+ }
+- *onto = peel_committish(repo, *advance_name);
++ *onto = peel_committish(repo, *advance_name, "--advance");
+ if (rinfo.positive_refexprs > 1)
+ die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
+ }
+@@ builtin/replay.c: int cmd_replay(int argc,
+ onto_name, &advance_name,
+ &onto, &update_refs);
+
+- if (!onto) /* FIXME: Should handle replaying down to root commit */
+- die("Replaying down to root commit is not supported yet!");
+-
+ if (prepare_revision_walk(&revs) < 0) {
+ ret = error(_("error preparing revisions"));
+ goto cleanup;
+@@ builtin/replay.c: int cmd_replay(int argc,
+ khint_t pos;
+ int hr;
+
+- if (!commit->parents)
++ if (!commit->parents) /* FIXME: Should handle replaying down to root commit */
+ die(_("replaying down to root commit is not supported yet!"));
+ if (commit->parents->next)
+ die(_("replaying merge commits is not supported yet!"));
## t/t3650-replay-basics.sh ##
-@@ t/t3650-replay-basics.sh: test_expect_success 'setup bare' '
- git clone --bare . bare
+@@ t/t3650-replay-basics.sh: test_expect_success 'argument to --advance must be a reference' '
+ test_cmp expect actual
'
+test_expect_success '--onto with invalid commit-ish' '
-+ cat >expect <<-EOF &&
-+ fatal: ${SQ}refs/not-valid${SQ} is not a valid commit-ish
-+ EOF
++ printf "fatal: ${SQ}refs/not-valid${SQ} is not " >expect &&
++ printf "a valid commit-ish for --onto\n" >>expect &&
+ test_must_fail git replay --onto=refs/not-valid topic1..topic2 2>actual &&
+ test_cmp expect actual
+'
-+
-+test_expect_success '--advance with invalid commit-ish' '
-+ cat >expect <<-EOF &&
-+ fatal: ${SQ}refs/not-valid${SQ} is not a valid commit-ish
-+ EOF
-+ test_must_fail git replay --advance=refs/not-valid topic1..topic2 2>actual &&
-+ test_cmp expect actual
-+'
+
test_expect_success 'using replay to rebase two branches, one on top of other' '
git replay --onto main topic1..topic2 >result &&
-: ----------- > 4: 9700630d95e replay: die if we cannot parse object
2: 5aa5b96950e ! 5: 5e1b9205df5 t3650: add more regression tests for failure conditions
@@ Commit message
a few more since these are simple to write and remove if they become
obsolete.
+ Helped-by: Phillip Wood <phillip.wood@dunelm.org.uk>
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
+
+ ## Notes (series) ##
+ v2:
+
+ Improve test `option --onto or --advance is mandatory`. Phillip pointed out
+ that using a pipe loses the return value. Instead let’s test the whole
+ output by just appending `git replay -h` to `expect`.
+
+ Also “normalize” to just using echo/printf for the `expect` since these are
+ just oneliner errors.
+
+ Also add two more tests (at the end).
+
## t/t3650-replay-basics.sh ##
-@@ t/t3650-replay-basics.sh: test_expect_success '--advance with invalid commit-ish' '
+@@ t/t3650-replay-basics.sh: test_expect_success 'setup' '
+ test_commit L &&
+ test_commit M &&
+
++ git switch --detach topic4 &&
++ test_commit N &&
++ test_commit O &&
++ git switch -c topic-with-merge topic4 &&
++ test_merge P O --no-ff &&
++ git switch main &&
++
+ git switch -c conflict B &&
+ test_commit C.conflict C.t conflict
+ '
+@@ t/t3650-replay-basics.sh: test_expect_success '--onto with invalid commit-ish' '
test_cmp expect actual
'
+test_expect_success 'option --onto or --advance is mandatory' '
-+ cat >expect <<-\EOF &&
-+ error: option --onto or --advance is mandatory
-+ EOF
-+ # First line is the error; rest is Usage
-+ test_must_fail git replay topic1..topic2 >&1 2>&1 |
-+ head -1 >actual &&
++ echo "error: option --onto or --advance is mandatory" >expect &&
++ test_might_fail git replay -h >>expect &&
++ test_must_fail git replay topic1..topic2 2>actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'no base or negative ref gives no-replaying down to root error' '
-+ cat >expect <<-\EOF &&
-+ fatal: replaying down to root commit is not supported yet!
-+ EOF
++ echo "fatal: replaying down to root commit is not supported yet!" >expect &&
+ test_must_fail git replay --onto=topic1 topic2 2>actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'options --advance and --contained cannot be used together' '
-+ cat >expect <<-EOF &&
-+ fatal: options ${SQ}--advance${SQ} and ${SQ}--contained${SQ} cannot be used together
-+ EOF
++ printf "fatal: options ${SQ}--advance${SQ} " >expect &&
++ printf "and ${SQ}--contained${SQ} cannot be used together\n" >>expect &&
+ test_must_fail git replay --advance=main --contained \
+ topic1..topic2 2>actual &&
+ test_cmp expect actual
+'
++
++test_expect_success 'cannot advance target ... ordering would be ill-defined' '
++ echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect &&
++ test_must_fail git replay --advance=main main topic1 topic2 2>actual &&
++ test_cmp expect actual
++'
++
++test_expect_success 'replaying merge commits is not supported yet' '
++ echo "fatal: replaying merge commits is not supported yet!" >expect &&
++ test_must_fail git replay --advance=main main..topic-with-merge 2>actual &&
++ test_cmp expect actual
++'
+
test_expect_success 'using replay to rebase two branches, one on top of other' '
git replay --onto main topic1..topic2 >result &&
base-commit: 9a2fb147f2c61d0cab52c883e7e26f5b7948e3ed
--
2.52.0.10.g08704017180
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 1/5] replay: remove dead code and rearrange
2025-12-30 15:01 ` [PATCH v2 0/5] " kristofferhaugsbakk
@ 2025-12-30 15:01 ` kristofferhaugsbakk
2025-12-30 22:50 ` Elijah Newren
2025-12-30 15:01 ` [PATCH v2 2/5] replay: find *onto only after testing for ref name kristofferhaugsbakk
` (4 subsequent siblings)
5 siblings, 1 reply; 24+ messages in thread
From: kristofferhaugsbakk @ 2025-12-30 15:01 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, christian.couder, newren, Siddharth Asthana,
Phillip Wood
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
22d99f01 (replay: add --advance or 'cherry-pick' mode, 2023-11-24) both
added `--advance` and made one of `--onto` or `--advance` mandatory.
But `determine_replay_mode` claims that there is a third alternative;
neither of `--onto` or `--advance` were given:
if (onto_name) {
...
} else if (*advance_name) {
...
} else {
...
}
But this is false—the fallthrough else-block is dead code.
Commit 22d99f01 was iterated upon by several people.[1] The initial
author wrote code for a sort of *guess mode*, allowing for shorter
commands when that was possible. But the next person instead made one
of the aforementioned options mandatory. In turn this code was dead on
arrival in git.git.
[1]: https://lore.kernel.org/git/CABPp-BEcJqjD4ztsZo2FTZgWT5ZOADKYEyiZtda+d0mSd1quPQ@mail.gmail.com/
Let’s remove this code. We can also join the if-block with the
condition `!*advance_name` into the `*onto` block since we do not set
`*advance_name` in this function. It only looked like we might set it
since the dead code has this line:
*advance_name = xstrdup_or_null(last_key);
Let’s also rename the function since we do not determine
the replay mode here. We simply populate data structures.
Note that there might be more dead code caused by this *guess mode*.
We only concern ourselves with this function for now.
Helped-by: Elijah Newren <newren@gmail.com>
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
v2: [new]
See the link in the commit message.
Not strictly needed for this series but I think it makes sense to fix it
here.
builtin/replay.c | 70 +++++++++++-------------------------------------
1 file changed, 16 insertions(+), 54 deletions(-)
diff --git a/builtin/replay.c b/builtin/replay.c
index 6172c8aacc9..54849f65c87 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -154,16 +154,16 @@ static void get_ref_information(struct repository *repo,
free(fullname);
}
}
-static void determine_replay_mode(struct repository *repo,
- struct rev_cmdline_info *cmd_info,
- const char *onto_name,
- char **advance_name,
- struct commit **onto,
- struct strset **update_refs)
+static void populate_for_onto_or_advance_mode(struct repository *repo,
+ struct rev_cmdline_info *cmd_info,
+ const char *onto_name,
+ char **advance_name,
+ struct commit **onto,
+ struct strset **update_refs)
{
struct ref_info rinfo;
get_ref_information(repo, cmd_info, &rinfo);
if (!rinfo.positive_refexprs)
@@ -174,69 +174,30 @@ static void determine_replay_mode(struct repository *repo,
if (onto_name) {
*onto = peel_committish(repo, onto_name);
if (rinfo.positive_refexprs <
strset_get_size(&rinfo.positive_refs))
die(_("all positive revisions given must be references"));
- } else if (*advance_name) {
+ *update_refs = xcalloc(1, sizeof(**update_refs));
+ **update_refs = rinfo.positive_refs;
+ memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
+ } else {
struct object_id oid;
char *fullname = NULL;
+ if (!*advance_name)
+ BUG("expected either onto_name or *advance_name in this function");
+
*onto = peel_committish(repo, *advance_name);
if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),
&oid, &fullname, 0) == 1) {
free(*advance_name);
*advance_name = fullname;
} else {
die(_("argument to --advance must be a reference"));
}
if (rinfo.positive_refexprs > 1)
die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
- } else {
- int positive_refs_complete = (
- rinfo.positive_refexprs ==
- strset_get_size(&rinfo.positive_refs));
- int negative_refs_complete = (
- rinfo.negative_refexprs ==
- strset_get_size(&rinfo.negative_refs));
- /*
- * We need either positive_refs_complete or
- * negative_refs_complete, but not both.
- */
- if (rinfo.negative_refexprs > 0 &&
- positive_refs_complete == negative_refs_complete)
- die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
- if (negative_refs_complete) {
- struct hashmap_iter iter;
- struct strmap_entry *entry;
- const char *last_key = NULL;
-
- if (rinfo.negative_refexprs == 0)
- die(_("all positive revisions given must be references"));
- else if (rinfo.negative_refexprs > 1)
- die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
- else if (rinfo.positive_refexprs > 1)
- die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
-
- /* Only one entry, but we have to loop to get it */
- strset_for_each_entry(&rinfo.negative_refs,
- &iter, entry) {
- last_key = entry->key;
- }
-
- free(*advance_name);
- *advance_name = xstrdup_or_null(last_key);
- } else { /* positive_refs_complete */
- if (rinfo.negative_refexprs > 1)
- die(_("cannot implicitly determine correct base for --onto"));
- if (rinfo.negative_refexprs == 1)
- *onto = rinfo.onto;
- }
- }
- if (!*advance_name) {
- *update_refs = xcalloc(1, sizeof(**update_refs));
- **update_refs = rinfo.positive_refs;
- memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
}
strset_clear(&rinfo.negative_refs);
strset_clear(&rinfo.positive_refs);
}
@@ -384,12 +345,13 @@ int cmd_replay(int argc,
"'%s' bit in 'struct rev_info' will be forced"),
"simplify_history");
revs.simplify_history = 0;
}
- determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
- &onto, &update_refs);
+ populate_for_onto_or_advance_mode(repo, &revs.cmdline,
+ onto_name, &advance_name,
+ &onto, &update_refs);
if (!onto) /* FIXME: Should handle replaying down to root commit */
die("Replaying down to root commit is not supported yet!");
if (prepare_revision_walk(&revs) < 0) {
--
2.52.0.10.g08704017180
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 2/5] replay: find *onto only after testing for ref name
2025-12-30 15:01 ` [PATCH v2 0/5] " kristofferhaugsbakk
2025-12-30 15:01 ` [PATCH v2 1/5] replay: remove dead code and rearrange kristofferhaugsbakk
@ 2025-12-30 15:01 ` kristofferhaugsbakk
2025-12-30 22:51 ` Elijah Newren
2025-12-30 15:01 ` [PATCH v2 3/5] replay: die descriptively when invalid commit-ish is given kristofferhaugsbakk
` (3 subsequent siblings)
5 siblings, 1 reply; 24+ messages in thread
From: kristofferhaugsbakk @ 2025-12-30 15:01 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, christian.couder, newren, Siddharth Asthana,
Phillip Wood, Junio C Hamano
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
We are about to make `peel_committish` die when it cannot find
a commit-ish instead of returning `NULL`. But that would make e.g.
`git replay --advance=refs/non-existent` die with a less descriptive
error message; the highest-level error message is that the name does
not exist as a ref, not that we cannot find a commit-ish based on
the name.
Let’s try to find the ref and only after that try to peel to
as a commit-ish.
Also add a regression test to protect this error-order from future
modifications.
Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
v2: [new]
Fallout of v1. Needs to be moved so that the new error message does not
“shadow” this one.
See: https://lore.kernel.org/git/xmqqpl85pb7k.fsf@gitster.g/
builtin/replay.c | 2 +-
t/t3650-replay-basics.sh | 7 +++++++
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/builtin/replay.c b/builtin/replay.c
index 54849f65c87..35813140e99 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -184,18 +184,18 @@ static void populate_for_onto_or_advance_mode(struct repository *repo,
char *fullname = NULL;
if (!*advance_name)
BUG("expected either onto_name or *advance_name in this function");
- *onto = peel_committish(repo, *advance_name);
if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),
&oid, &fullname, 0) == 1) {
free(*advance_name);
*advance_name = fullname;
} else {
die(_("argument to --advance must be a reference"));
}
+ *onto = peel_committish(repo, *advance_name);
if (rinfo.positive_refexprs > 1)
die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
}
strset_clear(&rinfo.negative_refs);
strset_clear(&rinfo.positive_refs);
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 58b37599357..7dea62f064f 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -49,10 +49,17 @@ test_expect_success 'setup' '
test_expect_success 'setup bare' '
git clone --bare . bare
'
+test_expect_success 'argument to --advance must be a reference' '
+ echo "fatal: argument to --advance must be a reference" >expect &&
+ oid=$(git rev-parse main) &&
+ test_must_fail git replay --advance=$oid topic1..topic2 2>actual &&
+ test_cmp expect actual
+'
+
test_expect_success 'using replay to rebase two branches, one on top of other' '
git replay --onto main topic1..topic2 >result &&
test_line_count = 1 result &&
--
2.52.0.10.g08704017180
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 3/5] replay: die descriptively when invalid commit-ish is given
2025-12-30 15:01 ` [PATCH v2 0/5] " kristofferhaugsbakk
2025-12-30 15:01 ` [PATCH v2 1/5] replay: remove dead code and rearrange kristofferhaugsbakk
2025-12-30 15:01 ` [PATCH v2 2/5] replay: find *onto only after testing for ref name kristofferhaugsbakk
@ 2025-12-30 15:01 ` kristofferhaugsbakk
2025-12-30 22:52 ` Elijah Newren
2025-12-30 15:01 ` [PATCH v2 4/5] replay: die if we cannot parse object kristofferhaugsbakk
` (2 subsequent siblings)
5 siblings, 1 reply; 24+ messages in thread
From: kristofferhaugsbakk @ 2025-12-30 15:01 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, christian.couder, newren, Siddharth Asthana,
Phillip Wood
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
Giving an invalid commit-ish to `--onto` makes git-replay(1) fail with:
fatal: Replaying down to root commit is not supported yet!
Going backwards from this point:
1. `onto` is `NULL` from `determine_replay_mode`;
2. that function in turn calls `peel_committish`; and
3. here we return `NULL` if `repo_get_oid` fails.
Let’s die immediately with a descriptive error message instead.
Doing this also provides us with a descriptive error if we “forget” to
provide an argument to `--onto` (but we really do unintentionally):[1]
$ git replay --onto ^main topic1
fatal: '^main' is not a valid commit-ish
Note that the `--advance` case won’t be triggered in practice because
of the “argument to --advance must be a reference” check (see the
previous test, and commit).
† 1: The argument to `--onto` is mandatory and the option parser accepts
both `--onto=<name>` (stuck form) and `--onto name`. The latter
form makes it easy to unintentionally pass something to the option
when you really meant to pass a positional argument.
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
v2:
Let’s use a slightly longer subject line in the commit message so that it
looks more like a full sentence (no dropped/implied words).[1]
Also remove the test for `--advance` which is now wrong because of the
previous commit/patch. And reword the commit message now that only `--onto`
is relevant in practice.
There was also feedback about *where* to give this error:[2]
> How many callers use this function? I am wondering if it is better
> to give a better message at the caller(s), rather than here, where
> we lack context to tell something like "You gave string 'ource' as
> the argument to the '--onto' option, but 'ource' does not name any
> commit" (in other words, "for what our caller is trying to peel
> <name> to a commit").
But I opted to keep the check here by using the new `mode` parameter to
provide the context; it is either `--onto` or `--advance`.
Also remove the “not supported yet” now that `*onto` cannot be `NULL` at
this point. I wasn’t confident enough to pull the trigger on that in the
first round. But after Elijah’s comment[3] I feel like I understand the code
well enough.
Also change the test to use printf since it’s only one line. That will be
in line with the later commits/patches here.
🔗 1: https://lore.kernel.org/git/xmqqecolrip7.fsf@gitster.g/
🔗 2: https://lore.kernel.org/git/xmqqikdxriw3.fsf@gitster.g/
🔗 3: https://lore.kernel.org/git/CABPp-BEcJqjD4ztsZo2FTZgWT5ZOADKYEyiZtda+d0mSd1quPQ@mail.gmail.com/
builtin/replay.c | 15 +++++++--------
t/t3650-replay-basics.sh | 7 +++++++
2 files changed, 14 insertions(+), 8 deletions(-)
diff --git a/builtin/replay.c b/builtin/replay.c
index 35813140e99..07a6767ade1 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -25,17 +25,19 @@ static const char *short_commit_name(struct repository *repo,
{
return repo_find_unique_abbrev(repo, &commit->object.oid,
DEFAULT_ABBREV);
}
-static struct commit *peel_committish(struct repository *repo, const char *name)
+static struct commit *peel_committish(struct repository *repo,
+ const char *name,
+ const char *mode)
{
struct object *obj;
struct object_id oid;
if (repo_get_oid(repo, name, &oid))
- return NULL;
+ die(_("'%s' is not a valid commit-ish for %s"), name, mode);
obj = parse_object(repo, &oid);
return (struct commit *)repo_peel_to_type(repo, name, 0, obj,
OBJ_COMMIT);
}
@@ -170,11 +172,11 @@ static void populate_for_onto_or_advance_mode(struct repository *repo,
die(_("need some commits to replay"));
die_for_incompatible_opt2(!!onto_name, "--onto",
!!*advance_name, "--advance");
if (onto_name) {
- *onto = peel_committish(repo, onto_name);
+ *onto = peel_committish(repo, onto_name, "--onto");
if (rinfo.positive_refexprs <
strset_get_size(&rinfo.positive_refs))
die(_("all positive revisions given must be references"));
*update_refs = xcalloc(1, sizeof(**update_refs));
**update_refs = rinfo.positive_refs;
@@ -191,11 +193,11 @@ static void populate_for_onto_or_advance_mode(struct repository *repo,
free(*advance_name);
*advance_name = fullname;
} else {
die(_("argument to --advance must be a reference"));
}
- *onto = peel_committish(repo, *advance_name);
+ *onto = peel_committish(repo, *advance_name, "--advance");
if (rinfo.positive_refexprs > 1)
die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
}
strset_clear(&rinfo.negative_refs);
strset_clear(&rinfo.positive_refs);
@@ -349,13 +351,10 @@ int cmd_replay(int argc,
populate_for_onto_or_advance_mode(repo, &revs.cmdline,
onto_name, &advance_name,
&onto, &update_refs);
- if (!onto) /* FIXME: Should handle replaying down to root commit */
- die("Replaying down to root commit is not supported yet!");
-
if (prepare_revision_walk(&revs) < 0) {
ret = error(_("error preparing revisions"));
goto cleanup;
}
@@ -367,11 +366,11 @@ int cmd_replay(int argc,
while ((commit = get_revision(&revs))) {
const struct name_decoration *decoration;
khint_t pos;
int hr;
- if (!commit->parents)
+ if (!commit->parents) /* FIXME: Should handle replaying down to root commit */
die(_("replaying down to root commit is not supported yet!"));
if (commit->parents->next)
die(_("replaying merge commits is not supported yet!"));
last_commit = pick_regular_commit(repo, commit, replayed_commits,
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index 7dea62f064f..d4399aa1662 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -56,10 +56,17 @@ test_expect_success 'argument to --advance must be a reference' '
oid=$(git rev-parse main) &&
test_must_fail git replay --advance=$oid topic1..topic2 2>actual &&
test_cmp expect actual
'
+test_expect_success '--onto with invalid commit-ish' '
+ printf "fatal: ${SQ}refs/not-valid${SQ} is not " >expect &&
+ printf "a valid commit-ish for --onto\n" >>expect &&
+ test_must_fail git replay --onto=refs/not-valid topic1..topic2 2>actual &&
+ test_cmp expect actual
+'
+
test_expect_success 'using replay to rebase two branches, one on top of other' '
git replay --onto main topic1..topic2 >result &&
test_line_count = 1 result &&
--
2.52.0.10.g08704017180
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 4/5] replay: die if we cannot parse object
2025-12-30 15:01 ` [PATCH v2 0/5] " kristofferhaugsbakk
` (2 preceding siblings ...)
2025-12-30 15:01 ` [PATCH v2 3/5] replay: die descriptively when invalid commit-ish is given kristofferhaugsbakk
@ 2025-12-30 15:01 ` kristofferhaugsbakk
2025-12-30 15:01 ` [PATCH v2 5/5] t3650: add more regression tests for failure conditions kristofferhaugsbakk
2025-12-30 22:53 ` [PATCH v2 0/5] replay: die descriptively when invalid commit-ish Elijah Newren
5 siblings, 0 replies; 24+ messages in thread
From: kristofferhaugsbakk @ 2025-12-30 15:01 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, christian.couder, newren, Siddharth Asthana,
Phillip Wood, Junio C Hamano
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
`parse_object` can return `NULL`. That will in turn make
`repo_peel_to_type` return the same.
Let’s die fast and descriptively with the `*_or_die` variant.
Suggested-by: Junio C Hamano <gitster@pobox.com>
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
v2: [new]
See: https://lore.kernel.org/git/xmqqikdxriw3.fsf@gitster.g/
With the `*_or_die` function we don’t have to check it at the call site.
builtin/replay.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/builtin/replay.c b/builtin/replay.c
index 07a6767ade1..ca5a14de4c7 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -34,11 +34,11 @@ static struct commit *peel_committish(struct repository *repo,
struct object *obj;
struct object_id oid;
if (repo_get_oid(repo, name, &oid))
die(_("'%s' is not a valid commit-ish for %s"), name, mode);
- obj = parse_object(repo, &oid);
+ obj = parse_object_or_die(repo, &oid, name);
return (struct commit *)repo_peel_to_type(repo, name, 0, obj,
OBJ_COMMIT);
}
static char *get_author(const char *message)
--
2.52.0.10.g08704017180
^ permalink raw reply related [flat|nested] 24+ messages in thread
* [PATCH v2 5/5] t3650: add more regression tests for failure conditions
2025-12-30 15:01 ` [PATCH v2 0/5] " kristofferhaugsbakk
` (3 preceding siblings ...)
2025-12-30 15:01 ` [PATCH v2 4/5] replay: die if we cannot parse object kristofferhaugsbakk
@ 2025-12-30 15:01 ` kristofferhaugsbakk
2025-12-30 22:53 ` [PATCH v2 0/5] replay: die descriptively when invalid commit-ish Elijah Newren
5 siblings, 0 replies; 24+ messages in thread
From: kristofferhaugsbakk @ 2025-12-30 15:01 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, christian.couder, newren, Siddharth Asthana,
Phillip Wood
From: Kristoffer Haugsbakk <code@khaugsbakk.name>
There isn’t much test coverage for basic failure conditions. Let’s add
a few more since these are simple to write and remove if they become
obsolete.
Helped-by: Phillip Wood <phillip.wood@dunelm.org.uk>
Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
---
Notes (series):
v2:
Improve test `option --onto or --advance is mandatory`. Phillip pointed out
that using a pipe loses the return value. Instead let’s test the whole
output by just appending `git replay -h` to `expect`.
Also “normalize” to just using echo/printf for the `expect` since these are
just oneliner errors.
Also add two more tests (at the end).
t/t3650-replay-basics.sh | 40 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 40 insertions(+)
diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
index d4399aa1662..c0c59ae6938 100755
--- a/t/t3650-replay-basics.sh
+++ b/t/t3650-replay-basics.sh
@@ -41,10 +41,17 @@ test_expect_success 'setup' '
git switch main &&
test_commit L &&
test_commit M &&
+ git switch --detach topic4 &&
+ test_commit N &&
+ test_commit O &&
+ git switch -c topic-with-merge topic4 &&
+ test_merge P O --no-ff &&
+ git switch main &&
+
git switch -c conflict B &&
test_commit C.conflict C.t conflict
'
test_expect_success 'setup bare' '
@@ -63,10 +70,43 @@ test_expect_success '--onto with invalid commit-ish' '
printf "a valid commit-ish for --onto\n" >>expect &&
test_must_fail git replay --onto=refs/not-valid topic1..topic2 2>actual &&
test_cmp expect actual
'
+test_expect_success 'option --onto or --advance is mandatory' '
+ echo "error: option --onto or --advance is mandatory" >expect &&
+ test_might_fail git replay -h >>expect &&
+ test_must_fail git replay topic1..topic2 2>actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'no base or negative ref gives no-replaying down to root error' '
+ echo "fatal: replaying down to root commit is not supported yet!" >expect &&
+ test_must_fail git replay --onto=topic1 topic2 2>actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'options --advance and --contained cannot be used together' '
+ printf "fatal: options ${SQ}--advance${SQ} " >expect &&
+ printf "and ${SQ}--contained${SQ} cannot be used together\n" >>expect &&
+ test_must_fail git replay --advance=main --contained \
+ topic1..topic2 2>actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'cannot advance target ... ordering would be ill-defined' '
+ echo "fatal: cannot advance target with multiple sources because ordering would be ill-defined" >expect &&
+ test_must_fail git replay --advance=main main topic1 topic2 2>actual &&
+ test_cmp expect actual
+'
+
+test_expect_success 'replaying merge commits is not supported yet' '
+ echo "fatal: replaying merge commits is not supported yet!" >expect &&
+ test_must_fail git replay --advance=main main..topic-with-merge 2>actual &&
+ test_cmp expect actual
+'
+
test_expect_success 'using replay to rebase two branches, one on top of other' '
git replay --onto main topic1..topic2 >result &&
test_line_count = 1 result &&
--
2.52.0.10.g08704017180
^ permalink raw reply related [flat|nested] 24+ messages in thread
* Re: [PATCH v2 1/5] replay: remove dead code and rearrange
2025-12-30 15:01 ` [PATCH v2 1/5] replay: remove dead code and rearrange kristofferhaugsbakk
@ 2025-12-30 22:50 ` Elijah Newren
2025-12-30 23:37 ` Junio C Hamano
0 siblings, 1 reply; 24+ messages in thread
From: Elijah Newren @ 2025-12-30 22:50 UTC (permalink / raw)
To: kristofferhaugsbakk
Cc: git, Kristoffer Haugsbakk, christian.couder, Siddharth Asthana,
Phillip Wood
On Tue, Dec 30, 2025 at 7:03 AM <kristofferhaugsbakk@fastmail.com> wrote:
>
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>
> 22d99f01 (replay: add --advance or 'cherry-pick' mode, 2023-11-24) both
> added `--advance` and made one of `--onto` or `--advance` mandatory.
> But `determine_replay_mode` claims that there is a third alternative;
> neither of `--onto` or `--advance` were given:
>
> if (onto_name) {
> ...
> } else if (*advance_name) {
> ...
> } else {
> ...
> }
>
> But this is false—the fallthrough else-block is dead code.
>
> Commit 22d99f01 was iterated upon by several people.[1] The initial
> author wrote code for a sort of *guess mode*, allowing for shorter
> commands when that was possible. But the next person instead made one
> of the aforementioned options mandatory. In turn this code was dead on
> arrival in git.git.
>
> [1]: https://lore.kernel.org/git/CABPp-BEcJqjD4ztsZo2FTZgWT5ZOADKYEyiZtda+d0mSd1quPQ@mail.gmail.com/
>
> Let’s remove this code. We can also join the if-block with the
> condition `!*advance_name` into the `*onto` block since we do not set
> `*advance_name` in this function. It only looked like we might set it
> since the dead code has this line:
>
> *advance_name = xstrdup_or_null(last_key);
>
> Let’s also rename the function since we do not determine
> the replay mode here. We simply populate data structures.
>
> Note that there might be more dead code caused by this *guess mode*.
> We only concern ourselves with this function for now.
>
> Helped-by: Elijah Newren <newren@gmail.com>
> Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
> ---
>
> Notes (series):
> v2: [new]
>
> See the link in the commit message.
>
> Not strictly needed for this series but I think it makes sense to fix it
> here.
>
> builtin/replay.c | 70 +++++++++++-------------------------------------
> 1 file changed, 16 insertions(+), 54 deletions(-)
>
> diff --git a/builtin/replay.c b/builtin/replay.c
> index 6172c8aacc9..54849f65c87 100644
> --- a/builtin/replay.c
> +++ b/builtin/replay.c
> @@ -154,16 +154,16 @@ static void get_ref_information(struct repository *repo,
>
> free(fullname);
> }
> }
>
> -static void determine_replay_mode(struct repository *repo,
> - struct rev_cmdline_info *cmd_info,
> - const char *onto_name,
> - char **advance_name,
> - struct commit **onto,
> - struct strset **update_refs)
> +static void populate_for_onto_or_advance_mode(struct repository *repo,
> + struct rev_cmdline_info *cmd_info,
> + const char *onto_name,
> + char **advance_name,
> + struct commit **onto,
> + struct strset **update_refs)
Renaming makes sense, but the new name is quite the mouthful, and it
feels slightly odd because "onto" is both a command line flag and a
variable -- and the variable value is used regardless of which command
line flag is used. Since the variable is used either way, there's a
risk someone might be confused by this function name. Maybe just
setup_replay_mode() ? Or maybe others have other suggestions?
> {
> struct ref_info rinfo;
>
> get_ref_information(repo, cmd_info, &rinfo);
> if (!rinfo.positive_refexprs)
> @@ -174,69 +174,30 @@ static void determine_replay_mode(struct repository *repo,
> if (onto_name) {
> *onto = peel_committish(repo, onto_name);
> if (rinfo.positive_refexprs <
> strset_get_size(&rinfo.positive_refs))
> die(_("all positive revisions given must be references"));
> - } else if (*advance_name) {
> + *update_refs = xcalloc(1, sizeof(**update_refs));
> + **update_refs = rinfo.positive_refs;
> + memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
> + } else {
> struct object_id oid;
> char *fullname = NULL;
>
> + if (!*advance_name)
> + BUG("expected either onto_name or *advance_name in this function");
> +
> *onto = peel_committish(repo, *advance_name);
> if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),
> &oid, &fullname, 0) == 1) {
> free(*advance_name);
> *advance_name = fullname;
> } else {
> die(_("argument to --advance must be a reference"));
> }
> if (rinfo.positive_refexprs > 1)
> die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
> - } else {
> - int positive_refs_complete = (
> - rinfo.positive_refexprs ==
> - strset_get_size(&rinfo.positive_refs));
> - int negative_refs_complete = (
> - rinfo.negative_refexprs ==
> - strset_get_size(&rinfo.negative_refs));
> - /*
> - * We need either positive_refs_complete or
> - * negative_refs_complete, but not both.
> - */
> - if (rinfo.negative_refexprs > 0 &&
> - positive_refs_complete == negative_refs_complete)
> - die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
> - if (negative_refs_complete) {
> - struct hashmap_iter iter;
> - struct strmap_entry *entry;
> - const char *last_key = NULL;
> -
> - if (rinfo.negative_refexprs == 0)
> - die(_("all positive revisions given must be references"));
> - else if (rinfo.negative_refexprs > 1)
> - die(_("cannot implicitly determine whether this is an --advance or --onto operation"));
> - else if (rinfo.positive_refexprs > 1)
> - die(_("cannot advance target with multiple source branches because ordering would be ill-defined"));
> -
> - /* Only one entry, but we have to loop to get it */
> - strset_for_each_entry(&rinfo.negative_refs,
> - &iter, entry) {
> - last_key = entry->key;
> - }
> -
> - free(*advance_name);
> - *advance_name = xstrdup_or_null(last_key);
> - } else { /* positive_refs_complete */
> - if (rinfo.negative_refexprs > 1)
> - die(_("cannot implicitly determine correct base for --onto"));
> - if (rinfo.negative_refexprs == 1)
> - *onto = rinfo.onto;
> - }
> - }
> - if (!*advance_name) {
> - *update_refs = xcalloc(1, sizeof(**update_refs));
> - **update_refs = rinfo.positive_refs;
> - memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
> }
> strset_clear(&rinfo.negative_refs);
> strset_clear(&rinfo.positive_refs);
> }
>
> @@ -384,12 +345,13 @@ int cmd_replay(int argc,
> "'%s' bit in 'struct rev_info' will be forced"),
> "simplify_history");
> revs.simplify_history = 0;
> }
>
> - determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name,
> - &onto, &update_refs);
> + populate_for_onto_or_advance_mode(repo, &revs.cmdline,
> + onto_name, &advance_name,
> + &onto, &update_refs);
>
> if (!onto) /* FIXME: Should handle replaying down to root commit */
> die("Replaying down to root commit is not supported yet!");
>
> if (prepare_revision_walk(&revs) < 0) {
> --
> 2.52.0.10.g08704017180
Looks fine otherwise.
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH v2 2/5] replay: find *onto only after testing for ref name
2025-12-30 15:01 ` [PATCH v2 2/5] replay: find *onto only after testing for ref name kristofferhaugsbakk
@ 2025-12-30 22:51 ` Elijah Newren
0 siblings, 0 replies; 24+ messages in thread
From: Elijah Newren @ 2025-12-30 22:51 UTC (permalink / raw)
To: kristofferhaugsbakk
Cc: git, Kristoffer Haugsbakk, christian.couder, Siddharth Asthana,
Phillip Wood, Junio C Hamano
On Tue, Dec 30, 2025 at 7:03 AM <kristofferhaugsbakk@fastmail.com> wrote:
>
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>
> We are about to make `peel_committish` die when it cannot find
> a commit-ish instead of returning `NULL`. But that would make e.g.
> `git replay --advance=refs/non-existent` die with a less descriptive
> error message; the highest-level error message is that the name does
> not exist as a ref, not that we cannot find a commit-ish based on
> the name.
>
> Let’s try to find the ref and only after that try to peel to
> as a commit-ish.
>
> Also add a regression test to protect this error-order from future
> modifications.
"error-order" looked like a typo and took a while for me to parse.
Maybe drop the hyphen or replace with "order of errors"?
>
> Suggested-by: Junio C Hamano <gitster@pobox.com>
> Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
> ---
>
> Notes (series):
> v2: [new]
>
> Fallout of v1. Needs to be moved so that the new error message does not
> “shadow” this one.
>
> See: https://lore.kernel.org/git/xmqqpl85pb7k.fsf@gitster.g/
>
> builtin/replay.c | 2 +-
> t/t3650-replay-basics.sh | 7 +++++++
> 2 files changed, 8 insertions(+), 1 deletion(-)
>
> diff --git a/builtin/replay.c b/builtin/replay.c
> index 54849f65c87..35813140e99 100644
> --- a/builtin/replay.c
> +++ b/builtin/replay.c
> @@ -184,18 +184,18 @@ static void populate_for_onto_or_advance_mode(struct repository *repo,
> char *fullname = NULL;
>
> if (!*advance_name)
> BUG("expected either onto_name or *advance_name in this function");
>
> - *onto = peel_committish(repo, *advance_name);
> if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),
> &oid, &fullname, 0) == 1) {
> free(*advance_name);
> *advance_name = fullname;
> } else {
> die(_("argument to --advance must be a reference"));
> }
> + *onto = peel_committish(repo, *advance_name);
> if (rinfo.positive_refexprs > 1)
> die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
> }
> strset_clear(&rinfo.negative_refs);
> strset_clear(&rinfo.positive_refs);
> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
> index 58b37599357..7dea62f064f 100755
> --- a/t/t3650-replay-basics.sh
> +++ b/t/t3650-replay-basics.sh
> @@ -49,10 +49,17 @@ test_expect_success 'setup' '
>
> test_expect_success 'setup bare' '
> git clone --bare . bare
> '
>
> +test_expect_success 'argument to --advance must be a reference' '
> + echo "fatal: argument to --advance must be a reference" >expect &&
> + oid=$(git rev-parse main) &&
> + test_must_fail git replay --advance=$oid topic1..topic2 2>actual &&
> + test_cmp expect actual
> +'
> +
> test_expect_success 'using replay to rebase two branches, one on top of other' '
> git replay --onto main topic1..topic2 >result &&
>
> test_line_count = 1 result &&
>
> --
> 2.52.0.10.g08704017180
Looks good otherwise.
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH v2 3/5] replay: die descriptively when invalid commit-ish is given
2025-12-30 15:01 ` [PATCH v2 3/5] replay: die descriptively when invalid commit-ish is given kristofferhaugsbakk
@ 2025-12-30 22:52 ` Elijah Newren
0 siblings, 0 replies; 24+ messages in thread
From: Elijah Newren @ 2025-12-30 22:52 UTC (permalink / raw)
To: kristofferhaugsbakk
Cc: git, Kristoffer Haugsbakk, christian.couder, Siddharth Asthana,
Phillip Wood
On Tue, Dec 30, 2025 at 7:03 AM <kristofferhaugsbakk@fastmail.com> wrote:
>
> From: Kristoffer Haugsbakk <code@khaugsbakk.name>
>
> Giving an invalid commit-ish to `--onto` makes git-replay(1) fail with:
>
> fatal: Replaying down to root commit is not supported yet!
>
> Going backwards from this point:
>
> 1. `onto` is `NULL` from `determine_replay_mode`;
`determine_replay_mode` no longer exists due to your new patch 1.
> 2. that function in turn calls `peel_committish`; and
> 3. here we return `NULL` if `repo_get_oid` fails.
>
> Let’s die immediately with a descriptive error message instead.
>
> Doing this also provides us with a descriptive error if we “forget” to
> provide an argument to `--onto` (but we really do unintentionally):[1]
>
> $ git replay --onto ^main topic1
> fatal: '^main' is not a valid commit-ish
>
> Note that the `--advance` case won’t be triggered in practice because
> of the “argument to --advance must be a reference” check (see the
> previous test, and commit).
>
> † 1: The argument to `--onto` is mandatory and the option parser accepts
> both `--onto=<name>` (stuck form) and `--onto name`. The latter
> form makes it easy to unintentionally pass something to the option
> when you really meant to pass a positional argument.
>
> Signed-off-by: Kristoffer Haugsbakk <code@khaugsbakk.name>
> ---
>
> Notes (series):
> v2:
>
> Let’s use a slightly longer subject line in the commit message so that it
> looks more like a full sentence (no dropped/implied words).[1]
>
> Also remove the test for `--advance` which is now wrong because of the
> previous commit/patch. And reword the commit message now that only `--onto`
> is relevant in practice.
>
> There was also feedback about *where* to give this error:[2]
>
> > How many callers use this function? I am wondering if it is better
> > to give a better message at the caller(s), rather than here, where
> > we lack context to tell something like "You gave string 'ource' as
> > the argument to the '--onto' option, but 'ource' does not name any
> > commit" (in other words, "for what our caller is trying to peel
> > <name> to a commit").
>
> But I opted to keep the check here by using the new `mode` parameter to
> provide the context; it is either `--onto` or `--advance`.
>
> Also remove the “not supported yet” now that `*onto` cannot be `NULL` at
> this point. I wasn’t confident enough to pull the trigger on that in the
> first round. But after Elijah’s comment[3] I feel like I understand the code
> well enough.
>
> Also change the test to use printf since it’s only one line. That will be
> in line with the later commits/patches here.
>
> 🔗 1: https://lore.kernel.org/git/xmqqecolrip7.fsf@gitster.g/
> 🔗 2: https://lore.kernel.org/git/xmqqikdxriw3.fsf@gitster.g/
> 🔗 3: https://lore.kernel.org/git/CABPp-BEcJqjD4ztsZo2FTZgWT5ZOADKYEyiZtda+d0mSd1quPQ@mail.gmail.com/
>
> builtin/replay.c | 15 +++++++--------
> t/t3650-replay-basics.sh | 7 +++++++
> 2 files changed, 14 insertions(+), 8 deletions(-)
>
> diff --git a/builtin/replay.c b/builtin/replay.c
> index 35813140e99..07a6767ade1 100644
> --- a/builtin/replay.c
> +++ b/builtin/replay.c
> @@ -25,17 +25,19 @@ static const char *short_commit_name(struct repository *repo,
> {
> return repo_find_unique_abbrev(repo, &commit->object.oid,
> DEFAULT_ABBREV);
> }
>
> -static struct commit *peel_committish(struct repository *repo, const char *name)
> +static struct commit *peel_committish(struct repository *repo,
> + const char *name,
> + const char *mode)
> {
> struct object *obj;
> struct object_id oid;
>
> if (repo_get_oid(repo, name, &oid))
> - return NULL;
> + die(_("'%s' is not a valid commit-ish for %s"), name, mode);
> obj = parse_object(repo, &oid);
> return (struct commit *)repo_peel_to_type(repo, name, 0, obj,
> OBJ_COMMIT);
> }
>
> @@ -170,11 +172,11 @@ static void populate_for_onto_or_advance_mode(struct repository *repo,
> die(_("need some commits to replay"));
>
> die_for_incompatible_opt2(!!onto_name, "--onto",
> !!*advance_name, "--advance");
> if (onto_name) {
> - *onto = peel_committish(repo, onto_name);
> + *onto = peel_committish(repo, onto_name, "--onto");
> if (rinfo.positive_refexprs <
> strset_get_size(&rinfo.positive_refs))
> die(_("all positive revisions given must be references"));
> *update_refs = xcalloc(1, sizeof(**update_refs));
> **update_refs = rinfo.positive_refs;
> @@ -191,11 +193,11 @@ static void populate_for_onto_or_advance_mode(struct repository *repo,
> free(*advance_name);
> *advance_name = fullname;
> } else {
> die(_("argument to --advance must be a reference"));
> }
> - *onto = peel_committish(repo, *advance_name);
> + *onto = peel_committish(repo, *advance_name, "--advance");
> if (rinfo.positive_refexprs > 1)
> die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
> }
> strset_clear(&rinfo.negative_refs);
> strset_clear(&rinfo.positive_refs);
> @@ -349,13 +351,10 @@ int cmd_replay(int argc,
>
> populate_for_onto_or_advance_mode(repo, &revs.cmdline,
> onto_name, &advance_name,
> &onto, &update_refs);
>
> - if (!onto) /* FIXME: Should handle replaying down to root commit */
> - die("Replaying down to root commit is not supported yet!");
> -
Removing the `if` makes sense given the current code, but I wonder if
we should keep a corrected FIXME here:
/* FIXME: Should allow replaying commits with the first as a root commit */
This is out-of-scope for this series, but behind that FIXME...
I'm guessing the user would specify to cherry-pick onto NULL via something like
git replay --root A..B
which would translate into making `onto` be NULL, and mean that the
first commit after A would be a root commit.
Similarly the user could be allowed to do something like
git replay --advance new-empty-branch A..B
where new-empty-branch doesn't yet point to a commit, this would also
result in `onto` being NULL, and start new-empty-branch by
cherry-picking some commits into it.
> if (prepare_revision_walk(&revs) < 0) {
> ret = error(_("error preparing revisions"));
> goto cleanup;
> }
>
> @@ -367,11 +366,11 @@ int cmd_replay(int argc,
> while ((commit = get_revision(&revs))) {
> const struct name_decoration *decoration;
> khint_t pos;
> int hr;
>
> - if (!commit->parents)
> + if (!commit->parents) /* FIXME: Should handle replaying down to root commit */
> die(_("replaying down to root commit is not supported yet!"));
I wonder if I should have written s/to/from/ here ?
> if (commit->parents->next)
> die(_("replaying merge commits is not supported yet!"));
>
> last_commit = pick_regular_commit(repo, commit, replayed_commits,
> diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh
> index 7dea62f064f..d4399aa1662 100755
> --- a/t/t3650-replay-basics.sh
> +++ b/t/t3650-replay-basics.sh
> @@ -56,10 +56,17 @@ test_expect_success 'argument to --advance must be a reference' '
> oid=$(git rev-parse main) &&
> test_must_fail git replay --advance=$oid topic1..topic2 2>actual &&
> test_cmp expect actual
> '
>
> +test_expect_success '--onto with invalid commit-ish' '
> + printf "fatal: ${SQ}refs/not-valid${SQ} is not " >expect &&
> + printf "a valid commit-ish for --onto\n" >>expect &&
> + test_must_fail git replay --onto=refs/not-valid topic1..topic2 2>actual &&
> + test_cmp expect actual
> +'
> +
> test_expect_success 'using replay to rebase two branches, one on top of other' '
> git replay --onto main topic1..topic2 >result &&
>
> test_line_count = 1 result &&
>
> --
> 2.52.0.10.g08704017180
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH v2 0/5] replay: die descriptively when invalid commit-ish
2025-12-30 15:01 ` [PATCH v2 0/5] " kristofferhaugsbakk
` (4 preceding siblings ...)
2025-12-30 15:01 ` [PATCH v2 5/5] t3650: add more regression tests for failure conditions kristofferhaugsbakk
@ 2025-12-30 22:53 ` Elijah Newren
5 siblings, 0 replies; 24+ messages in thread
From: Elijah Newren @ 2025-12-30 22:53 UTC (permalink / raw)
To: kristofferhaugsbakk
Cc: git, Kristoffer Haugsbakk, christian.couder, Siddharth Asthana,
Phillip Wood
On Tue, Dec 30, 2025 at 7:02 AM <kristofferhaugsbakk@fastmail.com> wrote:
>
[...]
> § Changes in v2
>
> In this version we remove some dead code (see previous section), improve
> tests and add a couple more, and improve the main “die descriptively”
> part, with two additional patches/commits to that end:
>
> • replay: find *onto only after testing for ref name
> • replay: die if we cannot parse object
>
> See the notes on each patch/commit for change details.
I had a few minor comments on a few of the patches, but otherwise it
looked good to me.
^ permalink raw reply [flat|nested] 24+ messages in thread
* Re: [PATCH v2 1/5] replay: remove dead code and rearrange
2025-12-30 22:50 ` Elijah Newren
@ 2025-12-30 23:37 ` Junio C Hamano
0 siblings, 0 replies; 24+ messages in thread
From: Junio C Hamano @ 2025-12-30 23:37 UTC (permalink / raw)
To: Elijah Newren
Cc: kristofferhaugsbakk, git, Kristoffer Haugsbakk, christian.couder,
Siddharth Asthana, Phillip Wood
> > -static void determine_replay_mode(struct repository *repo,
> > ...
> > +static void populate_for_onto_or_advance_mode(struct repository *repo,
> > ...
> Renaming makes sense, but the new name is quite the mouthful, and it
> feels slightly odd because "onto" is both a command line flag and a
> variable -- and the variable value is used regardless of which command
> line flag is used. Since the variable is used either way, there's a
> risk someone might be confused by this function name. Maybe just
> setup_replay_mode() ? Or maybe others have other suggestions?
After reading the above, the name that came to my mind is (curiously)
determine_replay_mode() ;-).
> Looks fine otherwise.
Thanks.
^ permalink raw reply [flat|nested] 24+ messages in thread
end of thread, other threads:[~2025-12-30 23:37 UTC | newest]
Thread overview: 24+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-12-22 22:04 [PATCH 0/2] replay: die descriptively when invalid commit-ish kristofferhaugsbakk
2025-12-22 22:04 ` [PATCH 1/2] " kristofferhaugsbakk
2025-12-23 3:12 ` Junio C Hamano
2025-12-23 10:52 ` Phillip Wood
2025-12-23 13:41 ` Junio C Hamano
2025-12-30 14:30 ` Kristoffer Haugsbakk
2025-12-22 22:04 ` [PATCH 2/2] t3650: add more regression tests for failure conditions kristofferhaugsbakk
2025-12-23 10:58 ` Phillip Wood
2025-12-30 14:33 ` Kristoffer Haugsbakk
2025-12-23 3:16 ` [PATCH 0/2] replay: die descriptively when invalid commit-ish Junio C Hamano
2025-12-30 14:33 ` Kristoffer Haugsbakk
2025-12-24 3:03 ` Elijah Newren
2025-12-30 14:31 ` Kristoffer Haugsbakk
2025-12-30 15:01 ` [PATCH v2 0/5] " kristofferhaugsbakk
2025-12-30 15:01 ` [PATCH v2 1/5] replay: remove dead code and rearrange kristofferhaugsbakk
2025-12-30 22:50 ` Elijah Newren
2025-12-30 23:37 ` Junio C Hamano
2025-12-30 15:01 ` [PATCH v2 2/5] replay: find *onto only after testing for ref name kristofferhaugsbakk
2025-12-30 22:51 ` Elijah Newren
2025-12-30 15:01 ` [PATCH v2 3/5] replay: die descriptively when invalid commit-ish is given kristofferhaugsbakk
2025-12-30 22:52 ` Elijah Newren
2025-12-30 15:01 ` [PATCH v2 4/5] replay: die if we cannot parse object kristofferhaugsbakk
2025-12-30 15:01 ` [PATCH v2 5/5] t3650: add more regression tests for failure conditions kristofferhaugsbakk
2025-12-30 22:53 ` [PATCH v2 0/5] replay: die descriptively when invalid commit-ish Elijah Newren
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 NNTP newsgroup(s).