$ sudo teach IT
Модуль 5 · GitHub · Урок 5.4

Clone, fetch и pull

Вы скачаете копию проекта с GitHub, научитесь узнавать об изменениях на сервере и забирать их, а также справляться с отклонённым push.

Теория + практика~30 минутНовичокclonefetchpull

git clone: копия репозитория

Чтобы скачать проект с GitHub, используют команду git clone <адрес>: она создаёт папку, кладёт в неё полный репозиторий со всей историей и сама записывает адрес как origin. Сейчас вы сделаете вторую копию своей визитки и научитесь синхронизировать две копии через GitHub.

Зачем вторая копия? Вы можете работать то на ноутбуке, то на настольном компьютере, а в команде у каждого свой клон, и всем нужно получать чужие изменения. Мы смоделируем это сами: вторая папка portfolio-colleague будет «компьютером коллеги», который работает с тем же репозиторием на GitHub.

Клон (clone) — это полная копия репозитория: все файлы, все коммиты и вся история. От простого скачивания архива он отличается тем, что остаётся связанным с оригиналом. Выполните, находясь в папке ~/projects (рядом с portfolio, а не внутри него):

Терминал
$ cd ~/projects
$ git clone https://github.com/alex-ivanov/portfolio.git portfolio-colleague
Cloning into 'portfolio-colleague'...
...
$ cd portfolio-colleague
$ git status
On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean
$ git remote -v
origin  https://github.com/alex-ivanov/portfolio.git (fetch)
origin  https://github.com/alex-ivanov/portfolio.git (push)
$ ls -A
.git  .gitignore  README.md  index.html  projects.html  style.css

Разбор по строкам

  • git clone <адрес> portfolio-colleague — скачать репозиторий в новую папку с указанным именем. Без имени Git взял бы имя из адреса (portfolio), и папка совпала бы с вашей рабочей.
  • Cloning into 'portfolio-colleague'... — идёт скачивание. Вместо ... вы увидите строки прогресса (Receiving objects, Resolving deltas), они зависят от размера проекта.
  • Your branch is up to date with 'origin/main'. — клон сразу настроен: ветка main связана с origin/main, и флаг -u не нужен.
  • git remote -v — origin уже записан, git remote add делать не пришлось.
  • ls -A — вся история лежит в скрытой папке .git. Папки notes нет: она в .gitignore, в Git не попадала и поэтому на GitHub не уехала.

Чтобы коммиты «коллеги» отличались от ваших, задайте для этой папки другое имя автора (без --global, настройка действует только в этом клоне):

Терминал
$ git config user.name "Коллега"
$ git config user.email colleague@example.com

Изменения с другой стороны: fetch

Пусть «коллега» что-то поменял и отправил на GitHub. Допишите в конец README.md в папке portfolio-colleague пустую строку и строку «Автор: Алекс Иванов.», затем:

Терминал
$ git commit -am "Добавить автора в README"
$ git push
To https://github.com/alex-ivanov/portfolio.git
   a3bf107..71155ae  main -> main

Теперь вернитесь в основную папку и спросите у Git, как дела:

Терминал
$ cd ../portfolio
$ git status
On branch main
Your branch is up to date with 'origin/main'.

Git утверждает, что всё актуально, хотя на GitHub уже лежит новый коммит. Это не баг. Помните, что origin/main — ваша локальная пометка о том, где была ветка на GitHub при последнем обмене. git status смотрит только на неё и в интернет не ходит. Чтобы пометка обновилась, нужно сходить на сервер:

Терминал
$ git fetch
From https://github.com/alex-ivanov/portfolio
   a3bf107..71155ae  main       -> origin/main
$ git status
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
  (use "git pull" to update your local branch)

nothing to commit, working tree clean
$ git log --oneline main..origin/main
71155ae Добавить автора в README

