read_ref_at(): special-case ref@{0} for an empty reflog

The previous commit special-cased get_oid_basic()'s handling of ref@{n} for a reflog with n entries. But its special case doesn't work for ref@{0} in an empty reflog, because read_ref_at() dies when it notices the empty reflog! We can make this work by special-casing this in read_ref_at(). It's somewhat gross, for two reasons: 1. We have no reflog entry to describe in the "msg" out-parameter. So we have to leave it uninitialized or make something up. 2. Likewise, we have no oid to put in the "oid" out-parameter. Leaving it untouched is actually the best thing here, as all of the callers will have initialized it with the current ref value via repo_dwim_log(). This is rather subtle, but it is how things worked in 6436a20284 (refs: allow @{n} to work with n-sized reflog, 2021-01-07) before we reverted it. The key difference from 6436a20284 here is that we'll return "1" to indicate that we _didn't_ find the requested reflog entry. Coupled with the special-casing in get_oid_basic() in the previous commit, that's enough to make looking up ref@{0} work, and we can flip 6436a20284's test back to expect_success. It also means that the call in show-branch which segfaulted with 6436a20284 (and which is now tested in t3202) remains OK. The caller notices that we could not find any reflog entry, and so it breaks out of its loop, showing nothing. This is different from the current behavior of producing an error, but it's just as reasonable (and is exactly what we'd do if you asked it to walk starting at ref@{1} but there was only 1 entry). Thus nobody should actually look at the reflog entry info we return. But we'll still put in some fake values just to be on the safe side, since this is such a subtle and confusing interface. Likewise, we'll document what's going on in a comment above the function declaration. If this were a function with a lot of callers, the footgun would probably not be worth it. But it has only ever had two callers in its 18-year existence, and it seems unlikely to grow more. So let's hold our noses and let users enjoy the convenience of a simulated ref@{0}. Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jeff King committed Feb 26, 2024 at 05:08 UTC 5edd12672086c6b6d92147925dda5dd3bca2b658
4 files changed +32 -4
refs.c
+15
@@ -1109,6 +1109,21 @@ int read_ref_at(struct ref_store *refs, const char *refname,
1109 refs_for_each_reflog_ent_reverse(refs, refname, read_ref_at_ent, &cb);
1110
1111 if (!cb.reccnt) {
1112 + if (cnt == 0) {
1113 + /*
1114 + * The caller asked for ref@{0}, and we had no entries.
1115 + * It's a bit subtle, but in practice all callers have
1116 + * prepped the "oid" field with the current value of
1117 + * the ref, which is the most reasonable fallback.
1118 + *
1119 + * We'll put dummy values into the out-parameters (so
1120 + * they're not just uninitialized garbage), and the
1121 + * caller can take our return value as a hint that
1122 + * we did not find any such reflog.
1123 + */
1124 + set_read_ref_cutoffs(&cb, 0, 0, "empty reflog");
1125 + return 1;
1126 + }
1127 if (flags & GET_OID_QUIETLY)
1128 exit(128);
1129 else
refs.h
+14 -1
@@ -440,7 +440,20 @@ int refs_create_reflog(struct ref_store *refs, const char *refname,
440 struct strbuf *err);
441 int safe_create_reflog(const char *refname, struct strbuf *err);
442
443 -/** Reads log for the value of ref during at_time. **/
443 +/**
444 + * Reads log for the value of ref during at_time (in which case "cnt" should be
445 + * negative) or the reflog "cnt" entries from the top (in which case "at_time"
446 + * should be 0).
447 + *
448 + * If we found the reflog entry in question, returns 0 (and details of the
449 + * entry can be found in the out-parameters).
450 + *
451 + * If we ran out of reflog entries, the out-parameters are filled with the
452 + * details of the oldest entry we did find, and the function returns 1. Note
453 + * that there is one important special case here! If the reflog was empty
454 + * and the caller asked for the 0-th cnt, we will return "1" but leave the
455 + * "oid" field untouched.
456 + **/
457 int read_ref_at(struct ref_store *refs,
458 const char *refname, unsigned int flags,
459 timestamp_t at_time, int cnt,
t/t1508-at-combinations.sh
+1 -1
@@ -110,7 +110,7 @@ test_expect_success '@{1} works with only one reflog entry' '
110 test_cmp_rev newbranch~ newbranch@{1}
111 '
112
113 -test_expect_failure '@{0} works with empty reflog' '
113 +test_expect_success '@{0} works with empty reflog' '
114 git checkout -B newbranch main &&
115 git reflog expire --expire=now refs/heads/newbranch &&
116 test_cmp_rev newbranch newbranch@{0}
t/t3202-show-branch.sh
+2 -2
@@ -279,8 +279,8 @@ test_expect_success '--reflog shows reflog entries' '
279
280 test_expect_success '--reflog handles missing reflog' '
281 git reflog expire --expire=now branch &&
282 - test_must_fail git show-branch --reflog branch 2>err &&
283 - grep "log .* is empty" err
282 + git show-branch --reflog branch >actual &&
283 + test_must_be_empty actual
284 '
285
286 test_done