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
- Què significa exactament "han divergit"
- Mesurar la divergència
- Els quatre missatges de la mateixa situació
- Les tres polítiques de
pulli quina triar - Com s'arriba a una divergència
- El
pushrebutjat: diagnòstic i sortides --force-with-leasei per què de vegades no protegeix- Quan el remot va reescriure i tu tens feina a sobre
- Recuperar la branca remota anterior amb
origin/branca@{1} - Històries no relacionades:
--allow-unrelated-histories - Coordinació: què cal dir i quan
- 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:
- Mesurar la divergència
Abans de resoldre, mesura. Aquestes ordres no modifiquen res.
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
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:
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..main7d3a8f4 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
# 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 ellsSi 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'
- 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
É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:
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.
- Les tres polítiques de
pull i quina triar
pull i quina triargit 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 --rebaseO, 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 tornarebase.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 --rebasereescriu 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:
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.
- 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.
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.
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.
- El
push rebutjat: diagnòstic i sortides
push rebutjat: diagnòstic i sortidesAquest é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}# 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
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 pushAmb pull configurat:
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:
Sortida B: forçar, quan la branca és teva
Només si es compleixen les tres condicions:
- La branca és de funcionalitat i ningú més no la fa servir.
- El que hi ha al servidor són versions antigues dels teus propis commits.
- Has comprovat el punt 3 del diagnòstic i no hi ha commits d'una altra persona.
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:
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).
--force-with-lease i per què de vegades no protegeix
--force-with-lease i per què de vegades no protegeixRecordant 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).
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 fetchjust 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 BrunoEl 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:
--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 | Sí |
Configura-ho com a comportament per defecte:
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:b52c9d1e4a7f3c8b6d2e9a5f1c7b3d8e4a6f2c9bVerbós, però no hi ha manera d'equivocar-se.
- 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.
Pas 1: entendre què ha passat
* 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 (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
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:
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(elmainvell del qual penjava la teva branca). - Branca:
GT-241.
* 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
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:
Si sí 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
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:
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:
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) |
- Recuperar la branca remota anterior amb
origin/branca@{1}
origin/branca@{1}Un detall que salva situacions i que gairebé ningú no coneix: les branques de seguiment remotes també tenen reflog.
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-forwardDues coses valuosíssimes en aquesta sortida:
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.origin/main@{1}és on era elmaindel 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/mainAixò é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:mainI també amb dates:
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.
- Històries no relacionades:
--allow-unrelated-histories
--allow-unrelated-historiesQuè 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
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 -20Si el remot només té el commit inicial de la plataforma:
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 servidorI 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.
- 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:
- Avisa abans, no després. Un missatge de trenta segons al canal de l'equip: "Forçaré
maina les 16:00 per treure el bolcat de la base de dades de l'historial. No feupullfins que avisi." - 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. - Fes-ho en un moment de poca activitat. Mai un divendres a la tarda, mai durant una entrega.
- Confirma quan acabi. El silenci genera
pulla cegues.
Quan ja ha passat i no vas avisar:
- 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. - Ofereix el procediment de l'apartat 8, no un "arregleu-ho".
- 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 requestUna 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-pushErrors 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
- Munta un remot local amb
git init --barei clona'l dues vegades (simulant l'Ana i el Bruno). - Des del clon del Bruno, fes dos commits i publica'ls.
- Des del de l'Ana, sense fer
fetch, fes dos commits diferents. - Executa
git fetchi mesura la divergència ambgit status -sb,git rev-list --left-right --countigit log --left-right --oneline. - Prova
git pushi transcriu el missatge exacte. - Resol-ho amb
pull --rebasei comprova ambgit log --graphque l'historial ha quedat lineal. - Repeteix tot l'exercici amb
pull --no-rebasei compara els dos grafs resultants.
Exercici 2: --force-with-lease i el seu forat
- Amb el mateix muntatge, fes que l'Ana publiqui un commit a
GT-241. - L'Ana executa
git commit --amendsobre aquest commit (sense publicar encara). - Mentrestant, el Bruno publica un commit nou a
GT-241. - L'Ana executa
git push --force-with-lease. Anota el resultat i explica'l. - L'Ana executa
git fetchi torna a intentar-ho. Anota el resultat i explica per què ha canviat. - Comprova que el commit del Bruno ha desaparegut del servidor i recupera'l fent servir
origin/GT-241@{1}des del clon del Bruno. - Repeteix el pas 5 amb
--force-if-includesi comprova que ara sí que protegeix.
Exercici 3: trasplantament després d'una reescriptura aliena
- Munta un remot amb tres commits a
maini clona'l dues vegades. - Des del clon de l'Ana, crea
GT-241sobremainamb tres commits propis (sense publicar-la). - Des del clon del Diego, fes
git rebase -isobremainper modificar el missatge del segon commit, i força elpush. - L'Ana fa
fetchi diagnostica la situació ambgit log --graph --alligit range-diff. - L'Ana trasplanta
GT-241ambgit rebase --ontoi alinea el seumain. - Verifica amb
range-diffque els seus tres commits són idèntics en contingut. - Comprova a
git reflog show origin/mainque queda registrat elforced-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}< 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
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.
* 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
Lineal, sense bifurcacions. Els seus dos commits tenen hashos nous (b52c9d1 → 8f2d6a1): 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# 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 -qTo /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.
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.
6e2b9c7 refs/remotes/origin/GT-241@{0}: fetch: forced-update
b52c9d1 refs/remotes/origin/GT-241@{1}: pushEn realitat el Bruno ni tan sols necessita el reflog: té el commit a la seva branca local. El seu GT-241 local continua intacte:
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-includesAra 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.
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 mainAnota 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* 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.
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 3Nomé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
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.
2f8c6e1 refs/remotes/origin/main@{0}: fetch: forced-update
7d3a8f4 refs/remotes/origin/main@{1}: clone: from /tmp/p9-03b/servidor.gitforced-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 -sbi ambgit rev-list --left-right --count main...origin/main, i s'entén mirant els dos costats ambgit log --left-right. Tot diagnòstic comença pergit fetch. - Els quatre missatges són la mateixa situació: el "have diverged" del
status, el "You have divergent branches" delpull, el "Not possible to fast-forward" delpull.ff onlyi el non-fast-forward delpush. - Tres polítiques de
pull:mergeper a branques realment compartides,rebaseper a les teves,ff-onlyquan prefereixes decidir a mà. L'elecció és per branca, no per costum. - Al
pushrebutjat 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-leaseno protegeix si has fetfetchjust abans, perquè l'arrendament s'actualitza amb el que encara no has vist.--force-if-includesho arregla;push.useForceIfIncludes trueho 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 ambgit rebase --onto <nova-base> <base-vella> <branca>, alinearmainambreset --hard origin/main, i verificar ambgit range-diffque el contingut no ha canviat. - Les branques remotes també tenen reflog.
origin/main@{1}guarda on era el servidor abans de la reescriptura, iforced-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 historiesno é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
mainal 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
- Què és Git?
- Instal·lant Git
- Terminologia Bàsica de Git
- El Model de Dades de Git
- Configurant Git
- Configuració Inicial
Mòdul 2: Operacions Bàsiques de Git
- Creant un Repositori
- Clonant un Repositori
- Flux de Treball Bàsic de Git
- Preparant i Confirmant Canvis
- Inspeccionant Canvis amb git diff
- Visualitzant l'Historial de Confirmacions
Mòdul 3: Branques i Fusió
- Entenent les Branques
- Creant i Canviant Branques
- Fusionant Branques
- Estratègies de Fusió
- Resolent Conflictes de Fusió
- Gestió de Branques
Mòdul 4: Treballant amb Repositoris Remots
- Entenent els Repositoris Remots
- Afegint un Repositori Remot
- Autenticació amb Repositoris Remots
- Obtenint i Baixant Canvis
- Enviant Canvis
- Rastrejant Branques
Mòdul 5: Operacions Avançades de Git
- Rebase
- Rebase Interactiu
- Cherry-Picking de Confirmacions
- Desant Canvis Temporals
- Etiquetant Confirmacions
- Revertint Confirmacions
Mòdul 6: Eines i Tècniques de Git
- Usant Git Hooks
- Git Bisect
- Git Blame
- Git Log i Àlies
- Submòduls de Git
- Múltiples Còpies de Treball amb git worktree
Mòdul 7: Estratègies de Col·laboració i Flux de Treball
- Forks i Pull Requests
- Revisions de Codi amb Git
- Flux de Treball Git Flow
- GitHub Flow
- Trunk Based Development
- Integració Contínua amb Git
Mòdul 8: Bones Pràctiques i Consells de Git
- Escrivint Bons Missatges de Confirmació
- Mantenint un Historial Net
- Ignorant Fitxers amb .gitignore
- Atributs de Fitxer amb .gitattributes
- Bones Pràctiques de Seguretat
- Consells de Rendiment
Mòdul 9: Resolució de Problemes i Depuració
- Problemes Habituals de Git
- Desfent Canvis
- Resolent Divergències amb el Remot
- Recuperant Confirmacions Perdudes
- Tractant amb Repositoris Corruptes
- Tècniques Avançades de Depuració
