git tutorial Differenza tra "master rebase" e "rebase--onto master" da un ramo derivato da un ramo del master




github rebase (4)

Data la seguente struttura di ramo:

  *------*---*
Master        \
               *---*--*------*
               A       \
                        *-----*-----*
                        B         (HEAD)

Se voglio unire le mie modifiche B (e solo le mie B cambiano, nessuna modifica A) in master, qual è la differenza tra questi due set di comandi?

>(B)      git rebase master
>(B)      git checkout master
>(master) git merge B
>(B)      git rebase --onto master A B
>(B)      git checkout master
>(master) git merge B

Sono principalmente interessato all'apprendimento se il codice del ramo A potrebbe trasformarlo in master se utilizzo la prima.


Puoi provarlo tu stesso e vedere. Puoi creare un repository git locale per giocare con:

#! /bin/bash
set -e
mkdir repo
cd repo

git init
touch file
git add file
git commit -m 'init'

echo a > file0
git add file0
git commit -m 'added a to file'

git checkout -b A
echo b >> fileA
git add fileA
git commit -m 'b added to file'
echo c >> fileA
git add fileA
git commit -m 'c added to file'

git checkout -b B
echo x >> fileB
git add fileB
git commit -m 'x added to file'
echo y >> fileB
git add fileB
git commit -m 'y added to file'
cd ..

git clone repo rebase
cd rebase
git checkout master
git checkout A
git checkout B
git rebase master
cd ..

git clone repo onto
cd onto
git checkout master
git checkout A
git checkout B
git rebase --onto master A B
cd ..

diff <(cd rebase; git log --graph --all) <(cd onto; git log --graph --all)

Resta con me per un po 'prima di rispondere alla domanda come chiesto. Una delle risposte precedenti è giusta, ma esistono etichette e altri problemi relativamente minori (ma potenzialmente confusi), quindi voglio iniziare con i disegni delle filiali e le etichette delle filiali. Inoltre, le persone che provengono da altri sistemi, o forse anche solo nuove dal controllo delle revisioni e dal git, spesso pensano che i rami siano "linee di sviluppo" piuttosto che "tracce di storia" (Git li implementa come questi ultimi, piuttosto che i primi, quindi un commit non è necessariamente su una specifica "linea di sviluppo").

Innanzitutto, c'è un piccolo problema nel modo in cui hai disegnato il tuo grafico:

  *------*---*
Master        \
               *---*--*------*
               A       \
                        *-----*-----*
                        B         (HEAD)

Ecco lo stesso grafico, ma con le etichette disegnate in modo diverso e altre teste di freccia aggiunte (e ho numerato i nodi di commit per l'uso di seguito):

0 <- 1 <- 2         <-------------------- master
           \
            3 <- 4 <- 5 <- 6      <------ A
                       \
                        7 <- 8 <- 9   <-- HEAD=B

Perché questo è importante perché il git è piuttosto sciolto riguardo a ciò che significa per un commit essere "su" qualche ramo - o forse una frase migliore è dire che alcuni commit sono "contenuti in" qualche insieme di rami. I commit non possono essere spostati o modificati, ma le etichette delle filiali possono muoversi e spostarsi.

Più specificamente, un nome di ramo come master , A o B punta a uno specifico commit . In questo caso, il master punta a commit 2, A punti a commit 6 e B punti a commit 9. I primi commit da 0 a 2 sono contenuti in tutti e tre i rami; commit 3, 4 e 5 sono contenuti all'interno di entrambi A e B ; commit 6 è contenuto solo all'interno di A ; e commette da 7 a 9 sono contenuti solo in B (Per inciso, più nomi possono puntare allo stesso commit, ed è normale quando si crea un nuovo ramo.)

Prima di procedere, permettimi di ridisegnare il grafico in un altro modo:

0
 \
  1
   \
    2     <-- master
     \
      3 - 4 - 5
              |\
              | 6   <-- A
               \
                7
                 \
                  8
                   \
                    9   <-- HEAD=B       

Ciò sottolinea semplicemente che non è una linea orizzontale di commit che contano, ma piuttosto le relazioni genitore / figlio. L' etichetta del ramo punta a un commit iniziale e quindi (almeno il modo in cui questi grafici sono disegnati) ci spostiamo a sinistra, forse anche salendo o scendendo secondo necessità, per trovare il commit del genitore.

Quando rebase commit, in realtà stai copiando quei commit.

Git non può mai cambiare alcun commit

C'è un "vero nome" per ogni commit (o addirittura qualsiasi oggetto in un repository git), che è il suo SHA-1: quella stringa di 40 cifre esadecimali come 9f317ce... che vedi nel git log per esempio. SHA-1 è un checksum 1 crittografico del contenuto dell'oggetto. I contenuti sono l'autore e il committente (nome e indirizzo email), i timestamp, un albero dei sorgenti e l'elenco dei commit principali. Il genitore di commit # 7 è sempre commit # 5. Se crei una copia per lo più esatta di commit # 7, ma imposta il suo genitore su commit # 2 invece di commit # 5, ottieni un commit diverso con un ID diverso. (A questo punto ho esaurito le singole cifre, normalmente uso lettere maiuscole per rappresentare gli ID commit, ma con i rami A e B pensavo che sarebbe stato un po 'confuso, quindi chiamerò una copia di # 7, # 7a, sotto.)

Cosa fa git rebase

Quando chiedi a git di ribilanciare una catena di commit, come ad esempio il commit 7-8-9 sopra, deve copiarli , almeno se si spostano ovunque (se non si muovono, può semplicemente lasciare il originali in atto). Per impostazione predefinita, copia di commit dal ramo attualmente estratto, quindi git rebase richiede solo due ulteriori informazioni:

  • Quale commit dovrebbe copiare?
  • Dove dovrebbero arrivare le copie? Cioè, qual è l'ID genitore di destinazione per il commit prima copiato? (I commit addizionali rimandano semplicemente al primo copiato, al secondo copiato e così via.)

Quando esegui git rebase <upstream> , fai uscire a git entrambe le parti da una singola informazione. Quando usi --onto , devi dire a git separatamente riguardo a entrambe le parti: fornisci ancora un upstream ma non calcola il target da <upstream> , calcola solo i commit da copiare da <upstream> . (Per inciso, penso che <upstream> non sia un buon nome, ma è quello che usa rebase e non ho niente di meglio, quindi rimettiamolo qui. Rebase chiama target <newbase> , ma penso che il target sia molto nome migliore.)

Diamo prima un'occhiata a queste due opzioni. Entrambi suppongono di essere sul ramo B in primo luogo:

  1. git rebase master
  2. git rebase --onto master A

Con il primo comando, l'argomento <upstream> per rebase è master . Con il secondo, è A

Ecco come calcola git che si impegna a copiare: passa il ramo corrente a git rev-list , e anche mani <upstream> a git rev-list , ma usando --not -o più precisamente, con l'equivalente del due- punto exclude..include notazione. Ciò significa che dobbiamo sapere come funziona git rev-list .

Mentre git rev-list è estremamente complicato, la maggior parte dei comandi git finisce per usarlo; è il motore per git log , git bisect , rebase , filter-branch e così via - questo caso particolare non è troppo difficile: con la notazione a due punti, la rev-list elenca tutte le commit raggiungibili dal lato destro (incluso che si impegna da sé), escludendo ogni commit raggiungibile dal lato sinistro.

In questo caso, git rev-list HEAD trova tutti i commit raggiungibili da HEAD - cioè, quasi tutti i commit: commit 0-5 e 7-9 - e git rev-list master trova tutti i commit raggiungibili dal master , che è commit #s 0, 1 e 2. Sottraendo 0-through-2 da 0-5,7-9 lascia 3-5,7-9. Questi sono i candidati che si impegna a copiare, come elencato da git rev-list master..HEAD .

Per il nostro secondo comando, abbiamo A..HEAD invece di master..HEAD , quindi i commit da sottrarre sono 0-6. Commit # 6 non appare nel set HEAD , ma va bene: sottraendo qualcosa che non c'è, non lo lascia lì. I candidati alla copia risultanti sono quindi 7-9.

Questo ci lascia ancora a capire l' obiettivo del rebase, cioè dove dovrebbe essere copiato il commit della terra? Con il secondo comando, la risposta è "il commit identificato dall'argomento --onto ". Dal momento che abbiamo detto --onto master , significa che il target è commit # 2.

rebase # 1

git rebase master

Con il primo comando, però, non abbiamo specificato direttamente un target, quindi git usa il commit identificato da <upstream> . Il <upstream> abbiamo dato era master , che punta a commit # 2, quindi il target è commit # 2.

Il primo comando inizierà quindi copiando commit # 3 con qualunque modifica minima sia necessaria affinché il suo genitore sia commit # 2. Il genitore è già impegnato # 2. Nulla deve cambiare, quindi non cambia nulla e rebase ri-usa solo il commit n. 3 esistente. Deve quindi copiare n. 4 in modo che il suo genitore sia il n. 3, ma il genitore è già n. 3, quindi usa solo il n. 4. Allo stesso modo, # 5 è già buono. Ignora completamente il # 6 (che non è nel set di commit da copiare); controlla #s 7-9 ma sono anche tutti buoni, quindi l'intero rebase finisce per riutilizzare tutti i commit originali. Puoi forzare le copie comunque con -f , ma non l'hai fatto, quindi questo intero rebase finisce per non fare nulla.

rebase # 2

git rebase --onto master A

Il secondo comando rebase usato --onto per selezionare # 2 come target, ma ha detto git to copy just commit 7-9. Il genitore di Commit # 7 è commit # 5, quindi questa copia deve davvero fare qualcosa. 2 Quindi git fa un nuovo commit-chiamiamo questo # 7a-che ha commesso # 2 come suo genitore. Il rebase passa a commit # 8: la copia ora ha bisogno di # 7a come genitore. Infine, il rebase passa al commit # 9, che ha bisogno di # 8a come suo genitore. Con tutti i commit copiati, l'ultima cosa che fa rebase è spostare l'etichetta (ricorda, le etichette si muovono e cambiano!). Questo dà un grafico come questo:

          7a - 8a - 9a       <-- HEAD=B
         /
0 - 1 - 2                    <-- master
         \
          3 - 4 - 5 - 6      <-- A
                    \
                     7 - 8 - 9   [abandoned]

OK, ma per quanto riguarda git rebase --onto master AB ?

Questo è quasi lo stesso di git rebase --onto master A La differenza è che B extra alla fine. Fortunatamente, questa differenza è molto semplice: se date a git rebase un argomento in più, esegue git checkout il git checkout su quell'argomento. 3

I tuoi comandi originali

Nel tuo primo set di comandi, hai eseguito git rebase master mentre si trovava sul ramo B Come notato sopra, questo è un grande no-op: poiché non è necessario spostare nulla, git copia nulla (a meno che non si usi -f / --force , cosa che non si è fatto). Quindi hai estratto il master e utilizzato git merge B , che, se viene detto a 4, crea un nuovo commit con l'unione. Quindi la risposta di Dherik , al momento in cui l'ho visto almeno, è corretta qui: Il commit di unione ha due genitori, uno dei quali è la punta del ramo B , e quel ramo torna indietro attraverso tre commit che si trovano sul ramo A e quindi parte di ciò che accade in A finisce per essere fuso in un master .

Con la tua seconda sequenza di comandi, hai prima controllato B (eri già su B quindi questo era ridondante, ma faceva parte del git rebase ). Hai quindi rebase copia tre commit, producendo il grafico finale sopra, con commit 7a, 8a e 9a. Quindi hai estratto il master e fatto un commit di unione con B (vedi di nuovo la nota 4). Anche in questo caso la risposta di Dherik è corretta: l'unica cosa che manca è che i commit originali e abbandonati non siano disegnati e non è così ovvio che i nuovi commit uniti sono copie.

1 Questo importa solo in quanto è straordinariamente difficile indirizzare un particolare checksum. Cioè, se qualcuno di cui ti fidi ti dice "Mi fido del commit con ID 1234567 ...", è quasi impossibile per qualcun altro - qualcuno di cui non ti fidi tanto - per inventare un commit che ha lo stesso ID, ma ha diversi contenuti. Le probabilità che accada accidentalmente sono 1 su 2 160 , che è molto meno probabile di un attacco di cuore quando vengono colpiti da un fulmine mentre annegate in uno tsunami mentre vengono rapiti dagli alieni spaziali. :-)

