Урок курса

Push отклонён: коллега успел раньше

Git и GitHub с нуля: бесплатный курс с практикой

В прошлом уроке мы разобрали, что происходит, когда history уже разошлась: --ff-only отказывает, и нужно явно выполнить fetch + merge origin/main. Теперь ситуация становится реальной командной: два человека работают параллельно в двух клонах одного remote, и один из них неизбежно получит отказ push — потому что коллега успел раньше.

Non-fast-forward отказ: механика и диагностика

Для обычного продвижения существующей remote-ветки её текущая вершина должна быть предком отправляемого коммита, не обязательно непосредственным родителем. При совпадении вершин обновлять нечего; создание новой ветки — отдельный случай. Тогда remote-указатель можно просто сдвинуть вперёд по прямой линии — это и называется fast-forward.

Когда коллега отправил коммит в main общего remote origin, а вы создали свой коммит от того же предка, история расходится. Фактическая main в общем remote уже на коммите коллеги; ваша локальная main от него не происходит. До fetch ваша локальная remote-tracking запись origin/main ещё может указывать на старого общего предка. Обычная отправка не может продвинуть общую main до вашего коммита, сохранив в ней коммит коллеги как предка.

Git это запрещает и возвращает отказ. Фактическое сообщение зависит от версии Git и от того, знает ли уже ваш clone о чужом коммите:

  • ! [rejected] main -> main (fetch first) — Git видит, что remote ушёл вперёд, но у вас нет его объектов локально.
  • ! [rejected] main -> main (non-fast-forward) — похожая ситуация, иная формулировка.

Оба варианта означают одно и то же: push не выполнен, ни локальная main, ни origin/main не сдвинулись. Это не две разные проблемы с разными решениями — это одна проблема с одним решением.

Принудительная отправка (--force) технически сняла бы отказ, но просто переместила бы main общего remote на ваш коммит и исключила коммит коллеги из истории этой ветки. Локальные копии коллеги и другие ссылки могут ещё сохранять этот коммит. В командной работе это катастрофа: чужая работа исчезает из общей ветки. Поэтому --force в этом уроке не используется.

Подготовка отказа в двух новых клонах. Распакуйте весь git_practice.zip в новую пустую папку и откройте терминал в её git_practice (либо cd "полный путь к новой папке/git_practice" со своим путём). remote/shared.git имеет main, один коммит A и исходный README с контактом «команда» и статусом «черновик», разделёнными пустой строкой. Папки клонов ещё не существуют. Создайте оба до первой отправки:

git clone remote/shared.git clone_alice
git clone remote/shared.git clone_bob
cd clone_alice
git config user.name "Алиса"
git config user.email "alice@example.invalid"
git status
git log --oneline main
git log --oneline origin/main
cd ../clone_bob
git config user.name "Боб"
git config user.email "bob@example.invalid"
git status
git log --oneline main
git log --oneline origin/main
cd ../clone_alice

Оба клона на чистой main, upstream origin/main установлен clone. Запишите общий ID A вне клонов. В clone_alice замените только Контакт: команда на Контакт: alice@example.com, сохраняя остальные строки и их окончания:

git add README.md
git commit -m "Обновить контакт"
git push
git log --oneline
cd ../clone_bob

Запишите ID коммита контакта. В clone_bob независимо замените только Статус: черновик на Статус: в работе, также сохраняя остальной текст и окончания строк:

git add README.md
git commit -m "Обновить статус"
git log --oneline
git status
git push

Запишите ID коммита статуса и фактический отказ. До этой попытки Bob не выполняет fetch или pull. После коммита рабочая папка чистая. Alice пока больше не отправляет коммитов. Все последующие команды восстановления выполняйте в clone_bob.

Граф расходящейся истории: чтение развилки двух участников

После того как push отклонён, первый шаг — получить коммит коллеги командой git fetch origin. Только после этого граф покажет полную картину расхождения.

git fetch origin
git log --graph --oneline --all

