Есть такой род статей на Хабре — читаешь ее и думаешь — вот как она была написана? Материал большой, длинный, много кода, и он в процессе повествования эволюционирует, улучшается. Автор показывает результаты выполнения, прогоны, бенчмарки. У статьи есть сюжет, драматургия, насколько это возможно для технической статьи — баги, тупики, отчаяние, метания и неожиданные озарения.

Я не раз вопрошал в восхищении: "вот как?..". Ведь если задуматься — чтобы автору выплеснуть на вас все палеонтологические слои своего кода, все версии от самых ранних и неуклюжих шагов к цели до матерого кода, обросшего абстракциями — как автор должен выстроить свою работу, чтобы в результате статья случилась такой, какой она есть?

Автор создавал статью параллельно с написанием кода и записывал все происходящее с ним "онлайн"? Эдакая разработка с карандашом и диктофоном. А может это все-таки честная ретроспектива, и автор сначала сделал, что хотел, а потом выуживал все подробности из памяти, пока свежо? Или, вероятно, подход гибридный? Тут зафиксировал веху, там сделал скриншот на всякий случай, здесь записал на полях важное примечание.

Я не спрашивал ни одного из авторов таких статей, как они это сделали. И, возможно, зря — мне не приходило это в голову до того момента, как я сел сейчас писать эту небольшую статью. Статью про то, как эту самую проблему стал решать я, когда внезапно сам для себя начал писать ретроспективные статьи на Хабр.

Мой опыт

Первая моя большая статья подобного жанра — где спустя время ты повествуешь историю успеха или неудачи, случилась спонтанно. И она была о событиях годовалой давности, о том как мы писали игру, так что это была честная ретроспективная статья. В ней не очень много "code-driven" повествования, но тем не менее даже в этой статье я чувствовал острую необходимость прыгать в прошлое не только воспоминаниями, но и непосредственно кодом. Чтобы вспомнить, как конкретно что-то баговало, чтобы получить скриншот "из прошлого" или уточнить, как тогда выглядел код или игра.

Очевидно, там где есть команда и код, там есть система контроля версий. И конечно же я просто прыгал по старым коммитам в git, чтобы выудить для себя нужную информацию.

Однако же, я часто сталкивался с трудностями. И все они сводились к одному и тому же — коммиты, сделанные в реальных полевых условиях для реального проекта — это не всегда лучшие коммиты для будущей ретроспективы. Типовые проблемы:

  • Именования коммитов не всегда хорошо ориентируют тебя в истории: ты ищешь место, где в игре еще не было работающего главного меню, но записи не позволяют однозначно понять, где же это было

  • Если коммит был достаточно большим, или, особенно, если это был squash-commit большой ветки, может оказаться, что в истории не существует того состояния игры, которое ты ищешь, потому что оно как бы промежуточное

  • Плохо переписанная история (rebase, cherry-pick, reset) может исказить действительную хронологию событий, и искать что-то станет куда труднее

  • Тупиковые эксперименты и неудавшиеся вариации программы могут и вовсе не остаться в истории гита

В общем, с этой статьей я набил шишек, потратил очень много лишнего времени и понял, что в следующий раз, если я буду знать или хотя бы подозревать, что код попахивает статьей, я отнесусь к ведению гита ответственно и расставлю в репозитории все необходимые метки, флаги и маяки.

Так и случилось, достаточно скоро, со статьей Punk riff generator. Это был тот случай, когда тебе приходит в голову дурацкая идея и ты сразу говоришь себе: "повеселюсь, еще и статью напишу". Соответственно, эта статья стала хорошим полигоном для обкатки практик хабрагитоведения.

Я вел GitHub репозиторий и аккуратно протоколировал все действия. Правило было буквально одно — ответственно относиться к коммитам. Чтобы это были атомарные, завершенные части работы; размер небольшой или маленький; хорошее именование. Ну, программисты и так знают, что так оно по-хорошему и должно быть.

Вот как выглядела история проекта по его завершению:

git-история проекта Punk riff generator
git-история проекта Punk riff generator

Мне потом действительно было легко писать статью, имея на руках такой конспект. Не скажу, что та статья далась мне легко, но по части ретроспективных откатов назад по коду у меня проблемы отсутствовали.

Обращу внимание на обилие тегов — ими было удобно проставлять значимые этапы, вехи. Например, теги могут играть роль того, что у вас в статье превратится в главы.

Второй пример совсем недавний — эпичная статья на 49 минут прочтения про подкидывание монетки. Многоплановое развитие кода, частые отступления от темы, смена цели в ходе повествования, тупиковые решения, баги. Все нужно было тщательно запротоколировать, чтобы статью можно было писать в комфортном режиме.

История репозитория по окончанию работы была страшноватой на первый взгляд:

git-история проекта Coin flip brutal
git-история проекта Coin flip brutal

Да, выглядит запутанно. Но если автор умеет ориентироваться в своей же системе, то ему не составит труда эффективно пользоваться этой шпаргалкой. Самый большой профит от хорошо простроенной истории проекта я получил, когда возникла необходимость задним числом сделать бенчмарки на очень большое количество версий программы. Полагаю, без нормальной гитовой истории я бы просто сдался.

Хороший git

