example - git rebase tutorial
Diferencia entre ''master de rebase'' y ''rebase--onto master'' de una rama derivada de una rama de maestro (5)
Antes de cualquiera de las operaciones dadas, su repositorio se ve así.
o---o---o---o---o master
/
x---x---x---x---x A
/
o---o---o B
Después de una rebase estándar (sin --onto master
) la estructura será:
o---o---o---o---o master
| /
| x''--x''--x''--x''--x''--o''--o''--o'' B
/
x---x---x---x---x A
... donde los x''
se comprometen desde la rama A
(Note cómo ahora están duplicados en la base de la rama B
).
En cambio, una rebase con --onto master
creará la siguiente estructura más limpia y simple:
o---o---o---o---o master
| /
| o''--o''--o'' B
/
x---x---x---x---x A
Dada la siguiente estructura de rama:
*------*---*
Master /
*---*--*------*
A /
*-----*-----*
B (HEAD)
Si quiero fusionar mis cambios B (y solo mi B cambia, no A cambia) en Master, ¿cuál es la diferencia entre estos dos conjuntos de comandos?
>(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
Estoy principalmente interesado en aprender si el código de la Rama A podría convertirlo en maestro si lo uso de la primera manera.
Las diferencias:
Primer set
(B)
git rebase master
*---*---* [master] / *---*---*---* [A] / *---*---* [B](HEAD)
No pasó nada. No hay nuevas confirmaciones en la rama master
desde la creación de la rama B
(B)
git checkout master
*---*---* [master](HEAD) / *---*---*---* [A] / *---*---* [B]
(maestro)
git merge B
*---*---*-----------------------* [Master](HEAD) / / *---*---*---* [A] / / / *---*---* [B]
Segundo set
(B)
git rebase --onto master AB
*---*---*-- [master] |/ | *---*---*---* [A] | *---*---* [B](HEAD)
(B)
git checkout master
*---*---*-- [master](HEAD) |/ | *---*---*---* [A] | *---*---* [B]
(maestro)
git merge B
*---*---*----------------------* [master](HEAD) |/ / | *---*---*---* [A] / | / *---*--------------* [B]
Quiero fusionar mis cambios B (y solo mis cambios B, no cambios A) en master
Ten cuidado con lo que entiendes porque "solo mi B cambia".
En el primer conjunto, la rama B
es (antes de la fusión final):
*---*---*
/
*---*---*
/
*---*---* [B]
Y en el segundo set tu rama B es:
*---*---*
|
|
|
*---*---* [B]
Si comprendo correctamente, lo que desea es solo las confirmaciones B que no están en la rama A. Por lo tanto, el segundo conjunto es la opción correcta para usted antes de la fusión.
Puedes probarlo tú mismo y ver. Puedes crear un repositorio de git local para jugar 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)
Tenga paciencia conmigo por un tiempo antes de responder a la pregunta como se le preguntó. Una de las respuestas anteriores es correcta, pero hay problemas de etiquetado y otros relativamente menores (pero potencialmente confusos), por lo que quiero comenzar con los dibujos y las etiquetas de las sucursales. Además, las personas que vienen de otros sistemas, o tal vez incluso nuevas en el control de revisión y git, a menudo piensan en las ramas como "líneas de desarrollo" en lugar de "rastros de la historia" (git las implementa como las últimas, en lugar de las primeras, por lo que un compromiso no está necesariamente en ninguna "línea de desarrollo" específica.
Primero, hay un problema menor con la forma en que dibujaste tu gráfica:
*------*---*
Master /
*---*--*------*
A /
*-----*-----*
B (HEAD)
Aquí está exactamente el mismo gráfico, pero con las etiquetas dibujadas de manera diferente y algunas más flechas agregadas (y he numerado los nodos de confirmación para su uso a continuación):
0 <- 1 <- 2 <-------------------- master
/
3 <- 4 <- 5 <- 6 <------ A
/
7 <- 8 <- 9 <-- HEAD=B
El motivo por el que esto es importante es que git es bastante vago acerca de lo que significa que un compromiso esté "en" alguna rama, o quizás una mejor frase sea decir que algún compromiso está "contenido en" algún conjunto de sucursales. Los compromisos no se pueden mover ni cambiar, pero las etiquetas de las ramas pueden moverse y lo hacen.
Más específicamente, un nombre de rama como master
, A
o B
apunta a un compromiso específico . En este caso, los puntos master
para cometer 2, los puntos A
para cometer 6 y B
puntos B
para cometer 9. Las primeras comillas 0 a 2 están contenidas dentro de las tres ramas; los comités 3, 4 y 5 están contenidos dentro de A
y B
; commit 6 está contenido solo dentro de A
; y los cometidos 7 a 9 están contenidos solo en B
(Por cierto, varios nombres pueden apuntar a la misma confirmación, y eso es normal cuando se crea una nueva rama).
Antes de continuar, permítanme volver a dibujar la gráfica de una manera más:
0
/
1
/
2 <-- master
/
3 - 4 - 5
|/
| 6 <-- A
/
7
/
8
/
9 <-- HEAD=B
Esto solo enfatiza que no es una línea horizontal de compromisos lo que importa, sino más bien las relaciones padre / hijo. La etiqueta de la rama apunta a un compromiso inicial, y luego (al menos la forma en que se dibujan estos gráficos) nos movemos hacia la izquierda, tal vez también subiendo o bajando según sea necesario, para encontrar los compromisos principales.
Cuando se vuelven a realizar los cambios, se están copiando esos compromisos.
Git nunca puede cambiar ningún compromiso.
Hay un "nombre verdadero" para cualquier confirmación (o incluso cualquier objeto en un repositorio de git), que es su SHA-1: esa cadena de 40 dígitos hexadecimales como 9f317ce...
que se ve en el git log
por ejemplo. El SHA-1 es una suma de control criptográfica 1 del contenido del objeto. Los contenidos son el autor y el remitente (nombre y correo electrónico), las marcas de tiempo, un árbol de origen y la lista de confirmaciones principales. El padre de commit # 7 siempre es commit # 5. Si realiza una copia casi exacta de la confirmación n. ° 7, pero configura su principal como la n. ° 2 en lugar de la n. ° 5, obtendrá una confirmación diferente con un ID diferente. (Me he quedado sin dígitos individuales en este punto; normalmente uso letras mayúsculas simples para representar las ID de confirmación, pero con las ramas llamadas A
y B
pensé que sería confuso. Así que llamaré una copia de # 7, # 7a, abajo.)
¿Qué hace git rebase
Cuando le pide a git que vuelva a unir una cadena de confirmaciones, como las confirmaciones # 7-8-9 anteriores, tiene que copiarlas , al menos si se van a mover a cualquier lugar (si no se mueven, puede dejar el originales en su lugar). Por defecto, copiar las confirmaciones de la rama que se está git rebase
actualmente, por lo que git rebase
solo necesita dos datos adicionales:
- ¿Qué compromisos debe copiar?
- ¿Dónde deben aterrizar las copias? Es decir, ¿cuál es el ID de padre objetivo para la confirmación de la primera copia? (Las confirmaciones adicionales simplemente apuntan de nuevo a la primera copiada, la segunda copiada, etc.)
Cuando ejecuta git rebase <upstream>
, le permite a git descubrir ambas partes a partir de una sola pieza de información. Cuando usas --onto
, puedes decirle a git por separado acerca de ambas partes: aún suministras un --onto
upstream
pero no calcula el objetivo desde <upstream>
, solo calcula las confirmaciones para copiar desde <upstream>
. (Por cierto, creo que <upstream>
no es un buen nombre, pero es lo que usa rebase y no tengo nada mejor, así que sigamos aquí. Rebase llama a target <newbase>
, pero creo que target es mucho mejor nombre)
Echemos un vistazo a estas dos opciones primero. Ambos suponen que estás en la rama B
en primer lugar:
-
git rebase master
-
git rebase --onto master A
Con el primer comando, el argumento <upstream>
para rebase
es master
. Con el segundo, es A
Así es como git calcula qué se compromete a copiar: entrega la rama actual a git rev-list
, y también entrega <upstream>
a git rev-list
, pero utilizando --not
o más precisamente, con el equivalente de los dos - punto exclude..include
notación. Esto significa que necesitamos saber cómo funciona git rev-list
.
Mientras que git rev-list
es extremadamente complicado, la mayoría de los comandos de git terminan usándolo; es el motor para git log
, git bisect
rebase
, rebase
, filter-branch
, etc., este caso en particular no es demasiado difícil: con la notación de dos puntos, rev-list
enumera todos los compromisos disponibles desde el lado derecho (incluido que se compromete a sí mismo), excluyendo todos los compromisos accesibles desde el lado izquierdo.
En este caso, git rev-list HEAD
encuentra todas las confirmaciones accesibles desde HEAD
, es decir, casi todas las confirmaciones: confirmaciones 0-5 y 7-9, y git rev-list master
encuentra todas las confirmaciones accesibles desde la master
, que son los #s de confirmación 0, 1 y 2. Restar de 0 a 2 de 0-5,7-9 hojas 3-5,7-9. Estos son los candidatos comprometidos a copiar, según lo listado por git rev-list master..HEAD
.
Para nuestro segundo comando, tenemos A..HEAD
lugar de master..HEAD
, por lo que las confirmaciones para restar son 0-6. Commit # 6 no aparece en el conjunto HEAD
, pero está bien: restar algo que no está allí, no lo deja ahí. Por lo tanto, los candidatos a copia resultantes son 7-9.
Eso todavía nos deja con averiguar el objetivo de la rebase, es decir, ¿dónde debería copiarse el terreno comprometido? Con el segundo comando, la respuesta es "la confirmación identificada por el argumento --onto
". Como dijimos --onto master
, eso significa que el objetivo es cometer # 2.
rebase # 1
git rebase master
Sin embargo, con el primer comando, no especificamos un objetivo directamente, por lo que git usa la confirmación identificada por <upstream>
. El <upstream>
que dimos fue master
, que apunta a cometer # 2, así que el objetivo es commit # 2.
Por lo tanto, el primer comando comenzará a copiar el commit # 3 con los cambios mínimos necesarios para que su principal sea el commit # 2. Su padre ya está comprometido # 2. Nada tiene que cambiar, así que nada cambia, y la rebase simplemente reutiliza el compromiso # 3 existente. Luego debe copiar # 4 para que su padre sea # 3, pero el padre ya es # 3, así que solo reutiliza # 4. Igualmente, el # 5 ya es bueno. Ignora completamente el # 6 (que no está en el conjunto de confirmaciones para copiar); comprueba #s 7-9 pero también están todos bien, por lo que toda la rebase termina simplemente reutilizando todas las confirmaciones originales. Puede forzar copias de todas formas con -f
, pero no lo hizo, por lo que toda esta rebase termina sin hacer nada.
rebase # 2
git rebase --onto master A
El segundo comando de rebase usó --onto
para seleccionar # 2 como su objetivo, pero le dijo a git que copie solo los 7-9. El padre de Commit # 7 es commit # 5, por lo que esta copia realmente tiene que hacer algo. 2 Entonces, git hace un nuevo compromiso, llamémoslo # 7a, que tiene el # 2 como su principal. La rebase avanza para cometer # 8: la copia ahora necesita # 7a como su padre. Finalmente, la rebase pasa a cometer # 9, que necesita # 8a como su padre. Con todas las confirmaciones copiadas, lo último que hace rebase es mover la etiqueta (recuerde, ¡las etiquetas se mueven y cambian!). Esto da una gráfica como esta:
7a - 8a - 9a <-- HEAD=B
/
0 - 1 - 2 <-- master
/
3 - 4 - 5 - 6 <-- A
/
7 - 8 - 9 [abandoned]
Está bien, pero ¿qué pasa con git rebase --onto master AB
?
Esto es casi lo mismo que git rebase --onto master A
La diferencia es que extra B
al final. Afortunadamente, esta diferencia es muy simple: si le da a git rebase
ese argumento adicional, primero ejecuta git checkout
en ese argumento. 3
Tus comandos originales
En su primer conjunto de comandos, ejecutó git rebase master
mientras estaba en la rama B
Como se señaló anteriormente, esto es un gran no-op: ya que nada necesita moverse, git no copia nada en absoluto (a menos que use -f
/ --force
, lo que no hizo). Luego verificó el master
y usó git merge B
, que, si se le dice a 4, crea una nueva confirmación con la combinación. Por lo tanto, la respuesta de Dherik , desde el momento en que lo vi al menos, es correcta aquí: la confirmación de fusión tiene dos padres, uno de los cuales es la punta de la rama B
, y esa rama se remonta a través de tres confirmaciones que están en la rama A
y, por lo tanto, algo de lo que hay en A
termina siendo fusionado en master
.
Con la segunda secuencia de comandos, primero verificaste B
(ya estabas en B
así que esto era redundante, pero era parte de la git rebase
). Luego tuvo que volver a hacer una copia de tres confirmaciones, produciendo el gráfico final anterior, con confirmaciones 7a, 8a y 9a. Luego verificó el master
e hizo un commit de combinación con B
(vea nuevamente la nota al pie de página 4). Nuevamente, la respuesta de Dherik es correcta: lo único que falta es que las confirmaciones originales y abandonadas no se dibujen y no es tan obvio que las nuevas confirmaciones combinadas son copias.
1 Esto solo importa porque es extraordinariamente difícil apuntar a una suma de control particular. Es decir, si alguien en quien confía le dice "Confío en el compromiso con ID 1234567 ...", es casi imposible para alguien más, alguien en quien no confíe tanto, que cree un compromiso que tenga la misma ID, pero Tiene diferentes contenidos. Las posibilidades de que ocurra por accidente son de 1 en 2 160 , lo cual es mucho menos probable que sufra un ataque cardíaco mientras se ve afectado por un rayo mientras se ahoga en un tsunami mientras es secuestrado por extraterrestres. :-)
2 La copia real se realiza utilizando el equivalente de git cherry-pick
: git compara el árbol de confirmación con el de su padre para obtener un diff, luego aplica la diferencia al árbol del nuevo padre.
3 Esto es en realidad, literalmente cierto en este momento: git rebase
es un script de shell que analiza sus opciones, luego decide qué tipo de rebase interno se ejecutará: el git-rebase--am
no interactivo git-rebase--am
o el git-rebase--interactive
interactivo git-rebase--interactive
Después de que haya resuelto todos los argumentos, si hay un argumento de nombre de rama sobrante, la secuencia de comandos realiza git checkout <branch-name>
branquista git checkout <branch-name>
antes de iniciar la rebase interna.
4 Dado que los puntos master
para cometer 2 y cometer 2 es un antecesor del cometer 9, esto no haría un comite de fusión después de todo, sino que haría lo que Git llama una operación de avance rápido . Puede indicar a Git que no haga estos avances rápidos usando git merge --no-ff
. Algunas interfaces, como la interfaz web de GitHub y quizás algunas GUI, pueden separar los diferentes tipos de operaciones, por lo que su "fusión" obliga a una verdadera fusión como esta.
Con una combinación de avance rápido, el gráfico final para el primer caso es:
0 <- 1 <- 2 [master used to be here]
/
3 <- 4 <- 5 <- 6 <------ A
/
7 <- 8 <- 9 <-- master, HEAD=B
En cualquier caso, las confirmaciones 1 a 9 ahora están en ambas ramas, master
y B
La diferencia, en comparación con la combinación verdadera, es que, desde el gráfico, puede ver el historial que incluye la combinación.
En otras palabras, la ventaja de una combinación de avance rápido es que no deja rastro de lo que de otra manera es una operación trivial. La desventaja de una combinación de avance rápido es, bueno, que no deja rastro. Entonces, la cuestión de si se debe permitir el avance rápido es realmente una cuestión de si desea dejar una fusión explícita en la historia formada por las confirmaciones.
git log --graph --decorate --oneline AB master
(o una herramienta GUI equivalente) se puede usar después de cada comando git para visualizar los cambios.
Este es el estado inicial del repositorio, con B
como la rama actual.
(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
Aquí hay un script para crear un repositorio en este estado.
#!/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
El primer comando de rebase no hace nada.
(B) git rebase master
Current branch B is up to date.
La comprobación del master
y la fusión de B
simplemente apunta al master
al mismo compromiso que B
, (es decir, 9a90b7c
). No se crean nuevos compromisos.
(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
El segundo comando de rebase copia las confirmaciones en el rango A..B
y las apunta al master
. Los tres compromisos en este rango son 9a90b7c C9, 2968483 C8, and 187c9c8 C7
. Las copias son nuevas confirmaciones con sus propias identificaciones de confirmación; 7c0e241
, 40b105d
y 5b0bda1
. Las ramas master
y A
han cambiado.
(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
Como antes, retirar el master
y fusionar B
simplemente apunta al master
a la misma confirmación que B
, (es decir, 7c0e241
). No se crean nuevos compromisos.
La cadena original de confirmaciones que B
estaba señalando todavía existe.
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