Come Risolvere i Conflitti di Merge Difficili in Git Passo dopo Passo
Risolvi conflitti di merge complessi in Git leggendo le versioni ours, theirs e base, gestendo rinomine, rebase, binari e test.
Come Risolvere i Conflitti di Merge Difficili in Git Passo dopo Passo
Un conflitto di merge difficile in Git raramente è difficile a causa dei marcatori di conflitto stessi. È difficile perché devi preservare l'intento di due modifiche diverse contemporaneamente. Un ramo ha rinominato una funzione mentre un altro ne ha cambiato il comportamento. Un ramo ha spostato un file mentre un altro lo ha modificato. Un ramo ha cambiato una sequenza di migrazione del database. Git può mostrarti la sovrapposizione, ma non può decidere cosa dovrebbe significare il software dopo.
Quando un merge si ferma con conflitti, non iniziare subito a cancellare i marcatori. Prima orientati.
git status
Git elencherà i percorsi non uniti. Potrebbe dire both modified, deleted by us, deleted by them, both added o qualcosa di simile. Quelle frasi ti dicono la forma del conflitto.
Se il merge sembra sbagliato o non sei pronto a risolverlo, interrompi prima di fare altre modifiche:
git merge --abort
Per un conflitto di rebase, l'equivalente è:
git rebase --abort
Non è un fallimento. È un reset pulito allo stato precedente all'operazione, che è spesso la mossa più intelligente quando ti rendi conto che hai bisogno di più contesto.
Leggi il Conflitto come Tre Versioni
Un normale marcatore di conflitto appare così:
<<<<<<< HEAD
versione del ramo corrente
=======
versione del ramo in arrivo
>>>>>>> feature-branch
Durante un merge, HEAD è il ramo che avevi estratto quando hai eseguito git merge. Il lato inferiore è il ramo che viene unito.
Per conflitti difficili, usa i tre stadi che Git mantiene nell'indice:
git show :1:percorso/del/file # antenato comune
git show :2:percorso/del/file # nostro
git show :3:percorso/del/file # loro
L'antenato comune è la versione da cui entrambi i rami sono partiti. È utile perché mostra cosa ha effettivamente cambiato ciascun ramo. Senza di esso, potresti confrontare due versioni finali e perdere il motivo dietro di esse.
Puoi anche usare:
git diff
git diff --ours -- percorso/del/file
git diff --theirs -- percorso/del/file
git diff --base -- percorso/del/file
È qui che molti vanno troppo veloci. L'obiettivo non è scegliere "nostro" o "loro" come voto di lealtà al team. L'obiettivo è produrre il file finale corretto.
Un Flusso di Lavoro Manuale Sicuro
Usa questa routine per ogni file in conflitto:
- Apri il file e trova ogni marcatore di conflitto.
- Leggi il codice circostante, non solo le righe marcate.
- Controlla i commit che hanno toccato il file su entrambi i rami.
- Modifica il file nella versione finale desiderata.
- Rimuovi tutti i marcatori di conflitto.
- Esegui il test più piccolo rilevante o il controllo di build.
- Metti in stage il file.
Comandi utili mentre lo fai:
git log --oneline --left-right --merge -- percorso/del/file
git diff --check
git grep -n '<<<<<<<\|=======\|>>>>>>>'
git diff --check cattura problemi di spazi bianchi rimasti. git grep cattura marcatori di conflitto dimenticati prima che arrivino alla CI.
Dopo aver risolto un file:
git add percorso/del/file
Quando tutti i conflitti sono in stage:
git status
git commit
Durante un rebase, usa:
git rebase --continue
Quando Entrambi i Rami Hanno Cambiato la Stessa Funzione
Questo è il caso comune. Supponiamo che un ramo aggiunga validazione e l'altro rinomini un parametro:
<<<<<<< HEAD
function createUser(email) {
return db.users.insert({ email });
}
=======
function createUser(rawEmail) {
const email = rawEmail.trim().toLowerCase();
return db.users.insert({ email });
}
>>>>>>> normalize-email
La risposta giusta potrebbe combinare entrambi:
function createUser(rawEmail) {
const email = rawEmail.trim().toLowerCase();
return db.users.insert({ email });
}
Ma solo se i chiamanti sono stati aggiornati per passare rawEmail, e solo se la normalizzazione è ancora desiderata. Cerca la funzione:
git grep -n 'createUser'
I conflitti difficili spesso richiedono di controllare i file vicini. Un conflitto di firma di funzione in un file può richiedere aggiornamenti in test, route, tipi, mock o documentazione.
Conflitti di Rinomina e Modifica
I conflitti di rinomina sono fastidiosi perché il file che vuoi potrebbe non essere dove ti aspetti. Inizia con lo stato:
git status --short
Poi ispeziona le informazioni nome-stato:
git diff --name-status --diff-filter=R
Se un ramo ha rinominato src/user.js in src/account.js e l'altro ha modificato src/user.js, di solito vuoi il contenuto modificato applicato al nuovo percorso. Uno strumento di merge visivo può aiutare, ma il concetto è semplice: preserva la rinomina e preserva le modifiche significative.
Dopo aver deciso il percorso finale, rimuovi il percorso obsoleto se necessario e metti in stage quello finale:
git rm vecchio/percorso.js
git add nuovo/percorso.js
Non mettere in stage entrambi i file a meno che il progetto finale non debba davvero contenerli entrambi.
Cancellato da Noi o Cancellato da Loro
Un conflitto di cancellazione/modifica significa che un ramo ha cancellato un file mentre l'altro lo ha modificato. Git non può sapere se la cancellazione ha reso la modifica irrilevante.
Se il file deve rimanere cancellato:
git rm percorso/del/file
Se il file deve rimanere, scegli la versione che vuoi e mettila in stage:
git checkout --theirs percorso/del/file
git add percorso/del/file
o:
git checkout --ours percorso/del/file
git add percorso/del/file
Fai attenzione con --ours e --theirs durante il rebase. In un rebase, le etichette possono sembrare invertite perché Git sta riproducendo i tuoi commit su un'altra base. Quando non sei sicuro, ispeziona gli stadi:
git show :2:percorso/del/file
git show :3:percorso/del/file
Conflitti di File Binari
Git non può unire la maggior parte dei file binari. Se due rami hanno cambiato la stessa immagine, archivio, documento o asset compilato, devi scegliere una versione o creare un nuovo file manualmente.
Per prendere la nostra versione:
git checkout --ours percorso/del/file.bin
git add percorso/del/file.bin
Per prendere la loro versione:
git checkout --theirs percorso/del/file.bin
git add percorso/del/file.bin
Se il binario è generato, la risposta migliore potrebbe essere rigenerarlo dalla fonte dopo aver risolto i file di testo. Se il binario è un asset di design o un documento, parla con la persona che ha cambiato l'altro lato. Indovinare può distruggere il lavoro.
Usa uno Strumento di Merge Quando il File è Troppo Difficile da Leggere
Un buono strumento di merge mostra quattro cose: la versione base, la tua versione, la loro versione e il risultato. Configurane uno che ti piace davvero. Visual Studio Code è comune:
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'
Poi esegui:
git mergetool
Altri team preferiscono Meld, KDiff3, Beyond Compare o strumenti integrati nell'IDE. Lo strumento conta meno della comprensione delle tre versioni. Non cliccare "accetta in arrivo" attraverso un conflitto complesso solo per far sparire i marcatori rossi.
Dopo aver usato un mergetool, controlla i file di backup come .orig:
git status --short
Puoi disabilitare i file di backup del mergetool globalmente se non li vuoi:
git config --global mergetool.keepBackup false
Le Opzioni di Strategia Non Sono Magiche
Potresti vedere consigli come:
git merge -X theirs feature
Questo non significa "sostituisci il mio ramo con feature." Significa che quando la strategia di merge di Git vede blocchi in conflitto, dovrebbe preferire l'altro lato per quei blocchi. Le modifiche non in conflitto da entrambi i rami vengono comunque unite. Può essere utile per file di blocco generati o conflitti di formattazione meccanici, ma è rischioso per la logica di business.
-X ours e -X theirs sono opzioni di strategia. La strategia di merge ours è diversa:
git merge -s ours vecchio-ramo
Questo registra un merge mantenendo l'albero corrente. È uno strumento specializzato, spesso usato per marcare un ramo come unito senza prenderne il contenuto. Non usarlo per la risoluzione normale dei conflitti a meno che tu non sia molto sicuro.
Conflitti di Rebase
Durante il rebase, Git riproduce i commit uno alla volta. Questo significa che potresti risolvere diversi conflitti più piccoli invece di un grande conflitto di merge.
Il ciclo è:
git status
# modifica i file
git add file-risolto
git rebase --continue
Se un commit in riproduzione non è più necessario perché la nuova base contiene già la modifica, usa:
git rebase --skip
Usa skip con attenzione. Elimina quel commit dal ramo rebasato. Leggi prima il commit:
git show
Ancora, --ours e --theirs possono essere confusi nel rebase. Ispeziona :2: e :3: in caso di dubbio.
Testa la Risoluzione, Non Solo il Merge
Un merge può essere sintatticamente risolto e ancora sbagliato. Dopo aver messo in stage i file, esegui i test che toccano l'area modificata. Per un conflitto frontend, potrebbe essere un typecheck e un test focalizzato sul componente. Per un conflitto backend, potrebbe essere un test di servizio o un controllo di migrazione. Per un conflitto di lockfile, reinstalla le dipendenze ed esegui il comando di verifica del gestore di pacchetti.
Come minimo:
git diff --check
git grep -n '<<<<<<<\|=======\|>>>>>>>'
Poi esegui il controllo specifico del progetto che avrebbe catturato una combinazione sbagliata.
Riduci i Conflitti Futuri
Il miglior conflitto è quello che non crei mai. Mantieni i rami di breve durata, fai rebase o merge dal ramo principale regolarmente, ed evita di mescolare modifiche meccaniche con modifiche alle funzionalità. Una PR solo di formattazione non dovrebbe anche cambiare la logica. Uno spostamento di file non dovrebbe anche riscrivere il file se puoi evitarlo.
Per i file che sono sempre dolorosi, considera cambiamenti di proprietà o struttura. Grandi file di configurazione, snapshot generati, lockfile, elenchi di migrazione e registri di route centrali spesso creano conflitti ripetuti perché tutti modificano la stessa area. A volte la soluzione è di processo. A volte la soluzione è dividere il file o generarlo da fonti più piccole.
Usa .gitattributes per i file che necessitano di un comportamento di merge speciale. Ad esempio, alcuni lockfile generati possono avere driver di merge specifici del gestore di pacchetti. Non inventarne uno casualmente, ma controlla se il tuo ecosistema ha un driver raccomandato.
I conflitti di merge sono in parte lavoro tecnico e in parte comunicazione. Se non capisci l'intento dell'altro ramo, chiedi. Dieci minuti con l'autore sono più economici che unire silenziosamente codice che passa i test ma rimuove la funzionalità che stavano costruendo.
Lockfile, Migrazioni e Altri File ad Alta Frizione
Alcuni file confliggono più spesso perché molti rami modificano la stessa piccola area. I lockfile delle dipendenze sono un esempio comune. Se due rami aggiungono pacchetti, il conflitto di lockfile può essere tecnicamente grande ma concettualmente semplice: rigenerarlo con il gestore di pacchetti dopo aver risolto il file manifest.
Per un progetto Node, potrebbe significare risolvere package.json, poi eseguire il gestore di pacchetti che possiede il lockfile:
npm install
# o pnpm install
# o yarn install
Poi metti in stage sia il manifest che il lockfile. Non modificare a mano un lockfile complesso a meno che tu non capisca il suo formato. Il gestore di pacchetti ha meno probabilità di fare un errore sottile nel grafo delle dipendenze.
Le migrazioni del database richiedono più attenzione. Se due rami creano migrazioni con ipotesi di ordinamento, accettare entrambi i file potrebbe non essere sufficiente. Controlla i timestamp delle migrazioni, i numeri di sequenza, le dipendenze e se entrambe le migrazioni modificano la stessa tabella o gli stessi dati. A volte la risoluzione giusta è una nuova migrazione successiva che riconcilia i due rami.
Gli snapshot generati e i file golden hanno lo stesso schema: risolvi prima la modifica della fonte, rigenera l'output, poi rivedi il diff generato. Se il diff generato è enorme, chiediti se appartiene allo stesso commit di merge. Grandi modifiche generate possono nascondere una cattiva risoluzione manuale.
Quando un conflitto si estende su più file, scrivi il comportamento finale desiderato prima di modificare. Una breve nota come "mantieni la nuova validazione dalla funzionalità A, mantieni il servizio rinominato dalla funzionalità B, rigenera i tipi client" ti impedisce di risolvere ogni file localmente mentre perdi il design complessivo.
Per merge particolarmente rischiosi, crea un ramo temporaneo prima di iniziare:
git switch -c merge-test/main-con-feature
git merge feature
Se la risoluzione diventa disordinata, puoi abbandonare il ramo temporaneo senza disturbare il tuo ramo originale. Questa piccola abitudine rende i conflitti difficili meno stressanti perché hai sempre una via di ritorno pulita.
Rivedi il Merge Finale Come un Cambiamento a Sé Stante
Una risoluzione di conflitto è un nuovo lavoro. Trattalo come tale nella revisione. Il diff finale dovrebbe mostrare non solo le modifiche di entrambi i rami, ma anche qualsiasi codice di colla che hai scritto per farli funzionare insieme. Se il commit di merge è grande, spiega la risoluzione nel messaggio di commit o nel commento della pull request. I revisori non dovrebbero dover fare reverse engineering per capire perché un lato è stato scelto.
Prima di fare push, confronta il risultato finale con entrambi i genitori quando possibile:
git diff HEAD^1..HEAD -- percorso/del/file
git diff HEAD^2..HEAD -- percorso/del/file
Per un merge non ancora committato, ispeziona le modifiche in stage:
git diff --cached
Cerca cancellazioni accidentali di test, import che non sono più usati, voci di configurazione duplicate e percorsi di codice in cui entrambi i rami hanno aggiunto logica simile sotto nomi diversi. Questi sono gli errori che Git non può identificare per te.
Se il conflitto coinvolgeva il comportamento, aggiungi o aggiorna un test che fallirebbe se avessi scelto il lato sbagliato. Quel test fa più che dimostrare il merge di oggi. Protegge la decisione dall'essere annullata nel prossimo refactoring.