Git: как скомбинировать все коммиты между двумя коммитами в один коммит
У меня есть филиал, над которым я лично работал на нескольких компьютерах в течение последних нескольких месяцев. В результате получается длинная цепочка истории, которую я хочу очистить, прежде чем объединять ее в главную ветвь. В конечном счете цель состоит в том, чтобы избавиться от всех тех НЗП коммитов, которые я часто делаю при работе над кодом сервера.
Вот скриншот визуализации истории gitk:
Путь в нижней части этого точка, где я отделился от мастера. Мастер немного изменился с тех пор, как я начал эту ветвь, но изменения были разрозненными, поэтому слияние должно быть куском пирога. Мой обычный рабочий процесс состоит в том, чтобы перебазироваться на 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 находится в середине такой цепи истории, как эта. Я хочу сохранить эту историю всех узлов, которые приходят после него.
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 dropsXи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 должен отворачивать свои метафорические глаза от commitAи смотреть вместо этого на commitA'. Поскольку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и начать сразу после commitB, он соберет существующий список commit как:E, D, C, A'А затем изменить порядок. (На самом деле, мы могли бы остановиться на
A', если захотим, как мы увидим.)Далее, фильтр-ветвь будет копировать
A'к новому коммиту. Этот новый коммит будет иметьBв качестве своего родителя, то же самое сообщение журнала, что иA', то же дерево, тот же автор и дата-штампы и так далее-короче говоря, он будет буквально идентичен. Таким образом, он получит тот же идентификатор хэша, что иA', и фактически будет commitA'.Далее,
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' <-- branchgit rebase -i, но без необходимости делать слияние снова и снова.Механика фильтра-ответвления
Чтобы сказатьgit filter-branch, где остановиться, используйте^<hash-id>или^<name>. В противном случаеgit filter-branchне перестанет перечислять коммиты для копирования, пока не закончатся коммиты: он будет следовать за commitBк своему родителю, и к этому родитель родителя, и так далее на всем протяжении истории. Копии этих коммитов будут бит за бит идентичны оригиналам, что означает, что они на самом деле будут оригиналами, одинаковым хэш-идентификатором и всем остальным; но на их создание уйдет много времени. Поскольку мы можем остановиться на<hash-id-of-B>или даже<hash-id-of-A'>, мы можем использовать^refs/replace/<hash>для идентификации commitA. Или мы можем просто использовать^<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, чтобы получить индекс, настроенный на соответствие commitA, чтобы сделать commitA'.
Сначала очистите текущее рабочее дерево, а затем выполните следующие команды:
#initial stategit branch backup thesis4 git checkout -b tmp thesis4git reset A --hardgit reset B --softgit commitgit cherry-pick A..thesis4git checkout thesis4git reset tmp --hard git branch -D tmp
Sэто сквош изX,Y,A.M'эквивалентноMиN'N. В случае если вы хотите восстановить исходное состояние, запуститеgit checkout thesis4 git reset backup --hard









Comments