В схеме ниже ID условные: f3a1b2c всегда обозначает коммит статуса Bob, e9d4c5a — коммит контакта Alice. В реальном выводе сопоставьте их с записями своих ID:

* f3a1b2c (HEAD -> main) Обновить статус
| * e9d4c5a (origin/main) Обновить контакт
|/
* a1b2c3d Начальный коммит

Читать этот граф нужно снизу вверх. Нижняя точка * a1b2c3d — общий предок: коммит, от которого оба клона начали работать. От него расходятся две линии. Левая ветвь (* f3a1b2c) — ваш локальный коммит, которого нет в remote. Правая ветвь (* e9d4c5a) — коммит коллеги, который уже в origin/main, но не в вашей локальной main.

Символы | и / — это буквальное изображение развилки: две независимые линии сходятся к общему предку. Именно это состояние делает fast-forward невозможным в обоих направлениях: ваш коммит не является потомком коммита коллеги, и коммит коллеги не является потомком вашего. Ни один из них нельзя получить, просто сдвинув указатель от другого.

Если выполнить git fetch origin до попытки push, граф появится раньше отказа. Но в типичном сценарии отказ приходит первым — и тогда fetch нужен именно для того, чтобы увидеть расхождение в явном виде, прежде чем его устранять.

Fetch, merge origin/main и повторный push: процедура восстановления

После отказа push последовательность действий фиксированная: fetch → merge → push. Каждый шаг выполняет строго свою задачу.

Шаг 1: fetch

git fetch origin

fetch загружает коммит коллеги и обновляет указатель origin/main. Ваша локальная main и рабочий файл README при этом не меняются — вы можете убедиться в этом, открыв файл или выполнив git status.

Шаг 2: merge

git merge -m "Объединить правки" origin/main

В основном опыте строки контакта и статуса разделены пустой строкой и изменены независимо. Git объединяет их автоматически и создаёт merge-коммит с двумя родителями: коммитом статуса Bob и коммитом контакта Alice. Проверьте git status и README: обе правки должны сохраниться.

Автоматическое объединение. Если ваши правки и правки коллеги затронули разные части файла — например, один изменил строку контакта, другой строку статуса — Git объединяет их сам и сразу создаёт merge-коммит. Вы увидите что-то вроде Merge made by the 'ort' strategy.

Отдельный опыт конфликта. Для него распакуйте архив заново в другую пустую папку. В её git_practice повторите подготовку обоих клонов выше с обеими настройками автора. Теперь Alice вместо контакта заменяет исходный статус на Статус: проверен, выполняет add, git commit -m "Проверить статус" и push. Bob независимо заменяет тот же исходный статус на Статус: в работе, выполняет add, git commit -m "Обновить статус" и получает отказ push до fetch. В этом отдельном clone_bob выполните git fetch origin и git merge -m "Объединить правки" origin/main.

Обе стороны изменили одну исходную строку на разные новые версии; status показывает незавершённое слияние, файл содержит:

<<<<<<< HEAD
Статус: в работе
=======
Статус: проверен
>>>>>>> origin/main

В отдельном опыте соберите итоговую строку Статус: проверен, в работе, удалите все маркеры <<<<<<<, =======, >>>>>>> — и завершить слияние:

git add README.md
git commit -m "Объединить правки"

Пока конфликт не разрешён и слияние не завершено, git status покажет You have unmerged paths. Результат объединения ещё не создан. Push отправляет существующие коммиты выбранной ветки и сам по себе не проверяет unmerged index; в этом опыте прежняя main всё ещё не включает опубликованный коммит коллеги, поэтому повторная отправка не устраняет non-fast-forward. Сначала завершите разрешение, add и commit, затем отправляйте результат.

Шаг 3: push

git push

В основном опыте Bob отправляет автоматически созданный merge-коммит; в отдельном конфликтном — коммит после ручного разрешения. Локальная main содержит оба исходных коммита как предков. main общего remote можно продвинуть fast-forward до результата: условие выполнено, push проходит. Коммит коллеги при этом остаётся в истории — он один из родителей merge-коммита.

