Git возвращается и выбирает вишни

Что-то я озадачен, вот краткое содержание.

В нашем репозитории есть ветка master и ветка d2l_phase_4. С момента создания ветки d2l_phase_4 в master было добавлено несколько новых коммитов.

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

Поэтому я сделал это:

  1. Отменил коммит и отправил его на fork/master
  2. Сделал запрос на вытягивание основного репо/мастера.
  3. Слил этот запрос на извлечение.

На данный момент мы благополучно отменили эффект коммита на нашей главной ветке.

Затем я сделал это:

  1. Проверил ветку d2l_phase_4
  2. Черри выбрал оригинальный коммит из master.
  3. отправлен на разветвление/d2l_phase_4
  4. Сделал запрос на вытягивание, нацеленный на основной репозиторий/d2l_phase_4.
  5. Объединил этот запрос на включение

На данный момент в ветке d2l_phase_4 есть желаемые изменения по сравнению с исходным коммитом, пока все хорошо.

Наконец, я хотел обновить ветку d2l_phase_4 со всеми остальными материалами на master с момента создания ветки d2l_phase_4. Итак, снова в ветке d2l_phase_4 я слился с мастером, и это создало новый коммит слияния.

Затем я отправляю эту обновленную ветку d2l_phase_4 в свою вилку / d2l_phase_4 и, наконец, создаю запрос на включение, нацеленный на основное хранилище / d2l_phase_4, а затем объединяю этот PR.

Однако до меня дошло, что Id теперь слился с более ранним возвратом из ветки master и поэтому, должно быть, аннулировал фиксацию, которую я ранее выбрал!

Однако при проверке состояния файлов на d2l_phase_4 нет никаких следов реверта, который был слит с мастера - это именно то, что я хочу, но я не понимаю.

Почему моя избранная фиксация на d2l_phase_4 не была аннулирована возвратом, который был объединен с мастером? Я ожидал, что мне нужно будет отменить мой возврат в ветке d2l_phase_4, но когда я попытался, этот git, казалось, сказал мне, что нечего возвращать...

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


person Hugh    schedule 03.02.2017    source источник


Ответы (1)


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

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

...--A--B--C--D--E   <-- master
         \
          F--G--H    <-- dl2_phase_4

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

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

«Таргетинг» не имеет значения; важен график коммитов. Запрос на вытягивание просто означает, что кто-то опубликовал репозиторий с теми же коммитами, что и у вас, плюс как минимум еще один коммит. Ключевой вопрос заключается в том, где этот коммит находится в их графике, и я не знаю на него ответа. Но давайте просто скажем, что оно идет после D, чтобы оно выглядело так:

                __I   <-- their-commit
               /
...--A--B--C--D--E    <-- master
         \
          F--G--H     <-- dl2_phase_4

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

Затем это, как вы говорите, объединяется в master, давая новый коммит слияния (я бы назвал его M, но у нас будет больше слияний, поэтому давайте просто продолжим с отдельными буквами и назовем его J):

                __I
               /   \
...--A--B--C--D--E--J   <-- master
         \
          F--G--H    <-- dl2_phase_4

(нам больше не нужно обозначать I, так как это просто второй родитель J — первый родитель — E; различие между первым и вторым родителем обычно теряется на этих рисунках).

Затем вы, в соответствии с вашими тремя шагами, откатываете слияние, используя длинный запутанный процесс (отталкивание к разветвлению, откат от разветвления), который дает вам тот же эффект, что и простое откатывание коммита I в новом коммите K на master:

                __I
               /   \
...--A--B--C--D--E--J--K   <-- master
         \
          F--G--H          <-- dl2_phase_4

Самое важное, что следует отметить в отношении фиксации K, заключается в том, что ее дерево — сохраненный снимок — точно такое же, как дерево из фиксации E. J — это просто слияние E и I (следовательно, J = E + I)1, а K имеет добавленное «отрицательное I», следовательно, K = E + I — I = E.


1Это не гарантируется! Если I дублирует изменение, которое находится в E, то J на самом деле не является Е плюс I, это Е плюс I минус дублированная часть. Но если бы это было так, мы бы увидели эффект позже, поэтому I не должно дублировать что-то в E, или, возможно, I идет после E. Вот почему так важно иметь граф, а иногда и сам репозиторий.


Затем вы прошли еще один длинный запутанный процесс, который на самом деле сводится к простому git cherry-pick:

  1. Черри выбрал исходный коммит из мастера [в dl2_phase_4].

Непонятно, какой из них является "оригиналом" (I или J, хотя если I на самом деле было перемотано, то нет J), но это тоже не имеет значения: мы просто получаем копию, которую я назову I':

                __I
               /   \
...--A--B--C--D--E--J--K   <-- master
         \
          F--G--H--I'      <-- dl2_phase_4

Хотя дерево для I' отличается от дерева для I, два относительных изменения — то, что мы получили бы при запуске git diff <id-of-D> <id-of-I> и git diff <id-of-H> <id-of-I'>, — это тоже самое. Вот что делает git cherry-pick: он сравнивает некоторую фиксацию с родительской, затем применяет полученную разницу к текущей фиксации и делает новую фиксацию.

