Обычный push требует fast-forward: удалённая ветка должна быть предком твоей локальной. Это защита от случайной потери чужих коммитов.
Иногда защиту нужно обойти - например, после локального rebase, amend или interactive rebase (interactive-rebase) SHA коммитов изменились, и обычный push отказывается:
! [rejected] feat -> feat (non-fast-forward)
Здесь приходит force-push.
--force: тупой и опасный
git push --force origin feat
Что делает: говорит remote'у «забудь, что у тебя там было, поставь ровно мою историю». Если кто-то другой запушил коммит между твоим последним fetch и push'ем - его коммит исчезает. Не сразу заметишь: его клон ещё содержит этот коммит, но в общем remote'е его уже нет.
Это и есть «forge force-push накосячил» - типичная катастрофа в командной работе.
--force-with-lease: безопасный
git push --force-with-lease origin feat
Что делает: говорит remote'у «перезапиши ветку, но только если её текущий SHA = тому, что я последний раз видел через fetch». Если SHA отличается - значит кто-то запушил после тебя, и force отменяется.
! [rejected] feat -> feat (stale info)
В этом случае: сделай git fetch, посмотри, что прилетело, реши
что с этим делать (обычно rebase своих коммитов поверх новых), и
повтори force-with-lease.
Правило: никогда --force, всегда --force-with-lease. Можно
настроить алиас, чтобы пальцы автоматически шли в правильную
сторону:
git config --global alias.pushf 'push --force-with-lease'
Или ещё параноидальнее: с явной проверкой SHA, который ты ожидаешь:
git push --force-with-lease=feat:abc123 origin feat
Когда force нужен
- После rebase своей feature-ветки. Это штатный сценарий.
- После amend последнего коммита запушенной feature-ветки.
- После interactive rebase для очистки истории перед PR.
Все три - только на ветках, которые видишь ты один. На main,
master, develop force-push запрещён branch-protection
правилами форджа.
Когда force НЕ нужен
- «Запушил с ошибкой в commit message» - обычно проще оставить, или
исправить новым коммитом. Force не за этим.
- «Хочу удалить из истории секрет» - недостаточно. См. git-filter-repo, плюс ротируй секрет в любом случае.
- «Не разобрался с merge» - нет, делай merge или rebase, не force.
Branch protection: сетка снизу
На GitHub/GitLab у защищённых веток (main, master, release/*) можно включить:
- Запрет force-push.
- Запрет прямого push (только через PR).
- Требование зелёного CI и аппрувов.
Это страховка от человеческой ошибки. Включай её на main всегда, даже в маленьких командах.
Подводные камни
- Фоновый fetch может обмануть
--force-with-lease. Lease без явного SHA сравнивается с твоим remote-tracking ref'ом (refs/remotes/origin/feat). Обычно «давно не fetch'ил» - это хорошо: lease видит старый SHA, не совпадает с реальным remote, push отклоняется соstale info. Опасный сценарий - когда что-то делает fetch без тебя (IDE, watcher, параллельный терминал): remote-tracking ref молча обновляется до свежего чужого SHA, lease совпадает, force проходит и затирает их коммиты. Защита - использовать явный SHA:git push --force-with-lease=feat:<sha>, где<sha>- то, что ты сам видел черезgit fetchи осознанно решил перезаписать. - На некоторых форджах reflog хранится 30+ дней. Если кого-то случайно перезаписали - обычно SHA можно восстановить из reflog форджа или из локального клона коллеги.
- Force-push ломает чужие watch'и. Если кто-то ребейзится на твою ветку (stacked PRs), force-push заставит его тоже ребейзиться. В stacked-workflow это норма, но предупреждай.