La lliçó anterior va tancar el desfer local: reset, restore, clean, i una taula de decisió que acabava amb una fila remetent aquí. Aquesta lliçó s'ocupa de tot el que passa entre el teu repositori i el servidor, i tanca la promesa que va quedar oberta a la lliçó 04-05, on vam veure el rebuig non-fast-forward i només vam donar la sortida bàsica.

El problema té un nom i una definició precisa: divergència. I té una fama de complicat que no es mereix, perquè tan bon punt es dibuixa el graf la solució sol ser evident. El que confon és que el mateix estat es presenta amb quatre missatges diferents segons com el descobreixis: un status que diu "have diverged", un pull que es nega a decidir, un push rebutjat, o un company avisant que li han desaparegut commits.

Veurem que els quatre són la mateixa situació, que hi ha exactament tres maneres de sortir-ne, i que l'elecció entre les tres depèn d'una sola pregunta: de qui és aquesta branca?

I al final, el cas seriós: què fer quan qui va reescriure l'historial publicat va ser un altre, i tu tens dos dies de feina recolzats en una base que ja no existeix.

Contingut

  1. Què significa exactament "han divergit"
  2. Mesurar la divergència
  3. Els quatre missatges de la mateixa situació
  4. Les tres polítiques de pull i quina triar
  5. Com s'arriba a una divergència
  6. El push rebutjat: diagnòstic i sortides
  7. --force-with-lease i per què de vegades no protegeix
  8. Quan el remot va reescriure i tu tens feina a sobre
  9. Recuperar la branca remota anterior amb origin/branca@{1}
  10. Històries no relacionades: --allow-unrelated-histories
  11. Coordinació: què cal dir i quan

  1. Què significa exactament "han divergit"

Dues branques han divergit quan cadascuna té commits que l'altra no té. Formalment: existeix un ancestre comú, i des d'ell surten dos camins diferents.

flowchart LR
    A["e91d4a8"] --> B["4f8a2e6<br/>(ancestre comú)"]
    B --> C["b52c9d1"] --> D["7d3a8f4"]
    B --> E["9c4e7b2"] --> F["3e7f1a8"] --> G["8a1f6c3"]
    L(["main (local)"]) -.-> D
    R(["origin/main"]) -.-> G

El teu main té dos commits que el servidor no té. El servidor en té tres que tu no tens. L'ancestre comú és 4f8a2e6.

Compara-ho amb els dos casos que no són divergència:

Situació Graf Què passa
Al dia Local i remot al mateix commit Res a fer
Endarrerit El remot va avançar, tu no git pull fa fast-forward: cap problema
Avançat Tu vas avançar, el remot no git push fa fast-forward: cap problema
Divergits Tots dos van avançar des de l'ancestre Cal decidir

La paraula clau és decidir. Una divergència no és un error: és una situació en què Git no pot saber què vols i es nega a inventar-s'ho. Tot el que segueix són maneres de prendre aquesta decisió.

Recuperant la lliçó 03-01: l'ancestre comú és el que Git calcula amb git merge-base, i és la base de la fusió a tres bandes. El pots veure directament:

git merge-base main origin/main
git merge-base --all main origin/main     # si hi ha diversos candidats
4f8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9a

  1. Mesurar la divergència

Abans de resoldre, mesura. Aquestes ordres no modifiquen res.

# El primer de tot, SEMPRE: portar l'estat real del servidor
git fetch origin

fetch actualitza origin/main sense tocar el teu main (lliçó 04-04). Sense això, estàs raonant sobre una foto vella del servidor.

El recompte

git rev-list --left-right --count main...origin/main
2	3

Els tres punts són fonamentals: A...B és la diferència simètrica, els commits que són en una branca però no en l'altra. --left-right els separa per costat.

Sortida Significat
0 0 Idèntiques
0 N Estàs endarrerit N commits. pull és fast-forward
N 0 Estàs avançat N commits. push és fast-forward
N M Divergides. Cal decidir

Amb la branca de seguiment configurada (lliçó 04-06), n'hi ha prou amb:

git rev-list --left-right --count @{u}...HEAD
git status -sb
## main...origin/main [ahead 2, behind 3]

Veure què hi ha a cada costat

Això és el que de debò permet decidir:

# Els meus commits que el servidor no té
git log --oneline main ^origin/main
# equivalent i més còmode:
git log --oneline origin/main..main
7d3a8f4 GT-241 estils de l'indicador de pendents
b52c9d1 GT-241 calcula el comptador sobre la llista completa
# Els del servidor que jo no tinc
git log --oneline main..origin/main
8a1f6c3 GT-238 corregeix el focus després d'esborrar
3e7f1a8 GT-244 documenta la instal·lació al README
9c4e7b2 GT-244 afegeix l'script d'arrencada
# Els dos costats alhora, amb marca de costat
git log --oneline --left-right --graph main...origin/main
< 7d3a8f4 GT-241 estils de l'indicador de pendents
< b52c9d1 GT-241 calcula el comptador sobre la llista completa
> 8a1f6c3 GT-238 corregeix el focus després d'esborrar
> 3e7f1a8 GT-244 documenta la instal·lació al README
> 9c4e7b2 GT-244 afegeix l'script d'arrencada

< és el costat esquerre (el teu main), > el dret (origin/main).

I la pregunta que decideix si hi haurà conflictes:

# Toquen els mateixos fitxers?
git diff --stat origin/main...main       # el que aporto jo
git diff --stat main...origin/main       # el que aporten ells

Si els conjunts de fitxers no se solapen, la integració serà neta.

Un àlies que val la pena

git config --global alias.div '!f() {
  git fetch -q ${1:-origin};
  echo "--- estat ---"; git status -sb | head -1;
  echo "--- meus (no són al remot) ---"; git log --oneline @{u}..HEAD;
  echo "--- seus (no els tinc) ---"; git log --oneline HEAD..@{u};
}; f'
git div

  1. Els quatre missatges de la mateixa situació

Segons com arribis a la divergència, Git te l'explica de manera diferent. Reconèixer-les totes és mitja lliçó.

A. A git status

On branch main
Your branch and 'origin/main' have diverged,
and have 2 and 3 different commits each, respectively.
  (use "git pull" if you want to integrate the remote branch with yours)

És el diagnòstic pur. Els números coincideixen amb rev-list --left-right --count.

B. A git pull sense política configurada

Des de Git 2.27, pull es nega a triar per tu:

hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint:
fatal: Need to specify how to reconcile divergent branches.

Això és bo. Abans, pull feia una fusió silenciosa i omplia l'historial de commits "Merge branch 'main' of git.exemple.cat..." que ningú no havia demanat. Ara t'obliga a tenir una política, i això és l'apartat 4.

C. A git pull amb pull.ff only

fatal: Not possible to fast-forward, aborting.

És la mateixa informació amb menys paraules: hi ha divergència i la teva política diu "no decideixis per mi". No és una fallada (lliçó 04-04, error 3).

D. A git push

 ! [rejected]        main -> main (non-fast-forward)
error: failed to push some refs to 'git.exemple.cat:equip/gestor-tasques.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.

O, en la seva variant més alarmant:

 ! [rejected]        main -> main (fetch first)

Totes dues diuen el mateix: el servidor té commits que tu no tens, i acceptar el teu enviament els deixaria inassolibles. És una protecció del servidor, no una fallada teva. És l'apartat 6.

  1. Les tres polítiques de pull i quina triar

git pull és fetch + integració (lliçó 04-04). La política decideix quina integració.

Política Configuració Què fa Resultat
Merge pull.rebase false git merge origin/main Commit de fusió; historial amb bifurcacions
Rebase pull.rebase true git rebase origin/main Els teus commits es reapliquen a sobre; historial lineal
Només fast-forward pull.ff only Avorta si hi ha divergència Cap: decideixes tu, a mà

Visualment, sobre el mateix punt de partida:

flowchart TD
    subgraph MERGE["pull.rebase false (merge)"]
        m1["4f8a2e6"] --> m2["b52c9d1 (meu)"] --> m3["7d3a8f4 (meu)"] --> mm["Merge"]
        m1 --> m4["9c4e7b2"] --> m5["3e7f1a8"] --> m6["8a1f6c3"] --> mm
    end
    subgraph REBASE["pull.rebase true"]
        r1["4f8a2e6"] --> r4["9c4e7b2"] --> r5["3e7f1a8"] --> r6["8a1f6c3"] --> r2["b52c9d1' (meu, hash nou)"] --> r3["7d3a8f4' (meu, hash nou)"]
    end

Quina triar, segons el flux del mòdul 7

Context Política recomanada Per què
La teva branca de funcionalitat personal (GT-241) rebase És teva, ningú més no la té; l'historial queda net per a la revisió
main a GitHub Flow / Trunk Based (07-04, 07-05) rebase o ff-only A main mai no hauries de tenir commits locals; si en tens, és un avís
Branca compartida de debò (dues persones treballant alhora) merge Rebasar una branca que un altre té és reescriure historial publicat
develop a Git Flow (07-03) merge És una branca de llarga vida i compartida
Quan no n'estàs segur ff-only T'obliga a mirar abans de decidir. L'opció més didàctica

La configuració recomanada per a la majoria de la gent:

# Per defecte: no decideixis per mi
git config --global pull.ff only

# I a les branques de funcionalitat, rebase explícit quan toqui
git pull --rebase