Разбор по строкам

  • git fetch — «сходи на GitHub, скачай новые коммиты и обнови пометки origin/*». Рабочие файлы и ваша ветка main не меняются.
  • From https://github.com/... — откуда скачали.
  • a3bf107..71155ae main -> origin/main — пометка origin/main передвинулась с коммита a3bf107 на 71155ae.
  • behind 'origin/main' by 1 commit, and can be fast-forwarded — ваша main отстаёт на один коммит, и её можно просто «перемотать» вперёд (fast-forward) без слияния.
  • git log --oneline main..origin/main — коммиты, которые есть в origin/main, но ещё нет в main. Запись A..B читается «всё, что в B, но не в A».

Частая путаница: fetch и pull

fetch только узнаёт о новом и скачивает коммиты, ваши файлы он не трогает, поэтому безопасен всегда. pull делает то же самое и сразу применяет новое к текущей ветке. Если хотите сначала посмотреть, что придёт, используйте fetch.

git pull = fetch + merge

**git pull** — это два действия подряд: git fetch (скачать) и затем слияние (merge) скачанного в вашу текущую ветку. Слияние вы уже знаете по модулю про ветки. Применим новый коммит:

Терминал
$ git pull
Updating a3bf107..71155ae
Fast-forward
 README.md | 2 ++
 1 file changed, 2 insertions(+)

Fast-forward — перемотка: ваша ветка просто двинулась вперёд на чужой коммит, а новых коммитов слияния не появилось. Теперь git status снова скажет up to date, и README.md содержит строку про автора.

Когда ветки разошлись

Сложнее, если новые коммиты есть и там, и там. Продолжим сценарий. «Коллега» дописывает под строкой про автора фразу «Технологии: HTML, CSS, Git и GitHub.» и отправляет (commit, push). А вы, не зная об этом, в основной папке добавляете в конец style.css пустую строку и правило a { color: #3b82f6; }, делаете коммит Покрасить ссылки в синий и пытаетесь отправить:

Терминал
$ git push
To https://github.com/alex-ivanov/portfolio.git
 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to 'https://github.com/alex-ivanov/portfolio.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Разбор по строкам

  • ! [rejected] main -> main (fetch first) — GitHub отказал: «на сервере есть коммиты, которых у вас нет, сначала скачайте». Если вы уже делали fetch, вместо (fetch first) будет (non-fast-forward) и пояснение, что ваша ветка позади.
  • failed to push some refs — отправить не получилось. Это защита: Git не даёт молча затереть чужую работу.
  • hint: ... use 'git pull' before pushing again — подсказка и есть лечение: сначала git pull.

Посмотрим на ситуацию через git fetch и git status:

Терминал
$ git fetch
$ git status
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 1 different commits each, respectively.
  (use "git pull" if you want to integrate the remote branch with yours)

diverged значит «разошлись»: у вас один свой коммит, на GitHub один чужой, и общее у вас только то, что было раньше. На схеме:

Схема
         D   main (у вас)
        /
A---B---C
        \
         E   origin/main (на GitHub)

Что на схеме: общая история (A, B, C) дальше расходится на два коммита, ваш D и чужой E. Их нужно объединить слиянием. Это делает git pull:

Терминал
$ git pull
Merge made by the 'ort' strategy.
 README.md | 1 +
 1 file changed, 1 insertion(+)

Перед этим откроется редактор с готовым сообщением коммита слияния Merge branch 'main' of https://github.com/alex-ivanov/portfolio. Сообщение менять не надо: сохраните файл и закройте вкладку. Git создаст коммит слияния, и история будет такой:

Терминал
$ git log --oneline --graph -5
*   71e4ceb Merge branch 'main' of https://github.com/alex-ivanov/portfolio
|\
| * 404e947 Добавить технологии в README
* | 55c1edd Покрасить ссылки в синий
|/
* 71155ae Добавить автора в README
* a3bf107 Дополнить README

Теперь можно отправлять: git push пройдёт, потому что ваша ветка включает всё, что есть на GitHub. Если бы оба коммита правили одну и ту же строку, при pull вы получили бы конфликт: он решается так же, как конфликт слияния веток из прошлого модуля (правка, git add, git commit).

Почему pull.rebase false и что будет без него

Когда ветки разошлись, Git может объединить их двумя способами: слиянием (merge) или перебазированием (rebase). Какой выбрать, решаете вы, а до решения Git git pull не выполнит. Именно поэтому в начале курса вы выполнили git config --global pull.rebase false: эта настройка говорит «объединять слиянием». Если её нет, вы увидите такое:

Вывод
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.

Перевод главной строки: «нужно указать, как согласовать разошедшиеся ветки». Исправление: git config --global pull.rebase false и повторите git pull. Вариант rebase разберём в модуле про чистую историю, а пока пользуйтесь слиянием.

Практика у себя

  1. В папке ~/projects склонируйте репозиторий: git clone https://github.com/ваш-логин/portfolio.git portfolio-colleague. Войдите в папку и проверьте git status: должно быть up to date with 'origin/main'.
  2. Задайте в клоне другого автора: git config user.name "Коллега" и git config user.email colleague@example.com (без --global).
  3. В клоне допишите в README.md пустую строку и «Автор: Алекс Иванов.», выполните git commit -am "Добавить автора в README" и git push.
  4. Перейдите в ~/projects/portfolio. Выполните git status (там up to date), затем git fetch и снова git status: теперь behind ... by 1 commit.
  5. Посмотрите, что придёт: git log --oneline main..origin/main. Примените: git pull. Должно появиться Fast-forward.
  6. Создайте расхождение. В клоне допишите под строкой про автора «Технологии: HTML, CSS, Git и GitHub.», закоммитьте как Добавить технологии в README и отправьте. В основной папке допишите в style.css пустую строку и a { color: #3b82f6; }, закоммитьте как Покрасить ссылки в синий.
  7. В основной папке выполните git push — получите rejected. Затем git fetch, git status (diverged), git pull (сохраните и закройте окно сообщения коммита) и git push.
  8. Посмотрите git log --oneline --graph -5: коммит слияния и два параллельных коммита.

Проверьте себя

  • В ~/projects две папки: portfolio и portfolio-colleague, обе с remote origin на ваш репозиторий.
  • В git log --oneline --graph виден коммит Merge branch 'main' of https://github.com/....
  • После последнего git push команда git status в основной папке сообщает up to date with 'origin/main'.
  • Вы можете объяснить разницу между fetch и pull и то, почему пометка origin/main не обновляется сама.

Частые ошибки

Начали работать с утра, не сделав pull

Что видно: вы закоммитили, а git push отвечает rejected. Причина: пока вы работали, на GitHub что-то изменилось (в команде — чужие коммиты, у вас — работа с другого компьютера). Привычка, которая этого избегает: начинать день с git pull, а перед сложной правкой делать git fetch и смотреть git status.

git push --force, «чтобы прошло»

Не используйте --force в общей ветке

git push --force отправляет вашу версию, стирая коммиты, которых у вас нет. На общей ветке main это безвозвратно удаляет чужую работу. Если Git отказал, решение одно: git pull, решить конфликты, git push. Принудительная отправка нужна в редких случаях после перебазирования, и о ней мы поговорим позже, с безопасным вариантом.

Клон внутри другого репозитория

Если выполнить git clone из папки portfolio, получится репозиторий внутри репозитория: внешний git status покажет неотслеживаемую папку portfolio-colleague/, а команды будут путаться между двумя .git. Исправление: удалите вложенную папку и клонируйте заново из ~/projects. Перед клонированием проверяйте pwd.

Конфликт при pull

Если после git pull Git пишет CONFLICT (content): Merge conflict in ..., ничего не сломалось: одну строку поменяли в двух местах. Решайте так же, как в модуле про ветки: правка файла, git add, git commit; передумали — git merge --abort.

Справка: git clone, git fetch, git pull.

Что важно запомнить

  • git clone <адрес> <папка> скачивает репозиторий с историей и сам настраивает origin.
  • origin/main — локальная пометка о том, где main была на GitHub при последнем обмене. git fetch обновляет её, git status в интернет не ходит.
  • git pull = git fetch + слияние. Если ваша ветка просто отстала, будет Fast-forward; если разошлась, — коммит слияния.
  • rejected ... (fetch first) значит: на GitHub есть чужие коммиты. Лечение: git pull, при необходимости решить конфликт, git push.
  • Не используйте git push --force на общей ветке. Начинайте работу с git pull.

Теперь у вас есть всё для синхронизации двух копий проекта через GitHub. Осталось доделать главное, ради чего мы затеяли этот модуль: показать визитку миру. В последнем уроке модуля вы включите GitHub Pages и превратите репозиторий в настоящий сайт.

На следующий урок