Почему моя ветка считает, что изменения от мастера "старые"? - PullRequest
2 голосов
/ 16 мая 2019

Фон

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

Задача

Одна из наших текущих веток попала в состояние, в котором git, по-видимому, считает, что содержимое ветви всегда новее, чем в master. Некоторые примеры:

  • Если мы объединяем master с веткой, любые файлы, которые являются новыми для master (добавляются после создания ветки), игнорируются; они не добавляются в ветку во время слияния. Я предполагаю, что git рассматривает их так, как если бы они были намеренно удалены в ветке.
  • Есть несколько файлов, которые были изменены в master с момента создания ветки. Запрос на вытягивание Bitbucket показывает изменение этих изменений как ожидающих изменений в master при объединении PR. Например, допустим, что строка master была изменена с
myConstant = 26

до

myConstant = 27

После слияния мастера с веткой PR показывает, что слияние [ветви обратно с мастером] изменит 27 [назад] на 26.

Примечание: это была довольно продолжительная ветвь функций, и у нее было несколько "ветвей подфункций". По крайней мере, один был объединен обратно. Другой по-прежнему существует, и обновления были объединены из главной в функциональную ветвь, а затем из функциональной ветви в вспомогательную ветвь.

Как исправить?

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

Даже если я найду дымящийся пистолет, я не уверен, что это лучший способ исправить ситуацию. Моя текущая мысль - перезаписать (извлечение от мастера) любые файлы, которые, как мы знаем, не были явно изменены в ветке. Очевидно, что в ветке также есть изменения, которые на новее, чем мастер, так что даже это утомительно и рискованно. Предложения приветствуются:)

Обновление 1

После продолжительного поиска и использования некоторых инструментов из превосходного ответа @ torek я обнаружил фиксацию проблемы - или хотя бы один из них. График коммитов выглядит примерно так (я использовал числа, чтобы избежать путаницы с буквами из предыдущего обсуждения. X s представляют один или несколько коммитов без слияния. Мастер внизу; я не заметил там отдельных коммитов.):

                                                                               X-------57-X--------61-X--------62--65
                                                                              /        /           /            \   \
                                                                           51-55-X-56- / -X-67-----73-------------63--66-71
                                                                          /       /  /     /      /                     /
    34-35-X----37-X-X-38-X-X-39------41-X-----42-X-----44-X-46---47-X-50-X-53---54--------60-----68                    /  
    /          /      /      /       / \      /        /        /    /          /                /                    /
32-33----36-X-X----- / ---- / ---X-40   ---- / --43-X---X---45-48-X-49-52---------58------------59-------69---72     /
/        /          /      /       /        /               /       /  /          /              \       /    /     /
-------------------------------------------------------------------------------------------       70--- / -- / -----
                                                                                           \      /    /    /
                                                                                            ----------------

Commit 39 оказывается проблемой. Похоже, что коммиттер запустил слияние, очистил все ожидающие изменения (без отмены слияния), сделал одно изменение файла, а затем зафиксировал. Сравнение с родительским коммитом, полученным из 38, показывает только одно изменение файла. Однако сравнение с родительским коммитом от master показывает подробный список непреднамеренных изменений.

Я сейчас на стадии "что с этим делать". Я копаюсь в опциях, включая изменение самого коммита 39, создание нового коммита, начиная с 71, чтобы отменить изменения в 39 и т. Д.

Обновление 2

После некоторого чтения кажется, что большинство людей рекомендуют один и тот же набор вещей - обрисованный довольно хорошо здесь . В настоящее время я думаю, что перебазирование этой далекой истории с кучей слияний может стать уродливым (хотя моя перебазировка Кунг-фу не сильная), поэтому я склонен придерживаться подхода возврата (git revert -m 2 <Commit_39_SHA>). В любом случае, Бисект был уже довольно хорошо разбит на этих ветвях.

План состоит в том, чтобы объединить все ветви подфункций в ветвь основной функции и выполнить возврат туда. Если я все правильно понимаю, я не думаю, что мне понадобится шаг «отменить возврат».

1 Ответ

4 голосов
/ 16 мая 2019

Это никогда не вопрос возраста . Для слияния это всегда вопрос баз слияния .

У каждого коммита есть родитель, а в случае коммитов слияния - два или более родителей. Истинное имя каждого коммита - это его хэш-идентификатор - большая уродливая строка букв и цифр, такая как 83232e38648b51abbcbdb56c94632b6906cc85a6. Это истинное имя позволяет Git найти фактический коммит. Сам коммит хранит хэш-идентификатор своих родителей, что позволяет Git находить те коммиты. Они хранят хэш-идентификаторы своих родителей по очереди и так далее. В результате для большинства коммитов существует простая линейная обратная цепочка:

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

, где H - некоторый хэш-идентификатор коммита, а родительский элемент H - G, родительский - F и т. Д.

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

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

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

Ответвления происходят, потому что два разных коммита имеют один общий коммит в качестве (общего) родителя:

             I--J   <-- branch1
            /
...--F--G--H
            \
             K--L   <-- branch2

Git будет запускаться при каждом коммитном коммите - в данном случае J и L - и работать в обратном направлении. При запуске из обеих веток и в обратном направлении, две ветки получают общий общий коммит H.

Если вы выберете одну ветку для git checkout, вы получите один коммит:

             I--J   <-- branch1 (HEAD)
            /
...--F--G--H
            \
             K--L   <-- branch2

Git прикрепил HEAD к имени branch1, что означает, что commit J - это зафиксированный вами коммит: branch1 идентифицирует commit J.

Теперь вы запускаете git merge branch2. Git находит commit L, потому что имя branch2 указывает на L. Начиная с J и L, Git работает в обратном направлении, один коммит за раз, чтобы найти общий коммит H. Это база слияния .

Git теперь сравнивает снимок в H - тот, с которого вы, на branch1, начали - со снимка в J, который вы, вероятно, также сделали и используете сейчас. Что бы здесь ни было отличается , это то, что вы изменили. Если вы изменили , измените myConstant = 26 на myConstant = 27, это ваше изменение в этом конкретном файле. Если вы не изменили myConstant вообще, вы сделали нет изменения.

Git также сравнивает снимок в H - который является лучшим, с которого они, на branch2, начали с того, с чего вы также начали (коммиты F и G тоже сработали бы, но они явно не так хорошо) - чтобы они совершили L, чтобы увидеть, что они изменили.

Сделав сравнение с H до J и снова с H до L, Git теперь знает, что you изменилось, и что они изменились. Не имеет значения , когда вы изменили вещи, или , когда они изменили вещи. Все, что имеет значение, это то, изменил ли кто-то что-то В любом случае, Git извлекает все файлы из H (не J или L), применяет оба набора изменений к ним и - если нет конфликтов - фиксирует результат:

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

Поскольку вы находитесь на branch1, новый коммит M продолжается branch1. Поскольку это последний коммит, Git обновляет имя branch1, чтобы идентифицировать коммит M. Между тем, тем не менее, коммит M имеет не один, а два родительских коммитов: J, ваш предыдущий совет и L, который по-прежнему является их ветвлением.

Теперь предположим, чтона вас и они внесли некоторые изменения в myConstant. Затем, при попытке объединить ваши два изменения, Git объявит конфликт слияния . Это оставит беспорядок в рабочем дереве (и индексе) для того, кто бы ни работал git merge, чтобы исправить. Затем human должен выяснить, что поместить в коммит M, отредактировав конфликтующие файлы и исправив эту строку. Что бы ни делал человек, Гит предполагает, что это правильный результат. Скажем, человек выбирает 26 вместо 27 и совершает это. Commit M теперь говорит myConstant = 26, а Git уверен, что это правильно , даже если это не так. Или мы можем сказать, что человек выбирает 27 вместо 26 и совершает это; теперь Git уверен, что 27 правильно.

Предположим, что вы и они продолжают совершать коммиты:

             I--J
            /    \
...--F--G--H      M--N--O   <-- branch1
            \    /
             K--L--P--Q--R   <-- branch2

Если вы сейчас выберете одну из этих веток для проверки, вы получите коммит O или коммит R. Допустим, вы выбрали O и снова запустили git merge branch2.

Git теперь начинается с O и работает в обратном направлении: O, N, M, оба- J -and- L .... И он начинается с R и работает в обратном направлении: R, Q, P, L, .... Обратите внимание, что оба они достигают коммита L. Commit L является базой слияния, а не M, L. Так что Git будет отличаться от M против O, чтобы увидеть, что вы изменили. Это может включать изменение 26 на 27 или - если M имеет неправильную константу - вообще ничего не делать. Затем он сравнивает L с R, чтобы увидеть, что они изменили.

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

             I--J
            /    \
...--F--G--H      M--N--O--S   <-- branch1 (HEAD)
            \    /        /
             K--L--P--Q--R   <-- branch2

где S - ваш результат слияния. Константа в этом файле устанавливается, однако вы, или Git, устанавливаете ее в зависимости от того, был ли конфликт и / или были ли вы и / или они изменили эту строку при сравнении L (базы слияния) с O и R (две стороны слияния).

Найти того, кто что-то изменил, легко, кроме случаев, когда это трудно

В Git есть несколько инструментов, с помощью которых вы можете попытаться выяснить, кто установил myConstant на любое значение. Два больших из них - git log и git blame. Оба начинают с некоторого коммита, чтобы увидеть, что находится в этом коммите, а затем работают назад, по одному коммиту за раз. Сравнивая то, что в родительском коммите с тем, что находится в child , Git может увидеть, скажем, что коммиты N и O имеют разные строки. Если это так, Git может сказать вам, что тот, кто сделал O, изменил эту строку.

Но когда Git работает в обратном направлении через слияние, такое как S или M, , какой родитель должен сравнивать Git с S? Ответы Git (множественное число) на это довольно сложно. Некоторые команды просто не мешают сравнивать - например, это то, что делает git log -p. Другие выбирают одного из родителей и продолжают этот участок слияния, то есть, начиная с S, возвращаются только к R или только O. Некоторые из них, такие как git show, могут показать вам комбинированный дифференциал , который, в общем, показывает только то место, где произошел конфликт слияния (комбинированные дифференциалы опускают все файлы, в которых не было материалов с обеих сторон). ).

Поскольку такого рода проблемы часто возникают при конфликтах слияний, комбинированные различия часто несколько полезны для нахождения источника. Это не обязательно очень полезно, хотя: более полезно использовать git bisect для автоматического поиска хороших и плохих фиксаций. Вы объявляете один коммит «хорошим» (если код работает / является правильным), а последующий коммит «плохим» (где код не работает / является неправильным), и Git автоматически выполняет поиск между этими двумя коммитами, работая с ответвлением. и-объединить структуры в графе фиксации, все автоматически.

ItТакже иногда полезно использовать git log -m -p. То, что это делает, это разделить каждое слияние. Вместо того, чтобы рассматривать S и M как один коммит каждый, он делает вид, с целью git diff -ing создать патч, что есть один S1 коммит с родителем O и второй S2 совершить с родителем R. Затем, достигнув M, он делает вид, что есть один M1 коммит с родителем J и второй M2 коммит с родителем L. Каждое из этих разделенных коммитов получает простой, не -комбинированный diff для своего (теперь одинокого) родителя, показывая вам, как результат слияния отличается от этого одного родителя.

Все это полезные инструменты, чтобы увидеть, что произошло. Тем не менее, они не предотвратят ошибочные слияния в будущем: все, что может сделать Git, - это сравнить базу с двумя советами, чтобы увидеть, что вы изменили против того, что они изменили. Не имеет значения , когда: только что изменилось имеет значение.

Редактировать: Форматирование

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