Re: Reftable reflog timezone encoding differs from specification
flat view
From: Patrick Steinhardt <hidden>
Date: 2026-09-28 12:13:37
Subsystem:
the rest · Maintainer:
Linus Torvalds
Hi, On Mon, Sep 28, 2026 at 12:00:43AM -0700, Josh McKinney wrote:
Hi, Git 2.55.0 appears to store reftable reflog timezone offsets as signed HHMM integers, whereas the specification requires signed minutes. https://git-scm.com/docs/reftable#_log_record states: "tz_offset is the absolute number of minutes from GMT the committer was at the time of the update." The specification also gives GMT+0230 as an example encoded as 150.
Oh dear, that's indeed the case. I was able to reproduce the issue, as well.
I reproduced the discrepancy on macOS arm64 by creating a SHA-1
reftable repository and committing with this date:
2026-09-27T12:00:00+05:30
An independent Python/zlib inspection of the resulting reftable found:
Stored timezone bytes: 02 12 = 530
Expected signed minutes: 01 4a = 330Yup. It's plain wrong the way we store it.
Both HEAD and refs/heads/main reflog entries contained 530. Git reads its own entries back correctly as +05:30, so its writer and reader appear internally consistent, but disagree with the specification. Here is a reproducer using only Git and Python's standard library:
And here's a Git reproducer:
diff --git a/t/helper/test-reftable.c b/t/helper/test-reftable.c
index fc49fafc34..e3cfb6a3fa 100644
--- a/t/helper/test-reftable.c
+++ b/t/helper/test-reftable.c@@ -103,7 +103,16 @@ static int dump_table(struct reftable_merged_table *mt) if (err < 0) return err; - algop = &hash_algos[hash_algo_by_id(reftable_merged_table_hash_id(mt))]; + switch (reftable_merged_table_hash_id(mt)) { + case REFTABLE_HASH_SHA1: + algop = &hash_algos[GIT_HASH_SHA1]; + break; + case REFTABLE_HASH_SHA256: + algop = &hash_algos[GIT_HASH_SHA256]; + break; + default: + die("unsupported hash algorithm: %d", reftable_merged_table_hash_id(mt)); + } while (1) { err = reftable_iterator_next_ref(&it, &ref);
diff --git a/t/t0610-reftable-basics.sh b/t/t0610-reftable-basics.sh
index 35e98b43db..e1d714077c 100755
--- a/t/t0610-reftable-basics.sh
+++ b/t/t0610-reftable-basics.sh@@ -1163,4 +1163,19 @@ test_expect_success 'writes do not persist peeled value for invalid tags' ' ) ' +test_expect_success 'writes do not persist peeled value for invalid tags' ' + test_when_finished rm -rf repo && + git init repo && + ( + cd repo && + + export GIT_AUTHOR_DATE='2026-09-27T12:00:00+05:30' && + export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" && + git commit --allow-empty --message "Timezone example" && + git refs optimize && + test-tool dump-reftable -t .git/reftable/*.ref >table && + test_grep "log{HEAD(2) C O Mitter <committer@example.com> 1790490600 0530" table + ) +' + test_done
And yes, the test-helper for reftables is broken, so we also have to fix that. [snip]
Is this a known discrepancy?
No, it's not, I wasn't aware of it at all.
Which representation should interoperable implementations use?
We should use the one that we have in our specification, so in my
opinion we should fix Git itself. This is also because JGit, which had a
reftable implementation for far longer compared to us, implements the
specification correctly:
private PersonIdent readPersonIdent() {
String name = readValueString();
String email = readValueString();
long epochSeconds = readVarint64();
ZoneOffset tz = ZoneOffset.ofTotalSeconds(readInt16() * 60);
return new PersonIdent(name, email, Instant.ofEpochSecond(epochSeconds), tz);
}
You can see that we indeed treat the integer as number of minutes there,
as expected.
If either the implementation or specification changes, how should existing tables be interpreted, given that values such as 330 are valid under both interpretations? Given that Git consistently writes and reads HHMM values, I suspect the practical resolution is to update the specification to match existing behavior. Are there other implementations or compatibility considerations that would prevent that?
That's a very good question. Given that JGit interprets the value as
expected my take is that we should fix this in Git and keep the spec
as-is.
The question is how much we really lose by "just" fixing the bug. Sure,
timezones would be wrong in that case. For example, if we had an entry
with original timezone of +0200 we'd now interpret that as +0320. It's
of course wrong given the original intent, but is it the end of the
world? I dunno. Overall, the amount of damage is kind of limited here as
the discrepancy is limited:
┌──────┬──────────────┬────────────────┬───────────┐
│tz │HHMM encoding │correct minutes │divergence │
├──────┼──────────────┼────────────────┼───────────┤
│+1400 │1400 │840 │560 │
├──────┼──────────────┼────────────────┼───────────┤
│-1200 │-1200 │-720 │480 │
├──────┼──────────────┼────────────────┼───────────┤
│+0530 │530 │330 │200 │
├──────┼──────────────┼────────────────┼───────────┤
│+0000 │0 │0 │0 │
└──────┴──────────────┴────────────────┴───────────┘
We could of course retroactively declare that version 2 of the format
uses the syntax that Git uses right now. After all, JGit only knows to
read version 1 of it anyway, so that could kind of fix it. But for any
repository that uses SHA1 we used to write version 1 anyway, so this
does not really buy us anything, I'd claim.
In summary:
- We have an upper limit in divergence of <10h.
- This only matters in the context of reflogs, we don't use these
anywhere else.
- The risk for data loss by a change is limited as our default grace
period for garbage collecting reflog entries is 30 days.
With these points I'm inclined to call it a bug and just fix it, without
handling backwards compatibility.
I'm very happy to hear alternative takes though.
In any case, thanks for your report!
Patrick