Установка указателя git parent на другого родителя

Если в прошлом у меня был коммит, который указывает на одного из родителей, но я хочу изменить родителя, на которого он указывает, как мне поступить?

4 ответа

Решение

С помощью git rebase, Это общая команда "принять коммиты и добавить их / их к другому родителю (базе)" в Git.

Однако нужно знать кое-что:

  1. Поскольку в SHA для фиксации участвуют их родители, при смене родителя для данного коммита его SHA изменится, как и SHA для всех коммитов, которые последуют за ним (более поздним, чем он) в линии разработки.

  2. Если вы работаете с другими людьми и уже опубликовали коммит в том месте, где он его вытащил, изменение коммитов, вероятно, является плохой идеей ™. Это происходит из-за #1, и, таким образом, возникает путаница, с которой сталкиваются репозитории других пользователей, когда пытаются выяснить, что произошло из-за того, что ваши SHA больше не соответствуют их для "тех же" коммитов. (Подробности см. В разделе "ВОССТАНОВЛЕНИЕ ОТ ИСПОЛЬЗОВАНИЯ UPSTREAM" на связанной странице man.)

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

git rebase --onto <new-parent> <old-parent>

Это переместит все после <old-parent> в текущей ветке, чтобы сидеть на вершине <new-parent> вместо.

Если окажется, что вам нужно избегать перебазирования последующих коммитов (например, потому что переписывание истории было бы несостоятельным), тогда вы можете использовать git replace (доступно в Git 1.6.5 и более поздних версиях).

# …---o---A---o---o---…
#
# …---o---B---b---b---…
#
# We want to transplant B to be "on top of" A.
# The tree of descendants from B (and A) can be arbitrarily complex.

replace_first_parent() {
    old_parent=$(git rev-parse --verify "${1}^1") || return 1
    new_parent=$(git rev-parse --verify "${2}^0") || return 2
    new_commit=$(
      git cat-file commit "$1" |
      sed -e '1,/^$/s/^parent '"$old_parent"'$/parent '"$new_parent"'/' |
      git hash-object -t commit -w --stdin
    ) || return 3
    git replace "$1" "$new_commit"
}
replace_first_parent B A

# …---o---A---o---o---…
#          \
#           C---b---b---…
#
# C is the replacement for B.

С установленной выше заменой любые запросы к объекту B фактически возвращают объект C. Содержимое C в точности совпадает с содержимым B, за исключением первого родителя (те же самые родители (кроме первого), того же дерева, то же самое сообщение коммита).

Замены активны по умолчанию, но их можно отключить с помощью --no-replace-objects возможность git (перед именем команды) или установив GIT_NO_REPLACE_OBJECTS переменная окружения. Замены можно поделиться, нажав refs/replace/* (в дополнение к обычному refs/heads/*)

Если вам не нравится фиксация (сделанная с помощью sed выше), вы можете создать заменяющий коммит, используя команды более высокого уровня:

git checkout B~0
git reset --soft A
git commit -C B
git replace B HEAD
git checkout -

Большая разница в том, что эта последовательность не распространяется на дополнительных родителей, если B является коммитом слияния.

Обратите внимание, что изменение коммита в Git требует, чтобы все коммиты, которые следуют за ним, также должны быть изменены.Это не рекомендуется, если вы опубликовали эту часть истории, и кто-то мог построить свою работу на истории, какой она была до изменений.

Альтернативное решение дляgit rebase в ответе Амбер упоминается использование механизма прививки(см. определение прививки Git в глоссарии Git и документацию .git/info/graftsфайл в документации Git Repository Layout), чтобы изменить родительский элемент коммита, проверьте, что он исправил ошибку с помощью некоторого средства просмотра истории (gitk,git log --graphи т. д.), а затем использоватьgit filter-branch (как описано в разделе "Примеры" на его странице руководства), чтобы сделать его постоянным (а затем удалить трансплантат и, при необходимости, удалить исходные ссылки, подкрепленные git filter-branchили отложить хранилище):

echo "$ commit-id $ graft-id" >>.git / info / grafts
git filter-branch $ graft-id..HEAD

НОТА!!! Это решение отличается от решения ReBase тем, что git rebase будет перебазировать / пересаживать изменения, в то время как решение на основе трансплантатов будет просто переопределять коммиты как есть, не принимая во внимание различия между старым родителем и новым родителем!

Чтобы уточнить приведенные выше ответы и бесстыдно подключить свой собственный скрипт:

Это зависит от того, хотите ли вы "перебазировать" или "переучить". Ребаз, как предлагает Амбер, перемещается по разным ссылкам. Родитель, как предположили Якуб и Крис, двигается вокруг снимков всего дерева. Если вы хотите, чтобы reparent, я предлагаю использовать git reparent вместо того, чтобы делать работу вручную.

сравнение

Предположим, у вас есть изображение слева, и вы хотите, чтобы оно выглядело как изображение справа:

                   C'
                  /
A---B---C        A---B---C

Как перебазирование, так и повторное воспитание будут давать одну и ту же картину, но определение C' отличается. С git rebase --onto A B, C' не будет содержать никаких изменений, внесенных B, С git reparent -p A, C' будет идентичным C (Кроме этого B не будет в истории).

Определенно ответ @Jakub помог мне несколько часов назад, когда я пробовал то же самое, что и OP.
Однако,git replace --graftтеперь более легкое решение относительно трансплантатов. Кроме того, основная проблема с этим решением заключалась в том, что filter-branch заставлял меня терять каждую ветку, которая не была объединена с веткой HEAD. Потом,git filter-repo справился со своей работой отлично и безупречно.

$ git replace --graft <commit> <new parent commit>
$ git filter-repo --force

Для получения дополнительной информации: ознакомьтесь с разделом "История пересадки" в документации.

Другие вопросы по тегам