A la lliçó anterior vam aprendre a decidir amb precisió què entra a cada confirmació. Però hi ha una pregunta prèvia que git status no respon: què he canviat exactament?

git status et diu quins fitxers han canviat i en quina zona són. No et diu quines línies. I aquesta diferència és enorme: confirmar sense haver llegit els teus propis canvis és la manera més habitual de ficar a l'historial un console.log de depuració, una contrasenya de proves, un bloc de codi comentat o un canvi que et pensaves haver desfet.

git diff és l'eina que respon aquesta pregunta. En aquesta lliçó veurem les seves tres formes fonamentals —i per què donen resultats diferents segons quines zones comparen—, aprendrem a llegir el format unified diff línia a línia (un format que apareix a Git, a les revisions de codi, als pedaços i a mitja informàtica), i recorrerem les opcions que fan llegible una sortida difícil.

En acabar hauries d'haver adquirit un hàbit: git diff --staged just abans de cada git commit.

Contingut

  1. Les tres formes de git diff i què compara cadascuna
  2. git diff: directori de treball contra àrea de preparació
  3. git diff --staged: àrea de preparació contra l'últim commit
  4. git diff HEAD: tot el que ha canviat
  5. Llegir el format unified diff pas a pas
  6. Comparar confirmacions concretes
  7. Limitar la comparació a fitxers o rutes
  8. Opcions que fan llegible la sortida
  9. git difftool: comparar amb una eina visual
  10. L'hàbit de revisar abans de confirmar

  1. Les tres formes de git diff i què compara cadascuna

Tot l'aparent misteri de git diff es dissol amb una idea: sempre compara dues de les tres zones, i la forma que facis servir determina quines.

graph LR
    WT["DIRECTORI<br/>DE TREBALL"]
    IDX["ÀREA DE<br/>PREPARACIÓ"]
    REPO["ÚLTIM COMMIT<br/>(HEAD)"]

    WT ---|"git diff"| IDX
    IDX ---|"git diff --staged"| REPO
    WT -.-|"git diff HEAD"| REPO

En forma de taula:

Ordre Compara Respon la pregunta
git diff Directori de treball ↔ Àrea de preparació Què he canviat i encara no he preparat?
git diff --staged Àrea de preparació ↔ HEAD Què entrarà exactament al pròxim commit?
git diff --cached Idèntic a --staged (Sinònim antic, continua funcionant)
git diff HEAD Directori de treball ↔ HEAD Què ha canviat en total des de l'últim commit?
git diff <sha1> <sha2> Dues confirmacions Què va canviar entre aquestes dues versions?

Dues conseqüències que convé interioritzar des del principi:

  • git diff tot sol no mostra els canvis preparats. Si ho prepares tot amb git add -A i després executes git diff, la sortida estarà buida. No és que no hagis canviat res: és que no hi ha diferència entre el teu disc i l'àrea de preparació. Aquest és el desconcert número u amb git diff.

  • git diff HEAD és la suma de les altres dues. Preparat o no, si difereix de l'últim commit, hi apareix.

  • Cap de les tres mostra fitxers sense seguiment. Un fitxer nou que no has afegit mai no té «versió anterior» amb la qual comparar-lo, així que git diff l'ignora completament. Per a això hi ha git status.

  1. git diff: directori de treball contra àrea de preparació

Tornem al repositori de l'Ana. Acaba de retocar estils.css i encara no ha preparat res:

cd ~/projectes/gestor-tasques
git status -s
 M estils.css
git diff
diff --git a/estils.css b/estils.css
index 2c9d4e6..8f1a3b7 100644
--- a/estils.css
+++ b/estils.css
@@ -12,4 +12,5 @@ body {
 #llista li {
   padding: 0.5rem 0;
-  border-bottom: 1px solid #ddd;
+  border-bottom: 1px solid #e5e7eb;
+  cursor: pointer;
 }

En tres segons sap què ha fet: ha suavitzat el color de la vora i ha afegit el cursor de punter. Cap sorpresa, cap rastre de depuració.

Ara prepara el canvi i repeteix:

git add estils.css
git diff
(sortida buida)

Exacte: el disc i l'àrea de preparació coincideixen, així que no hi ha res a mostrar. Per veure aquell canvi cal preguntar per l'altra comparació.

  1. git diff --staged: àrea de preparació contra l'últim commit

Aquesta és la forma més important de totes, perquè mostra literalment el que confirmaràs:

git diff --staged
diff --git a/estils.css b/estils.css
index 2c9d4e6..8f1a3b7 100644
--- a/estils.css
+++ b/estils.css
@@ -12,4 +12,5 @@ body {
 #llista li {
   padding: 0.5rem 0;
-  border-bottom: 1px solid #ddd;
+  border-bottom: 1px solid #e5e7eb;
+  cursor: pointer;
 }

