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

220

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

dougvk
источник

Ответы:

404

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

Однако некоторые вещи нужно знать:

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

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

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

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

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

янтарный
источник
Я использую git rebase --onto для смены родителей, но мне кажется, что я получаю только один коммит. Я поставил вопрос об этом: stackoverflow.com/questions/19181665/…
Эдуардо Мелло
7
Ага, так это то, что --ontoиспользуется для! Документация всегда была мне совершенно неясна, даже после прочтения этого ответа!
Даниэль С. Собрал
6
Эта строка: « git rebase --onto <new-parent> <old-parent>Скажи мне все о том, как использовать rebase --ontoвсе остальные вопросы и документы, которые я до сих пор читал».
jpmc26
3
Для меня эта команда должна читать:, rebase this commit on that oneили git rebase --on <new-parent> <commit>. Использование старого родителя здесь не имеет смысла для меня.
Самир Агиар
1
@kopranb Старый родитель важен, когда вы хотите отбросить часть пути от вашей головы к удаленной голове. Рассматриваемое вами дело рассматривается git rebase <new-parent>.
глухой
35

Если окажется, что вам нужно избегать перебазирования последующих коммитов (например, потому что переписывание истории было бы несостоятельным), тогда вы можете использовать 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 push ничего не толкает после этого
Baptiste Wicht
2
@BaptisteWicht: замены хранятся в отдельном наборе ссылок. Вам нужно будет организовать выдвижение (и выборку) refs/replace/иерархии ссылок. Если вам просто нужно сделать это один раз, то вы можете сделать это git push yourremote 'refs/replace/*'в исходном хранилище и git fetch yourremote 'refs/replace/*:refs/replace/*'в целевом хранилище. Если вам нужно сделать это несколько раз, вы можете вместо этого добавить эти refspecs в remote.yourremote.pushпеременную config (в исходном хранилище) и remote.yourremote.fetchпеременную config (в целевых хранилищах).
Крис Йонсен
3
Более простой способ сделать это - использовать git replace --grafts. Не уверен, когда это было добавлено, но его цель - создать замену, аналогичную коммиту B, но с указанными родителями.
Пол Уогланд
32

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

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

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

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

Якуб Наребски
источник
3
Из того, что я понимаю (из git.wiki.kernel.org/index.php/GraftPoint ), git replaceпроизошла замена git-трансплантатов (при условии, что у вас есть git 1.6.5 или более поздняя версия).
Александр Берд
4
@ Thr4wn: По большому счету это правда, но если вы хотите переписать историю, то способ сделать это - grafts + git-filter-branch (или интерактивная перебазировка, или fast-export + например, reposurgeon). Если вы хотите / должны сохранить историю , то git-replace намного превосходит прививки.
Якуб Наребски
@ JakubNarębski, не могли бы вы объяснить, что вы подразумеваете под переписать против сохранения истории? Почему я не могу использовать git-replace+ git-filter-branch? В моем тесте, filter-branchказалось, уважал замену, перебирая все дерево.
cdunn2001
1
@ cdunn2001: git filter-branchпереписывает историю, уважая трансплантаты и замены (но в переписанной истории замены будут спорными); толчок приведет к изменению не вперед-назад. git replaceсоздает на месте переносимые замены; push будет ускоренной перемоткой вперед, но вам нужно будет нажать, refs/replaceчтобы перенести замены, чтобы исправить историю. HTH
Jakub Narębski
23

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

Это зависит от того, хотите ли вы «перебазировать» или «переучить». Перебазироваться , как это было предложено Амбер , движется вокруг посмотреть различия . 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не будет в истории).

Марк Лодато
источник
1
+1 Я экспериментировал со reparentсценарием; это прекрасно работает; настоятельно рекомендуется . Я также рекомендовал бы создать новую branchметку и для checkoutэтой ветви перед вызовом (как метка ветви будет перемещаться).
Rhubbarb
Также стоит отметить, что: (1) исходный узел остается неизменным, и (2) новый узел не наследует дочерние узлы исходного узла.
Rhubbarb
0

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

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

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

Теодоро
источник