dimhold.by
← Writing

The timezone my public commits give away is the wrong one

Last week I published a post about walking my dog. It carried 2 GPS tracks. I shifted every coordinate into the Pacific first, which keeps the shape of a walk and drops where it happened. That felt like the whole of my duty on the subject.

Then I noticed I have been pushing commits to public repositories since 2012 without shifting anything or looking at what they say. Guessing somebody’s timezone from public commit times gets described as easy and I had assumed it works. I had only ever seen it demonstrated on people who were already found.

So I ran it on myself. 26 of my public repositories that are not forks, cloned bare with --filter=blob:none, which pulls the commit graph and skips file contents. 675 commits in the clones, 574 of them mine, 510 of those dated before today in UTC. git 2.43.0, gh 2.45.0, Python 3.12.3, Ubuntu 24.04.

My first pass found 23 fewer, because I filtered on the string dimhold and 23 of my commits carry a former employer’s work address instead. That address names the company and brackets 6 years.

The 26 clones I did not need

The commits endpoint of the REST API normalises author dates to Z. Commit 8c290e4 comes back as 2026-08-19T14:18:38Z. So does /git/commits/{sha}, which is the endpoint I expected to be raw. The commit object in the clone says more:

author Dmitriy Semenkevich <dimhold@gmail.com> 1787149118 +0300

A unix second and a UTC offset. 17:18:38 local. That offset is what everything below is about and it is why I cloned 26 repositories. I did not need to. GitHub serves any commit as a patch with a mail header:

$ curl -s https://github.com/dimhold/whotop/commit/8c290e4.patch | sed -n 3p
Date: Wed, 19 Aug 2026 17:18:38 +0300

No token and no clone. There is a fourth route and GitHub documents it. /stats/punch_card counts commits per weekday and hour. The docs say the hours are “based on the time zone of individual commits”. Unauthenticated, it reports [3, 17, 3] for that Wednesday, which is the local 17:00 rather than the 14:00 in UTC. Differencing it against /commits over all 26 repositories lands on +3, in 52 requests and no clone. Per repository it is weaker, with +3 winning outright in 18 of the 26 and +2 winning in 6. I found all this out with 26 bare clones already on disk.

commits api 14:18:38Z offset gone bare clone 1787149118 +0300 offset kept commit.patch 17:18:38 +0300 offset kept punch_card [3, 17, 3] local hour kept the same wednesday afternoon, only 1 of the 4 routes loses the clock
One commit, 4 public routes, no login on any of them. The commits endpoint normalises the offset away, which is why I cloned 26 repositories. The patch route hands over the same field in 1 request and punch_card counts by local hour because GitHub documents it that way.

The estimator and the 2 hours it misses

The trick is simple. Take the public commit times in UTC and try all 27 whole hour offsets. Keep the one that puts the fewest commits inside the hours a working adult is assumed to sleep. I used 01:00 to 06:59 local.

Across the 496 commits the REST API returned, one offset wins outright. It is +1, with 6 of them in the sleeping window. 422 of my 510 commit objects say +03:00, which makes +3 the mode rather than a constant. At +3 the same 496 put 28 in the window.

So the estimate is off by 2 hours. +01:00 does not appear in a single one of my 510 commits, so the trick returns an offset I have never committed from.

45 -2 26 -1 11 +0 6 +1 9 +2 28 +3 45 +4 76 +5 105 +6 134 +7 commits landing in the assumed sleep window candidate offset applied to the utc hours the minimum, also the wrong answer what 422 of 510 commits say
The estimator has one clean minimum over all 27 candidates and it sits 2 hours from the offset most of my commits state outright. No commit of mine carries plus 01:00 at all.

Why it misses

I work after midnight. 32 of my commits sit in local hours 00 and 01. The window the trick assumes to be sleep holds 28. Counting the 510 from the clones gives the same number as the 496 from REST. Measured at the same 6 hour width, my quietest window starts at 03:00 and holds 12. The 2 windows are 2 hours apart, which is the size of the error.

What the estimator finds is the offset that makes me look like a normal sleeper. It gets there by sliding my clock back until the work after midnight reads as late evening. I have not tested that story by dropping those commits and estimating again.

00 03 06 09 12 15 18 21 hour of day, as the commit object records it assumed asleep 01:00 to 06:59, holds 28 quietest 6 hours start at 03:00 and hold 12 62 at 17:00
The assumed sleeping window and the quietest window of the same width start 2 hours apart. That gap is where the error comes from. Every hour here is read off the offset stored in the commit.

I expected more data to fix this and it does the opposite. I drew 2000 random subsamples of the 496 UTC hours at each size. At n=20 the estimate says +1 in 41.2% of runs. At n=100 it says +1 in 59.5% of them and at n=200 in 67.3%. Over the same range +3 falls from 9.8% to 0.1%. The subsamples show the estimate converging on one person’s schedule, so they cannot separate a broken assumption from an unusual sleeper.

Who has done this already

The offset field is old ground. An OpenSym paper in 2016 read it off git logs and mailing lists. A SWAN paper in 2017 ran circular statistics over it. An MSR paper in 2022 used it to help place 2.2 billion commits on a map. Mo Beigi published the patch route in May 2025 and used it to reconstruct a colleague’s travel. The estimator is where the ground gets thin. In the xz backdoor threads of April 2024 one analyst counted the offsets and read +0800. Another looked at the hour histogram and wrote that the UTC+03 hypothesis “seems to have been backwards”. Same commits, 2 competent people, 2 answers. Neither had ground truth to settle it.

What the offset actually describes

7 of my commits carry -07:00 and -08:00, dated between 2012 and 2014. I had no idea why. All 7 match America/Los_Angeles across the 4 daylight saving switches that fall between them. One sits at -08:00 a full 4 days before that year’s switch. A clock nobody configured does not track US daylight saving for 21 months.

Then I checked what those commits were. 6 of the 7 are an Initial commit with no parent, each stamped within 1 second of the repository’s own creation time. The 7th is a merge of pull request #1. So 6 of the 7 came out of GitHub’s servers rather than a machine of mine. The 7th I am inferring from the message alone. Whose clock was set to US Pacific the data does not say, only that all 7 follow it.

It cuts the other way too. 96 of my 510 commits land on an exact minute, which is 18.8% where 1 in 60 is what a keyboard produces. 41 of the 96 sit exactly on the hour. Almost all of them were set by hand or by a script. A fifth of what I call ground truth is not when I worked.

The red line

The clones did not hold only my commits. 101 of the 675 rows belonged to 5 other addresses. Their objects carry offsets in the same field mine do.

That looked like the experiment this post wanted. Other people, answers known in advance, a way to tell a broken assumption from an unusual sleeper. I deleted the rows and the clones instead. The count of what was destroyed went into others-count.txt without the addresses.

The refusal cost me less than I expected. 1 of the 5 addresses is a bot with 3 commits. The 4 people hold 57, 34, 5 and 2. An estimate from 3 timestamps hits +3 only 12.2% of the time in those subsamples. Only 2 of those 4 were worth calibrating against. My headline number is still measured on a sample of 1.

Those timestamps sit in my working directory only because I cloned a repository their owners once contributed to. One line of awk would have profiled them and I do not think the ease of it is an argument.

What I did not check

The candidate grid is whole hours, so it cannot express +05:30 or +05:45. One of the other people in the same clones committed with an offset of -04:30. I did not look at the GraphQL API. I also did not check whether a rebase had rewritten any 2026 commit dates. If it had, the offsets I am treating as ground truth for this year are younger than the commits they sit in.