Git development
 help / color / mirror / Atom feed
* cat-file: dies with --batch if reflog entry is out of bounds
@ 2026-02-11 23:13 Kristoffer Haugsbakk
  2026-02-11 23:40 ` Junio C Hamano
  2026-02-12  4:07 ` Jeff King
  0 siblings, 2 replies; 8+ messages in thread
From: Kristoffer Haugsbakk @ 2026-02-11 23:13 UTC (permalink / raw)
  To: git

    $ git cat-file --batch
    HEAD@{7}
    <commit>
    branch@{20}
    fatal: log for 'branch' only has 5 entries
    [died]

This is a known limitation.

It used to just die without any flags until recently in 393bbb21
(object-name: make get_oid quietly return an error, 2025-06-12).[1] That
added a `GET_OID_GENTLY` flag which is respected here. If that is set
it returns normally from the function with an error.

Before that I was looking at `builtin/cat-file.c` and guessing that
`GET_OID_GENTLY` would be the thing that made `--batch` loop on. But it
seems to be the lack of `GET_OID_ONLY_TO_DIE`; that flag seems to be
used when you do a one object lookup like `git cat-file commit
HEAD`. And it is indeed not set for the batch modes.

`GET_OID_GENTLY` from commit 393bbb21 was added it seems so that a
commit in the same series could try to look up a reflog entry in
`builtin/stash.c` and give its own error message. And go to `cleanup` as
well. It’s the only user/usage.

I’m pretty new to git-cat-file(1) but it seems very consistent in
returning “missing” for object names that don’t exist, “little red
riding hood” and malformed rev expressions. Is this the only
could-be-an-object input it will die on?

[1]: https://lore.kernel.org/git/20250508234458.3665894-2-sandals@crustytoothpaste.net/#t

-- 
Kristoffer Haugsbakk
code at khaugsbakk.name

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

* Re: cat-file: dies with --batch if reflog entry is out of bounds
  2026-02-11 23:13 cat-file: dies with --batch if reflog entry is out of bounds Kristoffer Haugsbakk
@ 2026-02-11 23:40 ` Junio C Hamano
  2026-02-12  4:07 ` Jeff King
  1 sibling, 0 replies; 8+ messages in thread
From: Junio C Hamano @ 2026-02-11 23:40 UTC (permalink / raw)
  To: Kristoffer Haugsbakk; +Cc: git

"Kristoffer Haugsbakk" <kristofferhaugsbakk@fastmail.com> writes:

>     $ git cat-file --batch
>     HEAD@{7}
>     <commit>
>     branch@{20}
>     fatal: log for 'branch' only has 5 entries
>     [died]
>
> This is a known limitation.

It is pretty safe to call this a BUG, given that everywhere we seem
to try very hard to diagnose "you asked me about X, but that does
not name anything I know about", as you found out..

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

* Re: cat-file: dies with --batch if reflog entry is out of bounds
  2026-02-11 23:13 cat-file: dies with --batch if reflog entry is out of bounds Kristoffer Haugsbakk
  2026-02-11 23:40 ` Junio C Hamano
@ 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
                     ` (4 more replies)
  1 sibling, 5 replies; 8+ messages in thread
From: Jeff King @ 2026-02-12  4:07 UTC (permalink / raw)
  To: Kristoffer Haugsbakk; +Cc: git

On Thu, Feb 12, 2026 at 12:13:36AM +0100, Kristoffer Haugsbakk wrote:

>     $ git cat-file --batch
>     HEAD@{7}
>     <commit>
>     branch@{20}
>     fatal: log for 'branch' only has 5 entries
>     [died]
> 
> This is a known limitation.

I expect there are others. E.g.:

  $ git checkout --detach
  $ echo @{upstream} | git cat-file --batch
  fatal: HEAD does not point to a branch

So I'd guess that there is probably a lot more work to be done on top of
393bbb21 to make GET_OID_GENTLY work consistently. But doing that will
incrementally take us in a good direction (for cat-file and for other
callers).

-Peff

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

* [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

end of thread, other threads:[~2026-11-13 14:28 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-02-11 23:13 cat-file: dies with --batch if reflog entry is out of bounds Kristoffer Haugsbakk
2026-02-11 23:40 ` Junio C Hamano
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   ` [PATCH 2/4] object-name: let GET_OID_GENTLY affect only failing lookups 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

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox