Урок курса
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 содержит «# Командный справочник», «Контакт: команда», «Статус: черновик» с исходными пустыми строками.
- Создайте оба 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.
Проверка сохранности.
- В каждом clone на чистой main выполнить fetch origin и merge origin/main. Даже если merge-коммит уже виден в graph --all, это не заменяет обновления main; в уже согласованном clone допустим Already up to date.
- Посмотреть log --graph --oneline --all обоих clone. Указать исходный коммит, два обычных коммита участников и объединяющий merge-коммит; сравнить фактические вершины именно main и актуальные origin/main. Заранее заданные ID не нужны.
- Открыть 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?
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
