Что означает «изменения, внесенные коммитом» в git - PullRequest
0 голосов
/ 24 февраля 2019

Везде, где я вижу это: "... cherry-pick применяет изменения, введенные коммитом ..."

Я сделал это: создал этот файл в master:

** File 1 **

Content

** Footer **

затем разветвился на branch2 и совершил изменение:

** File 1 **

Content
Edit 1

** Footer **

, а затем еще одно:

** File 1 **

Content
Edit 2
Edit 1

** Footer **

Теперь я вернулся к мастеру и попытался выбрать последний коммит из branch2.,Я ожидал, что только 'Edit2' будет импортирован, так как это изменение не внесено этим коммитом по сравнению с предыдущим?

Вместо этого я получил следующий конфликт слияния:

** File 1 **

Content
<<<<<<< HEAD
=======
Edit 2
Edit 1
>>>>>>> b634e53...
** Footer **

Теперь мой очевидный вопрос заключается в том, что я неправильно понимаю о том, как работает cherry-pick, и конкретно, почему здесь возникает конфликт слияния, который был бы быстрым шагом вперед с git merge?

ВАЖНОЕ ЗАМЕЧАНИЕ: Это НЕ подразумевается как вопрос о конфликтах слияний, меня интересует то, что на самом деле здесь делает Cherry-pick.И я не спрашиваю от любопытства / чего бы то ни было, а потому что у меня проблемы с использованием мерзавца на моей работе.

1 Ответ

0 голосов
/ 24 февраля 2019

Как отметили несколько человек в комментариях (и сделали ссылки на другие вопросы), git cherry-pick на самом деле выполняет трехстороннее слияние. Как работает cherry-pick и revert? описывает это, но больше по содержанию, чем по механизму.

Я описываю источник определенного набора конфликтов слиянияв Почему я получаю этот конфликт слияния с помощью git rebase в интерактивном режиме? вместе с общими набросками cherry-pick и revert, но я думаю, что будет хорошей идеей отступить назад и спросить механизм вопрос, который вы задали.Однако я бы немного перефразировал его, так как эти три вопроса:

  • Является ли коммит действительно моментальным снимком?
  • Если коммит является моментальным снимком, как работает git showили git log -p показать это как изменение?
  • Если коммит является снимком, как могут git cherry-pick или git revert работать?

Ответпоследний требует сначала ответить на еще один вопрос:

  • Как Git выполняет git merge?

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

Действительно ли фиксация - это моментальный снимок?

Да, хотя с технической точки зрения фиксация означает снимок, а не как единица.Это довольно просто и понятно.Чтобы использовать Git, мы обычно запускаем git clone, что дает нам новый репозиторий.Иногда мы начинаем с создания пустой директории и использования git init для создания пустого хранилища.В любом случае, теперь у нас есть три объекта:

  1. Сам репозиторий, который представляет собой большую базу данных из объектов , плюс меньшую базу данных с именем дляотображения хеш-идентификаторов (например, для имен веток), а также множество других мини-баз данных, реализованных в виде отдельных файлов (например, по одной на reflog).

  2. Что-то, что Git вызывает index , или область подготовки , или иногда cache .То, что он получает, зависит от того, кто звонит.Индекс в основном там, где у вас есть Git, создающий следующий коммит, который вы сделаете, хотя он играет расширенную роль во время слияний.

  3. work-дерево , где вы можете видеть файлы и работать с ними / с ними.

База данных объектов содержит четыре типа объектов, которые Git вызывает коммит , деревья , капли и аннотированные теги .Деревья и BLOB-объекты - это в основном детали реализации, и здесь мы можем игнорировать аннотированные теги: основная функция этой большой базы данных для наших целей - хранить все наши коммиты.Эти коммиты затем ссылаются на деревья и BLOB-объекты, которые содержат файлы.В конце концов, на самом деле это комбинация деревьев плюс капли, которая является моментальным снимком.Тем не менее, каждый коммит имеет ровно одно дерево, и именно это дерево дает нам остальную часть пути к моментальному снимку, поэтому, за исключением множества дьявольских подробностей реализации, сам коммит также может быть снимком.

Как мы используем индекс для создания новых снимков

