Урок курса

Синхронизация без сюрпризов pull

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

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

git pull: fetch плюс объединение — и почему без флага это опасно

git pull — не отдельная операция, а сочетание двух уже знакомых действий: сначала git fetch origin получает новые коммиты и обновляет remote-tracking ветку origin/main, затем Git объединяет origin/main с текущей локальной веткой.

Проблема в слове «объединяет». Как именно — зависит от флагов команды и настроек Git, в том числе pull.rebase. Они могут быть заданы глобально или для конкретного репозитория:

  • Если pull.rebase=false — Git делает merge.
  • Если pull.rebase=true — Git делает rebase.
  • При расходящейся истории и отсутствии настроек или флагов, задающих способ объединения, современный Git может потребовать явного выбора. Само отсутствие pull.rebase не означает отказа: при fast-forward или уже согласованной истории выбирать между merge и rebase не требуется.

На вашей машине настройка может быть одна, на машине коллеги — другая. Один и тот же git pull на одной и той же истории даёт разный результат в зависимости от того, кто его запускает. Это и есть источник неожиданностей.

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

git pull --ff-only: fast-forward без merge-коммита

Флаг --ff-only разрешает объединение только одним способом — fast-forward. Это значит, что локальная main просто «догоняет» origin/main по уже существующей линейной истории: никакого нового коммита слияния не создаётся, HEAD перемещается вперёд.

Условие продвижения main к более новой origin/main: у локальной main нет коммитов, которых нет в origin/main. Иначе говоря, локальная ветка является прямым предком удалённой. В этом случае Git продвигает указатель main, обновляет индекс и рабочие файлы.

Для отдельного опыта распакуйте весь git_practice.zip в новую пустую папку. Откройте терминал в её git_practice или перейдите туда через cd "полный путь к новой папке/git_practice", подставив свой путь. Выданный remote/shared.git содержит чистую main с одним исходным коммитом A и README: заголовок «# Командный справочник», контакт «команда», статус «черновик», между ними пустые строки. Папки clone1 и clone2 ещё не существуют; git init не нужен.

Создайте оба клона до первой отправки и настройте автора отдельно в каждом:

git clone remote/shared.git clone1
git clone remote/shared.git clone2
cd clone1
git config user.name "Учебный автор 1"
git config user.email "learner1@example.invalid"
git status
git log --oneline main
git log --oneline origin/main
cd ../clone2
git config user.name "Учебный автор 2"
git config user.email "learner2@example.invalid"
git status
git log --oneline main
git log --oneline origin/main

В обоих клонах должны быть On branch main, чистая рабочая папка и один одинаковый коммит A в обоих логах. Clone установил upstream origin/main; не меняйте настройки remote и tracking. Записи ID и наблюдений храните вне клонов. В этом опыте никто больше не меняет remote.

В clone2 откройте README редактором, сохраняя исходные окончания строк. После строки статуса добавьте пустую строку и Версия: 1, сохраните файл. Опубликуйте коммит B:

git add README.md
git commit -m "Добавить версию"
git push
git log --oneline
cd ../clone1
git status
git pull --ff-only

Перед pull clone1 чист и не имеет своих новых коммитов. Запишите ID B из лога clone2 и сравните с вершиной main в clone1 после команды. Синтаксис самой команды:

git pull --ff-only

Вывод при продвижении содержит Fast-forward. Ниже условные ID; число добавленных строк зависит также от пустых строк и сохранённых окончаний строк:

Updating a1b2c3d..e4f5a6b
Fast-forward
 README.md | 1 +
 1 file changed, 1 insertion(+)

После этого:

  • git log --oneline показывает коммит B на вершине main;
  • README в clone1 содержит строку «Версия: 1»;
  • git status чист — ни staged, ни unstaged изменений нет;
  • нового merge-коммита в графе нет.

Если повторить git pull --ff-only сейчас, обновлять уже нечего: допустим успешный ответ Already up to date. Такой же ответ возможен, когда main только опережает полученную ветку без расхождения; наличие одного локального коммита само по себе не требует отказа.

Fast-forward работает именно потому, что clone1 не добавлял ничего своего поверх исходного коммита A. origin/main просто ушла вперёд, и Git переместил main в ту же точку.

Отказ --ff-only при расходящейся истории: два этапа, два разных результата

Расходящаяся история — это когда у main и origin/main есть коммиты, которых нет в другой. Ни одна из вершин не является предком другой: история разветвилась.

Подготовьте эту развилку в тех же двух клонах после получения B. В clone1 добавьте в конец README строку Правка: локальная и сохраните исходные окончания строк. Затем:

git add README.md
git commit -m "Локальная правка"
git log --oneline
git status
cd ../clone2

Запишите ID нового коммита C. В clone2, который всё ещё на B, независимо добавьте в тот же конец README Правка: коллега:

git add README.md
git commit -m "Правка коллеги"
git push
git log --oneline
cd ../clone1
git log --oneline main
git log --oneline origin/main
git status
git diff
git diff --staged
git pull --ff-only

Запишите ID D из clone2. Перед pull в clone1 чистая main на C, а ещё устаревшая origin/main на B; README содержит локальную правку, оба diff пусты. До этого pull не получайте D отдельно. Два этапа команды независимы по результату.

Первый этап — fetch. Git связывается с origin, получает новые объекты и обновляет origin/main. В данном опыте получение успешно завершается до проверки возможности объединения. Если само получение завершится ошибкой, выводы об актуальности ссылки делать нельзя.

Второй этап — объединение. Если fast-forward невозможен, Git отказывает именно здесь. Типичное сообщение:

fatal: Not possible to fast-forward, aborting.

Что при этом остаётся нетронутым:

  • локальная main — по-прежнему указывает на коммит C («Локальная правка»);
  • рабочие файлы — README содержит «Правка: локальная», ничего не изменилось;
  • индекс — чистый, как был.

Что уже изменилось в репозитории:

  • origin/main обновлена и теперь указывает на коммит D («Правка коллеги»);
  • объекты коммита D и его дерева уже получены и хранятся локально.

Проверить это можно сразу после отказа:

git log --graph --oneline --all

Граф покажет два расходящихся пути: main на C, origin/main на D, общий предок — B.

Важный вывод: отказ этапа объединения не «откатывает» fetch. Репозиторий уже знает о D, даже если main к нему не прикоснулась. Поэтому нельзя делать вывод об отсутствии любых изменений по одному лишь неизменному git log текущей ветки.

git fetch origin + git merge origin/main: явное объединение расходящихся веток

После того как pull --ff-only отказал, origin/main уже актуальна — fetch выполнен. Но повторить его не вредно: если с момента отказа в origin появились новые коммиты, повторный fetch их подхватит.

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

Здесь merge объединяет текущую main (коммит C) и origin/main (коммит D). У них общий предок B, поэтому Git попытается применить изменения обеих сторон.

Возможный конфликт. Оба коммита добавляли строки в конец одного файла README. Git видит два вставленных блока в одном участке и не знает, какой из них «правильный». В этом случае git merge остановится с сообщением о конфликте, а git status покажет:

You have unmerged paths.
  (fix conflicts and run "git commit")

Unmerged paths:
  (use "git add <file>..." to mark resolution...)
        both modified:   README.md

README будет содержать маркеры:

<<<<<<< HEAD
Правка: локальная
=======
Правка: коллега
>>>>>>> origin/main

Нужно открыть файл в редакторе, убрать маркеры и оставить обе строки:

# Командный справочник

Контакт: команда

Статус: черновик

Версия: 1
Правка: локальная
Правка: коллега

Затем зафиксировать разрешение:

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

Если Git объединил автоматически (правки не перекрылись), дополнительный commit не нужен — merge уже завершён и коммит создан с переданным сообщением.

После завершения:

git log --graph --oneline --all

Вывод покажет merge-коммит с двумя родителями — C и D. ID ниже условные; сообщения и родительство сопоставьте со своими записями:

*   f7a1234 Объединить правки
|\  
| * d3e5678 Правка коллеги
* | c2b4567 Локальная правка
|/  
* b1a2345 Добавить версию
* a0f1234 Initial commit