2 La copia effettiva viene creata usando l'equivalente di git cherry-pick : git confronta l'albero del commit con l'albero del genitore per ottenere un diff, quindi applica il diff sull'albero del nuovo genitore.

3 Questo è in realtà, letteralmente vero in questo momento: git rebase è uno script di shell che analizza le opzioni, quindi decide quale tipo di rebase interno eseguire: il git-rebase--am non interattivo o il git-rebase--am git-rebase--interactive interattivo-- git-rebase--interactive . Dopo aver individuato tutti gli argomenti, se c'è l'argomento del nome del ramo rimanente, lo script esegue git checkout <branch-name> prima di iniziare il rebase interno.

4 Poiché i punti master per commit 2 e commit 2 è un antenato di commit 9, questo normalmente non esegue un commit merge dopo tutto, ma invece esegue ciò che Git chiama un'operazione di avanzamento rapido . Puoi istruire Git a non eseguire questi rapidi avanzamenti utilizzando git merge --no-ff . Alcune interfacce, come l'interfaccia web di GitHub e forse alcune GUI, possono separare i diversi tipi di operazioni, in modo che la loro "unione" costringa una vera unione come questa.

Con una fusione veloce avanti, il grafico finale per il primo caso è:

0 <- 1 <- 2         [master used to be here]
           \
            3 <- 4 <- 5 <- 6      <------ A
                       \
                        7 <- 8 <- 9   <-- master, HEAD=B

In entrambi i casi, i commit da 1 a 9 sono ora su entrambi i rami, master e B La differenza, rispetto alla vera unione, è che, dal grafico, puoi vedere la cronologia che include l'unione.

In altre parole, il vantaggio di una fusione veloce è che non lascia traccia di ciò che altrimenti è un'operazione banale. Lo svantaggio di una fusione veloce è, beh, che non lascia traccia. Quindi la domanda se consentire il fast forward è in realtà una questione se si vuole lasciare un'unione esplicita nella storia formata dai commit.


Prima di una qualsiasi delle operazioni indicate il tuo repository appare così

           o---o---o---o---o  master
                \
                 x---x---x---x---x  A
                                  \
                                   o---o---o  B

Dopo un rebase standard (senza --onto master ) la struttura sarà:

           o---o---o---o---o  master
               |            \
               |             x'--x'--x'--x'--x'--o'--o'--o'  B
                \
                 x---x---x---x---x  A

... dove la x' viene commessa dal ramo A (Nota come sono ora duplicati alla base del ramo B )

Invece, un rebase con --onto master creerà la seguente struttura più pulita e semplice:

           o---o---o---o---o  master
               |            \
               |             o'--o'--o'  B
                \
                 x---x---x---x---x  A

git log --graph --decorate --oneline AB master (o uno strumento GUI equivalente) può essere usato dopo ogni comando git per visualizzare le modifiche.

Questo è lo stato iniziale del repository, con B come ramo corrente.

(B) git log --graph --oneline --decorate A B master
* 5a84c72 (A) C6
| * 9a90b7c (HEAD -> B) C9
| * 2968483 C8
| * 187c9c8 C7
|/  
* 769014a C5
* 6b8147c C4
* 9166c60 C3
* 0aaf90b (master) C2
* 8c46dcd C1
* 4d74b57 C0

Ecco uno script per creare un repository in questo stato.

#!/bin/bash

commit () {
    for i in $(seq $1 $2); do
        echo article $i > $i
        git add $i
        git commit -m C$i
    done
}

git init
commit 0 2

git checkout -b A
commit 3 6

git checkout -b B HEAD~
commit 7 9

Il primo comando di rebase non fa nulla.

(B) git rebase master
Current branch B is up to date.

La verifica del master e l'unione di B semplicemente indicano il master allo stesso commit di B , (cioè 9a90b7c ). Non vengono creati nuovi commit.

(B) git checkout master
Switched to branch 'master'

(master) git merge B
Updating 0aaf90b..9a90b7c
Fast-forward
<... snipped diffstat ...>

(master) git log --graph --oneline --decorate A B master
* 5a84c72 (A) C6
| * 9a90b7c (HEAD -> master, B) C9
| * 2968483 C8
| * 187c9c8 C7
|/  
* 769014a C5
* 6b8147c C4
* 9166c60 C3
* 0aaf90b C2
* 8c46dcd C1
* 4d74b57 C0

Il secondo comando rebase copia i commit nell'intervallo A..B e li punta al master . I tre commit in questo intervallo sono 9a90b7c C9, 2968483 C8, and 187c9c8 C7 . Le copie sono nuove commit con i propri ID di commit; 7c0e241 , 40b105d e 5b0bda1 . I rami master e A sono invariati.

(B) git rebase --onto master A B
First, rewinding head to replay your work on top of it...
Applying: C7
Applying: C8
Applying: C9

(B) log --graph --oneline --decorate A B master
* 7c0e241 (HEAD -> B) C9
* 40b105d C8
* 5b0bda1 C7
| * 5a84c72 (A) C6
| * 769014a C5
| * 6b8147c C4
| * 9166c60 C3
|/  
* 0aaf90b (master) C2
* 8c46dcd C1
* 4d74b57 C0

Come in precedenza, il check-out del master e l'unione di B semplicemente punti master allo stesso commit di B , (cioè 7c0e241 ). Non vengono creati nuovi commit.

La catena originale di commit che B stava indicando esiste ancora.

git log --graph --oneline --decorate A B master 9a90b7c
* 7c0e241 (HEAD -> master, B) C9
* 40b105d C8
* 5b0bda1 C7
| * 5a84c72 (A) C6
| | * 9a90b7c C9    <- NOTE: This is what B used to be
| | * 2968483 C8
| | * 187c9c8 C7
| |/  
| * 769014a C5
| * 6b8147c C4
| * 9166c60 C3
|/  
* 0aaf90b C2
* 8c46dcd C1
* 4d74b57 C0






git-rebase