Почему GitHub разрешает ветку с коммитами впереди и сзади, но не содержит изменений, которые нужно объединить? - PullRequest
0 голосов
/ 27 июня 2018

Недавно была объединена ветка, в которой было исправлено 3 строки. На следующий день был отправлен запрос ветки, содержащий 2 строки трехстрочного исправления. Это было несколько коммитов позади, позади оригинальной объединенной ветви. Дерево будет выглядеть примерно так ...

A1--A2--A3-------B3--C4
     \   \      /   /
      \   A3--B3   /
       \          /
        A2------C3

Таким образом, B3 будет содержать исправление из 3 строк, а C3 будет содержать 2 строки. Когда я иду проверять C4, он показывает разницу для коммита, хотя технически его нет. Фактически, когда выполняется интерактивная перебазировка, она останавливается и говорит, что это пустой коммит.

Я знаю, что git позволяет вам объединять пустые коммиты, но меня смущает:

  1. Почему GitHub позволяет вам объединять (и в определенной степени git), когда он обычно не позволяет, когда нет изменений. Я предполагаю, что это связано с коммитами вперед / назад, но просто любопытно, что является основной причиной
  2. Почему git показывает diff для коммитов, которые не делали diff (предназначено для sort'a)?

1 Ответ

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

TL; DR

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

Long

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

A1--A2--A3-------B5--C4
     \   \      /   /
      \   B3--B4   /
       \          /
        C2------C3

или, возможно:

A1--A2--A3-------B4--C4
     \   \      /   /
      \   ----B3   /
       \          /
        --------C3

И последнее, прежде чем я начну, маркировка этих вещей как последовательности An, Bn и Cn s не очень полезна с точки зрения Git, так как не имеет значения, какие ветви содержат коммиты, когда дело доходит до просмотра и объединения коммитов. Важно то, что коммиты сами и их родительские / дочерние отношения - имена ветвей здесь только для людей. :-) Итак, позвольте мне перерисовать это как:

...--A--B--C------H--I   <-- branchname
         \  \    /  /
          \  D--E  /
           \      /
            F----G

, где H и I - коммиты слияния. (Чтобы сделать H, кто-то должен был бежать:

git checkout branchname

когда branchname указывает на C, а затем:

git merge --no-ff <identifier-selecting-E>

или что-то эквивалентное. --no-ff вынуждает это быть реальным слиянием, а не «слиянием» быстрого слияния. Обратите внимание, что кнопка GitHub «merge» делает это, то есть использует --no-ff.)

Когда я проверяю [последний коммит слияния, теперь называемый I на моей диаграмме - ed.], Он показывает разницу для коммита, даже если технически его нет ...

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

Когда у нас есть обычный коммит без слияния, такой как E, этот коммит имеет один родительский коммит, поэтому Git может легко показать коммит E, извлекая снимок его родительского коммита D, затем извлекая моментальный снимок для самого E и сравнивая их. Для каждого файла, который отличается в этих двух снимках, Git пытается создать минимальную последовательность редактирования, которая изменит D на E.

Если вы запускаете git commit и ваш индекс - область, из которой Git строит новые коммиты - совпадает с текущим коммитом, Git обычно просто говорит вам «ничего не коммитить» и отказывается делать коммит. Добавление --allow-empty говорит Git: делает новый снимок, даже если он идентичен текущему снимку. Дело не в том, что сам снимок пуст, а в том, что отличается от предыдущий коммит будет пустым. Следовательно, нет особой причины беспокоиться о создании нового снимка.

Коммиты слияния отличаются. С одной стороны, они являются , как обычные коммиты: они содержат снимок. Разница в том, что у них есть два или более 1 родителей, каждый из которых также содержит снимок. Это означает, что трудно показать разницу между двумя снимками, когда задействовано как минимум три снимка.

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

TheПо умолчанию команда git show делает что-то сверхумное: она извлекает все снимки родителей и дочерний коммит, а затем сравнивает каждый файл в потомке с all родительские версии этого же файла. Затем он выбрасывает любой файл, который точно соответствует любому родительскому варианту, и только для некоторого файла, который отличается от обоих родителей, показывает, что Git называет комбинированный diff . Объединенный формат вывода diff задокументирован на нескольких страницах справочника Git, в том числе на git diff.

Добавление флага -m к этим командам указывает Git на разбиение на слияние в несколько виртуальных коммитов, по одному для каждого родителя. То есть вместо того, чтобы показывать комбинированный дифференциал для I против обоих H и G, Git покажет один дифференциал H -vs- I, а затем второй дифференциал G -vs- I. Эти различия содержат больше информации: например, если файл README.txt совпадает с H -vs- I, а foo.txt отличается, вы увидите foo.txt. Если README.txt отличается G -vs- I, а foo.txt - то же самое, вы увидите README.txt. Но если вы получите комбинированный diff, Git выбросит оба README.txt (H 's совпадения I' s) и foo.txt (G 's соответствует I s), поэтому вы не увидите ни одного файла.

Теперь, даже если все файлы точно совпадают в обоих входных коммитах - т.е. если моментальные снимки в H и G точно совпадают - Git все равно сделает коммит слияния, потому что коммит слияния Сам по себе I с двумя родительскими хеш-идентификаторами, указывающими на H и G, - это то, что фиксирует тот факт, что было слиянием. Это устанавливает новую базу слияния для будущих слияний. Git вычисляет базу слияния для последующей операции git merge, используя граф, поэтому нам нужен узел графа, который соединяет эти два узла. Это не так в вашем примере, но стоит отметить.


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


Фактически, когда выполняется интерактивная перебазировка, перебазирование останавливается и говорит [слияние] - пустой коммит.

Ребаз работает, копируя коммиты. Это требует превращения каждого коммита в набор изменений, дифференцируя коммит против его родителя. У слияний по определению есть по крайней мере два родителя, и поэтому невозможно скопировать таким образом. 2 Решение Rebase для этой дилеммы состоит в том, что оно удаляет коммитов слияния полностью. Однако:

... Таким образом, [E] будет содержать исправление из 3 строк, а [G] будет содержать 2 строки.

Давайте нарисуем имя второй ветви, указывающее на G, чтобы использовать его для перебазирования, не делая коммит слияния I вообще:

...--A--B--C------H   <-- branchname
         \  \    /
          \  D--E
           \
            F----G   <-- feature

Мы можем запустить git checkout feature, а затем git rebase branchname (с или без --interactive). Git будет перечислять коммиты, достижимые с feature, которые недоступны с branchname, начиная с G и работая в обратном направлении, начиная с H и работая в обратном направлении. Эти два шага «работать в обратном направлении» объединяются при коммите B, поэтому B является , а не копируется, и не является никаким более ранним коммитом. Это оставляет коммиты F и G для копирования в указанном порядке.

Копия F, вероятно, просто работает, давая:

...--A--B--C------H   <-- branchname
         \  \    / \
          \  D--E   F'  <-- HEAD
           \
            F----G   <-- feature

(обратите внимание, что rebase работает с отдельным HEAD, указывая непосредственно на какой-то коммит).

Теперь Git готов скопировать изменение в G, которое должно изменить две из трех уже измененных строк, оставив одну в покое. Git видит, что это бесполезно - эти две строки уже изменены - и, в зависимости от контекста, либо объявляет конфликт слияния (так что вам нужно все очистить и решает, какая версия является правильной), либо просто решает, что ничего нет изменить. Если Git останавливается с конфликтом слияния, вы исправляете его, используя версию с изменением трех строк из commit F'. Вы запускаете git rebase --continue, чтобы заставить его продолжать работать, и в этот момент Git говорит, что нечего коммитить. Теперь у вас есть выбор: зафиксировать с помощью --allow-empty, чтобы сохранить сообщение о фиксации и отдельный снимок (соответствующий его родительскому элементу), или полностью удалить коммит, используя git rebase --skip.

Предполагая, что вы делаете последнее, Git завершает ребазирование, перемещая имя (feature), чтобы оно указывало на последний скопированный коммит, и устанавливая ORIG_HEAD, чтобы запомнить, где feature использовалось для указания:

...--A--B--C------H   <-- branchname
         \  \    / \
          \  D--E   F'  <-- feature (HEAD)
           \
            F----G   <-- ORIG_HEAD

и все готово.


2 git rebase -i имеет дополнительный флаг --preserve или -p, который утверждает, что сохраняет слияния. Это не так: на самом деле повторно выполняет слияния. То есть он копирует достаточно коммитов, чтобы установить условия для слияния, затем запускает git merge, чтобы выполнить новое слияние. Если вы разрешали конфликты раньше, вам, вероятно, придется сделать это снова. Даже если вам раньше не приходилось разрешать конфликты, вам, возможно, придется на этом этапе. Если вы произвели более раннее слияние с git merge --no-commit с последующим редактированием содержимого, за которым следовали git merge --continue или git commit, чтобы произвести так называемое зло слияния , Git не знает, что вы сделали это, и делает новое не злое слияние, которое отбрасывает всю вашу подлость. Если эта подлость была на самом деле хорошей вещью, несмотря на термин злое слияние , вы потеряете ее.

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