Практическое руководство по использованию Git. Часть 3

Перевод третьей части статьи «How to Use Git and Git Workflows – a Practical Guide».

Photo by Sigmund on Unsplash

В первой части статьи мы рассмотрели установку Git, создание нового репозитория на GitHub и его клонирование на локальную машину. Также мы разобрали тему веток, научились проверять статус проекта и делать коммиты. Во второй части мы запушили наш коммит на GitHub, поближе познакомились со стейджингом, научились просматривать разницу между коммитами, создавать ветки отдельных функций и пушить их в репозиторий.

В этой части мы рассмотрим:

Совместная работа в Git

Вставить нашу Главу 2 в ветку main локально и на GitHub можно двумя разными способами. Выбор зависит от проекта и принятых в коллективе процедур.

Давайте рассмотрим оба варианта.

Первый — самый простой:

  1. Слить изменения из chapter-2 в локальную ветку main.
  2. Запушить локальную ветку main в origin/main.

Второй способ немного сложнее:

  1. Запушить нашу локальную ветку chapter-2 в origin (это создаст в origin новую ветку — origin/chapter-2).
  2. Слить origin/chapter-2 в origin/main на GitHub.
  3. Вытащить новые изменения из origin/main в нашу локальную ветку main.

Первый подход определенно проще. Если бы я работал над проектом один, без коллег, то, безусловно, выбрал бы его.

Но работая совместно с другими людьми, я бы предпочел не пушить свою локальную ветку напрямую в ветку main. Таким образом я бы изменил историю проекта и взял бы ее под контроль лично своих изменений — без ревью и какого-либо участия коллег.

Поэтому, если над одним проектом работает несколько человек, я предпочту второй способ, потому что для команды такая процедура лучше.

В этом руководстве мы рассмотрим оба варианта. Начнем с первого, как менее сложного.

Как слить (смержить) ветку в Git

Если вы хотите соединить содержимое двух веток в одной, вы можете сделать это разными способами. Первый и, вероятно, самый простой — сделать merge (слияние).

Как и следует из названия, merge пытается влить содержимое одной ветки в другую.

В нашем случае мы хотим взять содержимое ветки chapter-2 и слить (смержить) их в main. Иными словами, мы хотим взять текущее состояние main и добавить в него наши изменения из ветки chapter-2.

Мы можем это сделать при помощи команды git merge, а потом оценить результаты.

Первое, что нам нужно сделать, это оказаться в той ветке, куда мы хотим сливать изменения. Поскольку мы хотим, чтобы main вобрала в себя изменения из chapter-2, нам нужно оказаться в ветке main.

Чтобы переключиться обратно в ветку main, мы можем применить команду git checkout и указать имя ветки — main. Теперь мы не будем добавлять флаг -b, как делали в прошлый раз, потому что переключаемся в уже существующую ветку, а не создаем новую:

(chapter-2)$ git checkout main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
(main)$

Мы вернулись в ветку main и получили короткое сообщение о статусе: наша ветка полностью соответствует origin/main.

Теперь давайте смержим chapter-2 в main:

(main)$ git merge chapter-2
Updating f5b6e2f..741822a
Fast-forward
 chapter-2.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 chapter-2.txt

Посмотрим результат в логе:

(main)$ git log
commit 741822a9fd7b15b6e3caf437dd0617fabf918449 (HEAD -> main, chapter-2)
Author: John Mosesman <johnmosesman@gmail.com>
Date:   Mon Mar 22 10:33:26 2021 -0500

    Creates chapter 2 and adds the topic sentence

commit f5b6e2f18f742e2b851e38f52a969dd921f72d2f (origin/main, origin/HEAD)
Author: John Mosesman <johnmosesman@gmail.com>
Date:   Mon Mar 22 10:07:35 2021 -0500

    Added the intro line to chapter 1
    
...

Мы видим, что в нашей ветке main теперь содержится новый коммит из chapter-2, а наш origin по-прежнему стоит на предыдущем коммите (поскольку мы его еще не обновляли).

