La Carla obre una incidència amb un títol incòmode: "El botó d'esborrar tasca no esborra res". Ho reprodueix en tres navegadors. Prems la paperera, la tasca parpelleja i continua allà.

El que desconcerta és que funcionava. A la versió v1.0.0, que l'equip va etiquetar fa dos mesos (lliçó 05-05), l'esborrat funcionava perfectament: hi ha una demostració gravada que ho prova. Entre aquella etiqueta i main hi ha 214 commits.

Les opcions evidents són dolentes. Llegir-se els 214 diffs és un dia de feina i una garantia de no veure res. Buscar per paraules clau amb git log -S "esborrar" (lliçó 02-06) dona vint-i-tants candidats, i cap no sembla culpable. I preguntar al xat de l'equip només produeix teories.

Però hi ha una propietat del problema que ho canvia tot: l'historial està ordenat, i en algun punt d'aquesta seqüència el comportament va passar de "funciona" a "no funciona". Això és exactament l'escenari d'una cerca binària, i Git en porta una implementació integrada: git bisect. Amb ella, aquests 214 commits es redueixen a vuit proves.

Contingut

  1. La idea: cerca binària sobre l'historial
  2. Quants passos calen de veritat
  3. Una sessió manual completa
  4. git bisect skip: commits que no es poden provar
  5. git bisect log i replay: no perdre la feina
  6. Automatitzar amb git bisect run
  7. L'script de prova per a gestor-tasques
  8. Els codis de sortida, amb detall
  9. --first-parent: saltar-se l'interior de les fusions
  10. git bisect terms: quan no és "bo/dolent"
  11. Què fa que un historial sigui bisecable

  1. La idea: cerca binària sobre l'historial

La premissa de bisect és una de sola:

Existeix un commit concret a partir del qual el comportament va canviar. Abans d'ell, bé; des d'ell, malament.

Si això es compleix, no cal provar els commits un a un. N'hi ha prou de provar el del mig:

  • Si el del mig està malament, el culpable és a la primera meitat. La segona meitat es descarta sencera.
  • Si el del mig està , el culpable és a la segona meitat. La primera es descarta sencera.

I es repeteix sobre la meitat que queda. Cada prova elimina la meitat dels candidats.

flowchart TD
    A["214 candidats<br/>v1.0.0 (bé) ... main (malament)"] --> B["Provar el commit del mig"]
    B -- "funciona → good" --> C["107 candidats<br/>(meitat superior)"]
    B -- "no funciona → bad" --> D["107 candidats<br/>(meitat inferior)"]
    C --> E["Provar el nou mig"]
    D --> E
    E --> F["54 → 27 → 13 → 7 → 3 → 2 → 1"]
    F --> G["Commit culpable identificat"]

Git fa tota la feina de logística: calcula quin commit cal provar, fa el checkout per tu (en detached HEAD, lliçó 03-02), porta el compte dels descartats i al final et diu el hash exacte. Tu només respons una pregunta binària a cada pas: funciona o no?

  1. Quants passos calen de veritat

El nombre de proves és el logaritme en base 2 del nombre de candidats, arrodonit cap amunt. La diferència amb la cerca lineal és brutal i val la pena veure-la en una taula:

Commits entre bo i dolent Proves amb bisect (log₂) Proves una a una (pitjor cas)
8 3 8
32 5 32
100 7 100
214 8 214
1 000 10 1 000
10 000 14 10 000
1 000 000 20 1 000 000

Aquest és el titular: duplicar la mida de l'historial afegeix una sola prova. Un repositori amb un milió de commits es biseca en vint passos. És la raó per la qual bisect continua sent útil per gran que sigui el projecte.

El mateix Git t'ho diu en començar:

Bisecting: 106 revisions left to test after this (roughly 7 steps)

"106 revisions per provar, aproximadament 7 passos". No és una estimació optimista: és matemàtica.

  1. Una sessió manual completa

La Carla comença. El primer, deixar l'arbre de treball net: bisect farà molts checkout, i no pot si hi ha canvis sense confirmar. Si en té, git stash (lliçó 05-04).

cd ~/projectes/gestor-tasques
git status --short          # ha d'estar buit
git switch main
git pull

Pas 1: iniciar la sessió.

git bisect start

No respon res. Git ha entrat en mode bisecció: ha creat referències internes a .git/refs/bisect/ i un fitxer .git/BISECT_LOG.

Pas 2: marcar els dos extrems.

git bisect bad                 # el commit actual (main) està malament
git bisect good v1.0.0         # l'etiqueta v1.0.0 estava bé
Bisecting: 106 revisions left to test after this (roughly 7 steps)
[9f3c7a1e5b2d8c4f6a0e3b7d9c1f5a8e2b4d6c9f] Extreu la validació del formulari a la seva funció

Fixa't en el que ha passat: Git ha fet checkout del commit intermedi. L'arbre de treball de la Carla és ara el del commit 9f3c7a1, i HEAD està desacoblat:

git status
HEAD detached at 9f3c7a1
You are currently bisecting, started from main.
  (use "git bisect reset" to get back to the original branch)

nothing to commit, working tree clean

Pas 3: el bucle. La Carla obre index.html al navegador, crea una tasca, prem la paperera:

python3 -m http.server 8000    # servir el projecte
# ... provar al navegador ...

Funciona. Ho marca:

git bisect good
Bisecting: 53 revisions left to test after this (roughly 6 steps)
[4e8b2c9d7f1a3e5c6b0d8f2a4c7e9b1d3f5a7c8e] Afegeix el filtre per estat de la tasca

De 106 a 53 d'un cop. I així successivament:

# Prova a 4e8b2c9: NO funciona
git bisect bad
Bisecting: 26 revisions left to test after this (roughly 5 steps)
[7a1f5c3e9b2d6a8c4f0e7b3d5a9c1e6f2b8d4a7c] Reescriu el pintat del llistat
# Prova: NO funciona
git bisect bad
Bisecting: 12 revisions left to test after this (roughly 4 steps)
[2c6e9a4f8b1d7c3e5a0f9b2d6c8e4a1f7b3d5c9e] Extreu els estils del llistat a estils.css

La Carla continua: good, bad, good, bad... Cinc proves més tard:

git bisect bad
8d4f2a7c1e6b9d3f5a8c0e2b4d7f9a1c3e5b8d6f is the first bad commit
commit 8d4f2a7c1e6b9d3f5a8c0e2b4d7f9a1c3e5b8d6f
Author: Bruno Salas <[email protected]>
Date:   Mon Jun 22 11:14:38 2026 +0200

    Delega els esdeveniments del llistat al contenidor

 app.js | 22 ++++++++++------------
 1 file changed, 10 insertions(+), 12 deletions(-)

Vuit proves. 214 commits. Un culpable amb nom, data i diff.

Pas 4: mirar el diff.

git show 8d4f2a7
diff --git a/app.js b/app.js
--- a/app.js
+++ b/app.js
@@ -78,18 +78,16 @@
-  document.querySelectorAll('.esborrar').forEach((boto) => {
-    boto.addEventListener('click', (e) => {
-      esborraTasca(e.target.dataset.id);
-    });
-  });
+  llista.addEventListener('click', (e) => {
+    if (e.target.classList.contains('esborrar')) {
+      esborraTasca(e.target.dataset.id);
+    }
+  });

Aquí ho tens. El Bruno va canviar els escoltadors individuals per delegació d'esdeveniments al contenidor, que és una millora legítima. Però el botó d'esborrar conté un <svg> amb la icona de paperera, així que en prémer, e.target és el <svg>, no el botó: classList.contains('esborrar') dona false i no passa res. Amb e.target.closest('.esborrar') funcionaria.

Sense bisect, trobar això hauria portat hores. Amb ell, vuit proves i un diff de dotze línies.

Pas 5: sortir. I aquest pas no és opcional:

git bisect reset
Previous HEAD position was 8d4f2a7 Delega els esdeveniments del llistat al contenidor
Switched to branch 'main'

reset neteja l'estat de bisecció i torna HEAD on era en començar. Si te n'oblides, et quedes en detached HEAD sobre un commit antic, i el següent git status serà una sorpresa desagradable.

Resum de les ordres de la sessió:

Ordre Què fa
git bisect start Inicia la sessió
git bisect bad [<sha>] Marca un commit com a defectuós (per defecte, HEAD)
git bisect good [<sha>] Marca un commit com a correcte
git bisect start <dolent> <bo> Drecera: inicia i marca els dos extrems de cop
git bisect skip "No puc provar aquest" (apartat 4)
git bisect log Mostra la sessió fins ara
git bisect replay <fitxer> Reprodueix una sessió desada
git bisect visualize (o view) Obre gitk/log amb els candidats restants
git bisect run <ordre> Automatitza tota la sessió (apartat 6)
git bisect reset [<ref>] Acaba i torna al punt de partida

La drecera de l'inici estalvia dues ordres i és la que es fa servir a la pràctica:

git bisect start main v1.0.0     # <dolent> primer, <bo> després

  1. git bisect skip: commits que no es poden provar

A la sisena prova, la Carla es troba això:

python3 -m http.server 8000
# La pàgina apareix completament en blanc. La consola del navegador diu:
# Uncaught SyntaxError: Unexpected token '}' a app.js:96

Aquest commit està trencat per un altre motiu. No és que l'esborrat no funcioni: és que l'aplicació sencera no arrenca. Marcar-lo bad seria mentir a bisect i el podria portar pel camí equivocat; marcar-lo good, encara pitjor.

La resposta correcta és:

git bisect skip
Bisecting: 6 revisions left to test after this (roughly 3 steps)
[5b8e2d4a9c7f1e3b6d0a8c2f4e7b9d1a3c5f8e2b] Corregeix la clau sobrant a app.js

skip li diu a Git: "aquest commit no em dona informació". Git tria un altre candidat proper i continua. Si acabes saltant-te'n massa i no en pot aïllar un de sol, t'ho dirà:

There are only 'skip'ped commits left to test.
The first bad commit could be any of:
 3e7a1c5 ...
 8d4f2a7 ...
 b2c9e4f ...
We cannot bisect more!

En aquest cas et dona un conjunt petit de sospitosos, que ja és infinitament millor que 214.

També es poden saltar rangs sencers, si saps que tota una zona no compila:

git bisect skip a1b2c3d..e4f5a6b

Motius habituals per fer skip: el commit no compila, falten dependències que encara no eren al package.json, o el commit és d'un canvi de format massiu que fa impossible executar res. skip és el teu amic; mentir a bisect no.

  1. git bisect log i replay: no perdre la feina

A mitja bisecció llarga pot passar de tot: t'equivoques en marcar, es tanca el terminal, o has d'atendre una altra cosa. git bisect log desa la sessió:

git bisect log
git bisect start
# status: waiting for both good and bad commits
# bad: [c8f2a1e...] Afegeix el comptador de tasques pendents
git bisect bad c8f2a1e...
# status: waiting for good commit(s), bad commit known
# good: [1a5c9f3...] Versió 1.0.0
git bisect good 1a5c9f3...
# good: [9f3c7a1...] Extreu la validació del formulari a la seva funció
git bisect good 9f3c7a1...
# bad: [4e8b2c9...] Afegeix el filtre per estat de la tasca
git bisect bad 4e8b2c9...

Desar-ho i recuperar-ho després:

git bisect log > /tmp/sessio-esborrat.txt
git bisect reset                       # deixar-ho per avui

# Demà, o en una altra màquina:
git bisect replay /tmp/sessio-esborrat.txt

replay reconstrueix la sessió fins on vas arribar i et deixa al següent commit per provar.

I aquí hi ha el seu altre ús, el més valuós: corregir un error de marcatge. Si t'adones que vas dir good on havies de dir bad, no cal començar de zero:

git bisect log > /tmp/sessio.txt
# Editar /tmp/sessio.txt: treure la línia equivocada i les posteriors
git bisect reset
git bisect replay /tmp/sessio.txt

  1. Automatitzar amb git bisect run

La sessió manual funciona, però vuit voltes d'"obrir el navegador, crear una tasca, prémer, marcar" són vuit oportunitats d'equivocar-se i vint minuts de la vida de la Carla.

Si la comprovació es pot escriure com una ordre que retorna 0 quan està bé i alguna cosa diferent de 0 quan està malament, git bisect run ho fa tot sol:

git bisect start main v1.0.0
git bisect run <ordre>

Git prova, executa l'ordre, interpreta el codi de sortida, marca, avança i repeteix fins a trobar el culpable. Sense intervenció humana.

Amb una bateria de proves, és literalment això:

git bisect start main v1.0.0
git bisect run npm test

I per a un sol cas, pot ser prou una ordre d'una línia:

# Encara existeix la funció esborraTasca a app.js?
git bisect run grep -q "function esborraTasca" app.js

grep -q retorna 0 si troba, 1 si no. És exactament el contracte que bisect run espera.

  1. L'script de prova per a gestor-tasques

Com que la fallada de la Carla és de comportament al navegador, cal un script una mica més elaborat. L'equip té una prova automatitzada amb Node:

#!/usr/bin/env bash
#
# eines/provar-esborrat.sh
# Retorna 0 si l'esborrat de tasques funciona, 1 si no.
# Pensat per a 'git bisect run'.

set -uo pipefail        # ATENCIÓ: sense -e, volem controlar els codis a mà

# 1. Hi són els fitxers que necessitem? Si no, aquest commit no és avaluable.
if [ ! -f app.js ] || [ ! -f index.html ]; then
  echo "→ Falten fitxers del projecte: commit no avaluable"
  exit 125
fi

# 2. Dependències: s'instal·len sense soroll; si falla, no és culpa del codi
if [ -f package.json ]; then
  npm ci --silent >/dev/null 2>&1 || {
    echo "→ npm ci ha fallat: commit no avaluable"
    exit 125
  }
fi

# 3. El JavaScript és sintàcticament vàlid? Si no, tampoc és avaluable
if ! node --check app.js >/dev/null 2>&1; then
  echo "→ app.js no és sintàcticament vàlid: commit no avaluable"
  exit 125
fi

# 4. La prova de veritat
if node eines/prova-esborrat.mjs >/dev/null 2>&1; then
  echo "→ L'esborrat FUNCIONA"
  exit 0
else
  echo "→ L'esborrat NO funciona"
  exit 1
fi

I la prova en si, que simula el DOM mínim necessari:

// eines/prova-esborrat.mjs
// Prova mínima: crear una tasca, esborrar-la i comprovar que desapareix.
import { JSDOM } from 'jsdom';
import { readFileSync } from 'node:fs';

const html = readFileSync('index.html', 'utf8');
const dom = new JSDOM(html, { runScripts: 'outside-only' });
const { document } = dom.window;

// Exposar l'entorn del navegador que espera app.js
globalThis.window = dom.window;
globalThis.document = document;
globalThis.localStorage = dom.window.localStorage;

// Carregar l'aplicació
dom.window.eval(readFileSync('app.js', 'utf8'));

// 1. Crear una tasca
const camp = document.querySelector('#nova-tasca');
const formulari = document.querySelector('#formulari-tasca');
camp.value = 'Tasca de prova del bisect';
formulari.dispatchEvent(new dom.window.Event('submit'));

if (document.querySelectorAll('.tasca').length !== 1) {
  console.error('La tasca no s\'ha creat: la prova no és concloent');
  process.exit(125);           // no avaluable, no és la fallada que busquem
}

// 2. Prémer la icona de la paperera (el <svg> dins del botó)
const icona = document.querySelector('.tasca .esborrar svg')
           ?? document.querySelector('.tasca .esborrar');
icona.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true }));

// 3. Ha desaparegut?
const queden = document.querySelectorAll('.tasca').length;
process.exit(queden === 0 ? 0 : 1);

Els tres punts que fan que aquest script funcioni bé amb bisect:

  1. Prova una sola cosa. Només l'esborrat. Si la prova fallés també per altres motius, bisect trobaria el primer commit que trenca qualsevol cosa, que no és el que busquem.
  2. Distingeix "no funciona" de "no es pot avaluar". Aquest és el paper del 125, i és el que veurem ara.
  3. És silenciós i determinista. Sense sortida interactiva, sense dependre de la xarxa, sense fallades intermitents. Una prova que a vegades falla enverina tota la bisecció.

Llançant-lo:

git bisect start main v1.0.0
git bisect run ./eines/provar-esborrat.sh
running './eines/provar-esborrat.sh'
→ L'esborrat FUNCIONA
Bisecting: 53 revisions left to test after this (roughly 6 steps)
[4e8b2c9] Afegeix el filtre per estat de la tasca
running './eines/provar-esborrat.sh'
→ L'esborrat NO funciona
Bisecting: 26 revisions left to test after this (roughly 5 steps)
...
8d4f2a7c1e6b9d3f5a8c0e2b4d7f9a1c3e5b8d6f is the first bad commit
commit 8d4f2a7c1e6b9d3f5a8c0e2b4d7f9a1c3e5b8d6f
Author: Bruno Salas <[email protected]>
Date:   Mon Jun 22 11:14:38 2026 +0200

    Delega els esdeveniments del llistat al contenidor

 app.js | 22 ++++++++++------------
bisect run success

Quaranta segons, sense tocar el teclat. I un advertiment sobre el #!: l'script ha de ser executable (chmod +x) i estar fora de l'arbre de treball o present a tots els commits. Si viu dins del repositori i als commits antics no existia, bisect no el trobarà en fer checkout d'aquells commits. La solució habitual és copiar-lo a /tmp i executar-lo des d'allà:

cp eines/provar-esborrat.sh /tmp/provar.sh
git bisect run /tmp/provar.sh

  1. Els codis de sortida, amb detall

Aquest és el contracte complet de git bisect run, i conèixer-lo és la diferència entre una automatització que funciona i una que dona resultats falsos:

Codi de sortida Significat per a bisect Quan fer-lo servir
0 El commit és bo La comprovació passa
1–124 El commit és dolent La comprovació falla
125 Saltar aquest commit (skip) No es pot avaluar: no compila, falten dependències
126 Dolent (l'ordre no és executable) Error de permisos: revisa'l
127 Dolent (ordre no trobada) Error de ruta: revisa-la
128–255 Avorta la bisecció Error greu; Git s'atura i avisa

Tres conseqüències pràctiques:

  • El 125 és la peça clau. És l'equivalent automàtic de git bisect skip. Sense ell, un commit que no compila es marcaria com a "dolent" i bisect t'assenyalaria un culpable equivocat.
  • El 126 i el 127 són paranys mortals. Si l'script no és executable o la ruta està malament, bisect marcarà tots els commits com a dolents i "trobarà" com a culpable el primer del rang. Si el resultat et sembla absurd, comprova primer que l'script s'executa a mà: ./el-meu-script.sh; echo $?.
  • Evita exit 128 i superiors. Un kill -9 d'un procés dona 137, per exemple. Si el teu script pot acabar així, embolcalla'l per normalitzar el codi de sortida.

Comprovació prèvia obligatòria, sempre, abans de llançar un bisect run:

# En un commit que saps BO
git switch --detach v1.0.0
./eines/provar-esborrat.sh; echo "codi: $?"     # ha de ser 0

# En un commit que saps DOLENT
git switch --detach main
./eines/provar-esborrat.sh; echo "codi: $?"     # ha de ser 1

Trenta segons que eviten bisecar durant deu minuts cap a una resposta inventada.

  1. --first-parent: saltar-se l'interior de les fusions

L'historial de gestor-tasques té fusions (lliçó 03-03), i bisect per defecte explora tots els commits accessibles, inclosos els interns de cada branca fusionada. Això té dos inconvenients:

  • Els commits intermedis d'una branca en desenvolupament solen estar a mitges: pot ser que ni compilin, i acabes fent molts skip.
  • A efectes de main, el que importa és en quina fusió va entrar el problema, no quin dels sis commits de la branca el va introduir.
git bisect start --first-parent main v1.0.0

Amb --first-parent, Git només considera la línia principal: els commits directes de main i els commits de fusió, sense entrar dins de les branques. És exactament el mateix concepte de --first-parent que veurem a git log a la lliçó 06-04, i el mateix "primer pare" que triava -m 1 en revertir una fusió (lliçó 05-06).

gitGraph
   commit id: "v1.0.0"
   commit id: "a1"
   branch funcionalitat/esdeveniments
   commit id: "f1"
   commit id: "f2"
   commit id: "f3"
   checkout main
   commit id: "a2"
   merge funcionalitat/esdeveniments id: "M1"
   commit id: "a3"

Sense --first-parent, els candidats són a1, f1, f2, f3, a2, M1, a3. Amb --first-parent, només a1, a2, M1, a3.

Quan fer servir cadascun:

Situació Mode
L'equip fusiona branques i main sempre està sa --first-parent: més ràpid, menys skip
Vols el commit exacte dins de la branca culpable Sense --first-parent
L'historial és lineal (rebase abans d'integrar) Tant se val: no hi ha fusions
Molts commits intermedis trencats --first-parent, gairebé obligat

La tàctica de dues fases és la millor de les dues: primer --first-parent per identificar la fusió, després una segona bisecció dins d'aquella branca:

git bisect start --first-parent main v1.0.0
git bisect run /tmp/provar.sh
# → resulta ser la fusió M1

git bisect reset
git bisect start M1^2 M1^1        # dins de la branca: dolent=punta, bo=base
git bisect run /tmp/provar.sh

M1^2 és el segon pare (la punta de la branca fusionada) i M1^1 el primer (la base a main), amb la notació de la lliçó 02-06.

  1. git bisect terms: quan no és "bo/dolent"

bisect no és només per buscar regressions. Serveix per localitzar qualsevol canvi d'estat monòton a l'historial. Els exemples típics:

  • En quin commit es va arreglar una fallada? (aquí "bo" i "dolent" estan invertits)
  • En quin commit l'arrencada va passar de trigar 200 ms a 900 ms?
  • En quin commit el paquet va passar de 400 KB a 1,2 MB?

En aquests casos, la terminologia per defecte confon. git bisect terms permet reanomenar-la:

git bisect start --term-old=trencat --term-new=arreglat
git bisect trencat v1.0.0
git bisect arreglat main

A partir d'aquí, a cada pas respons git bisect trencat o git bisect arreglat, i Git busca el primer commit arreglat.

Àlies Equival a Significat
--term-old / --term-good good L'estat antic, el d'abans del canvi
--term-new / --term-bad bad L'estat nou, el que busquem

El que cal entendre és que a Git li són igual les paraules: sempre busca el primer commit amb l'estat "nou", sigui "trencat", "arreglat", "lent" o "gras". Consultar els termes actius en qualsevol moment:

git bisect terms
Your current terms are trencat for the old state
and arreglat for the new state.

Un exemple de rendiment, mesurant amb el mateix script:

#!/usr/bin/env bash
# /tmp/provar-mida.sh — 0 si el bundle pesa menys de 500 KB
npm ci --silent >/dev/null 2>&1 || exit 125
npm run build --silent >/dev/null 2>&1 || exit 125
bytes=$(wc -c < dist/app.min.js)
echo "→ ${bytes} bytes"
[ "$bytes" -lt 512000 ]
git bisect start --term-old=lleuger --term-new=pesat
git bisect lleuger v1.0.0
git bisect pesat main
git bisect run /tmp/provar-mida.sh

L'última línia de l'script és la que decideix: [ "$bytes" -lt 512000 ] retorna 0 si es compleix i 1 si no, que és just el contracte de bisect run.

  1. Què fa que un historial sigui bisecable

bisect és una eina que et torna el que li has donat. La seva eficàcia depèn directament de la qualitat de l'historial, i aquí és on el mòdul 5 i aquest es donen la mà:

Propietat de l'historial Efecte sobre bisect
Cada commit compila i arrenca La bisecció és neta, sense skip
Commits petits i atòmics El culpable és un diff de 10 línies, no de 800
Un canvi lògic per commit El diff assenyala la causa directament
Missatges descriptius Entens el perquè del canvi culpable a l'instant
Commits gegants "feina del divendres" Trobes el commit... i continues sense saber quina línia
Commits que no compilen skip continu, resultat difús o inconcloent
Branques fusionades amb història bruta Soroll: es resol amb --first-parent

Dit d'una altra manera: el rebase interactiu de la lliçó 05-02 no és cosmètica. Quan consolides els fixup!, divideixes un commit gegant i t'assegures que cadascun deixa el projecte en un estat funcional, el que estàs construint és un historial bisecable. La factura de no fer-ho es paga el dia que cal trobar una regressió de fa dos mesos.

I amb --exec (lliçó 05-02) pots comprovar per endavant que tota una branca és bisecable abans d'integrar-la:

git rebase -i --exec "node --check app.js" origin/main

Git aplica cada commit i executa la comprovació a cadascun. Si algun falla, t'atura allà.

Tot això —commits atòmics, historial que compila, què consolidar abans d'integrar— és el tema de la lliçó 08-02: Mantenint un Historial Net. bisect és la millor justificació pràctica que existeix per a aquestes pràctiques: converteix una virtut abstracta en minuts de feina estalviats.

Una última nota d'abast: bisect respon a "quin commit va canviar aquest comportament?". No respon a "per què el meu repositori està en un estat estrany?" ni "què està fent Git per sota?". Aquest diagnòstic —reflog, fsck, GIT_TRACE i companyia— és el terreny de les lliçons del mòdul 9, i en particular de la 09-06: Tècniques Avançades de Depuració.

Errors Habituals i Consells

Error 1: començar amb l'arbre de treball brut. bisect fa checkout a cada pas i fallarà o arrossegarà els teus canvis entre commits. Confirma o fes git stash abans.

Error 2: oblidar el git bisect reset. Et quedes en detached HEAD sobre un commit de juny. Tot commit que hi facis quedarà orfe.

Error 3: invertir good i bad en començar. L'ordre de la drecera és git bisect start <DOLENT> <BO>. Si l'inverteixes, Git buscarà en el sentit contrari i no trobarà res coherent.

Error 4: marcar bad un commit que està trencat per un altre motiu. Fes servir skip. Mentir a bisect produeix un culpable equivocat amb tota l'aparença de ser correcte.

Error 5: oblidar l'exit 125 a l'script. Sense ell, els commits que no compilen compten com a dolents i el resultat és brossa.

Error 6: un script no executable o amb la ruta malament. Codis 126/127: tot surt "dolent" i el culpable és el primer commit del rang. Prova l'script a mà abans.

Error 7: deixar l'script dins del repositori. Als commits antics pot no existir. Copia'l a /tmp i executa'l des de fora.

Error 8: una prova no determinista. Si falla una de cada cinc vegades, bisect et donarà una resposta diferent cada cop. Assegura el determinisme abans d'automatitzar.

Consell 1: tria el "bo" més recent que puguis. Començar a v1.0.0 en lloc del primer commit del projecte estalvia passos. Les etiquetes (lliçó 05-05) són perfectes per a això.

Consell 2: desa sempre el git bisect log. Una sessió llarga és feina valuosa. Un fitxer de text et permet reprendre-la o corregir un marcatge erroni.

Consell 3: primer --first-parent, després dins de la branca. Ràpid per localitzar la fusió, precís per localitzar el commit.

Consell 4: bisect run amb npm test sovint n'hi ha prou. Si ja tens proves, no escriguis res de nou.

Consell 5: quan trobis el culpable, no el rebentis sense més. Mira'l, entén-lo i decideix: git revert si està publicat (lliçó 05-06), o una correcció cap endavant si el canvi en si era bo i només faltava un detall —com en el cas del Bruno, on la delegació d'esdeveniments era correcta i només faltava closest().

Exercicis

Exercici 1: bisecció manual

Crea un repositori amb 15 commits on el fitxer valor.txt conté 10, i a partir del commit número 9 passa a contenir 99 (la "fallada"). Després:

  1. Inicia una bisecció amb main com a dolent i el primer commit com a bo.
  2. A cada pas, comprova cat valor.txt i marca good o bad.
  3. Comprova que Git identifica exactament el commit 9.
  4. Quants passos has necessitat? Coincideix amb log₂(14)?

Exercici 2: bisecció automàtica amb run

Amb el mateix repositori de l'exercici 1:

  1. Escriu un script a /tmp que retorni 0 si valor.txt conté 10 i 1 si no.
  2. Prova l'script a mà en un commit bo i en un de dolent abans de fer-lo servir.
  3. Llança git bisect run i comprova que troba el mateix commit.
  4. Afegeix al repositori un commit intermedi en què valor.txt no existeixi, i fes que l'script retorni 125 en aquest cas. Comprova que la bisecció continua funcionant.

Exercici 3: termes personalitzats

Crea un repositori de 10 commits on estat.txt conté trencat i a partir del commit 7 passa a contenir arreglat. Fent servir --term-old i --term-new, troba el primer commit arreglat. Escriu també el bisect run corresponent.

Solucions

Solució 1:

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

for i in $(seq 1 15); do
  if [ "$i" -lt 9 ]; then echo "10" > valor.txt; else echo "99" > valor.txt; fi
  echo "linia $i" >> registre.txt
  git add .
  git commit -q -m "Commit numero $i"
done

git log --oneline | tail -1        # el primer commit (bo)
2a7c9e1 Commit numero 1
git bisect start main 2a7c9e1
Bisecting: 6 revisions left to test after this (roughly 3 steps)
[8f3d2c7] Commit numero 8
cat valor.txt          # 10  → bo
git bisect good
Bisecting: 3 revisions left to test after this (roughly 2 steps)
[5c9a4e2] Commit numero 12
cat valor.txt          # 99  → dolent
git bisect bad
Bisecting: 1 revision left to test after this (roughly 1 step)
[1e6b8d3] Commit numero 10
cat valor.txt          # 99  → dolent
git bisect bad
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[7b2f5a9] Commit numero 9
cat valor.txt          # 99  → dolent
git bisect bad
7b2f5a9... is the first bad commit
commit 7b2f5a9...
    Commit numero 9
 valor.txt | 2 +-
git bisect reset

Quatre proves per a 14 candidats. log₂(14) ≈ 3,8, que arrodonit cap amunt són 4. Exacte.

Solució 2:

cat > /tmp/provar-valor.sh <<'FI'
#!/usr/bin/env bash
# 0 = bo, 1 = dolent, 125 = no avaluable
[ -f valor.txt ] || { echo "→ sense valor.txt: no avaluable"; exit 125; }
valor=$(cat valor.txt)
echo "→ valor = $valor"
[ "$valor" = "10" ]
FI
chmod +x /tmp/provar-valor.sh

Comprovació prèvia, que mai s'ha de saltar:

cd /tmp/practica-bisect
git switch --detach $(git log --oneline | tail -1 | cut -d' ' -f1)
/tmp/provar-valor.sh; echo "codi: $?"
→ valor = 10
codi: 0
git switch main
/tmp/provar-valor.sh; echo "codi: $?"
→ valor = 99
codi: 1
git bisect start main $(git log --oneline | tail -1 | cut -d' ' -f1)
git bisect run /tmp/provar-valor.sh
running '/tmp/provar-valor.sh'
→ valor = 10
Bisecting: 3 revisions left to test after this (roughly 2 steps)
running '/tmp/provar-valor.sh'
→ valor = 99
...
7b2f5a9... is the first bad commit
    Commit numero 9
bisect run success
git bisect reset

El commit no avaluable:

git switch -c amb-forat main
git rm -q valor.txt && git commit -q -m "Treu temporalment valor.txt"
echo "99" > valor.txt && git add . && git commit -q -m "Restaura valor.txt"

git bisect start amb-forat $(git log --oneline main | tail -1 | cut -d' ' -f1)
git bisect run /tmp/provar-valor.sh

Quan la bisecció arriba al commit sense valor.txt, l'script retorna 125 i Git el salta:

running '/tmp/provar-valor.sh'
→ sense valor.txt: no avaluable
Bisecting: 1 revision left to test after this (roughly 1 step)

El culpable continua sent el commit 9: el 125 ha evitat que un commit inavaluable falsegés el resultat.

Solució 3:

mkdir /tmp/practica-bisect-terms && cd /tmp/practica-bisect-terms
git init -b main

for i in $(seq 1 10); do
  if [ "$i" -lt 7 ]; then echo "trencat" > estat.txt; else echo "arreglat" > estat.txt; fi
  echo "linia $i" >> registre.txt
  git add . && git commit -q -m "Commit numero $i"
done

primer=$(git log --oneline | tail -1 | cut -d' ' -f1)
git bisect start --term-old=trencat --term-new=arreglat
git bisect trencat "$primer"
git bisect arreglat main
Bisecting: 4 revisions left to test after this (roughly 2 steps)
[9c4e7b2] Commit numero 5
cat estat.txt          # trencat
git bisect trencat
cat estat.txt          # arreglat
git bisect arreglat
cat estat.txt          # arreglat
git bisect arreglat
4f8a2e6... is the first arreglat commit
commit 4f8a2e6...
    Commit numero 7

Amb automatització:

git bisect reset

cat > /tmp/provar-estat.sh <<'FI'
#!/usr/bin/env bash
[ -f estat.txt ] || exit 125
grep -q '^trencat$' estat.txt   # 0 = estat antic (trencat), 1 = nou
FI
chmod +x /tmp/provar-estat.sh

git bisect start --term-old=trencat --term-new=arreglat
git bisect trencat "$primer"
git bisect arreglat main
git bisect run /tmp/provar-estat.sh
git bisect reset

L'important: encara que els termes es diguin trencat i arreglat, l'script continua retornant 0 per a l'estat antic i 1 per al nou. bisect no interpreta les paraules; només busca la frontera.

Conclusió

git bisect converteix una cerca desesperada en un procediment mecànic i acotat. L'essencial:

  • És una cerca binària sobre l'historial: cada prova descarta la meitat dels candidats, així que calen log₂(n) proves. 214 commits són 8 proves; un milió, vint.
  • La sessió manual és startbadgood → bucle de proves → reset. El reset final no és opcional: sense ell et quedes en detached HEAD.
  • git bisect skip és la resposta honesta a un commit que no es pot avaluar. Mentir marcant good o bad produeix un culpable fals.
  • git bisect log i replay salven sessions llargues i permeten corregir un marcatge erroni sense començar de zero.
  • git bisect run <ordre> automatitza tot el procés. El contracte són els codis de sortida: 0 bo, 1–124 dolent, 125 saltar, 128+ avortar. El 125 és el que separa una automatització fiable d'una que inventa respostes.
  • Prova sempre l'script a mà en un commit bo i en un de dolent abans de llançar bisect run, i treu-lo de l'arbre de treball (/tmp) perquè existeixi a tots els commits.
  • --first-parent limita la cerca a la línia principal: menys soroll i menys skip. La tàctica de dues fases —primer la fusió, després dins de la branca— combina velocitat i precisió.
  • git bisect terms generalitza l'eina a qualsevol canvi d'estat monòton: quan es va arreglar alguna cosa, quan va començar a anar lent, quan va engreixar el paquet.
  • I la conclusió de fons: un historial de commits petits que compilen és un historial bisecable. La lliçó 08-02 ho desenvolupa; bisect és la raó per la qual importa.

La Carla ja té el commit culpable i el diff exacte. Però en mirar app.js per arreglar-lo es troba una altra cosa: vint línies més avall hi ha una funció de normalització de text que ningú sap per què hi és, amb un replace de caràcters estranys que sembla arbitrari. Ningú de l'equip recorda haver-la escrit. Esborrar-la sembla temptador... i probablement trencaria alguna cosa.

Abans de tocar codi que no entens, cal saber qui va escriure cada línia, quan i en quin commit, per poder llegir el missatge que n'explica el perquè. Aquesta és l'eina de la lliçó 06-03: Git Blame.

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