Перевод третьей части статьи «How to Use Git and Git Workflows – a Practical Guide».
В первой части статьи мы рассмотрели установку Git, создание нового репозитория на GitHub и его клонирование на локальную машину. Также мы разобрали тему веток, научились проверять статус проекта и делать коммиты. Во второй части мы запушили наш коммит на GitHub, поближе познакомились со стейджингом, научились просматривать разницу между коммитами, создавать ветки отдельных функций и пушить их в репозиторий.
В этой части мы рассмотрим:
- Процесс совместной работы в Git
- Как слить (смержить) ветку в Git
- Процедуру пул-реквестов
- Как обновить локальный репозиторий
- Получение данных из удаленного репозитория
- Как разрешать конфликты слияния в Git
- Процесс работы над новой задачей от начала и до конца (повторение)
Совместная работа в Git
Вставить нашу Главу 2 в ветку main
локально и на GitHub можно двумя разными способами. Выбор зависит от проекта и принятых в коллективе процедур.
Давайте рассмотрим оба варианта.
Первый — самый простой:
- Слить изменения из
chapter-2
в локальную веткуmain
. - Запушить локальную ветку
main
вorigin/main
.
Второй способ немного сложнее:
- Запушить нашу локальную ветку
chapter-2
вorigin
(это создаст вorigin
новую ветку —origin/chapter-2
). - Слить
origin/chapter-2
вorigin/main
на GitHub. - Вытащить новые изменения из
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
внесенных изменений:
Можно заметить, что нам показан только файл 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. Это здесь и произошло.
Теперь наш локальный репозиторий знает о наличии новых коммитов, но мы пока ничего с ними не делали.
Запуск 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]