Fins ara hem tingut sort. Totes les fusions del mòdul han sortit bé perquè l'Ana i en Bruno tocaven zones diferents dels fitxers. S'ha acabat la sort: en aquesta lliçó tots dos modificaran les mateixes línies de la mateixa funció, cadascun per un motiu perfectament raonable, i Git s'aturarà a mitja fusió.

Els conflictes de fusió tenen mala fama, i és immerescuda. Un conflicte no és un error, no és una corrupció i no vol dir que hagis fet res malament. És Git dient-te, amb tota l'honestedat del món: «aquí hi ha dos canvis incompatibles sobre les mateixes línies i no tinc criteri per triar; decideix tu». L'alternativa —que un algorisme decidís pel seu compte quin codi de quina persona sobreviu— seria molt pitjor.

El que sí que genera angoixa és la sensació d'estar atrapat: el repositori en un estat rar, uns símbols estranys dins del codi i el dubte de si acabes de trencar alguna cosa. Aquesta lliçó elimina aquesta angoixa. En acabar-la sabràs exactament en quin estat és el teu repositori durant un conflicte, com llegir el que Git ha escrit al fitxer, com resoldre'l i, si t'atabales, com fer marxa enrere sense deixar rastre.

Contingut

  1. Per què es produeix un conflicte
  2. Provocant un conflicte de debò
  3. Els marcadors de conflicte
  4. L'estil diff3 i zdiff3: veure també l'ancestre
  5. Orientar-se durant el conflicte: git status
  6. git diff durant un conflicte: les tres versions
  7. Resoldre i marcar com a resolt
  8. Dreceres: --ours i --theirs per fitxer
  9. Avortar: --abort i --quit
  10. git mergetool: eines gràfiques
  11. Conflictes especials: esborrat davant de modificat
  12. git rerere: no resoldre dues vegades el mateix

  1. Per què es produeix un conflicte

Recorda la taula de decisió de la fusió a tres bandes de la lliçó 03-03. Per a cada tros de fitxer, Git compara tres versions: la de l'ancestre comú (base), la de la teva branca (ours) i la de la branca que fusiones (theirs).

Va canviar a ours Va canviar a theirs Resultat
No No Es conserva la base
No Guanya ours
No Guanya theirs
Sí, igual Guanya aquesta versió comuna
Sí, diferent CONFLICTE

Només l'última fila produeix conflicte: les dues branques van canviar el mateix de manera diferent.

I cal precisar què significa «el mateix», perquè és la font d'una por molt estesa:

  • Que dues persones toquin el mateix fitxer NO produeix conflicte. Si l'Ana edita la línia 10 i en Bruno la 200, Git combina totes dues sense dir res.
  • Produeix conflicte que toquin les mateixes línies, o línies tan properes que cauen al mateix tros (fragment) del diff. Git treballa amb un marge de context de tres línies, de manera que canvis separats per una o dues línies també poden xocar.

A més dels conflictes de contingut, existeixen els conflictes d'arbre (tree conflicts), que afecten l'existència o la ubicació del fitxer en lloc del seu interior:

Tipus Situació
Contingut Les dues branques modifiquen les mateixes línies
Esborrat/modificat Una branca esborra el fitxer, l'altra el modifica
Afegit/afegit Les dues branques creen un fitxer amb el mateix nom i contingut diferent
Reanomenat/esborrat Una branca el reanomena, l'altra l'esborra
Reanomenat/reanomenat Totes dues el reanomenen, amb noms diferents
Directori/fitxer Una branca crea informes/ i l'altra un fitxer anomenat informes

Els de contingut són el 90 % dels casos i són el que veurem primer. El d'esborrat/modificat, per ser el segon més freqüent, té el seu apartat.

  1. Provocant un conflicte de debò

Tornem al projecte. main està al dia amb tot el que s'ha integrat a les lliçons anteriors, i app.js conté aquesta funció, que pinta el llistat de tasques a la pantalla:

function pintaLlista() {
  const llista = document.getElementById('llista-tasques');
  llista.innerHTML = '';
  tasques.forEach(function (t) {
    llista.appendChild(creaElementTasca(t));
  });
  actualitzaComptador();
}

L'Ana obre una branca per mostrar les tasques ordenades alfabèticament:

git switch -c funcionalitat/ordre-alfabetic main

I modifica la funció:

function pintaLlista() {
  const llista = document.getElementById('llista-tasques');
  llista.innerHTML = '';
  const ordenades = tasques.slice().sort(function (a, b) {
    return a.titol.localeCompare(b.titol);
  });
  ordenades.forEach(function (t) {
    llista.appendChild(creaElementTasca(t));
  });
  actualitzaComptador();
}
git commit -am "Mostra les tasques ordenades alfabèticament"
[funcionalitat/ordre-alfabetic a1e5c93] Mostra les tasques ordenades alfabèticament
 1 file changed, 4 insertions(+), 1 deletion(-)

En Bruno, mentrestant, obre una altra branca des del mateix punt per arreglar una cosa que li han reportat: quan no hi ha tasques, la pantalla queda en blanc i sembla que l'aplicació estigui trencada.

git switch -c correccio/missatge-llista-buida main
function pintaLlista() {
  const llista = document.getElementById('llista-tasques');
  llista.innerHTML = '';
  if (tasques.length === 0) {
    llista.innerHTML = '<li class="buit">No hi ha tasques pendents</li>';
    actualitzaComptador();
    return;
  }
  tasques.forEach(function (t) {
    llista.appendChild(creaElementTasca(t));
  });
  actualitzaComptador();
}
git commit -am "Mostra un missatge quan el llistat és buit"
[correccio/missatge-llista-buida 6d3f8b2] Mostra un missatge quan el llistat és buit
 1 file changed, 5 insertions(+)
gitGraph
   commit id: "3b9e7d1"
   branch ordre-alfabetic
   checkout ordre-alfabetic
   commit id: "a1e5c93"
   checkout main
   branch missatge-llista-buida
   checkout missatge-llista-buida
   commit id: "6d3f8b2"

Els dos canvis són bons i el projecte els vol tots dos. Però tots dos insereixen codi just al mateix punt: entre llista.innerHTML = ''; i el forEach. Git no té manera de saber si l'if d'en Bruno va abans o després del sort de l'Ana, ni si el forEach ha de recórrer tasques o ordenades.

L'Ana integra primer el seu, que va net:

git switch main
git merge --no-ff --no-edit funcionalitat/ordre-alfabetic
Merge made by the 'ort' strategy.
 app.js | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

I ara, el d'en Bruno:

git merge --no-ff correccio/missatge-llista-buida
Auto-merging app.js
CONFLICT (content): Merge conflict in app.js
Automatic merge failed; fix conflicts and then commit the result.

Ja hi som. Anem a dissecar-ho.

  1. Els marcadors de conflicte

El primer que cal entendre: Git ha modificat el teu fitxer al disc. app.js ja no conté codi vàlid de JavaScript, sinó codi amb anotacions a dins:

function pintaLlista() {
  const llista = document.getElementById('llista-tasques');
  llista.innerHTML = '';
<<<<<<< HEAD
  const ordenades = tasques.slice().sort(function (a, b) {
    return a.titol.localeCompare(b.titol);
  });
  ordenades.forEach(function (t) {
=======
  if (tasques.length === 0) {
    llista.innerHTML = '<li class="buit">No hi ha tasques pendents</li>';
    actualitzaComptador();
    return;
  }
  tasques.forEach(function (t) {
>>>>>>> correccio/missatge-llista-buida
    llista.appendChild(creaElementTasca(t));
  });
  actualitzaComptador();
}

Els marcadors per defecte són tres:

Marcador Significat
<<<<<<< HEAD Comença la versió de la teva branca (ours). L'etiqueta indica d'on ve
======= Separador: acaba ours, comença theirs
>>>>>>> correccio/missatge-llista-buida Acaba la versió de l'altra branca (theirs), etiquetada amb el seu nom

Observacions importants que es passen per alt:

  1. Només la zona conflictiva porta marcadors. Les tres primeres línies de la funció i les tres últimes són netes: Git les va fusionar sense problema. Un fitxer de 400 línies amb un conflicte de 6 té 394 línies ja resoltes.

  2. Un fitxer pot tenir diversos blocs de conflicte, cadascun amb el seu joc de marcadors. Cal resoldre'ls tots.

  3. Els marcadors són text normal. No hi ha res de màgic: els pots editar, esborrar o deixar (si els deixes, git commit t'avisarà, però pots forçar-ho i ficar brossa al projecte, així que revisa sempre).

  4. L'etiqueta de la dreta és el nom de la branca fusionada, cosa que ajuda molt quan estàs fusionant diverses coses seguides i t'has perdut.

I aquí hi ha el problema del format per defecte: no veus el que hi havia abans. El forEach sobre tasques de la versió d'en Bruno és un canvi seu o simplement el codi original que ell no va tocar? Amb aquest format no ho pots saber, i aquesta informació és exactament la que necessites per resoldre bé. Per això hi ha l'apartat següent.

  1. L'estil diff3 i zdiff3: veure també l'ancestre

Git pot mostrar tres seccions en lloc de dues, afegint-hi la de l'ancestre comú. Es controla amb l'opció merge.conflictStyle:

git config --global merge.conflictStyle diff3

Avortem i repetim la fusió per veure-ho:

git merge --abort
git merge --no-ff correccio/missatge-llista-buida

Ara app.js conté:

function pintaLlista() {
  const llista = document.getElementById('llista-tasques');
  llista.innerHTML = '';
<<<<<<< HEAD
  const ordenades = tasques.slice().sort(function (a, b) {
    return a.titol.localeCompare(b.titol);
  });
  ordenades.forEach(function (t) {
||||||| 3b9e7d1
  tasques.forEach(function (t) {
=======
  if (tasques.length === 0) {
    llista.innerHTML = '<li class="buit">No hi ha tasques pendents</li>';
    actualitzaComptador();
    return;
  }
  tasques.forEach(function (t) {
>>>>>>> correccio/missatge-llista-buida
    llista.appendChild(creaElementTasca(t));
  });
  actualitzaComptador();
}

El bloc nou, delimitat per ||||||| i etiquetat amb el hash de l'ancestre comú, conté el codi original. I ara la lectura és completament diferent:

  • L'ancestre tenia tasques.forEach(...).
  • L'Ana ho va canviar pel bloc sort + ordenades.forEach(...).
  • En Bruno va afegir l'if al davant i no va tocar el forEach.

Amb aquesta informació, la resolució és evident: cal quedar-se amb les dues coses, posant l'if d'en Bruno primer i respectant el canvi de tasques a ordenades de l'Ana.

Sense l'ancestre, ho hauries hagut de deduir o consultar l'historial. Amb ell, es veu d'un cop d'ull.

zdiff3, encara millor

Des de Git 2.35 existeix un estil més: zdiff3 (per zealous diff3). Fa el mateix que diff3 però, a més, treu fora del conflicte les línies comunes als dos costats:

git config --global merge.conflictStyle zdiff3

En conflictes on els dos costats comparteixen línies al principi o al final del bloc, la zona conflictiva es redueix notablement i només queda a dins allò que de debò discrepa.

Estil Seccions Disponible des de Recomanació
merge 2 (ours, theirs) Sempre El de per defecte; el pitjor dels tres
diff3 3 (ours, base, theirs) Sempre Molt superior; adopta'l
zdiff3 3, amb les comunes fora Git 2.35 (2022) El millor si la teva versió el suporta

Aquest és probablement el consell més rendible de tota la lliçó. Posa'l ara mateix:

git config --global merge.conflictStyle zdiff3

Encaixa amb el que ja vas configurar a la lliçó 01-06 i no té cap contrapartida.

  1. Orientar-se durant el conflicte: git status

Durant un conflicte, el repositori és en un estat especial. git status és la teva brúixola i canvia del tot:

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:   app.js

no changes added to commit (use "git add" to commit)

Elements clau:

  • You have unmerged paths: hi ha una fusió a mitges.
  • Unmerged paths: secció nova, diferent de «preparats» i «sense preparar». Aquí hi van els fitxers en conflicte.
  • both modified: el tipus de conflicte. Altres possibles: deleted by us, deleted by them, both added, added by us.
  • Git et recorda les dues sortides: resoldre i confirmar, o avortar.

En format curt:

git status --short
UU app.js

UU significa unmerged a les dues columnes. La taula completa de codis de conflicte:

Codi Significat
UU Modificat per tots dos (both modified)
AA Afegit per tots dos (both added)
DD Esborrat per tots dos
AU Afegit per nosaltres, sense modificar per ells
UA Afegit per ells
DU Esborrat per nosaltres, modificat per ells
UD Modificat per nosaltres, esborrat per ells

Si només vols la llista de fitxers pendents, sense soroll:

git diff --name-only --diff-filter=U
app.js

És l'ordre que es fa servir en scripts i en dreceres d'editor per saltar d'un conflicte a un altre.

Què hi ha dins de .git en aquest moment

Val la pena mirar-ho un cop, perquè desmitifica l'estat:

ls .git/MERGE_HEAD .git/MERGE_MSG
.git/MERGE_HEAD  .git/MERGE_MSG
cat .git/MERGE_HEAD
6d3f8b2e4a9c1f5b7d3a8e2c6b4f9d1a5c7e3b8f

MERGE_HEAD guarda el commit que estàs fusionant. La seva existència és el que defineix «estar en una fusió»: és per això que git commit sap que ha de crear un commit amb dos pares, i per això git merge --abort sap què ha de desfer. MERGE_MSG guarda el missatge proposat.

I l'índex (l'àrea de preparació) també és especial ara. Normalment guarda una versió de cada fitxer; durant un conflicte en guarda tres, numerades per etapes:

git ls-files -u
100644 8e1f5b3d7a2c9e4b6f1d8a3c5e7b2d9f4a6c8e1b 1	app.js
100644 2a7c9e4f1b6d8a3c5e2f7b9d4a1c6e8b3f5d7a2c 2	app.js
100644 9f3b7d1a5c8e2f6b4d9a1c7e3b5f8d2a6c4e9b1f 3	app.js
Etapa Versió
1 La de l'ancestre comú (base)
2 La de la teva branca (ours)
3 La de la branca fusionada (theirs)

Aquestes tres etapes són el que fa possible tot el que ve a continuació: els diffs especials, --ours/--theirs i les eines gràfiques. Quan resols i fas git add, les tres etapes se substitueixen per una única versió normal, i així és com Git sap que aquest fitxer ja està resolt.

  1. git diff durant un conflicte: les tres versions

git diff a seques, durant un conflicte, mostra un format que no havies vist: el diff combinat.

git diff
diff --cc app.js
index 2a7c9e4,9f3b7d1..0000000
--- a/app.js
+++ b/app.js
@@@ -10,7 -10,11 +10,16 @@@ function pintaLlista()
    const llista = document.getElementById('llista-tasques');
    llista.innerHTML = '';
++<<<<<<< HEAD
 +  const ordenades = tasques.slice().sort(function (a, b) {
 +    return a.titol.localeCompare(b.titol);
 +  });
 +  ordenades.forEach(function (t) {
++=======
+   if (tasques.length === 0) {
+     llista.innerHTML = '<li class="buit">No hi ha tasques pendents</li>';
+     actualitzaComptador();
+     return;
+   }
+   tasques.forEach(function (t) {
++>>>>>>> correccio/missatge-llista-buida
      llista.appendChild(creaElementTasca(t));
    });
    actualitzaComptador();

Fixa't que hi ha dues columnes de marcadors +/- en lloc d'una, i en la capçalera @@@ amb tres arrobes. Cada columna correspon a un pare. És un format dens; a la pràctica es fan servir més els diffs per parelles.

Per comparar el fitxer en conflicte amb cadascuna de les tres versions:

# Davant de la versió de l'ancestre comú (etapa 1)
git diff --base app.js

# Davant de la versió de la teva branca (etapa 2)
git diff --ours app.js

# Davant de la versió de la branca fusionada (etapa 3)
git diff --theirs app.js

També pots recuperar qualsevol de les tres versions completes per veure-les de manera aïllada, amb la sintaxi :<etapa>:<ruta>:

git show :1:app.js    # la de l'ancestre
git show :2:app.js    # la teva
git show :3:app.js    # la de l'altra branca

Això és enormement útil quan el conflicte és gran i vols llegir cada versió sencera sense marcadors, en lloc d'intentar desxifrar el fitxer barrejat. Per exemple, per guardar la versió d'en Bruno en un fitxer a part i consultar-la mentre edites:

git show :3:app.js > /tmp/versio-bruno.js

  1. Resoldre i marcar com a resolt

Resoldre un conflicte és, simplement, deixar el fitxer com ha de quedar. Ni més ni menys. No hi ha cap ordre màgica: s'edita el fitxer, es treuen els marcadors i s'escriu el codi correcte.

En el nostre cas, sabem pel bloc ||||||| que cal combinar les dues aportacions. L'Ana edita app.js i el deixa així:

function pintaLlista() {
  const llista = document.getElementById('llista-tasques');
  llista.innerHTML = '';
  if (tasques.length === 0) {
    llista.innerHTML = '<li class="buit">No hi ha tasques pendents</li>';
    actualitzaComptador();
    return;
  }
  const ordenades = tasques.slice().sort(function (a, b) {
    return a.titol.localeCompare(b.titol);
  });
  ordenades.forEach(function (t) {
    llista.appendChild(creaElementTasca(t));
  });
  actualitzaComptador();
}

Sense marcadors. L'if d'en Bruno va primer (si no hi ha tasques, no té sentit ordenar-les) i després el sort de l'Ana. És codi que no existia a cap de les dues branques: és la síntesi que només una persona podia fer. Això és exactament el que Git t'estava demanant.

Abans de donar res per bo, cal provar-ho. Un conflicte resolt sense executar el codi és una juguesca:

# Comprovar que no queda cap marcador oblidat
grep -n '^<<<<<<<\|^=======\|^>>>>>>>\|^|||||||' app.js
(sense sortida)

I a continuació, obrir l'aplicació o passar les proves.

Ara es marca com a resolt:

git add app.js

git add sobre un fitxer en conflicte té un significat especial: substitueix les tres etapes de l'índex per la versió del disc i declara el conflicte resolt. No és «preparar un canvi»; és «he decidit, aquesta és la versió bona».

git status
On branch main
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:
	modified:   app.js

All conflicts fixed but you are still merging: la fusió continua en curs (MERGE_HEAD encara existeix), però ja no hi ha res bloquejat. Només falta confirmar:

git commit

Git obre l'editor amb el missatge que va guardar a MERGE_MSG, ara amb una anotació addicional:

Merge branch 'correccio/missatge-llista-buida'

# Conflicts:
#	app.js
#
# It looks like you may be committing a merge.
# If this is not correct, please run
#	git update-ref -d MERGE_HEAD
# and try again.

És un excel·lent costum descriure com s'ha resolt, perquè qui llegeixi això d'aquí a un any ho agrairà:

Fusiona el missatge de llista buida

Conflicte a pintaLlista(): la branca d'en Bruno afegia la comprovació
de llistat buit i la de l'Ana l'ordre alfabètic, totes dues al mateix
punt. Es conserven les dues: primer la sortida anticipada si no hi ha
tasques, després l'ordenació.
[main c2a8f1e] Fusiona el missatge de llista buida
git log --oneline --graph -5
*   c2a8f1e (HEAD -> main) Fusiona el missatge de llista buida
|\
| * 6d3f8b2 (correccio/missatge-llista-buida) Mostra un missatge quan el llistat és buit
* |   e1f3a7b Merge branch 'funcionalitat/ordre-alfabetic'
|\ \
| * | a1e5c93 (funcionalitat/ordre-alfabetic) Mostra les tasques ordenades alfabèticament
|/ /
* / 3b9e7d1 Afegeix l'exportació del llistat de tasques a CSV
|/

I aquí és on --cc, que vam veure a la lliçó 03-03, cobra tot el sentit:

git show --cc c2a8f1e

Mostra només les línies que difereixen dels dos pares, és a dir, exactament el codi que l'Ana va escriure a mà en resoldre. És la manera d'auditar una resolució de conflicte sense llegir tot el fitxer.

  1. Dreceres: --ours i --theirs per fitxer

De vegades no hi ha res per sintetitzar: una de les dues versions és la correcta i punt. Típic en fitxers generats automàticament, fitxers de bloqueig de dependències o quan saps que la feina d'un costat va deixar obsoleta la de l'altre.

# Quedar-se amb la versió de la teva branca, sencera
git checkout --ours app.js

# Quedar-se amb la versió de la branca fusionada, sencera
git checkout --theirs app.js

O amb les ordres modernes que vam veure a la lliçó 03-02:

git restore --ours app.js
git restore --theirs app.js

Aquestes ordres sobreescriuen el fitxer del disc amb l'etapa 2 o la 3 de l'índex, marcadors inclosos a fora. Després cal marcar-lo com a resolt igualment:

git restore --theirs paquets.lock
git add paquets.lock

Dos avisos importants:

  1. Agafen el fitxer SENCER, no només la zona conflictiva. Si app.js tenia a més canvis de l'altra branca en una altra part del fitxer que s'havien fusionat bé, --ours també els llença. Fes-ho servir només quan de debò vulguis una versió completa.

  2. No els confonguis amb -X ours/-X theirs de la lliçó anterior. Aquests actuen fitxer a fitxer, durant un conflicte ja produït; aquells actuen globalment, abans que el conflicte aparegui.

Un patró molt pràctic quan hi ha diversos fitxers i només alguns són «automàtics»:

# Resoldre a mà els fitxers de codi
vim app.js
git add app.js

# Els generats, amb la versió entrant
git restore --theirs dist/bundle.js paquets.lock
git add dist/bundle.js paquets.lock

git commit

I si prefereixes començar de zero un fitxer que has espatllat editant:

git checkout --merge app.js

Restaura el fitxer amb els marcadors de conflicte originals, tal com estava just després de la fusió fallida.

  1. Avortar: --abort i --quit

Si t'has perdut, si el conflicte és molt més gran del que esperaves o si simplement prefereixes fer-ho en un altre moment, se'n surt sense deixar rastre:

git merge --abort
(sense sortida)
git status
On branch main
nothing to commit, working tree clean

Com si la fusió no hagués existit. git merge --abort ho desfà tot: restaura el directori de treball i l'índex a l'estat anterior a git merge, i esborra MERGE_HEAD i MERGE_MSG.

És l'operació més tranquil·litzadora de Git i convé interioritzar-la: durant un conflicte mai no estàs atrapat. Sempre hi ha una tecla d'escapament.

Una precaució: si tenies canvis sense confirmar abans de començar la fusió, --abort pot no ser capaç de recuperar l'estat exacte. Per això la recomanació de la lliçó 03-03 —fusionar amb el directori net— no és una mania.

Existeix també un cosí rar:

git merge --quit

--quit surt de l'estat de fusió (esborra MERGE_HEAD) però deixa el directori de treball i l'índex tal com estan, amb els marcadors i tot. És per a casos molt concrets: vols conservar el resultat a mitges però no vols que Git creï un commit de fusió amb dos pares. Si no saps que el necessites, no el necessites: fes servir --abort.

Ordre Estat de fusió Directori de treball Quan
git merge --abort Es cancel·la Es restaura a l'estat previ Gairebé sempre
git merge --quit Es cancel·la Es deixa com està Casos avançats
git commit Es completa Es conserva el que s'ha resolt Quan has resolt

  1. git mergetool: eines gràfiques

Per a conflictes grans, editar marcadors a mà és incòmode. git mergetool obre una eina de tres o quatre plafons: base, ours, theirs i resultat.

git mergetool
Merging:
app.js

Normal merge conflict for 'app.js':
  {local}: modified file
  {remote}: modified file
Hit return to start merge resolution tool (vimdiff):

Si no n'has configurat cap, Git tria la primera que trobi instal·lada. Per fixar-ne una:

# Meld (multiplataforma, molt recomanable per començar)
git config --global merge.tool meld

# VS Code
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait --merge $REMOTE $LOCAL $BASE $MERGED'

# KDiff3
git config --global merge.tool kdiff3

# vimdiff
git config --global merge.tool vimdiff

I una opció que gairebé tothom acaba activant:

git config --global mergetool.keepBackup false

Sense ella, git mergetool deixa fitxers *.orig amb la versió conflictiva per tot arreu, i després s'han d'esborrar a mà (o ignorar-los al .gitignore).

Les quatre variables que rep l'eina, i que convé entendre per configurar la teva:

Variable Contingut
$BASE La versió de l'ancestre comú (etapa 1)
$LOCAL La teva versió (etapa 2, ours)
$REMOTE La versió entrant (etapa 3, theirs)
$MERGED El fitxer de destinació, on escrius el resultat

En desar i sortir de l'eina, git mergetool marca el fitxer com a resolt automàticament (fa el git add per tu) i passa al conflicte següent si n'hi ha.

Un apunt pràctic: avui la majoria d'editors moderns inclouen el seu propi resolutor de conflictes que detecta els marcadors al fitxer i ofereix botons tipus «acceptar l'actual / acceptar l'entrant / acceptar tots dos». Funcionen sobre el mateix fitxer amb marcadors que hem vist, així que tot el que has après aquí s'hi aplica igual. git mergetool continua sent útil quan vols els quatre plafons amb l'ancestre a la vista.

  1. Conflictes especials: esborrat davant de modificat

El segon tipus de conflicte més freqüent no passa dins del fitxer, sinó sobre la seva existència.

Situació: el projecte tenia un fitxer notes-internes.md amb apunts del principi del desenvolupament. L'Ana va decidir que estava obsolet i el va esborrar a la seva branca:

git switch -c neteja/esborrar-notes main
git rm notes-internes.md
git commit -m "Esborra les notes internes, ja obsoletes"

En Bruno, que no ho sabia, el va actualitzar a la seva:

git switch -c documentacio/actualitzar-notes main
# … edita notes-internes.md …
git commit -am "Actualitza les notes internes amb el flux nou"

L'Ana fusiona el seu a main (sense problema) i després el d'en Bruno:

git merge documentacio/actualitzar-notes
CONFLICT (modify/delete): notes-internes.md deleted in HEAD and modified in
documentacio/actualitzar-notes.  Version documentacio/actualitzar-notes of
notes-internes.md left in tree.
Automatic merge failed; fix conflicts and then commit the result.

Fixa't en dues coses:

  1. El tipus de conflicte és modify/delete, no content.
  2. No hi ha marcadors dins del fitxer. No tindria sentit: el conflicte no és sobre el seu contingut, sinó sobre si ha d'existir. Git ha deixat al disc la versió d'en Bruno, com diu el missatge.
git status
On branch main
You have unmerged paths.

Unmerged paths:
  (use "git add/rm <file>..." as appropriate to mark resolution)
	deleted by us:   notes-internes.md

deleted by us: nosaltres (la branca on som) el vam esborrar, ells el van modificar. Git no pot decidir si l'esborrat era correcte o si les actualitzacions d'en Bruno el justifiquen; això depèn de per què es va esborrar, i això només ho sap una persona.

La resolució consisteix a declarar què ha de passar, i només hi ha dues opcions:

# Opció A: mantenir l'esborrat (el fitxer ja no ha d'existir)
git rm notes-internes.md
notes-internes.md: needs merge
rm 'notes-internes.md'
# Opció B: conservar el fitxer amb els canvis d'en Bruno
git add notes-internes.md

En tots dos casos, després:

git commit

Abans de decidir, convé mirar què contenien aquests canvis, no fos cas que en Bruno hagués escrit alguna cosa que val la pena conservar en un altre lloc:

git show documentacio/actualitzar-notes:notes-internes.md

I si la resposta és «sí que val la pena, però no aquí», pots rescatar el contingut a un altre fitxer abans de resoldre.

Els altres conflictes d'arbre

Es resolen amb la mateixa lògica: git add si el fitxer ha d'existir amb el contingut que té al disc, git rm si no ha d'existir.

CONFLICT (add/add): Merge conflict in config.json

Les dues branques van crear el mateix fitxer amb contingut diferent. Aquí que hi ha marcadors a dins (Git tracta el buit com a base), així que es resol com un conflicte de contingut normal.

CONFLICT (rename/rename): Rename estils.css->css/estils.css in HEAD.
Rename estils.css->assets/estils.css in branca-b

Les dues branques el van moure a llocs diferents. Es decideix la ubicació bona, s'hi col·loca el fitxer, s'esborren les altres còpies i es marca tot amb git add/git rm.

  1. git rerere: no resoldre dues vegades el mateix

Un últim apunt, perquè sàpigues que existeix quan el necessitis.

rerere ve de reuse recorded resolution: «reutilitzar la resolució registrada». Si l'actives, Git memoritza com vas resoldre cada conflicte i, quan se li presenta el mateix conflicte una altra vegada, el resol sol amb la teva decisió anterior.

git config --global rerere.enabled true

Quan es repeteix un conflicte? Més vegades del que sembla: quan fusiones main a la teva branca de treball cada pocs dies, quan avortes una fusió i la repeteixes, quan refàs una branca amb rebase diverses vegades (mòdul 5), o quan treballes amb branques de llarga durada.

És una eina d'ús avançat i té els seus matisos —convé revisar el que resol pel seu compte, sobretot si la resolució anterior va ser precipitada—, així que aquí només l'esmentem. Amb saber que existeix i que s'activa amb aquella línia n'hi ha prou per ara.

Errors Habituals i Consells

Error 1: deixar marcadors al codi. Confirmar un fitxer amb <<<<<<< a dins trenca el projecte de manera espectacular. Git avisa si detecta marcadors en confirmar, però no és infal·lible. Comprova sempre abans de git add:

git diff --check
grep -rn '^<<<<<<<' .

Error 2: resoldre «triant un costat» sense pensar. L'impuls de quedar-se amb --ours per acabar de pressa destrueix la feina de l'altre costat, inclosos els canvis que s'havien fusionat bé a la resta del fitxer. Llegeix el conflicte: en la majoria dels casos la resposta correcta és quedar-se amb les dues coses, com a l'exemple d'aquesta lliçó.

Error 3: no provar el resultat. Un conflicte resolt pot compilar i tot i així estar malament: variables que queden sense fer servir, funcions duplicades, lògica que s'executa dues vegades. Executa l'aplicació o les proves abans de confirmar.

Error 4: entrar en pànic i esborrar el repositori. És més habitual del que hauria de ser. git merge --abort deixa tot exactament com estava. No hi ha cap situació de conflicte de la qual no es pugui sortir.

Error 5: confondre --ours/--theirs durant un conflicte amb -X ours/-X theirs. Els primers actuen sobre un fitxer concret, amb la fusió ja aturada. Els segons són una política global que s'aplica abans. I en un rebase, a més, els papers d'ours i theirs s'inverteixen respecte al que un esperaria (lliçó 05-01).

Consell 1: activa zdiff3 avui mateix. Veure l'ancestre comú canvia completament la qualitat de les teves resolucions:

git config --global merge.conflictStyle zdiff3

Consell 2: conflictes petits i freqüents en lloc de grans i rars. La millor tècnica per resoldre conflictes és tenir-ne menys. Branques de vida curta, integracions freqüents i portar main a la teva branca cada pocs dies converteixen un conflicte de 300 línies en cinc de tres.

Consell 3: si el conflicte és enorme, avorta i estudia. Abans de barallar-t'hi, entén per què hi és:

git merge --abort
git log --oneline main..altra-branca
git diff main...altra-branca --stat

De vegades la conclusió és que cal parlar amb l'altra persona abans de tocar res. Aquesta també és una resposta vàlida.

Consell 4: documenta la resolució al missatge del commit de fusió. Un commit de fusió amb conflictes conté decisions humanes que no són enlloc més. Explicar en tres línies què xocava i què es va decidir estalvia arqueologia a qui vingui després.

Consell 5: quan toqui decidir codi aliè, pregunta. Si el conflicte és en codi que no coneixes, resoldre'l «a ull» és una juguesca. Un missatge de trenta segons a qui el va escriure surt més barat que una fallada en producció.

Exercicis

Exercici 1: provocar i resoldre un conflicte de contingut

Munta un repositori de proves, provoca un conflicte en què les dues branques modifiquin la mateixa línia, i resol-lo combinant totes dues aportacions. Fes-ho primer amb l'estil de conflicte per defecte i després amb diff3, i explica quina informació aporta el segon.

Exercici 2: el conflicte d'esborrat davant de modificat

Provoca un conflicte modify/delete i resol-lo de les dues maneres possibles (conservant el fitxer i mantenint l'esborrat). Abans de decidir, mostra el contingut que aportava la branca que el va modificar.

Exercici 3: rescatar les tres versions

En un conflicte de contingut, sense obrir cap editor, guarda a /tmp les tres versions del fitxer (base, ours i theirs) per separat i compara la base amb cadascuna de les altres dues. Després, avorta la fusió.

Solucions

Solució 1:

mkdir /tmp/practica-conflicte && cd /tmp/practica-conflicte
git init -b main
cat > salutacio.js <<'FI'
function saluda(nom) {
  return "Hola " + nom;
}
FI
git add . && git commit -m "Afegeix la funció de salutació"

# Branca A: afegeix signes d'admiració
git switch -c branca-a
cat > salutacio.js <<'FI'
function saluda(nom) {
  return "Hola " + nom + "!";
}
FI
git commit -am "Afegeix signes d'admiració"

# Branca B: afegeix el cognom
git switch main
git switch -c branca-b
cat > salutacio.js <<'FI'
function saluda(nom, cognom) {
  return "Hola " + nom + " " + cognom;
}
FI
git commit -am "Inclou el cognom a la salutació"

# Fusionem
git switch main
git merge branca-a
git merge branca-b
Auto-merging salutacio.js
CONFLICT (content): Merge conflict in salutacio.js

Amb l'estil per defecte:

function saluda(nom) {
<<<<<<< HEAD
  return "Hola " + nom + "!";
=======
}
function saluda(nom, cognom) {
  return "Hola " + nom + " " + cognom;
>>>>>>> branca-b
}

Amb diff3:

git merge --abort
git config merge.conflictStyle diff3
git merge branca-b
<<<<<<< HEAD
function saluda(nom) {
  return "Hola " + nom + "!";
||||||| 8a1d5c3
function saluda(nom) {
  return "Hola " + nom;
=======
function saluda(nom, cognom) {
  return "Hola " + nom + " " + cognom;
>>>>>>> branca-b

Què aporta diff3: deixa clar que la base era "Hola " + nom, que la branca A només hi va afegir els signes d'admiració i que la branca B només hi va afegir el paràmetre i el cognom. Sense el bloc de la base, ho havies de deduir comparant mentalment els dos costats i era fàcil equivocar-se sobre què s'havia canviat.

Resolució que combina les dues aportacions:

cat > salutacio.js <<'FI'
function saluda(nom, cognom) {
  return "Hola " + nom + " " + cognom + "!";
}
FI
grep -c '<<<<<<<' salutacio.js
0
git add salutacio.js
git commit -m "Fusiona la salutació amb cognom i admiracions

Conflicte a saluda(): branca-a afegia els signes d'admiració i
branca-b el paràmetre cognom. Es conserven tots dos canvis."

Solució 2:

mkdir /tmp/practica-esborrat && cd /tmp/practica-esborrat
git init -b main
echo "Notes del projecte" > notes.md
echo "contingut" > altre.txt
git add . && git commit -m "Base"

# Branca que esborra
git switch -c esborrar-notes
git rm notes.md
git commit -m "Esborra les notes obsoletes"

# Branca que modifica
git switch main
git switch -c actualitzar-notes
echo "Notes del projecte - revisió 2" > notes.md
git commit -am "Actualitza les notes"

# Fusió
git switch main
git merge esborrar-notes        # fast-forward, sense problema
git merge actualitzar-notes
CONFLICT (modify/delete): notes.md deleted in HEAD and modified in
actualitzar-notes.  Version actualitzar-notes of notes.md left in tree.
git status --short
DU notes.md

Abans de decidir, veiem què aportava la branca:

git show actualitzar-notes:notes.md
Notes del projecte - revisió 2

Opció A: mantenir l'esborrat.

git rm notes.md
git commit -m "Fusiona actualitzar-notes mantenint l'esborrat de notes.md"
ls
altre.txt

Opció B: conservar el fitxer (partint de git merge --abort i repetint).

git add notes.md
git commit -m "Fusiona actualitzar-notes conservant notes.md actualitzat"
cat notes.md
Notes del projecte - revisió 2

La regla que resol tots els conflictes d'arbre: git add si el fitxer ha d'existir, git rm si no ha d'existir.

Solució 3:

Partint d'un conflicte de contingut en curs sobre salutacio.js:

git show :1:salutacio.js > /tmp/base.js      # ancestre comú
git show :2:salutacio.js > /tmp/ours.js      # la nostra branca
git show :3:salutacio.js > /tmp/theirs.js    # branca fusionada
diff /tmp/base.js /tmp/ours.js
2c2
<   return "Hola " + nom;
---
>   return "Hola " + nom + "!";
diff /tmp/base.js /tmp/theirs.js
1,2c1,2
< function saluda(nom) {
<   return "Hola " + nom;
---
> function saluda(nom, cognom) {
>   return "Hola " + nom + " " + cognom;

Els dos diffs, per separat i sense marcadors, mostren amb tota claredat què va fer cada branca respecte al punt de partida. És la tècnica que salva els conflictes grans: en lloc de llegir un fitxer barrejat, es llegeixen els dos canvis per separat.

git merge --abort
git status
On branch main
nothing to commit, working tree clean

Tot com estava.

Conclusió

Els conflictes han deixat de ser un misteri:

  • Un conflicte no és un error. Passa quan les dues branques van canviar les mateixes línies de manera diferent des de l'ancestre comú. Que dues persones toquin el mateix fitxer no n'hi ha prou: han de xocar a les mateixes línies.
  • Git escriu marcadors al fitxer: <<<<<<< obre la teva versió, ======= separa i >>>>>>> tanca l'entrant. Amb merge.conflictStyle a diff3 o zdiff3 apareix a més el bloc ||||||| amb el contingut de l'ancestre, que és la informació que de debò permet resoldre bé.
  • git status és la brúixola: la secció Unmerged paths, el tipus de conflicte (both modified, deleted by us…) i els codis curts (UU, DU, AA). Internament, l'índex guarda tres etapes del fitxer, accessibles amb git show :1:, :2: i :3:, i MERGE_HEAD és el que defineix que hi ha una fusió en curs.
  • Resoldre és deixar el fitxer com ha de quedar i marcar-lo amb git add (o git rm si no ha d'existir). No hi ha cap ordre màgica: hi ha una decisió humana, i sovint la resposta correcta és combinar les dues aportacions.
  • Les dreceres git checkout --ours/--theirs <fitxer> (o git restore --ours/--theirs) substitueixen el fitxer sencer per una de les versions. Útils per a fitxers generats; perillosos si el fitxer tenia a més canvis ben fusionats.
  • Mai no estàs atrapat: git merge --abort restaura l'estat previ del tot. --quit surt de la fusió conservant el desordre, i és per a casos avançats.
  • git mergetool obre una eina de diversos plafons amb base, ours, theirs i resultat, i marca el fitxer com a resolt en desar.
  • Els conflictes d'arbre (esborrat/modificat, afegit/afegit, reanomenaments) no porten marcadors: es resolen declarant què ha d'existir.
  • git rerere memoritza resolucions i les torna a aplicar quan el mateix conflicte reapareix.

El que ve

El projecte està en bona forma: main conté el comptador, el filtre, l'exportació a CSV, l'ordre alfabètic i el missatge de llista buida. Però el repositori de l'Ana comença a estar fet un desastre. Té branques de funcionalitats ja integrades, branques d'experiments abandonats, branques el nom de les quals ningú no recorda i una que es diu simplement prova.

A l'última lliçó del mòdul, Gestió de Branques, hi posarem ordre: llistar les branques amb informació útil (-v, --merged, --no-merged, formats a mida ordenats per data), reanomenar, esborrar amb seguretat (-d davant de -D) i entendre exactament què significa que Git es negui a esborrar una branca. Veurem també les convencions de noms —quins caràcters admet Git i quins prefixos fa servir la gent— i tancarem el mòdul amb el problema que portem tota la lliçó esquivant: tot això passa en un únic portàtil.

Dominant Git: De Principiant a Avançat

Mòdul 1: Introducció a Git

Mòdul 2: Operacions Bàsiques de Git

Mòdul 3: Branques i Fusió

Mòdul 4: Treballant amb Repositoris Remots

Mòdul 5: Operacions Avançades de Git

Mòdul 6: Eines i Tècniques de Git

Mòdul 7: Estratègies de Col·laboració i Flux de Treball

Mòdul 8: Bones Pràctiques i Consells de Git

Mòdul 9: Resolució de Problemes i Depuració

Mòdul 10: Git al Món Real

© Copyright 2026. Tots els drets reservats