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
- Per què es produeix un conflicte
- Provocant un conflicte de debò
- Els marcadors de conflicte
- L'estil
diff3izdiff3: veure també l'ancestre - Orientar-se durant el conflicte:
git status git diffdurant un conflicte: les tres versions- Resoldre i marcar com a resolt
- Dreceres:
--oursi--theirsper fitxer - Avortar:
--aborti--quit git mergetool: eines gràfiques- Conflictes especials: esborrat davant de modificat
git rerere: no resoldre dues vegades el mateix
- 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 |
| Sí | No | Guanya ours |
| No | Sí | Guanya theirs |
| Sí | Sí, igual | Guanya aquesta versió comuna |
| Sí | 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.
- 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:
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();
}[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.
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();
}[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:
I ara, el d'en Bruno:
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.
- 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:
-
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.
-
Un fitxer pot tenir diversos blocs de conflicte, cadascun amb el seu joc de marcadors. Cal resoldre'ls tots.
-
Els marcadors són text normal. No hi ha res de màgic: els pots editar, esborrar o deixar (si els deixes,
git committ'avisarà, però pots forçar-ho i ficar brossa al projecte, així que revisa sempre). -
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.
- L'estil
diff3 i zdiff3: veure també l'ancestre
diff3 i zdiff3: veure també l'ancestreGit pot mostrar tres seccions en lloc de dues, afegint-hi la de l'ancestre comú. Es controla amb l'opció merge.conflictStyle:
Avortem i repetim la fusió per veure-ho:
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'
ifal davant i no va tocar elforEach.
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:
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 zdiff3Encaixa amb el que ja vas configurar a la lliçó 01-06 i no té cap contrapartida.
- Orientar-se durant el conflicte:
git status
git statusDurant un conflicte, el repositori és en un estat especial. git status és la teva brúixola i canvia del tot:
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:
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:
É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:
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:
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.
git diff durant un conflicte: les tres versions
git diff durant un conflicte: les tres versionsgit diff a seques, durant un conflicte, mostra un format que no havies vist: el diff combinat.
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.jsTambé 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 brancaAixò é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:
- 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.jsI a continuació, obrir l'aplicació o passar les proves.
Ara es marca com a resolt:
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».
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 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ó.
* 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:
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.
- Dreceres:
--ours i --theirs per fitxer
--ours i --theirs per fitxerDe 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.jsO amb les ordres modernes que vam veure a la lliçó 03-02:
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:
Dos avisos importants:
-
Agafen el fitxer SENCER, no només la zona conflictiva. Si
app.jstenia a més canvis de l'altra branca en una altra part del fitxer que s'havien fusionat bé,--ourstambé els llença. Fes-ho servir només quan de debò vulguis una versió completa. -
No els confonguis amb
-X ours/-X theirsde 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 commitI si prefereixes començar de zero un fitxer que has espatllat editant:
Restaura el fitxer amb els marcadors de conflicte originals, tal com estava just després de la fusió fallida.
- Avortar:
--abort i --quit
--abort i --quitSi 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:
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:
--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 |
git mergetool: eines gràfiques
git mergetool: eines gràfiquesPer 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.
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 vimdiffI una opció que gairebé tothom acaba activant:
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.
- 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:
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:
- El tipus de conflicte és
modify/delete, nocontent. - 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.
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:
En tots dos casos, després:
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:
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.
Les dues branques van crear el mateix fitxer amb contingut diferent. Aquí sí 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.
git rerere: no resoldre dues vegades el mateix
git rerere: no resoldre dues vegades el mateixUn ú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.
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:
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:
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:
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-bAmb l'estil per defecte:
function saluda(nom) {
<<<<<<< HEAD
return "Hola " + nom + "!";
=======
}
function saluda(nom, cognom) {
return "Hola " + nom + " " + cognom;
>>>>>>> branca-b
}Amb diff3:
<<<<<<< HEAD
function saluda(nom) {
return "Hola " + nom + "!";
||||||| 8a1d5c3
function saluda(nom) {
return "Hola " + nom;
=======
function saluda(nom, cognom) {
return "Hola " + nom + " " + cognom;
>>>>>>> branca-bQuè 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.jsgit 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-notesCONFLICT (modify/delete): notes.md deleted in HEAD and modified in actualitzar-notes. Version actualitzar-notes of notes.md left in tree.
Abans de decidir, veiem què aportava la branca:
Opció A: mantenir l'esborrat.
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.mdLa 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 fusionada1,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.
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. Ambmerge.conflictStyleadiff3ozdiff3apareix 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 ambgit show :1:,:2:i:3:, iMERGE_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(ogit rmsi 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>(ogit 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 --abortrestaura l'estat previ del tot.--quitsurt de la fusió conservant el desordre, i és per a casos avançats. git mergetoolobre una eina de diversos plafons amb base,ours,theirsi 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 rererememoritza 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
- Què és Git?
- Instal·lant Git
- Terminologia Bàsica de Git
- El Model de Dades de Git
- Configurant Git
- Configuració Inicial
Mòdul 2: Operacions Bàsiques de Git
- Creant un Repositori
- Clonant un Repositori
- Flux de Treball Bàsic de Git
- Preparant i Confirmant Canvis
- Inspeccionant Canvis amb git diff
- Visualitzant l'Historial de Confirmacions
Mòdul 3: Branques i Fusió
- Entenent les Branques
- Creant i Canviant Branques
- Fusionant Branques
- Estratègies de Fusió
- Resolent Conflictes de Fusió
- Gestió de Branques
Mòdul 4: Treballant amb Repositoris Remots
- Entenent els Repositoris Remots
- Afegint un Repositori Remot
- Autenticació amb Repositoris Remots
- Obtenint i Baixant Canvis
- Enviant Canvis
- Rastrejant Branques
Mòdul 5: Operacions Avançades de Git
- Rebase
- Rebase Interactiu
- Cherry-Picking de Confirmacions
- Desant Canvis Temporals
- Etiquetant Confirmacions
- Revertint Confirmacions
Mòdul 6: Eines i Tècniques de Git
- Usant Git Hooks
- Git Bisect
- Git Blame
- Git Log i Àlies
- Submòduls de Git
- Múltiples Còpies de Treball amb git worktree
Mòdul 7: Estratègies de Col·laboració i Flux de Treball
- Forks i Pull Requests
- Revisions de Codi amb Git
- Flux de Treball Git Flow
- GitHub Flow
- Trunk Based Development
- Integració Contínua amb Git
Mòdul 8: Bones Pràctiques i Consells de Git
- Escrivint Bons Missatges de Confirmació
- Mantenint un Historial Net
- Ignorant Fitxers amb .gitignore
- Atributs de Fitxer amb .gitattributes
- Bones Pràctiques de Seguretat
- Consells de Rendiment
Mòdul 9: Resolució de Problemes i Depuració
- Problemes Habituals de Git
- Desfent Canvis
- Resolent Divergències amb el Remot
- Recuperant Confirmacions Perdudes
- Tractant amb Repositoris Corruptes
- Tècniques Avançades de Depuració
