Безопасный force-push
После rebase или amend обычный push отклоняется как non-fast-forward. git push --force перезаписывает удалёнку и может стереть коммиты коллег. git push --force-with-lease безопаснее: отказывает, если удалёнка сдвинулась с твоего fetch. Не форси общие ветки вроде main.
Ты причёсываешь свою feature-ветку интерактивным rebase, пытаешься запушить, и git отклоняет: (non-fast-forward). Конечно — ты переписал коммиты, поэтому ветка больше не продолжает ту, что на сервере. Соблазнительный фикс — git push --force. Он работает, но --force — это та единственная команда, которая чаще всего тихо удаляет работу коллеги. Есть более безопасный двойник, --force-with-lease, и понимание разницы отделяет аккуратного инженера от того, кто отправляет сообщение «извините, я перезаписал main».
К концу этого урока ты сможешь пушить переписанную историю, не уничтожая чужую, тянясь к --force-with-lease и зная, какие ветки нельзя форсить вообще.
После этого урока ты сможешь объяснить, почему переписанная история требует force push, использовать git push --force-with-lease как безопасное умолчание, описать, чем он отличается от --force, и сформулировать правило о том, какие ветки можно форсить.
Переписывание истории разводит твою ветку с удалёнкой, поэтому обычный push отклоняется. Команды вроде git rebase, git commit --amend и git reset не правят коммиты на месте — они создают новые коммиты с новыми хешами и перемещают на них твою ветку. Старые коммиты, на которые удалёнка всё ещё указывает, теперь исчезли с линии твоей ветки:
git commit --amend -m "Better message" # новый хеш; старый коммит заменён
git push
# ! [rejected] feature -> feature (non-fast-forward)У feature на удалёнке всё ещё есть исходный коммит, которого твоя переписанная ветка больше не содержит, — поэтому push перестаёт быть fast-forward.
git push --force перезаписывает ветку удалёнки безусловно — и в этом опасность. Force говорит git: «сделай так, чтобы ветка удалёнки указывала ровно туда, куда моя, что бы там ни было». Он не проверяет, что сейчас на сервере:
git push --force origin featureЕсли между твоим последним fetch и этим push коллега запушил коммит, который ты не видел, --force его выбрасывает. Коммит не сливается, о нём не предупреждают — его просто заменяют. На ветке, которой пользуешься только ты, это нормально. На общей — это потеря данных.
git push --force-with-lease отказывает, если удалёнка сдвинулась с твоего последнего fetch. Это безопасная версия. Git помнит, куда, по его мнению, указывала ветка удалёнки (твоя origin/feature). Push проходит только если сервер всё ещё совпадает с этим — то есть никто не запушил в промежутке:
git push --force-with-lease origin feature
# Проходит, только если origin/feature там, где ты видел его в последний раз.
# Иначе: ! [rejected] (stale info) — и твой push отклонён.«Lease» (аренда) — как заявка: «Я считаю, что удалёнка на X; дай мне перезаписать, только если это всё ещё так». Если кто-то запушил, твоё представление устарело, аренда нарушена, и git останавливает тебя прежде, чем ты затрёшь их коммит.
▸Почему это работает
Почему --force-with-lease реально защищает, когда --force нет? --force ничего не сравнивает — он просто навязывает твою вершину. --force-with-lease сравнивает текущую позицию удалёнки с твоей удалённо-отслеживающей ссылкой (origin/feature) и прерывается при несовпадении. Тонкая ловушка: git fetch (или авто-fetch твоей IDE) молча обновляет origin/feature до нового коммита коллеги, что освежает твою аренду и позволяет более позднему force-with-lease всё равно пройти. Поэтому правило: никогда не делай вслепую fetch-а-потом-force-with-lease; сначала просмотри, что изменилось. Даже безопасному инструменту нужно, чтобы ты смотрел.
Никогда не форси общую ветку вроде main; только свои feature- или PR-ветки. Граница в том, строят ли другие поверх ветки:
- Можно форсить: твоя личная
feature/xили PR-ветка, где ты единственный автор и rebase/amend ожидаемы перед слиянием. - Нельзя форсить никогда:
main,develop, релизные ветки — всё, откуда коллеги пуллят и на чём базируют работу. Переписывание их переписывает историю из-под всех, ломая их клоны.
Многие команды форсят это через защищённые ветки (protected branches) на GitHub/GitLab, которые отклоняют любой force push в main напрямую, превращая социальное правило в жёсткое ограждение.
Один и тот же force push, с арендой и без.
Ты сделал rebase своей feature-ветки, чтобы выкинуть лишний коммит; локально она теперь заканчивается на R. Твоя origin/feature всё ещё показывает старую вершину O с твоего последнего fetch. Без твоего ведома коллега только что запушил правку из ревью T поверх O, так что feature сервера на самом деле на T.
git push --forceигнорирует состояние сервера и ставитfeatureнаR. Коммит коллегиTтеперь недостижим — исчез с ветки, без предупреждения. Они запушили работу, и она пропала.git push --force-with-leaseсравнивает сервер (T) с твоей запомненнойorigin/feature(O). Они различаются, поэтому аренда устарела, и git прерывается: «stale info, refusing to update». Ты затем делаешьgit fetch, видишьT, ребейзишь свою работу поверх него и пушишь — сохраняя и свою чистку, и их правку.
Тот же замысел, та же переписанная ветка. Аренда — единственное, что превратило тихое удаление в безопасное «остановись и посмотри».
▸Частая ошибка
Классическая ошибка — считать --force-with-lease магией, которая никогда не теряет данные, и затем по привычке запускать git fetch прямо перед ним. Fetch продвигает origin/feature до коммита коллеги, поэтому аренда теперь совпадает с сервером, и force проходит — перезаписывая ровно тот коммит, который аренда должна была защитить. Дисциплина: когда force-with-lease отклонён, остановись и осмотрись, не «чини» это через fetch и повтор. Отклонение — это работающая защита.
Ты сделал rebase своей feature-ветки, и push отклонён как non-fast-forward. Коллега мог запушить в неё с твоего последнего fetch. Какая команда — безопасный способ запушить твою переписанную историю?
Переписывание истории через rebase, amend или reset создаёт новые хеши коммитов, поэтому твоя ветка больше не fast-forward-ит удалёнку, и обычный push отклоняется как non-fast-forward. git push --force перезаписывает удалёнку безусловно и может тихо стереть коммиты коллеги. git push --force-with-lease — безопасное умолчание: он перезаписывает, только если удалёнка всё ещё совпадает с твоей последней полученной origin/<ветка>, прерываясь, когда кто-то другой запушил, — но слепой git fetch прямо перед ним освежает аренду и убирает эту защиту. Форси только свои feature/PR-ветки; никогда main или другие общие ветки, которые правила защищённых веток должны блокировать напрямую. Дальше ты поднимешь эту коллаборацию на уровень платформы — pull request.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.