Наконец, давайте отправим (запушим) наши изменения в origin/main:

(main)$ git push origin main
Total 0 (delta 0), reused 0 (delta 0)
To github.com:johnmosesman/practical-git-tutorial.git
   f5b6e2f..741822a  main -> main

Мы успешно смержили нашу ветку chapter-2 и запушили наши изменения на GitHub!

Финальный шаг: нам нужно удалить ветку функции chapter-2, поскольку она уже слита в main:

(main)$ git branch -d chapter-2
Deleted branch chapter-2 (was 741822a).

Примечание. Команда git branch без указания ветки в качестве аргумента выведет список всех веток, которые есть в вашем локальном проекте. Добавление флага -d и имени ветки позволяет удалить указанную ветку.

Процедура пул-реквестов

Чтобы изучить командную работу над проектом, давайте повторим действия, которые мы уже совершали, создавая Главу 1 и 2, и создадим новую ветку с Главой 3 — chapter-3. Не упустите возможность попробовать сделать это самостоятельно!

(main)$ git checkout -b chapter-3
(chapter-3)$ touch chapter-3.txt
(chapter-3)$ echo "Chapter 3 - The End?" >> chapter-3.txt
(chapter-3)$ git add .
(chapter-3)$ git commit -m "Adds Chapter 3"

Итак, у нас есть новый коммит в новой ветке chapter-3.

Давайте повторим наш план действий. Мы собираемся смержить эти изменения в main, но при этом самостоятельно не трогать main. Для этого мы:

  • запушим нашу локальную ветку chapter-3 в origin (это создаст в origin новую ветку — origin/chapter-3);
  • смержим origin/chapter-3 в origin/main на GitHub;
  • вытащим новые изменения из origin/main в нашу локальную ветку main.

В общем, будет пара дополнительных шагов, но ничего сверхъестественного.

Первый шаг — запушить новую ветку на GitHub. Поскольку этой ветки в нашем удаленном репозитории еще не существует, GitHub создаст для нас новую ветку — копию той, которую мы отправили:

(chapter-3)$ git push origin chapter-3
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 16 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 299 bytes | 299.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
remote:
remote: Create a pull request for 'chapter-3' on GitHub by visiting:
remote:      https://github.com/johnmosesman/practical-git-tutorial/pull/new/chapter-3
remote:
To github.com:johnmosesman/practical-git-tutorial.git
 * [new branch]      chapter-3 -> chapter-3

Теперь, когда наша ветка есть на GitHub, мы можем создать пул-реквест (pull request), чтобы его просмотрели наши коллеги.

GitHub даже предоставляет нам URL, чтобы мы могли посмотреть результат своих действий: https://github.com/johnmosesman/practical-git-tutorial/pull/new/chapter-3.

Пара примечаний. В следующей части будет обсуждаться процесс работы с пул-реквестами в пользовательском интерфейсе GitHub, но в других сервисах (GitLab, Bitbucket и т. п.) все происходит аналогичным образом.

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

Перейдя по указанному выше адресу, мы попадаем на страницу для открытия нового пул-реквеста.

Мы видим несколько вещей:

  • место для указания имени пул-реквеста (предложение с указанием темы, позволяющее легко определить, о чем этот пул-реквест)
  • блок для описания, где мы можем объяснить сделанные нами изменения и дать любой необходимый контекст (также можно добавить изображения, гифки или видео)
  • под всем этим — список файлов, которые мы изменили, и самих изменений (diff).
Открываем новый пул-реквест

Обратите внимание, что UI показывает base: main <- compare: chapter-3. Таким образом GitHub сообщает нам, что мы составляем пул-реквест для вливания chapter-3 в main.

Под описанием пул-реквеста мы видим diff внесенных изменений:

diff пул-реквеста

Можно заметить, что нам показан только файл chapter-3.txt. Это потому, что только он у нас изменился.

В настоящее время в нашем проекте есть и другие файлы (chapter-1.txt, chapter-2.txt), но они не менялись, так что показывать их нет нужды.

Мы видим одну строку, которую вставили в chapter-3.txt, перед которой стоит знак плюс, а также видим зеленый фон, означающий добавление контента в файл.

После нажатия «Create Pull Request» («Создать пул-реквест») мы попадаем в новый, только что созданный PR (пул-реквест).

На этом этапе мы можем назначить нашему PR ревьюера и вступить с ним в обсуждение нашего кода. Обсуждение идет путем добавления комментариев к отдельным строкам в diff. После проверки кода (код-ревью) и внесения всех нужных изменений мы можем смержить наш код.

В этом руководстве мы пропустим процедуру ревью и просто кликнем большую зеленую кнопку merge:

Смерженный пул-реквест

После этого наш пул-реквест вольется в main!

Как обновить локальный репозиторий

Итак мы внесли изменение в origin/main безопасным и контролируемым способом, а наши изменения прошли код-ревью.

Но наш локальный репозиторий ничего не знает об этих изменениях. Локально Git все еще думает, что мы находимся в ветке chapter-3, которая не слита в main:

(chapter-3)$ git log
commit 085ca1ce2d0010fdaa1c0ffc23ff880091ce1692 (HEAD -> chapter-3, origin/chapter-3)
Author: John Mosesman <johnmosesman@gmail.com>
Date:   Tue Mar 23 09:19:14 2021 -0500

    Adds Chapter 3

commit 741822a9fd7b15b6e3caf437dd0617fabf918449 (origin/main, origin/HEAD, main)
Author: John Mosesman <johnmosesman@gmail.com>
Date:   Mon Mar 22 10:33:26 2021 -0500

    Creates chapter 2 and adds the topic sentence

...

git log показывает, что origin/main указывает на предыдущий коммит, начинающийся с 741822. Чтобы обновить наш локальный репозиторий, нам нужно вытянуть новую информацию из нашего origin.

Как получить данные из удаленного репозитория

В Git зачастую одну и ту же задачу можно выполнить разными способами, и этот случай — не исключение.

Для целей этой статьи мы рассмотрим самый простой способ, который работает в большинстве случаев.

Для начала давайте переключимся обратно на нашу локальную ветку main:

(chapter-3)$ git checkout main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

Git думает, что эта ветка полностью соответствует origin/main, потому что со времени клонирования удаленного репозитория мы пока не получали из него новую информацию.

Репозитории Git не обновляются в режиме реального времени. Это лишь снимки истории на определенные моменты времени. Чтобы получить новую информацию о репозитории, нам нужно запросить ее заново.

Для запроса новой информации мы используем команду git fetch (англ. fetch — делать выборку):

(main)$ git fetch
From github.com:johnmosesman/practical-git-tutorial
   741822a..10630f2  main       -> origin/main

В выводе мы видим, что origin/main сейчас указывает на коммит, начинающийся с 10630f2. Этот префикс совпадает с SHA коммита, по которому мы делали пул-реквест.

Есть несколько способов смержить две ветки (одну в другую), и один из них — создать merge commit. Это здесь и произошло.

merge commit нашего PR

Теперь наш локальный репозиторий знает о наличии новых коммитов, но мы пока ничего с ними не делали.

Запуск git fetch ничего не меняет в наших файлах. Мы просто загрузили из удаленного репозитория новую информацию о его статусе.

Теперь наш локальный репозиторий знает о статусе каждой ветки (но эти ветки локально не изменены и не обновлены). Давайте еще раз проверим статус проекта:

(main)$ git status
Your branch is behind 'origin/main' by 2 commits, and can be fast-forwarded.
  (use "git pull" to update your local branch)

Наш локальный Git теперь знает, что локальная ветка main отстает от origin/main на 2 коммита (коммит из ветки chapter-3 и merge commit пул-реквеста).

Он также подсказывает нам использовать git pull для обновления локальной ветки:

john:~/code/practical-git-tutorial (main)$ git pull origin main
From github.com:johnmosesman/practical-git-tutorial
 * branch            main       -> FETCH_HEAD
