git push: безопасная отправка и защита от перезаписи

Рейтинг: 62.1% · 15 голосов
Самый подробный курс по Git - от первого коммита до объектной базы и packfiles. Терминал и первый репозиторий, ветки и слияние, конфликты, удалённая работа и форджи (GitHub, GitLab), rebase и переписывание истории, спасение кода через reflog и fsck, stash и worktree, bisect и blame, подмодули и монорепо, хуки и подпись коммитов, внутреннее устройство и масштаб. Для новичков и профи. Актуально на 2026 (Git 2.54, дорожная карта 3.0).
Ответить
Аватара пользователя
Pavel_Git
Сообщения: 52
Зарегистрирован: 11 май 2026, 05:31

git push: безопасная отправка и защита от перезаписи

Сообщение Pavel_Git »

Оглавление курса (52)
  1. Что такое контроль версий и почему победил Git
  2. Установка Git на Windows, macOS и Linux и первая настройка
  3. Терминал и как спрашивать у Git: выживание в командной строке
  4. Ментальная модель Git: три состояния, коммит-снимок и граф
  5. Первый репозиторий: init, add, commit, status
  6. Просмотр изменений: status, diff и индексация по частям
  7. История проекта: git log, диапазоны и поиск
  8. .gitignore: что не должно попасть в репозиторий
  9. .gitattributes: окончания строк, бинарники и атрибуты
  10. Ветки как указатели: branch, switch и detached HEAD
  11. Слияние веток: fast-forward против трёхстороннего merge
  12. Конфликты слияния: анатомия и уверенное разрешение
  13. Практикум: твой первый день с Git от нуля до push
  14. Удалённые репозитории: remote, fetch, базовый pull и refspec
  15. git push: безопасная отправка и защита от перезаписи (вы здесь)
  16. Клонирование: shallow, partial clone, bundle и backfill
  17. GitHub, GitLab и форки: pull request, rulesets и защита веток
  18. Рабочие процессы команды: GitFlow, GitHub Flow, trunk-based
  19. Merge queue и merge trains: безопасное вливание в trunk
  20. Культура code review и Conventional Commits
  21. git reset: три дерева и отмена индексации и коммитов
  22. git revert: безопасная отмена в общей истории
  23. Правка последнего коммита: git commit --amend
  24. git rebase: пересадка истории и золотое правило
  25. Интерактивный rebase: переписать серию коммитов
  26. git cherry-pick: перенос отдельных коммитов
  27. Stacked diffs: цепочка зависимых PR
  28. Массовое переписывание истории: git filter-repo и git replace
  29. git reflog: журнал, который помнит всё
  30. Восстановление утерянных коммитов, веток и файлов
  31. Откат опасных операций: reset --hard, сломанный rebase, плохой merge
  32. git stash: тайник для незавершённой работы
  33. git worktree: несколько рабочих деревьев одного репозитория
  34. git bisect: бинарный поиск регрессии
  35. git blame, mailmap и археология кода
  36. Теги, релизы, archive и bundle
  37. Подмодули: внешние репозитории внутри проекта
  38. Subtree, монорепозитории и sparse-checkout
  39. Git LFS и большие файлы
  40. Вспомогательные команды: clean, mv, rm, restore
  41. Псевдонимы и кастомизация .gitconfig
  42. Хуки Git: автоматизация на клиенте и сервере
  43. Подпись коммитов и тегов: SSH, GPG и Sigstore
  44. Внутреннее устройство I: объектная база Git
  45. Внутреннее устройство II: ссылки, HEAD, notes и reftable
  46. Внутреннее устройство III: индекс, packfiles, gc и обслуживание
  47. От SHA-1 к SHA-256: переход на новый хеш
  48. Огромные репозитории: partial clone, FSMonitor, Scalar
  49. Git в CI/CD: shallow, кеш, merge_group и безопасность
  50. Куда движется Git: дорожная карта 3.0 и экосистема
  51. Профессиональный setup: собираем рабочее окружение Git
  52. 30+ типичных ошибок Git и как из них выбираться
Ты сделал коммиты, проверил их локально - и теперь хочешь отдать работу команде. За это отвечает одна команда: git push. На словах все просто: "отправить изменения git на сервер". На деле push - это место, где новички ломают чужую работу, теряют коммиты коллег и получают пугающее "rejected non-fast-forward". В этом уроке разберем, как push реально устроен, почему он иногда отказывается отправлять, и главное - как пушить с силой, не превращая историю команды в кладбище.

Запомни сразу: 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, ок
Конфликт начинается, когда чужой коммит уже лег на сервер между B и твоим C. Тогда твой C больше не потомок серверной верхушки, перемотка невозможна, и push отклоняется. Это и есть знаменитое non-fast-forward, к нему вернемся отдельно.

Сама команда в полной форме выглядит так:

Код: Выделить всё

git push <remote> <локальная-ветка>:<удаленная-ветка>
git push origin main:main
Слева от двоеточия - что отправляем (любой ref или коммит-иш), справа - куда кладем на сервере. В 99% случаев имена совпадают, и ты пишешь коротко: git push origin main. А часто и просто git push - но что именно тогда уйдет, решает настройка push.default, и про нее надо знать точно.

Изображение

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
Если не помнишь, что у тебя - оставь 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 не знает, куда отправлять новую ветку - связи с сервером еще нет. Лечится флагом -u (он же --set-upstream): он и пушит, и запоминает связь, чтобы дальше хватало голого git push и git pull.

Код: Выделить всё

