На прошлой неделе я выложил пост про прогулку с собакой. В нём было 2 GPS-трека. Перед публикацией я сдвинул все координаты в Тихий океан: форма прогулки остаётся, место пропадает. Мне казалось, что этим долг по теме приватности закрыт.
А потом я заметил, что пушу коммиты в публичные репозитории с 2012 года, ничего в них не сдвигая и ни разу не посмотрев, что они рассказывают. Про то, что часовой пояс человека угадывается по времени публичных коммитов, пишут как про простое дело. Я это принял на веру. Показывали мне это только на людях, которых уже нашли.
Поэтому я прогнал приём на себе. 26 моих публичных репозиториев, не форки, склонированы голыми с --filter=blob:none: тянет граф коммитов, содержимое файлов пропускает. В клонах 675 коммитов, 574 мои, из них 510 датированы до сегодняшнего дня по UTC. git 2.43.0, gh 2.45.0, Python 3.12.3, Ubuntu 24.04.
Первый прогон нашёл на 23 меньше. Я фильтровал по строке dimhold, а в 23 коммитах стоит рабочий адрес прежнего работодателя, использованный с апреля 2015 по июнь 2021. Этот адрес называет компанию.
26 клонов, которые были не нужны
Эндпоинт commits в REST API нормализует даты автора в Z. Коммит 8c290e4 приходит как 2026-08-19T14:18:38Z. Точно так же его отдаёт /git/commits/{sha}, то есть эндпоинт, от которого я ждал сырых данных. Объект коммита в клоне говорит больше:
author Dmitriy Semenkevich <dimhold@gmail.com> 1787149118 +0300
Секунда unix и смещение от UTC. По месту это 17:18:38. Дальше всё будет про это смещение. Ради него я и склонировал 26 репозиториев. Не нужно было. GitHub отдаёт любой коммит патчем, а в патче стоит почтовый заголовок:
$ curl -s https://github.com/dimhold/whotop/commit/8c290e4.patch | sed -n 3p
Date: Wed, 19 Aug 2026 17:18:38 +0300
Ни токена, ни клона. Есть и четвёртый путь, причём документированный. /stats/punch_card считает коммиты по дням недели и часам. В документации написано, что часы там “based on the time zone of individual commits”. Без логина он отдаёт по той среде ячейку [3, 17, 3], то есть местные 17:00, а не 14:00 по UTC. По всем 26 репозиториям часы из punch_card сходятся с местными часами из клонов, а не с часами UTC. Разность 2 эндпоинтов по всем 26 даёт +3, за 52 запроса и без клона. По одному репозиторию выходит слабее: +3 побеждает в 18 из 26, а +2 в 6. Выяснил я всё это, когда 26 голых клонов уже лежали на диске.
Оценка и 2 часа, на которые она промахивается
Приём простой. Взять публичные времена коммитов в UTC, перебрать все 27 целочасовых смещений и оставить то, при котором меньше всего коммитов попадает в часы, отведённые взрослому работающему человеку на сон. Окно я взял с 01:00 до 06:59 по месту.
На 496 коммитах, которые отдал REST API, побеждает одно смещение, причём уверенно. Это +1, при 6 коммитах внутри окна сна. В 422 объектах моих коммитов из 510 записано +03:00, то есть +3 это мода, а не константа. При +3 те же 496 дают в окне 28.
Значит, оценка промахивается на 2 часа. Смещения +01:00 нет ни в одном из моих 510 коммитов, так что приём возвращает пояс, из которого я не коммитил никогда.
Почему промахивается
Я работаю после полуночи. 32 моих коммита стоят в местных часах 00 и 01. В окне, которое приём считает сном, лежат 28 коммитов. По 510 коммитам из клонов выходит то же число, что по 496 из REST. При той же ширине в 6 часов самое тихое моё окно начинается в 03:00 и держит 12. 2 окна разъезжаются на 2 часа. Это размер ошибки.
Оценка находит то смещение, при котором я похож на нормально спящего человека. Она отодвигает мои часы назад, пока работа после полуночи не начинает читаться как поздний вечер. Эту версию я не проверял: не выбрасывал те коммиты и не пересчитывал оценку заново.
Я ждал, что больше данных вылечит промах. Выходит наоборот. Я 2000 раз брал случайную подвыборку из 496 часов UTC на каждый её размер. При n=20 оценка говорит +1 в 41.2% прогонов. При n=100 она говорит +1 в 59.5%, при n=200 в 67.3%. Настоящее +3 на том же интервале падает с 9.8% до 0.1%. Подвыборки показывают, что оценка сходится к расписанию одного человека, поэтому сломанное допущение от нетипичного режима сна она отличить не может.
Кто это уже сделал
Само поле смещения хорошо истоптано. Работа на OpenSym в 2016 году читала его из логов git и из заголовков рассылок. Работа на SWAN в 2017 гоняла по нему круговую статистику. Работа на MSR в 2022 помогла разнести по карте 2.2 миллиарда коммитов. Mo Beigi опубликовал приём с патчем в мае 2025 и восстановил по нему поездку коллеги. Истоптанное кончается там, где начинается оценка. В обсуждениях бэкдора в xz в апреле 2024 один автор посчитал смещения и получил +0800. Другой посмотрел на гистограмму часов и написал, что гипотеза UTC+03 “seems to have been backwards”. Те же коммиты, 2 грамотных человека, 2 ответа. Сверить их было не с чем.
Что смещение описывает на самом деле
В 7 моих коммитах стоит -07:00 и -08:00, все датированы между 2012 и 2014 годом. Я не понимал, откуда они. Все 7 сходятся с America/Los_Angeles, а между первым и последним умещаются 4 перехода на летнее время. Один из них стоит на -08:00 за целых 4 дня до перехода того года. Часы, которые никто не настраивал, не отслеживают американский переход на летнее время 21 месяц подряд.
Потом я посмотрел, что это за коммиты. 6 из 7 это Initial commit без родителя. Каждый проштампован в пределах 1 секунды от времени создания самого репозитория. Седьмой это мерж пул-реквеста номер 1. Значит 6 из 7 вышли из серверов GitHub, а не с какой-то моей машины, а про седьмой я делаю вывод по одному сообщению. Чьи часы стояли на американском тихоокеанском поясе, замер не говорит, он говорит только, что все 7 этому поясу следуют.
Работает это и в обратную сторону. 96 моих коммитов из 510 стоят на ровной минуте. Это 18.8% там, где клавиатура даёт 1 к 60. В эту группу попадают 65 коммитов из 66 со смещением +02:00, а 41 метка из 96 стоит ровно на часе. Почти все они поставлены руками или скриптом, то есть пятая часть того, что я называю истиной, не про время работы.
Красная линия
В клонах лежали не только мои коммиты. 101 строка из 675 принадлежала 5 другим адресам. В их объектах смещение записано в том же поле, что и в моих.
Это был ровно тот замер, которого статье не хватало. Другие люди, ответы известны заранее, появляется способ отличить сломанное допущение от нетипичного режима сна. Вместо этого я удалил строки вместе с клонами. Число удалённых строк ушло в others-count.txt без адресов.
Отказ обошёлся дешевле, чем я думал. Один из 5 адресов это бот с 3 коммитами. У 4 людей 57, 34, 5 и 2. Оценка по 3 меткам времени попадает в +3 в 12.2% этих подвыборок. Калибровать имело смысл только на 2 из 4. Мой главный результат по-прежнему измерен на выборке из одного человека.
Эти метки времени лежат в моём рабочем каталоге только потому, что я склонировал репозиторий, в который их владельцы когда-то коммитили. Одна строка awk разобрала бы их по часам. То, что это просто, я аргументом не считаю.
Чего я не проверял
Сетка кандидатов целочасовая, поэтому +05:30 и +05:45 она выразить не умеет. Кто-то из других людей в тех же клонах коммитил со смещением -04:30. GraphQL API я не смотрел. И я не проверял, переписывал ли ребейз даты коммитов 2026 года. Если переписывал, то смещения, которые я беру за истину по этому году, младше самих коммитов.