Запомни сразу: push - единственная команда из всего курса, которой ты можешь навредить НЕ ТОЛЬКО себе. Локально ты в песочнице: reflog, checkout, reset - все откатывается. А вот неудачный git push --force затирает ветку на сервере, и если ты снес чужие коммиты, которых у тебя нет, восстанавливать их придется из чужих машин. Поэтому относись к push серьезнее, чем к любой локальной операции.
Что вообще делает git push под капотом
push - это переговоры двух репозиториев. Твой Git берет коммит, на который указывает локальная ветка, и говорит удаленному: "поставь свою ветку refs/heads/main на этот объект, а недостающие коммиты, деревья и blob-ы я тебе сейчас докину". Сервер смотрит: а можно ли передвинуть его ссылку на новый коммит, не потеряв ничего? Если новый коммит является прямым потомком старого (между ними только добавление сверху) - это fast-forward, перемотка вперед, сервер просто двигает указатель. Безопасно.
Код: Выделить всё
до push (сервер): A --- B (origin/main)
твоя ветка: A --- B --- C (main)
push: подвинь origin/main с B на C -> fast-forward, ок
Сама команда в полной форме выглядит так:
Код: Выделить всё
git push <remote> <локальная-ветка>:<удаленная-ветка>
git push origin main:main

