Удаленный GIT, слияния и конфликты проще некуда

Сегодня я продолжу рассказывать про систему контроля версиями git. Если ты не видел первую часть, то текстовая версия есть в разделе статей на моем сайте (Учимся работать с GIT проще некуда), а видеоурок доступен здесь

Сегодня я продолжу рассказывать про систему контроля версиями git. Если ты не видел первую часть, то текстовая версия есть в разделе статей на моем сайте (https://www.flenov.info/story/show/Uchimsya-rabotaty-s-GIT-prosche-nekuda), а видеоурок доступен здесь https://www.youtube.com/watch?v=2jb05JSqGCI.

Начнем знакомство с удаленными ветками. Когда github был платным для собственных проектов я использовал свой собственный сервер для хранения удаленных репозиториев. Создать собственный git сервер не сложно, но github дает удобный Web интерфейс с огромным количеством возможностей, поэтому сегодня рассмотрим его.

Если у тебя нет github аккаунта, то создай его. Если есть, то авторизуйся и в верхнем правом углу кликаем на плюсик и в появившемся меню выбираем New repository:

В результате вы должны увидеть страницу, как показано на следующем скриншоте:

Имя репозитория – для данного примера назовем его demo. Описание не обязательно, оставляем пустым. Теперь нужно выбрать каким должен быть репозиторий – публичный (public) или private (частный). Публичный доступен всем, и кто угодно может в него комитить свой код. Обычно это OpenSource проекты. Частный или private будет доступ только тем, кому вы дадите права. Для теста можно выбрать любой, я оставлю private. Зачем мусорить тестовыми данными открытое сообщество.

Теперь у нас есть опции:

- Add a readme file. Помните, после инициализации нового репозитория у него не будет еще ветки мастер и для ее создания нужно сделать какой-нибудь комит. Github предлагает создать текстовый файл readme и закомитить его сразу же после инициализации, чтобы создать дерево. Мы уже сделали первый комит, так что эту опцию выбирать смысла нет.

- Add .gitignore – мы его добавим потом вручную и заодно поговорим и увидим, для чего нужен файл.

- Add a license – лицензионные вопросы, в нашем мире нужно везде защищаться от проблем с адвокатами. У нас это тест и репозиторий закрытый, поэтому эту опцию тоже можно не выбирать.

В общем все опции оставляем нетронутыми и нажимаем Create repository. Теперь вы должны увидеть примерно такую страницу:

В самом вверху показан путь к git репозиторию, который мы только что создали. К нему можно получить доступ по https или ssh.

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

ssh – удобен тем, что можно использовать ssh ключи для авторизации

Дальше дается два варианта первого использования репозитория. Если вы локально ничего не создавали, то можно выполнить следующие команды:

echo "# demo" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/mflenov/demo.git
git push -u origin main

Мы все это уже рассматривали на первом уроке. Здесь:

- создается текстовый файл

- инициализируется новый локальный репозиторий

- добавлется текстовый файл

- создается первый комит, чтобы посадить дерево

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

Итак, давайте рассмотрим три новых команды, которые мы еще не видели до этого:

git branch -M main

Команда git branch без параметров отображает все ветки, которые у вас созданы локально. Если вы повторяли все, что мы делали в прошлый раз, то вы должны увидеть три ветки:

  demo/initialwork
* master
  secondone

Звездочкой помечена та ветка, которая выбрана сейчас.

Команда git branch с ключами позволяет выполнять различные операции над ветками. Ключ -M позволяет переименовать ветку. После последних протестов в США слово master стало не очень популярным и его привязали к расизму, поэтому git решил рекомендовать всем переименовывать master в main. Пока это делают по желанию.

По мере знакомства с git я покажу еще некоторые опции git branch.

Следующее, что мы должны сделать – связать наш репозиторий с удаленным. Для этого нам предлагают выполнить команду:

git remote add origin https://github.com/mflenov/demo.git

Вот ее выполняем. Здесь мы говорим, что мы хотим выполнить операцию git remote и выбрали опцию add добавить origin и путь. Что такое origin. Это как бы ссылка на удаленный репозиторий. Не совсем точно определение, но пока воспринимайте это так, чуть позже вы увидите разницу.

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

cat .git/config

В результате вы должны увидеть что-то типа:

[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
	ignorecase = true
	precomposeunicode = true
[remote "origin"]
	url = https://github.com/mflenov/demo.git
	fetch = +refs/heads/*:refs/remotes/origin/*

У нас появились три новых строки, они в самом конце. Появился новый раздел [remote "origin"], в котором уже две реально важные конфигурационные строки. Первая из них - url, это как раз url, который мы добавили и который мы видели в github.

Потом идет параметр fetch. Он говорит, какие именно ветки будут копироваться локально. Это очень интересный параметр, который стоит рассмотреть чуть позже, когда мы познакомимся с самой командой fetch.

И последняя команда, которую нам предложи выполнить github – это:

git push -u origin main

Если вы не переименовывали master в main, то команда будет выглядит по-другому:

git push origin master

Здесь мы говорим

- git push толкнуть изменения

- origin это ссылка на то, то мы создаем “удаленный”

- master имя бренча

В результате в консоли вы должны увидеть что-то типа:

Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 4 threads
Compressing objects: 100% (7/7), done.
Writing objects: 100% (9/9), 794 bytes | 397.00 KiB/s, done.
Total 9 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), done.
To https://github.com/mflenov/demo.git
 * [new branch]      master -> master

Здесь немного статистики о том, что на сервер отправились объекты и в последней строке показывается, что создан новый бренч master. Как так? Почему новый бренч? Мы же ничего не создавали? Давайте выполним git branch чтобы отобразить все ветки:

  demo/initialwork
* master
  secondone

Ничего нового нет. Дело в том, что эта команда отображает только локальные, а создан был удаленный. Чтобы увидеть удаленные нужно указать ключ -a:

git branch -a

В результате будут все ветки, локальные и удаленные:

  demo/initialwork
  master
* secondone
  remotes/origin/master

И вот она в последней строке новая ветка remotes/origin/master. Именно ее мы и создали. Когда вы будете работать с удаленными ветками то слово remotes указывать не нужно. Для работы с удаленным мастером указываем просто origin/master.

Чтобы отправить на сервер ветку secondone можно выполнить команду

git push origin secondone

Эту команду выполнять не обязательно, но можете выполнить. Переключаться на ветку, которую вы отправляете на сервер тоже не нужно, можно находиться на ветке master, отправлять на сервер secondone. Не делайте пока этого, мы чуть позже отправим эту ветку на сервер.

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

git clone https://github.com/mflenov/demo.git demo2

Здесь мы говорим, что нужно создать клон репозитория https://github.com/mflenov/demo.git в папке demo2.

Второй вариант – создаем новую папку demo3, переходим в эту папку и в ней инициализируем новый репозиторий и добавляем ссылку на наш удаленный путь:

git init
git remote add origin https://github.com/mflenov/demo.git

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

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

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

demo – в ней я изначально создавал ветку

demo2 – ее я создал клонированием

demo3 – ее я создал через init и добавлением remote

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

git checkout secondone

Измените index.html файл. Я добавил одну строку <p>Copyright: Flenov</p> в конец блока body просто чтобы появились какие-то изменения.

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

git diff

Если выполнить команду без параметров, то будут отображены изменения во всех файлах. Если вы изменили 100 файлов, то конечно же все их видеть одновременно может быть проблематично. Можно посмотреть изменения файлов по-отдельности, указав имя файла в качестве параметра:

git diff index.html

Оба варианта показаны на следующем скриншоте:

Теперь, когда мы убедились в том, что мы видим именно те изменения, которые и хотели видеть, добавляем изменения в коммит, коммитим и отправляем на сервер:

git add -u
git commit -m 'learning how to work together'
git push origin secondone

В этот момент изменения есть в ветке secondone папки, где мы их сделали и на сервере github, потому что туда мы их отправили.

Перейдите в папку demo2 и выполните команду:

git branch -a

Как я уже говорил, эта команда должна выводить все ветки – локальные и удаленные и у меня на экран вот такой результат:

* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master

Здесь нет никакого secondone. Это потому, что в этй папку мы его еще не выкачали с сервера. Чтобы сделать это нужно выполнить команду:

git fetch

Есть еще команду git pull, но я ее не люблю, потому что она не только вытаскивает ветки с сервера, но и обновляет текущую последними изменениями. Я это не люблю, я делаю обновления только тогда, когда мне это нужно.

Если вы находитесь на ветке master, то git pull в реальности будет

git fetch
git merge origin/master

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

Выполните команду git fetch. В результате вы должны увидеть:

remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://github.com/mflenov/demo
 * [new branch]      secondone  -> origin/secondone

Очень много статистики различной, но в самой последней строке самое интересное – найдена новая ветка decondone и локально сделана копия origin/secondone.

И снова git branch -a, среди уже известных нам веток должна появиться:

remotes/origin/secondone

Что мы можем сделать с этой веткой? Вы можете смерджить ее с мастером. На demo компьютере в папке demo мы сделали изменения, а на demo2 компьютере мы получили их и можем смерджить с мастером. Чтобы это сделать можно выполнить:

git checkout master
git merge origin/secondone

Можно, но не будем пока это делать.

Допустим, что у нас два программиста работают над одной задачей и второй программист хочет еще что-то изменить. Он не может работать с удаленной версией ветки, он должен сделать локальную свою версию:

git checkout -b secondone origin/secondone

У нас нет локальной ветки, мы ее создаем, поэтому checkout с ключом b. Имя ветки secondone и будет не master, а удаленная ветка origin/secondone. В результате будет:

ranch 'secondone' set up to track remote branch 'secondone' from 'origin'.
Switched to a new branch 'secondone'

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

Давайте теперь на demo2 сделаем какие-нибудь изменения. Измените файл, добавьте его в коммит и отправьте изменения на сервер:

git add -u 

или

git add index.html
git push origin secondone

Возвращаемся на сервер demo в папку demo. Здесь выполняем git fetch, чтобы забрать изменения. Здесь у нас secondone уже существует, поэтому его создавать не нужно. Если вы все еще находитесь на этой ветке, то просто нужно слить удаленные изменения с нашей текущей веткой:

git fetch
git merge origin/secondone

Первой командой забрали удаленные изменения, второй командой слили удаленные origin/secondone изменения в локальную ветку. Убедитесь, что изменения, которые мы сделали в другой папке были принесены.

Получается, что на сервере у нас находится ветка origin/secondone и у двух программистов созданы свои локальные версии secondone. Каждый пишет изменения в свою локальную версию, коммитит и отправляет на сервер.

Как видите, удаленная ветка и локальная может отличаться. Если удаленно кто-то изменил ветку master, и вы забрали изменения с сервера, то ваша локальная может отличаться от удаленной, если вы не сольете удаленные изменения. Помните, что я говорил в прошлый раз, что нужно всегда создавать ветку от удаленного master, если он существует. Вот это как раз и причина для этого.

Допустим, что вы находитесь на ветке secondone и пока вы работали над кодом, кто-то изменил master и вы хотите принести эти изменения из мастера к себе локально. Без проблем:

git checkout secondone
git merge origin/master

В первой строке я просто убеждаюсь, что нахожусь на ветке secondone и во второй сливаю изменения из мастера в эту ветку. Причем удаленные изменения с origin.

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

Давайте изменим index.html файл, я поменял Welcome на Htllo:

-               <p>Welcome to my website</p>
+               <p>Htllo to my website</p>

Ты скажешь – здесь же опечатка! Да, это сделано намеренно. Добавляем изменения и коммитим:

git add -u 
git commit -m 'Change Welcome to hallo'
git push origin secondone

Переходим в папку demo2 и здесь меняем этот же файл, но только в этот раз пишем меняем на Hallo:

-               <p>Welcome to my website</p>
+               <p>Hallo to my website</p>

Сохраняем изменения и здесь:

git add -u 
git commit -m 'Change Welcome to hallo'
git push origin secondone

Но здесь у нас не получиться отправить, команда push закончится ошибкой:

! [rejected]        secondone -> secondone (fetch first)
error: failed to push some refs to 'https://github.com/mflenov/demo.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Обратите внимание на первую строку – тут написано rejected (отказано) и в скобках в конце строки можно увидеть fetch first.

Если на сервере есть изменения, которых нет локально, вы не можете отправлять новые изменения на сервер. Мы действительно должны выполнить команду git fetch:

remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://github.com/mflenov/demo
   7246e08..98c72f9  secondone  -> origin/secondone

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

git merge origin/secondone

В результате мы должны увидеть что-то типа:

Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

Во второй строке нам говорят, что у нас есть конфликт на файле index.html. Выполняем git status, чтобы посмотреть текущее состояние:

On branch secondone
Your branch and 'origin/secondone' 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:   index.html

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

У нас в статусе появилась новая секция – unmerged paths и здесь сейчас один файл index.html, в котором найден конфликт. Откройте файл в любом редакторе, кроме VS Code, чтобы вы могли увидеть все в чистом виде. Мы потом посмотрим на VS Code.

<html>
        <head>
                <title>Welcome</title>
        </head>
        <body>
                <h1>Home page </h1>
<<<<<<< HEAD
                <p>Hallo to my website</p>
=======
                <p>Htllo to my website</p>
>>>>>>> origin/secondone
                <p>Copyright: Flenov</p>
        </body>
</html>

Как видите у нас появились очень интересные символы, которые подсвечивают конфликтующие строки. Между <<<< и символом ==== находятся изменения, сделанные локально. Между ==== и >>>> находятся изменения, которые сделаны удаленно. Сливание – вы должны выбрать тот вариант, который вы считаете верным и убрать строки <<<< === и >>>>>.

Логично было бы оставить только:

<html>
        <head>
                <title>Welcome</title>
        </head>
        <body>
                <h1>Home page </h1>
                <p>Hallo to my website</p>
                <p>Copyright: Flenov</p>
        </body>
</html>

Сохраняем изменения и можем сохранять их:

git add index.html
git commit -m 'Merge master and Fix conflicts'
git push

На этот раз эти изменения должны будут уйти на сервер без ошибок, но не торопитесь выполнять эти команды, мы их выполним чуть позже. Сначала посмотрим, какие еще способы есть. Выполните следующую команду, чтобы отменить процесс слияния:

git merge --abort

Теперь снова начинаем слияние:

git merge origin/secondone

В результате мы снова должны увидеть конфликт.

Работать с текстовым редактором можно, но есть способ удобнее. Давайте откроем index.html файл в Visual Studio Code.

VS Code удобней тем, что он красиво подсвечивает измененные строки и перед подсвечиванием есть команды:

Accept Current Change – выбрать изменения, сделанные локально, показаны зеленым

Accept Incoming Change – выбрать удаленные изменения – показаны синим

Accept Both Changes – убрать символы <<< === >>> и оставить оба изменения

Compare Changes – сравнить. Если выбрать эту опцию, то VS покажет изменения друг напротив друга. Очень удобно бывает понять, что именно вы хотите выбрать.

Нам нужны локальные изменения, поэтому нажмите Accept Current Change. Теперь мы снова готовы сохранить результат слияния выполнением трех команд:

git add index.html
git commit -m 'Merge master and Fix conflicts'
git push

И снова не делайте это.

Открывать каждый файл в отдельности может быть нормально, если конфликт произошел только на одном файле. А если сразу у 10 файлов конфликт? Есть способ удобнее, использовать команду git mergetool. Эта команда запускает процесс исправления конфликта и для каждого файла с конфликтами вызывает программу, которая будет просить вас выбрать, какие изменения нужно использовать. Очень круто, но по умолчанию утилита использует программу vimdiff, которая… Ну это vimdiff.

Если вы работаете на macOS, сначала запускаем VS Code и нажимаем Command+Shift+P. Сверху в редакторе появиться командная строка, начните печатать Install и вы должны увидеть команду Install 'code' command in PATH. В Windows этого делать ненужно. Смысл здесь в том, чтобы можно было из терминала запускать VS Code просто напечатав в командной строке code. Попробуйте сделать это сейчас и убедитесь, что теперь это работает.

Второй шаг для macOS и единственный для Windows – открываем в любом текстовом редакторе глобальный .gitconfig файл, который у вас расположен в домашней директории. В Windows это будет C:\Users\USERNAME\.gitconfig, а в macOS и Linux это vi /Users/USERNAME/.gitconfig. В этом файле нужно добавить следующие строки:

[merge]
     tool = vscode
[mergetool "vscode"]
     cmd = code --wait $MERGED
[diff]
     tool = vscode
[difftool "vscode"]
     cmd = code --wait --diff $LOCAL $REMOTE

Вот теперь попробуй выполнить команду:

git mergetool

Должен автоматически запуститься VS Code и подсветить конфликтные места. Вы просто выбираете тот код, который имеет смысл, сохраняете изменения и закрываете файл. Если есть еще файлы с конфликтами, то автоматически появится следующий. Если все конфликты исправлены, то процесс заканчивается, и мы снова попадаем в состояние, когда можно просто добавить файлы в коммит и отправить на сервер.

Вот теперь все готово, можете выполнить эти три команды и на этот раз проблем не должно быть:

git add index.html
git commit -m 'Merge master and Fix conflicts'
git push

Ух. Достаточно большой и подробный сегодня получился документ. Наверно нужно закругляться, потому что слишком много информации за один раз – это тоже плохо. Она просто не отложиться.



Внимание!!! Если ты копируешь эту статью себе на сайт, то оставляй ссылку непосредственно на эту страницу. Спасибо за понимание

Комментарии

Паника, что-то случилось!!! Ничего не найдено в комментариях. Срочно нужно что-то добавить, чтобы это место не оставалось пустым.

Добавить Комментарий

О блоге

Программист, автор нескольких книг серии глазами хакера и просто блогер. Интересуюсь безопасностью, хотя хакером себя не считаю

Обратная связь

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

Пишите мне