30 сентября 2013 в 15:52

Тонкости благополучного git-merge

Вступительное слово


Считается, что «киллер фичей» СКВ Git является легковесное ветвление. Я ощутил это преимущество в полной мере, ведь я перешел на Git с SVN, где ветвление было достаточно дорогим процессом: для создания ветки нужно было скопировать весь рабочий каталог. В Git все проще: создание ветки подразумевает лишь создание нового указателя на определенный коммит в папке .git/refs/heads, который является файлом с 40 байтами текста, хешем коммита.

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

Попробуем же разобраться в тонкостях git-merge и приручить эту великую магию.

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

Анатомия команды


Если верить мануалу, команда имеет следующий синтаксис:

git merge [-n] [--stat] [--no-commit] [--squash] [--[no-]edit]
[-s <strategy>] [-X <strategy-option>]
[--[no-]rerere-autoupdate] [-m <msg>] [<commit>...]
git merge <msg> HEAD <commit>...
git merge --abort


По большому счету, в Git есть два вида слияния: перемотка (fast-forward merge) и «истинное» слияние (true merge). Рассмотрим несколько примеров обоих случаев.

«Истинное» слияние (true merge)


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

master: A - B - C - D
                 \
feature:          X - Y


Выполним на ветке master git merge feature:

master: A - B - C - D - (M)
                 \      /
feature:          X - Y


Это наиболее частый паттерн слияния. В данном случае в ветке master создается новый коммит (M), который будет ссылаться на двух родителей: коммит D и коммит Y; а указатель master установится на коммит (M). Таким образом Git будет понимать, какие изменения соответствуют коммиту (M) и какой коммит последний в ветке master. Обычно коммит слияния делается с сообщением вроде «Merge branch 'feature'», но можно определить и свое сообщение коммита с помощью ключа -m.

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

$ git log --oneline
92384bd (M)
bceb5a4 D
5dce5b1 Y
76f13e7 X
d1920dc C
3a5c217 B
844af94 A


А теперь посмотрим информацию о коммите (M):

$ git cat-file -p 92384bd
tree 2b5c78f9086384bd86a2ab9d00c7e41a56f01d04
parent bceb5a4ad88e80467404473b94c3e0758dd8e0be
parent 5dce5b1edef64bd0d4e1039061a77be4d7182678
author Andre <andrey.prokopyuk@gmail.com> 1380475972 +0400
committer Andre <andrey.prokopyuk@gmail.com> 1380475972 +0400

(M)


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

Посмотрим, куда ссылается указатель master:

$ cat .git/refs/heads/master
92384bd77304c09b81dcc4485da165923b96ed5f


Действительно, он теперь передвинут на коммит (M).

Squash и no-commit


Но что делать, если за содержимое ветки feature вас могут побить? К примеру, улучшение было небольшим, и вполне могло уместиться в один логичный коммит, но так вышло, что посреди работы вам было нужно убегать на электричку, а продолжать уже дома? В таком случае есть два выхода: экспорт репозитория с последующим импортом на другой машине, либо (особенно когда до электрички 10 минут, а до вокзала около километра) — сделать push origin feature.

Заливать незаконченные коммиты в основную ветку плохо, и с этим нужно что-то делать. Одним из способов, и, пожалуй самым простым, является опция --squash.

git merge feature --squash объединит изменения всех коммитов ветки feature, перенесет их в ветку master и добавит в индекс. При этом коммит слияния не будет создан, вам нужно будет сделать его вручную.

Такого же поведения без параметра squash можно добиться, передав при слиянии параметр --no-commit.

В случае применения такого слияния коммиты ветки feature не будут включены в нашу историю, но коммит Sq будет содержать все их изменения:

master: A - B - C - D - Sq
                 \ 
feature:          X - Y



Позже, в случае выполнения «классического» git merge feature можно исправить это. Тогда история примет следующий вид:

master: A - B - C - D - Sq - (M)
                 \           /
feature:          X    -    Y



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

Перемотка (fast-forward merge)


Рассмотрим другой случай истории коммитов:

master: A - B - C
                 \
feature:          X - Y


Все как и в прошлый раз, но теперь в ветке master нет коммитов после ответвления. В этом случае происходит слияние fast-forward (перемотка). В этом случае отсутствует коммит слияния, указатель (ветка) master просто устанавливается на коммит Y, туда же указывает и ветка feature:

master, feature: A - B - C - X - Y


Чтобы предотвратить перемотку, можно использовать параметр --no-ff.
В случае, если мы выполним git merge feature --no-ff -m '(M)', мы получим уже такую картину:

master: A - B - C   -  (M)
                 \     /
feature:          X - Y


Если же для нас единственным приемлемым поведением является fast-forward, мы можем указать опцию --ff-only. В этом случае, если к слиянию не применима перемотка, будет выведено сообщение о невозможности совершить слияние. Именно так было бы, если бы мы добавили опцию --ff-only в самом первом примере, где после ответвления feature в ветке master был сделано коммит C.

Можно добавить, что при выполнении git pull origin branch_name применяется как раз что-то вроде --ff-only. То есть, в случае, если при слиянии с веткой origin/branch_name не приемлема перемотка, операция отменяется и выводится сообщении о невозможности выполнения.

Стратегии слияния


У команды git-merge есть интересный параметр, --strategy, стратегия. Git поддерживает следующие стратегии слияния:
  • resolve
  • recursive
  • ours
  • octopus
  • subtree


Стратегия resolve

Стратегия resolve — классическое трехсторонее слияние (three-way merge). Стандартный алгоритм трехстороннего слияния применяется для двух файлов с общим предком. Условно этот алгоритм можно представить в виде следующих шагов:
  1. поиск общего предка,
  2. поиск блоков, изменившихся в обеих версиях относительно общего предка,
  3. записываются блоки, оставшиеся без изменения,
  4. блоки, изменившиеся только в одном из потомков, записываются как измененные,
  5. блоки, изменившиеся в обеих версиях, записываются только если изменения идентичны, в ином случае объявляется конфликт, разрешение которого предоставляется пользователю.

Эта стратегия имеет один недостаток: в качестве общего предка двух веток всегда выбирается наиболее ранний общий коммит. Для случая из нашего первого примера это не страшно, можно смело применять git merge feature -s resolve, и результат будет ожидаемым:

master: A - B - C - D - (M)
                 \      /
feature:          X - Y


Здесь C — общий коммит двух веток, дерево файлов, соответствующее этому коммиту, принимается за общего предка. Анализируются изменения, произведенные в ветках master и feature со времен этого коммита, после чего для коммита (M) создается новая версия дерева файлов в соответствии с пунктами 4 и 5 нашего условного алгоритма.

В каком же случае проявляется недостаток стратегии resolve? Он проявляется в том случае, если для коммита (M) нам пришлось разрешить конфликты, после чего мы продолжили разработку и еще раз хотим выполнить git merge feature -s resolve. В этом случае в качестве общего предка снова будет использован коммит C, и конфликты произойдут снова и будут нуждаться в нашем вмешательстве.

Стратегия recursive

Данная стратегия решает проблемы стратегии resolve. Она так же реализует трехстороннее слияние, но в качестве предка используется не реальный, а «виртуальный» предок, который конструируется по следующему условному алгоритму:
  1. проводится поиск всех кандидатов на общего предка,
  2. по цепочке проводится слияние кандидатов, в результате чего появляется новый «виртуальный» предок, причем более свежие коммиты имеют более высокий приоритет, что позволяет избежать повторного проявления конфликтов.

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

Для иллюстрации этой стратегии позаимствуем пример из статьи Merge recursive strategy из блога «The plasticscm blog»:

Merge recursive

Итак, у нас есть две ветки: main и task001. И так вышло, что наши разработчики знают толк в извращениях: они слили коммит 15 из ветки main с коммитом 12 из ветки task001, а так же коммит 16 с коммитом 11. Когда нам понадобилось слить ветки, оказалось, что поиск реального предка — дело неблагодарное, но стратегия recursive с ее конструированием «виртуального» предка нам поможет. В результате мы получим следующую картину:

Merge recursive

Стратегия recursive имеет множество опций, которые передаются команде git-merge с помощью ключа -X:
  • ours и theirs
    Используются для автоматического разрешения конфликтов. Ours — предпочитать «нашу» версию, версию «dst», theirs — предпочитать «их» версию.
  • renormalize (no-renormalize)
    Предотвращает ложные конфликты при слиянии вариантов с разными типами перевода строк.
  • diff-algorithm=[patience|minimal|histogram|myers], а так же опция patience
    Выбор алгоритма дифференциации файлов.
    Дополнительную информацию об этих опциях можно найти в документации по git-diff. Если кратко, свойства этих алгоритмов следующие:
    default, myers — стандартный, жадный алгоритм. Он используется по умолчанию.
    minimal — производится поиск минимальнейших изменений, что занимает дополнительное время.
    patience — использовать алгоритм «patience diff». О нем можно почитать у автора алгоритма, либо в сокращенном варианте на SO.
    histogram — расширяет алгоритм patience с целью, описанной как «support low-occurrence common elements». Сказать честно, я не смог найти достаточно ясного ответа на вопрос, какие конкретно случаи подразумеваются и буду очень рад, если кто-нибудь поможет найти этот ответ.
  • ignore-space-change, ignore-all-space, ignore-space-at-eol
    Корни этих опций лежат, опять же, в git-diff и относятся к дифференциации файлов при слиянии.
    ignore-space-change — игнорируются различия в количестве пробелов, идущих подряд, а так же пробелы в конце строки,
    ignore-all-space — пробелы абсолютно игнорируются при сравнении,
    ignore-space-at-eol — игнорируются различия в пробелах в конце строки.
  • rename-threshold=<n>
    Данная опция задает порог, по достижении которого файл может считаться не новым, а переименованным файлом, которого git-diff не досчитался. Например, -Xrename-threshold=90% подразумевает, что переименованным считается файл, который содержит от 90% контента некоторого удаленного файла.
  • subtree[=<path>]
    Выполнение рекурсивного слияния с этой опцией будет более продвинутым вариантом стратегии subtree, где алгоритм основывается на предположении, как деревья должны совместиться при слиянии. Вместо этого в этом случае указывается конкретный вариант.


Стратегия octopus

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

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

Стратегия ours

Не следует путать стратегию ours и опцию ours стратегии recursive.

Выполняя git merge -s ours obsolete, вы как бы говорите: я хочу слить истории веток, но проигнорировать все изменения, которые произошли в ветке obsolete. Иногда рекомендуют вместо стратегии ours использовать следующий вариант:

$ git checkout obsolete
$ git merge -s recursive -Xtheirs master


Стратегия ours — более радикальное средство.

Стратегия subtree

Для иллюстрации данной стратегии возьмем пример из главы Слияние поддеревьев книги «Pro Git».

Добавим в наш проект новые удаленный репозиторий, rack:

$ git remote add rack_remote git@github.com:schacon/rack.git
$ git fetch rack_remote
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 git@github.com:schacon/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"



Ясно, что ветки master и rack_branch имеют абсолютно разные рабочие каталоги. Добавим файлы из rack_branch в master с использованием squash, чтобы избежать засорения истории ненужными нам фактами:

$ git checkout master
$ git merge --squash -s subtree --no-commit rack_branch
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested


Теперь файлы проекта rack у нас в рабочем каталоге.

Заключительное слово


Итак, я собрал вместе все знания, которые я получил за время работы с Git относительно благополучного git-merge. Я буду рад, если кому-то это поможет, но так же я буду рад, если кто-то поможет мне дополнить материал или исправить неточности и ошибки, если вдруг я допустил такие.
Андрей Прокопюк @Andre_487
карма
73,0
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (46)

  • +6
    «SVN, где ветвление было достаточно дорогим процессом: для создания ветки нужно было скопировать весь рабочий каталог»
    Вовсе не обязательно. SVN управляет отдельными файлами и папками, можно ветвить конкретный фолдер — никто не запрещает. Хотя конечно это не особо нужно.

    «В Git все проще: создание ветки подразумевает лишь создание нового указателя на определенный коммит в папке»
    В SVN ничем не хуже:
    svnbook.red-bean.com/en/1.7/svn.branchmerge.using.html
    «Copying a directory on the server, however, is a constant-time operation»
    «Subversion's repository has a special design. When you copy a directory, you don't need to worry about the repository growing huge—Subversion doesn't actually duplicate any data. Instead, it creates a new directory entry that points to an existing tree»
    • 0
      Может быть, мое знакомство с SVN закончилось достаточно рано для постижения некоторых истин, я никогда не считал себя знатоком этой системы, но если я усвоил правильно, тут именно что «on the server». При создании ветки на рабочем компьютере, выполняя копирование всего trunk'а или нужной его части в папку branches. Или я ошибаюсь?
      • +2
        > именно что «on the server».
        Так и что же? В SVN-е все по сути on the server, никто локально репозитарий не держит. Локально у вас working copy.

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

        Обычно вы говорите серверу сделать копию фолдера (например /trunk в /branches/myBranch), а потом через switch (про который написали ниже) переключаете свой working copy с /trunk на /branches/myBranch. Все просто.
        • 0
          Это ясно, что в SVN практически все происходит в центральном репозитории, но тут вопрос другой: какова нагрузка на файловую систему рабочего компьютера пользователя при создании им ветки. В Git создается файл размером 40 байт, который содержит ссылку на коммит, а в SVN все-таки нужно создать в branches копию файлов, над которыми предполагается работа.

          То есть, при создании /branches/myBranch, файлы туда все-таки должны загрузиться, не пропадая из папки trunk. То есть, копии будут созданы.
          • +4
            > нужно создать в branches копию файлов, над которыми предполагается работа
            Да нет же, не нужно! Я же скинул вам цитату!

            Локально вы просто не чекаутите branches, а держите все в одном working copy.

            На сервере «Subversion doesn't actually duplicate any data. Instead, it creates a new directory entry that points to an existing tree», и тречит свои диффы от этого места.

            > файлы туда все-таки должны загрузиться
            Не должно ничего никуда «загружаться» — опять же см. цитату из документации.
          • 0
            В svn все ветки — это обычные папки относительно корня репозитория и при создании ветки, грубо говоря, делается просто svn copy в папку /branches. Разумеется, при этом никакие файлы никуда физически не копируются и далее как обычно просто инкрементальные изменения от этого локального среза этой конкретной версии файла.
            • 0
              Я все это написал еще в первом своем комментарии. К чему капитанство?

              Речь о том что если человек зачекаутил фолдер trunk, а потом сделал switch на branches/какойТоБренч, то никаких дополнительных расходов в локальной ФС не будет (ведь он сам писал — «вопрос другой: какова нагрузка на файловую систему рабочего компьютера»).

              Но сверх того, и на сервере не будет никакого копирования, а именно инкрементальные изменения от этого среза, как вы и пишете сами.
              • 0
                Ну я видел первый камент, мне тоже не очень понятно что тут ещё может быть непонятно. Думал, может, более простыми словами да по-русски пояснить.

                з.ы. а про локальные копии — в общем-то, да, всё верно. Но тут и в git так же по сути работает в локальной части то.
              • 0
                Вероятно, я чего-то не понимаю, но в моем представлении работа с ветками в SVN происходит следующим образом. У нас на рабочем компьютере есть репозиторий с папками trunk, branches, tags — копия центрального репозитория. Для создания ветки выполняем:

                svn copy trunk branches/myBranch
                Результатом будет полное рекурсивное копирование папки trunk в папку branches/myBranch. И если trunk большой, копирование будет долгим.

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

                svn commit -m 'add branch myBranch'

                Сервер теперь знает о нашей ветке. В репозитории на сервере файлы, конечно же, не дублируются, но у нас теперь две копии рабочего каталога: в папке trunk и в папке branches/myBranch.

                Разве не так это происходит на рабочем компьютере? Если я описал процесс неправильно, очень хотелось бы узнать, как это происходит на самом деле.
                • +2
                  > У нас на рабочем компьютере есть репозиторий с папками trunk, branches, tags — копия центрального репозитория.
                  Вам не обязательно держать все это локально. После того как вы создали папки trunk, branches, tags на сервере, можете смело оставить локально только trunk. Это будет ваш working copy.

                  > svn copy trunk branches/myBranch
                  > Результатом будет полное рекурсивное копирование
                  Так вам же сказано — копируйте на сервере. А вы пишете опять про локальное копирование. Зачем? На сервере же, на сервере. Еще раз:! копируйте на сервере!
                  Почему вы постоянно возвращаетесь к копированию локально? Я не понимаю.

                  Копирование на сервере:
                  Допустим ваш репозитарий находится на http://svn.myrepo.com/
                  Делайте svn copy http://svn.myrepo.com/trunk http://svn.myrepo.com/branches/myBranch
                  Готово.

                  Потом в своем фолдере где у вас был trunk делаете svn switch http://svn.myrepo.com/branches/myBranch .
                  Все, готово.

                  P.S. Но, даже если вы уже почему-то сделали копию локально — просто сотрите локальную копию trunk, и работайте с копией branches/myBranch. И место на диске, которое вас так беспокоило, освободится.
                  • 0
                    Теперь ясно: чтобы облегчить копирование, нужно делать копирование на сервере и копировать не весь репозиторий. Но все-таки это не совсем те легковесные ветки, которые предоставляет Git, когда можно позволить себе создать ветку на пару коммитов для фикса маленького бага, а потом слить его с основной веткой с параметром squash, даже не отправляя на сервер.
                    • 0
                      > можно позволить себе создать ветку на пару коммитов для фикса маленького бага, а потом слить его с основной веткой с параметром squash, даже не отправляя на сервер
                      Как сказать. Фактически ваша машина и есть сервер (-:
      • +4
        P.S. Я вам зато другой пример могу привести в котором GIT гораздо накладнее нежели SVN.
        Особое отличие GIT-а в том что он тречит только полностью состояние всего репозитария, но не работает с отдельными файлами/каталогами. В каком-то смысле это его ахиллесова пята.

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

        А в SVN легко можно было-бы вычекаутить конкретную папку с конкретным апликейшном, подфиксать его, вкомитать, и даже завести папки с бренчами для этого отдельного апликейшна. Такие дела.
        • +4
          > Когда-то сам Торвальдс говорил
          На всякий случай, пруфлинк: lwn.net/Articles/246381/

          As you are probably aware, some people have tried to import the whole KDE
          history into git. Quite frankly, the way git works (tracking whole trees
          at a time, never single files), that ends up being very painful, because
          it's an «all or nothing» approach.

          So I'm hoping that if you guys are seriously considering git, you'd also
          split up the KDE repository so that it's not one single huge one, but with
          multiple smaller repositories (ie kdelibs might be one, and each major app
          would be its own), and then using the git «submodule» support to tie it
          all together.

        • 0
          В этом плане да. Но такова философия Git — он распределенный, на каждом компьютере лежит полноценный репозиторий проекта. В некоторых случаях — когда надо срочно внести небольшую правку в большой проект, — это минус. В некоторых — наоборот, неоценимый плюс. Например, при работе на плохом сетевом канале или при потере центрального репозитория (пришлось мне как-то столкнуться с такой трагедией).
          • +4
            Философия философией, а копии всей истории всего репозитария у каждого работника на проекте это настоящий «не философский» оверхед по нагрузке на файловую систему, которая вас так беспокоила при работе с SVN, но совершенно не беспокоит при работе с GIT. Ну да — ведь GIT это круто и модно, ему можно. А SVN — отсталая ерунда, ему нелья. Так что ли? ((-:

            Полные копии репозитария у всех оно, конечно, здорово — на случай ядерной войны например (-:
            Но компаниям всегда лучше иметь авторитетный источник — центральный сервер, с своими бекапами. А не полагаться на то, что каждый работник будет постоянно у себя держать правильный бекап всего репозитария (а вдруг у них только пара submodule останутся?).

            Я не знаю, может у вас и бывает что теряются центральные репозитарии — это, понятное дело, трагедия. Но ведь это событие из ряда вон выходящее. Это то, чего не должно быть никогда. Бекапы надо делать с центрального репо, и в отдельном здании хранить (на случай пожара). Что, в принципе, очевидно.
            • 0
              Под нагрузкой я имел в виду объем дискового ввода-вывода во время создания ветки. Конечно, я рассматривал случай локального создания ветки в SVN.

              И я отнюдь не утверждаю, что SVN — отсталая и какая-то еще плохая. Как я упомянул ниже, я вспоминаю ее и добрым словом )
        • 0
          Я понял почти все слова, кроме «тречит». Что это значит?
          • 0
            Да, переборщил я малось с жаргоном.
            Тречит — от слова трекинг (tracking). «Тречит диффы» = отслеживает изменения.
  • +1
    я перешел на Git с SVN, где ветвление было достаточно дорогим процессом: для создания ветки нужно было скопировать весь рабочий каталог.
    поясните тем, кто знает про svn switch?
    • 0
      Я написал выше. Если я ошибаюсь, поправьте.
  • +3
    Уберите упоминания svn и будет отличная статья про стратегии мерджа.
    • +1
      Не стоит воспринимать мои слова относительно SVN как низкую оценку этой системы. Это просто личный опыт. Причем, мне и добрым словом вспомнить SVN есть за что.

      К тому же, по этому поводу уже началась дискуссия, и если я уберу упоминание, не будет ясно, что послужило причиной для нее.
  • +2
    Автор, прости, а где тонкости :)? Я лично ожидал увидеть только описание recursive, с опциями, и подробным рассмотрением кейсов использования этих опций. Например, у нас какое-то время назад была задача, в которой надо было смержить старую ветку, которая трогала то же самое, что и в мастере, с, собственно, мастером. Было примерно стопицот строк конфликтов, и хотелось бы как-то их решить с помощью гита (например, откинуть все «чужие» конфликтующие изменения). К сожалению, я так и не смог понять, какие опции гиту нужно указать, чтобы предпочитать свои изменения, но только в случае конфликта мержа. Поэтому пришлось писать скрипт, который просто удаляет маркеры конфликтов :). Я ожидал увидеть что-нибудь похожее :). А то пока что у меня сложилось ощущение, что этими опциями мержа вы ни разу не пользовались, а просто о них рассуждаете.
    • 0
      Я ставил перед собой другую цель, так что не думаю, что претензия правомерная ) Ведь даже в заголовке указано, что рассматривается благополучный случай. А разрешение конфликтов — отдельная интересная тема, и у вас, кстати говоря, много опыта — можете развить ее )

      Я, кстати говоря, сталкивался с подобной проблемой. Конечно, к сожалению, мне не доводилось работать в проектах уровня Badoo, но как-то раз я присоединился к проекту, где рабочей веткой была version-2, потому что никто не знал, как разрешить конфликты при слиянии с master'ом. Но тут все решилось просто, как по учебнику: стратегия ours. Как раз были откинуты коммиты мастера после ответвления version-2, в том числе те, которые все сломали.
  • +1
    Для меня всегда было проблемой, что squash commit не делает струкутрных связей, так что если мы объединили изменения второй ветки в основную, а потом продолжили работу над второй веткой, мы вообще теряем информацию, что в этой второй ветке сквошмерджилось, а что — нет (только в описании сквош-коммита остается текстовая инфа). И далее при классическом мердже этой ветки, то есть «новых» изменений, в историю также попадают ранее смердженные сквошем коммиты, таким образом в общей истории получаем дубль: отдельные говнокоммиты, и их squash-merge.

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

    С горе-разработчиками, которые любят «нагадить в коммитах» очень актуально.

    Может есть какая-то фича гита, которая делает именно так как я хочу? Кто как с этим борется? :)
    • +1
      А чем Вам тогда мердж коммит не подходит? Отрибейсите топик-ветку на красивые коммиты перед мерджем и будет красивый мердж коммит.
      • 0
        Если ее отrebase'ить, то ее нельзя будет заpushить. А если заpushить с --force — тогда у горе-разработчиков работа вообще стает :)

        Может я не знаю, и ее можно как-то ее отребейзить на сервере «прозрачно» для разработчика, т.е. чтобы у него все автоматом обновилось без лишних вопросов?
        • +1
          Тут процесс дисциплины в комманде. Если каждый человек будет рибейсить свои коммиты с ветки перед отправкой на удаленный репозиторий, то история будет красивая сразу. Избегать мердж коммита ради того, чтобы его не было в истории как-то странно.
          • 0
            Вы не поняли. Я избегаю не мердж-коммита, а неугодных коммитов в истории.

            То есть вот имеется:
            master       A -> B -> C
            shitcommits       \ -> D -> E 
            


            Т.к. D и E — «не красивая история», делаем squash merge:
            master       A -> B -> C -> D+E(squash)
            shitcommits       \ -> D -> E 
            


            История мастера красивая: A->B->C->D+E

            Но тем временем разработчик продолжает работать над своей веткой.

            master       A -> B -> C -> D+E(squash)
            shitcommits       \ -> D -> E -> F
            


            И тут проблемы во всех случаях:
            1. Мы забыли про сквош и сделали обычный merge, получаем полную хрень истории главной A -> B -> C -> D -> E -> D+E -> F
            2. Мы снова делаем squash commit, и если мы еще и улучшали его код после предыдущего сквоша, получаем конфликты

            Если же на шаге создания D+E сделать rebase -i ветки разработчика:
            master       A -> B -> C -> (merge_shitcommits_r)
            shitcommits      \ -> D+E -> /
            shitcommits[0]   \ -> D -> E
            


            То нам необходимо как-то «переопределить» разработчика на новую ребейзнутую ветку, с «перетертой» историей. Простой пуш ребейзнутых веток сами понимаете к чему приводит…

            В общем, вот ищу выход из данной ситуации, но так, чтобы это было прозрачно для разработчика, который знает только пару команд гит.
            • 0
              Не нашлось решения?
              • 0
                Красивого и прозрачного — нет. Только «вручную» сообщить разработчику о необходимости переключиться на такую-то ветку сразу после выполнения мерджа. Судя по всему, гит описанного юзкейса не предусматривает. :(
  • –1
    Я бы очень хотел услышать в тонкостях про причины появления и, соответственно, избегания наличия пустых комитов в истории
    Merge branch 'feature_branch' into develop
    • +3
      На самом деле эти коммиты не пустые :). Они «пустые» в случае отсутствия конфликтов при мерже. Ваш вопрос, к сожалению, обычно приводит к холивору «merge vs. rebase», который ничем хорошим не заканчивается :).
      • 0
        Они «пустые» в случае отсутствия конфликтов при мерже.

        По-моему они пустые всегда при git checkout develop && git merge --no-ff feature_branch && git push.

        Ваш вопрос, к сожалению, обычно приводит к холивору «merge vs. rebase»

        Я не думаю что rebase тут при чем. Проблема эта локального репозитория, соответственно, можно обойтись наверняка каким-нибудь no-commit при мердже и т.п.

        Вообщем, я сам до конца не понимаю механизма получения таких мутных комитов, поэтому я и написал, что я хотел бы почитать про них.
        • +2
          Пруф:

          (develop)$ vim test.php
          
          (develop)$ cat test.php
          <?php
          echo 'Hello world';
          
          (develop)$ git add test.php 
          
          (develop)$ git commit -m 'One commit'
          [develop ad4834f] [develop]: One commit
           1 file changed, 2 insertions(+)
           create mode 100644 test.php
          
          (develop)$ git checkout feature
          Switched to branch 'feature'
          
          (feature)$ vim test.php
          
          (feature)$ cat test.php
          <?php
          
          echo 'Hello world (from feature)';
          
          (feature)$ git add test.php
          
          (feature)$ git commit -m 'Feature commit'
          [feature c996a10] Feature commit
           1 file changed, 3 insertions(+)
           create mode 100644 test.php
          
          (feature)$ git checkout develop
          Switched to branch 'develop'
          
          (develop)$ git merge feature
          Auto-merging test.php
          CONFLICT (add/add): Merge conflict in test.php
          Automatic merge failed; fix conflicts and then commit the result.
          (develop|MERGING)$ git status
          # On branch develop
          # You have unmerged paths.
          #   (fix conflicts and run "git commit")
          #
          # Unmerged paths:
          #   (use "git add <file>..." to mark resolution)
          #
          #	both added:         test.php
          #
          no changes added to commit (use "git add" and/or "git commit -a")
          
          (develop|MERGING)$ git diff test.php
          diff --cc test.php
          index bd1ca6f,1001057..0000000
          --- a/test.php
          +++ b/test.php
          @@@ -1,2 -1,3 +1,7 @@@
            <?php
          ++<<<<<<< HEAD
           +echo 'Hello world';
          ++=======
          + 
          + echo 'Hello world (from feature)';
          ++>>>>>>> feature
          
          (develop|MERGING)$ vim test.php
          
          (develop|MERGING)$ git status
          # On branch develop
          # You have unmerged paths.
          #   (fix conflicts and run "git commit")
          #
          # Unmerged paths:
          #   (use "git add <file>..." to mark resolution)
          #
          #	both added:         test.php
          #
          no changes added to commit (use "git add" and/or "git commit -a")
          
          (develop|MERGING)$ cat test.php
          <?php
          echo 'Hello world (from feature)';
          
          (develop|MERGING)$ git add test.php
          
          (develop|MERGING)$ git commit
          [develop 8355f7c] [develop]: Merge branch 'feature' into develop
          
          (develop)$ git log -p
          commit 8355f7c42f619c46ba448732d1bc27302d4f8d73
          Merge: ad4834f c996a10
          Author: Yuriy Nasretdinov <...>
          Date:   Mon Sep 30 19:38:02 2013 +0400
          
              [develop]: Merge branch 'feature' into develop
              
              Conflicts:
                  test.php
          ... # якобы нет изменений, «пустой» коммит
          
          (develop)$ git show
          commit 8355f7c42f619c46ba448732d1bc27302d4f8d73
          Merge: ad4834f c996a10
          Author: Yuriy Nasretdinov <y.nasretdinov@corp.badoo.com>
          Date:   Mon Sep 30 19:38:02 2013 +0400
          
              [develop]: Merge branch 'feature' into develop
              
              Conflicts:
                  test.php
          
          diff --cc test.php
          index bd1ca6f,1001057..614ad2b
          --- a/test.php
          +++ b/test.php
          @@@ -1,2 -1,3 +1,3 @@@
            <?php
          - echo 'Hello world';
           -
          + echo 'Hello world (from feature)';
          ++
          


          То есть, несмотря на то, что в «git log -p» изменений не видно, в «git show <коммит>» изменения всё же покажутся, причём именно то, что я менял.
          • 0
            Я просто оставлю это здесь.
            (feature)$ git show
            commit 504e36a28a81c175518ca4b478e28d93e3146df8
            Merge: c278018 5654da6
            Author: Evgeniy Makhrov <e.makhrov@corp.badoo.com>
            Date:   Thu Oct 31 03:12:01 2013 +0400
            
                Merge branch 'bugfix' into feature
            
                Conflicts:
                    test.php
            
            (feature)$ git show -m
            commit 504e36a28a81c175518ca4b478e28d93e3146df8 (from 5654da6fa73e780b9d9ecb06b3a3b90f6707b9c2)
            Merge: c278018 5654da6
            Author: Evgeniy Makhrov <e.makhrov@corp.badoo.com>
            Date:   Thu Oct 31 03:12:01 2013 +0400
            
                Merge branch 'bugfix' into feature
            
                Conflicts:
                    test.php
            
            diff --git a/test.php b/test.php
            index 51de675..8fc81b9 100644
            --- a/test.php
            +++ b/test.php
            @@ -1,4 +1,4 @@
             <?php
            
            -echo "Hello, world\n";
            +echo "Hello from feature\n";
            
            (feature)$ git log -1 -p
            commit 504e36a28a81c175518ca4b478e28d93e3146df8
            Merge: c278018 5654da6
            Author: Evgeniy Makhrov <e.makhrov@corp.badoo.com>
            Date:   Thu Oct 31 03:12:01 2013 +0400
            
                Merge branch 'bugfix' into feature
            
                Conflicts:
                    test.php
            (feature)$ git log -1 -p -m
            commit 504e36a28a81c175518ca4b478e28d93e3146df8 (from 5654da6fa73e780b9d9ecb06b3a3b90f6707b9c2)
            Merge: c278018 5654da6
            Author: Evgeniy Makhrov <e.makhrov@corp.badoo.com>
            Date:   Thu Oct 31 03:12:01 2013 +0400
            
                Merge branch 'bugfix' into feature
            
                Conflicts:
                    test.php
            
            diff --git a/test.php b/test.php
            index 51de675..8fc81b9 100644
            --- a/test.php
            +++ b/test.php
            @@ -1,4 +1,4 @@
             <?php
            
            -echo "Hello, world\n";
            +echo "Hello from feature\n";
            
        • +1
          git merge --no-ff

          Вы кажется сами только что ответили на свой вопрос, почему они создаются. Вы наглухо запрещаете Fast-Forward стратегию, которая как раз позволяет избежать этих коммитов.
          • 0
            Почему они создаются я понимаю в этом случае, а почему они пустые я не понимаю. При no-ff я хочу видеть этот комит со всеми diff которые составляют дельту между предыдущим комитом:

            A->B->C-->(E)
                \     /
                 ->D->
            


            где (E) = diff(C, D)
            • 0
              «git log -p -m» вам всё покажет. Только, уверяю вас, лучше на это не смотреть :).
      • 0
        Даже если изменений в них как таковых нет (вызванных конфликтами), они не пустые. Пустым коммит может быть только если он полностью дублирует какой-то предыдущий. Рассмотрим ситуацию:

        A->B->C-->(E)
            \     /
             ->D->
        


        в C у нас добавился файл C.txt, в D — соответственно D.txt

        Ведь состояние рабочего дерева в E отличается и от C и от D. Значит он не пустой, что, в свою очередь, означает, что это состояние как-то необходимо адресовать, к примеру, для ветвления от него (ветвление в данном случае от C или D не будет тождественно, т.к. в первом случае не будет файла D.txt, а во втором — C.txt).

        Отсутствие пустого возможно только в случае fast-forward, то есть если изменения были лишь в одной ветке после ветвления — в этом случае действительно «форсированный» --no-ff сделает «пустой» коммит, где рабочее дерево полностью тождественно дереву из предыдущего коммита.
  • +3
    В каком же случае проявляется недостаток стратегии resolve? Он проявляется в том случае, если для коммита (M) нам пришлось разрешить конфликты, после чего мы продолжили разработку и еще раз хотим выполнить git merge feature -s resolve. В этом случае в качестве общего предка снова будет использован коммит C, и конфликты произойдут снова и будут нуждаться в нашем вмешательстве.

    В этой ситуации может помочь rerere.
  • –3
    чего только люди не делают, лишь бы меркуриалом не пользоваться!
  • 0
    Скажите пожалуйста, правильно ли я понял, что можно использовать рекурсивную стратегию слияния при ребейзе ветки, чтобы повторно не разрешать конфликты?
    • 0
      Отвечает ggray:
      мне кажется что рекурсивная стратегия никак не связана с решениме конфликтов при ребейзе, а автору я бы посоветововал включить rerere если у него часто такие кейсы возникают :)
      • +1
        Благодаря упоминанию — смог залогиниться.

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

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