Git Fusionando sucursales seleccionadas en una nueva sucursal, conservando el acceso a las sucursales

¿De qué exactamente escribo para ir de: (También tengo la sensación de que mis dibujos sugieren que no entiendo muy bien a Git, tengan paciencia conmigo).

-<>-<>-<>-<>- (B) / -----master- \ --<>-<>- (A) where '<>' is a commit. 

a esto:

  (merge A and B into C) --------------o-> (C, new 'clean' branch off master) / / /-<>-<>-<>-<>-/ (B) // / -----master-- / \ / --<>-<>-/ (A) where 'o' is a merge of A and B into C. 

¿Y aún así podré git check-out las twigs (A) y (B) ?

Y / o podría hacer esto:

  --------------o-<>-(C) / / /-<>-<>-<>-<>-/-<>-<>-(B) // / -----master-- / \ / --<>-<>-/-<>-<>-<>-(A) 

Si puedes, incluso en algunas vueltas, ¿podrías explicarlo? Gracias.

Hagamos una copy de security aquí y comencemos con lo simple, los commits comunes funcionan en Git. Primero, definamos qué es una confirmación. Realmente son bastante simples. Intenta, como experimento, ejecutar:

 $ git cat-file -p HEAD 

Esto imprimirá, en su terminal, su confirmación actual, que se parecerá mucho a esto, pero con diferentes grandes y desagradables ID hash (y, por supuesto, nombres de curso):

 tree 142feb985388972de41ba56af8bc066f1e22ccf9 parent 62ebe03b9e8d5a6a37ea2b726d64b109aec0508c author AU Thor <thor@example.com> 1501864272 -0700 committer AU Thor <thor@example.com> 1501864272 -0700 this is some commit It has a commit message. 

Eso es todo, ¡eso es todo lo que necesitas para comprometerte! Sin embargo, hay mucho escondiéndose a la vista de todos. En particular, están las líneas parent y parent , que tienen estas grandes identificaciones hash feas. De hecho, el nombre HEAD actúa como sustituto de otro:

 $ git rev-parse HEAD 4384e3cde2ce8ecd194202e171ae16333d241326 

(De nuevo, tu número será diferente).

Estas ID de hash son los "nombres verdaderos" de cada confirmación (o, como para el tree algún otro object de Git). Estas ID de hash son en realidad sums de verificación criptográficas de los contenidos de la confirmación (u otro tipo de object como tree ). Si conoce los contenidos (la secuencia de bytes que compone el object) y su tipo y tamaño, puede calcular esta ID de hash usted mismo, aunque no hay motivos reales para molestarse.

Qué hay en un commit

Como puede ver en lo anterior, una confirmación almacena una cantidad relativamente pequeña de información. El object real, esta pequeña cadena de líneas de text, entra en la database de Git y obtiene una ID de hash única. Esa ID de hash es su "verdadero nombre": cuando Git quiere ver lo que hay en la confirmación, le das a Git algo que produce la ID, y Git recupera el object de la database de Git. Dentro de un object de commit, tenemos:

  • Un tree Esto contiene el tree fuente que guardó (mediante la git add y, finalmente, la git commit de git commit paso de git commit final de git commit escribe primero el tree, luego el compromiso).
  • Un padre Esta es la ID hash de alguna otra confirmación. Volveremos sobre esto en un momento.
  • Un autor y un committer: tienen el nombre de la persona que escribió el código (es decir, el autor) y realizaron la confirmación. Se separan en caso de que alguien le envíe un parche de correo electrónico: luego, la otra persona es el autor, pero usted es el autor. (Git nació en los días previos a los sitios de queueboración como GitHub, por lo que los parches de correo electrónico eran bastante comunes.) Estos almacenan una dirección de correo electrónico y una timestamp también, con la timestamp en esa extraña forma de par numérico.
  • Un post de logging . Esto es solo text de forma libre, lo que sea que quieras proporcionar. Lo único que Git interpreta aquí es la línea en blanco que separa el asunto del post de logging del rest del post de logging (y aun así, solo para formatear: git log --oneline vs git log , por ejemplo).

Hacer commits, comenzando con un repository completamente vacío

Supongamos que tenemos un repository completamente vacío, sin compromisos. Si tuviéramos que ir a dibujar las confirmaciones, terminaríamos con un dibujo en blanco o una pizarra en blanco. Así que hagamos el primer commit, git add algunos files, como un README , y ejecutando git commit .

Este primer compromiso tiene una gran identificación de hash fea, pero vamos a llamarlo "commit A", y dibujarlo en:

 A 

Ese es el único compromiso. Entonces … ¿cuál es su padre?

La respuesta es que no tiene ningún padre. Es el primer compromiso, por lo que no puede. Entonces no tiene una línea parent después de todo. Esto lo convierte en un commit raíz .

Hagamos un segundo commit, haciendo un file útil, no solo un README . Luego vamos a git add ese file y git commit . El nuevo commit recibe otra gran identificación hash fea, pero la llamaremos B Vamos a dibujarlo:

 A <-B 

Si miramos B con git cat-file -p <hash for B> , veremos que esta vez tenemos una línea parent , y muestra el hash para A Decimos que B "apunta a" A ; A es el padre de B

Si hacemos un tercer commit C , y lo miramos, veremos que el padre de B es el hash de B :

 A <-B <-C 

Así que ahora C apunta a B , B apunta a A , y A es una confirmación raíz y no apunta a ninguna parte. Así es como funciona el compromiso de Git: cada uno apunta hacia atrás , hacia su padre. La cadena de pointers hacia atrás termina cuando alcanzamos el compromiso raíz.

Ahora, todos estos pointers internos son fijos, al igual que todo lo demás sobre una confirmación. No puedes cambiar nada en ningún commit, nunca, porque su gran ID de hash feo es una sum de comprobación criptográfica de los contenidos de ese commit. Si de alguna manera lograste cambiar algo, la sum de comprobación criptográfica también cambiaría. Tendrás una nueva confirmación diferente.

Como todos los pointers internos son fijos (y siempre apuntan hacia atrás), no tenemos que molestarnos en dibujarlos:

 A--B--C 

basta. Pero, aquí es donde entran los nombres de las sucursales y el nombre HEAD , necesitamos saber por dónde empezar . Las ID de hash parecen bastante aleatorias, a diferencia de nuestro sencillo y simple ABC donde sabemos el order de las letras. Si tiene dos identificaciones, por ejemplo:

 62ebe03b9e8d5a6a37ea2b726d64b109aec0508c 3e05c534314fd5933ff483e73f54567a20c94a69 

no se sabe en qué order entran, al less no desde los ID. Entonces, necesitamos saber cuál es la última confirmación, es decir, la sugerencia de confirmación de alguna twig como master . Luego, podemos comenzar con la última confirmación y trabajar hacia atrás, siguiendo estos enlaces principales de a uno por vez. Si podemos encontrar la confirmación C , C nos permitirá encontrar B , y B nos permitirá encontrar A

Nombres de sucursales almacenar ID de hash

Lo que hace Git es almacenar la identificación hash de la confirmación de sugerencia de una twig, en una (otra) database. En lugar de usar identificadores hash como las keys, las keys aquí son los nombres de las twigs, y sus valores no son los objects reales, sino solo los identificadores hash de las sugerencias de punta.

(Esta "database" es, al less en la actualidad, principalmente un set de files: .git/refs/heads/master es un file que contiene la ID de hash para el master . Por lo tanto, "actualizar la database" significa "escribir una nueva ID de hash" en el file ". Pero este método no funciona muy bien en Windows, ya que esto significa que master y MASTER , que se supone que son dos twigs diferentes, usan el mismo file, lo que causa todo tipo de problemas. Por ahora, nunca use dos nombres de twigs que difieren solo en el caso).

Así que ahora veamos agregar un nuevo compromiso D a nuestra serie de tres compromisos. Primero, dibujemos el nombre master :

 A--B--C <-- master 

El master nombre tiene la ID hash de C en este momento, lo que nos permite (o Git) encontrar C , hacer lo que queramos con ella, y usar C para encontrar B Luego usamos B para encontrar A , y luego como A es una confirmación raíz, hemos terminado. Decimos que el master apunta a C

Ahora agregamos o cambiamos algunos files y git commit . Git escribe un nuevo tree como siempre, y luego escribe un nuevo compromiso D El padre de D será C :

 A--B--C <-- master \ D 

y finalmente Git simplemente agrega el hash de D , sea lo que fuere, al master :

 A--B--C \ D <-- master 

Ahora el master señala a D , por lo que la próxima vez que trabajemos con el master comenzaremos con el compromiso D , luego seguiremos la flecha principal de D hasta C , y así sucesivamente. Al señalar a D , el master nombre de twig ahora tiene D como confirmación de sugerencia. (Y, por supuesto, ya no hay una razón para dibujar el gráfico con un doblez así).

Mantenemos las flechas con los nombres de las twigs, porque a diferencia de los commits, los nombres de las twigs se mueven . Los commits en sí nunca se pueden cambiar, pero los nombres de las twigs registran cualquier commit que queramos llamar "el último".

Varias twigs

Ahora veamos cómo crear más de una twig y por qué necesitamos HEAD .

Continuaremos con nuestros cuatro compromisos hasta ahora:

 A--B--C--D <-- master 

Ahora hagamos una nueva twig, develop , usando git branch develop o git checkout -b develop . Dado que los nombres de las sucursales son solo files (o inputs de la database) que contienen identificadores hash, haremos que el nuevo nombre también se develop para apuntar D :

 A--B--C--D <-- master, develop 

Pero ahora que tenemos dos o más nombres de twigs, necesitamos saber: ¿en qué twig estamos? Aquí es donde entra HEAD .

El HEAD en Git es en realidad otro file, .git/HEAD , que normalmente contiene la cadena ref: seguido del nombre completo de la twig. Si estamos en master , .git/HEAD tiene ref: refs/heads/master en él. Si estamos en develop , .git/HEAD tiene ref: refs/heads/develop en él. Estos refs/heads/ things son los nombres de los files que contienen los hashes de confirmación de punta, por lo que Git puede leer READ , get el nombre de la twig, luego leer el file de twig y get la ID de hash correcta.

Vamos a dibujar esto también, antes de cambiar a la twig de develop :

 A--B--C--D <-- master (HEAD), develop 

y luego después de cambiar para develop :

 A--B--C--D <-- master, develop (HEAD) 

¡Eso es todo lo que sucede aquí! Hay más cosas que sucede en otros lugares cuando se cambian las sucursales, pero para lidiar con el gráfico , todo lo que hace el git checkout es cambiar el nombre al que se adjunta HEAD .

Ahora hagamos un nuevo compromiso E El nuevo compromiso entra como de costumbre, y su nuevo padre es lo que HEAD dice, que es D , entonces:

 A--B--C--D <-- master, develop (HEAD) \ E 

Ahora tenemos que actualizar alguna twig. La twig actual está develop , así que esa es la que actualizamos. Escribimos el ID hash de E , y ahora tenemos:

 A--B--C--D <-- master \ E <-- develop (HEAD) 

Esto es todo, ¡esto es todo lo que hay que hacer para que las sucursales crezcan en Git! Simplemente agregamos un nuevo compromiso a donde HEAD esté ahora, haciendo que el padre del nuevo commit sea el antiguo HEAD commit. Luego movemos cualquier twig para señalar el nuevo compromiso que acabamos de hacer.

Fusionar y fusionar confirma

Ahora que tenemos múltiples twigs, hagamos algunas confirmaciones más en cada una. Tendremos que git checkout cada twig y hacer algunos compromisos para llegar aquí, pero supongamos que terminamos con este gráfico:

 A--B--C--D--G <-- master (HEAD) \ E--F <-- develop 

Ahora tenemos un compromiso extra en master (que es la twig en la que estamos), y dos en develop , más los cuatro compromisos de ABCD originales que se encuentran en ambas twigs.

(Esto, por cierto, es una característica peculiar de Git, que no se encuentra en muchos otros sistemas de control de versiones. En la mayoría de los VCS, la bifurcación de un compromiso está "activada" cuando se establece el compromiso, al igual que los commits 'se establecen los padres en piedra en ese momento. Pero en Git, los nombres de las twigs son cosas muy ligeras y esponjosas que solo apuntan a una única confirmación: la punta de la twig. Por lo tanto, el set de twigs que algunos cometen está "encendido" y se determina al encontrar todas las twigs nombres, y luego seguir todas las flechas que apuntan hacia atrás para ver qué compromisos son alcanzables comenzando en qué branch-tips. Este concepto de alcanzable importa mucho, pronto, aunque no llegaremos a este anuncio. Consulte también http: //think-like-a-git.net/ por ejemplo.)

Ahora git merge develop para fusionar los commit de develop en master . Restring, actualmente estamos en el master mira HEAD en el dibujo. Entonces Git usará el nombre develop para encontrar su confirmación de punta , que es F , y el nombre HEAD para encontrar nuestra comisión de punta, que es G

Entonces, Git usará este gráfico que hemos estado dibujando para encontrar el compromiso común de la combinación de fusión . Aquí, eso es cometer D Commit D es donde estas dos twigs se unen nuevamente.

El process de fusión subyacente de Git es algo complicado y desorderado, pero si todo va bien, y generalmente lo hace, no tenemos que profundizar en él. Solo podemos saber que Git compara el compromiso D para comprometer G para ver lo que hicimos en el master , y compara el compromiso D para cometer F para ver lo que hicieron en el develop . Git luego combina ambos sets de cambios, asegurándose de que todo lo hecho en ambas twigs se hace exactamente una vez.

Este process de calcular y combinar los sets de cambios es el process de fusión . Más específicamente, es una combinación de tres vías (probablemente llamada así porque hay tres inputs: la base de fusión y las dos sugerencias de twig). Esto es lo que me gusta llamar la "parte del verbo" de la fusión: fusionar , hacer el trabajo de una fusión tripartita.

El resultado de este process de fusión, este merge-as-a-verb, es un tree fuente, y usted sabe lo que hacemos con un tree, ¿verdad? ¡Hacemos un compromiso! Entonces eso es lo que hace Git a continuación: hace un nuevo compromiso. El nuevo commit funciona mucho como cualquier commit ordinario. Tiene un tree, que es el que acaba de hacer Git. Tiene un autor, un committer y un post de confirmación. Y tiene un padre, que es nuestro compromiso actual o HEAD … y otro , segundo padre, ¡que es el compromiso que fusionamos!

Vamos a dibujar en nuestra fusión cometer H , con sus dos flechas principales hacia atrás apuntando:

 A--B--C--D--G---H <-- master (HEAD) \ / E--F <-- develop 

(No lo hicimos -porque es demasiado difícil- dibujar en el hecho de que el primer padre es G y el segundo es F , pero esa es una propiedad útil más adelante).

Al igual que con cada confirmación, la nueva confirmación entra en la twig actual y hace avanzar el nombre de la twig. Así que el master ahora apunta a la nueva combinación H commit. Es H que señala tanto a G como a F

Este tipo de commit, este commit de fusión , también usa la palabra "merge". En este caso, "fusión" es un adjetivo, pero nosotros (y Git) a menudo simplemente llamamos a esto "una fusión", usando la palabra "fusionar" como sustantivo. Entonces, una fusión , el sustantivo, se refiere a un compromiso de fusión , con fusión como adjetivo. Un compromiso de fusión es simplemente cualquier compromiso con al less dos padres .

Hacemos un commit de fusión ejecutando git merge . Sin embargo, hay un pequeño inconveniente: la git merge no siempre hace una fusión. Puede hacer el tipo de verbo fusionar sin hacer el tipo de adjetivo, y de hecho, ni siquiera siempre hace el tipo de verbo tampoco. Podemos forzar a Git a hacer un commit de fusión usando git merge --no-ff , incluso en el caso de que se saltee todo el trabajo.

Por el momento, solo usaremos --no-ff , forzando a Git a hacer una fusión real. Pero veremos primero por qué necesitaremos --no-ff , y luego, ¡por qué no nos deberíamos haber molestado!

Volver a tu problema de tu pregunta

Vamos a volver a dibujar tus charts a mi manera, porque mi path es mejor. 🙂 Tienes esto para empezar:

  B--C--D--E <-- branch-B / --o--o--A <-- master \ F--G <-- branch-A 

(Aquí no hay nada labeldo como HEAD porque no sabemos ni nos importa en este momento cuál es HEAD , si es que es alguno de estos).

Ahora desea crear una nueva twig, la branch-C , apuntando a confirmar A , y convertirla en la twig actual . La forma más rápida de hacerlo, asumiendo que todo está limpio, es usar:

 $ git checkout -b branch-C master 

que se mueve hacia (verifica en el índice y en el tree de trabajo) la confirmación identificada por el master (confirmación A ), luego crea una nueva branch-C apunta a esa confirmación, y luego hace que HEAD nombre twig branch-C .

  B--C--D--E <-- branch-B / --o--o--A <-- master, branch-C (HEAD) \ F--G <-- branch-A 

Ahora ejecutaremos la primera git merge para recoger la branch-A :

 $ git merge --no-ff branch-A 

Esto comparará el compromiso actual A con el compromiso de fusión-base, que es A nuevamente. (Esta es la razón por la que necesitamos --no-ff : ¡la base de fusión es la confirmación actual!) Luego, comparará el compromiso actual para comprometer G Git combinará los cambios, lo que significa "tomar G ", y realizar un nuevo compromiso de fusión en nuestra twig actual. El nombre master continuará apuntando a A , pero ahora voy a dejar de dibujarlo por completo debido a las limitaciones del arte ASCII:

  B--C--D--E <-- branch-B / --o--o--A------H <-- branch-C (HEAD) \ / F--G <-- branch-A 

A continuación, fusionaremos la branch-B :

 $ git merge branch-B 

Esto comparará la combinación base de fusión commit A para confirmar H , y también comparará A con E (Esta vez, la base de fusión no es la confirmación actual, así que no necesitamos --no-ff .) Git intentará, como de costumbre, combinar los cambios, fusionar como un verbo, y si tiene éxito, Git hará otro merge commit (fusionar como sustantivo o adjetivo), que podemos dibujar así:

  B--C--D--E <-- branch-B / \ --o--o--A------H-----I <-- branch-C (HEAD) \ / F--G <-- branch-A 

Tenga en count que ninguno de los otros nombres se movió en absoluto. Las twigs branch-A y branch-B aún apuntan a sus commits originales. El master sucursal aún apunta a A (y si fuera una pizarra o papel o algo así, podríamos mantenerlo dibujado). El nombre branch-C ahora apunta al segundo de los dos commits de fusión que usamos, ya que cada una de nuestras fusiones solo puede apuntar a dos commits, no a tres a la vez.

Git tiene un tipo de fusión tres a la vez

Si, por alguna razón, no te gusta tener dos fusiones, Git ofrece algo llamado fusión de pulpo , que puede fusionar más de dos twigs a la vez. Pero nunca hay ningún requisito para hacer una fusión de pulpo, así que solo lo menciono aquí para completarlo.

Lo que realmente deberíamos observar es que una de estas dos fusiones era innecesaria .

No necesitamos una de las fusiones

Comenzamos con git merge --no-ff branch-A , y tuvimos que usar --no-ff para evitar que Git hiciera lo que Git llama una fusión de avance rápido . También notamos por qué : es porque la base de fusión, cometer A en nuestro dibujo, era el mismo compromiso al que apuntó la branch-C en ese momento.

La forma en que hacemos que Git combine los "cambios" que van del compromiso A al compromiso A (todo el cero de estos "cambios") con los cambios que encontró pasando del compromiso A a cometer G fue a usar --no-ff : OK, Git , Sé que puedes hacer esto como un avance rápido sin fusión, pero al final quiero una fusión real, así que imagina que trabajaste duro y cometiste una fusión. Si dejamos fuera esta opción, Git simplemente "deslizaría la label de la twig hacia adelante", yendo en contra de la dirección de las flechas de compromiso interno. Comenzaríamos con:

  B--C--D--E <-- branch-B / --o--o--A <-- master, branch-C (HEAD) \ F--G <-- branch-A 

y luego Git haría esto:

  B--C--D--E <-- branch-B / --o--o--A <-- master \ F--G <-- branch-A, branch-C (HEAD) 

Luego, cuando hicimos la segunda fusión, para la cual no necesitábamos y aún no necesitamos, no --no-ff -Git encontraría la base de combinación A , compararía A contra G , compararía A contra E , combinaría los cambios para hacer una nueva tree object, y hacer un nuevo compromiso H fuera del resultado:

  B--C--D-----E <-- branch-B / \ --o--o--A <-- master H <-- branch-C (HEAD) \ / F-----------G <-- branch-A 

Al igual que antes, ninguna de las otras tags se mueve en absoluto (y esta vez podemos dibujar el nombre master extendiendo un poco el gráfico). Solo obtenemos la combinación de fusión H , en lugar de dos fusiones H--I

Por qué es posible que desee --no-ff todos modos

Si hacemos dos fusiones, usando git merge --no-ff , el tree de fonts que obtenemos, cuando Git combine todos nuestros cambios, será el mismo que el tree de fonts que obtenemos si permitimos la fusión de un avance rápido. Pero el gráfico final es diferente.

El gráfico de compromiso , en Git, es la historia. Si quiere saber lo que sucedió en el pasado, lo que tiene, lo que puede ver, es el gráfico de confirmación . El gráfico está compuesto por todos los commits, y los commits almacenan los nombres y dates de autor y committer y los posts de logging. Se vinculan a los treees fuente guardados y proporcionan los enlaces principales que componen el gráfico.

Esto significa que en el futuro, si desea saber que realizó dos fusiones, debe realizar dos fusiones ahora. Pero si en el futuro, no te importa cuántos commands de git merge , puedes permitir que cualquier número de esos pasos de git merge sean operaciones de avance rápido (sin fusión). No dejan ningún rastro en el gráfico de compromiso; simplemente mueven una label de nombre de sucursal de un compromiso a otro, por lo que en el futuro no se puede saber realmente si esto sucedió alguna vez. El gráfico no almacena movimiento de nombre; solo tiene los commits.