--cached és un sinònim exacte i més antic. --staged es va afegir a Git 1.6 perquè resultava més comprensible; fes servir el que prefereixis, encara que --staged és més clar.

El cas MM vist amb diff

Aquí és on git diff demostra el seu valor. L'Ana continua treballant i afegeix una línia més a estils.css després d'haver preparat:

#llista li {
  padding: 0.5rem 0;
  border-bottom: 1px solid #e5e7eb;
  cursor: pointer;
  transition: opacity 0.2s;
}
git status -s
# → MM estils.css

Ara les tres formes donen tres respostes diferents:

git diff                # el que NO hi entrarà
@@ -14,3 +14,4 @@
   border-bottom: 1px solid #e5e7eb;
   cursor: pointer;
+  transition: opacity 0.2s;
 }
git diff --staged       # el que SÍ hi entrarà
@@ -12,4 +12,5 @@
 #llista li {
   padding: 0.5rem 0;
-  border-bottom: 1px solid #ddd;
+  border-bottom: 1px solid #e5e7eb;
+  cursor: pointer;
 }
git diff HEAD           # el total
@@ -12,4 +12,6 @@
 #llista li {
   padding: 0.5rem 0;
-  border-bottom: 1px solid #ddd;
+  border-bottom: 1px solid #e5e7eb;
+  cursor: pointer;
+  transition: opacity 0.2s;
 }

Tres preguntes, tres respostes precises. El MM de git status t'avisa que hi ha alguna cosa estranya; git diff et diu exactament què.

  1. git diff HEAD: tot el que ha canviat

git diff HEAD

Compara el teu directori de treball amb l'última confirmació, ignorant l'àrea de preparació. És la resposta a «què he tocat des de l'últim commit?», que és la pregunta que un es fa quan torna de dinar o quan reprèn la feina al matí.

HEAD no té res d'especial aquí: és simplement una referència a una confirmació, i git diff n'accepta qualsevol. Aquestes variants són igual de vàlides:

git diff HEAD~1        # contra la confirmació anterior a l'última
git diff HEAD~5        # contra la de fa cinc confirmacions
git diff 4e7f2a9       # contra una confirmació concreta

La notació HEAD~n i la resta de formes de referir-se a una confirmació les veurem en detall a Visualitzant l'Historial de Confirmacions.

  1. Llegir el format unified diff pas a pas

El format que produeix git diff s'anomena unified diff i és un estàndard de la informàtica des dels anys vuitanta. El veuràs a Git, a les revisions de codi de GitHub i GitLab, als pedaços de correu i a la sortida de desenes d'eines. Saber llegir-lo és una habilitat transferible.

Dissequem una sortida completa, línia per línia. L'Ana ha modificat app.js:

diff --git a/app.js b/app.js
index 7b2e8f1..3c9d4a2 100644
--- a/app.js
+++ b/app.js
@@ -28,8 +28,12 @@ function pintaLlista() {
   llista.innerHTML = '';
   for (const tasca of tasques) {
     const li = document.createElement('li');
     li.textContent = tasca.text;
-    li.className = 'tasca';
+    li.className = tasca.feta ? 'tasca feta' : 'tasca';
+    li.addEventListener('click', function () {
+      tasca.feta = !tasca.feta;
+      pintaLlista();
+    });
     llista.appendChild(li);
   }
 }

Línia 1: la capçalera

diff --git a/app.js b/app.js

Indica que comença la comparació d'un fitxer. a/ és la versió antiga i b/ la nova; són prefixos convencionals, no directoris reals. En un reanomenament hi veuries noms diferents a cada costat.

Si el diff inclou diversos fitxers, veuràs una d'aquestes línies per cadascun: és el separador que et diu on comença cada bloc.

Línia 2: els identificadors d'objecte

index 7b2e8f1..3c9d4a2 100644

Els hashos abreujats dels blobs antic i nou, i el mode de fitxer (100644 = fitxer normal). Això connecta directament amb El Model de Dades de Git: el diff no és una entitat desada a Git, sinó una cosa que Git calcula al vol comparant dos blobs. Si el mode canviés (per exemple, en fer un fitxer executable), hi veuries old mode i new mode en línies a part.

Línies 3 i 4: els marcadors de fitxer

--- a/app.js
+++ b/app.js

--- marca la versió antiga i +++ la nova. Per això, al cos del diff, - significa «era a la versió antiga» i + significa «és a la nova».

Casos especials que aclareixen molt:

--- /dev/null          ← el fitxer és nou: abans no existia
+++ b/nou.js
--- a/antic.js
+++ /dev/null          ← el fitxer s'ha esborrat: ja no existeix

Línia 5: la capçalera de fragment (@@)

@@ -28,8 +28,12 @@ function pintaLlista() {

Aquesta és la línia que més costa al principi i la que conté més informació. Es llegeix així:

@@ -<línia inicial antiga>,<nre. de línies antigues> +<línia inicial nova>,<nre. de línies noves> @@ <context>

Aplicat a l'exemple:

Part Valor Significat
-28,8 antiga A la versió antiga, aquest bloc comença a la línia 28 i abasta 8 línies
+28,12 nova A la versió nova, comença a la línia 28 i abasta 12 línies
function pintaLlista() { context La funció o secció on és el canvi

Dels números se'n dedueix tot: 8 línies s'han convertit en 12, per tant el bloc ha crescut en 4. I efectivament, si comptes el cos: 7 línies de context, 1 d'eliminada i 5 d'afegides. Així es verifica qualsevol capçalera @@:

  • Línies antigues = context + eliminades → 7 + 1 = 8.
  • Línies noves = context + afegides → 7 + 5 = 12.

El text del final no és una línia del fitxer: Git l'extreu buscant cap amunt l'última línia que sembla una declaració de funció o secció. Serveix per orientar-te quan el diff és llarg. Es pot afinar per llenguatge amb .gitattributes, que veurem a Atributs de Fitxer amb .gitattributes.

Quan el nombre de línies és 1, s'omet: @@ -12 +12 @@ significa «una línia a cada costat».

El cos: les línies del fragment

Cada línia del cos comença amb un caràcter que n'indica la naturalesa:

Primer caràcter Significa
(espai) Context: la línia existeix igual a totes dues versions
- Eliminada: era a la versió antiga, ja no hi és
+ Afegida: abans no hi era, ara sí
\ Nota especial, gairebé sempre \ No newline at end of file

Aplicat al nostre exemple:

   llista.innerHTML = '';                           ← context
   for (const tasca of tasques) {                   ← context
     const li = document.createElement('li');       ← context
     li.textContent = tasca.text;                   ← context
-    li.className = 'tasca';                        ← ELIMINADA
+    li.className = tasca.feta ? 'tasca feta' : 'tasca';   ← AFEGIDA
+    li.addEventListener('click', function () {     ← AFEGIDA
+      tasca.feta = !tasca.feta;                    ← AFEGIDA
+      pintaLlista();                               ← AFEGIDA
+    });                                            ← AFEGIDA
     llista.appendChild(li);                        ← context

Dues observacions importants:

  1. Git no entén de «línies modificades». Una modificació es representa sempre com una eliminació seguida d'una addició. Per això el primer parell de línies apareix com a - i + encara que conceptualment sigui «la mateixa línia, canviada».
  2. Les línies de context hi són per alguna cosa. Per defecte Git mostra 3 línies de context a cada costat del canvi, perquè el puguis situar. S'ajusta amb -U<n>:
git diff -U0        # sense context: només les línies canviades
git diff -U10       # deu línies de context a cada costat

El detall de \ No newline at end of file

-const versio = 1;
\ No newline at end of file
+const versio = 2;

Significa que aquell fitxer no acaba amb un salt de línia. És una convenció POSIX que moltes eines donen per feta, i la seva absència genera diffs sorollosos: si algú afegeix el salt final, totes les línies properes poden aparèixer com a canviades. Configurar l'editor perquè afegeixi sempre el salt final evita aquest soroll.

Exercici mental ràpid

Abans de continuar, llegeix aquest fragment i respon: quantes línies tenia el bloc abans i quantes en té ara?

@@ -45,5 +45,3 @@ function actualitzaComptador() {
   const pendents = tasques.filter(t => !t.feta).length;
-  console.log('DEBUG pendents:', pendents);
-  console.log('DEBUG total:', tasques.length);
   document.querySelector('#comptador').textContent = pendents;
 }

Cinc abans, tres després: s'han eliminat dues línies de depuració. Aquest és exactament el tipus de troballa que justifica revisar el diff abans de confirmar.

  1. Comparar confirmacions concretes

git diff també compara dos punts qualssevol de l'historial:

git diff 1a4c8d6 4e7f2a9

Mostra tot el que va canviar entre aquelles dues confirmacions, agregat en un sol diff. Tant se val quantes confirmacions hi hagi pel mig: compara els dos estats finals.

Formes equivalents i molt utilitzades:

# Què va introduir l'última confirmació
git diff HEAD~1 HEAD

# Els canvis de les últimes tres confirmacions, junts
git diff HEAD~3 HEAD

# Des d'una confirmació fins a l'estat actual del disc
git diff 1a4c8d6

Aquesta última mereix una nota: quan li dones un sol argument, git diff compara aquella confirmació amb el teu directori de treball, no amb HEAD. És la mateixa lògica que git diff HEAD.

Veure què va introduir una confirmació concreta

Per inspeccionar una confirmació aïllada, l'idiomàtic és:

git show 4e7f2a9

que mostra les metadades (autor, data, missatge) i el diff. És el tema de la lliçó següent.

Una nota sobre branques

git diff accepta noms de branca exactament igual que hashos:

git diff main desenvolupament

I existeix una notació de tres punts, git diff main...desenvolupament, que compara des del punt en què totes dues es van separar. Com que en aquest mòdul treballem sempre sobre main, ho deixem apuntat: es desenvolupa al mòdul 3.

  1. Limitar la comparació a fitxers o rutes

En un canvi que toca quinze fitxers, veure el diff complet és inabastable. S'acota amb -- seguit de rutes:

# Un fitxer concret
git diff -- app.js

# Diversos
git diff -- app.js estils.css

# Un directori sencer
git diff -- src/

# Un patró
git diff -- "*.css"

El doble guionet -- separa les opcions de les rutes. És opcional quan no hi ha ambigüitat, així que git diff app.js funciona igual. Es torna obligatori quan un nom de fitxer es podria confondre amb un nom de branca o de confirmació:

git diff -- main       # el FITXER anomenat main
git diff main          # la BRANCA anomenada main

Sense el --, Git intentaria interpretar main com una referència i, si la branca existís, obtindries una cosa completament diferent del que demanaves. Acostumar-se a posar -- abans de les rutes és un bon hàbit.

Es combina amb tot l'anterior:

git diff --staged -- app.js
git diff HEAD~3 HEAD -- estils.css

  1. Opcions que fan llegible la sortida

--stat: el resum

git diff --stat HEAD~3 HEAD
 app.js     | 27 +++++++++++++++++++++-----
 estils.css |  9 ++++++++-
 index.html |  3 ++-
 3 files changed, 33 insertions(+), 6 deletions(-)

Cada línia mostra el fitxer, el nombre total de línies afectades i una barra proporcional de + (addicions) i - (eliminacions). És la primera vista que convé mirar davant d'un canvi gran: et diu on has de mirar després.

Variants:

git diff --shortstat HEAD~3 HEAD
# → 3 files changed, 33 insertions(+), 6 deletions(-)

git diff --numstat HEAD~3 HEAD
# → 22	5	app.js
# → 8	1	estils.css
# → 2	1	index.html

--numstat dona addicions, eliminacions i nom separats per tabuladors: ideal per processar amb scripts.

--name-only i --name-status: només els fitxers

git diff --name-only HEAD~3 HEAD
app.js
estils.css
index.html
git diff --name-status HEAD~3 HEAD
M	app.js
M	estils.css
A	favicon.svg
D	antic.js
R100	README.md	LLEGEIXME.md

--name-status afegeix una lletra per fitxer: M modificat, A afegit, D esborrat, R reanomenat (el número indica el percentatge de similitud: R100 és un reanomenament sense canvis de contingut).

Aquestes opcions són molt pràctiques encadenades amb altres ordres:

# Comptar quants fitxers ha tocat l'última confirmació
git diff --name-only HEAD~1 HEAD | wc -l

--word-diff: comparar per paraules

En un fitxer de text (documentació, README, contingut HTML), canviar una paraula fa que tota la línia aparegui com a eliminada i afegida. És il·legible:

-El gestor de tasques permet afegir i llistar tasques pendents de l'equip.
+El gestor de tasques permet afegir, llistar i esborrar tasques pendents de l'equip.

Amb --word-diff el canvi real salta a la vista:

git diff --word-diff -- README.md
El gestor de tasques permet [-afegir i llistar-]{+afegir, llistar i esborrar+} tasques pendents de l'equip.

El que s'ha eliminat va entre [- -] i el que s'ha afegit entre {+ +}. Amb color al terminal encara és més clar.

Variants:

git diff --word-diff=color      # només color, sense marcadors
git diff --color-words          # equivalent abreujat

És imprescindible per revisar textos i molt útil en CSS i HTML.

-w: ignorar espais en blanc

Un reformatatge automàtic, un canvi d'indentació de tabuladors a espais o un ajust de l'editor poden generar un diff de dues-centes línies on el canvi real és de dues. La família d'opcions d'espais en blanc ho resol:

Opció Efecte
-w, --ignore-all-space Ignora tots els espais, siguin on siguin
-b, --ignore-space-change Ignora canvis en la quantitat d'espais, no la seva aparició o desaparició
--ignore-space-at-eol Ignora espais al final de línia
--ignore-blank-lines Ignora línies en blanc afegides o eliminades
git diff -w

Un avís: -w serveix per llegir, no per decidir. Si el fitxer és sensible a la indentació (Python, YAML, Makefile), amagar els canvis d'espaiat pot ocultar-te un error real. Fes-lo servir per localitzar el canvi de fons i després torna al diff complet.

Altres opcions útils

# Detectar codi mogut i mostrar-lo amb un altre color
git diff --color-moved

# Diferències entre caràcters, no paraules
git diff --word-diff-regex=.

# Forçar el color encara que la sortida vagi a un fitxer o a una canonada
git diff --color=always

# Mostrar també els fitxers binaris com a diferència binària
git diff --binary

# Comparació de més qualitat (algoritme patience)
git diff --patience

--color-moved és una joia poc coneguda: quan refactoritzes movent un bloc d'un lloc a un altre, distingeix visualment «això s'ha mogut» de «això és nou».

  1. git difftool: comparar amb una eina visual

Per a diffs grans o per a gent que se'n surt millor amb una vista de dues columnes, Git pot delegar en una eina externa:

git difftool
git difftool --staged
git difftool HEAD~1 HEAD -- app.js

Accepta exactament els mateixos arguments que git diff.

Veure quines eines tens disponibles

git difftool --tool-help
'git difftool --tool=<tool>' may be set to one of the following:
		vimdiff
		vimdiff2
		nvimdiff

The following tools are valid, but not currently available:
		araxis
		bc
		kdiff3
		meld
		opendiff
		vscode
		...

Configurar-lo

Per a Visual Studio Code, que és el que fa servir l'Ana:

git config --global diff.tool vscode
git config --global difftool.vscode.cmd 'code --wait --diff "$LOCAL" "$REMOTE"'

Per a Meld (multiplataforma, gratuït i molt clar):

git config --global diff.tool meld

Per a l'opendiff de macOS, que li va bé a en Bruno:

git config --global diff.tool opendiff

I un ajust que gairebé tothom acaba posant:

git config --global difftool.prompt false

Sense ell, Git pregunta abans d'obrir cada fitxer, cosa que és esgotadora quan el canvi n'afecta deu.

Les variables $LOCAL i $REMOTE que apareixen a la configuració són fitxers temporals que Git crea amb cada versió i passa a l'eina. Tot això es desa al ~/.gitconfig amb el mecanisme que vam veure a Configurant Git:

[diff]
	tool = vscode
[difftool]
	prompt = false
[difftool "vscode"]
	cmd = code --wait --diff "$LOCAL" "$REMOTE"

Terminal o eina visual?

git diff al terminal git difftool
Velocitat Instantani Obre una finestra per fitxer
Canvis petits Perfecte Excessiu
Refactoritzacions grans Difícil de seguir Molt més clar
Ús en scripts No
Funciona per SSH sense escriptori No

La recomanació pràctica: fes servir git diff com a eina per defecte —és més ràpida i sempre hi és— i reserva difftool per als canvis grans.

Menció a part: existeix també git mergetool, l'equivalent per resoldre conflictes de fusió. Es configura de manera anàloga i el veurem a Resolent Conflictes de Fusió.

  1. L'hàbit de revisar abans de confirmar

Tot l'anterior es condensa en una rutina de trenta segons que convé automatitzar mentalment:

git status -s              # quins fitxers hi ha en joc?
git diff                   # què em queda sense preparar?
git add -p                 # preparar amb criteri
git diff --staged          # és EXACTAMENT això el que vull confirmar?
git commit -m "..."

El pas realment valuós és el penúltim. El que detecta habitualment:

  • Restes de depuració: console.log, print, punts d'interrupció.
  • Credencials o URL de proves.
  • Blocs de codi comentat que anaves a esborrar.
  • Canvis accidentals per autoformatat de l'editor en fitxers que no tocaves.
  • Fitxers que s'han colat en un git add -A apressat.
  • Canvis que et pensaves haver fet i no hi eren.

Si prefereixes no executar una ordre a part, l'opció -v de git commit inclou el diff a la plantilla del missatge:

git commit -v

I per deixar-ho activat sempre:

git config --global commit.verbose true

És probablement l'ajust amb millor relació entre esforç i benefici de tot Git.

Errors Habituals i Consells

  • Executar git diff després de git add i creure que no hi ha canvis. La sortida buida significa «el disc coincideix amb l'àrea de preparació». El que busques és git diff --staged.
  • Esperar veure fitxers nous a git diff. Un fitxer sense seguiment no té versió anterior, així que no hi apareix. Per a això hi ha git status. Si el vols veure, prepara'l abans (git add -N fitxer el registra com a buit i fa que els seus continguts surtin al diff).
  • Llegir a l'inrevés la capçalera @@. El primer número és de la versió antiga i el segon de la nova. Confondre'ls porta a interpretar malament el sentit del canvi.
  • Pensar que Git desa diffs. No els desa: desa instantànies i calcula el diff quan l'hi demanes, com vam veure al model de dades. Per això pots comparar dues confirmacions qualssevol, per llunyanes que siguin.
  • Abusar de -w. En llenguatges sensibles a la indentació, ignorar espais pot ocultar-te l'error que busques. És una eina de lectura, no de revisió final.
  • No fer servir -- abans d'una ruta ambigua. Si tens un fitxer que es diu igual que una branca, git diff main no farà el que et penses.
  • Revisar un canvi de 40 fitxers amb el diff complet. Comença per --stat per veure el mapa, i baixa al detall només on importi.
  • Consell: defineix àlies per al que facis servir més: git config --global alias.d diff i git config --global alias.ds "diff --staged".
  • Consell: si la sortida de git diff se't queda «atrapada» al paginador, recorda que es recorre amb les fletxes o la barra espaiadora i se'n surt amb q. Per desactivar-lo puntualment, git --no-pager diff.
  • Consell: git diff --stat just després de git add -A és la manera més ràpida de detectar que has afegit alguna cosa que no volies.

Exercicis

Exercici 1: Les tres zones, tres diffs diferents

Munta aquest escenari i respon raonadament abans d'executar cada ordre:

mkdir -p ~/practica/diffs && cd ~/practica/diffs
git init
cat > app.js <<'FI'
const tasques = [];

function afegeixTasca(text) {
  tasques.push(text);
}
FI
git add app.js && git commit -m "Versió inicial"

# Canvi 1: es prepara
sed -i 's/tasques.push(text);/tasques.push({ text: text, feta: false });/' app.js
git add app.js

# Canvi 2: NO es prepara
echo "" >> app.js
echo "function comptaTasques() { return tasques.length; }" >> app.js
  1. Què mostrarà git status -s?
  2. Què mostrarà git diff? Quantes línies + tindrà?
  3. Què mostrarà git diff --staged?
  4. Què mostrarà git diff HEAD?
  5. Si confirmes ara sense cap més git add, què contindrà el fitxer a l'historial?
  6. Comprova les teves respostes executant-ho tot.

Exercici 2: Llegir un diff sense executar-lo

Interpreta aquesta sortida i respon les preguntes:

diff --git a/estils.css b/estils.css
index a1b2c3d..d4e5f6a 100644
--- a/estils.css
+++ b/estils.css
@@ -8,10 +8,8 @@ body {
 h1 {
   font-size: 1.8rem;
-  color: #333;
-  margin-bottom: 1rem;
+  color: #1f2937;
 }

-.ocult { display: none; }
 #comptador {
   color: #6b7280;
 }
diff --git a/config.js b/config.js
new file mode 100644
index 0000000..7a8b9c0
--- /dev/null
+++ b/config.js
@@ -0,0 +1,4 @@
+const API_URL = 'https://api.exemple.cat';
+const TEMPS_ESPERA = 5000;
+const CLAU_DEBUG = 'test-1234';
+export { API_URL, TEMPS_ESPERA, CLAU_DEBUG };
  1. Quants fitxers afecta aquest canvi i què li passa a cadascun?
  2. A estils.css, quantes línies s'han eliminat i quantes se n'han afegit? Com canvia el nombre total de línies del bloc?
  3. Què significa exactament --- /dev/null al segon fitxer?
  4. Què significa index 0000000..7a8b9c0?
  5. Hi ha alguna cosa en aquest diff que no hauries de confirmar? Explica per què i què faries.
  6. Escriu l'ordre que mostraria només el resum per fitxers d'aquest canvi.

Exercici 3: Trobar el canvi real entre el soroll

Simula la situació d'un fitxer reindentat alhora que se'n canvia una línia de lògica:

mkdir -p ~/practica/soroll && cd ~/practica/soroll
git init
cat > app.js <<'FI'
function calculaTotal(tasques) {
    let total = 0;
    for (const tasca of tasques) {
        if (!tasca.feta) {
            total = total + 1;
        }
    }
    return total;
}
FI
git add app.js && git commit -m "Versió inicial"

# Reindentar de 4 a 2 espais I canviar la condició
cat > app.js <<'FI'
function calculaTotal(tasques) {
  let total = 0;
  for (const tasca of tasques) {
    if (!tasca.feta && !tasca.arxivada) {
      total = total + 1;
    }
  }
  return total;
}
FI
  1. Executa git diff i compta quantes línies apareixen com a canviades.
  2. Fes servir l'opció adequada per veure només el canvi de lògica.
  3. Explica per què el resultat de les dues ordres és tan diferent.
  4. Què hauria estat millor fer des del principi perquè l'historial fos llegible? Escriu la seqüència d'ordres correcta.
  5. Fes servir --word-diff sobre el mateix canvi i comenta si aporta res en aquest cas.

Solucions

Solució a l'Exercici 1

1. git status -s:

MM app.js

Hi ha una versió preparada (canvi 1) i el disc en difereix (canvi 2).

2. git diff — el que no està preparat. Mostra únicament el canvi 2:

@@ -3,3 +3,5 @@ const tasques = [];
 function afegeixTasca(text) {
   tasques.push({ text: text, feta: false });
 }
+
+function comptaTasques() { return tasques.length; }

Dues línies +: la línia en blanc i la funció. La línia tasques.push({...}) apareix com a context, no com a afegida, perquè a l'àrea de preparació ja és així.

3. git diff --staged — el que hi entrarà. Només el canvi 1:

@@ -1,5 +1,5 @@
 const tasques = [];

 function afegeixTasca(text) {
-  tasques.push(text);
+  tasques.push({ text: text, feta: false });
 }

4. git diff HEAD — el total, tots dos canvis junts:

@@ -1,5 +1,7 @@
 const tasques = [];

 function afegeixTasca(text) {
-  tasques.push(text);
+  tasques.push({ text: text, feta: false });
 }
+
+function comptaTasques() { return tasques.length; }

5. En confirmar sense cap més git add hi entraria només el canvi 1. El fitxer a l'historial tindria el push amb l'objecte, però no la funció comptaTasques, que continuaria pendent al directori de treball.

Comprovació:

git commit -m "Desa l'estat de cada tasca"
git show HEAD:app.js
const tasques = [];

function afegeixTasca(text) {
  tasques.push({ text: text, feta: false });
}
git status -s
# →  M app.js      ← el canvi 2 continua allà, sense confirmar

Solució a l'Exercici 2

1. Dos fitxers:

  • estils.css: modificat (existeix el parell --- a/ +++ b/ amb el mateix nom i mode 100644).
  • config.js: fitxer nou, com indiquen new file mode 100644 i --- /dev/null.

2. A estils.css: 3 línies eliminades (color: #333;, margin-bottom: 1rem; i .ocult { display: none; }) i 1 d'afegida (color: #1f2937;).

El compte encaixa amb la capçalera @@ -8,10 +8,8 @@: el bloc passa de 10 a 8 línies, és a dir, en perd 2 (−3 +1 = −2). I es pot verificar comptant a mà les línies del fragment:

  • Versió antiga = línies de context + línies - → 7 de context + 3 d'eliminades = 10.
  • Versió nova = línies de context + línies + → 7 de context + 1 d'afegida = 8.

Aquest exercici de comprovació és la millor manera d'assegurar-te que has entès la capçalera @@: els dos números sempre han de quadrar amb el que veus al cos.

3. --- /dev/null indica que la versió antiga del fitxer no existeix: és un fitxer nou. /dev/null és el «dispositiu nul» dels sistemes Unix, i aquí funciona com «el no-res». El cas simètric, +++ /dev/null, indicaria un esborrat.

4. index 0000000..7a8b9c0 són els hashos dels blobs antic i nou. El de l'esquerra és tot zeros perquè no hi ha blob antic: coherent amb el fet que el fitxer sigui nou.

5. Sí, hi ha un problema: CLAU_DEBUG = 'test-1234' a config.js. Encara que sigui una clau de proves, és un secret al codi, i confirmar-lo el deixa a l'historial de manera permanent per a qualsevol que cloni el repositori. Encara que avui sigui inofensiva, estableix un mal precedent i és exactament el mecanisme pel qual acaben filtrant-se claus reals.

Què faria:

# Treure la constant del fitxer i portar-la a una variable d'entorn
# o a un fitxer de configuració local ignorat.
# Després, treure config.js de l'àrea de preparació:
git restore --staged config.js

# Editar-lo, i tornar a preparar només la part legítima:
git add -p config.js

Si config.js no s'hagués de versionar en absolut:

git restore --staged config.js
echo "config.js" >> .gitignore
git add .gitignore

6. El resum per fitxers:

git diff --stat
 config.js  | 4 ++++
 estils.css | 4 +---
 2 files changed, 5 insertions(+), 3 deletions(-)

Solució a l'Exercici 3

1. El diff complet:

git diff
@@ -1,9 +1,9 @@
 function calculaTotal(tasques) {
-    let total = 0;
-    for (const tasca of tasques) {
-        if (!tasca.feta) {
-            total = total + 1;
-        }
-    }
-    return total;
+  let total = 0;
+  for (const tasca of tasques) {
+    if (!tasca.feta && !tasca.arxivada) {
+      total = total + 1;
+    }
+  }
+  return total;
 }

Catorze línies apareixen com a canviades (7 d'eliminades i 7 d'afegides), quan el canvi real de comportament és un de sol.

2. Veure només la lògica:

git diff -w
@@ -1,7 +1,7 @@
 function calculaTotal(tasques) {
   let total = 0;
   for (const tasca of tasques) {
-        if (!tasca.feta) {
+    if (!tasca.feta && !tasca.arxivada) {
     total = total + 1;
     }
   }

Ara només hi ha un parell -/+: la condició. La indentació de les altres línies s'ignora, així que el canvi real apareix aïllat.

3. Per què la diferència és tan gran. Git compara línies completes, caràcter a caràcter. Canviar quatre espais per dos al començament d'una línia la converteix, a ulls de Git, en una línia diferent: l'antiga s'elimina i la nova s'afegeix. Com que el reformatatge afecta les set línies del cos de la funció, les set apareixen canviades i el canvi de lògica es perd entre elles. -w diu a Git que normalitzi els espais abans de comparar, amb la qual cosa només queda la diferència real.

4. El correcte des del principi: dues confirmacions separades. Barrejar reformatatge amb canvis funcionals és una mala pràctica ben coneguda, perquè fa impossible revisar el canvi i arruïna el git blame de tot el bloc.

# Confirmació 1: NOMÉS el reformatatge
# (reindentar el fitxer, sense tocar la lògica)
git add app.js
git commit -m "Reindenta calculaTotal a 2 espais"

# Confirmació 2: NOMÉS el canvi de comportament
# (afegir la condició !tasca.arxivada)
git add app.js
git commit -m "Exclou les tasques arxivades del total"

El diff de la segona confirmació és de dues línies i es revisa en cinc segons. A més, si demà cal desfer el canvi de lògica, es desfà sense arrossegar el reformatatge.

Si els dos canvis ja estan barrejats al disc, git add -p amb l'opció e permet separar-los, encara que en aquest cas concret —on cada línia conté tots dos canvis alhora— seria més pràctic refer la feina en dos passos.

5. Amb --word-diff:

git diff --word-diff
@@ -1,9 +1,9 @@
 function calculaTotal(tasques) {
   let total = 0;
   for (const tasca of tasques) {
     if (!tasca.feta[- -]{+ && !tasca.arxivada +}) {

Aporta força: en comparar per paraules en lloc de per línies, la reindentació deixa de generar soroll gairebé del tot i el canvi de la condició queda assenyalat amb precisió quirúrgica. En canvis de text i en casos com aquest, --word-diff i -w es complementen bé:

git diff -w --word-diff

Conclusió

Ja saps llegir els canvis abans que entrin a la història del projecte. Recapitulant:

  • git diff compara sempre dues zones, i la forma que facis servir decideix quines: git diff (disc ↔ preparació), git diff --staged (preparació ↔ HEAD) i git diff HEAD (disc ↔ HEAD, és a dir, el total).
  • Una sortida buida de git diff no significa «no he canviat res»: gairebé sempre significa que ja ho has preparat tot.
  • Cap git diff mostra fitxers sense seguiment. Per a això hi ha git status.
  • El format unified diff té una estructura fixa: capçalera diff --git, hashos de blob, marcadors ---/+++, capçaleres de fragment @@ -a,b +c,d @@ amb el seu context, i línies de context, eliminades (-) i afegides (+). Git no representa «línies modificades»: una modificació és una eliminació més una addició.
  • Es poden comparar dues confirmacions qualssevol (git diff <sha1> <sha2>) i acotar per ruta amb -- <ruta>, fent servir -- sempre que el nom pugui ser ambigu.
  • Les opcions canvien la llegibilitat radicalment: --stat per al mapa general, --name-only/--name-status per a la llista, --word-diff per a textos i -w per separar el canvi real del soroll d'espaiat.
  • git difftool delega en una eina visual i accepta els mateixos arguments; compensa en canvis grans.
  • L'hàbit que importa: git diff --staged abans de cada git commit, o directament commit.verbose = true.

Amb git status saps on ets, amb git diff saps què has canviat i amb git add/git commit decideixes què queda registrat. Falta l'última peça del cicle bàsic: mirar enrere.

Un historial només val el que val la capacitat de consultar-lo. Després de dues-centes confirmacions, com trobes quan es va introduir una funció? Qui va tocar estils.css la setmana passada? En quina confirmació va desaparèixer aquella línia que jures haver escrit?

A la lliçó següent, Visualitzant l'Historial de Confirmacions, veurem git log amb tots els seus formats i filtres, els formats personalitzats amb --pretty, la cerca «pickaxe» que troba quan va aparèixer o desaparèixer un text concret, git show per inspeccionar una confirmació, i les diferents formes de referir-se a un commit —HEAD, HEAD~3, HEAD^— que ja hem anat fent servir de passada i que convé entendre bé.

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