Наконец, я хотел обновить ветку d2l_phase_4 со всеми остальными материалами на master с момента создания ветки d2l_phase_4. Итак, снова в ветке d2l_phase_4 я слился с мастером, и это создало новый коммит слияния.

Теперь давайте нарисуем это:

                __I
               /   \
...--A--B--C--D--E--J--K     <-- master
         \              \
          F--G--H--I'----L   <-- dl2_phase_4

Однако - до меня дошло, что я сейчас слился в более ранний реверт из ветки master... [но] изучив состояние файлов на d2l_phase_4, нет никаких следов реверта, который был слит из master - это точно что я хочу - но я не понимаю.

Это попадает в первый ключевой элемент:

Git merge не смотрит ни на один из «внутренних» коммитов.

Почему моя избранная фиксация на d2l_phase_4 не была аннулирована возвратом, который был объединен с мастером? [отрезать]

Как видно из графика, мы объединяем фиксацию K, оставаясь «включенной» фиксацией I'. база слияния — фиксация фиксации, в которой соединяются линии графика — это фиксация B.

Процесс слияния, слияние как глагол, работает, по сути, выполняя (и затем объединяя) два git diffs: один из базы слияния в текущую фиксацию и один из базы слияния в другую. совершить. Следовательно:

git diff <id-of-B> <id-of-I'>
git diff <id-of-B> <id-of-K>

Мы уже отмечали, что с точки зрения дерева (на что смотрит git diff) фиксация K аналогична фиксации E. Таким образом, первый diff игнорирует как неправильную фиксацию, так и ее реверсию. Второй diff, сравнивающий B и I', рассматривает выбранный коммит как изменение, поэтому включает это изменение. Объединение первого и второго различий дает вам одну копию изменения.

Врет команда git log, немного (а иногда и много)

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

Теперь, когда у нас есть это окончательное слияние L, git log должно показать нам содержимое коммита L, содержимое первого и второго родителей L, содержимое их родителей и так далее.

Коммит L является коммитом слияния, поэтому у него есть два родителя, K и I'. I' — ваш лучший коммит, так что вы увидите его, когда git log дойдет до этой точки.

K — это простая фиксация с одним родителем, J; но J — это коммит слияния с двумя родителями, E и I. Вы ожидаете увидеть здесь коммит I, и — эта часть становится сложной — иногда вы это делаете.

Когда вы запускаете git log -- <pathspecs>, вы даете Git неявный аргумент, который (я думаю) не может быть написан явно, хотя --dense и --sparse могут быть достаточно близки для пояснительных целей. Они включают упрощение истории, а упрощение истории отбрасывает одну "сторону" слияния, если это возможно. В любом случае вы также указываете Git игнорировать все файлы, не указанные в ваших аргументах <pathspecs>.

Когда Git просматривает коммит L, это коммит слияния с двумя родителями, поэтому применяется эта часть правила упрощения истории:

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

Это «TREESAME» определяется как: после отбрасывания всех путей, кроме указанных, является ли усеченное дерево коммита таким же, как дерево одного из его родителей? Таким образом, для L мы сравниваем его с K и I' после удаления всех файлов, кроме одного, о котором вы спрашиваете. Если версия файла в L совпадает с версией в I' — возможно, так и есть, — тогда git log обрезает K в этот момент и больше не видит ни K, ни J, ни родителей J, ни E, ни D, ни C. Однако он будет следовать за I' назад к H, а затем к G и F и B и так далее, назад по истории.

Если версия файла в L совпадает с версией в K, то git log обрезает I'; но поскольку I' определенно коснулся файла, Git следует здесь только за I'.

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

Примеры всего этого есть в документации git log, но это определенно блестящий материал.

Еще одна вещь, которую следует отметить, это то, что git log часто ничего не говорит о самих коммитах слияния, если только вы не дадите команду «разделить» их (с помощью -m) или принудительно объединить diff (с помощью -c или --cc). То есть он может распечатать сообщение журнала, но не показать никаких изменений, даже если git show что-то покажет (комбинированный diff в стиле --cc).

person torek    schedule 03.02.2017
comment
Спасибо за объяснение слияния, я думаю, что теперь понимаю это. Кстати, рабочий процесс, который кажется трудоемким, связан с тем, что по замыслу PR — единственный способ получить коммиты в репозиториях нашей организации. Они полагаются на то, что у всех нас есть вилки. Кроме того, фиксация, первоначально объединенная PR, была простой быстрой перемоткой вперед, без буквы «E». - person Hugh; 04.02.2017
comment
@Hugh: это (форки и запросы на вытягивание) имеет смысл, хотя в зависимости от того, насколько вы доверяете способностям и компетентности своих программистов, вы можете упростить его до одного центрального репозитория с ветвями для каждого пользователя. :-) - person torek; 04.02.2017