Impostazione del puntatore genitore git su un genitore diverso


220

Se in passato ho un commit che punta a un genitore, ma voglio cambiare il genitore a cui punta, come potrei farlo?

Risposte:


404

Usando git rebase. È il generico comando "take commit (s) e plop / loro su un altro genitore (base)" in Git.

Alcune cose da sapere, tuttavia:

  1. Poiché gli SHA di commit coinvolgono i loro genitori, quando cambi il genitore di un determinato commit, il suo SHA cambierà, così come gli SHA di tutti i commit che lo seguono (più recenti di quelli) nella linea di sviluppo.

  2. Se lavori con altre persone e hai già spinto il commit in questione in pubblico nel punto in cui l'hanno portato, modificare il commit è probabilmente una Bad Idea ™. Ciò è dovuto al n. 1, e quindi alla conseguente confusione che incontreranno i repository degli altri utenti quando provano a capire cosa è successo a causa dei tuoi SHA che non corrispondono più ai loro per gli "stessi" commit. (Vedere la sezione "RECUPERO DALLA REVISIONE DI UPSTREAM" della pagina man collegata per i dettagli.)

Detto questo, se sei attualmente in una filiale con alcuni commit che vuoi spostare in un nuovo genitore, sarebbe simile a questo:

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

Ciò sposterà tutto ciò che segue <old-parent> nel ramo corrente per sedersi in cima <new-parent>invece.


Sto usando git rebase --onto per cambiare i genitori, ma sembra che finisca con un solo commit. Ho posto una domanda al riguardo: stackoverflow.com/questions/19181665/…
Eduardo Mello,

7
Ah, quindi è quello che --ontoserve! La documentazione è sempre stata completamente oscura per me, anche dopo aver letto questa risposta!
Daniel C. Sobral,

6
Questa linea: git rebase --onto <new-parent> <old-parent>mi ha detto tutto su come usare rebase --ontoogni altra domanda e documento che ho letto finora non è riuscito.
jpmc26,

3
Per me questo comando dovrebbe leggere:, rebase this commit on that oneo git rebase --on <new-parent> <commit>. Usare il vecchio genitore qui non ha senso per me.
Samir Aguiar,

1
@kopranb Il vecchio genitore è importante quando vuoi scartare parte del percorso dalla tua testa alla testa remota. Il caso che stai descrivendo è gestito da git rebase <new-parent>.
clacke

35

Se si scopre che è necessario evitare di riassegnare i commit successivi (ad es. Perché una riscrittura della cronologia sarebbe insostenibile), è possibile utilizzare il comando di sostituzione git (disponibile in Git 1.6.5 e versioni successive).

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

Con la sostituzione sopra stabilita, qualsiasi richiesta per l'oggetto B restituirà effettivamente l'oggetto C. I contenuti di C sono esattamente gli stessi del contenuto di B ad eccezione del primo genitore (stessi genitori (tranne il primo), stesso albero, stesso messaggio di commit).

I sostituti sono attivi per impostazione predefinita, ma possono essere disattivati ​​usando l' --no-replace-objectsopzione per git (prima del nome del comando) o impostando la GIT_NO_REPLACE_OBJECTSvariabile di ambiente. I ricambi possono essere condivisi premendo refs/replace/*(oltre al normalerefs/heads/* ).

Se non ti piace il commit-munging (fatto con sed sopra), puoi creare il tuo commit sostitutivo usando i comandi di livello superiore:

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

La grande differenza è che questa sequenza non propaga i genitori aggiuntivi se B è un commit di unione.


E poi, come spingeresti le modifiche in un repository esistente? Sembra funzionare localmente, ma git push non spinge nulla dopo
Baptiste Wicht,

2
@BaptisteWicht: i ricambi sono memorizzati in un set separato di riferimenti. Dovrai organizzare per spingere (e recuperare) la refs/replace/gerarchia dei riferimenti. Se hai solo bisogno di farlo una volta, puoi farlo git push yourremote 'refs/replace/*'nel repository di origine e git fetch yourremote 'refs/replace/*:refs/replace/*'nei repository di destinazione. Se è necessario farlo più volte, è possibile invece aggiungere tali refspec a una remote.yourremote.pushvariabile di configurazione (nel repository di origine) e una remote.yourremote.fetchvariabile di configurazione (nei repository di destinazione).
Chris Johnsen,

3
Un modo più semplice per farlo è usare git replace --grafts. Non sono sicuro di quando è stato aggiunto, ma il suo scopo è quello di creare un sostituto uguale a commit B ma con i genitori specificati.
Paul Wagland,