push.default и почему голый git push ведет себя по-разному
Когда ты не указываешь ветку, поведение определяет push.default. Значений несколько, разберу рабочие:
- simple - дефолт с Git 2.0 и по сей день. Пушит текущую ветку в ветку с тем же именем на upstream, но только если upstream называется так же. Это защита от выстрела в ногу: если ты отслеживаешь чужую ветку под другим именем, голый push откажется и заставит уточнить. Для 95% людей это правильный выбор.
- current - пушит текущую ветку в одноименную на сервере, не требуя совпадения upstream. Удобно, когда имена всегда зеркальны и ты не хочешь думать.
- upstream - пушит в ту ветку, которую отслеживаешь (даже если имя другое). Опасновато: легко отправить feature-x в main, если когда-то так настроил трекинг.
- nothing - заставляет всегда указывать ветку явно. Параноидальный режим для тех, кто обжегся.
Код: Выделить всё
git config push.default
simple
git config --global push.default simple
Первый push новой ветки: -u и ловушка autoSetupRemote
Создал ветку локально, коммитишь, делаешь git push - и получаешь:
Код: Выделить всё
fatal: The current branch feature/login has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin feature/login
Код: Выделить всё
git push -u origin feature/login
Branch 'feature/login' set up to track remote branch 'feature/login' from 'origin'.
Код: Выделить всё
git config --global push.autoSetupRemote true
Тег, удаление ветки и отправка конкретного коммита
Теги по умолчанию не уходят при обычном push - их надо толкать явно:
Код: Выделить всё
git push origin v2.4.0 # один тег
git push origin --tags # все локальные теги
git push --follow-tags # коммиты + аннотированные теги, относящиеся к ним
Удалить ветку на сервере. Логика старого синтаксиса такая: пуш "пустоты" в удаленную ветку стирает ее. Отсюда древняя форма git push origin :feature/login (ничего слева от двоеточия). Читается плохо, поэтому есть человеческий вариант:
Код: Выделить всё
git push origin --delete feature/login
- [deleted] feature/login
Отправить не всю ветку, а только часть истории - до конкретного коммита. Слева от двоеточия можно указать любой коммит-иш:
Код: Выделить всё
git push origin HEAD~2:main
git push rejected: разбираем non-fast-forward
Самая частая ошибка git push в командной работе:
Код: Выделить всё
$ git push origin main
To github.com:team/app.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'github.com:team/app.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
Правильное лечение - забрать чужое и встроить свое сверху:
Код: Выделить всё
git fetch origin
git rebase origin/main # или git pull --rebase
# решаешь конфликты, если есть
git push origin main # теперь твои коммиты сверху -> fast-forward
Категорически нельзя на этой ошибке делать git push --force вслепую. force скажет серверу "поставь main на мой коммит, плевать на твой" - и чужие коммиты, которых у тебя нет, исчезнут. Это классический способ стереть работу коллеги одной командой.
git push force и как НЕ затереть чужое
Иногда force нужен по-настоящему: ты сделал rebase своей feature-ветки, переписал хеши, и серверная версия теперь "расходится" с локальной не потому, что там чужое, а потому что ты сам переписал свою историю. Обычный push такое отклонит - тут force законен. Вопрос только в том, КАКОЙ force.
git push --force (он же -f) - грубая сила. Сервер ставит ветку на твой коммит без всяких проверок. Если между твоим последним fetch и пушем кто-то добавил коммит - он умрет молча. Этот флаг почти никогда не нужен; держи его как ругательство.
git push --force-with-lease - вот цивилизованный force. "Lease" - это аренда: Git помнит, на каком коммите была удаленная ветка при твоем последнем fetch, и соглашается перезаписать, ТОЛЬКО если сервер все еще на том же коммите. Появился чужой коммит - сервер ушел вперед - аренда просрочена - push отклонен. Ты сначала затрешь свою же память о состоянии, а не чужую работу.
Код: Выделить всё
$ git push --force-with-lease origin feature/login
To github.com:team/app.git
! [rejected] feature/login -> feature/login (stale info)
error: failed to push some refs
Грабли force-with-lease. Опасность в том, что некоторые команды (background fetch, git pull, иные IDE) могут незаметно обновить твою локальную ссылку remote-tracking - и тогда "аренда" будет сверяться с уже свежим, чужим состоянием, считая его твоим. Защита снимается. Поэтому существует еще более строгий уровень.
git push --force-if-includes - усиление к lease. Он требует, чтобы коммиты, которые ты переписываешь, реально были у тебя в работе - то есть твоя локальная ветка действительно ВКЛЮЧАЕТ ту удаленную верхушку, что лежит на сервере (как было бы после нормального pull/rebase). Если remote-tracking обновился втихую фоновым fetch-ем, а ты эти изменения в работу не взял, --force-if-includes это распознает и откажет. Применяют вместе:
Код: Выделить всё
git push --force-with-lease --force-if-includes origin feature/login
Код: Выделить всё
git config --global alias.pushf "push --force-with-lease --force-if-includes"
git pushf origin feature/login
Защищенные ветки и правила на форджах
Даже идеально вооружившись lease, ты не должен полагаться только на дисциплину. GitHub, GitLab и подобные форджи (об их устройстве - отдельный урок) дают защищенные ветки и правила: запрет force-push в main, запрет прямого push в обход pull request, обязательное прохождение CI и ревью. На GitHub в 2026 это repository rulesets (новый механизм вместо старой classic branch protection), у GitLab - protected branches и merge trains. Включив их, ты физически закроешь возможность снести main даже случайной командой - сервер отвергнет push с понятным сообщением о нарушении правила. Локальная аккуратность плюс серверные правила - это и есть взрослая защита от перезаписи.
Мини-лаба: проживи отказ и безопасный force руками
Сделай два репозитория и воспроизведи non-fast-forward и lease, не рискуя ничем.
- Создай "сервер":
Код: Выделить всё
git init --bare /tmp/srv.git - Клонируй как Алиса:
Код: Выделить всё
git clone /tmp/srv.git /tmp/alice && cd /tmp/alice - Сделай коммит и запушь с -u:
Код: Выделить всё
echo a > f.txt && git add f.txt && git commit -m init && git push -u origin main - Клонируй как Боб:
Код: Выделить всё
git clone /tmp/srv.git /tmp/bob - Боб коммитит и пушит:
Код: Выделить всё
cd /tmp/bob && echo b >> f.txt && git commit -am bob && git push - Алиса (не делая fetch) коммитит и пробует push - получаешь rejected non-fast-forward. Прочитай hint целиком.
- Алиса лечит правильно: , решает конфликт,
Код: Выделить всё
git fetch origin && git rebase origin/main- проходит.Код: Выделить всё
git push - Теперь Алиса делает (переписала хеш своей верхушки) и пробует
Код: Выделить всё
git commit --amend -m fixed- проходит, потому что сервер на том коммите, что она помнит.Код: Выделить всё
git push --force-with-lease --force-if-includes - Подделай гонку: пусть Боб быстро запушит еще коммит, а Алиса снова - увидишь stale info и поймешь, как lease спас бы чужую работу.
Код: Выделить всё
--force-with-lease
Контрольные вопросы
- Чем git push --force-with-lease отличается от git push --force и в каком случае lease все равно может пропустить опасную перезапись (и что это исправляет)?
- Ты получил "rejected non-fast-forward". Почему это произошло на уровне коммитов и какие команды решают проблему, не теряя чужого?
- push.autoSetupRemote включена по умолчанию в Git 2026 года? Что произойдет при первом push новой ветки, если она выключена, и как это лечить?
- Как удалить ветку на сервере двумя способами и чем git push origin :name отличается от git push origin --delete name по читаемости?
push двигает серверную ссылку на твой коммит и докидывает недостающие объекты; безопасно это происходит только как fast-forward. "rejected non-fast-forward" - не баг, а защита чужой работы: лечится fetch плюс rebase, а не слепым force. Когда force реально нужен (после rebase своей ветки), бери только --force-with-lease вместе с --force-if-includes, никогда голый --force, и только на личных ветках. push.autoSetupRemote удобна, но в 2026 не дефолт - включай руками или ставь -u. А общие ветки запирай серверными правилами форджа, чтобы дисциплина подкреплялась железом.