Советы ведения репозитория, который должен быть в боевой готовности для потенциальной статьи:

  • Грамотно оформленные коммиты — база. Перед тем как сделать коммит, подумайте, насколько информативным, полезным и прозрачным он будет для вас будущего, который роется в истории в поисках нужной информации

  • Тэги могут дополнительно помочь для расстановки ключевых моментов или добавления любой мета-информации

  • Если у вас есть тупиковый сценарий, но вы хотите сохранить его в истории или, например, вы наткнулись на показательный баг, создайте ветку. В выделенной ветке этот код будет лежать обособленно и отдельно от основного "флоу" повествования. К тому же вы получаете тег автоматом

Плохой git

Я расписывал, как все хорошо, но есть одна большая проблема. Она возникает, когда вам хочется переписать историю репозитория. А вам захочется ее переписать. Причин может быть множество:

  • Спустя 10 коммитов вы поняли, как можно написать показательнее и прозрачнее

  • Вы осознали, насколько дурацкое имя у функции, которую вы написали еще в самом начале работы с кодом, и вся история и все коммиты пронизаны использованием этой функции

  • Вы хотите немного поменять местами хронологию событий

И все эти действия возможны! Вам на помощь придут git rebase -i, git cherry-pick, git filter-branch. Проблема лишь в том, что при переписывании истории тэги и ветки остаются на прежних, старых местах. Т.е. ветка с багом, которая почковалась от коммита aaaa, так и останется базированной на aaaa, даже если ваш "настоящий" aaaa уже давно превратился в bbbb в процессе переписывания истории через git rebase . И если вы в какой-то момент переписали историю "с головы до ног", все-все ветки (и тэги) придется вручную, через нелегкие игры с git reset --hard и git reflog, пришивать на обновленные коммиты. Процесс настолько трудоемкий, что я бы не рекомендовал им заниматься.

А это подводит меня к единственно возможному выводу: git, вероятно, не лучший инструмент, не лучшая система контроля версий для подобной задачи, где постоянное переписывание истории коммитов — типовой рабочий процесс. Проблема в том, что я не знаком так сильно с альтернативными системами контроля версий. И был бы рад, если кто-то мог подсказать что-то стоящее.

А, возможно, вы пишете свои ретроспективные статьи совсем по-другому? Возможно, есть путь куда более простой. И я бы и здесь был рад, если кто-то сможет поделиться своим опытом.

Бонус-шпаргалка

Шпаргалка по крайне специфичным git-командам

- Переименовать функцию `doWork` в `spin` по всей-всей истории:

git filter-branch --tree-filter 'find . -type f -name "*.cpp" | xargs sed -i "s/doWork/spin/g"' -- --all
  • То же самое, но для диапазона коммитов:

git filter-branch --tree-filter 'find . -type f -name "*.cpp" | xargs sed -i "s/doWork/spin/g"' -- --all deadbe..abcdef
  • Удалить все тэги локально:

git tag -d $(git tag -l)
  • Удалить все тэги удаленно:

git ls-remote --tags origin | awk '{print $2}' | sed 's#refs/tags/##' | while read -r tag; do
  git push origin --delete "$tag"
done
  • Поменять коммиты aaa1 и aaa2 местами:

    • Вызываем git rebase -i aaaa1~

    • В списке коммитов меняем местами строки aaa1 и aaa2

    • Решаем конфликты

  • Разбить старый коммит на несколько:

    • Вызываем git rebase -i aaaa~

    • В списке коммитов помечаем коммит aaaa как e (edit) и продолжаем

    • git reset HEAD~

    • Далее делаем столько коммитов из неиндексированных изменений, сколько захочется

  • Из множества изменений в одном файле точечно проиндексировать только желаемые:

# запустит интерактивный режим добавления изменений
git add -p

Комментарии (5)


  1. Kodex_Faber
    28.09.2025 10:33

    Очень полезная, отличная статья. Автору большое спасибо.

    Если честно, то не очень люблю конкретно вот такие ретроспективные статьи о которых пишет автор. Коротко и по делу - вот мой критерий оценки статей. Еще в библии сказано: "да и нет - остальное от лукавого..."


    1. AskePit Автор
      28.09.2025 10:33

      А вот я фанат кодового нарратива - мне кажется это уникальным жанром технической литературы :)


  1. sepulkary
    28.09.2025 10:33

    ... вы поняли, как можно написать показательнее и прозрачнее

    Вы осознали, насколько дурацкое имя у функции, которую вы написали еще в самом начале

    На мой взгляд, это повод создать еще один коммит, только и всего.

    Вы хотите немного поменять местами хронологию событий

    Зачем?

    Мой стиль использования git, наверное, можно назвать "наивным", но я предпочитаю использовать его в стиле "бумажного ежедневника" - если уж что-то в git попало, то всё, теперь уже навечно, не надо пытаться это корректировать. Бывают, конечно, исключения, вроде инфицирования репозитория большим динамически изменяющимся бинарником, тогда да, нужно почистить.

    Git действительно позволяет проворачивать нам всяческие кунштюки, но же не забывайте, что в больших проектах бывает еще и человеческий лог, вики, документация и прочее, что тоже требует постоянной актуализации. Если вы залезете во внутренности действительно "взрослого" проекта, к примеру, Linux или git, то думаю, там вы вряд ли найдёте широкое применение описанных вами инструментов.


  1. kinjalik
    28.09.2025 10:33

    Я правильно понимаю, что в такие моменты (написания) существование git bisect полностью забывается? Не в укор, а скорее как вопрос "за жизу"


    1. AskePit Автор
      28.09.2025 10:33

      не обязательно, но и не исключено :). просто это дольше, чем если иметь историю, которую можно просто прочитать и сразу найти место.