32

Nota che cambiare un commit in Git richiede che anche tutti i commit che lo seguono debbano essere modificati. Questo è scoraggiato se hai pubblicato questa parte della storia e qualcuno potrebbe aver costruito il proprio lavoro sulla storia che era prima del cambiamento.

La soluzione alternativa git rebasemenzionata nella risposta di Amber consiste nell'utilizzare il meccanismo degli innesti (vedere la definizione degli innesti Git nel Glossario Git e la documentazione del .git/info/graftsfile nella documentazione relativa al layout del repository Git ) per modificare il genitore di un commit, verificare che sia stato corretto con un visualizzatore di cronologia ( gitk, git log --graph, ecc.) e quindi utilizzare git filter-branch(come descritto nella sezione "Esempi" della sua pagina man) per renderlo permanente (e quindi rimuovere l'innesto e, facoltativamente, rimuovere i riferimenti originali di cui è stato eseguito il backup dagit filter-branch o riposizionare il repository):

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

NOTA !!! Questa soluzione è diversa dalla soluzione di rebase in quanto git rebaserifarebbe / trapianta i cambiamenti , mentre la soluzione basata su innesti semplicemente riassegnerebbe gli commit così come sono , senza tener conto delle differenze tra vecchio genitore e nuovo genitore!


3
Da quello che ho capito (da git.wiki.kernel.org/index.php/GraftPoint ), git replaceha sostituito gli innesti git (supponendo che tu abbia git 1.6.5 o successivo).
Alexander Bird,

4
@ Thr4wn: in gran parte vero, ma se vuoi riscrivere la storia allora innesti + git-filter-branch (o rebase interattivo, o esportazione veloce + ad esempio reposurgeon) è il modo per farlo. Se vuoi / hai bisogno di preservare la storia , allora git-replace è di gran lunga superiore agli innesti.
Jakub Narębski,

@ JakubNarębski, potresti spiegare cosa intendi per riscrivere vs preservare la storia? Perché non posso usare git-replace+ git-filter-branch? Nel mio test, filter-branchsembrava rispettare la sostituzione, riequilibrando l'intero albero.
cdunn2001,

1
@ cdunn2001: git filter-branchriscrive la storia, rispettando gli innesti e le sostituzioni (ma nella cronologia riscritta le discussioni saranno discutibili); la pressione comporterà un cambio non in avanti. git replacecrea sostituzioni trasferibili sul posto; push sarà un avanzamento rapido, ma è necessario premere refs/replaceper trasferire le sostituzioni per avere una cronologia corretta. HTH
Jakub Narębski,

23

Per chiarire le risposte sopra e collegare spudoratamente il mio script:

Dipende se si desidera "rebase" o "reparent". Un rebase , come suggerito da Amber , si sposta tra diff . Un parente , come suggerito da Jakub e Chris , si muove intorno a istantanee dell'intero albero. Se vuoi ripartire, ti suggerisco di usaregit reparent invece di fare il lavoro manualmente.

Confronto

Supponiamo che tu abbia l'immagine a sinistra e desideri che assomigli all'immagine a destra:

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

Sia il rebasing che il reparenting produrranno la stessa immagine, ma la definizione di C'differisce. Con git rebase --onto A B, C'non conterrà alcuna modifica introdotta da B. Con git reparent -p A, C'sarà identico a C(tranne che Bnon sarà nella storia).


1
+1 Ho sperimentato la reparentsceneggiatura; funziona magnificamente; altamente raccomandato . Consiglierei anche di creare una nuova branchetichetta e a checkoutquel ramo prima di chiamare (poiché l'etichetta del ramo si sposterà).
Rabarbaro,

Vale anche la pena notare che: (1) il nodo originale rimane intatto e (2) il nuovo nodo non eredita i figli del nodo originale.
Rabarbaro,

0

Sicuramente la risposta di Jakub mi ha aiutato alcune ore fa quando stavo provando esattamente la stessa cosa dell'OP.
Tuttavia, git replace --graftora è la soluzione più semplice per quanto riguarda gli innesti. Inoltre, un grosso problema con quella soluzione era che il ramo del filtro mi faceva perdere tutti i rami che non venivano uniti nel ramo della HEAD. Quindi, ha git filter-repofatto il lavoro perfettamente e in modo impeccabile.

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

Per maggiori informazioni: consulta la sezione "Reinnesto della cronologia" nei documenti

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.