Git: как скомбинировать все коммиты между двумя коммитами в один коммит



У меня есть филиал, над которым я лично работал на нескольких компьютерах в течение последних нескольких месяцев. В результате получается длинная цепочка истории, которую я хочу очистить, прежде чем объединять ее в главную ветвь. В конечном счете цель состоит в том, чтобы избавиться от всех тех НЗП коммитов, которые я часто делаю при работе над кодом сервера.



Вот скриншот визуализации истории gitk:



Введите описание изображения здесьhttp://imgur.com/a/I9feO



Путь в нижней части этого точка, где я отделился от мастера. Мастер немного изменился с тех пор, как я начал эту ветвь, но изменения были разрозненными, поэтому слияние должно быть куском пирога. Мой обычный рабочий процесс состоит в том, чтобы перебазироваться на master, а затем раздавить фиксации wip.



Я попытался выполнить простой



git rebase -i master


И я отредактировал коммиты на sqush.



Казалось, что все начиналось хорошо, но потом не получилось, и мне захотелось разрешить конфликт. Тем не менее, казалось, что не было никакого хорошего способа, чтобы обратитесь к нему, посмотрев на диффы. Каждая часть использовала переменные, которые не были определены в области видимости, поэтому я не был уверен, как их разрешить.

Я также попытался использовать git rebase -i -s recursive -X theirs master, что не привело к конфликту, но изменило состояние HEAD из пересмотренной ветви (я хочу отредактировать историю таким образом, чтобы конечный результат в HEAD не менялся).



Я считаю, что эти конфликты возникают из тех частей цепи, где вы можете видеть алмазный узор. (напр. между переделанными классификаторы... и объединить ветку iccv).



Для формулировки моего вопроса лучше пусть A= "слияние ветвей iccv", а B=" переработанные классификаторы " обратитесь к примеру на рисунке. А коммиты между ними будут X и Y.



      ...
|
|
A
/
| X
Y |
/
B
|
|
...


Я хочу переписать историю так, чтобы состояние A было точно таким, как оно есть, и эффективно уничтожить промежуточные представления X и Y, чтобы полученная история выглядела так

      ...
|
|
A
|
|
B
|
|
...


Есть ли способ раздавить разрешенное состояние A, X и Y в один коммит в середине такой цепи истории?



Если A и B являются Шейдами коммитов, есть ли простая команда, которую я могу выполнить (или, возможно, сценарий), которая достигает желаемого результата?



Если бы A была голова, я думаю, что мог бы сделать



git reset B
git commit -am "recreating the A state"


Чтобы создать новую голову, но как я могу это сделать, если A находится в середине такой цепи истории, как эта. Я хочу сохранить эту историю всех узлов, которые приходят после него.

705   2  

2 ответов:

Это может быть сделано, но это где-то от небольшой боли, до большой боли, с обычными механизмами.

Фундаментальная проблема заключается в том, что вы должны копировать коммиты на новые (немного отличающиеся) коммиты всякий раз, когда вы хотите что-то изменить. Причина в том, чтоникакая фиксация никогда не может изменить .1 причина в том, что хэш-идентификатор коммита является коммитом в очень реальном смысле: хэш-идентификаторы Git-это то, как Git находит базовый объект. Измените любой бит внутри объекта, и он получит новый, другой идентификатор хэша.2 следовательно, когда вы хотите идти от:
       X
      / \
...--B   A--C--D--E   <-- branch
      \ /
       Y

К чему-то, что выглядит так:

...--B--A--C--D--E   <-- branch

Вещь после B не может быть A, это должен быть другой коммит, который просто пахнет как A. Мы можем назвать это коммитом A', Чтобы отличить их друг от друга:

...--B--A'-...
Но если мы скопируем A на новое, более свежее по запаху (но такое же дерево) A', у которого больше нет промежуточного материала в его истории-то есть, A' соединяется непосредственно с B - тогда мы должны также скопировать первый коммит после A'. Как только мы это сделаем, мы должны скопировать коммит после этого, и так далее. В результате получается:
...--B--A'-C'-D'-E'  <-- branch

1психологи любят говорить, чтоизменение трудно , Но для Git это буквально невозможно! :- )

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


Используя git rebase -i

Примечание: используйте этот метод, если это возможно; это гораздо легче понять и получить право.

Стандартная команда, копирующая коммиты, выглядит так: git rebase. Тем не менее, rebase очень плохо справляется с слияние коммитов типа A. На самом деле, он обычно выбрасывает их полностью, предпочитая вместо линеаризации все:

...--B--X--Y'-C'-D'-E'   <-- branch

Например.

Теперь, если коммит слияния A прошел хорошо, то есть ничто в X не зависит от Y или наоборот, простого git rebase -i <hash-of-B> может быть достаточно. Вы можете изменить все, кроме первого из picks для коммитов X и Y-которые на самом деле могут быть многими коммитами-на squash , и все просто идет хорошо, и вы сделали: git drops X и Y' полностью в пользу одного комбинированного XY' коммита, который имеет то же дерево, что и ваш коммит слияния A. В результате получается:
...--B--XY'-C'-D'-E'   <-- branch

А если мы позвоним XY' A', а затем отбросьте все галочки, забыв их оригинальные хэш-идентификаторы, мы получим именно то, что вы хотели.


Используя git replace

Если слияние было трудным, то вы хотите сохранить дерево от слияния, отбросив все фиксации X и Y. Здесь git replace является (или) правильным решением . Замена Git несколько сложна, но вы можете поручить Git сделать новый коммит A', который "похож на A, но имеет B в качестве единственного родительского хэш-идентификатора". Git теперь будет иметь такую структуру графика фиксации:

       X
      / \
...--B   A--C--D--E   <-- branch
     |\ /
     | Y
     \
      A'  <-- refs/replace/<complicated-thing>

Это специальное refs/replace имя говорит Git, что, когда он делает такие вещи, как git log и другие команды, которые используют идентификаторы commit, Git должен отворачивать свои метафорические глаза от commit A и смотреть вместо этого на commit A'. Поскольку A' в противном случае является копией A, git checkout <hash of A> заставляет Git смотреть на A' и проверять то же самое дерево; и git log показывает то же самое сообщение журнала, когда он смотрит в сторону на A' вместо A.

обратите внимание, что и A, и A' существуют в репозитории на данный момент. они как бы бок о бок, и Git просто показывает вам A' вместо A, Если вы не используете специальный флаг --no-replace-objects. Как только Git показал вам (и использовал) A' вместо A, он следует по обратной ссылке от A' к B, пропуская прямо над всеми X и Y.

Делая замену постоянной, сбрасывая X и Y полностью

Как только вы будете довольны заменой, вы можете сделать ее постоянной. Вы можете сделать это с помощью git filter-branch, который просто копирует коммиты. Он копирует, начиная с некоторой начальной точки и двигаясь вперед в истории, в обратном направлении от нормального обратного Git " начните с сегодняшнего дня и работайте назад в история " манера.

Когда filter-branch делает свои копии-и свой список того, что нужно скопировать,-он обычно делает то же самое, что и остальная часть Git. Таким образом, если у нас есть история, показанная выше, и мы говорим filter-branch, чтобы закончить на branch и начать сразу после commit B, он соберет существующий список commit как:

E, D, C, A'

А затем изменить порядок. (На самом деле, мы могли бы остановиться на A', если захотим, как мы увидим.)

Далее, фильтр-ветвь будет копировать A' к новому коммиту. Этот новый коммит будет иметь B в качестве своего родителя, то же самое сообщение журнала, что и A', то же дерево, тот же автор и дата-штампы и так далее-короче говоря, он будет буквально идентичен. Таким образом, он получит тот же идентификатор хэша, что и A', и фактически будет commit A'.

Далее, filter-branch скопирует C в новый коммит. Этот новый коммит будет иметь A' в качестве своего родителя, то же самое сообщение журнала, что и C, и то же самое дерево и так далее. Это немного отличается от оригинала C, родителем которого является A, а не A'. Таким образом, этот новый коммит получает другой хэш-идентификатор: он становится коммитом C'.

Далее, filter-branch будет копировать D. Это станет D', точно так же, как копия C была C'.

Наконец, filter-branch скопирует E в E' и заставит branch указать на E', давая нам следующее:

       X
      / \
...--B   A--C--D--E   <-- refs/original/refs/heads/branch
     |\ /
     | Y
     \
      A'  <-- refs/replace/<complicated-thing>
       \
        C'-D'-E'  <-- branch

Теперь мы можем удалить имя refs/replace/ и резервную копию refs/heads/branch, которую фильтр-ветвь сделал для сохранения оригинал E. Когда мы это делаем, имена убираются с дороги, и мы можем заново нарисовать наш график:

...--B--A'-C'-D'-E'  <-- branch
Это именно то, что мы хотели (и получили) от использования git rebase -i, но без необходимости делать слияние снова и снова.

Механика фильтра-ответвления

Чтобы сказать git filter-branch, где остановиться, используйте ^<hash-id> или ^<name>. В противном случае git filter-branch не перестанет перечислять коммиты для копирования, пока не закончатся коммиты: он будет следовать за commit B к своему родителю, и к этому родитель родителя, и так далее на всем протяжении истории. Копии этих коммитов будут бит за бит идентичны оригиналам, что означает, что они на самом деле будут оригиналами, одинаковым хэш-идентификатором и всем остальным; но на их создание уйдет много времени. Поскольку мы можем остановиться на <hash-id-of-B> или даже <hash-id-of-A'>, мы можем использовать ^refs/replace/<hash> для идентификации commit A. Или мы можем просто использовать ^<hash-id>, что, вероятно, на самом деле проще.

Кроме того, мы можем написать либо ^<hash> branch, либо <hash>..branch. Оба означают одно и то же (Подробнее см. документацию gitrevisions). Итак:

git filter-branch -- <hash>..branchname

Достаточно сделать фильтрацию, чтобы цементировать замену на место.

Если все прошло хорошо, удалите ссылку refs/original/, как показано в конце документации git filter-branch , а также удалите ссылку на замену, и все готово.


Использование cherry-pick

В качестве альтернативы git replace, вы также можете использовать git cherry-pick для копирование коммитов. Подробности см. В ответе Элпиекея. Это принципиально та же идея, что и раньше, но использует инструмент "копировать коммиты" вместо инструмента "перебазировать для копирования коммитов, а затем скрыть оригиналы". Он имеет один сложный шаг, используя git reset --soft, чтобы получить индекс, настроенный на соответствие commit A, чтобы сделать commit A'.

Сначала очистите текущее рабочее дерево, а затем выполните следующие команды:

#initial state

Введите описание изображения здесь

git branch backup thesis4
git checkout -b tmp thesis4

Введите описание изображения здесь

git reset A --hard

Введите описание изображения здесь

git reset B --soft

Введите описание изображения здесь

git commit

Введите описание изображения здесь

git cherry-pick A..thesis4

Введите описание изображения здесь

git checkout thesis4

Введите описание изображения здесь

git reset tmp --hard
git branch -D tmp

Введите описание изображения здесь

S это сквош из X,Y,A. M' эквивалентно M и N' N. В случае если вы хотите восстановить исходное состояние, запустите

git checkout thesis4
git reset backup --hard

Comments

    Ничего не найдено.