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 conmutador funciona, has confirmado 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 es rápido y silencioso: Git compara las dos ramas, no encuentra nada que se superponga, e integra el trabajo sin pasos adicionales. A veces, sin embargo, tú y un compañero de equipo cambiaron las mismas líneas exactas en ramas diferentes, y Git no puede adivinar qué versión quieres mantener. Esa situación aparece en casi todos los proyectos con más de una persona confirmando en él. Es una parte normal y cotidiana de ramificar, y significa que Git encontró dos ideas sobre las mismas líneas y quiere tu decisión sobre cuál gana.

Traer una rama de vuelta con git merge

Digamos que construiste el conmutador de Fahrenheit en su propia rama, add-fahrenheit-toggle, y main no ha avanzado desde que te ramificaste. 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 <branch> siempre fusiona la rama nombrada en la rama en la que estés parado, así que cambiar a main primero es importante. La rama en la que estés parado es la que recibe la fusión. Después de 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 eliminarla ahora que su trabajo también vive en main.

Esa línea Fast-forward en la salida vale la pena leerla cuidadosamente. Significa que main no había ganado ningún commit nuevo desde que te ramificaste, así que Git no necesitaba combinar nada: movió la etiqueta main hacia adelante para apuntar al mismo commit que add-fahrenheit-toggle ya apuntaba. Una fusión de avance rápido es Git poniendo una etiqueta al día a donde ya necesitaba estar, nada más.

Si main hubiera recogido otros commits mientras trabajabas, digamos que un compañero fusionó una corrección de error en el ínterin, Git ya no puede mover la etiqueta hacia adelante, porque main y tu rama han ido en direcciones diferentes desde que se dividieron. En su lugar, crea un nuevo commit que une ambos historiales, llamado un commit de fusión:

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 lado a lado, encontrándose de nuevo en el commit de fusión. Ninguno de los dos resultados te pide nada adicional. Git decide cuál se aplica basándose en si las ramas divergieron, y de cualquier manera tu funcionalidad es ahora parte de main.

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

Un avance rápido es el caso donde una de esas tres instantáneas resulta ser redundante: la base y una punta son el mismo commit, así que no hay nada de ese lado para combinar, y Git mueve la etiqueta sin crear nada nuevo. Un commit de fusión es el caso general, donde ambos lados cambiaron algo desde la base, y Git registra una nueva instantánea con dos padres, uno apuntando atrás en el historial de cada rama. Todo conflicto de fusión que encuentres en la próxima sección viene de la misma comparación de 3 vías encontrando que ambos lados tocaron las mismas líneas, sin forma de combinarlas por su cuenta.

JunoTraer una rama de vuelta con git mergegit merge branch-name trae los commits de esa rama a la rama en la que estés parado, así que cambia a main primero. La mayoría de las fusiones suceden silenciosamente en el trasfondo y sigues adelante con tu día. Nada sobre la rama de funcionalidad en sí cambia; puedes seguir usándola o eliminarla una vez que main tenga el trabajo.
JunoTraer una rama de vuelta con git merge Un avance rápido significa que main no había avanzado, así que Git solo deslizó la etiqueta hacia adelante. Una vez que main recoge otros commits en el ínterin, fusionar crea un verdadero commit de fusión con dos padres en su lugar. De cualquier forma el comando es el mismo git merge branch-name; Git decide qué tipo de fusión se ajusta.
JunoTraer una rama de vuelta con git merge Una fusión de 3 vías compara el commit base compartido contra cada punta de rama para averiguar qué cambió en cada lado, lo que hace que combinar dos ramas sea automático la mayoría de las veces. El avance rápido es el caso especial donde la base y una punta ya coinciden. Todavía me atrapo pausándome para averiguar qué commit cuenta como la base en un gráfico desordenado, así que dibújalo si no estás seguro.

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 decirte qué versión quieres. Digamos que un compañero fusionó un spinner de carga en main que cambió una función en src/index.js, y tu rama add-fahrenheit-toggle cambió la misma función para añadir el conmutador. Fusionar ahora se detiene a mitad de camino en lugar 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.

Ese es un conflicto de fusión. Abre el archivo y encontrarás que Git ha marcado exactamente dónde las dos versiones no están de acuerdo:

<<<<<<< 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 entre los marcadores es el mismo puñado de líneas, escritas de dos formas diferentes.

Un conflicto significa que Git necesita tu juicio; nada está roto. Git encontró dos cambios en 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 se lea como quieres, manteniedo cualquiera de las versiones, ambas, o algo nuevo que las combine, luego elimina las líneas de marcador en sí:

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

Una vez que el archivo se ve bien, etapifica y confirma como cualquier otro cambio:

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

Ejecutar git commit sin mensaje aquí abre tu editor con un mensaje que Git ya preparó, describiendo la fusión. Guarda y ciérralo, y la fusión está completa.

Mientras un conflicto está abierto, git status es lo primero que vale la pena verificar. Lista cada archivo que aún necesita atención bajo "Unmerged paths", así que en una fusión más grande con varios archivos en conflicto sabes exactamente cuántos quedan por arreglar:

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 está resuelto. No verifica que realmente hayas eliminado los marcadores, así que lee el archivo de nuevo antes de etapificarlo. Si la fusión se ve más desordenada de lo que esperabas, o elegiste la rama incorrecta, git merge --abort se retira completamente y devuelve tu directorio de trabajo a cómo se veía antes de ejecutar git merge, sin nada a mitad de camino dejado atrás.

Los conflictos no son exclusivos de git merge. Rebase es la otra forma de traer una línea de historia al día con otra, y golpea el mismo tipo de choque, con un resultado diferente una vez que se resuelve. Una fusión une dos historiales con un commit de fusión que tiene dos padres, preservando el hecho de que una rama existía y exactamente cuándo se reincorporó. Un rebase en su lugar 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 commit de fusión y sin rastro de que los dos alguna vez divergieron:

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

El intercambio es real. Un historial lineal de rebase se lee limpiamente en git log, un commit tras otro, que algunos equipos prefieren para una historia ordenada. Una fusión mantiene la forma verdadera de lo que sucedió, incluido el punto exacto donde una rama de funcionalidad se reincorporó a main, que algunos equipos prefieren por la precisión. Ninguna opción es incorrecta; elige una convención por equipo y mantén coherencia.

Una regla se sostiene independientemente de qué convención elijas: nunca hagas rebase en una rama en la que otras personas ya han extraído o construido trabajo. Rebase reescribe commits en otros nuevos con nuevos IDs, y si el historial de una rama compartida cambia debajo de un colaborador, su copia local y la remota reescrita ya no están de acuerdo, forzando un arreglo manual de su parte. Haz rebase libremente en tu propia rama antes de que cualquier otro la haya extraído. Una vez que se comparta, fusiona.

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 elijas el resultado. Abre el archivo, busca <<<<<<<, =======, y >>>>>>>, edita hasta que se lea como quieres, luego elimina esas líneas de marcador. Termina con git add y git commit. Mi primer conflicto se sintió como una emergencia; resultó ser Git haciéndome una pregunta bastante ordinaria.
JunoCómo se ve un conflicto de fusióngit status lista cada archivo sin resolver bajo unmerged paths, útil en el momento en que un conflicto toca más de uno. git add marca un archivo como resuelto sin verificar tu trabajo, así que léelo de nuevo antes de etapificar. Si una fusión sale mal, git merge --abort te devuelve a un estado limpio sin nada a mitad de camino.
JunoCómo se ve un conflicto de fusión Rebase se encuentra con los mismos choques que merge; reproduce commits sobre una nueva base en lugar de unir historiales, intercambiando una forma de rama preservada por una línea recta. Mantén ese intercambio a ramas que solo tú usas. En el momento en que una rama se comparte, reescribir sus commits con rebase hace la vida difícil para cualquiera que ya la haya extraído, así que recurre a merge en su lugar.

Volverlo a juntar

Fusionar es lo que hace que ramificar valga la pena: experimentas de manera segura en tu propia línea de historia, cubierto en Branches, luego integras ese trabajo de vuelta en main una vez que está listo, conflictos y todo. En GitHub, la misma operación generalmente sucede a través de una pull request en lugar de un git merge local en tu máquina; el capítulo pull request flow cubre proponer, revisar e integrar un cambio en un proyecto compartido. Si una fusión alguna vez llega a algún lugar que no intentabas después de que ya la hayas confirmado, undoing things cubre cómo retroceder de manera segura.