Согласованная конечная история: что и как проверять в обоих клонах

Продолжайте здесь основной опыт с контактом Alice и статусом Bob. После успешного push Bob общая main remote и origin/main в клоне Bob указывают на merge-коммит. Но второй клон — тот, что отправил первым — об этом ещё не знает. Его локальная main стоит на своём исходном коммите, рабочий файл содержит только одну из двух правок.

Важное разграничение: видимость merge-коммита в графе и фактическое обновление рабочего файла — это два разных события.

В основном clone_bob перейдите командой cd ../clone_alice. На чистой main Алисы после одного git fetch origin граф покажет merge-коммит через origin/main:

git fetch origin
git log --graph --oneline --all
*   d7e8f9a (origin/main) Объединить правки
|\  
| * e9d4c5a (HEAD -> main) Обновить контакт
* | f3a1b2c Обновить статус
|/  
* a1b2c3d Начальный коммит

HEAD -> main стоит на коммите Алисы, origin/main ушёл вперёд к merge-коммиту. README в рабочей папке по-прежнему содержит только правку контакта — строка статуса ещё «черновик». git status сообщит, что локальная ветка отстаёт от origin/main.

Чтобы рабочий файл обновился, нужен явный merge:

git merge origin/main

Поскольку origin/main является прямым потомком текущей main Алисы, Git выполнит fast-forward: указатель main просто сдвинется до merge-коммита, никакого нового коммита не создаётся. После этого README будет содержать обе правки одновременно.

После обновления Alice перейдите в Bob (cd ../clone_bob) и выполните git fetch origin, git merge origin/main, git status. Здесь ожидается Already up to date. В обоих клонах проверьте итоговое состояние; ID схемы условные, коммит контакта и коммит статуса сохраняют свои фактические ID во всех клонах:

  • git log --graph --oneline --all в каждом клоне: указатели HEAD -> main и origin/main указывают на один и тот же merge-коммит; оба исходных коммита участников видны как его родители.
  • README открыт в редакторе: строка Контакт: alice@example.com и строка Статус: в работе присутствуют одновременно.
  • git status: nothing to commit, working tree clean.

Если в уже синхронизированном клоне выполнить merge origin/main повторно, Git ответит Already up to date — merge-коммит уже достижим из текущей main, делать нечего. Это нормальный результат, а не ошибка.

Задача 1

Два участника создали разные коммиты от А. Коллега первым отправил К, а местная main содержит только свой Л поверх А. Новых отправок коллеги после К нет. Почему обычный push Л не принят?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 2

Теоретический случай; выполнять принудительную отправку не нужно. От общего коммита А независимо созданы К коллеги и Л автора; ни один не является предком другого. Общая main указывает на К, местная main автора — на Л, а main в clone коллеги — на К. Допустим, сервер разрешил принудительную смену общей main с К на Л; merge не выполняли, других ссылок и клонов не изменяли. Что после этого верно о К?

Выберите один вариант
Без регистрации · результат не сохраняется

Задача 3

Самостоятельная практика:
Настоящая локальная практика Git на своём компьютере. Площадка не запускает команды и не проверяет ваш репозиторий.
Входные данные. Распакуйте выданный git_practice.zip в новую пустую папку и перейдите в git_practice. remote/shared.git: main, один исходный коммит; README содержит «# Командный справочник», «Контакт: команда», «Статус: черновик» с исходными пустыми строками.

  1. Создайте оба clone до первой отправки и настройте оба поля автора:
git clone remote/shared.git clone_alice
git clone remote/shared.git clone_bob
cd clone_alice
git config user.name "Алиса"
git config user.email "alice@example.invalid"
cd ../clone_bob
git config user.name "Боб"
git config user.email "bob@example.invalid"
cd ..