Updating 741822a..10630f2
Fast-forward
 chapter-3.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 chapter-3.txt

Команда git pull — это, собственно, сокращенная форма запуска двух команд: git fetch, за которой сразу идет git merge.

Команда git fetch не вносит никаких изменений в локальный репозиторий. Поэтому она полезна для проверки того, соответствуют ли наши ветки веткам в удаленном репозитории (мало ли — может, мы пока вообще не хотим мержить свои изменения). Также эта команда удобна для вытаскивания новых веток, существующих в удаленном репозитории, но пока отсутствующих на нашей локальной машине.

Когда мы делаем выборку (fetch) новой ветки из удаленного репозитория, она загружается на нашу локальную машину. Поскольку до сих пор такой ветки у нас не было, любые конфликты исключены.

Мы могли бы изначально сделать git pull и обойтись без git fetch, но я хотел познакомить вас с git fetch, потому что это очень полезная команда.

После запуска git pull запустим еще раз git status, чтобы посмотреть, все ли обновилось.

Вот и все! Мы вытащили наши изменения из удаленного репозитория и обновили локальный!

Как исправлять конфликты слияния в Git

И последняя важная тема, которую мы рассмотрим, — как быть с конфликтами.

Пока что Git просто чудесно справлялся со всеми обновлениями файлов. Чаще всего так и происходит. Но бывают случаи, когда Git не знает, как скомбинировать изменения, и это создает конфликт.

Конфликт происходит при попытке слить два изменения, касающиеся одной и той же строки в файле. Если два коммита изменили одну строку, Git не знает, какие именно изменения применить. Тут выбор придется делать вам.

Чтобы смоделировать такой сценарий, мы создадим еще одну ветку на GitHub — chapter-3-collaboration. Представим, что наш коллега уже начал работать над этой веткой и попросил нас принять участие в завершении Главы 3.

Поскольку это новая ветка, которой у нас нет на локальной машине, мы можем получить информацию о ней из удаленного репозитория при помощи git fetch, а затем перейти в эту ветку при помощи git checkout:

(main)$ git fetch
From github.com:johnmosesman/practical-git-tutorial
 * [new branch]      chapter-3-collaboration -> origin/chapter-3-collaboration

(main)$ git checkout chapter-3-collaboration
Branch 'chapter-3-collaboration' set up to track remote branch 'chapter-3-collaboration' from 'origin'.
Switched to a new branch 'chapter-3-collaboration'
(chapter-3-collaboration)$

Мы вытащили новую ветку в свой локальный репозиторий и переключились на нее. В настоящее время в этой ветке в файле chapter-3.txt есть следующий текст:

(chapter-3-collaboration)$ cat chapter-3.txt
Chapter 3 - The End?

This is a sentence.

Это название главы и одно предложение. Давайте изменим название, например, назовем главу «Chapter 3 — The End Is Only The Beginning».

Теперь содержимое файла chapter-3.txt выглядит так:

(chapter-3-collaboration)$ cat chapter-3.txt
Chapter 3 - The End Is Only The Beginning

This is a sentence.

После коммита этих изменений мы можем попытаться их запушить, но получим следующее сообщение:

(chapter-3-collaboration)$ git push origin chapter-3-collaboration
To github.com:johnmosesman/practical-git-tutorial.git
 ! [rejected]        chapter-3-collaboration -> chapter-3-collaboration (non-fast-forward)
error: failed to push some refs to 'git@github.com:johnmosesman/practical-git-tutorial.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Наш коллега уже сделал какие-то коммиты до нас и запушил их в удаленную ветку. Наша локальная ветка теперь отстает от удаленной. Поэтому GitHub не будет принимать наш push, пока мы не смержим изменения, внесенные нашим товарищем:

... the tip of your current branch is behind its remote counterpart. Integrate the remote changes ... before pushing again.

В сообщении также есть подсказка, как это сделать: воспользоваться git pull.

(chapter-3-collaboration)$ git pull origin chapter-3-collaboration
From github.com:johnmosesman/practical-git-tutorial
 * branch            chapter-3-collaboration -> FETCH_HEAD
Auto-merging chapter-3.txt
CONFLICT (content): Merge conflict in chapter-3.txt
Automatic merge failed; fix conflicts and then commit the result.

А после применения команды git pull мы получаем конфликт слияния (название раздела как бы намекало на это).

Git попытался автоматически влить изменения, сделанные нашим коллегой, в наши. Но в файле было место, которое он не смог смержить автоматически: мы оба изменили одну и ту же строку.

Git остановился посреди мержа и сообщил, что прежде чем он сможет завершить, нам нужно разрешить конфликты слияния. Давайте посмотрим статус проекта в настоящее время:

(chapter-3-collaboration)$ git status
On branch chapter-3-collaboration
Your branch and 'origin/chapter-3-collaboration' have diverged,
and have 1 and 1 different commits each, respectively.
  (use "git pull" to merge the remote branch into yours)

You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
        both modified:   chapter-3.txt

no changes added to commit (use "git add" and/or "git commit -a")

Наша ветка и удаленная ветка отличаются друг от друга на 1 коммит. Git также сообщает, что у нас есть некоторые «размерженные пути» («unmerged paths»), т. е., мы выполнили слияние наполовину и должны разрешить конфликты.

Нам показано, что файл chapter-3.txt изменен. Давайте посмотрим на его содержимое:

(chapter-3-collaboration)$ cat chapter-3.txt
<<<<<<< HEAD
Chapter 3 - The End Is Only The Beginning
=======
Chapter 3 - The End But Not The Ending
>>>>>>> 2f6874f650a6a9d2b7ccefa7c9618deb1d45541e

This is a sentence.

Git добавил в файл маркеры, показывающие, где именно случился конфликт. И мы, и наш коллега изменили название главы, поэтому наши варианты окружены маркерами конфликта — стрелками <<< и >>> — и разделены линией ===.

Верхняя строка — «Chapter 3 — The End Is Only The Beginning» — помечена <<<<<<< HEAD. Это изменение, которое внесли мы. Git показывает, что это строка, на которую в настоящее время указывает HEAD. Т.е. это изменение в нашем текущем коммите.

Строка ниже — «Chapter 3 — The End But Not The Ending» — помечена >>>>>>> 2f6874f650a6a9d2b7ccefa7c9618deb1d45541e. Это строка и номер коммита нашего коллеги.

В общем, Git спрашивает: «Какую из этих строк (или какую их комбинацию) вы хотите сохранить?»

Обратите внимание, что строка внизу файла не помечена маркерами. Она ни с чем не конфликтует, так как не была изменена сразу двумя коммитами.

Нам нужно разрешить конфликт, удалив одну из строк или скомбинировав две строки в одну (еще нужно не забыть удалить лишние маркеры, вставленные Git).

Я собираюсь скомбинировать строки, чтобы итоговый вариант выглядел так:

(chapter-3-collaboration)$ cat chapter-3.txt
Chapter 3 - The End Is Not The Ending--But Only The Beginning

This is a sentence.

Чтобы завершить этот мерж, нам нужно просто закоммитить наше разрешение конфликта:

(chapter-3-collaboration)$ git add .
(chapter-3-collaboration)$ git commit -m "Merge new title from teammate"
[chapter-3-collaboration bd621aa] Merge new title from teammate

(chapter-3-collaboration)$ git status
On branch chapter-3-collaboration
Your branch is ahead of 'origin/chapter-3-collaboration' by 2 commits.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean

В результате git status сообщает нам, что наша локальная ветка опережает ветку в origin на 2 коммита (is ahead of 'origin/chapter-3-collaboration' by 2 commits).

Лог подтверждает это:

commit bd621aa0e491a291af409283f5fd1f68407b94e0 (HEAD -> chapter-3-collaboration)
Merge: 74ed9b0 2f6874f
Author: John Mosesman <johnmosesman@gmail.com>
Date:   Thu Mar 25 09:20:42 2021 -0500

    Merge new title from teammate

