Как поменять порядок двух родителей на Git commit?

Согласование слияния - это фиксация с по меньшей мере двумя родителями. Эти родители находятся в определенном порядке.

Если я нахожусь в ветке master, и я сливаюсь в ветке feature, я создаю новую фиксацию с ее первым родителем, являющимся фиксацией из master, а вторая фиксация является фиксацией из feature. Этот порядок особенно очевиден, запустив git log --first-parent.

*   The merge commit
|\
| * The commit from `feature`
* | The commit from `master`

Скажем, теперь я понимаю, что порядок неправильный: я намеревался объединить ветвь master в feature, запустив git checkout feature; git merge master. Я хочу поменять порядок родителей на фиксацию слияния, но я не хочу перебирать проблемы с повторением всех конфликтов слияния. Как я могу это сделать?

*   The merge commit
|\
* | The commit from `feature`
| * The commit from `master`

Ответы

Ответ 1

Фактически, там действительно классная команда, которую я недавно узнал, будет делать именно то, что вы хотите:

git commit-tree -p HEAD^2 -p HEAD^1 -m "Commit message" "HEAD^{tree}" 

Это создаст новую фиксацию, основанную на том, что в настоящее время HEAD, но притворится, что это родители HEAD ^ 2, HEAD ^ 1 (обратите внимание, что это обратный порядок).

git -commit-tree печатает новую ревизию как вывод, поэтому вы можете объединить ее с git - reset -hard:

git reset --hard $(git commit-tree -p HEAD^2 -p HEAD^1 -m "New commit message" "HEAD^{tree}")

Ответ 2

Один из способов заключается в моделировании слияния. Итак, как мы можем это сделать?

Предположим, что у вас есть что-то вроде следующего графика фиксации:

* (master) Merge branch 'feature'
|\
| * (feature) feature commit
* | master commit
. .
. .
. .

Сохраните изменения

Мы хотим объединить master в feature, но мы хотим сохранить изменения, поэтому сначала мы переключаемся на master, из которого мы "вручную" обновляем нашу ссылку HEAD до точки feature, а не изменение рабочего дерева.

git checkout master
git symbolic-ref HEAD refs/heads/feature

Команда symbolic-ref похожа на git checkout feature, но не касается рабочего дерева. Таким образом, все изменения от master остаются.

Отменить старое слияние

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

(Если вы хотите сохранить сообщение о фиксации, сейчас самое подходящее время для его копирования где-нибудь сохранить.)

# Optional
git tag tmp master

git branch -f master master^

Теперь ваше дерево фиксации должно выглядеть так же, как перед слиянием.

Подделка слияния

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

git rev-parse master > .git/MERGE_HEAD

Если вы используете git bash, git теперь скажет вам, что он в настоящее время находится в процессе слияния.

Чтобы завершить наше слияние, нам просто нужно commit.

git commit
# Enter your commit message

И все. Мы воссоздали наше слияние, но с замененными родителями. Итак, вы фиксируете историю, теперь должны выглядеть так:

* (feature) Merge branch 'master'
|\
| * (master) master commit
* | feature commit
. .
. .
. .

Если вам нужна дополнительная информация, не стесняйтесь спрашивать.

Ответ 3

Обновить. Это никогда не бывает так просто, не так ли?

Я признал недостаток исходных инструкций, которые я предложил: при выполнении аргументов checkout с аргументами пути вы можете ожидать, что файл будет удален из вашего рабочего пути, потому что он не находится в проверке, которую вы проверяете вне; но тогда вы можете быть удивлены...


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

Предыдущие ответы в порядке, но если вы хотите избежать использования (или необходимости знать) команд сантехники или других внутренних действий git - и если это более важно для вас, чем однострочное решение, такое как решение Jared - тогда вот альтернатива:

Общая структура этого решения похожа на zeeker's, но где он использует команды сантехники для управления HEAD или "трюки" git для завершения слияния, мы просто используем фарфор.

Итак, как и раньше,

* (master) Merge Commit
| \
| * (feature) fixed something
* | older commit on master

Пусть начнется:

1) Пометьте старое слияние

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

git tag badmerge master

Теперь мы имеем

* (master) [badmerge] Merge Commit
| \
| * (feature) fixed something
* | older commit on master

2) Возьмите слияние из мастер-истории

git branch -f master master^

Достаточно просто:

* [badmerge] Merge Commit
| \
| * (feature) fixed something
* | (master) older commit on master

3) Начните новое слияние

git checkout feature

Если мы просто запустим git merge master сейчас, мы знаем, что слияние не удастся; но мы не хотим повторять ручное разрешение конфликтов. Если бы у нас был способ наложить данные из badmerge на наш новый комманд слияния, мы бы все установили... и мы делаем!

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

git merge --no-commit --strategy=ours master

4) Наложение уже разрешенных/объединенных данных из badmerge

Удостоверившись, что мы находимся в корневом каталоге репо, мы могли бы сделать git checkout badmerge -- . (обратите внимание, что мы предоставили путь (.) до git checkout, поэтому он обновляет только рабочее дерево и индекс); но это может быть проблемой.

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

Мы могли бы сначала очистить рабочее дерево... но нам нужно быть осторожным, чтобы не уничтожить папку .git, и может потребоваться специальная обработка для чего-либо в нашем файле ignore.

В простом случае -.git - единственный. файл, ничего не игнорируется - мы могли бы сделать

rm -rf *
git checkout badmerge -- .

Или, если это кажется слишком рискованным, другой подход состоит в том, чтобы пропустить rm, сделать git checkout badmerge -- ., а затем использовать diff от badmerge, чтобы увидеть, нужно ли что-то очищать.

5) Завершите слияние

git commit
git tag -d badmerge

Ответ 4

Вдохновленный этим ответом, я придумал это:

git replace -g HEAD HEAD^2 HEAD^1 && 
git commit --amend && 
git replace -d [email protected]{1}

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

Вторая команда создает новый коммит.

Третья команда удаляет более старую заменяемую ссылку, поэтому она не портит другие коммиты в зависимости от этого коммита.

Ответ 5

Вы можете использовать git replace --graft (например, git replace --graft HEAD HEAD^2 HEAD^1) вместо переписывания истории. Он создает заменяющий коммит и сохраняет своих потомков без изменений.

Редактировать: созданные замены не являются общими по умолчанию. Вам необходимо настроить пульты, такие как push = refs/replace/*:refs/replace/* а также указать своим участникам установить fetch = refs/replace/*:refs/replace/*.

Смотрите также: