All of lore.kernel.org
 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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.