В чем разница между Git-merge и Git-cherry-pick для определенного коммита? - PullRequest
0 голосов
/ 29 декабря 2018

Есть ли разница между: git merge <commit-id> и git cherry-pick <commit-id>?где '' commit-id '' - это хеш коммита из моей новой ветки, который я хочу получить в главной ветке.

Ответы [ 3 ]

0 голосов
/ 30 декабря 2018

Существует огромная разница во всех, кроме тривиальных случаях (и даже в тривиальных случаях, разница есть).Чтобы понять это правильно, это немного сложная задача, но как только вы это сделаете, вы уже на пути к настоящему пониманию самого Git.

TL; DR - это в основном то, что ItayB уже сказал : cherry-pick означает скопировать некоторый существующий коммит.Суть этого копирования состоит в том, чтобы превратить коммит в набор изменений, а затем повторно применить тот же набор изменений к некоторому другому существующему коммиту, чтобы сделать новый коммит.Новый коммит «выполняет те же изменения», что и скопированный коммит, но применяет это изменение к другому снимку.

Это описание полезно и практично, но не на 100% точно - это не поможет, еслиВы получаете конфликтов слияния во время своего выбора вишни.Как следует из этого, cherry-pick внутренне реализован как особый вид слияния.Если нет конфликтов слияния, вам не нужно это знать.Если вы, ну, вероятно, лучше начать с правильного понимания слияния стиля git merge.

Слияние (как это сделано git merge) более сложное: оно не копия что угодно .Вместо этого он делает новый коммит типа merge , который ... ну, делает что-то сложное.:-) Это не может быть объяснено адекватно без предварительного описания графа коммитов Git .Он также состоит из двух частей, которые я хотел бы обозначить, во-первых, слияние как глагол (действие объединения изменений), а во-вторых, commit-of-type-merge или объединить как существительное или прилагательное: Git называет эти слиянием или коммитом слияния .

Когда cherry-pick выполняет слияние, оно делает только первоенаполовину, сливается как глагол , и делает это немного странно.Если слияние не удается с конфликтом, результаты могут быть очень удивительными.Их можно объяснить, только зная, как Git выполняет слияние как процесс глагола .

Также есть что-то, что Git вызывает операцию fast-forward или иногда ускоренное слияние , которое вообще не является слиянием.Это, к сожалению, также сбивает с толку;давайте подождем с этим.

Все ниже - длинный ответ: только для чтения, если вы хотите понять (больше) Git

Что нужно знать о коммитах

ПервыйВажно знать, что, возможно, вы уже знаете, что Git в основном касается коммитов, и каждый коммит Git сохраняет полный снимок каждого файла.То есть коммиты Git не change-sets.Если вы измените один файл - скажем, README.md - и сделаете с ним новый коммит, то новый коммит будет иметь каждый файл, полностью, включая (полный текст) измененный README.md.Когда вы проверяете коммит, используя git show или git log -p, Git покажет вам, что вы изменили, но это делается путем извлечения сначала сохраненных файлов предыдущего коммита, затемзафиксировать сохраненные файлы, а затем , сравнивая два снимка.Поскольку только README.md изменилось , оно показывает только README.md и даже тогда показывает только разницу - набор изменений в одном файле.

Это, в свою очередь,, означает, что каждый коммит знает своего непосредственного предка или parent commit.В Git коммиты имеют фиксированное постоянное «истинное имя», которое всегда означает этот конкретный коммит .Это истинное имя, или идентификатор хеша , или иногда OID («O» означает Object), представляет собой большую некрасивую строку букв и цифр, которую Git печатает в выводе git log.Например, 5d826e972970a784bd7a7bdf587512510097b8c7 - это коммит в Git-репозитории для Git.Эти вещи выглядят случайными (хотя это не так) и, как правило, бесполезны для людей, но именно так Git находит каждый коммит.Этот конкретный коммит имеет одного родителя - какой-то другой большой уродливый идентификатор хеша - и Git сохраняет хэш родителя в коммите, чтобы Git мог использовать коммит, чтобы посмотреть назад на своего родителя.

Результатв том, что если у нас есть серия коммитов, они образуют обратную цепочку.Мы - или Git - начнем с конца этой цепочки и будем работать в обратном направлении, чтобы найти историю в хранилище.Давайте представим, что у нас есть крошечный репозиторий всего с тремя коммитами.Вместо их настоящих хеш-идентификаторов, которые слишком велики и уродливы, давайте назовем их коммитами A, B и C и нарисуем их в отношениях их родитель / потомок:

A <-B <-C

Commit C является самым последним, поэтому он является потомком B.У Git C запоминается B идентификатор хэша, поэтому мы говорим, что C указывает на B.Когда мы сделали B, был только один предыдущий коммит, A, поэтому A является родителем B и B указывает на A.Коммит A является своего рода особым случаем: когда мы его сделали, было зафиксировано нет коммитов.У него нет родителя, и это то, что позволяет Git прекратить погоню в обратном направлении.

Коммиты также полностью, полностью, доступны только для чтения: после создания ничего в любом коммите не может быть изменено.Это связано с тем, что идентификатор хеша на самом деле представляет собой криптографическую контрольную сумму всего содержимого фиксации.Измените хотя бы один бит в любом месте, и вы получите новый, другой хэш-идентификатор - новый, другой коммит.Таким образом, моментальный снимок фиксации сохраняет состояние ваших файлов навсегда - или, по крайней мере, до тех пор, пока сам коммит продолжает существовать.(Изначально вы можете думать об этом как о «навсегда»; механизмы забвения или замены коммита более продвинуты и становятся довольно хитрыми, когда это не последний commit.)

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

A--B--C

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

Что нужно знать об именах ветвей

Это легко, в упрощенных схемах, подобных приведенной выше, чтобы сказать, какой коммит является последним .Буква C следует после B, в конце концов, так что C является последним.Но идентификаторы хешей в Git выглядят совершенно случайными, и Git нужен реальный идентификатор хешей.Итак, что Git делает здесь: сохраняет хэш-идентификатор последнего коммита в имени ветви .

Фактически этосамо определение имени ветви: имя типа master просто хранит хэш-идентификатор коммита, который мы хотим назвать последним для этой ветви .Таким образом, учитывая A--B--C строку коммитов, мы просто добавляем имя master, указывающее на коммит C:

A--B--C   <-- master

Что особенного в имени ветви, так это то, что в отличие от коммитов они изменение .Они не только меняются, они делают это автоматически .Процесс создания нового коммита в Git состоит из записи содержимого коммита - его родительского хеш-кода, информации об авторе / коммиттере, сохраненного снимка, сообщения журнала и т. Д. - который вычисляет новый хешИдентификатор нового коммита, а затем изменив имя ветви , чтобы записать хэш-идентификатор нового коммита.Если мы создадим новый коммит D на master, Git сделает это, выписав D, указывая на C, а затем обновив master, указав D:

A--B--C--D   <-- master

Предположим, теперь мы создаем новое имя ветви, develop.Новое имя будет также указывать на коммит D:

A--B--C--D   <-- develop, master

Давайте сделаем новый коммит E сейчас, чьим родителем будет D:

A--B--C--D
          \
           E

Какое имя ветки следует обновить Git?Мы хотим, чтобы master указывал на E, или мы хотим, чтобы develop указывал на E?Ответ на этот вопрос заключается в специальном имени HEAD.

ГОЛОВА Git запоминает ветвь и, таким образом, текущий коммит

Remember , какую ветку мы хотим, чтобы Git обновил , а также , коммит, который мы извлекли прямо сейчас , Git имеет специальное имя HEAD, написанное всеми заглавными буквами, как это,(Нижний регистр работает в Windows и MacOS из-за причуды, но не работает в системах Linux / Unix, которые не разделяют эту причуду, поэтому лучше использовать написание всего заглавными буквами. Если вам не нравится вводить слово,Вы можете использовать символ @, который является синонимом. Обычно Git присоединяет имя HEAD к одному имени ветви:

A--B--C--D   <-- develop (HEAD), master

Здесь мы находимсяветвь develop, потому что это тот, к которому прикреплен HEAD.(Обратите внимание, что все четыре коммита находятся на обеих ветвях.) Если теперь мы делаем новый коммит E, Git знает, какое имя обновлять:

A--B--C--D   <-- master
          \
           E   <-- develop (HEAD)

Имя HEAD остаетсяприкреплен к ветке;само имя ветви меняет, какой хеш-идентификатор запоминается;и коммит E теперь является текущим коммитом .Если мы сделаем новый коммит сейчас, его родитель будет E, а Git обновит develop.(Новый коммит E только на develop, в то время как коммиты A-B-C-D все еще включены обе ветви!)

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

(Detached HEAD - это нормально., но немного в особом случае: вы не будете использовать его для повседневной разработки, кроме как при выполнении некоторых операций git rebase. В основном вы будете использовать его для изучения исторических фиксаций - тех, которые не на кончике имени какой-либо ветви.игнорируйте его здесь.)

График коммитов и git merge

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

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

       G--H   <-- master
      /
...--D
      \
       E--F   <-- develop

Мы будем git checkout master, так что HEAD будет привязан к master, указывая на H, и затем запустим git merge develop.

Git в этот момент будет следовать обе цепи в обратном направлении.То есть он начнется с H и вернется к G, а затем к D.Он также начнется с F и вернется к E, а затем к D.На этом этапе Git обнаружил общий коммит - коммит, который находится на обеих ветвях.Все предыдущие коммиты также являются общими, но это лучший , потому что это наиболее близкий коммит к обоим подсказкам ветвления.

Этот лучший общий коммит называется база слияния .Таким образом, в этом случае D является базой слияния master (H) и develop (F). Фиксация базы слияния полностью определяется графом коммитов, начиная с текущего коммита (HEAD = master = коммит H) и другого коммита, который вы называете в командной строке (develop = совершить F).Единственное использование ветки names в этом процессе - это локализация коммитов - все после этого зависит от графа.

Найдя базу слияния, git merge делает теперь только объединить изменения .Помните, однако, что мы говорили, что коммиты - это снимки, а не наборы изменений.Таким образом, чтобы найти изменения, Git должен начать с извлечения самого коммита базы слияния во временную область.

Теперь, когда Git извлек базу слияния, git diff найдет то, что мы изменили, на master: разница между снимком в D и снимком в HEAD (H).Это первый набор изменений.

Git теперь должен запустить второй git diff, чтобы найти, что они изменили, на develop: разница между снимком в D и снимком в F.Это второй набор изменений.

Следовательно, то, что делает git merge, найдя базу слияния, запускает эти две команды git diff:

git diff --find-renames <hash-of-D> <hash-of-H>    # what we changed
git diff --find-renames <hash-of-D> <hash-of-F>    # what they changed

Git затем объединяет эти дванаборы изменений, применяет объединенные изменения к тому, что находится в снимке в D (база слияния), и делает новый коммит из результата.Или, скорее, он делает все это , пока объединение работает - или точнее, пока Git считает, что объединение сработало .

А пока давайтеПредположим, что Git думает, что это работает.Мы скоро вернемся к конфликтам слияния.

Результат фиксации объединенных изменений, примененных к базе слияния, является новым коммитом.Этот новый коммит имеет одну особенность: помимо сохранения полного снимка, как обычно, он имеет не один, а два родительских коммита. первый этих двух родителей - это коммит, на котором вы были, когда вы выполняли git merge, а второй - другой коммит.То есть новый коммит I является коммитом слияния:

       G--H
      /    \
...--D      I   <-- master (HEAD)
      \    /
       E--F   <-- develop

Поскольку история в репозитории Git представляет собой набор коммитов, это делаетодин новый коммит, история которого обе ветви.Начиная с I, Git может работать в обратном направлении до H и до F, а от них до G и E соответственно и оттуда до D.Имя master теперь указывает на I.Имя develop не изменилось: оно продолжает указывать на F.

Теперь можно удалить имя develop, если мы хотим, потому что мы (и Git)можно найти коммит F из коммита I.В качестве альтернативы, мы можем продолжать развивать его, делая больше новых коммитов:

       G--H
      /    \
...--D      I   <-- master
      \    /
       E--F--J--K--L   <-- develop

Если мы сейчас git checkout master снова и запустим git merge develop снова , Gitбудет делать то же самое, что и раньше: найти базу слияния, запустить два git diff и зафиксировать результат.Интересно, что из-за коммита I база слияния больше не D.

Можете ли вы назвать базу слияния?Попробуйте это как упражнение: начните с L и работайте задом наперед, перечисляя коммиты.(Не забудьте идти только назад: из F, вы не можете добраться до I, потому что это неправильное направление. Вы можете добраться до E, чтов обратном направлении.) Затем начните с I и вернитесь к F и H.Является ли один из тех в списке, который вы сделали для develop?Если это так, это база слияния (а именно F) для нового слияния, поэтому Git будет использовать это для своих двух git diff команд.

В конце концов, еслислияние работает, мы получим новый коммит слияния M на master:

       G--H
      /    \
...--D      I--------M   <-- master (HEAD)
      \    /        /
       E--F--J--K--L   <-- develop

и в будущем слияние, если мы добавим больше коммитов к develop, будет использовать L в качествебаза слияния.

В Cherry-picking используется механизм слияния - два сравнения - со странной базой

Давайте вернемся к этому состоянию и прикрепим HEAD к master:

       G--H   <-- master (HEAD)
      /
...--D
      \
       E--F   <-- develop

Теперь давайте посмотрим, как Git фактически реализует git cherry-pick develop.

Во-первых, Git преобразует имя develop в идентификатор хеша коммита.Поскольку develop указывает на F, это фиксация F.

Commit F является снимком и должна быть превращена в набор изменений.Git делает это с git diff <hash-of-E> <hash-of-F>.

Git может , на данный момент просто примените те же самые изменения к снимку в H.Это то, о чем говорило наше не совсем точное описание высокого уровня: мы просто берем этот дифференциал и применяем его к H.И в большинстве случаев, то, что происходит , выглядит так: Git делал именно это - и в очень старых версиях Git (которые никто больше не использует), Git действительно делал .Но есть случаи, когда это не работает должным образом, поэтому Git теперь выполняет странное слияние.

Inнормальное слияние, Git найдет базу слияния и запустит две разницы.В слиянии типа вишневого пика Git просто заставляет основание слияния стать родителем коммита, выбранного вишней.То есть, поскольку мы выбираем вишню F, Git заставляет базу слияния совершать E.

Теперь Git делает git diff --find-renames <hash-of-E> <hash-of-H>, чтобы посмотреть, что мы изменили,и git diff --find-renames <hash-of-E> <hash-of-F> чтобы увидеть, что они (commit F) изменились.Затем он объединяет два набора изменений и применяет результат к снимку в E.Это сохраняет вашу работу (потому что независимо от того, что вы изменили, вы все еще изменились) при добавлении набора изменений также из F.

Если все идет хорошо, что часто происходит, Git делает новый коммит, ноЭтот новый коммит является обычным коммитом с одним родителем, который продолжается master.Это очень похоже на F, и на самом деле Git копирует лог-сообщение также из F, поэтому давайте назовем этот новый коммит F', чтобы помнить:

       G--H--F'   <-- master (HEAD)
      /
...--D
      \
       E--F   <-- develop

Обратите внимание, что так же, какраньше develop не двигался.Однако мы также не сделали коммит слияния : новый F' не записывает F сам.График не объединен; база слияния из F' и F по-прежнему фиксируется D.

Таким образом, это полный и точный ответ

Это полная разница междувишневый кирка и настоящее слияние: вишневый кирк использует механизм слияния Git для объединения изменений, но оставляет graph необработанным, просто делая копию некоторого существующего коммита.Два набора изменений, используемые в объединении, основаны на родительском коммите, выбранном вишней, а не на вычисленной базе слияния.Новая копия имеет новый хэш-идентификатор, который никак не связан с исходным коммитом.Истории, найденные начиная с названия ветви, master или develop здесь, все еще уходят в прошлое.При истинном слиянии новый коммит представляет собой слияние с двумя родителями, и истории прочно объединяются - и, конечно, два набора изменений, которые объединяет git merge, формируются из вычисленной базы слияния, поэтому они являются различными изменениями.наборы.

Когда слияние завершается конфликтом

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

Предположим, что Git объединяет изменения, а изменение -set --ours говорит касание строки 17 файла A, строки 30 файла B и строк 3-6 файла D .Между тем набор изменений --theirs ничего не говорит о файле A, но говорит изменить строку 30 файла B, строку 12 файла C и строки 10-15 файла D.

Так как только наш касается файла A, и только их касается файла C, Git может просто использовать нашу версию A и их версию C. Мы оба касаемся файла D, но наш касается строки 3-6, а их - строки 10-15, поэтомуGit может принять оба изменения в файл D. Файл B - настоящая проблема: мы оба коснулись строки 30.

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

На этом этапе index Git (о котором я не говорилздесь) становится решающим.Я собираюсь продолжать не говорить об этом, за исключением того, что Git оставляет в нем все три версии конфликтующего файла .Между тем, есть также копия рабочего файла файла B, и в файле рабочего дерева Git прилагает все усилия, чтобы объединить изменения, используя маркеры конфликта, чтобы показать, где проблема.

Ваша задача, как человека, управляющего Git, - разрешать каждый конфликт любым удобным для вас способом.Устранив все конфликты, вы затем используете git add, чтобы обновить индекс Git для нового коммита.Затем вы можете запустить git merge --continue или git cherry-pick --continue, в зависимости от причины проблемы, чтобы Git зафиксировал результат, или вы можете запустить git commit, что является старым способом сделать то же самое.Фактически, операции --continue в основном просто запускают git commit для вас: код фиксации проверяет, есть ли конфликт, который он должен завершить, и, если да, делает либо регулярную (cherry-pick) фиксацию, либо слияниеcommit.

Особый случай: слияние с ускоренной перемоткой вперед

Когда вы запускаете git merge <em>othercommit</em>, Git находит базу слияния как обычно, но иногда база слияния довольно тривиальна.Рассмотрим, например, график, подобный следующему:

...--F--G--H   <-- develop (HEAD)
            \
             I--J   <-- feature-X

Если вы сейчас запустите git merge feature-X, Git найдет базу слияния, начав с коммитов J и H и выполнив обычную обратную ходьбунайти первый общий коммит.Но этот первый общий коммит - это сам коммит H, где develop очков.

Возможно, что Git совершит реальное слияние, выполнив:

git diff --find-renames <hash-of-H> <hash-of-H>   # what we changed
git diff --find-renames <hash-of-H> <hash-of-J>   # what they changed

и вы может заставить Git сделать это, используя git merge --no-ff.Но очевидно, что дифференцирование коммита против самого себя покажет без изменений .Часть --ours из двух наборов изменений будет пустой.Результатом слияния будет тот же моментальный снимок, который находится в коммите J, поэтому, если мы осуществим истинное слияние:

...--F--G--H------J'   <-- develop (HEAD)
            \    /
             I--J   <-- feature-X

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

Если мы не инициирует истинное слияние, Git понимает, что J' и J будут соответствовать так, и просто не потрудится сделать новый коммит .Вместо этого он «скользит по имени, к которому прикреплен заголовок вперед», против внутренних стрелок, указывающих назад:

...--F--G--H
            \
             I--J   <-- develop (HEAD), feature-X

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

0 голосов
/ 28 июня 2019

Кажется, как говорит д-р Беко, сам процесс слияния одинаков как для слияния, так и для выбора вишни, хотя, как он отмечает, основы и другие аспекты различны.Я думаю, что есть аргумент, что способ слияния, то есть правила слияния, должен быть разным для слияния и выбора вишни, и в этом году мы представили доклад на XML в Праге «Слияние и прививка: два близнеца, которыеNeed To Grow Apart "http://www.xmlprague.cz/day2-2019/#merge, что может представлять интерес.

0 голосов
/ 29 декабря 2018

cherry-pick принимает ровно один коммит в вашу текущую ветку.merge берет всю ветку (может быть несколько коммитов) и объединяет ее с вашей веткой.

То же самое, если вы объединяете ее с <commit-id> - это не только конкретная фиксация, но приведенные ниже коммиты (если есть), а также.

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