Как исправить конфликты при git pull

You can resolve merge conflicts using the command line and a text editor.

Merge conflicts occur when competing changes are made to the same line of a file, or when one person edits a file and another person deletes the same file. For more information, see «About merge conflicts.»

Tip: You can use the conflict editor on GitHub to resolve competing line change merge conflicts between branches that are part of a pull request. For more information, see «Resolving a merge conflict on GitHub.»

Competing line change merge conflicts

To resolve a merge conflict caused by competing line changes, you must choose which changes to incorporate from the different branches in a new commit.

For example, if you and another person both edited the file styleguide.md on the same lines in different branches of the same Git repository, you’ll get a merge conflict error when you try to merge these branches. You must resolve this merge conflict with a new commit before you can merge these branches.

  1. Open TerminalTerminalGit Bash.

  2. Navigate into the local Git repository that has the merge conflict.

    cd REPOSITORY-NAME
  3. Generate a list of the files affected by the merge conflict. In this example, the file styleguide.md has a merge conflict.

    $ git status
    > # On branch branch-b
    > # You have unmerged paths.
    > #   (fix conflicts and run "git commit")
    > #
    > # Unmerged paths:
    > #   (use "git add ..." to mark resolution)
    > #
    > # both modified:      styleguide.md
    > #
    > no changes added to commit (use "git add" and/or "git commit -a")
  4. Open your favorite text editor, such as Visual Studio Code, and navigate to the file that has merge conflicts.

  5. To see the beginning of the merge conflict in your file, search the file for the conflict marker <<<<<<<. When you open the file in your text editor, you’ll see the changes from the HEAD or base branch after the line <<<<<<< HEAD. Next, you’ll see =======, which divides your changes from the changes in the other branch, followed by >>>>>>> BRANCH-NAME. In this example, one person wrote «open an issue» in the base or HEAD branch and another person wrote «ask your question in IRC» in the compare branch or branch-a.

    If you have questions, please
    <<<<<<< HEAD
    open an issue
    =======
    ask your question in IRC.
    >>>>>>> branch-a
    
  6. Decide if you want to keep only your branch’s changes, keep only the other branch’s changes, or make a brand new change, which may incorporate changes from both branches. Delete the conflict markers <<<<<<<, =======, >>>>>>> and make the changes you want in the final merge. In this example, both changes are incorporated into the final merge:

    If you have questions, please open an issue or ask in our IRC channel if it's more urgent.
  7. Add or stage your changes.

    $ git add .
  8. Commit your changes with a comment.

    $ git commit -m "Resolved merge conflict by incorporating both suggestions."

You can now merge the branches on the command line or push your changes to your remote repository on GitHub and merge your changes in a pull request.

Removed file merge conflicts

To resolve a merge conflict caused by competing changes to a file, where a person deletes a file in one branch and another person edits the same file, you must choose whether to delete or keep the removed file in a new commit.

For example, if you edited a file, such as README.md, and another person removed the same file in another branch in the same Git repository, you’ll get a merge conflict error when you try to merge these branches. You must resolve this merge conflict with a new commit before you can merge these branches.

  1. Open TerminalTerminalGit Bash.

  2. Navigate into the local Git repository that has the merge conflict.

    cd REPOSITORY-NAME
  3. Generate a list of the files affected by the merge conflict. In this example, the file README.md has a merge conflict.

    $ git status
    > # On branch main
    > # Your branch and 'origin/main' have diverged,
    > # and have 1 and 2 different commits each, respectively.
    > #  (use "git pull" to merge the remote branch into yours)
    > # You have unmerged paths.
    > #  (fix conflicts and run "git commit")
    > #
    > # Unmerged paths:
    > #  (use "git add/rm ..." as appropriate to mark resolution)
    > #
    > #	deleted by us:   README.md
    > #
    > # no changes added to commit (use "git add" and/or "git commit -a")
  4. Open your favorite text editor, such as Visual Studio Code, and navigate to the file that has merge conflicts.

  5. Decide if you want keep the removed file. You may want to view the latest changes made to the removed file in your text editor.

    To add the removed file back to your repository:

    $ git add README.md

    To remove this file from your repository:

    $ git rm README.md
     > README.md: needs merge
     > rm 'README.md'
  6. Commit your changes with a comment.

    $ git commit -m "Resolved merge conflict by keeping README.md file."
    > [branch-d 6f89e49] Merge branch 'branch-c' into branch-d

You can now merge the branches on the command line or push your changes to your remote repository on GitHub and merge your changes in a pull request.

Further reading

  • «About merge conflicts»
  • «Checking out pull requests locally»

I have to solve some conflict after a git pull.

$ git pull
CONFLICT (rename/add): Renamed vignette_generator_mashed.h->vision_problem_8.h in 49423dd0d47abe6d839a783b5517bdfd200a202f. vision_problem_8.h added in HEAD
Added as vision_problem_8.h~HEAD_1 instead
Removed vignette_generator_cross_square.cc
Automatic merge failed; fix conflicts and then commit the result.

So I googled it a bit, and found people saying using git mergetool. But here is what I got:

$ git mergetool
merge tool candidates: meld kdiff3 tkdiff xxdiff meld gvimdiff emerge opendiff emerge vimdiff
No files need merging
$ git mergetool opendiff
merge tool candidates: meld kdiff3 tkdiff xxdiff meld gvimdiff emerge opendiff emerge vimdiff
opendiff: file not found

So does it mean I have to install something?

What if I simply want the version from git pull to overwrite everything?

Léo Léopold Hertz 준영's user avatar

asked Sep 16, 2009 at 22:05

Tim's user avatar

2

You don’t need mergetool for this. It can be resolved pretty easily manually.

Your conflict is that your local commits added a file, vision_problem_8.h, that a remote commit also created, by a rename from vignette_generator_mashed.h. If you run ls -l vision_problem_8.h* you will probably see multiple versions of this file that git has preserved for you. One of them will be yours, another of them will be the remote version. You can use an editor or whatever tools you like to resolve the conflicting contents. When you’re done, git add the affected files and commit to complete the merge.

If you just want to use the remote commit’s version, then you can just move the copy that you didn’t write into place and git add it.


Regarding the merge tools, have a look at git help mergetool. Basically, it’s going to try running each of the included possibilities until it finds one, or use one you have explicitly configured.

answered Sep 16, 2009 at 22:18

Phil Miller's user avatar

Phil MillerPhil Miller

35.7k11 gold badges66 silver badges88 bronze badges

2

I think you just forgot «-t» switch at your command line. According git help page it stands for «-t , —tool=» so it makes what you intended to.

Try:

git mergetool -t gvimdiff

Of course you may use your prefered merge tool instead of mine gvimdiff, meld is great too…

answered Oct 8, 2012 at 19:20

mano2a0c40's user avatar

mano2a0c40mano2a0c40

731 silver badge4 bronze badges

2

If you run your merge from a subdirectory of your project, git will run the merge for your whole project. However, mergetool can only see (and merge) files in or below the working directory. So, if this scenario occurs, make sure you are trying to run your conflict resolution from the top-level directory in your project.

answered Feb 16, 2010 at 18:52

Bluejack's user avatar

BluejackBluejack

511 silver badge1 bronze badge

What if I simply want the version from git pull to overwrite everything?
If you want just that you should use:

 git fetch
 git reset --hard origin/your-branch-name

answered Mar 23, 2018 at 10:36

saferJo's user avatar

saferJosaferJo

4875 silver badges15 bronze badges

1

If you have a proper branching strategy — aka features are merged into develop , just before merge rebased etc. , most often when you want to run a git pull on a branch you basically want to get all the new stuff from the git server, and late on apply your own stuff , which is git lingua franca is something like this:

# put all my changes on the stash 
git stash 

# fully reset the current branch to the state it is on the server
git clean -d -x -f ; git reset HEAD --hard ; git pull --force

# apply your own changes 
git stash pop

You could also instead of stash put you current state into a tmp branch … but eventually if there are conflicts you would have to manually resolve them …

answered Nov 9, 2020 at 17:37

Yordan Georgiev's user avatar

Yordan GeorgievYordan Georgiev

4,8681 gold badge52 silver badges53 bronze badges

Системы контроля версий предназначены для управления дополнениями, вносимыми в проект множеством распределенных авторов (обычно разработчиков). Иногда один и тот же контент могут редактировать сразу несколько разработчиков. Если разработчик A попытается изменить код, который редактирует разработчик B, может произойти конфликт. Для предотвращения конфликтов разработчики работают в отдельных изолированных ветках. Основная задача команды git merge заключается в слиянии отдельных веток и разрешении любых конфликтующих правок.

Общие сведения о конфликтах слияния

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

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

Типы конфликтов слияния

Конфликт во время слияния может произойти в двух отдельных точках — при запуске и во время процесса слияния. Далее рассмотрим, как разрешать каждый из этих конфликтных сценариев.

Git прерывает работу в самом начале слияния

Выполнение команды слияния прерывается в самом начале, если Git обнаруживает изменения в рабочем каталоге или разделе проиндексированных файлов текущего проекта. Git не может выполнить слияние, поскольку иначе эти ожидающие изменения будут перезаписаны новыми коммитами. Такое случается из-за конфликтов не с другими разработчиками, а с ожидающими локальными изменениями. Локальное состояние необходимо стабилизировать с помощью команд git stash, git checkout, git commit или git reset. Если команда слияния прерывается в самом начале, выдается следующее сообщение об ошибке:

error: Entry '<fileName>' not uptodate. Cannot merge. (Changes in working directory)

Git прерывает работу во время слияния

Сбой В ПРОЦЕССЕ слияния говорит о наличии конфликта между текущей локальной веткой и веткой, с которой выполняется слияние. Это свидетельствует о конфликте с кодом другого разработчика. Git сделает все возможное, чтобы объединить файлы, но оставит конфликтующие участки, чтобы вы разрешили их вручную. При сбое во время выполнения слияния выдается следующее сообщение об ошибке:

error: Entry '<fileName>' would be overwritten by merge. Cannot merge. (Changes in staging area)

Создание конфликта слияния

Чтобы лучше разобраться в конфликтах слияния, в следующем разделе мы смоделируем конфликт для дальнейшего изучения и разрешения. Для запуска моделируемого примера будет использоваться интерфейс Git c Unix-подобной командной строкой.

$ mkdir git-merge-test
$ cd git-merge-test
$ git init .
$ echo "this is some content to mess with" > merge.txt
$ git add merge.txt
$ git commit -am"we are commiting the inital content"
[main (root-commit) d48e74c] we are commiting the inital content
1 file changed, 1 insertion(+)
create mode 100644 merge.txt

С помощью приведенной в этом примере последовательности команд выполняются следующие действия.

  • Создается новый каталог с именем git-merge-test, выполняется переход в этот каталог и инициализация его как нового репозитория Git.
  • Создается новый текстовый файл merge.txt с некоторым содержимым.
  • В репозиторий добавляется файл merge.txt и выполняется коммит.

Теперь у нас есть новый репозиторий с одной веткой main и непустым файлом merge.txt. Далее создадим новую ветку, которая будет использоваться как конфликтующая при слиянии.

$ git checkout -b new_branch_to_merge_later
$ echo "totally different content to merge later" > merge.txt
$ git commit -am"edited the content of merge.txt to cause a conflict"
[new_branch_to_merge_later 6282319] edited the content of merge.txt to cause a conflict
1 file changed, 1 insertion(+), 1 deletion(-)

Представленная выше последовательность команд выполняет следующие действия.

  • Создает новую ветку с именем new_branch_to_merge_later и выполняет переход в нее.
  • Перезаписывает содержимое файла merge.txt.
  • Выполняет коммит нового содержимого.

В этой новой ветке new_branch_to_merge_later мы создали коммит, который переопределил содержимое файла merge.txt.

git checkout main
Switched to branch 'main'
echo "content to append" >> merge.txt
git commit -am"appended content to merge.txt"
[main 24fbe3c] appended content to merge.tx
1 file changed, 1 insertion(+)

Эта последовательность команд выполняет переключение на ветку main, добавляет содержимое в файл merge.txt и делает коммит. После этого в нашем экспериментальном репозитории находятся два новых коммита, первый — в ветке main, а второй — в ветке new_branch_to_merge_later. Теперь запустим команду git merge new_branch_to_merge_later и посмотрим, что из этого выйдет!

$ git merge new_branch_to_merge_later
Auto-merging merge.txt
CONFLICT (content): Merge conflict in merge.txt
Automatic merge failed; fix conflicts and then commit the result.

БАХ! 💥 Возник конфликт. Хорошо, что система Git сообщила нам об этом.

Выявление конфликтов слияния

Как мы убедились на выполняемом примере, Git выводит небольшое описательное сообщение о возникновении КОНФЛИКТА. Чтобы получить более глубокое понимание проблемы, можно запустить команду git status.

$ git status
On branch main
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:   merge.txt

Вывод команды git status говорит о том, что из-за конфликта не удалось слить пути. Теперь файл merge.text отображается как измененный. Давайте изучим этот файл и посмотрим, что изменилось.

$ cat merge.txt
<<<<<<< HEAD
this is some content to mess with
content to append
=======
totally different content to merge later
>>>>>>> new_branch_to_merge_later

Для просмотра содержимого файла merge.txt воспользуемся командой cat. Видно, что в файле появились новые странные дополнения:

  • <<<<<<< HEAD
  • =======
  • >>>>>>> new_branch_to_merge_later

Эти новые строки можно рассматривать как «разделители конфликта». Строка ======= является «центром» конфликта. Все содержимое между этим центром и строкой <<<<<<< HEAD находится в текущей ветке main, на которую ссылается указатель HEAD. А все содержимое между центром и строкой >>>>>>> new_branch_to_merge_later является содержимым ветки для слияния.

Разрешение конфликтов слияния с помощью командной строки

Самый простой способ разрешить конфликт — отредактировать конфликтующий файл. Откройте файл merge.txt в привычном редакторе. В нашем примере просто удалим все разделители конфликта. Измененное содержимое файла merge.txt будет выглядеть следующим образом:

this is some content to mess with
content to append
totally different content to merge later

После редактирования файла выполните команду git add merge.txt, чтобы добавить новое объединенное содержимое в раздел проиндексированных файлов. Для завершения слияния создайте новый коммит, выполнив следующую команду:

git commit -m "merged and resolved the conflict in merge.txt"

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

Команды Git, с помощью которых можно разрешить конфликты слияния

Общие инструменты

Команда status часто используется во время работы с Git и помогает идентифицировать конфликтующие во время слияния файлы.

При передаче аргумента --merge для команды git log будет создан журнал со списком конфликтов коммитов между ветками, для которых выполняется слияние.

Команда diff помогает найти различия между состояниями репозитория/файлов. Она полезна для выявления и предупреждения конфликтов слияния.

Инструменты для случаев, когда Git прерывает работу в самом начале слияния

Команда checkout может использоваться для отмены изменений в файлах или для изменения веток.

Команда reset может использоваться для отмены изменений в рабочем каталоге или в разделе проиндексированных файлов.

Инструменты для случаев, когда конфликты Git возникают во время слияния

При выполнении команды git merge с опцией --abort процесс слияния будет прерван, а ветка вернется к состоянию, в котором она находилась до начала слияния.

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

Резюме

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

Существует множество способов разрешения конфликтов слияния. В этой статье мы рассмотрели немалое количество инструментов командной строки, которые предоставляет Git. Более подробную информацию об этих инструментах см. на отдельных страницах для команд git log, git reset, git status, git checkout и git reset. Помимо этого многие сторонние инструменты также предлагают оптимизированные функции, поддерживающие работу с конфликтами слияния.

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

Однако, иногда всё же будут возникать сложные конфликты.
В отличие от других систем управления версиями, Git не пытается быть слишком умным при разрешении конфликтов слияния.
Философия Git заключается в том, чтобы быть умным, когда слияние разрешается однозначно, но если возникает конфликт, он не пытается сумничать и разрешить его автоматически.
Поэтому, если вы слишком долго откладываете слияние двух быстрорастущих веток, вы можете столкнуться с некоторыми проблемами.

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

Конфликты слияния

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

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

Давайте рассмотрим очень простой пример.
Допустим, у нас есть файл с исходниками на Ruby, выводящими на экран строку ‘hello world’.

#! /usr/bin/env ruby

def hello
  puts 'hello world'
end

hello()

В нашем репозитории, мы создадим новую ветку по имени whitespace и выполним замену всех окончаний строк в стиле Unix на окончания строк в стиле DOS.
Фактически, изменения будут внесены в каждую строку, но изменятся только пробельные символы.
Затем мы заменим строку «hello world» на «hello mundo».

$ git checkout -b whitespace
Switched to a new branch 'whitespace'

$ unix2dos hello.rb
unix2dos: converting file hello.rb to DOS format ...
$ git commit -am 'Convert hello.rb to DOS'
[whitespace 3270f76] Convert hello.rb to DOS
 1 file changed, 7 insertions(+), 7 deletions(-)

$ vim hello.rb
$ git diff -b
diff --git a/hello.rb b/hello.rb
index ac51efd..e85207e 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,7 @@
 #! /usr/bin/env ruby

 def hello
-  puts 'hello world'
+  puts 'hello mundo'^M
 end

 hello()

$ git commit -am 'Use Spanish instead of English'
[whitespace 6d338d2] Use Spanish instead of English
 1 file changed, 1 insertion(+), 1 deletion(-)

Теперь мы переключимся обратно на ветку master и добавим к функции некоторую документацию.

$ git checkout master
Switched to branch 'master'

$ vim hello.rb
$ git diff
diff --git a/hello.rb b/hello.rb
index ac51efd..36c06c8 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
   puts 'hello world'
 end

$ git commit -am 'Add comment documenting the function'
[master bec6336] Add comment documenting the function
 1 file changed, 1 insertion(+)

Теперь мы попытаемся слить в текущую ветку whitespace и в результате получим конфликты, так как изменились пробельные символы.

$ git merge whitespace
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.

Прерывание слияния

В данный момент у нас есть несколько вариантов дальнейших действий.
Во-первых, давайте рассмотрим как выйти из этой ситуации.
Если вы, возможно, не были готовы к конфликтам и на самом деле не хотите связываться с ними, вы можете просто отменить попытку слияния, используя команду git merge --abort.

$ git status -sb
## master
UU hello.rb

$ git merge --abort

$ git status -sb
## master

Эта команда пытается откатить ваше состояние до того, что было до запуска слияния.
Завершиться неудачно она может только в случаях, если перед запуском слияния у вас были не припрятанные или не зафиксированные изменения в рабочем каталоге, во всех остальных случаях всё будет хорошо.

Если по каким-то причинам вы обнаружили себя в ужасном состоянии и хотите просто начать всё сначала, вы можете также выполнить git reset --hard HEAD (либо вместо HEAD указав то, куда вы хотите откатиться).
Но помните, что это откатит все изменения в рабочем каталоге, поэтому удостоверьтесь, что никакие из них вам не нужны.

Игнорирование пробельных символов

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

Стратегии слияния, используемой по умолчанию, можно передать аргументы, и некоторые из них предназначены для соответствующей настройки игнорирования изменений пробельных символов.
Если вы видите, что множество конфликтов слияния вызваны пробельными символами, то вы можете прервать слияние и запустить его снова, но на этот раз с опцией -Xignore-all-space или -Xignore-space-change.
Первая опция игнорирует изменения в любом количестве существующих пробельных символов, вторая игнорирует вообще все изменения пробельных символов.

$ git merge -Xignore-space-change whitespace
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

Поскольку в этом примере реальные изменения файлов не конфликтуют, то при игнорировании изменений пробельных символов всё сольётся хорошо.

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

Ручное слияние файлов

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

То что нам действительно нужно — это перед выполнением самого слияния прогнать сливаемый файл через программу dos2unix.
Как мы будем делать это?

Во-первых, мы перейдём в состояние конфликта слияния.
Затем нам необходимо получить копии нашей версии файла, их версии файла (из ветки, которую мы сливаем) и общей версии (от которой ответвились первые две).
Затем мы исправим либо их версию, либо нашу и повторим слияние только для этого файла.

Получить эти три версии файла, на самом деле, довольно легко.
Git хранит все эти версии в индексе в разных «состояниях», каждое из которых имеет ассоциированный с ним номер.
Состояние 1 — это общий предок, состояние 2 — ваша версия и состояния 3 взято из MERGE_HEAD — версия, которую вы сливаете («их» версия).

Вы можете извлечь копию каждой из этих версий конфликтующего файла с помощью команды git show и специального синтаксиса.

$ git show :1:hello.rb > hello.common.rb
$ git show :2:hello.rb > hello.ours.rb
$ git show :3:hello.rb > hello.theirs.rb

Если вы хотите что-то более суровое, то можете также воспользоваться служебной командой ls-files -u для получения SHA-1 хешей для каждого из этих файлов.

$ git ls-files -u
100755 ac51efdc3df4f4fd328d1a02ad05331d8e2c9111 1	hello.rb
100755 36c06c8752c78d2aff89571132f3bf7841a7b5c3 2	hello.rb
100755 e85207e04dfdd5eb0a1e9febbc67fd837c44a1cd 3	hello.rb

Выражение :1:hello.rb является просто сокращением для поиска такого SHA-1 хеша.

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

$ dos2unix hello.theirs.rb
dos2unix: converting file hello.theirs.rb to Unix format ...

$ git merge-file -p 
    hello.ours.rb hello.common.rb hello.theirs.rb > hello.rb

$ git diff -b
diff --cc hello.rb
index 36c06c8,e85207e..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,8 -1,7 +1,8 @@@
  #! /usr/bin/env ruby

 +# prints out a greeting
  def hello
-   puts 'hello world'
+   puts 'hello mundo'
  end

  hello()

Теперь у нас есть корректно слитый файл.
На самом деле, данный способ лучше, чем использование опции ignore-all-space, так как в его рамках вместо игнорирования изменений пробельных символов перед слиянием выполняется корректное исправление таких изменений.
При слиянии с ignore-all-space мы в результате получим несколько строк с окончаниями в стиле DOS, то есть в одном файле смешаются разные стили окончания строк.

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

Чтобы сравнить результат слияния с тем, что было в вашей ветке до слияния, или другими словами увидеть, что привнесло данное слияние, вы можете выполнить git diff --ours

$ git diff --ours
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index 36c06c8..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -2,7 +2,7 @@

 # prints out a greeting
 def hello
-  puts 'hello world'
+  puts 'hello mundo'
 end

 hello()

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

Если вы хотите узнать чем результат слияния отличается от сливаемой ветки, то можете выполнить команду git diff --theirs.
В этом и следующем примере мы используем опцию -w для того, чтобы не учитывать изменения в пробельных символах, так как мы сравниваем результат с тем, что есть в Git, а не с нашим исправленным файлом hello.theirs.rb.

$ git diff --theirs -b
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index e85207e..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
   puts 'hello mundo'
 end

И, наконец, вы можете узнать как изменился файл по сравнению сразу с обеими ветками с помощью команды git diff --base.

$ git diff --base -b
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index ac51efd..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,8 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
-  puts 'hello world'
+  puts 'hello mundo'
 end

 hello()

В данный момент мы можем использовать команду git clean для того, чтобы удалить не нужные более дополнительные файлы, созданные нами для выполнения слияния.

$ git clean -f
Removing hello.common.rb
Removing hello.ours.rb
Removing hello.theirs.rb

Использование команды checkout в конфликтах

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

Давайте немного изменим пример.
Предположим, что у нас есть две долгоживущих ветки, каждая из которых имеет несколько коммитов, что при слиянии приводит к справедливому конфликту.

$ git log --graph --oneline --decorate --all
* f1270f7 (HEAD, master) Update README
* 9af9d3b Create README
* 694971d Update phrase to 'hola world'
| * e3eb223 (mundo) Add more tests
| * 7cff591 Create initial testing script
| * c3ffff1 Change text to 'hello mundo'
|/
* b7dcc89 Initial hello world code

У нас есть три уникальных коммита, которые присутствуют только в ветке master и три других, которые присутствуют в ветке mundo.
Если мы попытаемся слить ветку mundo, то получим конфликт.

$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.

Мы хотели бы увидеть в чем состоит данный конфликт.
Если мы откроем конфликтующий файл, то увидим нечто подобное:

#! /usr/bin/env ruby

def hello
<<<<<<< HEAD
  puts 'hola world'
=======
  puts 'hello mundo'
>>>>>>> mundo
end

hello()

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

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

Полезным в данном случае инструментом является команда git checkout с опцией --conflict.
Она заново выкачает файл и заменит маркеры конфликта.
Это может быть полезно, если вы хотите восстановить маркеры конфликта и попробовать разрешить его снова.

В качестве значения опции --conflict вы можете указывать diff3 или merge (последнее значение используется по умолчанию).
Если вы укажете diff3, Git будет использовать немного другую версию маркеров конфликта — помимо «нашей» и «их» версий файлов будет также отображена «базовая» версия, и таким образом вы получите больше информации.

$ git checkout --conflict=diff3 hello.rb

После того, как вы выполните эту команду, файл будет выглядеть так:

#! /usr/bin/env ruby

def hello
<<<<<<< ours
  puts 'hola world'
||||||| base
  puts 'hello world'
=======
  puts 'hello mundo'
>>>>>>> theirs
end

hello()

Если вам нравится такой формат вывода, то вы можете использовать его по умолчанию для будущих конфликтов слияния, установив параметру merge.conflictstyle значение diff3.

$ git config --global merge.conflictstyle diff3

Команде git checkout также можно передать опции --ours и --theirs, которые позволяют действительно быстро выбрать одну из версий файлов, не выполняя слияния совсем.

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

История при слиянии

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

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

$ git log --oneline --left-right HEAD...MERGE_HEAD
< f1270f7 Update README
< 9af9d3b Create README
< 694971d Update phrase to 'hola world'
> e3eb223 Add more tests
> 7cff591 Create initial testing script
> c3ffff1 Change text to 'hello mundo'

Это список всех шести коммитов, включённых в слияние, с указанием также ветки разработки, в которой находится каждый из коммитов.

Мы также можем сократить его, попросив предоставить нам более специализированную информацию.
Если мы добавим опцию --merge к команде git log, то она покажет нам только те коммиты, в которых изменялся конфликтующий в данный момент файл.

$ git log --oneline --left-right --merge
< 694971d Update phrase to 'hola world'
> c3ffff1 Change text to 'hello mundo'

Если вы выполните эту команду с опцией -p, то получите только список изменений файла, на котором возник конфликт.
Это может быть действительно полезным для быстрого получения информации, которая необходима, чтобы понять почему что-либо конфликтует и как наиболее правильно это разрешить.

Комбинированный формат изменений

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

Если вы выполните git diff сразу после конфликта слияния, то получите информацию в довольно своеобразном формате.

$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,11 @@@
  #! /usr/bin/env ruby

  def hello
++<<<<<<< HEAD
 +  puts 'hola world'
++=======
+   puts 'hello mundo'
++>>>>>>> mundo
  end

  hello()

Такой формат называется «комбинированным» («Combined Diff»), для каждого различия в нем содержится два раздела с информацией.
В первом разделе отображены различия строки (добавлена она или удалена) между «вашей» веткой и содержимым вашего рабочего каталога, а во втором разделе содержится то же самое, но между «их» веткой и рабочим каталогом.

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

Если мы разрешим конфликт и снова выполним команду git diff, то получим ту же информацию, но в немного более полезном представлении.

$ vim hello.rb
$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end

  hello()

В этом выводе указано, что строка «hola world» при слиянии присутствовала в «нашей» ветке, но отсутствовала в рабочей копии, строка «hello mundo» была в «их» ветке, но не в рабочей копии, и, наконец, «hola mundo» не была ни в одной из сливаемых веток, но сейчас присутствует в рабочей копии.
Это бывает полезно просмотреть перед коммитом разрешения конфликта.

Такую же информацию вы можете получить и после выполнения слияния с помощью команды git log, узнав таким образом как был разрешён конфликт.
Git выводит информацию в таком формате, если вы выполните git show для коммита слияния или вызовете команду git log -p с опцией --cc (без неё данная команда не показывает изменения для коммитов слияния).

$ git log --cc -p -1
commit 14f41939956d80b9e17bb8721354c33f8d5b5a79
Merge: f1270f7 e3eb223
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Sep 19 18:14:49 2014 +0200

    Merge branch 'mundo'

    Conflicts:
        hello.rb

diff --cc hello.rb
index 0399cd5,59727f0..e1d0799
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end

  hello()

Отмена слияний

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

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

Случайный коммит слияния

Рисунок 137. Случайный коммит слияния

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

Исправление ссылок

Если нежелаемый коммит слияния существует только в вашем локальном репозитории, то простейшее и лучшее решение состоит в перемещении веток так, чтобы они указывали туда куда вам нужно.
В большинстве случаев, если вы после случайного git merge выполните команду git reset --hard HEAD~, то указатели веток восстановятся так, что будут выглядеть следующим образом:

История после `git reset --hard HEAD~`

Рисунок 138. История после git reset --hard HEAD~

Мы рассматривали команду reset ранее в Раскрытие тайн reset, поэтому вам должно быть не сложно разобраться с тем, что здесь происходит.
Здесь небольшое напоминание: reset --hard обычно выполняет три шага:

  1. Перемещает ветку, на которую указывает HEAD.
    В данном случае мы хотим переместить master туда, где она была до коммита слияния (C6).

  2. Приводит индекс к такому же виду что и HEAD.

  3. Приводит рабочий каталог к такому же виду, что и индекс.

Недостаток этого подхода состоит в изменении истории, что может привести к проблемам в случае совместно используемого репозитория.
Загляните в Опасности перемещения, чтобы узнать что именно может произойти; кратко говоря, если у других людей уже есть какие-то из изменяемых вами коммитов, вы должны отказаться от использования reset.
Этот подход также не будет работать, если после слияния уже был сделан хотя бы один коммит; перемещение ссылки фактически приведёт к потере этих изменений.

Отмена коммита

Если перемещение указателей ветки вам не подходит, Git предоставляет возможность сделать новый коммит, который откатывает все изменения, сделанные в другом.
Git называет эту операцию «восстановлением» («revert»), в данном примере вы можете вызвать её следующим образом:

$ git revert -m 1 HEAD
[master b1d8379] Revert "Merge branch 'topic'"

Опция -m 1 указывает какой родитель является «основной веткой» и должен быть сохранен.
Когда вы выполняете слияние в HEAD (git merge topic), новый коммит будет иметь двух родителей: первый из них HEAD (C6), а второй — вершина ветки, которую сливают с текущей (C4).
В данном случае, мы хотим отменить все изменения, внесённые слиянием родителя #2 (C4), и сохранить при этом всё содержимое из родителя #1 (C6).

История с коммитом восстановления (отменой коммита слияния) выглядит следующим образом:

История после `git revert -m 1`

Рисунок 139. История после git revert -m 1

Новый коммит ^M имеет точно такое же содержимое как C6, таким образом, начиная с нее всё выглядит так, как будто слияние никогда не выполнялось, за тем лишь исключением, что «теперь уже не слитые» коммиты всё также присутствуют в истории HEAD.
Git придет в замешательство, если вы вновь попытаетесь слить topic в ветку master:

$ git merge topic
Already up-to-date.

В ветке topic нет ничего, что ещё недоступно из ветки master.
Плохо, что в случае добавления новых наработок в topic, при повторении слияния Git добавит только те изменения, которые были сделаны после отмены слияния:

История с плохим слиянием

Рисунок 140. История с плохим слиянием

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

$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic

История после повторения отменённого слияния

Рисунок 141. История после повторения отменённого слияния

В этом примере, M и ^M отменены.
В коммите ^^M, фактически, сливаются изменения из C3 и C4, а в C8 — изменения из C7, таким образом, ветка topic полностью слита.

Другие типы слияний

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

Выбор «нашей» или «их» версий

Во-первых, существует ещё один полезный приём, который мы можем использовать в обычном «рекурсивном» режиме слияния.
Мы уже видели опции ignore-all-space и ignore-space-change, которые передаются с префиксом -X, но мы можем также попросить Git при возникновении конфликта использовать ту или иную версию файлов.

По умолчанию, когда Git при слиянии веток замечает конфликт, он добавляет в код маркеры конфликта, отмечает файл как конфликтующий и позволяет вам разрешить его.
Если же вместо ручного разрешения конфликта вы хотите, чтобы Git просто использовал какую-то определённую версию файла, а другую игнорировал, то вы можете передать команде merge одну из двух опций -Xours или -Xtheirs.

В этом случае Git не будет добавлять маркеры конфликта.
Все неконфликтующие изменения он сольёт, а для конфликтующих он целиком возьмёт ту версию, которую вы указали (это относится и к бинарным файлам).

Если мы вернёмся к примеру «hello world», который использовали раньше, то увидим, что попытка слияния в нашу ветку приведёт к конфликту.

$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Resolved 'hello.rb' using previous resolution.
Automatic merge failed; fix conflicts and then commit the result.

Однако, если мы выполним слияние с опцией -Xours или -Xtheirs, конфликта не будет.

$ git merge -Xours mundo
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 test.sh  | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)
 create mode 100644 test.sh

В этом случае, вместо добавления в файл маркеров конфликта с «hello mundo» в качестве одной версии и с «hola world» в качестве другой, Git просто выберет «hola world».
Однако, все другие неконфликтующие изменения будут слиты успешно.

Такая же опция может быть передана команде git merge-file, которую мы обсуждали ранее, то есть для слияния отдельных файлов можно использовать команду git merge-file --ours.

На случай если вам нужно нечто подобное, но вы хотите, чтобы Git даже не пытался сливать изменения из другой версии, существует более суровый вариант — стратегия слияния «ours».
Важно отметить, что это не то же самое что опция «ours» рекурсивной стратегии слияния.

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

$ git merge -s ours mundo
Merge made by the 'ours' strategy.
$ git diff HEAD HEAD~
$

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

Это часто бывает полезно, когда нужно заставить Git считать, что ветка уже слита, а реальное слияние отложить на потом.
Для примера предположим, что вы создали ветку release и проделали в ней некоторую работу, которую когда-то впоследствии захотите слить обратно в master.
Тем временем в master были сделаны некоторые исправления, которые необходимо перенести также в вашу ветку release.
Вы можете слить ветку с исправлениями в release, а затем выполнить merge -s ours этой ветки в master (хотя исправления в ней уже присутствуют), так что позже, когда вы будете снова сливать ветку release, не возникнет конфликтов, связанных с этими исправлениями.

Слияние поддеревьев

Идея слияния поддеревьев состоит в том, что у вас есть два проекта и один из проектов отображается в подкаталог другого.
Когда вы выполняете слияние поддеревьев, Git в большинстве случаев способен понять, что одно из них является поддеревом другого и выполнить слияние подходящим способом.

Далее мы рассмотрим пример добавления в существующий проект другого проекта и последующее слияние кода второго проекта в подкаталог первого.

Первым делом мы добавим в наш проект приложение Rack.
Мы добавим Rack в наш собственный проект, как удалённый репозиторий, а затем выгрузим его в отдельную ветку.

$ git remote add rack_remote https://github.com/rack/rack
$ git fetch rack_remote --no-tags
warning: no common commits
remote: Counting objects: 3184, done.
remote: Compressing objects: 100% (1465/1465), done.
remote: Total 3184 (delta 1952), reused 2770 (delta 1675)
Receiving objects: 100% (3184/3184), 677.42 KiB | 4 KiB/s, done.
Resolving deltas: 100% (1952/1952), done.
From https://github.com/rack/rack
 * [new branch]      build      -> rack_remote/build
 * [new branch]      master     -> rack_remote/master
 * [new branch]      rack-0.4   -> rack_remote/rack-0.4
 * [new branch]      rack-0.9   -> rack_remote/rack-0.9
$ git checkout -b rack_branch rack_remote/master
Branch rack_branch set up to track remote branch refs/remotes/rack_remote/master.
Switched to a new branch "rack_branch"

Таким образом, теперь у нас в ветке rack_branch находится основная ветка проекта Rack, а в ветке master — наш собственный проект.
Если вы переключитесь сначала на одну ветку, а затем на другую, то увидите, что они имеют абсолютно разное содержимое:

$ ls
AUTHORS         KNOWN-ISSUES   Rakefile      contrib         lib
COPYING         README         bin           example         test
$ git checkout master
Switched to branch "master"
$ ls
README

Может показаться странным, но, на самом деле, ветки в вашем репозитории не обязаны быть ветками одного проекта.
Это мало распространено, так как редко бывает полезным, но иметь ветки, имеющие абсолютно разные истории, довольно легко.

В данном примере, мы хотим выгрузить проект Rack в подкаталог нашего основного проекта.
В Git мы можем выполнить это с помощью команды git read-tree.
Вы узнаете больше о команде read-tree и её друзьях в главе Git изнутри, сейчас же вам достаточно знать, что она считывает содержимое некоторой ветки в ваш текущий индекс и рабочий каталог.
Мы просто переключимся обратно на ветку master и выгрузим ветку rack_branch в подкаталог rack ветки master нашего основного проекта:

$ git read-tree --prefix=rack/ -u rack_branch

Когда мы будем выполнять коммит, он будет выглядеть так, как будто все файлы проекта Rack были добавлены в этот подкаталог — например, мы скопировали их из архива.
Важно отметить, что слить изменения одной из веток в другую довольно легко.
Таким образом, если проект Rack обновился, мы можем получить изменения из его репозитория просто переключившись на соответствующую ветку и выполнив операцию git pull:

$ git checkout rack_branch
$ git pull

Затем мы можем слить эти изменения обратно в нашу ветку master.

Для того, чтобы получить изменения и заполнить сообщение коммита используйте параметр --squash, вместе с опцией -Xsubtree рекурсивной стратегии слияния.
Вообще-то, по умолчанию используется именно рекурсивная стратегия слияния, но мы указали и её тоже для пущей ясности.

$ git checkout master
$ git merge --squash -s recursive -Xsubtree=rack rack_branch
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested

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

Таким образом, слияние поддеревьев даёт нам возможность использовать рабочий процесс в некоторой степени похожий на рабочий процесс с подмодулями, но при этом без использования подмодулей (которые мы рассмотрим в Подмодули).
Мы можем держать ветки с другими связанными проектами в нашем репозитории и периодически сливать их как поддеревья в наш проект.
С одной стороны это удобно, например, тем, что весь код хранится в одном месте.
Однако, при этом есть и некоторые недостатки — поддеревья немного сложнее, проще допустить ошибки при повторной интеграции изменений или случайно отправить ветку не в тот репозиторий.

Другая небольшая странность состоит в том, что для получения различий между содержимым подкаталога rack и содержимого ветки rack_branch — для того, чтобы увидеть необходимо ли выполнять слияния между ними — вы не можете использовать обычную команду diff.
Вместо этого следует выполнить команду git diff-tree, указав ветку, с которой производится сравнение:

$ git diff-tree -p rack_branch

Вот как выглядит процедура сравнения содержимого подкаталога rack с содержимым ветки master на сервере после последнего скачивания изменений:

$ git diff-tree -p rack_remote/master
  1. git pull = git fetch + git merge.
  2. Things went wrong in the second half, the git merge.
  3. For git merge to choose not to do a «fast forward» instead of a real merge, someone (perhaps the developer, perhaps you) has to have done something unusual.
  4. Only a real merge can have merge conflicts,

So, the solution to the problem lies in figuring out the «something unusual» and/or the person involved.

Note that if you are actively making your own changes, the merge conflict is not unusual after all. I’m taking you at your word that you have not done your own git commits.

In any case, the first thing to do (if you don’t want to just re-clone everything from scratch) is to terminate the conflicted merge, so that you’re back to where you would be if you had used git fetch and not also used git merge. (You did use both, via git pull, and it was the merge that failed.) To end the attempt to merge, since you’re in this state where it failed, you can just use:

git merge --abort

Next, it’s probably worth figuring out why git wants to do a merge in the first place. The easiest way to do that is to run these two commands:

git fetch
git log --graph --oneline --decorate --all

The log will show you where your branch—probably named master—ends, and where the «remote» (origin/master) ends. (The git fetch step is just in case you’re using an older git, where git pull runs git fetch in such a way that it skips a step, which will make the git log output misleading.)

If you see something like this:

* f96824d (HEAD, master) some commit message
* d1574b8 (origin/master) another commit message
* b9491ea a third commit message
...

this means you have a commit they don’t.1 If you see:

* 676699a (origin/master) here a commit message
| * 9124325 (HEAD, master) there a commit message
|/
* a19ce3f everywhere a commit message
...

then again, you have a commit they don’t.

If you did not make this (or these) commit(s), they must have retracted some commit(s). You have them because you grabbed them at some point, with an earlier git fetch, and then put them on your master branch with an earlier git merge. But they’ve retracted these commit(s), and then perhaps made more new ones. With your latest git merge you’re asking git to combine their old commit(s) and their new ones, and those are conflicting.

In both cases you can simply force your master to move to the same commit as their origin/master, with:

git reset --hard origin/master

The reset command means «take whatever my current branch is»—in this case, master—»and make it point to the exact same commit as I name here: origin/master«. The --hard means «and also, throw away my current work-tree and replace it with the one attached to the commit I’m moving to now.»


The normal case—normal for your setup anyway—is that they can have commit(s) that you don’t, but every commit you have, they also have. In this case your git merge sees that it can simply «slide your branch up» to match theirs, which is a «fast forward» operation. In cases like that, the git log --all --decorate --graph --oneline (the order of these options does not matter) output looks more like:

* 7cf123a (origin/master) lots of twisty
* b9cce10 little commit messages,
* 009cda1 (HEAD, master) all somewhat alike
...

In this case, just imagine that the labels (HEAD, master) are slid up along the lines connecting the *s. (The actual lines are too small to draw here—there’s just not enough room between lines—but git log --graph --all --decorate will show them: this time we leave out --oneline so there are more lines; as will graphical viewers like gitk. As shown above, in some cases there is some room for lines, which git log draws with | and / characters and such.)


1Actually, if you see this, you should just get an everything up to date from your git merge, and hence no conflict. So I’m expecting something like the 2nd one.

  1. git pull = git fetch + git merge.
  2. Things went wrong in the second half, the git merge.
  3. For git merge to choose not to do a «fast forward» instead of a real merge, someone (perhaps the developer, perhaps you) has to have done something unusual.
  4. Only a real merge can have merge conflicts,

So, the solution to the problem lies in figuring out the «something unusual» and/or the person involved.

Note that if you are actively making your own changes, the merge conflict is not unusual after all. I’m taking you at your word that you have not done your own git commits.

In any case, the first thing to do (if you don’t want to just re-clone everything from scratch) is to terminate the conflicted merge, so that you’re back to where you would be if you had used git fetch and not also used git merge. (You did use both, via git pull, and it was the merge that failed.) To end the attempt to merge, since you’re in this state where it failed, you can just use:

git merge --abort

Next, it’s probably worth figuring out why git wants to do a merge in the first place. The easiest way to do that is to run these two commands:

git fetch
git log --graph --oneline --decorate --all

The log will show you where your branch—probably named master—ends, and where the «remote» (origin/master) ends. (The git fetch step is just in case you’re using an older git, where git pull runs git fetch in such a way that it skips a step, which will make the git log output misleading.)

If you see something like this:

* f96824d (HEAD, master) some commit message
* d1574b8 (origin/master) another commit message
* b9491ea a third commit message
...

this means you have a commit they don’t.1 If you see:

* 676699a (origin/master) here a commit message
| * 9124325 (HEAD, master) there a commit message
|/
* a19ce3f everywhere a commit message
...

then again, you have a commit they don’t.

If you did not make this (or these) commit(s), they must have retracted some commit(s). You have them because you grabbed them at some point, with an earlier git fetch, and then put them on your master branch with an earlier git merge. But they’ve retracted these commit(s), and then perhaps made more new ones. With your latest git merge you’re asking git to combine their old commit(s) and their new ones, and those are conflicting.

In both cases you can simply force your master to move to the same commit as their origin/master, with:

git reset --hard origin/master

The reset command means «take whatever my current branch is»—in this case, master—»and make it point to the exact same commit as I name here: origin/master«. The --hard means «and also, throw away my current work-tree and replace it with the one attached to the commit I’m moving to now.»


The normal case—normal for your setup anyway—is that they can have commit(s) that you don’t, but every commit you have, they also have. In this case your git merge sees that it can simply «slide your branch up» to match theirs, which is a «fast forward» operation. In cases like that, the git log --all --decorate --graph --oneline (the order of these options does not matter) output looks more like:

* 7cf123a (origin/master) lots of twisty
* b9cce10 little commit messages,
* 009cda1 (HEAD, master) all somewhat alike
...

In this case, just imagine that the labels (HEAD, master) are slid up along the lines connecting the *s. (The actual lines are too small to draw here—there’s just not enough room between lines—but git log --graph --all --decorate will show them: this time we leave out --oneline so there are more lines; as will graphical viewers like gitk. As shown above, in some cases there is some room for lines, which git log draws with | and / characters and such.)


1Actually, if you see this, you should just get an everything up to date from your git merge, and hence no conflict. So I’m expecting something like the 2nd one.

Reading Time: 2 minutes

“How to resolve merge conflicts in Git pull request or a PR?”

This question we often ask our friends and colleagues and even Google. It can also look complex and confusing especially if you are new to Git. To resolve your queries, we will talk about it in this article and cover some important questions like “What is a merge conflict?”, “When does a merge conflict occur?”, and “How to resolve a merge conflict?”.

What Is A Merge Conflict?

We all know that Git tracks file changes. Now, let’s try to understand this with an example.

Let’s say you have a feature branch called featureA and you want to merge this feature into your master branch. Now, Git will automatically merge all the “new” pieces of code into your master branch however there will be some cases where it will not be able to do so automatically.

A merge conflict is a scenario where Git is not able to automatically merge changes as it gets confused between two different versions of code for the same file.

If you want to know more about Git, click here to read our previous articles.

So, now that you know what is a merge conflict and when does it occur, let’s see how to fix it!

Follow these steps to resolve merge conflicts in Git pull requests:

  1. We will make sure that code on both the branches is updated with the remote. If not, first take pull of both the branches or push your local changes if any.
  2. Switch to the branch you want to merge using git checkout command.
  3. Try to merge locally like this:
git pull <the parent branch> origin 
  1. You will see an output similar to this:
Auto-merging origin_<file_name>
CONFLICT (content): Merge conflict in origin_<file_name>
Automatic merge failed; fix conflicts and then commit the result.
  1. When you open the conflicting file, you will see something like this:
int i = 10;
<<<<<< HEAD
System.out.println(i);
====== master
System.out.println("Hello!");

In this, git is telling you that the line to print “Hello!” from master branch was over-written with a different print statement in the branch. Now, you have to choose which version you want to keep. In our case, let’s choose our branch’s version.

  1. Manually resolve the conflict by editing the file keeping the content you want , something like this:
int i = 10;
System.out.println(i);
  1. The small editing we just did is considered a “change” in Git. So, now do git add, and when you do git commit , it will show you a commit message that will be autogenerated , which you can modify or just save and now push it , it will automatically update the pr that you have created.
  2. Now, you have a pr which is all ready to be reviewed and merged!

Conclusion

Well, that was it! I hope you understood what is a Git conflict and why does it occur. I am glad I could help you resolve merge conflicts in Git pull requests. You can repeat the process for every file which has a conflict. Do let me know in the comments section if you have any questions and do share this article with your friends and colleagues.



Also published on Medium.

Время прочтения
8 мин

Просмотры 47K

Относительно недавно мне посчастливилось присоединиться к команде разработки Bitbucket Server в Atlassian (до сентября он был известен как Stash). В какой-то момент мне стало любопытно, как этот продукт освещён на Хабре, и к моему удивлению, нашлось лишь несколько заметок о нём, подавляющее большинство которых на сегодняшний день уже устарело.

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

Позвольте начать с перевода статьи Тима Петтерсена «A better pull request» о том, как должен выглядеть pull request, чтобы наиболее эффективно решать возложенную на него задачу.

Небольшое отступление по поводу терминов

В любой русскоязычной технической статье, касающейся систем контроля версий (как, впрочем, большинства любых связанных с IT тем), автор сталкивается с необходимостью использования специфичных терминов, которые могут быть или не быть переведены на русский. В жизни большинство этих терминов не переводится и используется при вербальном общении «как есть», то есть, по сути, транслитерируется. В письменной же форме их, строго говоря, следует переводить, однако в этом случае термины зачастую совершенно перестают быть созвучными англоязычным версиям, что сильно затрудняет их восприятие читателями.

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


Если вы используете Git, то наверняка пользуетесь и pull request-ами. Они в той или иной форме существуют с момента появления распределённых систем управления версиями. До того, как Bitbucket и GitHub предложили удобный веб-интерфейс, pull request мог представлять собой простое письмо от Алисы с просьбой забрать какие-то изменения из её репозитория. Если они были стóящими, вы могли выполнить несколько команд, чтобы влить эти изменения в вашу основную ветку master:

$ git remote add alice git://bitbucket.org/alice/bleak.git
$ git checkout master
$ git pull alice master

Разумеется, включать изменения от Алисы в master не глядя — это далеко не лучшая идея: ведь master содержит код, который вы собираетесь поставлять клиентам, а потому наверняка хотите внимательно следить за тем, что в него попадает. Более правильный путь, чем простое включение изменений в master, — это сначала влить их в отдельную ветку и проанализировать перед тем, как cливать в master:

$ git fetch alice
$ git diff master...alice/master

Приведённая команда git diff с синтаксисом трёх точек (в дальнейшем «triple dot» git diff) покажет изменения между вершиной ветки alice/branch и её merge base — общим предком с локальной веткой master, иначе говоря, с точкой расхождения истории коммитов этих веток. В сущности, это будут ровно те изменения, которые Алиса просит нас включить в основную ветку.


git diff master…alice/master эквивалентен git diff A B

На первый взгляд, это кажется разумным способом проверки изменений pull request-а. Действительно, на момент написания статьи, именно такой алгоритм сравнения применяется в реализации pull request-ов в большинстве инструментов, предоставляющих хостинг git-репозиториев.

Несмотря на это, есть несколько проблем в использовании «triple dot» git diff для анализа изменений pull request-а. В реальном проекте основная ветка, скорее всего, будет сильно отличаться от любой ветки функциональности (в дальнейшем feature-ветка). Работа над задачами ведётся в отдельных ветках, которые по окончании вливаются в master. Когда master продвигается вперёд, простой git diff от вершины feature-ветки до её merge base уже недостаточен для отображения настоящего различия между этими ветками: он покажет разницу вершины feature-ветки лишь с одним из предыдущих состояний master.


Ветка master продвигается за счёт вливания новых изменений. Результат git diff master…alice/master не отражает этих изменений master.

Почему же невозможность увидеть эти изменения в ходе анализа pull request-а является проблемой? Тому есть две причины.

Конфликты слияния (merge conflicts)

С первой проблемой вы наверняка регулярно сталкиваетесь — конфликты слияния. Если в вашей feature-ветке вы измените файл, который в то же время был изменён в master, git diff по-прежнему будет отображать только изменения, сделанные вами в feature-ветке. Однако при попытке выполнить git merge вы столкнётесь с ошибкой: git расставит маркеры конфликтов в файлах вашей рабочей копии, поскольку сливаемые ветки имеют противоречивые изменения, — такие, которые git не в состоянии разрешить даже с помощью продвинутых стратегий слияния.


Конфликт слияния

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

Однако конфликты слияния — это меньшая неприятность, с которой вы можете столкнуться при использовании «triple dot» git diff для pull request-ов, по сравнению с другой проблемой: особый тип логического конфликта будет успешно слит, но сможет внести коварную ошибку в кодовую базу.

Логические конфликты, остающиеся незамеченными во время слияния

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

Это может произойти различными путями, однако самым распространённым является вариант, когда два разработчика случайно замечают и исправляют одну и ту же ошибку в двух разных ветках. Представьте, что приведённый ниже код на javascript вычисляет стоимость авиабилета:

// flat fees and taxes
var customsFee          = 5.5;
var immigrationFee      = 7;
var federalTransportTax = .025;

function calculateAirfare(baseFare) {
    var fare = baseFare;                
    fare += immigrationFee;
    fare *= (1 + federalTransportTax);
    return fare;
}

Здесь содержится очевидная ошибка: автор забыл включить в расчёт таможенный сбор!

Теперь представьте двух разработчиков, Алису и Боба, каждый из которых заметил эту ошибку и исправил её независимо от другого в своей ветке.

Алиса добавила строку для учёта customsFee

перед

immigrationFee:

function calculateAirfare(baseFare) {
    var fare = baseFare;                
+++ fare += customsFee; // Fixed it! Phew. Glad we didn't ship that! - Alice
    fare += immigrationFee;
    fare *= (1 + federalTransportTax);
    return fare;
}

Боб сделал аналогичную правку, однако поместил её

после

immigrationFee:

function calculateAirfare(baseFare) {
    var fare = baseFare;                
    fare += immigrationFee;
+++ fare += customsFee; // Fixed it! Gee, lucky I caught that one. - Bob
    fare *= (1 + federalTransportTax);
    return fare;
}

Поскольку в каждой из этих веток были изменены разные строки кода, слияние обеих с master пройдёт успешно одно за другим. Однако теперь master будет содержать обе добавленные строки, а значит, клиенты будут дважды платить таможенный сбор:

function calculateAirfare(baseFare) {
    var fare = baseFare;                
    fare += customsFee; // Fixed it! Phew. Glad we didn't ship that! - Alice
    fare += immigrationFee;
    fare += customsFee; // Fixed it! Gee, lucky I caught that one. - Bob
    fare *= (1 + federalTransportTax);
    return fare;
}

(Это, разумеется, надуманный пример, однако дублированный код или логика могут вызвать весьма серьёзные проблемы: к примеру, дыру в реализации SSL/TLS в iOS.)

Предположим, что вы сначала слили в master изменения pull request-а Алисы. Вот что показал бы pull request Боба, если бы вы использовали «triple dot» git diff:

function calculateAirfare(baseFare) {
    var fare = baseFare;                
    fare += immigrationFee;
+++ fare += customsFee; // Fixed it! Gee, lucky I caught that one. - Bob
    fare *= (1 + federalTransportTax);
    return fare;
}

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

На самом же деле, при анализе pull request-а вы хотели бы видеть,

как master изменится после слияния

изменений из ветки Боба:

function calculateAirfare(baseFare) {
    var fare = baseFare;                
    fare += customsFee; // Fixed it! Phew. Glad we didn't ship that! - Alice
    fare += immigrationFee;
+++ fare += customsFee; // Fixed it! Gee, lucky I caught that one. - Bob
    fare *= (1 + federalTransportTax);
    return fare;
}

Здесь явно обозначена проблема. Рецензент pull request-а, будем надеяться, заметит дублированную строчку и уведомит Боба о том, что код нужно доработать, и тем самым предотвратит попадание серьёзной ошибки в master и, в конечном счёте, в готовый продукт.

Таким образом мы решили реализовать показ изменений в pull request-ах в Bitbucket. При просмотре pull request-а вы видите,

как на самом деле будет выглядеть результат слияния

(т.е. фактически, результирующий коммит). Чтобы осуществить это, мы производим настоящее слияние веток и показываем разницу между получившимся коммитом и верхушкой целевой ветки pull request-а:


git diff C D, где D — это коммит, получившийся в результате слияния, показывает все различия между двумя ветками

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

  • Pull request на GitHub со сравнением «triple dot» git diff
  • Pull request на GitLab со сравнением «triple dot» git diff
  • Pull request на Bitbucket со сравнением, использующим коммит слияния

Сравнение на основе коммита слияния, используемое в Bitbucket, показывает фактические изменения, которые будут применены, когда вы выполните слияние. Загвоздка в том, что этот алгоритм сложнее в реализации и требует значительно больше ресурсов для выполнения.

Продвижение веток

Во-первых, коммит слияния D на самом деле ещё не существует, а его создание — относительно дорогой процесс. Во-вторых, недостаточно просто создать коммит D и на этом закончить: B и C, родительские коммиты для нашего коммита слияния, могут поменяться в любое время. Мы называем изменение любого из родительских коммитов пересмотром (rescope) pull request-а, поскольку оно, по сути, модифицирует тот набор изменений, который будет применён в результате слияния pull request-а. Если ваш pull request нацелен на нагруженную ветку вроде master, он наверняка пересматривается очень часто.


Коммиты слияния создаются каждый раз, когда любая из веток pull request-а изменяется

Фактически, каждый раз когда кто-то коммитит или сливает pull request в master или feature-ветку, Bitbucket должен создать новый коммит слияния, чтобы показать актуальную разницу между ветками в pull request-е.

Обработка конфликтов слияния

Другая проблема при выполнении слияния для отображения разницы меджу ветками в pull request-е заключается в том, что время от времени вам придётся иметь дело с конфликтами слияния. Поскольку git сервер работает в неинтерактивном режиме, разрешать такие конфликты будет некому. Это ещё больше усложняет задачу, но на деле оказывается преимуществом. В Bitbucket мы действительно включаем маркеры конфликтов в коммит слияния D, а затем помечаем их при отображении разницы между ветками, чтобы явно указать вам на то, что pull request содержит конфликты:


Зелёные строки добавлены, красные — удалены, а жёлтые означают конфликт

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

Несмотря на дополнительную сложность реализации и ресурсоёмкость используемого подхода, я считаю, что выбранный нами в Bitbucket подход предоставляет наиболее точную и практичную разницу между ветками pull request-а.


Автор оригинальной статьи — Тим Петтерсен, участвовал в разработке JIRA, FishEye/Crucible и Stash. С начала 2013 года он рассказывает о процессах разработки, git, непрерывной интеграции и поставке (continuous integration/deployment) и инструментах Atlassian для разработчиков, особенно о Bitbucket. Тим регулярно публикует заметки об этих и других вещах в Twitter под псевдонимом @kannonboy.

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

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

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

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

  • Процесс совместной работы в Git
  • Как слить (смержить) ветку в Git
  • Процедуру пул-реквестов
  • Как обновить локальный репозиторий
  • Получение данных из удаленного репозитория
  • Как разрешать конфликты слияния в Git
  • Процесс работы над новой задачей от начала и до конца (повторение)

Совместная работа в 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 улучшились.

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

Понравилась статья? Поделить с друзьями:

Читайте также:

  • Как исправить конфликт портов
  • Как исправить конфликт драйверов видеокарты
  • Как исправить контрольную сумму биоса
  • Как исправить контроллер заряда аккумулятора телефона
  • Как исправить контрастность монитора

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии