La lliçó anterior va acabar amb un exercici incòmode: un BREAKING CHANGE perfectament escrit va desaparèixer en integrar la branca amb squash merge, i l'eina de versionatge va publicar com a MINOR un canvi que trencava tots els consumidors. El missatge era impecable; el que va fallar va ser la política d'integració.

Aquesta lliçó resol aquesta qüestió i, amb ella, la que vam deixar deliberadament oberta a la lliçó 07-04: quan arriba el moment de fusionar una pull request, merge, squash o rebase? És probablement la discussió més recurrent i més religiosa de tots els equips que fan servir Git, i té resposta raonada —encara que no sigui la mateixa per a tots els projectes.

Però abans cal respondre una pregunta prèvia que gairebé ningú es fa: net per a què? Un historial no és una obra d'art. És una eina de treball amb usos concrets i mesurables. Un historial "bonic" que no serveix per a cap d'aquests usos no és un historial net: és decoració cara.

Contingut

  1. Per a què serveix realment un historial
  2. El commit atòmic
  3. Com arribar a commits atòmics: add -p i rebase -i
  4. La decisió d'integració: merge, squash o rebase
  5. Taula comparativa a fons
  6. Quina política encaixa amb cada flux
  7. Recomanació raonada per tipus de projecte
  8. Netejar abans de publicar, no tocar el que s'ha publicat
  9. Commits de fusió: quan aporten i quan són soroll
  10. Historial lineal davant d'historial amb topologia
  11. Els commits de reformatatge massiu i .git-blame-ignore-revs
  12. Què significa que un historial sigui "bisecable"

  1. Per a què serveix realment un historial

Si no penses mirar mai enrere, qualsevol historial serveix. La qualitat de l'historial només es paga quan el consultes, i es consulta per a cinc coses molt concretes. Totes les coneixes ja del curs:

Ús Eina Què necessita de l'historial
Entendre el perquè d'una línia git blame + git show (06-03) Commits petits amb missatges que expliquin la motivació
Localitzar on es va trencar alguna cosa git bisect (06-02) Que cada commit compili i passi les proves
Desfer un canvi concret git revert (05-06) Que aquell canvi estigui aïllat al seu propi commit
Revisar una proposta git diff main...branca (07-02) Commits que expliquin una història pas a pas
Auditar què va entrar i quan git log, git tag --contains (05-05, 06-04) Traçabilitat de branca, autor i tiquet

Fixa't que cap dels cinc demana un graf bonic. Demanen commits petits, autocontinguts, ben descrits i que funcionin. Aquest és l'objectiu real. La topologia —lineal o amb branques— és un mitjà, no una finalitat, i per això la discussió merge/squash/rebase només es pot resoldre preguntant quin d'aquests cinc usos importa més al teu projecte.

Definició de treball: un historial net és aquell en què qualsevol commit es pot entendre, provar, revertir i atribuir per separat.

  1. El commit atòmic

Un commit atòmic és un commit que conté un canvi complet i només un. Les dues meitats de la definició importen igual:

  • Complet: no deixa el projecte trencat. Compila, passa les proves, la funcionalitat que toca funciona.
  • Només un: no arrossega canvis que no tenen a veure amb el seu propòsit.

Què hi entra i què no

Sí que entra al commit No hi entra
El codi del canvi Un altre canvi diferent que vas fer de passada
Les seves proves Reformatatge de fitxers que només vas obrir per llegir
La documentació que aquell canvi invalida Reanomenar variables "ja que hi soc"
La migració de dades que aquell canvi exigeix Corregir una errada en un altre mòdul
El canvi de dependència que aquell codi requereix Un console.log de depuració

El cas patològic clàssic, que tothom ha comès:

git log --oneline -1 --stat
a1b2c3d Afegeix el filtre per etiqueta

 app.js            | 340 ++++++++++++++++++++++++-------------------
 estils.css        | 128 +++++++++++++----
 index.html        |  22 ++-
 README.md         |   4 +-
 package.json      |   6 +-
 .github/ci.yml    |  18 ++-
 6 files changed, 340 insertions(+), 178 deletions(-)

340 línies canviades a app.js per "afegir un filtre". A dins hi ha, amb tota seguretat: el filtre (30 línies), un reformatatge del fitxer sencer en desar amb l'editor (250 línies), un reanomenament de dues variables (20), una correcció d'una errada sense relació (5) i la pujada d'una dependència (el package.json).

Què hi perds, amb aquest commit:

  • git blame sobre qualsevol línia d'app.js t'hi porta, encara que aquella línia només canviés de sagnat.
  • Si el filtre té una fallada, git revert t'obliga a revertir també la pujada de la dependència.
  • git bisect l'assenyalarà com a culpable, però el diff és tan gran que no hauràs avançat gens.
  • El revisor no el pot avaluar: 340 línies de les quals 250 són soroll.

La prova de l'assumpte

Hi ha un test molt simple, i enllaça amb la lliçó anterior: si no pots descriure el commit en un assumpte de 50 caràcters sense fer servir "i", no és atòmic. La incapacitat d'anomenar-lo és el símptoma.

  1. Com arribar a commits atòmics: add -p i rebase -i

L'objecció pràctica és real: "però jo no programo així, jo vaig tocant coses". Ningú programa en commits atòmics. Els commits atòmics es fabriquen després, amb dues eines que ja domines.

Abans de confirmar: git add -p

A la lliçó 02-04 vam veure git add -p per preparar canvis per fragments. Aquest és exactament el seu propòsit aquí: has tocat tres coses a app.js i en vols tres commits.

git add -p app.js
@@ -12,7 +12,7 @@ function renderitzaLlista(tasques) {
-  const visibles = tasques.filter(t => !t.completada);
+  const visibles = aplicaFiltres(tasques);

(1/4) Stage this hunk [y,n,q,a,d,j,J,g,/,s,e,?]?

Respons y als fragments del filtre i n als altres, confirmes, i repeteixes. Recorda les tecles que més renten:

Tecla Efecte
y / n Preparar / no preparar aquest fragment
s Dividir el fragment en fragments més petits
e Editar el fragment a mà (per partir-lo línia a línia)
q Sortir

I una comprovació imprescindible abans de confirmar, perquè preparar per fragments permet crear un commit que no compila:

git diff --staged        # el que entrarà
git stash push --keep-index   # aparta el que NO entrarà
npm test                 # el commit funciona per si sol?
git commit
git stash pop            # recupera la resta

Aquest stash --keep-index de la lliçó 05-04 és l'única manera honesta de verificar que un commit fabricat amb add -p és realment complet.

Després de confirmar: git rebase -i

Si ja has confirmat i el resultat és un desastre, el rebase interactiu de la lliçó 05-02 ho arregla, sempre que no ho hagis publicat (apartat 8).

git rebase -i main
pick a1b2c3d wip
pick b2c3d4e segueixo amb el filtre
pick c3d4e5f arregla allò d'abans
pick d4e5f6a ara sí
pick e5f6a7b afegeix proves del filtre

Es converteix en:

pick a1b2c3d wip
fixup b2c3d4e segueixo amb el filtre
fixup c3d4e5f arregla allò d'abans
fixup d4e5f6a ara sí
pick e5f6a7b afegeix proves del filtre

I amb reword al primer, el resultat són dos commits nets: feat(filtres): afegeix el filtre per etiqueta i test(filtres): cobreix el filtrat per múltiples etiquetes.

Les tres ordres que resolen el 90 % dels casos:

Ordre Quan
fixup El commit era una correcció de l'anterior; el seu missatge sobra
squash Igual, però vols conservar i combinar els missatges
reword El canvi està bé, el missatge no
edit Necessites partir un commit en dos (amb reset HEAD^ i add -p)

I el flux que fa tot això automàtic, de la lliçó 05-02: git commit --fixup=<sha> mentre treballes, i git rebase -i --autosquash main al final.

  1. La decisió d'integració: merge, squash o rebase

Arribem al punt. La branca GT-134-timeout-sync del Bruno té quatre commits, està aprovada i entrarà a main. Hi ha tres maneres de fer-ho, i produeixen tres historials diferents.

Partim sempre d'aquesta situació:

gitGraph
    commit id: "A"
    commit id: "B"
    branch GT-134
    commit id: "C1"
    commit id: "C2"
    commit id: "C3"
    checkout main
    commit id: "D"

main ha avançat amb D mentre el Bruno treballava, així que no hi ha avanç ràpid possible.

Opció 1: merge commit (--no-ff)

git switch main
git merge --no-ff GT-134-timeout-sync
gitGraph
    commit id: "A"
    commit id: "B"
    branch GT-134
    commit id: "C1"
    commit id: "C2"
    commit id: "C3"
    checkout main
    commit id: "D"
    merge GT-134 id: "M"

Els tres commits del Bruno entren tal qual, amb els seus SHA i les seves dates originals, i s'afegeix un commit de fusió M amb dos pares que marca on i quan es va integrar la branca. És el que vam veure a la lliçó 03-03.

Opció 2: squash merge

git switch main
git merge --squash GT-134-timeout-sync
git commit          # s'escriu un missatge nou
gitGraph
    commit id: "A"
    commit id: "B"
    commit id: "D"
    commit id: "S (C1+C2+C3)" type: HIGHLIGHT

Els tres commits es fonen en un de sol, S, amb un missatge nou. main queda lineal. La branca original continua existint al repositori del Bruno, però main no hi té cap referència: Git no sap que S ve de C1, C2 i C3.

Opció 3: rebase and merge

git switch GT-134-timeout-sync
git rebase main
git switch main
git merge --ff-only GT-134-timeout-sync
gitGraph
    commit id: "A"
    commit id: "B"
    commit id: "D"
    commit id: "C1'"
    commit id: "C2'"
    commit id: "C3'"

Els tres commits es reescriuen damunt de D (SHA nous, mateix contingut i mateix missatge) i main avança en línia recta. És el rebase de la lliçó 05-01.

L'observació clau

Les tres produeixen exactament el mateix arbre de fitxers. El codi final és idèntic byte a byte. L'única cosa que canvia és quina informació queda registrada sobre com s'hi va arribar. Per això la decisió no és tècnica: és una decisió sobre quina informació vols poder recuperar d'aquí a un any.

  1. Taula comparativa a fons

Dimensió Merge commit (--no-ff) Squash Rebase
Historial de main Amb topologia: bombolles de branca Estrictament lineal Estrictament lineal
Nre. de commits a main per PR N + 1 (els de la branca més la fusió) Exactament 1 N
Es conserven els commits originals? Sí, amb el seu SHA No, desapareixen No: mateix contingut, SHA nous
Traçabilitat de la branca Màxima: el commit M diu quina branca, quan i qui va fusionar Només el que digui el missatge del squash Cap: els commits queden solts
Llegibilitat de git log --oneline Sorollós si hi ha molts wip Màxima: una línia per canvi Depèn de la disciplina de la branca
git bisect Pot aterrar en un commit intermedi trencat Òptim: cada pas és un canvi íntegre i provat Bo si cada commit compila; dolent si no
Granularitat de bisect Fina (arriba al commit exacte) Gruixuda (arriba a la PR sencera) Fina
git revert Un revert -m 1 desfà la PR completa Trivial: un commit, un revert Cal revertir N commits, en ordre invers
git blame Apunta al commit real, amb el seu missatge detallat Apunta al squash: missatge més genèric Apunta al commit real
Atribució d'autoria Correcta i per commit Un sol autor; la resta només si hi ha Co-authored-by Correcta i per commit
Dates Es conserven les d'autoria original Data única, la de la fusió Data d'autoria original, data de commit nova
Conflictes Es resolen una vegada, a la fusió Es resolen una vegada Es resolen commit a commit (es pot repetir)
Risc de reescriptura Cap Cap a main; la branca queda òrfena La branca es reescriu: obliga a push --force-with-lease
Compatibilitat amb la regla d'or (05-01) Total Total Només si la branca és d'una persona o està coordinada
--first-parent Molt útil: dona la llista de PRs Irrellevant (no hi ha fusions) Irrellevant
Deducció de SemVer (08-01) Llegeix tots els commits: fiable Llegeix només el squash: fràgil Llegeix-los tots: fiable
Corba d'aprenentatge Baixa Mínima Alta (cal entendre rebase i force-push)

Els tres punts on de debò es decideix

1. revert davant de bisect. Squash guanya en revert i en llegibilitat; merge i rebase guanyen en granularitat de bisect i en blame. Pregunta't què fas més sovint: desfer funcionalitats senceres, o caçar el commit exacte que va trencar alguna cosa?

2. La qualitat real dels commits del teu equip. Si les branques arriben amb wip, wip2 i ara sí, el merge i el rebase fiquen aquesta brossa a main per sempre. El squash l'absorbeix. Squash és la política que protegeix de la indisciplina; merge i rebase són les que la premien quan existeix.

3. La mida de les pull requests. Amb PRs de 200 línies, un squash perd poca informació. Amb PRs de 2.000 línies, el squash produeix commits monstruosos que arruïnen blame i bisect alhora. Squash i PRs petites van juntes; és coherent amb el que vam dir sobre la mida de la PR a la lliçó 07-02.

  1. Quina política encaixa amb cada flux

Cada flux del mòdul 7 té una política que li escau de manera natural:

Flux (mòdul 7) Política natural Motiu
Git Flow (07-03) Merge --no-ff sempre El valor és a la topologia: cal poder veure què va entrar a develop, què es va promoure a release i què s'hi va colar per un hotfix. Aplanar destrueix la informació que justifica el flux.
GitHub Flow (07-04) Squash per defecte Branques curtes, d'una persona, amb una funcionalitat. Un commit per PR a main produeix un historial llegible on cada línia és una unitat desplegable.
Trunk Based Development (07-05) Rebase o squash Les branques viuen hores. L'objectiu és un tronc estrictament lineal, i amb branques tan petites la diferència entre les dues opcions és mínima.
Fork extern (07-01) Merge o squash Amb el Diego, que no té permís d'escriptura, el rebase de la seva branca és incòmode: no pots reescriure el seu fork. Es fusiona o es condensa des del costat del projecte.

Un matís sobre Git Flow que se sol passar per alt: en un repositori amb --no-ff sistemàtic, la lectura d'alt nivell es fa amb --first-parent (apartat 9), que amaga l'interior de cada bombolla i deixa només la seqüència d'integracions. Sense aquesta eina, un historial amb topologia és efectivament il·legible, i d'aquí ve bona part del rebuig al merge.

  1. Recomanació raonada per tipus de projecte

No hi ha una resposta universal, però sí que hi ha respostes defensables segons el projecte:

Aplicació web amb desplegament continu, equip de 3 a 15 persones → squash. És el cas de gestor-tasques. Les PRs són petites, cadascuna correspon a un tiquet GT-NNN, i el que més es fa és respondre a "què ha canviat aquesta setmana?" i "reverteix això que ha trencat producció". main queda amb una línia per canvi, revert és trivial i bisect aterra a la PR culpable, que és una granularitat suficient quan les PRs són de 200 línies. Condició innegociable: el títol del squash s'escriu amb la mateixa cura que un commit (lliçó 08-01) i s'hi propaguen els trailers rellevants, inclosos Co-authored-by i BREAKING CHANGE.

Biblioteca o producte amb versions i suport de diverses branques → merge --no-ff. Aquí cal respondre a "en quina versió va entrar això?", "aquest pedaç és a la branca 2.x?" i "de quina release va sortir?". La topologia és la resposta, i git log --first-parent, git tag --contains i git branch --contains en depenen. A més, amb cherry-pick entre branques de manteniment (lliçó 05-03), tenir els commits originals intactes facilita moltíssim la feina.

Projecte de codi obert amb molts col·laboradors externs → merge o squash, segons la qualitat de les contribucions. El projecte Git i el nucli de Linux fan servir merge amb historial de topologia, però exigeixen als contribuïdors sèries de pedaços ja netes. Un projecte que no pot exigir això fa millor de condensar.

Repositori intern petit, 1 o 2 persones → allò que us resulti còmode. Amb dues persones i context compartit, la diferència pràctica és petita. Trieu-ne un i no hi doneu més voltes.

Regla transversal, la que de debò importa:

Tria una política, configura-la a la plataforma perquè sigui l'única disponible, i escriu-la al README.md. El pitjor de tots els mons és un main on uns han condensat, altres han fusionat i altres han rebasat: no es pot llegir amb cap eina de manera consistent, --first-parent dona resultats absurds i ningú sap què esperar.

A gestor-tasques, l'acord escrit va quedar així:

## Política d'integració

Totes les pull requests s'integren amb **squash merge**.

- El títol de la PR és l'assumpte del commit: `tipus(àmbit): descripció` (veure 08-01).
- La descripció de la PR és el cos del commit: el perquè.
- Els trailers `Refs: GT-NNN`, `Co-authored-by` i `BREAKING CHANGE` es
  copien al quadre de squash abans de confirmar la fusió.
- Les PRs es mantenen per sota de 400 línies. Si creixen, es parteixen.
- La branca s'esborra després de fusionar.

  1. Netejar abans de publicar, no tocar el que s'ha publicat

Tot el de l'apartat 3 —rebase -i, fixup, reword, partir commits— té una frontera claríssima, i és la regla d'or de la lliçó 05-01:

Reescriu lliurement allò que només existeix a la teva màquina. No reescriguis mai allò que altres ja s'han baixat.

La raó, recordada en una frase: reescriure crea commits nous amb SHA diferents. Qui tingués els vells es queda amb dues versions divergents de la mateixa història, i el seu pull següent produeix un embolic que cal desfer a mà.

flowchart LR
    A["Feina en local<br/>commits bruts"] -->|"rebase -i, fixup,<br/>add -p, amend"| B["Sèrie neta"]
    B -->|"push"| C["Publicat"]
    C -->|"revert, commits nous"| D["Continua avançant"]
    C -.->|"PROHIBIT<br/>rebase, amend, force"| B

La zona grisa: la teva pròpia branca de PR

Hi ha un cas intermedi que cal resoldre explícitament, perquè genera discussions eternes: la teva branca de funcionalitat ja és a origin i oberta com a PR, i la vols netejar abans de fusionar.

És legítim, amb condicions:

  • La branca és teva i ningú més hi ha basat feina. Si la Carla hi ha fet un commit al damunt, ja no.
  • Fas servir sempre git push --force-with-lease (lliçó 04-05), mai --force a seques: si algú hi ha enviat alguna cosa mentrestant, l'operació es rebutja en lloc de destruir-la.
  • Avises a la PR. Els revisors perden el fil dels seus comentaris quan els SHA canvien.
  • Si ja hi ha comentaris de revisió, afegeix commits nous en lloc de reescriure, i condensa al final amb el squash de la fusió. És la raó principal per la qual el squash és tan popular: fa innecessari el rebase de neteja.
  • Per revisar què ha canviat entre la versió anterior i la reescrita, git range-diff de la lliçó 07-02.

Mai, sota cap circumstància: reescriure main, una branca de release o una branca compartida. Si allà cal desfer alguna cosa, es desfà amb git revert (lliçó 05-06), que afegeix un commit nou en lloc d'esborrar el vell.

  1. Commits de fusió: quan aporten i quan són soroll

No tots els commits de fusió són iguals. N'hi ha de dues classes i convé distingir-les.

Fusions que aporten

Són les deliberades: git merge --no-ff d'una branca de funcionalitat a main. El commit registra una decisió —"aquesta funcionalitat es va integrar aquí"— i el seu missatge la pot documentar:

Merge branch 'GT-134-timeout-sync'

Puja el temps d'espera de la sincronització després dels mesuraments
en preproducció de la setmana del 12. Aprovat per l'Ana.

Refs: GT-134

Aquest commit és informació pura, i amb --first-parent produeix un historial d'alt nivell excel·lent.

Fusions que són soroll

Són les accidentals: les que apareixen en fer git pull quan el remot ha avançat.

Merge branch 'main' of git.exemple.cat:equip/gestor-tasques

Aquest commit no registra cap decisió. Registra que la Carla va fer pull un dimarts a les 11:40. Un main amb dos-cents d'aquests és il·legible, i omplen el graf de bombolles que no signifiquen res.

La cura és la de la lliçó 04-04, i hauria de ser a la configuració global de tothom:

git config --global pull.rebase true

O, si prefereixes que Git t'obligui a decidir en comptes de triar per tu:

git config --global pull.ff only
# Si no hi ha avanç ràpid possible, git pull falla i decideixes tu

Llegir un historial amb moltes fusions: --first-parent

Un commit de fusió té dos pares: el primer és la branca on eres (main) i el segon és la que vas fusionar. --first-parent segueix únicament el primer, amb la qual cosa amaga el contingut intern de cada branca i deixa només la seqüència d'integracions.

# Tot, inclosos els commits interns de cada branca
git log --oneline
9f8e7d6 Merge branch 'GT-141-indexeddb'
6c5b4a3 test(sync): cobreix la migració des de localStorage
3a2b1c0 feat(sync): desa les tasques a IndexedDB
8d7c6b5 refactor(sync): extreu l'accés a dades a un mòdul
7f6e5d4 Merge branch 'GT-134-timeout-sync'
4e3d2c1 fix(sync): evita duplicar tasques en reintentar
1b0a9f8 chore(sync): puja el temps d'espera a 30 s
...
# Només la línia d'integracions
git log --oneline --first-parent
9f8e7d6 Merge branch 'GT-141-indexeddb'
7f6e5d4 Merge branch 'GT-134-timeout-sync'
...

De centenars de línies a una desena. Aquest és l'argument que rescata el merge: un historial amb topologia no és il·legible, és un historial amb dos nivells de lectura. El d'alt nivell s'obté amb --first-parent, i el detall continua sent-hi quan el necessites.

Es mereix un àlies, reprenent els de la lliçó 06-04:

git config --global alias.integracions \
  "log --oneline --first-parent --decorate"

--first-parent funciona també en altres ordres:

git log --first-parent --stat        # quins fitxers va tocar cada PR
git bisect start --first-parent      # bisecar per PRs, no per commits interns
git blame --first-parent fitxer      # atribuir a la fusió, no al commit intern

Aquest git bisect --first-parent és especialment útil: converteix un historial amb merges en una cosa amb la mateixa granularitat que un historial de squash, però sense perdre el detall quan hi vols baixar.

  1. Historial lineal davant d'historial amb topologia

flowchart TB
    subgraph LIN["Lineal (squash o rebase)"]
        direction LR
        L1["A"] --> L2["B"] --> L3["C"] --> L4["D"] --> L5["E"]
    end
    subgraph TOP["Amb topologia (merge --no-ff)"]
        direction LR
        T1["A"] --> T2["B"] --> TM1["M1"] --> TM2["M2"]
        T2 --> R1["C1"] --> R2["C2"] --> TM1
        TM1 --> S1["D1"] --> S2["D2"] --> TM2
    end
Lineal Amb topologia
Llegir git log --oneline Directe, sense eines Necessita --first-parent per a l'alt nivell
git log --graph Una columna Diverses columnes; amb moltes branques paral·leles, il·legible
Context de cada canvi Es perd l'agrupació per branca Explícit: es veu quins commits van anar junts
Quan es va fer la feina L'ordre és el d'integració, no el real Es veu la feina concurrent tal com va passar
bisect Directe, sense sorpreses Pot entrar dins d'una branca; --first-parent ho evita
revert d'una PR Squash: un commit. Rebase: N commits revert -m 1 sobre la fusió, un de sol
Honestedat històrica És una reconstrucció: l'ordre no va passar mai així És un registre: reflecteix el que va passar de debò
Cost de manteniment Requereix rebase i force-push (rebase) o perdre detall (squash) Cap: és el que surt tot sol

L'última fila és la tensió de fons, i mereix dir-se clarament: un historial lineal és una ficció útil. Ningú va treballar en aquell ordre. L'Ana i el Bruno van programar alhora durant tres dies; l'historial lineal explica que un va acabar i després va començar l'altre. La pregunta honesta no és "què és més veritable?" sinó "em serveix de res la veritat concurrent?". Per a la majoria dels productes, no. Per a un projecte amb suport de diverses versions en paral·lel, sí, i molt.

  1. Els commits de reformatatge massiu i .git-blame-ignore-revs

Hi ha un tipus de commit que trenca l'historial més que cap altre, i no és un commit dolent: és necessari. El dia que l'equip de gestor-tasques va decidir adoptar Prettier, l'Ana va executar el formatador sobre tot el projecte:

npx prettier --write .
git commit -am "style: aplica Prettier a tot el projecte"
 app.js       | 1204 ++++++++++++++++++++--------------------
 estils.css   |  486 +++++++--------
 index.html   |  152 ++---
 3 files changed, 921 insertions(+), 921 deletions(-)

El dany: a partir d'aquell moment, git blame app.js atribueix totes les línies del fitxer a l'Ana i a aquell commit. Tota la informació d'autoria i intenció dels últims dos anys queda enterrada sota un commit que no va canviar ni una coma de comportament.

Les regles perquè faci el mínim mal:

  1. Aïlla el reformatatge al seu propi commit. Mai barrejat amb canvis funcionals: és l'aplicació més important del principi del commit atòmic.
  2. Que aquell commit no canviï el comportament. Només el formatador; res a mà. Ha de poder reproduir-se executant l'eina.
  3. Digues-ho al missatge, inclosa la versió de l'eina i la seva configuració, perquè sigui reproduïble.
  4. Registra'l a .git-blame-ignore-revs.

Aquest fitxer, que vam veure a la lliçó 06-03 i que ara tanquem, és una llista de SHA que git blame ha de travessar com si no existissin:

# .git-blame-ignore-revs
# Commits de reformatatge massiu que blame ha d'ignorar.
# Documentació: git blame --ignore-revs-file
#
# Adopció de Prettier 3.2 a tot el projecte (GT-160)
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
#
# Migració de tabuladors a 2 espais a estils.css (GT-171)
b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1
#
# Reordenació alfabètica de les propietats CSS (GT-183)
c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2

Requisits: SHA complets de 40 caràcters (els abreujats no valen) i una línia per commit.

Es fa servir així:

# Puntualment
git blame --ignore-revs-file .git-blame-ignore-revs app.js

# Permanentment, per a aquest repositori
git config --local blame.ignoreRevsFile .git-blame-ignore-revs

Amb aquesta configuració local, git blame app.js torna a mostrar els autors reals de cada línia. Les plataformes d'allotjament més habituals reconeixen el fitxer pel seu nom convencional i l'apliquen automàticament a la seva vista de blame.

El detall que cal documentar al README.md: blame.ignoreRevsFile és configuració local (lliçó 01-05), així que no viatja amb el clon. El fitxer sí que està versionat, però cada persona l'ha d'activar. Afegeix-ho a l'script de posada en marxa del projecte:

# script d'arrencada del repositori
git config --local blame.ignoreRevsFile .git-blame-ignore-revs
git config --local commit.template .gitmissatge
git config --local core.hooksPath .githooks

  1. Què significa que un historial sigui "bisecable"

A la lliçó 06-02 vam dir que git bisect només és útil sobre un historial "bisecable" i vam remetre aquí. Ja tenim totes les peces per definir-ho.

Un historial és bisecable quan qualsevol commit escollit a l'atzar es pot compilar, arrencar i provar.

git bisect funciona per cerca binària: et planta en un commit intermedi i et pregunta si està bé o malament. Si aquell commit ni tan sols compila, no pots respondre cap de les dues coses. L'has de marcar amb git bisect skip, i cada skip degrada la cerca. Amb prou commits trencats, la bisecció deixa de convergir i no serveix de res.

Què trenca la bisecabilitat

Pràctica Efecte sobre bisect
Commits wip que no compilen Cadascun és un skip obligat
Separar el codi de les seves proves en dos commits El commit intermedi "falla" per una raó falsa
Afegir una dependència en un commit i fer-la servir a l'anterior El commit intermedi no arrenca
Canviar l'esquema de dades sense la seva migració Arrenca però falla en temps d'execució
PRs gegants condensades Compila sempre, però bisect només arriba a "aquesta PR de 2.000 línies"
add -p sense verificar el resultat Commits que no inclouen tot el que necessiten

Què la garanteix

  1. Commits atòmics i complets: apartat 2. Cadascun deixa el projecte funcionant.
  2. El codi i les seves proves, al mateix commit.
  3. CI sobre cada commit, no només sobre la punta de la branca. És l'única comprovació real. Amb la validació de la lliçó 07-06, es pot executar la compilació sobre cada commit de la PR:
      - name: Comprovar que cada commit compila
        run: |
          BASE="${{ github.event.pull_request.base.sha }}"
          for sha in $(git rev-list --reverse "$BASE..HEAD" --no-merges); do
            echo "::group::Compilant $sha"
            git checkout --quiet "$sha"
            npm ci --silent && npm run build --silent || {
              echo "::error::El commit $sha no compila: trenca la bisecabilitat"
              exit 1
            }
            echo "::endgroup::"
          done
  1. git rebase --exec, de la lliçó 05-02, per comprovar-ho tu abans de publicar. És l'eina perfecta per a això:
# Executa les proves a cada commit des de main; s'atura al primer que falli
git rebase main --exec "npm test"

Si el rebase s'atura, tens un commit no bisecable a la branca. L'arregles amb edit o fixup i continues.

I la política d'integració

Tanca el cercle de l'apartat 4:

  • Squash garanteix la bisecabilitat gairebé de franc: cada commit de main és una PR sencera, que va passar el CI. La granularitat és gruixuda, però no falla mai.
  • Merge amb --first-parent dona el mateix que el squash, i permet baixar al detall quan la PR és gran.
  • Rebase dona la granularitat més fina, però només si cada commit compila, cosa que exigeix disciplina i una comprovació com la de dalt.

Errors Habituals i Consells

Error 1: confondre "net" amb "bonic". Un graf amb una sola línia recta que no permet revertir, bisecar ni entendre res no és net. Els cinc usos de l'apartat 1 són el criteri; l'estètica no.

Error 2: barrejar polítiques d'integració al mateix repositori. És pitjor que triar la política equivocada. Configura-la a la plataforma deixant una sola opció activa.

Error 3: condensar PRs enormes. Un squash de 2.000 línies destrueix blame i deixa bisect sense granularitat. Squash exigeix PRs petites; si no en tens, arregla això primer.

Error 4: perdre el BREAKING CHANGE al squash. És el que vam veure a l'exercici 3 de la lliçó 08-01. El quadre de missatge del squash surt preomplert amb la concatenació de tots els commits; llegeix-lo i edita'l, no l'acceptis tal qual ni el buidis.

Error 5: rebasar una branca compartida. La regla d'or de la 05-01 no té excepcions pràctiques. Si dubtes de si algú més la té, no la rebasis.

Error 6: acumular fusions de pull. git config --global pull.rebase true i desapareixen. És un canvi d'una línia que millora l'historial per a tota la vida.

Error 7: fer el reformatatge massiu barrejat amb un canvi funcional. Enterraràs el canvi funcional en 900 línies de soroll i arruïnaràs el blame sense possibilitat de rescat, perquè .git-blame-ignore-revs no pot ignorar un commit que a més conté codi real.

Error 8: pensar que --first-parent no existeix. Molta gent rebutja el merge per "il·legible" sense haver provat mai git log --first-parent. Prova-ho abans de decidir.

Consell 1: escriu la política al README.md. Amb les condicions (mida de PR, format del títol, trailers). El Diego, que ve de fora, la necessita abans de la seva primera PR.

Consell 2: git rebase main --exec "npm test" abans d'obrir la PR. És la manera més barata de garantir que la teva branca és bisecable.

Consell 3: àlies per llegir. git config --global alias.integracions "log --oneline --first-parent --decorate".

Consell 4: revisa la teva branca abans de publicar-la. git log --oneline main..HEAD. Si hi veus un wip, encara et queda feina.

Consell 5: .git-blame-ignore-revs des del primer commit. Crea'l buit, amb la capçalera comentada. Així, el dia del primer reformatatge massiu, el lloc ja existeix.

Consell 6: esborra les branques fusionades. Amb squash, la branca no queda enllaçada des de main; deixar-la viva suggereix que hi ha feina pendent. Recorda git branch --merged i git fetch --prune de la lliçó 03-06.

Exercicis

Exercici 1: els tres historials, amb les mans

Munta un repositori de proves que reprodueixi la situació de l'apartat 4: main amb dos commits, una branca amb tres commits i un commit posterior a main. Després, des de tres còpies del mateix estat, integra la branca amb cadascuna de les tres polítiques.

Per a cada resultat, respon:

  1. Quants commits té main?
  2. Què mostra git log --oneline --graph?
  3. Què mostra git log --oneline --first-parent?
  4. Quina ordre exacta faries servir per desfer la integració completa?
  5. Si bisect assenyalés el problema, a quin nivell de detall et deixaria?

Exercici 2: fabricar commits atòmics d'un desastre

Simula el commit patològic de l'apartat 2:

  1. Crea un app.js amb tres funcions i confirma'l.
  2. En un sol canvi, modifica una funció (canvi funcional), torna a sagnar-ne una altra (reformatatge) i corregeix una errada en un comentari de la tercera.
  3. Fes servir git add -p (amb s i e quan calgui) per separar els tres en tres commits atòmics amb missatges en Conventional Commits.
  4. Verifica amb git stash push --keep-index que el primer commit funciona per si sol.
  5. Comprova amb git log --oneline --stat que cada commit toca només el que li pertoca.

Exercici 3: rescatar un blame arruïnat

  1. Crea un repositori amb un estils.css d'unes 20 línies, escrit en tres commits de tres autors diferents (fes servir git -c user.name=... -c user.email=... commit).
  2. Comprova amb git blame estils.css que l'autoria és correcta.
  3. Aplica un reformatatge massiu (per exemple, canvia tot el sagnat) i confirma'l com a style: torna a sagnar estils.css a 2 espais.
  4. Comprova el dany amb git blame.
  5. Crea .git-blame-ignore-revs, configura blame.ignoreRevsFile i demostra que l'autoria original es recupera.
  6. Explica per què el pas 5 hauria estat impossible si el reformatatge hagués vingut barrejat amb un canvi funcional.

Solucions

Solució 1:

# Estat de partida, reutilitzable
mkdir /tmp/practica-hist && cd /tmp/practica-hist
git init -b main
git config user.name "Ana Ferrer"; git config user.email "[email protected]"

echo "linia A" > app.js && git add . && git commit -m "chore: commit inicial (A)"
echo "linia B" >> app.js && git commit -am "feat: base del projecte (B)"

git switch -c GT-134
echo "C1" >> app.js && git commit -am "chore(sync): puja el temps d'espera (C1)"
echo "C2" >> app.js && git commit -am "fix(sync): evita duplicar en reintentar (C2)"
echo "C3" >> app.js && git commit -am "test(sync): cobreix el reintent (C3)"

git switch main
echo "D" >> README.md && git add . && git commit -m "docs: afegeix el README (D)"

# Tres còpies del mateix estat
cd /tmp && cp -r practica-hist h-merge && cp -r practica-hist h-squash && cp -r practica-hist h-rebase
# --- MERGE ---
cd /tmp/h-merge
git merge --no-ff GT-134 -m "Merge branch 'GT-134'"
git log --oneline --graph
*   f1e2d3c Merge branch 'GT-134'
|\
| * c3d4e5f test(sync): cobreix el reintent (C3)
| * b2c3d4e fix(sync): evita duplicar en reintentar (C2)
| * a1b2c3d chore(sync): puja el temps d'espera (C1)
* | 9a8b7c6 docs: afegeix el README (D)
|/
* 8f7e6d5 feat: base del projecte (B)
* 7e6d5c4 chore: commit inicial (A)
# --- SQUASH ---
cd /tmp/h-squash
git merge --squash GT-134
git commit -m "fix(sync): evita duplicar tasques en reintentar l'enviament" \
           -m "Puja el temps d'espera a 30 s i afegeix la cobertura del reintent." \
           -m "Refs: GT-134"
git log --oneline
5d4c3b2 fix(sync): evita duplicar tasques en reintentar l'enviament
9a8b7c6 docs: afegeix el README (D)
8f7e6d5 feat: base del projecte (B)
7e6d5c4 chore: commit inicial (A)
# --- REBASE ---
cd /tmp/h-rebase
git switch GT-134 && git rebase main
git switch main && git merge --ff-only GT-134
git log --oneline
3c2b1a0 test(sync): cobreix el reintent (C3)
2b1a0f9 fix(sync): evita duplicar en reintentar (C2)
1a0f9e8 chore(sync): puja el temps d'espera (C1)
9a8b7c6 docs: afegeix el README (D)
8f7e6d5 feat: base del projecte (B)
7e6d5c4 chore: commit inicial (A)

Les respostes:

Merge Squash Rebase
1. Commits a main 6 (3+1 de nous) 4 (1 de nou) 6 (3 de nous)
2. --graph Bifurcació visible amb dues branques Una columna Una columna
3. --first-parent 3 línies: A, B, D, M Igual que --oneline Igual que --oneline
4. Desfer git revert -m 1 f1e2d3c git revert 5d4c3b2 git revert 3c2b1a0 2b1a0f9 1a0f9e8 (o git revert 9a8b7c6..HEAD)
5. Detall de bisect Commit exacte (o la PR, amb --first-parent) La PR sencera Commit exacte

Observa la fila 4: amb rebase, desfer la integració exigeix revertir tres commits en ordre invers, i si l'intermedi depèn del primer, hi ha conflictes. És el desavantatge més pràctic del rebase pur.

Solució 2:

mkdir /tmp/practica-atom && cd /tmp/practica-atom && git init -b main

cat > app.js <<'FI'
function saluda(n) { return "Hola " + n; }
function suma(a, b) { return a + b; }
// Retorna el nombre de tasqes    <-- errada
function compta(l) { return l.length; }
FI
git add . && git commit -m "chore: estat inicial"
# 2. Els tres canvis de cop
cat > app.js <<'FI'
function saluda(n) { return `Hola ${n}`; }
function suma(a, b) {
  return a + b;
}
// Retorna el nombre de tasques
function compta(l) { return l.length; }
FI
# 3. Separar amb add -p
git add -p app.js
# Fragment 1 (saluda):    y
# Fragment 2 (suma):      n
# Fragment 3 (comentari): n
git commit -m "refactor(app): usa plantilles de cadena a saluda()"

git add -p app.js
# Fragment de suma: y ; el del comentari: n
git commit -m "style(app): reformata suma() a diverses línies"

git add app.js
git commit -m "docs(app): corregeix l'errada del comentari de compta()"

Si els canvis fossin al mateix fragment, s el parteix i e permet editar el fragment a mà, deixant amb - les línies que no vols preparar.

# 4. Verificar l'aïllament del primer commit
git reset --soft HEAD~2        # deixa els dos últims com a canvis preparats
git stash push --keep-index    # aparta el que no està preparat... (veure nota)
node -e "require('./app.js')"  # o npm test
git stash pop

A la pràctica es fa abans de confirmar: prepares el fragment, git stash push --keep-index aparta tota la resta, executes les proves sobre exactament el que entrarà, i només llavors confirmes i fas git stash pop.

# 5. Cada commit toca el que li pertoca
git log --oneline --stat -3

Solució 3:

mkdir /tmp/practica-blame && cd /tmp/practica-blame && git init -b main

printf '.tasca {\n    color: #333;\n}\n' > estils.css
git -c user.name="Ana Ferrer" -c user.email="[email protected]" \
    commit -am "feat(css): estil base de la tasca" --allow-empty-message 2>/dev/null || {
  git add . && git -c user.name="Ana Ferrer" -c user.email="[email protected]" \
    commit -m "feat(css): estil base de la tasca"
}

printf '.tasca.completada {\n    opacity: 0.5;\n}\n' >> estils.css
git add . && git -c user.name="Bruno Salas" -c user.email="[email protected]" \
    commit -m "feat(css): atenua les tasques completades"

printf '.tasca.vencuda {\n    border-left: 3px solid #c00;\n}\n' >> estils.css
git add . && git -c user.name="Carla Vidal" -c user.email="[email protected]" \
    commit -m "feat(css): marca les tasques vençudes"
# 2. Autoria correcta
git blame estils.css
^a1b2c3d (Ana Ferrer   2026-07-20 .tasca {
^a1b2c3d (Ana Ferrer   2026-07-20     color: #333;
b2c3d4e5 (Bruno Salas  2026-07-21 .tasca.completada {
c3d4e5f6 (Carla Vidal  2026-07-22 .tasca.vencuda {
# 3. El reformatatge massiu
sed -i 's/^    /  /' estils.css
git -c user.name="Ana Ferrer" -c user.email="[email protected]" \
    commit -am "style: torna a sagnar estils.css a 2 espais"

# 4. El dany
git blame estils.css

Ara totes les línies sagnades apareixen a nom de l'Ana i del commit d'estil.

# 5. El rescat
SHA=$(git rev-parse HEAD)         # SHA complet de 40 caràcters, imprescindible
cat > .git-blame-ignore-revs <<FI
# Commits de reformatatge massiu que blame ha d'ignorar.
# Nou sagnat d'estils.css a 2 espais
$SHA
FI
git add .git-blame-ignore-revs
git commit -m "chore: registra el commit de sagnat a blame-ignore-revs"

git config --local blame.ignoreRevsFile .git-blame-ignore-revs
git blame estils.css       # autoria original recuperada

6. Perquè .git-blame-ignore-revs funciona saltant-se el commit sencer: quan blame troba aquell SHA, atribueix la línia al commit anterior que la va tocar. Si el commit contingués a més un canvi funcional real, ignorar-lo atribuiria també aquell canvi funcional a qui no el va fer, i perdries la informació de qui va introduir el codi de debò. El fitxer només és segur amb commits que demostrablement no canvien el comportament. Per això la regla 1 de l'apartat 11 —aïllar el reformatatge— no és un consell estètic: és la condició que fa possible el rescat.

Conclusió

L'essencial d'aquesta lliçó:

  • Un historial és net quan qualsevol commit es pot entendre, provar, revertir i atribuir per separat. Els cinc usos reals —blame, bisect, revert, revisió i auditoria— són el criteri; l'estètica del graf no ho és.
  • El commit atòmic —un canvi complet i només un— és la unitat que fa possible tota la resta. No es programa així: es fabrica després, amb git add -p abans de confirmar i amb git rebase -i després.
  • La decisió que vam deixar oberta a la 07-04 queda tancada així:
    • Merge --no-ff quan la topologia és informació: Git Flow, biblioteques amb diverses versions vives, projectes amb branques de manteniment. Es llegeix amb --first-parent.
    • Squash quan el que importa és que main sigui una llista llegible de canvis desplegables: GitHub Flow, aplicacions web, equips les branques dels quals arriben amb wip. Exigeix PRs petites i un títol de squash escrit amb cura.
    • Rebase quan vols granularitat fina i linealitat alhora, i l'equip té la disciplina perquè cada commit compili: Trunk Based Development.
    • I per damunt de les tres: tria'n una, configura-la com a única opció a la plataforma i escriu-la al README.md. Barrejar-les és pitjor que triar malament.
  • Es neteja abans de publicar; no es toca el que s'ha publicat. La regla d'or de la 05-01 es manté, amb l'única zona grisa de la teva pròpia branca de PR, i sempre amb --force-with-lease i avís previ.
  • Els commits de fusió deliberats són informació; els accidentals de git pull són soroll que s'elimina amb pull.rebase true. I --first-parent converteix un historial amb topologia en un historial de dos nivells de lectura, cosa que rescata el merge de l'acusació d'il·legible.
  • Un reformatatge massiu s'aïlla al seu propi commit, es documenta i es registra a .git-blame-ignore-revs amb el SHA complet, activant-lo amb blame.ignoreRevsFile. Aïllar-lo no és estètica: és el que fa possible el rescat.
  • Un historial és bisecable quan qualsevol commit compila i es pot provar. Es garanteix amb commits atòmics, amb el codi i les seves proves junts, amb git rebase --exec abans de publicar i amb CI sobre cada commit.

gestor-tasques ja té missatges que expliquen el perquè i un historial que es pot llegir, bisecar i revertir. Però continua tenint a dins coses que no hi haurien de ser: la carpeta node_modules que el Bruno va pujar sense adonar-se'n, els .DS_Store de macOS, els Thumbs.db de Windows i un fitxer de configuració amb una contrasenya.

El següent és decidir què no entra mai en un repositori, a la lliçó 08-03: Ignorant Fitxers amb .gitignore.

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