Объяснение Git: Переписывание истории
Одна из ключевых возможностей Git'а — переписывание истории
, то есть изменение
существующих коммитов. Я использую кавычки, потому что — несмотря на видимость — история Git'а неизменна. По замыслу, невозможно изменить или удалить существующий коммит с помощью обычных команд Git.
И всё же у многих пользователей Git'а есть страх потерять некоторые изменения или устроить беспорядок. На самом деле, любое зафиксированное изменение можно считать безопасным, то есть его можно найти даже после изменения
истории.
Это первая часть из серии Объяснение Git:
- Часть 1: Переписывание истории
- Часть 2: Диапазоны коммитов
Что означает переписывание истории git
Зачем вам вообще нужно изменять историю Git? Представим, что вы допустили опечатку в последнем коммите:
* 5c7a782 - (HEAD -> main) Fix deefct 47
* fb2546f - Add checkout page
Обычно для переписывания
истории Git'а можно использовать такие команды, как reset
или rebase (-i)
. Однако исправление последнего коммита встречается довольно часто, поэтому есть более простая альтернатива:
git commit --amend -m "Fix defect 47"
Повторный просмотр истории Git показывает правильное сообщение:
* 2b049ea - (HEAD -> main) Fix defect 47
* fb2546f - Add checkout page
Кажется, что мы только что изменили последний коммит. Однако хэш изменился с 5c7a782
на 2b049ea
, что означает, что мы создали новый коммит. Вот причина:
Поскольку мы только что изменили сообщение коммита, Git создал новый коммит с другим хэшем. То же самое происходит при перемещении
(например, ребазинге) коммитов, потому что меняется родительский коммит.
Недоступные коммиты
Но куда делся другой коммит? По умолчанию git log
скрывает все коммиты, недоступные по какому-либо указателю, например HEAD
или ветвь. Эти недоступные (или висящие) коммиты можно отобразить с помощью флага --reflog
:
git log --graph --oneline --reflog
* 2b049ea - (HEAD -> main) Fix defect 47
| * 5c7a782 - Fix deefct 47
|/
* fb2546f - Add checkout page
В качестве альтернативы можно использовать git reflog
для поиска недоступных коммитов:
git reflog
2b049ea (HEAD -> master) HEAD@{0}: commit (amend): Fix defect 47
5c7a782 HEAD@{1}: commit: Fix deefct 47
fb2546f HEAD@{2}: commit: Add checkout page
Как видно, изменение
истории — это не что иное, как создание новых коммитов и перемещение указателей HEAD
и main
. Поэтому более подходящим является термин альтернативная история
. Это также означает, что при необходимости можно отменить разрушительную
команду commit --amend
:
git reset --hard 5c7a782
AuthorDate
vs. CommitDate
Git хранит две временные метки для каждого коммита:
AuthorDate
: Дата оригинального коммита.CommitDate
: Дата (последней) "изменённого" коммита.
При создании нового коммита обе временные метки будут одинаковыми. Однако при изменении
существующего коммита они будут отличаться. При использовании таких команд, как show
или log
, Git по умолчанию отображает дату коммита. Чтобы увидеть обе временные метки, используйте fuller
формат:
git show --format=fuller 2b049ea
commit 2b049eadac74e183e48b918e377e41765fca2a99
Author: Darek Kay
AuthorDate: Thu Mar 31 19:18:02 2022
Commit: Darek Kay
CommitDate: Fri May 6 18:26:49 2022
Если хотите синхронизировать дату коммита с оригинальной (авторской) датой при изменении
истории Git, используйте флаг --committer-date-is-author-date
:
git rebase -i --committer-date-is-author-date
Сборка мусора
Ранее я утверждал, что все коммиты можно считать безопасными. Однако есть одно ограничение:
Особенно в потоке rebase создаётся и копируется множество коммитов. Сборщик мусора делает уборку и удаляет все висящие коммиты через определённое время. В повседневной работе я редко хочу их сохранять. Если хотите, назначьте ветку на висящий коммит:
git branch my-branch 5c7a782
Изменение публичной истории
Пока мы изменяем
коммиты, не являющиеся публичными (т. е. они не были выложены в удалённый репозиторий), можно делать всё что угодно. Всё становится сложнее, когда необходимо переместить публичную ветвь.
Общепринятая практика Git гласит:
Не изменяйте публичную историю
Думаю, это отличный совет для новичков в Git, но он может быть ограничивающим, если вы и ваши коллеги знаете, что делаете.
Давайте сначала посмотрим, к каким последствиям приведёт переписывание публичной истории Git. Предположим, что исправленный коммит из предыдущего раздела уже был выгружен на удалённый origin
. После выполнения git commit --amend
лог будет выглядеть следующим образом:
* 2b049ea - (HEAD -> main) Fix defect 47
| * 5c7a782 - (origin/main) Fix deefct 47
|/
* fb2546f - Add checkout page
Если попытаться выложить исправленный коммит в origin
, то возникнет ошибка:
git push
To ../origin/
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to '../origin/'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Это ожидаемое поведение, потому что по умолчанию git push
разрешено добавлять новые коммиты только на последнюю вершину
удалённого (origin
). Git предлагает простое решение: интегрировать удалённые изменения
. В нашем случае git pull
эквивалентен git merge origin/main
, а это не то что нужно. Вместо этого мы хотим заменить удалённый коммит. Этого можно добиться с помощью force push
:
git push -f
git push --force-with-lease # безопасная версия
Теперь всё выглядит хорошо:
* 2b049ea - (HEAD -> main, origin/main) Fix defect 47
* fb2546f - Add checkout page
Но теперь и Janet, и Steve, и все остальные коллеги, работающие над ветвью main
, получат ту же проблему, что и вы раньше. Вот почему в проектах часто запрещено принудительное обновление общих ветвей (например, main
, develop
).
Как поступить в этой ситуации?
Если вы сомневаетесь, следуйте рекомендациям Не изменяйте публичную историю
. Опечатка выглядит плохо, но хотите ли вы пройти через все неприятности, чтобы исправить её?
Если вам всё же придётся принудительно запушить общую публичную ветвь, первый и самый важный шаг — это общение. Все ваши коллеги, работающие над той же ветвью, должны знать, что при взаимодействии с общим репозиторием следует ожидать возникновения проблем. Они также должны знать, как исправить ситуацию. Для исправленных изменений вот решение, охватывающее большинство случаев использования:
git pull --rebase
Эта команда получит удалённую ветвь и перебазирует все локальные коммиты поверх неё. Изменённые коммиты будут разрешены автоматически (поэтому коммит с опечаткой будет пропущен).
Другим решением может быть отмена всех локальных изменений и сброс локальной ветви main
до origin/main
:
git reset --hard origin/main
Другие случаи использования могут быть более сложными для исправления, включая (интерактивный) ребазинг и cherry-pick. Всегда учитывайте компромиссы, прежде чем навязывать их.
А если вы работаете в одиночку на публичной ветви? В этом случае вы, как правило, можете принудительно пушить сколько угодно. Опять, общение играет ключевую роль. Я бы перефразировал приведённую выше рекомендацию по Git:
В целом, не следует изменять публичную историю на ветках, над которыми работают несколько человек.
Заключение
Контроль версий — недооценённый навык. Большинство инженеров-программистов используют его ежедневно, но многие не хотят тратить на его изучение больше, чем необходимо. Это нормально, но знание большего, чем commit
/push
/pull
, по крайней мере, сделает вас более эффективным. Это также поможет решать проблемы, с которыми вы (и ваши коллеги) можете столкнуться.
Надеюсь, эта статья объясняет поведение Git'а по умолчанию и побуждает вас опробовать некоторые расширенные возможности.
- Объяснение Git: Диапазоны коммитов
- Демистификация git cherry-pick: обзор команды с примерами
- Git: Руководство по исправлению ошибок (Часть 1)
- Git: Руководство по исправлению ошибок (Часть 2)
- Текущая ветвь/current branch в git
- Как HEAD работает в git
- Современные команды и возможности Git
- Поддерживайте чистоту ветви с помощью fixup и autosquash
- Шпаргалка по Git