O, si el teu equip treballa amb branques curtes i vol historial lineal (que és el que fa l'Ana a gestor-tasques):

git config --global pull.rebase true
git config --global rebase.autoStash true   # aparta els canvis sense confirmar i els torna

rebase.autoStash evita el "cannot pull with rebase: You have unstaged changes", que és la queixa més freqüent contra pull --rebase.

La regla d'or continua vigent (lliçó 05-01): pull --rebase reescriu els teus commits locals, que encara no has publicat. Això és legítim. El que no s'ha de fer mai és rebasar commits que ja són al servidor i altres poden tenir.

I un ajust que evita un error clàssic:

git config --global pull.rebase.autoSquash true

Si tenies commits fixup! pendents (lliçó 05-02), no s'aixafaran per sorpresa durant un pull. Comprova-ho a la teva configuració: si el tens activat sense saber-ho, un pull --rebase et pot reorganitzar commits.

  1. Com s'arriba a una divergència

Entendre la causa importa, perquè determina quina de les sortides és correcta.

Causa 1: la normal i sana

L'Ana va confirmar en local mentre el Bruno publicava el seu. Ningú no ha fet res malament. És el funcionament normal d'un sistema distribuït amb diverses persones.

Sortida: integrar (merge o rebase, segons la política) i enviar. Sense drama.

Causa 2: un --amend després de publicar

La Carla va publicar GT-247, es va adonar d'una errada al missatge i va executar git commit --amend.

git log --oneline -1
git push
b52c9d1 GT-247 mostra el nombre de tasques pendents
 ! [rejected]        GT-247 -> GT-247 (non-fast-forward)

Què ha passat. --amend no modifica el commit: en crea un de nou amb un hash diferent i mou la branca (lliçó 09-01, apartat 5.3). El commit original continua al servidor. Local i remot tenen ara un commit cadascun que l'altre no té: divergència d'1 i 1.

git rev-list --left-right --count GT-247...origin/GT-247
1	1

Sortida: si la branca és només seva, push --force-with-lease. Si no, cal integrar, i el resultat és lleig (el commit apareixeria dues vegades).

Causa 3: un reset seguit de feina nova

El Bruno va fer git reset --hard HEAD~2 sobre una branca publicada per "treure dos commits", i després va treballar dues hores més.

Local:  A → B → X → Y          (X, Y són la feina nova)
Remot:  A → B → C → D          (C, D són els commits que va treure)

Divergència de 2 i 2. I aquí hi ha una decisió de fons: els commits C i D han de desaparèixer del projecte, o no?

Sortida: si han de desaparèixer i la branca és seva, --force-with-lease. Si la branca és compartida, la resposta correcta era git revert (lliçó 05-06) i no reset; ara toca integrar i revertir.

Causa 4: algú va reescriure l'historial publicat

El Diego va fer un rebase -i sobre main per "netejar l'historial" i va forçar el push. Tots els qui tenien main baixat es troben amb una divergència que no han provocat.

Local:  A → B → C → D          (el que tenies, correcte)
Remot:  A → B' → C' → D'       (els mateixos canvis, altres hashos)

És el cas seriós, i té apartat propi: el 8.

Causa 5: git pull sobre una branca en què treballen dues persones

L'Ana i la Carla treballen alhora a GT-241. Totes dues fan pull --rebase. Cada rebase reescriu els commits de l'altra. El resultat és una espiral de commits duplicats que es resol malament.

Sortida: en branques realment compartides, pull.rebase false. La política ha de ser per branca, no per costum.

  1. El push rebutjat: diagnòstic i sortides

Aquest és el tancament del que es va prometre a la lliçó 04-05.

Diagnòstic complet, en quatre ordres

# 1. La foto real del servidor
git fetch origin

# 2. Quant he divergit?
git rev-list --left-right --count HEAD...@{u}
2	3
# 3. Què hi ha al servidor que jo no tingui? És d'una altra persona?
git log --oneline --format='%h %an  %s' HEAD..@{u}
8a1f6c3 Bruno Salas  GT-238 corregeix el focus després d'esborrar
3e7f1a8 Bruno Salas  GT-244 documenta la instal·lació
9c4e7b2 Carla Vidal  GT-244 afegeix l'script d'arrencada
# 4. I què tinc jo?
git log --oneline @{u}..HEAD

Amb aquestes quatre respostes ja saps quina sortida correspon.

L'arbre de decisió

flowchart TD
    Q1{"Hi ha commits al remot<br/>que jo no tingui?"}
    Q1 -->|No| R0["No és divergència:<br/>simplement fes push"]
    Q1 -->|Sí| Q2{"Són d'una altra persona,<br/>o commits legítims meus<br/>que vull conservar?"}
    Q2 -->|Sí| R1["INTEGRAR:<br/>pull --rebase (branca pròpia)<br/>o pull --no-rebase (compartida)"]
    Q2 -->|No: són versions velles<br/>dels meus propis commits| Q3{"Aquesta branca la fa servir<br/>algú més?"}
    Q3 -->|Sí| R2["INTEGRAR igualment,<br/>o coordinar abans de forçar"]
    Q3 -->|No, és només meva| R3["push --force-with-lease"]

Sortida A: integrar i reenviar (el cas normal)

git fetch origin
git rebase origin/main          # o: git merge origin/main
# ...resoldre conflictes si n'hi ha (lliçó 03-05)...
git push

Amb pull configurat:

git pull --rebase
git push

Si apareixen conflictes durant el rebase, la mecànica és la de la lliçó 05-01, inclosa la inversió d'ours/theirs. I si t'angoixes:

git rebase --abort      # tornes exactament a l'estat anterior

Sortida B: forçar, quan la branca és teva

Només si es compleixen les tres condicions:

  1. La branca és de funcionalitat i ningú més no la fa servir.
  2. El que hi ha al servidor són versions antigues dels teus propis commits.
  3. Has comprovat el punt 3 del diagnòstic i no hi ha commits d'una altra persona.
git push --force-with-lease
 + b52c9d1...7d3a8f4 GT-241 -> GT-241 (forced update)

El + al davant indica que ha estat un enviament forçat.

Mai --force a seques. La diferència és l'apartat 7.

Sortida C: la que gairebé ningú no considera

De vegades la resposta correcta és no forçar i no integrar, sinó publicar en un altre lloc:

git push origin GT-241:GT-241-v2

Especialment útil si la branca té una pull request oberta amb comentaris de revisió (lliçó 07-02): forçar pot deixar els comentaris orfes. Publicar una branca nova ho conserva tot i permet comparar amb git range-diff (lliçó 07-02).

  1. --force-with-lease i per què de vegades no protegeix

Recordant la lliçó 04-05: --force-with-lease només força si el servidor és on tu creus que és. Compara la punta remota real amb la teva còpia local de la branca de seguiment (refs/remotes/origin/GT-241).

git push --force-with-lease
 ! [rejected]        GT-241 -> GT-241 (stale info)
error: failed to push some refs

Aquest stale info significa: "el servidor no és on el teu origin/GT-241 diu; algú ha publicat alguna cosa des del teu últim fetch". És exactament la protecció funcionant: t'ha evitat destruir la feina d'un altre.

El forat: el fetch que anul·la la protecció

I aquí hi ha el detall que gairebé ningú no coneix, i que cal entendre bé:

Si fas git fetch just abans del --force-with-lease, la protecció desapareix.

Perquè fetch actualitza origin/GT-241 amb el que hi ha al servidor ara mateix, inclosos els commits nous del teu company. El teu "arrendament" (lease) passa a coincidir amb la realitat, la comprovació passa, i forces a sobre de feina que no has vist mai.

# Seqüència perillosa
git fetch                       # ara origin/GT-241 inclou el commit del Bruno
git push --force-with-lease     # la comprovació passa... i destrueix el commit del Bruno

El pitjor és que és una seqüència raonable: "actualitzaré abans de forçar" sona a bona pràctica. I molts IDE fan fetch automàticament en segon pla, així que et pot passar sense que tu escriguis l'ordre.

La solució: --force-if-includes

Git 2.30 va afegir l'opció que arregla el forat:

git push --force-with-lease --force-if-includes

--force-if-includes comprova, a més, que els commits remots que sobreescriuràs estan incorporats al teu historial local, mirant el teu reflog. És a dir: exigeix que hagis vist i integrat el que estàs a punt de reemplaçar, no només que ho hagis baixat.

Opció Què comprova Protegeix del company
--force Res No
--force-with-lease Que origin/branca local == servidor Sí, tret que hagis fet fetch
--force-with-lease --force-if-includes A més, que hagis integrat el remot

Configura-ho com a comportament per defecte:

git config --global push.useForceIfIncludes true

Amb això, cada --force-with-lease que executis porta la comprovació extra automàticament. És una línia de configuració que elimina una classe sencera d'accidents.

I la variant explícita, quan vols ser absolutament precís:

# Força només si la punta remota és EXACTAMENT aquest hash
git push --force-with-lease=GT-241:b52c9d1e4a7f3c8b6d2e9a5f1c7b3d8e4a6f2c9b

Verbós, però no hi ha manera d'equivocar-se.

  1. Quan el remot va reescriure i tu tens feina a sobre

Aquest és el cas seriós, i el que produeix més pànic. Es mereix el procediment complet.

La situació. El Diego va fer un rebase -i sobre main per netejar l'historial i va forçar el push. L'Ana tenia main baixat i, a sobre, tres commits de GT-241 que encara no havia publicat.

El primer: no has perdut res. Els teus commits continuen al teu repositori, íntegres. I els commits antics de main també els tens tu en local, encara que el servidor ja no els tingui. De fet, el teu clon és ara l'única còpia de la versió anterior, i això et dona una posició sòlida.

git fetch origin
git status -sb
## GT-241...origin/GT-241 [ahead 3, behind 7]

Pas 1: entendre què ha passat

git log --oneline --graph --all -20
* 9c4e7b2 (HEAD -> GT-241) GT-241 estils de l'indicador
* 3e7f1a8 GT-241 calcula el comptador
* 8a1f6c3 GT-241 base del filtre
* 7d3a8f4 GT-238 corregeix el focus després d'esborrar     <- el meu main vell
* b52c9d1 GT-244 documenta la instal·lació
* 4f8a2e6 chore: configuració d'eslint
| * 2f8c6e1 (origin/main) GT-238 corregeix el focus després d'esborrar   <- el main nou
| * 7a4d9b3 GT-244 documenta la instal·lació
| * 5c1e8f2 chore: configuració d'eslint
|/
* 1e6f2c8 chore: commit inicial

Els missatges es repeteixen a les dues branques, amb hashos diferents. Aquest és el patró inequívoc d'un historial reescrit. Confirma-ho:

git range-diff origin/main...7d3a8f4

git range-diff (lliçó 07-02) compara dues sèries de commits i diu si el contingut és el mateix:

1:  5c1e8f2 = 1:  4f8a2e6 chore: configuració d'eslint
2:  7a4d9b3 = 2:  b52c9d1 GT-244 documenta la instal·lació
3:  2f8c6e1 ! 3:  7d3a8f4 GT-238 corregeix el focus després d'esborrar
    @@ app.js
     - camp.focus();
     + campNovaTasca.focus();

Els dos primers són idèntics (=); el tercer va canviar (!) i t'ensenya exactament què. Aquesta és la informació que necessites abans de decidir res.

Pas 2: la còpia

git branch copia-GT-241
git branch copia-main-antic 7d3a8f4

Dos segons. A partir d'aquí, res del que facis no és irreversible.

Pas 3: trasplantar els teus commits amb rebase --onto

L'ordre que resol exactament això és el --onto de la lliçó 05-01. La seva forma és:

git rebase --onto <nova-base> <base-antiga> <branca>

que es llegeix: "agafa els commits de <branca> que siguin després de <base-antiga> i reaplica'ls sobre <nova-base>".

En el nostre cas:

  • Nova base: origin/main (l'historial nou).
  • Base antiga: 7d3a8f4 (el main vell del qual penjava la teva branca).
  • Branca: GT-241.
git rebase --onto origin/main 7d3a8f4 GT-241
Successfully rebased and updated refs/heads/GT-241.
git log --oneline --graph -8
* 4b8e2c9 (HEAD -> GT-241) GT-241 estils de l'indicador
* 8f2d6a1 GT-241 calcula el comptador
* 6e2b9c7 GT-241 base del filtre
* 2f8c6e1 (origin/main) GT-238 corregeix el focus després d'esborrar
* 7a4d9b3 GT-244 documenta la instal·lació
* 5c1e8f2 chore: configuració d'eslint
* 1e6f2c8 chore: commit inicial

Els teus tres commits, amb hashos nous, sobre la base nova. Ni un commit duplicat.

Com trobar la base antiga si no la tens anotada:

# El reflog de la teva pròpia branca diu d'on va sortir
git reflog show GT-241 | tail -5

# O l'ancestre comú entre la teva branca i el main vell del reflog remot
git merge-base GT-241 origin/main@{1}

Pas 4: posar el main local al seu lloc

git switch main
git status -sb
## main...origin/main [ahead 3, behind 3]

El teu main local encara té la versió vella. Com que no tenies res propi a main (tota la teva feina era a GT-241), simplement alinea'l:

git reset --hard origin/main

Si que tenies commits propis a main, no facis això: treu-los primer a una branca (git branch els-meus-commits-main) i trasplanta'ls amb el mateix --onto.

Pas 5: verificar abans d'enviar

git switch GT-241
git range-diff copia-GT-241...GT-241
1:  8a1f6c3 = 1:  6e2b9c7 GT-241 base del filtre
2:  3e7f1a8 = 2:  8f2d6a1 GT-241 calcula el comptador
3:  9c4e7b2 = 3:  4b8e2c9 GT-241 estils de l'indicador

Tres =: el contingut dels teus commits és idèntic al d'abans del trasplantament. No s'ha perdut ni alterat res. Ara pots esborrar la còpia amb tranquil·litat:

git branch -D copia-GT-241 copia-main-antic

I executa les proves abans de publicar: el rebase ha reaplicat el teu codi sobre una base diferent, i encara que no hi hagi hagut conflictes, hi pot haver incompatibilitats semàntiques.

El cas en què la teva feina SÍ que estava publicada

Si els teus commits de GT-241 ja eren al servidor, després del rebase la teva branca diverge d'origin/GT-241. Com que GT-241 és teva:

git push --force-with-lease --force-if-includes

El resum del procediment

Pas Ordre Per a què
1 git fetch origin La foto real
2 git log --graph --all + git range-diff Entendre què van reescriure
3 git branch copia-<branca> Xarxa de seguretat
4 git rebase --onto origin/main <base-vella> <branca> Trasplantar
5 git switch main && git reset --hard origin/main Alinear main
6 git range-diff copia-<branca>...<branca> Verificar
7 Executar les proves Verificar de debò
8 git push --force-with-lease --force-if-includes Publicar (si escau)

  1. Recuperar la branca remota anterior amb origin/branca@{1}

Un detall que salva situacions i que gairebé ningú no coneix: les branques de seguiment remotes també tenen reflog.

git reflog show origin/main
2f8c6e1 refs/remotes/origin/main@{0}: fetch: forced-update
7d3a8f4 refs/remotes/origin/main@{1}: fetch: fast-forward
b52c9d1 refs/remotes/origin/main@{2}: fetch: fast-forward

Dues coses valuosíssimes en aquesta sortida:

  1. forced-update és la prova que algú va reescriure l'historial publicat. Si dubtaves de què va passar, allà tens l'evidència amb la seva data.
  2. origin/main@{1} és on era el main del servidor abans de la reescriptura.
# Veure l'historial anterior
git log --oneline origin/main@{1} -10

# Crear una branca sobre ell per poder treballar
git branch main-abans-del-rebase origin/main@{1}

# Comparar les dues versions
git range-diff origin/main@{1}...origin/main

Això és el que permet reconstruir el que el servidor tenia abans, i és la raó per la qual, quan algú força i destrueix feina, qualsevol company que tingués la branca baixada la pot tornar:

# Tornar el main del servidor a com estava (amb permisos i coordinació)
git push --force-with-lease origin main-abans-del-rebase:main

I també amb dates:

git log --oneline origin/main@{yesterday}
git log --oneline origin/main@{2.days.ago}

Aquest és exactament el mecanisme del reflog, el tractament complet del qual —inclosa la sintaxi @{n} davant de @{temps} i els límits de caducitat— és la lliçó 09-04.

  1. Històries no relacionades: --allow-unrelated-histories

git pull origin main
fatal: refusing to merge unrelated histories

Què significa. Les dues branques no tenen cap ancestre comú. No és una divergència: és que són dos arbres genealògics completament independents, cadascun amb el seu propi commit arrel.

flowchart LR
    subgraph H1["Història local"]
        a1["1e6f2c8 (arrel)"] --> a2["4f8a2e6"] --> a3["b52c9d1"]
    end
    subgraph H2["Història remota"]
        b1["9a3c7d1 (arrel)"] --> b2["2f8c6e1"]
    end
git merge-base main origin/main
echo $?           # 1: no hi ha ancestre comú

Com s'hi arriba

Causa Situació típica
Vas crear el repositori local abans de clonar git init + commits, i després vas afegir un origin que ja tenia un README inicial
El servidor va inicialitzar el repositori La plataforma va crear el repo amb README.md i .gitignore, tu tenies el projecte en local
Algú va reescriure l'historial complet git filter-repo (08-05) sense --force, o una reescriptura des de l'arrel
push --force d'un repositori equivocat Es va publicar un altre projecte a sobre. Això és un incident, no un cas normal
Fusionar dos projectes a propòsit Absorbir un repositori dins d'un altre

La sortida

Abans de res, comprova que és el cas benigne:

# Les dues arrels
git log --oneline --max-parents=0 main
git log --oneline --max-parents=0 origin/main

# Què hi ha al remot? Si són 47 commits d'un altre projecte, ATURA'T.
git log --oneline origin/main | head -20

Si el remot només té el commit inicial de la plataforma:

git pull origin main --allow-unrelated-histories

Probablement entrarà en conflicte a README.md: resol-lo amb normalitat (lliçó 03-05) i confirma.

Si el remot té el projecte de debò i la teva història local és la sobrera:

git branch la-meva-historia-local      # còpia
git fetch origin
git reset --hard origin/main           # adoptar la del servidor

I si algú ha publicat un altre projecte a sobre del vostre, això és un incident: no fusionis res, avisa l'equip, i fes servir origin/main@{1} de l'apartat 9 (o el clon de qualsevol company) per restaurar. La lliçó 09-05 tracta l'ús d'altres clons com a còpia de seguretat.

No executis mai --allow-unrelated-histories "perquè Git es queixa". Aquesta negativa és una comprovació de seny molt útil: Git t'està dient que aquestes dues coses no tenen res a veure. Esbrina per què abans de saltar-te-la.

  1. Coordinació: què cal dir i quan

La part no tècnica, que és la que més temps estalvia.

Abans de reescriure alguna cosa compartida:

  1. Avisa abans, no després. Un missatge de trenta segons al canal de l'equip: "Forçaré main a les 16:00 per treure el bolcat de la base de dades de l'historial. No feu pull fins que avisi."
  2. Digues què han de fer els altres. Encara que per a tu sigui obvi, no ho és per a qui està en una altra cosa:
    Després de l'avís, cadascú:
      git fetch origin
      git switch main
      git reset --hard origin/main
    Si teniu branques amb commits propis, aviseu-me abans de tocar res
    i fem el rebase --onto junts.
    
  3. Fes-ho en un moment de poca activitat. Mai un divendres a la tarda, mai durant una entrega.
  4. Confirma quan acabi. El silenci genera pull a cegues.

Quan ja ha passat i no vas avisar:

  1. Avisa igualment, com abans millor. El cost de dir "he forçat main, no feu pull" al cap de cinc minuts és zero. Al cap de cinc hores és una tarda de feina per a tres persones.
  2. Ofereix el procediment de l'apartat 8, no un "arregleu-ho".
  3. Si has destruït feina aliena, l'apartat 9 la recupera des de qualsevol clon de l'equip. No s'ha perdut.

Prevenció estructural (lliçó 07-06):

# Al servidor: branques protegides
# - main: prohibit el push forçat, prohibit el push directe
# - només s'integra via pull request

Una branca protegida converteix tot aquest apartat en innecessari. Si main no accepta --force, el problema no pot passar. És la mesura més rendible de la lliçó.

I en local, una protecció personal:

# Un hook pre-push que impedeix forçar sobre main (lliçó 06-01)
cat > .git/hooks/pre-push <<'EOF'
#!/usr/bin/env bash
# Bloqueja el push forçat sobre branques protegides
protegides="main develop"
while read -r ref_local sha_local ref_remota sha_remot; do
  branca="${ref_remota#refs/heads/}"
  for p in $protegides; do
    if [ "$branca" = "$p" ] && [ "$sha_remot" != "0000000000000000000000000000000000000000" ]; then
      if ! git merge-base --is-ancestor "$sha_remot" "$sha_local"; then
        echo "BLOQUEJAT: això seria un push no-fast-forward sobre '$branca'." >&2
        echo "Si realment cal, fes servir --no-verify i avisa l'equip." >&2
        exit 1
      fi
    fi
  done
done
exit 0
EOF
chmod +x .git/hooks/pre-push

Errors Habituals i Consells

Error 1: respondre a un push rebutjat amb --force. El rebuig significa que el servidor té commits que tu no tens. Forçar els destrueix. Diagnostica primer: git log --oneline HEAD..@{u} diu de qui són.

Error 2: raonar sobre origin/main sense haver fet fetch. origin/main és una foto local que pot tenir hores. Tot diagnòstic comença per git fetch.

Error 3: git fetch just abans de --force-with-lease. Anul·la la protecció. Fes servir --force-if-includes, o configura-ho amb push.useForceIfIncludes true.

Error 4: fer servir pull --rebase en una branca que una altra persona també té. Reescriu commits publicats i genera duplicats en cadena. La política ha de ser per branca, no per costum.

Error 5: fer --allow-unrelated-histories sense mirar què hi ha a l'altre costat. Pots acabar fusionant un altre projecte sencer dins del teu. Mira git log origin/main primer.

Error 6: git reset --hard origin/main sense comprovar què tens al davant. git log --oneline @{u}..HEAD costa un segon i et diu exactament què llençaràs.

Error 7: refer la feina a mà després d'una reescriptura aliena. git rebase --onto la trasplanta en una ordre i range-diff demostra que no s'ha alterat res.

Error 8: forçar una branca amb una pull request oberta plena de comentaris. Pot deixar la revisió òrfena. Considera publicar una branca nova.

Error 9: no avisar. El cost tècnic d'una reescriptura compartida és petit; el cost social de no anunciar-la, enorme.

Consell 1: git status -sb com a reflex. Una línia que diu [ahead N, behind M] i resol la meitat dels dubtes.

Consell 2: pull.ff only com a valor global. T'obliga a mirar abans d'integrar, i evita commits de fusió que ningú no va demanar.

Consell 3: push.useForceIfIncludes true. Una línia de configuració que elimina el forat del --force-with-lease.

Consell 4: git range-diff abans i després de qualsevol reescriptura. És la prova objectiva que el contingut no ha canviat.

Consell 5: recorda origin/branca@{1}. L'historial anterior del servidor continua al teu reflog remot, i amb ell es restaura el que un altre va destruir.

Consell 6: protegeix main al servidor. És l'única solució que fa impossible el problema, en lloc de tractar-lo.

Exercicis

Exercici 1: provocar i mesurar una divergència

  1. Munta un remot local amb git init --bare i clona'l dues vegades (simulant l'Ana i el Bruno).
  2. Des del clon del Bruno, fes dos commits i publica'ls.
  3. Des del de l'Ana, sense fer fetch, fes dos commits diferents.
  4. Executa git fetch i mesura la divergència amb git status -sb, git rev-list --left-right --count i git log --left-right --oneline.
  5. Prova git push i transcriu el missatge exacte.
  6. Resol-ho amb pull --rebase i comprova amb git log --graph que l'historial ha quedat lineal.
  7. Repeteix tot l'exercici amb pull --no-rebase i compara els dos grafs resultants.

Exercici 2: --force-with-lease i el seu forat

  1. Amb el mateix muntatge, fes que l'Ana publiqui un commit a GT-241.
  2. L'Ana executa git commit --amend sobre aquest commit (sense publicar encara).
  3. Mentrestant, el Bruno publica un commit nou a GT-241.
  4. L'Ana executa git push --force-with-lease. Anota el resultat i explica'l.
  5. L'Ana executa git fetch i torna a intentar-ho. Anota el resultat i explica per què ha canviat.
  6. Comprova que el commit del Bruno ha desaparegut del servidor i recupera'l fent servir origin/GT-241@{1} des del clon del Bruno.
  7. Repeteix el pas 5 amb --force-if-includes i comprova que ara sí que protegeix.

Exercici 3: trasplantament després d'una reescriptura aliena

  1. Munta un remot amb tres commits a main i clona'l dues vegades.
  2. Des del clon de l'Ana, crea GT-241 sobre main amb tres commits propis (sense publicar-la).
  3. Des del clon del Diego, fes git rebase -i sobre main per modificar el missatge del segon commit, i força el push.
  4. L'Ana fa fetch i diagnostica la situació amb git log --graph --all i git range-diff.
  5. L'Ana trasplanta GT-241 amb git rebase --onto i alinea el seu main.
  6. Verifica amb range-diff que els seus tres commits són idèntics en contingut.
  7. Comprova a git reflog show origin/main que queda registrat el forced-update.

Solucions

Solució 1:

rm -rf /tmp/p9-03 && mkdir /tmp/p9-03 && cd /tmp/p9-03
git init -q --bare servidor.git

git clone -q servidor.git ana && cd ana
git config user.name "Ana Ferrer"; git config user.email "[email protected]"
echo "// gestor-tasques" > app.js && git add . && git commit -q -m "chore: inici"
git push -q -u origin main
cd ..
git clone -q servidor.git bruno && cd bruno
git config user.name "Bruno Salas"; git config user.email "[email protected]"
cd ..
# 2. El Bruno publica
cd /tmp/p9-03/bruno
echo "// GT-238" >> app.js && git commit -q -am "GT-238 corregeix el focus"
echo "// GT-244" >> app.js && git commit -q -am "GT-244 script d'arrencada"
git push -q
# 3. L'Ana treballa sense saber-ho
cd /tmp/p9-03/ana
echo "// GT-241 a" >> estils.css && git add . && git commit -q -m "GT-241 base del filtre"
echo "// GT-241 b" >> estils.css && git commit -q -am "GT-241 estils de l'indicador"
# 4. Mesurar
git fetch -q
git status -sb | head -1
git rev-list --left-right --count HEAD...@{u}
git log --oneline --left-right HEAD...@{u}
## main...origin/main [ahead 2, behind 2]
2	2
< 7d3a8f4 GT-241 estils de l'indicador
< b52c9d1 GT-241 base del filtre
> 3e7f1a8 GT-244 script d'arrencada
> 9c4e7b2 GT-238 corregeix el focus
# 5. El rebuig
git push
To /tmp/p9-03/servidor.git
 ! [rejected]        main -> main (non-fast-forward)
error: failed to push some refs to '/tmp/p9-03/servidor.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart.
# 6. Rebase
git pull -q --rebase
git log --oneline --graph -5
git push -q && echo "publicat"
* 4b8e2c9 (HEAD -> main) GT-241 estils de l'indicador
* 8f2d6a1 GT-241 base del filtre
* 3e7f1a8 (origin/main) GT-244 script d'arrencada
* 9c4e7b2 GT-238 corregeix el focus
* 1e6f2c8 chore: inici
publicat

Lineal, sense bifurcacions. Els seus dos commits tenen hashos nous (b52c9d18f2d6a1): el rebase els ha recreat sobre la base nova.

# 7. L'alternativa amb merge (sobre una còpia)
cd /tmp/p9-03/ana
git switch -q -c prova-merge 8f2d6a1
git reset -q --hard b52c9d1        # tornem a l'estat previ al rebase...
# (més simple: refer l'escenari. L'important és el graf resultant:)
*   c7e2a91 Merge branch 'main' of /tmp/p9-03/servidor
|\
| * 3e7f1a8 GT-244 script d'arrencada
| * 9c4e7b2 GT-238 corregeix el focus
* | 7d3a8f4 GT-241 estils de l'indicador
* | b52c9d1 GT-241 base del filtre
|/
* 1e6f2c8 chore: inici
pull --rebase pull --no-rebase
Commits resultants 4 5 (un commit de fusió extra)
Hashos de l'Ana Canvien Es conserven
Forma del graf Lineal Bifurcada
Bo per a Branques pròpies, historial llegible Branques realment compartides

Solució 2:

cd /tmp/p9-03/ana && git switch -q -c GT-241 origin/main
echo "// v1" > filtre.js && git add . && git commit -q -m "GT-241 primera versio"
git push -q -u origin GT-241

# 2. L'Ana esmena
git commit -q --amend -m "GT-241 primera versio del filtre de pendents"
git rev-parse --short HEAD
6e2b9c7
# 3. El Bruno publica a sobre (sense que l'Ana ho sàpiga)
cd /tmp/p9-03/bruno
git fetch -q && git switch -q -c GT-241 origin/GT-241
echo "// aportacio del Bruno" >> filtre.js && git commit -q -am "GT-241 valida el filtre buit"
git push -q
# 4. L'Ana força amb lease, SENSE fetch previ
cd /tmp/p9-03/ana
git push --force-with-lease
To /tmp/p9-03/servidor.git
 ! [rejected]        GT-241 -> GT-241 (stale info)
error: failed to push some refs

La protecció ha funcionat. L'origin/GT-241 de l'Ana apunta al seu commit original; el servidor és al commit del Bruno. Com que no coincideixen, Git es nega.

# 5. L'Ana fa fetch "per actualitzar-se" i ho reintenta
git fetch -q
git push --force-with-lease
To /tmp/p9-03/servidor.git
 + b52c9d1...6e2b9c7 GT-241 -> GT-241 (forced update)

Ha passat. I ha destruït el commit del Bruno. El fetch va actualitzar origin/GT-241 al commit del Bruno; l'"arrendament" va passar a coincidir amb la realitat; la comprovació es va satisfer. L'Ana no va veure mai la feina del Bruno i tot i així la va sobreescriure.

# 6. Recuperar-lo des del clon del Bruno
cd /tmp/p9-03/bruno
git reflog show origin/GT-241
6e2b9c7 refs/remotes/origin/GT-241@{0}: fetch: forced-update
b52c9d1 refs/remotes/origin/GT-241@{1}: push

En realitat el Bruno ni tan sols necessita el reflog: té el commit a la seva branca local. El seu GT-241 local continua intacte:

git log --oneline -2
git branch rescat-bruno GT-241
2f8c6e1 GT-241 valida el filtre buit
b52c9d1 GT-241 primera versio

I per tornar-lo al servidor caldria integrar-lo amb la versió esmenada de l'Ana —un cherry-pick del commit del Bruno sobre la branca nova (lliçó 05-03)—, coordinant-ho entre tots dos.

# 7. Amb --force-if-includes
cd /tmp/p9-03/ana
# (refet l'escenari: el Bruno torna a publicar alguna cosa que l'Ana no té)
git fetch -q
git push --force-with-lease --force-if-includes
To /tmp/p9-03/servidor.git
 ! [rejected]        GT-241 -> GT-241 (stale info)

Ara sí que protegeix, fins i tot després del fetch. La comprovació addicional exigeix que els commits remots que se sobreescriuran estiguin incorporats a l'historial local de l'Ana, no només baixats. I no ho estan: mai no els va integrar.

git config --global push.useForceIfIncludes true    # per no haver-se'n de recordar

Solució 3:

rm -rf /tmp/p9-03b && mkdir /tmp/p9-03b && cd /tmp/p9-03b
git init -q --bare servidor.git
git clone -q servidor.git ana && cd ana
git config user.name "Ana Ferrer"; git config user.email "[email protected]"
for i in 1 2 3; do echo "linia $i" >> app.js; git add .; git commit -q -m "chore: commit $i"; done
git push -q -u origin main
cd .. && git clone -q servidor.git diego && cd diego
git config user.name "Diego Rueda"; git config user.email "[email protected]"
cd ..
# 2. L'Ana crea GT-241
cd /tmp/p9-03b/ana
git switch -q -c GT-241
echo "// filtre 1" > filtre.js && git add . && git commit -q -m "GT-241 base del filtre"
echo "// filtre 2" >> filtre.js && git commit -q -am "GT-241 calcula el comptador"
echo "// filtre 3" >> filtre.js && git commit -q -am "GT-241 estils de l'indicador"
git branch --show-current
git rev-parse --short main
GT-241
7d3a8f4

Anota aquest 7d3a8f4: és la base antiga.

# 3. El Diego reescriu main i força
cd /tmp/p9-03b/diego
GIT_SEQUENCE_EDITOR="sed -i '2s/^pick/reword/'" \
GIT_EDITOR="sed -i '1s/.*/chore: commit 2 (missatge corregit)/'" \
  git rebase -i HEAD~2
git push -q --force
git log --oneline -3
2f8c6e1 chore: commit 3
7a4d9b3 chore: commit 2 (missatge corregit)
5c1e8f2 chore: commit 1
# 4. L'Ana diagnostica
cd /tmp/p9-03b/ana
git fetch -q
git log --oneline --graph --all -10
* 9c4e7b2 (HEAD -> GT-241) GT-241 estils de l'indicador
* 3e7f1a8 GT-241 calcula el comptador
* 8a1f6c3 GT-241 base del filtre
* 7d3a8f4 (main) chore: commit 3
* b52c9d1 chore: commit 2
| * 2f8c6e1 (origin/main) chore: commit 3
| * 7a4d9b3 chore: commit 2 (missatge corregit)
|/
* 5c1e8f2 chore: commit 1

Dues línies paral·leles amb els mateixos missatges: historial reescrit.

git range-diff origin/main...main
1:  7a4d9b3 ! 1:  b52c9d1 chore: commit 2 (missatge corregit)
    @@ Metadata
      ## Commit message ##
    -    chore: commit 2 (missatge corregit)
    +    chore: commit 2
2:  2f8c6e1 = 2:  7d3a8f4 chore: commit 3

Només va canviar un missatge; el contingut és idèntic. Diagnòstic complet en una ordre.

# 5. Trasplantament
git branch copia-GT-241
git rebase --onto origin/main 7d3a8f4 GT-241
git log --oneline --graph -7
* 4b8e2c9 (HEAD -> GT-241) GT-241 estils de l'indicador
* 8f2d6a1 GT-241 calcula el comptador
* 6e2b9c7 GT-241 base del filtre
* 2f8c6e1 (origin/main) chore: commit 3
* 7a4d9b3 chore: commit 2 (missatge corregit)
* 5c1e8f2 chore: commit 1
git switch -q main
git reset -q --hard origin/main
git switch -q GT-241
# 6. Verificació
git range-diff copia-GT-241...GT-241
1:  8a1f6c3 = 1:  6e2b9c7 GT-241 base del filtre
2:  3e7f1a8 = 2:  8f2d6a1 GT-241 calcula el comptador
3:  9c4e7b2 = 3:  4b8e2c9 GT-241 estils de l'indicador

Tres =. Contingut idèntic, base nova, zero commits duplicats.

git diff copia-GT-241 GT-241 -- filtre.js
(sense sortida)
git branch -D copia-GT-241
# 7. L'evidència al reflog remot
git reflog show origin/main
2f8c6e1 refs/remotes/origin/main@{0}: fetch: forced-update
7d3a8f4 refs/remotes/origin/main@{1}: clone: from /tmp/p9-03b/servidor.git

forced-update amb la seva data, i origin/main@{1} guardant l'historial anterior del servidor. Si hagués calgut restaurar-lo, allà era.

Conclusió

Les divergències deixen de fer por tan bon punt es veuen com el que són: una situació en què Git no pot decidir per tu.

  • Divergir significa que cada branca té commits que l'altra no té. Es mesura amb git status -sb i amb git rev-list --left-right --count main...origin/main, i s'entén mirant els dos costats amb git log --left-right. Tot diagnòstic comença per git fetch.
  • Els quatre missatges són la mateixa situació: el "have diverged" del status, el "You have divergent branches" del pull, el "Not possible to fast-forward" del pull.ff only i el non-fast-forward del push.
  • Tres polítiques de pull: merge per a branques realment compartides, rebase per a les teves, ff-only quan prefereixes decidir a mà. L'elecció és per branca, no per costum.
  • Al push rebutjat s'hi respon diagnosticant, no forçant: git log --oneline HEAD..@{u} diu de qui són els commits del servidor. Si són d'un altre, s'integra. Si són versions velles dels teus i la branca és només teva, es força amb --force-with-lease.
  • --force-with-lease no protegeix si has fet fetch just abans, perquè l'arrendament s'actualitza amb el que encara no has vist. --force-if-includes ho arregla; push.useForceIfIncludes true ho deixa posat per sempre.
  • Quan un altre reescriu l'historial publicat i tu tens feina a sobre, el procediment és: fer còpia amb git branch, trasplantar amb git rebase --onto <nova-base> <base-vella> <branca>, alinear main amb reset --hard origin/main, i verificar amb git range-diff que el contingut no ha canviat.
  • Les branques remotes també tenen reflog. origin/main@{1} guarda on era el servidor abans de la reescriptura, i forced-update és l'evidència que va passar. Amb això, qualsevol clon de l'equip pot restaurar el que es va destruir.
  • refusing to merge unrelated histories no és divergència: són dos arbres sense ancestre comú. Mira què hi ha a l'altre costat abans de fer servir --allow-unrelated-histories.
  • I el que més estalvia: avisar abans, i protegir main al servidor perquè el problema no pugui passar.

Queda una idea que ha aparegut a gairebé tots els apartats sense desenvolupar-se: el reflog. Ha estat la font de l'ORIG_HEAD de la lliçó anterior, de l'origin/main@{1} d'aquesta, i de la promesa que portem oberta des de la lliçó 03-06 ("-D és recuperable") i la 05-01 ("un rebase desastrós es pot desfer").

Toca desenvolupar-lo sencer: què és exactament, on viu, quant dura, com es llegeix, i el receptari complet de recuperació —el reset --hard que es va emportar tres dies, la branca esborrada amb -D, el rebase que va sortir malament, el desament temporal eliminat— més què fer quan ni tan sols el reflog no n'hi ha prou.

Continua a la lliçó 09-04: Recuperant Confirmacions Perdudes.

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