Définition du pointeur git parent sur un autre parent


220

Si j'ai un commit dans le passé qui pointe vers un parent, mais que je veux changer le parent vers lequel il pointe, comment dois-je procéder?

Réponses:


404

Utilisation git rebase. C'est la commande générique "take commit (s) and plop it / them on a different parent (base)" command in Git.

Quelques choses à savoir cependant:

  1. Étant donné que les SHA de validation impliquent leurs parents, lorsque vous changez le parent d'une validation donnée, sa SHA change - tout comme les SHA de toutes les validations qui la suivent (plus récente qu'elle) dans la ligne de développement.

  2. Si vous travaillez avec d'autres personnes et que vous avez déjà poussé le commit en question public vers l'endroit où il l'a tiré, la modification du commit est probablement une mauvaise idée ™. Cela est dû au numéro 1, et donc à la confusion qui en résulte que les référentiels des autres utilisateurs rencontreront lorsqu'ils essaieront de comprendre ce qui s'est passé parce que vos SHA ne correspondent plus aux leurs pour les «mêmes» commits. (Voir la section "RÉCUPÉRATION À PARTIR D'UNE REBASE EN AMONT" de la page de manuel liée pour plus de détails.)

Cela dit, si vous êtes actuellement sur une branche avec des validations que vous souhaitez déplacer vers un nouveau parent, cela ressemblerait à ceci:

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

Cela déplacera tout ce qui suit <old-parent> dans la branche actuelle pour s'asseoir au-dessus <new-parent>.


J'utilise git rebase --onto pour changer de parent mais il semble que je ne me retrouve qu'avec un seul commit. J'ai posé une question dessus: stackoverflow.com/questions/19181665/…
Eduardo Mello

7
Aha, alors c'est à ça qu'on --ontosert! La documentation a toujours été complètement obscure pour moi, même après avoir lu cette réponse!
Daniel C.Sobral

6
Cette ligne: git rebase --onto <new-parent> <old-parent>m'a tout expliqué sur l'utilisation de rebase --ontotoutes les autres questions et documents que j'ai lus jusqu'à présent.
jpmc26

3
Pour moi, cette commande doit se lire:, rebase this commit on that oneou git rebase --on <new-parent> <commit>. Utiliser le vieux parent ici n'a pas de sens pour moi.
Samir Aguiar

1
@kopranb L'ancien parent est important lorsque vous souhaitez supprimer une partie du chemin de votre tête à la tête distante. Le cas que vous décrivez est géré par git rebase <new-parent>.
clacke

35

S'il s'avère que vous devez éviter de rebaser les validations suivantes (par exemple parce qu'une réécriture de l'historique serait intenable), alors vous pouvez utiliser le remplacement de git (disponible dans Git 1.6.5 et versions ultérieures).

# …---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.

Avec le remplacement ci-dessus établi, toute demande pour l'objet B retournera réellement l'objet C. Le contenu de C est exactement le même que le contenu de B sauf pour le premier parent (mêmes parents (sauf pour le premier), même arbre, même message de validation).

