Skip to content
This page has been auto-translated and may contain errors.View in English

Fusionar ramas y resolver conflictos ​

docs.scrimba.com

Tu rama de funcionalidad está terminada. El interruptor funciona, ya confirmaste los cambios y ahora quieres que ese trabajo esté en main, donde vive el código del resto del equipo. La mayoría de las veces traerlo de vuelta es rápido y silencioso: Git compara las dos ramas, no encuentra nada que se superponga y combina el trabajo sin pasos adicionales. Pero a veces tú y un compañero de equipo cambiaron exactamente las mismas líneas en ramas distintas, y Git no puede adivinar cuál versión quieres conservar. Esa situación aparece en casi todo proyecto con más de una persona haciendo commits. Es una parte normal y cotidiana de trabajar con ramas, y significa que Git encontró dos ideas distintas sobre las mismas líneas y necesita que tú decidas cuál prevalece.

Traer una rama de vuelta con git merge ​

Supongamos que construiste el interruptor de Fahrenheit en su propia rama, add-fahrenheit-toggle, y main no se ha movido desde que te ramificaste de ella. Cambia a main y ejecuta git merge con el nombre de la rama que quieres traer:

bash
$ git switch main
$ git merge add-fahrenheit-toggle
Updating 4f2a891..8c3d1a0
Fast-forward
 src/index.js | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

git merge <rama> siempre fusiona la rama indicada dentro de la rama en la que te encuentres parado, por lo que cambiar primero a main es importante. La rama en la que estés parado es la que recibe la fusión. Después de ejecutar esto, main tiene todos los commits de add-fahrenheit-toggle. La rama de funcionalidad en sí no se toca: puedes seguir trabajando en ella o borrarla ahora que su trabajo también vive en main.

Esa línea Fast-forward en la salida vale la pena leerla con atención. Significa que main no había recibido commits nuevos desde que te ramificaste, así que Git no necesitó combinar nada: simplemente movió la etiqueta main hacia adelante para que apuntara al mismo commit al que ya apuntaba add-fahrenheit-toggle. Un merge fast-forward es Git poniendo al día una etiqueta hasta donde ya debía estar, nada más.

Si main hubiera recibido otros commits mientras trabajabas —digamos que un compañero de equipo fusionó una corrección de errores mientras tanto—, Git ya no puede mover la etiqueta hacia adelante, porque main y tu rama tomaron rumbos distintos desde que se separaron. En su lugar, crea un nuevo commit que une ambas historias, llamado merge commit:

bash
$ git switch main
$ git merge add-fahrenheit-toggle
Merge made by the 'ort' strategy.
 src/index.js | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

git log --graph --oneline después de una fusión como esta muestra la bifurcación y la unión: dos líneas de historia corriendo en paralelo, que vuelven a encontrarse en el merge commit. Ninguno de los dos resultados te exige nada extra. Git decide cuál aplica según si las ramas se separaron o no, y en cualquier caso tu funcionalidad ya forma parte de main.

Ambos resultados anteriores se basan en la misma idea de fondo: la fusión de 3 vías. Para combinar dos ramas, Git compara más que solo las dos puntas. Primero encuentra el commit que ambas ramas comparten como punto de partida común, la base de fusión, y luego observa tres instantáneas: la base y la punta de cada rama. Comparar cada punta con la base compartida le dice a Git exactamente qué líneas cambió cada lado, y eso es lo que le permite combinar dos conjuntos de ediciones automáticamente en vez de preguntarte cada vez.

Un fast-forward es el caso en el que una de esas tres instantáneas resulta redundante: la base y una de las puntas son el mismo commit, así que no hay nada que combinar de ese lado, y Git mueve la etiqueta sin crear nada nuevo. Un merge commit es el caso general, donde ambos lados cambiaron algo desde la base, y Git registra una nueva instantánea con dos padres, cada uno apuntando hacia atrás en la historia de su respectiva rama. Todo conflicto de fusión que vas a encontrar en la siguiente sección surge de esta misma comparación de 3 vías, al descubrir que ambos lados tocaron las mismas líneas sin ninguna forma de combinarlas por sí sola.

JunoTraer una rama de vuelta con git mergegit merge nombre-de-rama trae los commits de esa rama hacia la rama en la que estés parado en ese momento, así que primero cambia a main. La mayoría de las fusiones ocurren en silencio y tú sigues con tu día. Nada cambia en la rama de funcionalidad en sí; puedes seguir usándola o borrarla una vez que main ya tenga el trabajo.
JunoTraer una rama de vuelta con git merge Un fast-forward significa que main no se había movido, así que Git solo deslizó la etiqueta hacia adelante. Una vez que main recibe otros commits mientras tanto, la fusión crea un merge commit real con dos padres en su lugar. De cualquier forma el comando es el mismo, git merge nombre-de-rama; Git decide qué tipo de fusión corresponde.
JunoTraer una rama de vuelta con git merge Una fusión de 3 vías compara el commit base compartido con la punta de cada rama para deducir qué cambió en cada lado, y eso es lo que hace que combinar dos ramas sea automático la mayoría de las veces. El fast-forward es el caso especial donde la base y una de las puntas ya coinciden. Yo mismo todavía me detengo a veces a deducir qué commit cuenta como base en un gráfico complicado, así que dibújalo si tienes dudas.

Cómo se ve un conflicto de fusión ​

Un conflicto ocurre cuando las mismas líneas cambian en ambos lados de una fusión, y Git no tiene forma de saber qué versión quieres. Supongamos que un compañero de equipo fusionó en main un indicador de carga que modificó una función en src/index.js, y tu rama add-fahrenheit-toggle modificó esa misma función para agregar el interruptor. Ahora la fusión se detiene a la mitad en vez de terminar:

bash
$ git switch main
$ git merge add-fahrenheit-toggle
Auto-merging src/index.js
CONFLICT (content): Merge conflict in src/index.js
Automatic merge failed; fix conflicts and then commit the result.

Eso es un conflicto de fusión. Abre el archivo y verás que Git marcó exactamente dónde las dos versiones no coinciden:

<<<<<<< HEAD
  showLoadingSpinner(true);
=======
  const tempF = celsiusToFahrenheit(tempC);
>>>>>>> add-fahrenheit-toggle

<<<<<<< HEAD marca el inicio de lo que está actualmente en tu rama (main, en este caso). ======= divide las dos versiones. >>>>>>> add-fahrenheit-toggle marca el final de la versión de la rama entrante. Todo lo que está entre los marcadores son las mismas pocas líneas, escritas de dos formas distintas.

Un conflicto significa que Git necesita tu criterio; nada está roto. Git encontró dos cambios sobre las mismas líneas y no tiene forma de adivinar cuál quieres, así que se detiene y te pide que decidas. Edita el archivo hasta que quede como lo quieres, conservando una versión, ambas, o algo nuevo que las combine, y luego borra las líneas de los marcadores:

  showLoadingSpinner(true);
  const tempF = celsiusToFahrenheit(tempC);

Cuando el archivo se vea bien, agrégalo y confírmalo exactamente como cualquier otro cambio:

bash
$ git add src/index.js
$ git commit

Ejecutar git commit sin ningún mensaje aquí abre tu editor con un mensaje que Git ya preparó, describiendo la fusión. Guarda y cierra, y la fusión queda completa.

Mientras un conflicto está abierto, git status es lo primero que vale la pena revisar. Lista todos los archivos que todavía necesitan atención bajo "Unmerged paths", así que en una fusión grande con varios archivos en conflicto sabes exactamente cuántos quedan por resolver:

bash
$ git status
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   src/index.js

git add le dice a Git que el conflicto de un archivo ya está resuelto. No verifica que en verdad hayas quitado los marcadores, así que revisa el archivo antes de agregarlo. Si la fusión resulta más complicada de lo que esperabas, o elegiste la rama equivocada, git merge --abort la cancela por completo y devuelve tu directorio de trabajo a como estaba antes de ejecutar git merge, sin dejar nada a medio terminar.

Los conflictos no son exclusivos de git merge. El rebase es la otra forma de poner una línea de historia al día con otra, y se topa con el mismo tipo de choque, aunque con un resultado distinto una vez resuelto. Una fusión une dos historias con un merge commit que tiene dos padres, conservando el hecho de que una rama existió y el momento exacto en que se reincorporó. Un rebase, en cambio, reproduce los commits de tu rama, uno a la vez, sobre la punta de la otra rama, produciendo una línea recta de historia sin merge commit y sin rastro de que las dos alguna vez se separaron:

bash
$ git switch add-fahrenheit-toggle
$ git rebase main

La contrapartida es real. Una historia lineal producida por rebase se lee de forma limpia en git log, un commit tras otro, algo que algunos equipos prefieren por lo ordenado que se ve. Una fusión conserva la forma real de lo que sucedió, incluyendo el punto exacto en que una rama de funcionalidad se reincorporó a main, algo que otros equipos prefieren por la precisión. Ninguna opción está mal; elige una convención por equipo y mantente consistente.

Hay una regla que se sostiene sin importar qué convención elijas: nunca hagas rebase de una rama sobre la que otras personas ya hicieron pull o construyeron trabajo. El rebase reescribe commits en otros nuevos con IDs nuevos, y si la historia de una rama compartida cambia debajo de un colaborador, su copia local y la remota reescrita ya no coinciden, obligándolo a hacer una corrección manual de su lado. Haz rebase libremente en tu propia rama antes de que alguien más le haya hecho pull. Una vez que se comparte, usa merge.

JunoCómo se ve un conflicto de fusión Un conflicto de fusión significa que dos ramas cambiaron las mismas líneas, y Git necesita que tú elijas el resultado. Abre el archivo, busca <<<<<<<, =======, y >>>>>>>, edita hasta que quede como quieres y luego borra esas líneas de marcadores. Termina con git add y git commit. Mi primer conflicto se sintió como una emergencia; resultó ser Git haciéndome una pregunta bastante común.
JunoCómo se ve un conflicto de fusióngit status lista todos los archivos sin resolver bajo unmerged paths, útil en el momento en que un conflicto afecta más de un archivo. git add marca un archivo como resuelto sin revisar tu trabajo, así que revísalo antes de agregarlo. Si una fusión se sale de control, git merge --abort te devuelve a un estado limpio sin nada a medio hacer.
JunoCómo se ve un conflicto de fusión El rebase se topa con los mismos choques que la fusión; reproduce los commits sobre una nueva base en vez de unir las historias, cambiando la forma preservada de la rama por una línea recta. Reserva esa opción para ramas que solo tú estés usando. En el momento en que una rama se comparte, reescribir sus commits con rebase le complica la vida a cualquiera que ya le haya hecho pull, así que en ese caso usa merge.

Uniendo todo de nuevo ​

Fusionar es lo que hace que ramificar valga la pena: experimentas de forma segura en tu propia línea de historia, tema que se cubre en Ramas, y luego incorporas ese trabajo de vuelta a main una vez que está listo, conflictos incluidos. En GitHub, la misma operación normalmente ocurre a través de un pull request en lugar de un git merge local en tu máquina; el capítulo de flujo de pull requests cubre cómo proponer, revisar y fusionar un cambio en un proyecto compartido. Si alguna vez una fusión termina en un lugar que no querías después de haberla confirmado, deshacer cosas explica cómo retroceder con seguridad.