git push -u origin feature/login
Branch 'feature/login' set up to track remote branch 'feature/login' from 'origin'.
Теперь важный момент, на котором путаются даже опытные. С Git 2.37 есть опция push.autoSetupRemote - включив ее, ты избавляешься от ручного -u: первый push сам делает --set-upstream. ВАЖНО и честно: даже в 2026 году в ванильном Git она НЕ включена по умолчанию. Никакого "теперь Git сам все настраивает" - это миф. Хочешь автоматику - включай руками:

Код: Выделить всё

git config --global push.autoSetupRemote true
После этого первый git push новой ветки сам поставит upstream без -u. До этого - либо -u, либо терпи fatal. Не верь статьям, где пишут, что это дефолт - проверь у себя командой git config push.autoSetupRemote (пусто = выключено).

Тег, удаление ветки и отправка конкретного коммита

Теги по умолчанию не уходят при обычном push - их надо толкать явно:

Код: Выделить всё

git push origin v2.4.0          # один тег
git push origin --tags          # все локальные теги
git push --follow-tags          # коммиты + аннотированные теги, относящиеся к ним
--follow-tags - самый аккуратный вариант для релизов: уйдут только те аннотированные теги, что висят на отправляемых коммитах, а не весь твой локальный зоопарк.

Удалить ветку на сервере. Логика старого синтаксиса такая: пуш "пустоты" в удаленную ветку стирает ее. Отсюда древняя форма git push origin :feature/login (ничего слева от двоеточия). Читается плохо, поэтому есть человеческий вариант:

Код: Выделить всё

git push origin --delete feature/login
 - [deleted]         feature/login
Запомни --delete, двоеточие-форму держи в голове только чтобы понимать чужие команды.

Отправить не всю ветку, а только часть истории - до конкретного коммита. Слева от двоеточия можно указать любой коммит-иш:

Код: Выделить всё

git push origin HEAD~2:main
Это отправит на origin/main состояние "две коммита назад от моего HEAD", а два самых свежих локальных коммита останутся у тебя. Редкий, но спасительный прием, когда верхушку еще надо доработать, а нижние коммиты уже готовы и нужны команде.

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.
Перевожу: пока ты работал, кто-то запушил в main. Серверная верхушка ушла вперед, твой коммит больше не ее прямой потомок, перемотка невозможна. Git защищает чужую работу и отказывается двигать ссылку, потому что это потеряло бы те самые чужие коммиты.

Правильное лечение - забрать чужое и встроить свое сверху:

Код: Выделить всё

git fetch origin
git rebase origin/main      # или git pull --rebase
# решаешь конфликты, если есть
git push origin main        # теперь твои коммиты сверху -> fast-forward
После rebase твои коммиты переехали поверх свежей серверной верхушки, история линейная, push проходит как обычная перемотка. Можно и git pull (merge) вместо rebase - тогда появится merge-коммит, что в некоторых командах нежелательно. Выбор merge против rebase - тема отдельного урока, но суть одна: на "rejected non-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
"stale info" значит: на сервере не то, что ты помнишь, кто-то поработал - иди разбирайся, fetch, смотри, что прилетело. Именно так force и должен отказывать, когда есть риск.

Грабли 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
Это золотой стандарт принудительной отправки после rebase своей ветки. Сделай себе алиас, чтобы пальцы не забывали:

Код: Выделить всё

git config --global alias.pushf "push --force-with-lease --force-if-includes"
git pushf origin feature/login
Правило на всю карьеру: никогда --force, всегда --force-with-lease --force-if-includes. И force только на СВОИХ ветках - на общих (main, develop, release) форсить запрещено в принципе.

Защищенные ветки и правила на форджах

Даже идеально вооружившись 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
    - проходит, потому что сервер на том коммите, что она помнит.
  • Подделай гонку: пусть Боб быстро запушит еще коммит, а Алиса снова

    Код: Выделить всё

    --force-with-lease
    - увидишь stale info и поймешь, как lease спас бы чужую работу.
После этой лабы "rejected" перестанет тебя пугать, а force станет осознанным инструментом, а не русской рулеткой.

Контрольные вопросы
  • Чем 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. А общие ветки запирай серверными правилами форджа, чтобы дисциплина подкреплялась железом.
👍7 ❤️4 🔥 😄 🤔1
Аватара пользователя
nutmeg
Сообщения: 1
Зарегистрирован: 13 май 2026, 05:20

Re: git push: безопасная отправка и защита от перезаписи

Сообщение nutmeg »

А я думал autoSetupRemote давно дефолт, везде про это пишут. Проверил git config push.autoSetupRemote - пусто. Спасибо, что развеяли, поставил true и -u больше не пишу.
👍1 ❤️ 🔥 😄 🤔
Аватара пользователя
Kistbant
Сообщения: 1
Зарегистрирован: 15 май 2026, 05:36

Re: git push: безопасная отправка и защита от перезаписи

Сообщение Kistbant »

До сих пор на rejected делал force и удивлялся, куда деваются коммиты тимлида. Теперь дошло. Сделал алиас pushf с force-with-lease и force-if-includes, как в уроке.
👍 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Удалённые репозитории: remote, fetch, базовый pull и refspec
Следующая глава →
Клонирование: shallow, partial clone, bundle и backfill

Все главы курса «Git профессионально: от первого коммита до внутреннего устройства»

Поделиться темой: ✈ Telegram VK
  • Похожие темы
Похожие запросы: git push и git pull: отправка и получение измененийgit remote: удалённые репозитории

Вернуться в «Git профессионально: от первого коммита до внутреннего устройства»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость