Come si rileva una fusione del male in git?

Ho creato un semplice repository git per illustrare la mia domanda, disponibile su GitHub qui: https://github.com/smileyborg/EvilMerge

Ecco un’illustrazione della cronologia dei repository:

master A---B---D---E-----G-----I \ / \ / another_branch ----C \ / \ / another_branch2 F---H 

(Nel repository effettivo su GitHub, D è 4a48c9 e I è 48349d .)

D è una “semplice” fusione del male, in cui il merge commit “correttamente” risolve un conflitto di merge, ma rende anche un cambiamento “malvagio” non correlato che non esisteva in nessuno dei due genitori. È ansible scoprire la parte “ctriggers” di questa unione usando git show -c su questo commit, poiché l’output include ++ e -- (al contrario di single + e - ) per indicare le modifiche che non esistevano in uno dei due genitori (vedi questa risposta per il contesto).

I sono un diverso tipo di unione del male, in cui il merge commit “correttamente” risolve un conflitto di merge (causato da modifiche da F a file.txt che sono in conflitto con le modifiche da G ), ma anche “evilly” scarta completamente le modifiche apportate a file diverso file2.txt (annullando efficacemente le modifiche da H ).

Come puoi sapere che I sono un’unione malefica? In altre parole, quale comando si può usare per scoprire che non solo risolvo manualmente un conflitto, ma non I a unire le modifiche che dovrebbe avere?

Modifica / Aggiornamento: cos’è una fusione del male?

Come sottolineato in seguito da René Link , è difficile (forse imansible) definire un insieme generico di criteri per identificare una “fusione del male”. Tuttavia, proprio come la giustizia della Corte Suprema Stewart ha detto sulla pornografia , la fusione del male è qualcosa che sai quando vedi.

Quindi forse una domanda migliore da porsi è questa: quale comando git si può usare su un commit di unione per ottenere un output diff di tutte le nuove modifiche introdotte esclusivamente nel commit di merge stesso . Questa diff dovrebbe includere:

  • tutti uniscono le risoluzioni dei conflitti (almeno, se la risoluzione implica qualcosa di più complesso della scelta delle modifiche di un genitore rispetto alle altre)
  • tutte le aggiunte o le rimozioni che non esistevano in nessuno dei due genitori (come visto in D )
  • tutti i cambiamenti che esistevano in uno dei genitori ma che l’unione commette scarti (come visto in I )

L’objective qui è quello di essere in grado di avere un aspetto umano di questo output e sapere se l’unione ha avuto successo o (accidentalmente o maliziosamente) “male” senza dover riesaminare tutte le modifiche precedentemente riviste (es. F e H ) che vengono integrati nella fusione.

La cosa più semplice da fare sarebbe quella di diffare i risultati della risoluzione del conflitto con un’unione che risolve automaticamente i conflitti senza l’intervento umano. Qualsiasi risoluzione automatica verrà ignorata, poiché verranno risolte esattamente nello stesso modo.

Vedo due modi di visualizzare le possibili risoluzioni “cattive”. Se lo stai facendo in uno script, aggiungi &> /dev/null alla fine di tutte le righe che non ti interessa vedere l’output.

1) Usa due diff separati, uno che favorisce il primo genitore, e un secondo che favorisce il secondo genitore.

 MERGE_COMMIT= git checkout $MERGE_COMMIT~ git merge --no-ff --no-edit -s recursive -Xours $MERGE_COMMIT^2 echo "Favor ours" git diff HEAD..$MERGE_COMMIT git checkout $MERGE_COMMIT~ git merge --no-ff --no-edit -s recursive -Xtheirs $MERGE_COMMIT^2 echo "Favor theirs" git diff HEAD..$MERGE_COMMIT 

2) Diff contro i risultati della fusione conflittuale con i conflitti ancora in.

 MERGE_COMMIT= git checkout $MERGE_COMMIT~ git -c merge.conflictstyle=diff3 merge --no-ff $MERGE_COMMIT^2 --no-commit git add $(git status -s | cut -c 3-) git commit --no-edit git diff HEAD..$MERGE_COMMIT 

Prima di poter rilevare le unioni del male, dobbiamo definire quali sono le mire del male.

Ogni unione che presenta conflitti deve essere risolta manualmente. Per risolvere i conflitti possiamo

  1. prendere una delle modifiche e omettere l’altra.
  2. eventualmente prendere entrambe le modifiche (in questo caso l’ordine nel risultato potrebbe essere importante)
  3. non prendere nessuno di loro e creare un nuovo cambiamento che è il consolidamento di entrambi.
  4. non prendere nessuno di loro e omettere entrambi.

Quindi cos’è una fusione del male ora?

Secondo questo blog lo è

una fusione è considerata malvagia se non integra fedelmente tutti i cambiamenti di tutti i genitori.

Quindi cos’è una “integrazione fedele”? Penso che nessuno possa dare una risposta generale, perché dipende dalla semantica del codice o del testo o di qualunque cosa venga unita.

Altri dicono

Un’unione male è un’unione che introduce cambiamenti che non appaiono in nessun genitore.

Con questa definizione tutti i conflitti risolti da

  1. prendere una delle modifiche e omettere l’altra.
  2. non prendere nessuno di loro e creare un nuovo cambiamento che è il consolidamento di entrambi.
  3. non prendere nessuno di loro e omettere entrambi.

il male si fonde.

Quindi arriviamo finalmente alle domande.

È legale a

  • prendi solo una delle modifiche e ometti l’altra?
  • prendere entrambe le modifiche?
  • non prendere nessuno di loro e creare un nuovo cambiamento che è il consolidamento di entrambi?
  • non prendere nessuno di loro e omettere entrambi?

E le cose possono diventare più complesse se pensiamo alla fusione di polpi.

La mia conclusione

L’unica fusione malvagia che possiamo rilevare è un’unione che è stata fatta senza conflitti. In questo caso è ansible ripetere l’unione e confrontarla con l’unione già eseguita. Se ci sono differenze rispetto a chi ha introdotto più di lui / lei dovrebbe e possiamo essere sicuri che questa fusione sia una fusione del male.

Almeno penso che dobbiamo rilevare il male si fonde manualmente, perché dipende dalla semantica dei cambiamenti e non sono in grado di formulare una definizione formale di cosa sia una fusione del male.

Ho ampliato la risposta di Joseph K. Strauss per creare due script di shell completi che possono essere facilmente utilizzati per ottenere un output diff significativo per un dato commit di unione.

Gli script sono disponibili in questo GitHub Gist: https://gist.github.com/smileyborg/913fe3221edfad996f06

Il primo script, detect_evil_merge.sh , utilizza la strategia di ripetere automaticamente l’unione di nuovo senza risolvere eventuali conflitti, quindi diffondendolo nell’attuale unione.

Il secondo script, detect_evil_merge2.sh , utilizza la strategia di ripetere automaticamente l’unione due volte, una volta risolti i conflitti con la versione del primo genitore, e la seconda risolve i conflitti utilizzando la versione del secondo genitore, quindi differisce ciascuno di quelli nell’effettiva unione .

Entrambi gli script faranno il lavoro, è solo una preferenza personale su quale modo si trova più facile capire come sono stati risolti i conflitti.

Disclaimer: Come sottolineato da @smileyborg, questa soluzione non rileverà un caso in cui la fusione del male ha completamente annullato una modifica introdotta da uno dei genitori. Questo difetto si verifica perché in base a Git Docs per l’opzione -c

Inoltre, elenca solo i file che sono stati modificati da tutti i genitori.

Recentemente ho scoperto una soluzione molto più semplice a questa domanda rispetto a qualsiasi delle risposte attuali.

Fondamentalmente, il comportamento predefinito di git show per i commit di unione dovrebbe risolvere il tuo problema. Nei casi in cui le modifiche da entrambi i lati della fusione non vengono toccate e non vengono apportate modifiche “malvagie”, non vi sarà alcun output diff. In precedenza avevo pensato che git show non mostra mai le differenze per i commit di unione. Tuttavia, se un commit di unione comporta un conflitto disordinato o un’unione male, allora un diff sarà visualizzato nel formato combinato .

Per visualizzare il formato combinato durante la visualizzazione di un numero di patch di commit con log -p , è sufficiente aggiungere il parametro --cc .

Nell’esempio fornito da GitHub nella domanda viene visualizzato quanto segue (con i miei commenti intervallati):

 $ git show 4a48c9 

( D nell’esempio)

 commit 4a48c9d0bbb4da5fb30e1d24ae4e0a4934eabb8d Merge: 0fbc6bb 086c3e8 Author: Tyler Fox  Date: Sun Dec 28 18:46:08 2014 -0800 Merge branch 'another_branch' Conflicts: file.txt diff --cc file.txt index 8be441d,f700ccd..fe5c38a --- a/file.txt +++ b/file.txt @@@ -1,9 -1,7 +1,9 @@@ This is a file in a git repo used to demonstrate an 'evil merge'. 

Le seguenti righe non sono malvagie. Le modifiche apportate dal primo genitore sono indicate da un + / - nella colonna più a sinistra; le modifiche apportate dal secondo genitore sono indicate da + / - nella seconda colonna.

 - int a = 0; - int b = 1; + int a = 1; + int b = 0; +int c = 2; - a = b; + b = a; a++; 

Ecco la parte malvagia: ++ era il cambiamento -- da entrambi i genitori. Notare i principali -- e ++ che indicano che questi cambiamenti avvengono da entrambi i genitori, nel senso che qualcuno ha introdotto nuove modifiche in questo commit che non erano già riflesse in uno dei genitori. Non confondere il principale, diff-generato ++ / -- con il trailing ++ / -- che è parte del contenuto del file.

 --b++; ++b-- ; 

Fine della malvagità

  +c++; 

Per visualizzare rapidamente tutti i commit di unione che potrebbero presentare problemi:

 git log --oneline --min-parents=2 --cc -p --unified=0 

Tutte le unioni non interessanti verranno visualizzate su una singola riga, mentre quelle disordinate, malvagie o no, mostreranno la differenza combinata.

Spiegazione:

  • -p – Mostra patch
  • --oneline – Visualizza ogni intestazione di commit su una singola riga
  • --min-parents=2 – Mostra solo le --min-parents=2 .
  • --cc – Mostra la differenza combinata, ma solo per i luoghi in cui le modifiche da entrambi i genitori si sovrappongono
  • --unified=0 – Visualizza 0 linee di contesto; Modifica il numero per essere più aggressivo nel trovare le unioni del male.

In alternativa, aggiungere quanto segue per eliminare tutti i commit non interessanti:

 -z --color=always | perl -pe 's/^([^\0]*\0\0)*([^\0]*\0\0)(.*)$/\n$2\n$3/' 
  • -z – Visualizza NUL anziché newline alla fine dei log di commit
  • --color=always – Non distriggersre il colore quando si --color=always piping su perl
  • perl -pe 's/^([^\0]*\0\0)*([^\0]*\0\0) – Massimizza l’output per hide le voci del registro con differenze vuote

La cosa più semplice è probabilmente la migliore qui: diff i risultati di un’automata non rimossa (e incompleta), senza conflitti risolti se ce ne sono, con i risultati di unione effettivi.

Risoluzioni ordinarie nostre / loro verranno visualizzate come tutte e 3 (4 per un 3diff) le linee del segnalino conflitto eliminate, e una parte o l’altra delle modifiche cambiate eliminate anche, che sarà facile da eyeball.

Qualsiasi alterazione delle modifiche di entrambe le diramazioni apparirà come un mix dall’aspetto strano, ad esempio qualsiasi hunk con aggiunta o rimozione gratuita verrà visualizzato all’esterno dei marker di conflitto.

Nell’esempio repo, dopo

 git clone https://github.com/smileyborg/EvilMerge git checkout master^ git merge --no-commit master^2 # --no-commit so w/ or w/o conflict work the same 

esegue il diff suggerito

 $ git diff -R master # -R so anything master adds shows up as an add diff --git b/file.txt a/file.txt index 3835aac..9851407 100644 --- b/file.txt +++ a/file.txt @@ -1,12 +1,6 @@ This is a file in a git repo used to demonstrate an 'evil merge'. -<<<<<<< HEAD -int a = 3; -

| merged common ancestors -int a = 1; -======= -int d = 1; ->>>>>>> master^2 +int d = 3; int b = 0; int c = 2; b = a; diff –git b/file2.txt a/file2.txt index d187a25..538e79f 100644 — b/file2.txt +++ a/file2.txt @@ -4,6 +4,6 @@ int x = 0; int y = 1; int z = 2; x = y; -x–; -y–; -z–; +x++; +y++; +z++;

ed è immediatamente chiaro che qualcosa è fasullo: in file.txt le modifiche su entrambi i rami sono state scartate e una linea da nessuna parte inserita., mentre in file2.txt non c’è mai stato un conflitto e l’unione modifica semplicemente il codice. Un po ‘di scavo mostra che qui è una inversione di commit, ma non importa, il punto è che i soliti cambiamenti seguono schemi facilmente riconoscibili e qualsiasi cosa insolita è facilmente rilevabile e vale la pena controllare.

Allo stesso modo, dopo

 git branch -f wip 4a48 git checkout wip^ git merge --no-commit wip^2 