git status чист, README содержит «Версию: 1», «Правку: локальная» и «Правку: коллега». Отправьте результат из clone1 и обновите чистую main в clone2:

git push
cd ../clone2
git fetch origin
git merge origin/main
git status
git log --graph --oneline --all

Оба клона теперь имеют одинаковую вершину main и актуальную origin/main, версию и обе правки в README. Сравнивайте фактические ID, а не условные хеши рисунка.

Задача 1

Чистая местная main и origin/main находятся на А. Коллега отправил потомка Б в общую main. Upstream настроен; местных новых коммитов нет. Обычный pull --ff-only успешно завершился. Каков итог?

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

Задача 2

Изначально origin/main на общем предке А, чистая местная main на своём потомке Л. Коллега отправил другого потомка К. После pull --ff-only получен отказ fast-forward. Какое изменение могло уже произойти?

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

Задача 3

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

git clone remote/shared.git clone1
git clone remote/shared.git clone2
cd clone1
git config user.name "Учебный автор 1"
git config user.email "learner1@example.invalid"
cd ../clone2
git config user.name "Учебный автор 2"
git config user.email "learner2@example.invalid"
cd ..

Оба clone на чистой main с upstream origin/main; init не нужен.
Часть 1. В clone2 добавьте в конец README «Версия: 1», выполните git add README.md, git commit -m "Добавить версию", git push. В clone1 выполните git pull --ff-only. Подтвердите fast-forward: main и файл обновились до опубликованного коммита, новый merge-коммит не создан, версия видна, status чист.
Часть 2. В clone1 поверх полученного B добавьте в конец README «Правка: локальная» и сделайте коммит C. В clone2 поверх B независимо добавьте в тот же конец «Правка: коллега», сделайте коммит D и отправьте. В clone1 до этого D не получайте. Сохраните исходные log main, граф --all, README, status, diff и diff --staged; рабочая папка и индекс чистые.
В clone1 выполните pull --ff-only и запишите фактический отказ. Проверьте: main и README остались на C, индекс прежний, но origin/main после fetch указывает на полученный D; граф --all показывает расхождение. Не делайте вывод об отсутствии любых изменений репозитория по одному неизменному локальному log.
Явно выполните fetch origin и merge -m "Объединить правки" origin/main. Проверяйте status и README: вставки в тот же участок могут конфликтовать. При конфликте соберите текст с обеими строками правок, сохраните исходные строки и версию, удалите маркеры, выполните add README.md и commit -m "Объединить правки". Если merge уже завершён автоматически, дополнительный commit не нужен.
Подтвердите обе правки в README, merge-коммит с двумя родителями и чистый status. Отправьте main из clone1; в чистой main clone2 выполните fetch origin и merge origin/main. Проверьте одинаковое итоговое содержимое README, согласованные main и актуальные origin/main обоих clone, чистые status/diff/staged diff. Приложите сравнение указателей до/после отказа и доказательство завершения merge; заранее заданных SHA и дат нет.
Доступные средства: редактор, git clone, git config user.name/user.email, git add, git commit -m, git push, git pull --ff-only, git fetch origin, git merge -m, git merge origin/main, git log --oneline, git log --graph --oneline --all, git status, git diff, git diff --staged. Маркеры/add/commit разрешения уже изучены в lesson_17.

Письменное объяснение:
Коллега говорит: «Я всегда использую просто git pull без флагов — зачем усложнять? Ведь Git сам разберётся, что делать». Объясните, почему git pull без явного флага может давать разный результат на разных машинах, и какие два подхода позволяют сделать поведение предсказуемым: один для линейной истории, другой для расходящейся.

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

Контрольный случай:
В другом clone после разных изменений одной строки описания pull --ff-only отказал. Затем выполнили merge origin/main. При осмотре README нашли блок с <<<<<<< HEAD, ======= и >>>>>>> origin/main; status показывает unmerged paths для этого файла. После merge других команд не выполняли. Можно ли уже выполнить обычный push и считать синхронизацию завершённой?

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

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

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

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