Как отменить merge
Перейти к содержимому

Как отменить merge

  • автор:

Как отменить merge, не потеряв код?

Разработку вел в master. Потом сделал валидацию в ветке validation и gui в ветке gui. Нужно было выложить на гитхаб разными ветками это. А я смерджил в master и validation и gui и только после этого отправил на гитхаб. Можно как то исправить? Мне важно что бы именно на гитхабе они отображались как разные ветки. Поэтому если есть способ на самом сайте их разделить визуально, при этом на компьютере оставив слитыми, то такой способ мне тоже подойдет. Код в этих ветках меня устраивает, и при слиянии не было никаких конфликтов. Хочу разделить, только потому что люди, которые будут изучать проект на гитхабе должны четко понимать, где закончилась ветка master. Т.е. только небольшая часть проекта должна быть в мастер. А у меня получилось, что весь проект в master.

Отслеживать
задан 8 мая 2016 в 5:58
Александр Елизаров Александр Елизаров
2,788 2 2 золотых знака 17 17 серебряных знаков 36 36 бронзовых знаков

3 ответа 3

Сортировка: Сброс на вариант по умолчанию

Во-первых, потерять код в git не так-то легко. Если что, у вас всегда есть git reflog , чтобы восстановить потерянный коммит. А ещё всегда можно сделать бэкап текущей ветки с помощью git branch backup (имя произвольное).

Дальше, нужно откатить ветку master до коммита перед слиянием: git reset —hard commit-name . Подробнее: Как вернуться (откатиться) к более раннему коммиту?

Просто так запушить ветку не получится, т.к. на гитхабе теперь есть коммиты, которых в вашей ветке нет, поэтому не получится автоматический fast-forward merge. Поможет git push -f . Эта команда заменяет содержимое удаленной ветки.

Осталось запушить остальные ветки. Чтобы каждый раз не указывать локальную и удаленную ветку, можете однажды назначить локальной ветке удаленную upstream-ветку:

git push -u origin master git push -u origin validation git push -u origin gui 

Можно запушить все ветки сразу (вообще все в refs/heads/ ):

git push -u --all origin Branch master set up to track remote branch master from origin. Branch validation set up to track remote branch validation from origin. Branch gui set up to track remote branch gui from origin. 

Отмена коммитов — Введение в Git

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

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

  • Забыли добавить в коммит нужные файлы
  • Изменения нужно «откатить», чтобы доработать
  • Изменения больше не актуальны, и их нужно удалить
  • Изменения были сделаны по ошибке, и их нужно отменить

По большей части Git движется «только вперед». Правильный подход при работе с ним — это создавать новое, а не изменять старое.

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

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

Git revert

Самая простая ситуация — отмена изменений. Фактически она сводится к созданию еще одного коммита, который выполняет изменения, противоположные тому коммиту, который отменяется:

Руками создавать подобный коммит довольно сложно, поэтому в Git добавили команду, автоматизирующую откат. Эта команда называется git revert :

# Этой команде нужен идентификатор коммита # Это коммит, которым мы удалили файл PEOPLE.md git revert aa600a43cb164408e4ad87d216bc679d097f1a6c # После этой команды откроется редактор, ожидающий ввода описания коммита # Обычно сообщение revert не меняют, поэтому достаточно просто закрыть редактор [main 65a8ef7] Revert "remove PEOPLE.md" 1 file changed, 1 insertion(+) create mode 100644 PEOPLE.md # В проект вернулся файл PEOPLE.md git log -p commit 65a8ef7fd56c7356dcee35c2d05b4400f4467ca8 Author: tirion Date: Sat Sep 26 15:32:46 2020 -0400 Revert "remove PEOPLE.md" This reverts commit aa600a43cb164408e4ad87d216bc679d097f1a6c. diff --git a/PEOPLE.md b/PEOPLE.md new file mode 100644 index 0000000..4b34ba8 --- /dev/null +++ b/PEOPLE.md @@ -0,0 +1 @@ +Haskell Curry 

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

Команда git reset

Представьте, что вам нужно удалить только что сделанный по ошибке коммит. Конечно, и в этом случае подходит git revert , но так история становится менее читаемой. Если этот коммит сделан был только сейчас и еще не отправлялся на GitHub, то лучше сделать так, как будто бы этого коммита не существовало в принципе.

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

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

Для удаления коммита используется команда git reset :

# Добавляем новый коммит, который мы сразу же удалим echo 'test' >> INFO.md git add INFO.md git commit -m 'update INFO.md' [main 17a77cb] update INFO.md 1 file changed, 1 insertion(+) # Важно, что мы не делаем git push git reset --hard HEAD~ HEAD is now at 65a8ef7 Revert "remove PEOPLE.md" # Если посмотреть `git log`, то последнего коммита там больше нет 

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

Флаг —hard означает полное удаление. Без него git reset отменит коммит, но не удалит его, а поместит все изменения этого коммита в рабочую директорию, так что с ними можно будет продолжить работать.

Флаг HEAD~ означает «один коммит от последнего коммита». Если бы мы хотели удалить два последних коммита, то могли бы написать HEAD~2 :

HEAD (переводится как «голова») — так обозначается последний сделанный коммит. Подробнее эту терминологию мы разберем в уроке, посвященном внутреннему устройству Git.

Если не указывать флаг —hard , то по умолчанию подразумевается флаг —mixed . В таком варианте git reset отправляет изменения последнего коммита в рабочую директорию. Затем их можно исправить или отменить и выполнить новый коммит:

echo 'no code no pain' > README.md git add README.md git commit -m 'update README.md' [main f85e3a6] update README.md 1 file changed, 1 insertion(+) # Теперь откатываем последний коммит git reset HEAD~ Unstaged changes after reset: M README.md git status On branch main Your branch is up to date with 'origin/main'. Changes not staged for commit: (use "git add . " to update what will be committed) (use "git restore . " to discard changes in working directory) modified: README.md 

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

Самостоятельная работа
  1. Выполните все шаги из урока
  2. Измените добавленный текст на No code No pain
  3. Закоммитьте изменения с сообщением update README.md
  4. Залейте изменения на GitHub

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

2.4 Основы Git — Операции отмены

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

Отмена может потребоваться, если вы сделали коммит слишком рано, например, забыв добавить какие-то файлы или комментарий к коммиту. Если вы хотите переделать коммит — внесите необходимые изменения, добавьте их в индекс и сделайте коммит ещё раз, указав параметр —amend :

$ git commit --amend

Эта команда использует область подготовки (индекс) для внесения правок в коммит. Если вы ничего не меняли с момента последнего коммита (например, команда запущена сразу после предыдущего коммита), то снимок состояния останется в точности таким же, а всё что вы сможете изменить — это ваше сообщение к коммиту.

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

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

$ git commit -m 'Initial commit' $ git add forgotten_file $ git commit --amend

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

Примечание

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

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

Отмена индексации файла

Следующие два раздела демонстрируют как работать с индексом и изменениями в рабочем каталоге. Радует, что команда, которой вы определяете состояние этих областей, также подсказывает вам как отменять изменения в них. Например, вы изменили два файла и хотите добавить их в разные коммиты, но случайно выполнили команду git add * и добавили в индекс оба. Как исключить из индекса один из них? Команда git status напомнит вам:

$ git add * $ git status On branch master Changes to be committed: (use "git reset HEAD . " to unstage) renamed: README.md -> README modified: CONTRIBUTING.md

Прямо под текстом «Changes to be committed» говорится: используйте git reset HEAD …​ для исключения из индекса. Давайте последуем этому совету и отменим индексирование файла CONTRIBUTING.md :

$ git reset HEAD CONTRIBUTING.md Unstaged changes after reset: M CONTRIBUTING.md $ git status On branch master Changes to be committed: (use "git reset HEAD . " to unstage) renamed: README.md -> README Changes not staged for commit: (use "git add . " to update what will be committed) (use "git checkout -- . " to discard changes in working directory) modified: CONTRIBUTING.md

Команда выглядит несколько странно, но — работает! Файл CONTRIBUTING.md изменен, но больше не добавлен в индекс.

Примечание

Команда git reset может быть опасной если вызвать её с параметром —hard . В приведённом примере файл не был затронут, следовательно команда относительно безопасна.

На текущий момент этот магический вызов — всё, что вам нужно знать о команде git reset . Мы рассмотрим в деталях что именно делает reset и как с её помощью делать действительно интересные вещи в разделе Раскрытие тайн reset главы 7.

Отмена изменений в файле

Что делать, если вы поняли, что не хотите сохранять свои изменения файла CONTRIBUTING.md ? Как можно просто отменить изменения в нём — вернуть к тому состоянию, которое было в последнем коммите (или к начальному после клонирования, или ещё как-то полученному)? Нам повезло, что git status подсказывает и это тоже.

В выводе команды из последнего примера список изменений выглядит примерно так:

Changes not staged for commit: (use "git add . " to update what will be committed) (use "git checkout -- . " to discard changes in working directory) modified: CONTRIBUTING.md

Здесь явно сказано как отменить существующие изменения. Давайте так и сделаем:

$ git checkout -- CONTRIBUTING.md $ git status On branch master Changes to be committed: (use "git reset HEAD . " to unstage) renamed: README.md -> README

Как видите, откат изменений выполнен.

Важно понимать, что git checkout — — опасная команда. Все локальные изменения в файле пропадут — Git просто заменит его версией из последнего коммита. Ни в коем случае не используйте эту команду, если вы не уверены, что изменения в файле вам не нужны.

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

Помните, всё что попало в коммит почти всегда Git может восстановить. Можно восстановить даже коммиты из веток, которые были удалены, или коммиты, перезаписанные параметром —amend (см. Восстановление данных). Но всё, что не было включено в коммит и потеряно — скорее всего, потеряно навсегда.

Отмена действий с помощью git restore

Git версии 2.23.0 представил новую команду: git restore . По сути, это альтернатива git reset, которую мы только что рассмотрели. Начиная с версии 2.23.0, Git будет использовать git restore вместо git reset для многих операций отмены.

Давайте проследим наши шаги и отменим действия с помощью git restore вместо git reset .

Отмена индексации файла с помощью git restore

В следующих двух разделах показано, как работать с индексом и изменениями рабочей копии с помощью git restore . Приятно то, что команда, которую вы используете для определения состояния этих двух областей, также напоминает вам, как отменить изменения в них. Например, предположим, что вы изменили два файла и хотите зафиксировать их как два отдельных изменения, но случайно набираете git add * и индексируете их оба. Как вы можете убрать из индекса один из двух? Команда git status напоминает вам:

$ git add * $ git status On branch master Changes to be committed: (use "git restore --staged . " to unstage) modified: CONTRIBUTING.md renamed: README.md -> README

Прямо под текстом «Changes to be committed», написано использовать git restore —staged …​ для отмены индексации файла. Итак, давайте воспользуемся этим советом, чтобы убрать из индекса файл CONTRIBUTING.md :

$ git restore --staged CONTRIBUTING.md $ git status On branch master Changes to be committed: (use "git restore --staged . " to unstage) renamed: README.md -> README Changes not staged for commit: (use "git add . " to update what will be committed) (use "git restore . " to discard changes in working directory) modified: CONTRIBUTING.md

Файл CONTRIBUTING.md изменен, но снова не индексирован.

Откат изменённого файла с помощью git restore

Что, если вы поймете, что не хотите сохранять изменения в файле CONTRIBUTING.md ? Как легко его откатить — вернуть обратно к тому, как он выглядел при последнем коммите (или изначально клонирован, или каким-либо образом помещён в рабочий каталог)? К счастью, git status тоже говорит, как это сделать. В выводе последнего примера, неиндексированная область выглядит следующим образом:

Changes not staged for commit: (use "git add . " to update what will be committed) (use "git restore . " to discard changes in working directory) modified: CONTRIBUTING.md

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

$ git restore CONTRIBUTING.md $ git status On branch master Changes to be committed: (use "git restore --staged . " to unstage) renamed: README.md -> README

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

7.8 Инструменты Git — Продвинутое слияние

Обычно выполнять слияния в 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 >>>>>> mundo end hello()

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

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

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

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

$ git checkout --conflict=diff3 hello.rb

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

#! /usr/bin/env ruby def hello >>>>>> 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 ++>>>>>> 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 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

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

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