commit 74ed9b0d0d9154c912e1f194f04dbd6abea602e6
Author: John Mosesman <johnmosesman@gmail.com>
Date:   Thu Mar 25 09:02:03 2021 -0500

    New title

commit 2f6874f650a6a9d2b7ccefa7c9618deb1d45541e (origin/chapter-3-collaboration)
Author: John Mosesman <johnmosesman@gmail.com>
Date:   Thu Mar 25 08:58:58 2021 -0500

    Update title

...

Итоговая история коммитов содержит оба коммита в этой ветке и наш merge commit сверху.

Нам остается лишь запушить наши изменения в удаленную ветку:

(chapter-3-collaboration)$ git pull origin chapter-3-collaboration
Enumerating objects: 10, done.
Counting objects: 100% (10/10), done.
Delta compression using up to 16 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 647 bytes | 647.00 KiB/s, done.
Total 6 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 1 local object.
To github.com:johnmosesman/practical-git-tutorial.git
   2f6874f..bd621aa  chapter-3-collaboration -> chapter-3-collaboration

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

В идеале мы сообщим ему, что мы запушили новое изменение. Так он сможет сначала вытянуть эти изменения на свою машину, а уж потом продолжить редактирование. Это снизит вероятность того, что теперь уже ему придется разрешать конфликт слияния.

Ответвления веток

Мы также могли бы создать нашу собственную ветку, сделав ответвление от chapter-3-collaboration. Это позволило бы нам работать, не беспокоясь о конфликтах слияния до самого конца работы.

Завершив работу в нашей отдельной ветке, мы могли бы смержить нашу ветку в ветку коллеги, а затем в main.

chapter-3-collaboration-john -> chapter-3-collaboration -> main

Как видите, по мере появления новых веток и параллельной работы над ними структура становится все более сложной.

Из-за этого в целом считается здравой идеей делать маленькие изолированные ветки и стараться мержить их как можно быстрее.

Это позволяет избежать многих болезненных конфликтов слияния.

Повторение: как начать рабочий процесс над новым функционалом

Давайте быстренько повторим, как начать работать над новой задачей и какие команды для этого потребуются.

Допустим, вам на вашей новой работе выдали первый тикет. Нужно исправить маленький баг в продукте вашей команды.

Первое, что нужно сделать, это вытащить репозиторий на свою локальную машину при помощи команды git clone <URL>.

Затем нужно создать ветку для своей работы (feature branch). Вы делаете ответвление от main, используя команду git checkout -b <имя_ветки>. После этого вы исправляете баг и делаете коммит изменений при помощи git add и git commit.

Возможно, решение проблемы потребует нескольких коммитов. Также может случиться, что вы сделаете несколько коммитов в безуспешных попытках решить проблему, прежде чем вам это наконец удастся. Это нормально.

После итогового коммита вы делаете push своей новой ветки в origin (git push origin <имя_ветки>) и создаете пул-реквест. После прохождения код-ревью ваша ветка будет слита (ура!).

Вы успешно выполнили порученную вам задачу. Пора переключиться назад в main (при помощи git checkout main), применить git pull, чтобы вытянуть последние изменения (как свои, так и чужие) и начать все заново в новой ветке.

Итоги

Как я говорил в начале статьи, в Git одинаковые задачи можно решать по-разному. В нем также много невидимой «магии» (т. е. кода, который запускается, хотя вы его пока не понимаете). Со временем вы научитесь многим другим приемам работы.

Я провел первые годы своей карьеры, просто пользуясь заученными командами и процедурами. Это работало. Встречаясь с проблемами и работая вместе с коллегами, я учился все новым приемам, и в результате мои навыки работы с Git улучшились.

Со временем и вы всему научитесь. Но в самом начале не усложняйте себе жизнь без необходимости!

[customscript]techrocks_custom_after_post_html[/customscript]

[customscript]techrocks_custom_script[/customscript]

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх