Resolución de conflictos
Un conflicto significa que Git encontró dos cambios en el mismo lugar y se niega a adivinar. Una resolución es una decisión sobre la intención, y la opción segura por defecto es conservar la intención de ambos lados. Resolver borrando un lado hace desaparecer los marcadores pero empeora el problema: la compilación sigue pasando, y el cambio de un colega desaparece.
1. Saber en qué operación estás — ours y theirs dependen de ello
git status # "rebase in progress" / "You have unmerged paths" / "cherry-pick in progress"
| Operación | Lado --ours / HEAD | Lado --theirs |
|---|---|---|
merge | la rama en la que estás | la rama que se fusiona |
rebase | la rama sobre la que estás haciendo rebase (upstream) | tu propio commit que se está reproduciendo |
cherry-pick, revert, stash pop | la rama en la que estás | el commit que se aplica |
La fila de rebase es la trampa. Durante git rebase main, "ours" es main y "theirs" es el trabajo del propio usuario, lo contrario que en un merge. git checkout --theirs archivo en un rebase conserva tu cambio y descarta el de upstream. Vuelve a leer esta tabla antes de usar cualquiera de las dos banderas.
2. Ver el conflicto completo, no solo los marcadores
git diff --name-only --diff-filter=U # archivos sin fusionar
git status # el tipo: ambos modificados / eliminado por nosotros / ambos agregados
git config merge.conflictStyle zdiff3 # muestra también el ancestro común (git 2.35+; usa diff3 en versiones anteriores)
git checkout --conflict=zdiff3 -- <archivo> # vuelve a mostrar UN archivo con la base visible (descarta las ediciones ya hechas)
Con la base visible, los marcadores tienen tres partes: tu lado, el original, su lado. La mayoría de los conflictos aparentes se vuelven obvios cuando puedes ver de qué cambió cada lado.
Para entender por qué cambió cada lado:
git log --merge -p -- <archivo> # commits de ambos lados que tocan el archivo (merge o rebase en curso)
git show REBASE_HEAD # durante un rebase: el commit que se está reproduciendo
git show :1:<archivo> :2:<archivo> :3:<archivo> # base, "ours", "theirs" como archivos completos
Lee los mensajes de commit. Una resolución que no puedas explicar en una frase por lado es una suposición.
3. Resolver combinando
- Ambos lados editaron las mismas líneas por razones diferentes: escribe la versión que haga ambas cosas. Por ejemplo, un lado añadió un parámetro
tax_ratey el otro un parámetrodiscount: la función resuelta toma ambos y aplica ambos. - Un lado movió o renombró, el otro editó: aplica la edición en la nueva ubicación. No conserves el archivo antiguo y el nuevo.
- Eliminado por un lado, modificado por el otro: esto es una decisión real, no mecánica. Averigua por qué se eliminó; si la modificación sigue siendo relevante, debe trasladarse; de lo contrario, gana la eliminación. Pregunta al usuario si no está claro.
- Archivos generados y archivos de bloqueo (
package-lock.json,poetry.lock,go.sum, instantáneas, salida compilada): no los fusiones manualmente. Toma cualquier lado y luego vuelve a generar con la herramienta (npm install,poetry lock,go mod tidy, el comando de instantánea). - Archivos binarios: no hay fusión posible; elige un lado deliberadamente (ten en cuenta la tabla anterior) y dílo.
El texto puede fusionarse limpiamente y seguir estando mal: si un lado renombró una función y el otro añadió una llamada al nombre antiguo, Git no ve conflicto. Por eso el paso 4 incluye compilar.
Las banderas generales ocultan decisiones. git checkout --ours ., -X ours / -X theirs (que resuelven cada fragmento conflictivo hacia un lado) y -s ours (que descarta la otra rama por completo) descartan trabajo sin mirarlo. Úsalas solo cuando el usuario haya pedido exactamente eso y sepa lo que se está descartando.
4. Verificar, luego continuar
git diff --check # falla si quedan marcadores de conflicto o errores de espacio en blanco
grep -rnE '^(<<<<<<<|=======|>>>>>>>)' . # cinturón y tirantes, en archivos que git diff-check no revisa
# compilar y ejecutar las pruebas relevantes más rápidas
git add <archivos-resueltos>
Continúa con el comando correspondiente. Añade GIT_EDITOR=true donde se abriría un editor, ya que un editor se cuelga en una terminal no interactiva:
git merge --continue # o: git commit --no-edit
GIT_EDITOR=true git rebase --continue
GIT_EDITOR=true git cherry-pick --continue
Un rebase reproduce commits uno a uno, por lo que puede detenerse de nuevo en el siguiente commit. Repite el bucle. Resuelve en el primer commit donde aparezca un conflicto; los posteriores pueden ser el mismo conflicto reproduciéndose.
Si las pruebas fallan después de resolver, el fallo es información sobre la resolución. No continúes ignorándolo ni "lo arregles" editando las pruebas hasta que sepas qué comportamiento de qué lado está protegiendo la prueba.
5. Cuándo detenerse y preguntar
Detente en lugar de adivinar cuando:
- ambos lados cambiaron la lógica de negocio de formas incompatibles (dos reglas fiscales diferentes, dos políticas de reintento diferentes);
- no puedes decir en una frase por qué cada lado hizo su cambio;
- la resolución hace que falle una prueba y la causa no está clara.
Indica las dos intenciones, di qué elegirías y por qué, y deja que el usuario decida. Todo antes de --continue es reversible: git merge --abort, git rebase --abort, git cherry-pick --abort devuelven el árbol a su estado inicial.
Conflictos repetidos
Para un rebase largo donde el mismo conflicto se repite, o una rama que se fusiona y abandona repetidamente:
git config rerere.enabled true # registra cada resolución y la reproduce la próxima vez
git rerere diff # lo que registró
git rerere forget-path <archivo> # descarta una resolución registrada que era incorrecta
rerere reaplica una resolución sin preguntar, por lo que una mala se propaga. Después de un conflicto resuelto por rerere, sigue verificando el resultado.
Cuando una rama tiene mucha vida y está muy desactualizada, fusionar el tronco una vez normalmente significa una ronda de conflictos; hacer rebase los reproduce por commit. Esa elección es del usuario.
Barreras de seguridad
- Nunca resuelvas tomando un lado por completo para que desaparezcan los conflictos. El otro lado es el trabajo de alguien.
- No abortes un rebase que el usuario te pidió terminar solo porque sea tedioso; aborta solo si va mal, y dilo.
- No hagas push después de resolver a menos que te lo pidan. Un rebase necesita un force-push; sigue
git-safe-history-rewrite. - Informa sobre cada archivo en conflicto: qué hizo cada lado, qué conservaste y cualquier cosa que no pudieras verificar.