* [PATCH 0/4] cat-file: do not die on reflog entries out of range
2026-02-12 4:07 ` Jeff King
@ 2026-11-13 14:28 ` Caspar Duregger
2026-11-13 14:28 ` [PATCH 1/4] object-name: do not read a peel suffix as a reflog selector Caspar Duregger
` (3 subsequent siblings)
4 siblings, 0 replies; 8+ messages in thread
From: Caspar Duregger @ 2026-11-13 14:28 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, Junio C Hamano, Jeff King,
brian m . carlson, Caspar Duregger
Hi,
I necropost with a patch as I stumbled on this the other day.
I have one "git cat-file --batch-check" process per worktree, but it
dies on "ref@{N}" past the end of the reflog. I work around by using
the old "git rev-parse" for these but it's sad.
Passing GET_OID_GENTLY from cat-file is the obvious fix, but on its
own it would change answers, not only failures:
- "ref@{N}" for a reflog with N entries and "ref@{<old date>}"
resolve today, but fail with GET_OID_GENTLY (patch 2, which picks
up a review question from the series that added the flag [1]);
- "ref@{999999}^{tree}" would resolve to the current value of the
ref instead of dying, because after the peel fails get_oid_basic()
reads "999999}^{tree}" as an approxidate. The same bug already
makes "git rev-parse --verify 'HEAD@{1}^{blob}'" print a commit
(patch 1);
- an emptied reflog still dies inside read_ref_at() (patch 3).
Patch 4 then flips the flag in cat-file. As Peff noted, this does not
make batch lookups die-free: "HEAD@{999999}~1" and "@{upstream}" on a
detached HEAD still die. The former needs get_parent() and
get_nth_ancestor() to pass flags down, which I left for a separate
series.
[1] https://lore.kernel.org/git/xmqqa5745ttw.fsf@gitster.g/
Caspar Duregger (4):
object-name: do not read a peel suffix as a reflog selector
object-name: let GET_OID_GENTLY affect only failing lookups
refs: let read_ref_at() fail gently on an empty reflog
cat-file: report out-of-range reflog entries as missing
builtin/cat-file.c | 1 +
object-name.c | 28 +++++++++++++++++++---------
refs.c | 2 ++
refs.h | 4 ++++
t/t1006-cat-file.sh | 35 +++++++++++++++++++++++++++++++++++
t/t1508-at-combinations.sh | 4 ++++
t/t3903-stash.sh | 17 +++++++++++++++++
7 files changed, 82 insertions(+), 9 deletions(-)
--
2.56.1.windows.1
^ permalink raw reply [flat|nested] 8+ messages in thread* [PATCH 1/4] object-name: do not read a peel suffix as a reflog selector
2026-02-12 4:07 ` Jeff King
2026-11-13 14:28 ` [PATCH 0/4] cat-file: do not die on reflog entries out of range Caspar Duregger
@ 2026-11-13 14:28 ` Caspar Duregger
2026-11-13 14:28 ` [PATCH 2/4] object-name: let GET_OID_GENTLY affect only failing lookups Caspar Duregger
` (2 subsequent siblings)
4 siblings, 0 replies; 8+ messages in thread
From: Caspar Duregger @ 2026-11-13 14:28 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, Junio C Hamano, Jeff King,
brian m . carlson, Caspar Duregger
To find a "@{...}" reflog selector, get_oid_basic() checks that the
name ends in '}' and then takes the last "@{" in it.
For "HEAD@{1}^{blob}" the final '}' closes "^{blob}", nevertheless we
take "1}^{blob" as the selector. That is not a number, so it goes to
approxidate_careful(), which accepts it without complaint as "now".
The name then resolves to the current value of HEAD.
We only parse the whole string like this after peel_onion() has
failed, so a peel that should fail succeeds with the wrong object:
$ git rev-parse --verify 'HEAD@{1}^{/no-such-subject}'
<object name of HEAD>
Only accept a selector whose "@{" is closed by the final '}'. Reflog
selectors never contain a '}', so no valid selector is rejected.
Signed-off-by: Caspar Duregger <caspar.duregger@gmail.com>
---
object-name.c | 6 ++++++
t/t1508-at-combinations.sh | 4 ++++
2 files changed, 10 insertions(+)
diff --git a/object-name.c b/object-name.c
index 6546685760..13e4dc0f21 100644
--- a/object-name.c
+++ b/object-name.c
@@ -712,6 +712,12 @@ static int get_oid_basic(struct repository *r, const char *str, int len,
nth_prior = 1;
continue;
}
+ /*
+ * The final '}' must close this "@{", not a
+ * later suffix as in "ref@{1}^{tree}".
+ */
+ if (memchr(str + at + 2, '}', len - at - 3))
+ break;
if (!upstream_mark(str + at, len - at) &&
!push_mark(str + at, len - at)) {
reflog_len = (len-1) - (at+2);
diff --git a/t/t1508-at-combinations.sh b/t/t1508-at-combinations.sh
index 87a4286414..10a8c3ad9d 100755
--- a/t/t1508-at-combinations.sh
+++ b/t/t1508-at-combinations.sh
@@ -84,6 +84,10 @@ nonsense "@{-1}@{-1}"
check "HEAD@{3}" commit old-two
nonsense "@{3}"
+# a failed peel must not be retried as a reflog date
+nonsense "HEAD@{1}^{blob}"
+nonsense "@{1}^{/no-such-subject}"
+
test_expect_success 'switch to old-branch' '
git checkout old-branch
'
--
2.56.1.windows.1
^ permalink raw reply related [flat|nested] 8+ messages in thread* [PATCH 2/4] object-name: let GET_OID_GENTLY affect only failing lookups
2026-02-12 4:07 ` Jeff King
2026-11-13 14:28 ` [PATCH 0/4] cat-file: do not die on reflog entries out of range Caspar Duregger
2026-11-13 14:28 ` [PATCH 1/4] object-name: do not read a peel suffix as a reflog selector Caspar Duregger
@ 2026-11-13 14:28 ` Caspar Duregger
2026-11-13 14:28 ` [PATCH 3/4] refs: let read_ref_at() fail gently on an empty reflog Caspar Duregger
2026-11-13 14:28 ` [PATCH 4/4] cat-file: report out-of-range reflog entries as missing Caspar Duregger
4 siblings, 0 replies; 8+ messages in thread
From: Caspar Duregger @ 2026-11-13 14:28 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, Junio C Hamano, Jeff King,
brian m . carlson, Caspar Duregger
393bbb21c9 (object-name: make get_oid quietly return an error,
2025-06-12) introduced GET_OID_GENTLY so that a caller can turn the
die() on an exhausted reflog into an error return. The check was
placed after the if/else cascade that decides whether the lookup
succeeded, though, so passing the flag also turns two successful
lookups into errors:
- "ref@{<date>}" older than the reflog;
- "ref@{N}" for a reflog with N entries.
This was questioned in review [1], but the code was merged as-is.
Make the check a branch of the cascade, just before the die(), so
that the flag changes only how a failing lookup fails. With the flag,
"ref@{<date>}" then resolves to the oldest known value and "ref@{N}"
to the "old" side of the oldest entry, as they do without it.
The only gentle caller so far is "git stash export", which can now
export "stash@{<date>}" like "git stash show" can show it. The oldest
entry of a stash reflog normally has a null "old" side, so
"stash@{N}" past the end keeps failing.
[1] https://lore.kernel.org/git/xmqqa5745ttw.fsf@gitster.g/
Signed-off-by: Caspar Duregger <caspar.duregger@gmail.com>
---
object-name.c | 9 ++++-----
t/t3903-stash.sh | 9 +++++++++
2 files changed, 13 insertions(+), 5 deletions(-)
diff --git a/object-name.c b/object-name.c
index 13e4dc0f21..c066a7d6a2 100644
--- a/object-name.c
+++ b/object-name.c
@@ -812,17 +812,16 @@ static int get_oid_basic(struct repository *r, const char *str, int len,
* still fill in the oid with the "old" value,
* which we can use.
*/
- } else if (!(flags & GET_OID_GENTLY)) {
+ } else if (flags & GET_OID_GENTLY) {
+ free(real_ref);
+ return -1;
+ } else {
if (flags & GET_OID_QUIETLY) {
exit(128);
}
die(_("log for '%.*s' only has %d entries"),
len, str, co_cnt);
}
- if (flags & GET_OID_GENTLY) {
- free(real_ref);
- return -1;
- }
}
}
diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
index 70c6031958..74ac66f22e 100755
--- a/t/t3903-stash.sh
+++ b/t/t3903-stash.sh
@@ -1557,6 +1557,15 @@ test_expect_success 'stash export can accept specified stashes' '
test_line_count = 2 actual
'
+test_expect_success 'stash export resolves stash reflog selectors like rev-parse' '
+ git stash clear &&
+ git stash import refs/heads/bar &&
+ git stash export --to-ref refs/heads/oldest "stash@{1970-01-02}" &&
+ test_cmp_rev t-stash0 oldest^2 &&
+ test_must_fail git stash export --print stash@{2} 2>err &&
+ test_grep "unable to find stash entry stash@{2}" err
+'
+
test_expect_success 'stash export rejects invalid arguments' '
test_must_fail git stash export --print --to-ref refs/heads/invalid 2>err &&
test_grep "exactly one of --print and --to-ref is required" err &&
--
2.56.1.windows.1
^ permalink raw reply related [flat|nested] 8+ messages in thread* [PATCH 3/4] refs: let read_ref_at() fail gently on an empty reflog
2026-02-12 4:07 ` Jeff King
` (2 preceding siblings ...)
2026-11-13 14:28 ` [PATCH 2/4] object-name: let GET_OID_GENTLY affect only failing lookups Caspar Duregger
@ 2026-11-13 14:28 ` Caspar Duregger
2026-11-13 14:28 ` [PATCH 4/4] cat-file: report out-of-range reflog entries as missing Caspar Duregger
4 siblings, 0 replies; 8+ messages in thread
From: Caspar Duregger @ 2026-11-13 14:28 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, Junio C Hamano, Jeff King,
brian m . carlson, Caspar Duregger
When a reflog exists but has no entries, read_ref_at() can answer
"ref@{0}" with the current value of the ref. For any other request it
dies with "log for <ref> is empty", or exits silently with
GET_OID_QUIETLY, even when the caller asked for GET_OID_GENTLY. Such
reflogs are left behind by "git reflog expire --expire=all".
Return -1 in this case when GET_OID_GENTLY is given, and make
get_oid_basic() treat it as a failed lookup. "git stash export" on an
emptied stash reflog now reports "unable to find stash entry" instead
of exiting without a word.
Signed-off-by: Caspar Duregger <caspar.duregger@gmail.com>
---
object-name.c | 13 +++++++++----
refs.c | 2 ++
refs.h | 4 ++++
t/t3903-stash.sh | 8 ++++++++
4 files changed, 23 insertions(+), 4 deletions(-)
diff --git a/object-name.c b/object-name.c
index c066a7d6a2..1d7cbd8a03 100644
--- a/object-name.c
+++ b/object-name.c
@@ -761,7 +761,7 @@ static int get_oid_basic(struct repository *r, const char *str, int len,
warning(warn_msg, len, str);
if (reflog_len) {
- int nth, i;
+ int nth, i, ret;
timestamp_t at_time;
timestamp_t co_time;
int co_tz, co_cnt;
@@ -789,9 +789,14 @@ static int get_oid_basic(struct repository *r, const char *str, int len,
return -1;
}
}
- if (read_ref_at(get_main_ref_store(r),
- real_ref, flags, at_time, nth, oid, NULL,
- &co_time, &co_tz, &co_cnt)) {
+ ret = read_ref_at(get_main_ref_store(r),
+ real_ref, flags, at_time, nth, oid, NULL,
+ &co_time, &co_tz, &co_cnt);
+ if (ret < 0) {
+ free(real_ref);
+ return -1;
+ }
+ if (ret) {
if (!len) {
if (!skip_prefix(real_ref, "refs/heads/", &str))
str = "HEAD";
diff --git a/refs.c b/refs.c
index 3f7458332e..b6ce62b12a 100644
--- a/refs.c
+++ b/refs.c
@@ -1227,6 +1227,8 @@ int read_ref_at(struct ref_store *refs, const char *refname,
set_read_ref_cutoffs(&cb, 0, 0, "empty reflog");
return 1;
}
+ if (flags & GET_OID_GENTLY)
+ return -1;
if (flags & GET_OID_QUIETLY)
exit(128);
else
diff --git a/refs.h b/refs.h
index ee3b8a62ef..25b8987ca9 100644
--- a/refs.h
+++ b/refs.h
@@ -603,6 +603,10 @@ int refs_create_reflog(struct ref_store *refs, const char *refname,
* that there is one important special case here! If the reflog was empty
* and the caller asked for the 0-th cnt, we will return "1" but leave the
* "oid" field untouched.
+ *
+ * If the reflog was empty and the caller asked for anything else, we die,
+ * or exit silently if "flags" contains GET_OID_QUIETLY. If "flags" contains
+ * GET_OID_GENTLY, we instead return -1.
**/
int read_ref_at(struct ref_store *refs,
const char *refname, unsigned int flags,
diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh
index 74ac66f22e..2c41cc2b3f 100755
--- a/t/t3903-stash.sh
+++ b/t/t3903-stash.sh
@@ -1566,6 +1566,14 @@ test_expect_success 'stash export resolves stash reflog selectors like rev-parse
test_grep "unable to find stash entry stash@{2}" err
'
+test_expect_success 'stash export reports entries of an empty stash reflog' '
+ git stash clear &&
+ git stash import refs/heads/bar &&
+ git reflog expire --expire=all refs/stash &&
+ test_must_fail git stash export --print stash@{1} 2>err &&
+ test_grep "unable to find stash entry stash@{1}" err
+'
+
test_expect_success 'stash export rejects invalid arguments' '
test_must_fail git stash export --print --to-ref refs/heads/invalid 2>err &&
test_grep "exactly one of --print and --to-ref is required" err &&
--
2.56.1.windows.1
^ permalink raw reply related [flat|nested] 8+ messages in thread* [PATCH 4/4] cat-file: report out-of-range reflog entries as missing
2026-02-12 4:07 ` Jeff King
` (3 preceding siblings ...)
2026-11-13 14:28 ` [PATCH 3/4] refs: let read_ref_at() fail gently on an empty reflog Caspar Duregger
@ 2026-11-13 14:28 ` Caspar Duregger
4 siblings, 0 replies; 8+ messages in thread
From: Caspar Duregger @ 2026-11-13 14:28 UTC (permalink / raw)
To: git
Cc: Kristoffer Haugsbakk, Junio C Hamano, Jeff King,
brian m . carlson, Caspar Duregger
The batch modes of "git cat-file" report a name that does not resolve
as "missing" and go on with the next line. A reflog selector past the
end of the reflog is an exception: get_oid_basic() dies, and takes
the batch process and any requests queued behind it down with it:
$ printf 'HEAD@{999999}\nHEAD\n' | git cat-file --batch-check
fatal: log for 'HEAD' only has 3 entries
Pass GET_OID_GENTLY so that these lookups fail like any other. The
previous commits make sure that this changes only lookups that would
have died: selectors at the boundary or by date still resolve, an
emptied reflog fails gently as well, and a peel suffix after the
selector cannot resurrect the name through the reflog date parser.
Not every die() on the way is covered. "HEAD@{999999}~1" still dies,
as get_parent() and get_nth_ancestor() do not pass the caller's flags
down, and so does "@{upstream}" on a detached HEAD.
Reported-by: Kristoffer Haugsbakk <kristofferhaugsbakk@fastmail.com>
Signed-off-by: Caspar Duregger <caspar.duregger@gmail.com>
---
builtin/cat-file.c | 1 +
t/t1006-cat-file.sh | 35 +++++++++++++++++++++++++++++++++++
2 files changed, 36 insertions(+)
diff --git a/builtin/cat-file.c b/builtin/cat-file.c
index 8870a210ec..56dea8eb80 100644
--- a/builtin/cat-file.c
+++ b/builtin/cat-file.c
@@ -627,6 +627,7 @@ static void batch_one_object(const char *obj_name,
struct object_context ctx = {0};
int flags =
GET_OID_HASH_ANY |
+ GET_OID_GENTLY |
(opt->follow_symlinks ? GET_OID_FOLLOW_SYMLINKS : 0);
enum get_oid_result result;
diff --git a/t/t1006-cat-file.sh b/t/t1006-cat-file.sh
index f2b24dd278..5fe2b4ad14 100755
--- a/t/t1006-cat-file.sh
+++ b/t/t1006-cat-file.sh
@@ -451,6 +451,41 @@ test_expect_success 'empty --batch-check notices missing object' '
test_cmp expect actual
'
+test_expect_success '--batch-check reports reflog entries out of range as missing' '
+ test_when_finished "rm -rf reflog-range" &&
+ git init reflog-range &&
+ (
+ cd reflog-range &&
+ test_commit one &&
+ test_commit two &&
+ test_commit three &&
+ # keep a non-null "old" side in the oldest entry
+ git reflog delete HEAD@{2} &&
+ git branch empty &&
+ git reflog expire --expire=all refs/heads/empty &&
+ cat >in <<-\EOF &&
+ HEAD@{2}
+ HEAD@{1970-01-02}
+ HEAD@{3}
+ HEAD@{999999}^{tree}
+ HEAD@{999999}:one.t
+ empty@{1}
+ HEAD
+ EOF
+ cat >expect <<-EOF &&
+ $(git rev-parse one)
+ $(git rev-parse one)
+ HEAD@{3} missing
+ HEAD@{999999}^{tree} missing
+ HEAD@{999999}:one.t missing
+ empty@{1} missing
+ $(git rev-parse three)
+ EOF
+ git cat-file --batch-check="%(objectname)" <in >actual &&
+ test_cmp expect actual
+ )
+'
+
batch_tests () {
boid=$1
loid=$2
--
2.56.1.windows.1
^ permalink raw reply related [flat|nested] 8+ messages in thread