Мы пока не будем слишком углубляться в сорняки, но скажем, что индекс работает, держа сжатый Git-ified,в основном замороженная копия каждого файла.Технически, он содержит ссылку на фактически замороженную копию, сохраненную как blob .То есть, если вы начнете с git clone <em>url</em>, Git запустит git checkout <em>branch</em> как последний шаг клона.Этот checkout заполнил индекс из коммита в кончике ответвления , так что в индексе есть копия каждого файла в этом коммите.

Действительно, большинство 1 git checkout операций заполняют оба индекса и рабочего дерева из коммита.Это позволяет вам видеть и использовать все ваши файлы в рабочем дереве, но копии рабочего дерева не являются теми, которые на самом деле в коммите.В коммите есть (есть?) Замороженные, сжатые, Git-ified, мгновенные снимки больших двоичных объектов всех этих файлов.Это сохраняет эти версии этих файлов навсегда - или до тех пор, пока существует сам коммит - и отлично подходит для архивирования, но бесполезно для выполнения любой реальной работы.Вот почему Git de-Git вставляет файлы в рабочее дерево.

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

Затем при каждом запускеgit add, Git сжимает и Git-ifies файл, который вы добавляете, и теперь обновляет индекс .Если вы изменили только несколько файлов, а затем git add только эти несколько файлов, Git должен обновить только несколько записей индекса.Таким образом, это означает, что во все времена индекс имеет следующий моментальный снимок внутри него , в специальной сжатой форме Git-only и готовой к замораживанию.

Thisв свою очередь означает, что git commit просто необходимо заморозить содержимое индекса.Технически, он превращает индекс в новое дерево, готовое для нового коммита.В некоторых случаях, например, после некоторых возвратов или для git commit --allow-empty, новое дерево будет фактически таким же деревом, как некоторые предыдущие коммиты, но вам не нужно знать об этом или заботиться о нем.

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

Наконец, Git записывает хеш-код нового коммита в текущее имя ветки, так что имя ветки теперь ссылается на new commit, а не родитель нового коммита, как это было раньше.То есть, какой бы коммит не был вершиной ветви, теперь этот коммит находится на один шаг позади вершины ветви.Новый совет - это только что сделанный вами коммит.


1 Вы можете использовать git checkout <em>commit</em> -- <em>path</em> для извлечения одного конкретного файла из одного конкретного коммита.Этот все еще сначала копирует файл в индекс, так что это на самом деле не исключение.Однако вы также можете использовать git checkout для копирования файлов только из индекса в рабочее дерево, и вы можете использовать git checkout -p для выборочного, интерактивного исправления файлов, например.Каждый из этих вариантов имеет свой особый набор правил относительно того, что он делает с индексом и / или рабочим деревом.

SincGit создает новые коммиты из индекса, может быть целесообразно - хотя и болезненно - часто пересматривать документацию.К счастью, git status многое говорит вам о том, что сейчас находится в индексе - сравнивая текущий коммит с индексом, затем сравнивая индекс с рабочим деревом, и для каждого такого сравнения сообщая вам, что отличается .Таким образом, большую часть времени вам не нужно носить в голове все дико меняющиеся детали влияния каждой команды Git на индекс и / или рабочее дерево: вы можете просто запустить команду и использовать git status позже.


Как git show или git log -p показывает коммит как изменение?

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

... <-F <-G <-H   <--master

Нам нужно толькоесть способ найти последний коммит.Таким образом: имя ветки , например, master здесь, идентифицирует last commit.Если этот хэш-идентификатор последнего коммита H, Git находит коммит H в базе данных объектов.H хранит хэш-идентификатор G, из которого Git находит G, в котором хранится хэш-идентификатор F, из которого Git находит F и т. Д.

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

Обратите внимание, что это работает только для коммитов non-merge .Когда Git создает коммит merge , у нас в Git хранится не один, а два родительских хеш-идентификатора.Например, после запуска git merge feature в режиме master мы можем иметь:

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

Commit M имеет двух родителей: его первый родитель - I, что был вершина коммита master только минуту назад.Его вторым родителем является L, который все еще является коммитом tip на feature.Трудно - ну, вообще-то, невозможно - представить коммит M как простое изменение или I или L, и по умолчанию git log просто не мешает отображать какие-либоизменяется здесь!

(Вы можете сказать git log и git show, что, по сути, split объединению: показать разницу от I до M, изатем, чтобы показать второй, отдельный diff от L до M, используя git log -m -p или git show -m. Команда git show по умолчанию выдает то, что Git называет комбинированный diff , которыйэто немного странно и необычно: по сути, он запускает обе разницы, как для -m, затем , игнорируя большую часть того, что они говорят , и показывает вам только некоторые из тех изменений, которые произошли от Оба фиксируют. Это довольно сильно связано с тем, как работают слияния: идея состоит в том, чтобы показать части, которые могли иметь конфликты слияния.)

Это приводит нас к нашему встроенному вопросу, который мы должны рассмотреть перед тем, какмы добираемся до вишни и возвращаемся.Нам нужно поговорить о механике git merge, то есть о том, как мы получили снимок для коммита M.

Как Git выполняет git merge?

Давайте начнем с того, что отметим точку слияния - ну, во всяком случае, большинства слияний - это объединить работу .Когда мы сделали git checkout master, а затем git merge feature, мы имели в виду: Я немного поработал над master.Кто-то еще работал над feature.Я хотел бы объединить работу, которую они проделали, с работой, которую я сделал. Есть процесс для этого объединения, а затем более простой процесс для сохранения результата.

Таким образом, тамЭто две части истинного слияния, результатом которых является фиксация, подобная M выше.Первая часть - это то, что я люблю называть частью глагол , для объединения .Эта часть фактически объединяет наши различные изменения.Вторая часть делает слиянием или коммитом слияния: здесь мы используем слово «слияние» как существительное или прилагательное.

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

Слияние как существительное или прилагательное

Вторая часть реального слияния - более легкая часть.Как только мы завершили процесс для слияния , merge-as-a-verb, мы заставили Git сделать новый коммит обычным способом, используя все, что есть в индексе.Это означает, что индекс должен заканчиваться объединенным содержимым.Git будет строить дерево как обычно и собирать сообщения журнала как обычно - мы можем использовать не очень хорошее значение по умолчанию, merge branch <em>B</em>, или создать хорошее, если мы чувствуем себя особенно усердно.Git добавит наше имя, адрес электронной почты и метку времени как обычно.Затем Git запишет коммит, но вместо того, чтобы хранить в этом новом коммите только один родитель, Git будет хранить дополнительного второго родителя, который является идентификатором хешакоммит, который мы выбрали при запуске git merge.

Для нашего git merge feature, например, при master, первым родителем будет коммит I - коммит, который мы извлекли, запустив git checkout master.Вторым родителем будет коммит L, на который указывает feature.Вот и все, что нужно для a слияния: коммит слияния - это просто коммит как минимум с двумя родителями, а стандартные два родителя для стандартного слияния таковы, что первый такой же, как и для any коммит, а второй - тот, который мы выбрали, запустив git merge <em>something</em>.

Слияние как глагол

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

Мы заявили выше, что внесли некоторые изменения вmaster, и они - кем бы они ни были - внесли некоторые изменения в feature.Но мы уже видели, что Git не хранит изменений.Git хранит снимки.Как перейти от снимок к изменить?

Мы уже знаем ответ на этот вопрос! Мы видели его, когда смотрели на git show.Git сравнивает два снимка.Так что для git merge нам просто нужно выбрать правильные снимки .Но какие из них являются правильными снимками?

Ответ на этот вопрос лежит в графе коммитов.Перед запуском git merge график выглядит следующим образом:

       G--H--I   <-- master (HEAD)
      /
...--F
      \
       J--K--L   <-- feature

Мы сидим на коммите I, верхушка master.Их коммит это коммит L, верхушка featureI мы можем работать в обратном направлении до H, а затем G, затем F, а затем предположительно E и так далее.Между тем, с L мы можем работать в обратном направлении до K, а затем J, а затем F и предположительно E и т. Д.

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

  • сравнить F с I: это то, что мы изменили
  • сравнить F сL: вот что они изменили

Мызаболел, по сути, просто заставил Git запустить два git diff s.Один выяснит, что мы изменили, а другой выяснит, что они изменили.Фиксация F является нашей общей отправной точкой, или, говоря языком контроля версий, база слияния .

Теперь, чтобы фактически выполнить слияние, Git расширяет индекс.Вместо того, чтобы хранить одну копию каждого файла, Git теперь будет содержать индекс три копии каждого файла.Одна копия поступит из базы слияния F.Второй экземпляр придет с нашего коммита I.Последняя, ​​третья копия получена из их коммита L.

Между тем, Git также просматривает результаты двух различий, файл за файлом.Пока коммиты F, I и L имеют все одинаковые файлы, 2 есть только эти пять возможностей:

  1. Никто не трогал файл,Просто используйте любую версию: они все одинаковые.
  2. Мы изменили файл, а они нет.Просто используйте нашу версию.
  3. Они изменили файл, а мы нет.Просто используйте их версию.
  4. Мы и они оба изменили файл, но мы сделали те же самые изменения .Используйте либо наш, либо свой - оба одинаковы, поэтому не имеет значения, какой.
  5. Мы и они оба изменили один и тот же файл, но мы сделали другим Изменения.

Случай 5 является единственным сложным.Для всех остальных Git знает - или, по крайней мере, предполагает, что знает - каков правильный результат, поэтому для всех остальных случаев Git сокращает временные интервалы индекса для рассматриваемого файла до одного слота (нумерованного нуля), который содержитправильный результат.

Однако для случая 5 Git помещает все три копии трех входных файлов в три пронумерованных слота в индексе.Если файл с именем file.txt, :1:file.txt содержит базовую копию слияния из F, :2:file.txt содержит нашу копию из коммита I, а :3:file.txt содержит их копию из L.Затем Git запускает низкоуровневый драйвер слияния - мы можем установить его в .gitattributes или использовать по умолчанию.

По умолчанию для низкоуровневого слияния используются две разности, от базовой до нашей и от базовой дои пытается объединить их, взяв оба набора изменений.Всякий раз, когда мы касаемся различных строк в файле, Git принимает наши или их изменения.Когда мы касаемся тех же строк , Git объявляет конфликт слияния. 3 Git записывает полученный файл в рабочее дерево как file.txt, с маркерами конфликта, если были конфликты.Если вы установите для merge.conflictStyle значение diff3, маркеры конфликта включают файл base из слота 1, а также строки из файлов в слотах 2 и 3. Мне нравится этот стиль конфликта гораздо лучше, чемзначение по умолчанию, которое опускает контекст slot-1 и показывает только слот-2 против конфликта slot-3.

Конечно, если есть конфликты, Git объявляет слияние конфликтующим.В этом случае он (в конце концов, после обработки всех других файлов) останавливается в середине слияния, оставляя беспорядок маркера конфликта в рабочем дереве и все три копии file.txt в индексе в слотах 1,2 и 3. Но если Git может разрешить два разных набора изменений самостоятельно, он идет вперед и стирает слотов 1-3, записывает успешно объединенный файл в рабочее дерево, 4 копирует файл рабочего дерева в индекс с нормальным нулевым интервалом и продолжает работу с остальными файлами как обычно.

Если объединение останавливает Это ваша работа, чтобы исправить беспорядок.Многие люди делают это путем редактирования конфликтующего файла рабочего дерева, выяснения правильного результата, выписывания файла рабочего дерева и запуска git add для копирования этого файла в индекс. 5 шаг copy-to-index удаляет записи 1-3 этапа и записывает обычную запись нулевого этапа, так что конфликт разрешается, и мы готовы к фиксации.Затем вы говорите, что объединение продолжается, или запускаете git commit напрямую, поскольку git merge --continue все равно запускает git commit.

Th, чтобы объединить процесс, хотя и немного сложный, в конце довольно прост:

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

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


2 Если три входных коммита не имеют всеодни и те же файлы, все становится сложнее.У нас могут быть конфликты добавления / добавления, изменения / переименования, изменения / удаления конфликтов и т. Д., Которые я называю high level конфликтами.Они также останавливают слияние в середине, оставляя слоты 1-3 индекса заполненными соответствующим образом.Флаги -X, -X ours и -X theirs, не не влияют на конфликты высокого уровня.

3 Вы можете использовать -X ours или -X theirsчтобы Git выбрал «наше изменение» или «их изменение» вместо того, чтобы прекратить конфликт.Обратите внимание, что вы указываете это в качестве аргумента git merge, поэтому он применяется к всем файлам, которые имеют конфликты.Это возможно сделать по одному файлу за раз, после возникновения конфликта, более интеллектуальным и избирательным способом, используя git merge-file, но Git не делает это так просто, как следовало бы.

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

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

Как работают git cherry-pick и git revert?

Это также триоперации слиянияОни используют граф фиксации таким же образом, как и git show.Они не такие причудливые, как git merge, даже если они используют слияние как глагол часть кода слияния.

Вместо этого мы начнем с любого графом фиксации, который у вас может быть, например:

...---o--P--C---o--...
      .      .
       .    .
        .  .
 ...--o---o---H   <-- branch (HEAD)

Фактические отношения , если есть , между H и P и между H и C, не важны.Единственное, что здесь имеет значение, это то, что текущий (HEAD) коммит - H, и что существует некоторый коммит C (дочерний) с (одним, единственным) родительским коммитом P,То есть P и C являются непосредственно родителем и коммитом коммита, который мы хотим выбрать или вернуть.

Так как мы на коммите H, это то, что находится в нашем индексеи дерево работы.Наша ГОЛОВА прикреплена к ветви с именем branch и branch пунктов для фиксации H. 6 Теперь, что Git делает для git cherry-pick <em>hash-of-C</em> прост:

  • Выберите commit P в качестве базы объединения.
  • Выполните стандартное трехстороннее объединение, объедините какглагол part, используя текущий коммит H в качестве нашего и коммит C в качестве их.

Этот процесс слияния как глагол происходит в индексе, как и для git merge.Когда все выполнено успешно - или вы убрали беспорядок, если не удалось успешно и вы запустили git cherry-pick --continue - Git продолжает делать обычным, неmerge commit.

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

  • diff commit P vsC: это то, что они изменили
  • diff commit P против H: это то, что мы изменили
  • объединить эти различия, применяя их к тому, что в P

Итак, git cherry-pick - это трехстороннее слияние.Просто то, что они изменили - это то же самое, что git show покажет!Между тем, то, что мы изменили - это все, что нам нужно, чтобы превратить P в H - и нам нужно это нужно, потому что мы хотим сохранить H как наша отправная точка, и только добавьте их изменения к этому.

Но это также как и почему вишневый пикант иногда видит какое-то странное - мы думаем -конфликты.Он должен объединить весь набор P -vs- H изменений с изменениями P -vs- C.Если P и H очень далеки друг от друга, эти изменения могут быть значительными.

Команда git revert столь же проста, как и git cherry-pick, и фактически реализована с помощью тех же исходных файлов.в Git.Все, что он делает, это использует commit C в качестве базы слияния и коммит P как их commit (при использовании H как наш, как обычно).То есть, Git выполнит diff C, обязательство вернуться, против H, чтобы увидеть, что мы сделали.Затем он будет C, обязательство вернуться, против P, чтобы увидеть, что они сделали - что, конечно, противоположно тому, что они на самом деле сделали.Затем механизм слияния, часть, которая реализует объединение как глагол , объединит эти два набора изменений, применив объединенные изменения к C и поместив результат в индекс и наше рабочее дерево.Объединенный результат сохраняет наши изменения (C против H) и отменяет их изменения (C против P, являющийся обратным дифференциалом).

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

...---o--P--C---o--...
      .      .
       .    .
        .  .
 ...--o---o---H--I   <-- branch (HEAD)

Разница от H до I, что мы и увидим с git show, это либо copy из P -то- C изменений (вишневый пик) или изменение из P -то- C изменений (возврат).


6 Как cherry-pick и revert отказываются запускаться, если индекс и рабочее дерево не соответствуют текущему коммиту, хотя у них есть режимы, которые позволяют им быть разными.«Разрешено быть другим» - это всего лишь вопрос изменения ожиданий.и тот факт, что если выбор или возврат не удастся , восстановление может быть невозможным.Если рабочее дерево и индекс соответствуют коммиту, его легко восстановить после неудачной операции, поэтому существует это требование.

...