Les remplacements sont actifs par défaut, mais peuvent être désactivés en utilisant l' --no-replace-objectsoption git (avant le nom de la commande) ou en définissant la GIT_NO_REPLACE_OBJECTSvariable d'environnement. Les remplacements peuvent être partagés en poussant refs/replace/*(en plus de la normale refs/heads/*).

Si vous n'aimez pas le commit-munging (fait avec sed ci-dessus), vous pouvez créer votre commit de remplacement en utilisant des commandes de niveau supérieur:

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

La grande différence est que cette séquence ne propage pas les parents supplémentaires si B est un commit de fusion.


Et puis, comment pousseriez-vous les modifications vers un référentiel existant? Cela semble fonctionner localement, mais git push ne pousse rien après cela
Baptiste Wicht

2
@BaptisteWicht: Les remplacements sont stockés dans un ensemble séparé de références. Vous devrez vous arranger pour pousser (et récupérer) la refs/replace/hiérarchie des références. Si vous n'avez besoin de le faire qu'une seule fois, vous pouvez le faire git push yourremote 'refs/replace/*'dans le référentiel source et git fetch yourremote 'refs/replace/*:refs/replace/*'dans les référentiels de destination. Si vous devez le faire plusieurs fois, vous pouvez ajouter ces paramètres à une remote.yourremote.pushvariable de configuration (dans le référentiel source) et une remote.yourremote.fetchvariable de configuration (dans les référentiels de destination).
Chris Johnsen

3
Une façon plus simple de le faire est d'utiliser git replace --grafts. Je ne sais pas quand cela a été ajouté, mais son objectif est de créer un remplacement identique à la validation B mais avec les parents spécifiés.
Paul Wagland

32

Notez que la modification d'un commit dans Git nécessite que tous les commits qui le suivent également soient modifiés. Ceci est découragé si vous avez publié cette partie de l'histoire et que quelqu'un a pu construire son travail sur l'histoire avant le changement.

La solution alternative à git rebasementionnée dans la réponse d'Amber consiste à utiliser le mécanisme de greffes (voir la définition des greffes Git dans le glossaire Git et la documentation du .git/info/graftsfichier dans la documentation de mise en page du référentiel Git ) pour changer le parent d'un commit, vérifiez qu'il a fait la chose correcte avec un visualiseur d'historique ( gitk, git log --graph, etc.) puis utilisez git filter-branch(comme décrit dans la section "Exemples" de sa page de manuel) pour le rendre permanent (puis supprimez le greffon, et supprimez éventuellement les références d'origine sauvegardées par git filter-branch, ou reclonez le référentiel):

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

REMARQUE !!! Cette solution est différente de la solution de rebasage dans ce git rebaseserait rebasage / transplantation changements , tandis que la solution basée sur les greffes serait simplement Reparent commits comme il est , ne prend pas en compte les différences entre l' ancien parent et nouveau parent!


3
D'après ce que je comprends (de git.wiki.kernel.org/index.php/GraftPoint ), git replacea remplacé les greffes git (en supposant que vous ayez git 1.6.5 ou version ultérieure).
Alexander Bird

4
@ Thr4wn: Largement vrai, mais si vous voulez réécrire l'historique, alors les greffes + git-filter-branch (ou rebase interactif, ou exportation rapide + par exemple repurgeon) est le moyen de le faire. Si vous voulez / devez conserver l'histoire , alors git-replace est bien supérieur aux greffes.
Jakub Narębski

@ JakubNarębski, pourriez-vous expliquer ce que vous entendez par réécrire vs préserver l' histoire? Pourquoi ne puis-je pas utiliser git-replace+ git-filter-branch? Dans mon test, filter-branchsemblait respecter le remplacement, rebasant l'arbre entier.
cdunn2001

1
@ cdunn2001: git filter-branchréécrit l'histoire, en respectant les greffes et les remplacements (mais dans l'histoire réécrite, les remplacements seront sans objet); la poussée entraînera un changement non fasf-forward. git replacecrée des remplacements transférables sur place; push sera en avance rapide, mais vous devrez pousser refs/replacepour transférer les remplacements pour avoir l'historique corrigé. HTH
Jakub Narębski

23

Pour clarifier les réponses ci-dessus et brancher sans vergogne mon propre script:

Cela dépend si vous voulez "rebaser" ou "réparer". Un rebase , comme suggéré par Amber , se déplace autour des diffs . Un réparateur , comme suggéré par Jakub et Chris , se déplace autour d' instantanés de l'arbre entier. Si vous voulez réparer, je suggère d'utiliser git reparentplutôt que de faire le travail manuellement.

Comparaison

Supposons que vous ayez l'image à gauche et que vous souhaitiez qu'elle ressemble à l'image de droite:

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

Le rebasage et le reparentage produiront la même image, mais la définition de C'diffère. Avec git rebase --onto A B, C'ne contiendra aucune modification introduite par B. Avec git reparent -p A, C'sera identique à C(sauf que Bce ne sera pas dans l'histoire).


1
+1 J'ai expérimenté le reparentscript; cela fonctionne à merveille; fortement recommandé . Je recommanderais également de créer une nouvelle branchétiquette, et à checkoutcette branche avant d'appeler (car l'étiquette de branche se déplacera).
Rhubbarb

Il convient également de noter que: (1) le nœud d'origine reste intact et (2) le nouveau nœud n'hérite pas des enfants du nœud d'origine.
Rhubbarb

0

Certainement, la réponse de @ Jakub m'a aidé il y a quelques heures lorsque j'essayais exactement la même chose que l'OP.
Cependant, git replace --graftc'est maintenant la solution la plus facile concernant les greffes. De plus, un problème majeur avec cette solution était que la branche de filtre m'a fait perdre toutes les branches qui n'étaient pas fusionnées dans la branche de la tête. Ensuite, a git filter-repofait le travail parfaitement et parfaitement.

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

Pour plus d'informations: consultez la section "Historique de re-greffage" dans la documentation

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.