esegue il diff suggerito

 $ git diff -R wip diff --git b/file.txt a/file.txt index 3e0e047..fe5c38a 100644 --- b/file.txt +++ a/file.txt @@ -1,19 +1,9 @@ This is a file in a git repo used to demonstrate an 'evil merge'. -<<<<<<< HEAD -int a = 0; -int b = 1; -int c = 2; -a = b; -

| merged common ancestors -int a = 0; -int b = 1; -a = b; -======= int a = 1; int b = 0; +int c = 2; b = a; ->>>>>>> wip^2 a++; -b++; +b–; c++;

e di nuovo la stranezza salta fuori: wip ha aggiunto un int c = 2 alle modifiche del ramo wip^2 , e ha b-- a b++ dal nulla.

Da qui puoi diventare carino e automatizzare alcune delle cose prevedibili per rendere più veloce la verifica di massa, ma questa è una domanda a parte.

Nota preliminare: sto usando qui la definizione di “Evil Merge” di Linus Torvalds, che come nota Junio ​​Hamano a volte può essere una buona cosa (ad esempio, per risolvere i conflitti semantici piuttosto che i conflitti testuali). Ecco la definizione di Linus:

una “fusione del male” è qualcosa che apporta cambiamenti che provengono da entrambi i lati e non risolvono realmente un conflitto [fonte: LKML]

Come ha notato @ joseph-k-strauss nella sua risposta , il problema con qualsiasi rilevamento di unione male basata esclusivamente su “-c” o “–cc” è questo:

“Inoltre, elenca solo i file che sono stati modificati da tutti i genitori.” [La fonte: man git-log]

E così per scoprire la malvagità di I, dobbiamo trovare i file modificati da alcuni, ma non tutti , dai suoi genitori.

Credo che le unioni pulite abbiano una proprietà simmetrica. Considera questo diagramma:

inserisci la descrizione dell'immagine qui

In una fusione pura le diagonali sono le stesse: b1 == m2 e b2 == m1 . Gli insiemi di linee modificate si sovrappongono solo quando si verificano conflitti e le unioni pulite non hanno conflitti. Quindi l’insieme delle modifiche in b2 deve corrispondere a m1 , poiché l’intero punto di b2 è riprodurre m1 sopra parent2, per portare parent2 in sync con parent1 (e ricordare — non c’erano conflitti). E viceversa per m2 e b1 .

Un altro modo di pensare a questa simmetria: quando rebase, essenzialmente buttiamo via b1 e sostituiamolo con m2 .

Quindi, se vuoi rilevare le unioni del male, puoi usare “git show -c” ​​per i file modificati da entrambi i genitori, e in caso contrario controlla che la simmetria valga per i quattro segmenti del diagramma usando “git diff –name-only”.

Se assumiamo la fusione dal diagramma è HEAD (ad esempio, vediamo se la fusione che ho appena commesso è malvagia), e usiamo la fantastica notazione git diff di tre punti che calcola merge-base per te, penso che hai solo bisogno di questi quattro righe:

 git diff --name-only HEAD^2...HEAD^1 > m1 git diff --name-only HEAD^1...HEAD^2 > b1 git diff --name-only HEAD^1..HEAD > m2 git diff --name-only HEAD^2..HEAD > b2 

Quindi analizzare i contenuti per vedere che m1 == b2 e b1 == m2 . Se non corrispondono, allora hai il male!

Qualsiasi uscita da uno di questi indica il male, dal momento che se prendiamo cat b1 e m2 e li ordiniamo, ogni riga dovrebbe apparire due volte.

 cat b1 m2 | sort | uniq -c | grep -v ' 2 ' cat b2 m1 | sort | uniq -c | grep -v ' 2 ' 

E per l’esempio EvilMerge, commit I restituisce quanto segue:

 cat b2 m1 | sort | uniq -c | grep -v ' 2 ' 1 file2.txt 

La modifica a “file2.txt” si è verificata solo una volta tra le diagonali b2 e m1 . L’unione non è simmetrica, quindi non è un’unione pulita. EVIL RILEVATO SUCCESSO!

Che dire del rifare l’unione “virtualmente” e confrontare il risultato? In altre parole

codice pseudo:

  1. a partire da I
  2. prendi i 2 genitori: G, H
  3. git checkout E
  4. git merge H
  5. ora hai nuovo-I.
  6. confronta I e new-I, usando git diff o confrontando l’output di git show I e git show new-I

Soprattutto l’ultimo passaggio sarà un duro lavoro, se si vuole farlo in modo completamente automatico, almeno se ci sono stati conflitti nel commit