Оба clone на чистой main с upstream origin/main.
2. В clone_alice замените только строку контакта на «Контакт: alice@example.com», выполните add README.md, commit -m "Обновить контакт", push.
3. В clone_bob независимо замените только строку статуса на «Статус: в работе», выполните add README.md, commit -m "Обновить статус", push. До попытки отправки чужой коммит в clone_bob не получать.
4. Зафиксируйте собственный фактический отказ: подпись может быть fetch first либо non-fast-forward. Сравните локальную main и remote через актуальный граф после fetch; отказ не продвинул ни локальную, ни общую ветку.
5. Без force получите историю в clone_bob, объясните развилку, объедините её и отправьте. Контакт и статус в выданном README находятся в непересекающихся участках. Для автоматического merge используйте изученный флаг -m; при возникшем конфликте закончите ручное разрешение с сохранением обеих правок, add и commit -m до повторной отправки. Alice больше не изменяет remote.
6. В чистой main clone_alice выполните fetch origin и просмотрите graph --all: merge-коммит виден через origin/main, но main и README пока прежние. Затем явно выполните merge origin/main, обновляющий main и файл.
7. В обоих clone проверьте согласованные локальные main и актуальные origin/main, оба исходных коммита и merge-коммит, одновременно обновлённый контакт и статус README, чистый status. Представьте реальный отказ, граф до/после объединения, промежуточное наблюдение Alice после одного fetch и итоговые файлы/status. Критерий не требует заранее заданных SHA или дат.
Доступные средства: редактор, git clone, git config user.name/user.email, git add, git commit -m, git push, git fetch origin, git log --oneline, git log --graph --oneline --all, git merge -m, git merge origin/main, git status. Все команды уже изучены.

Самостоятельная практика:
Настоящая локальная практика Git на своём компьютере. Площадка не запускает команды и не проверяет репозиторий.
Входные данные. Используйте два clone после завершения предыдущей task_140: оба на main, объединённый коммит опубликован; новых правок/отправок нет. Для нового опыта сначала выполните task_140 целиком в новой распаковке архива, включая оба поля автора в каждом clone.
Проверка сохранности.

  1. В каждом clone на чистой main выполнить fetch origin и merge origin/main. Даже если merge-коммит уже виден в graph --all, это не заменяет обновления main; в уже согласованном clone допустим Already up to date.
  2. Посмотреть log --graph --oneline --all обоих clone. Указать исходный коммит, два обычных коммита участников и объединяющий merge-коммит; сравнить фактические вершины именно main и актуальные origin/main. Заранее заданные ID не нужны.
  3. Открыть README в обоих clone: контакт alice@example.com и статус «в работе» присутствуют одновременно. Проверить чистые status, diff и diff --staged. Объяснить, почему одна видимость merge через origin/main после fetch ещё не доказывала бы правильный рабочий файл.
    Доступные средства: редактор, git fetch origin, git merge origin/main, git log --oneline, git log --graph --oneline --all, git status, git diff, git diff --staged.

Письменное объяснение:
Коллега говорит: «Мой push отклонён с ошибкой non-fast-forward. Я знаю, что git push --force заставит его пройти — это же просто принудительная отправка. Что плохого в том, чтобы использовать --force?»

Объясните: что именно произойдёт с историей remote-репозитория при --force, почему это опасно для коллеги, и какой правильный порядок действий нужно использовать вместо этого.

Платформа проверяет только ответ ниже; действия в своём репозитории и письменные ответы вы проверяете самостоятельно.

Контрольный случай:
В другом проекте после общего коммита автор изменил описание README в своей main, а коллега независимо изменил контакт и отправил свой коммит в origin. Из чистой локальной main автор выполнил только fetch origin и log --graph --oneline --all. Граф показывает два разных коммита над общим родителем: один помечен main, другой — origin/main. Status чистый. Полна ли синхронизация этого clone?

Выберите один вариант
Без регистрации · результат не сохраняется

Продолжить с сохранением прогресса

Перейдите к уроку в учебном кабинете.

Перейти к интерактивному уроку