it-swarm.com.de

Wie führe ich einen Drei-Wege-Vergleich in Git durch, ohne zu verschmelzen?

Ich möchte ein Drei-Wege-Diff zwischen zwei Git-Zweigen mit einer gemeinsamen Merge-Basis durchführen und es mit kdiff3 anzeigen.

Ich habe viele Anleitungen zu SO gefunden (und ein paar sehr ähnliche Fragen ( 1 , 2 , 3 )), aber keine direkte gefunden Antworten. Insbesondere ein Kommentar zu dieser Antwort impliziert, dass das, was ich will, möglich ist, aber es hat bei mir nicht funktioniert. Hoffentlich kann dieser Benutzer hier reinkommen :)

Im Hintergrund verwende ich beim Zusammenführen einen Diff3-Konfliktstil:

git config --global merge.conflictstyle diff3

Und ich habe git mergetool konfiguriert, um kdiff3 zu verwenden.

Bei der Lösung von Zusammenführungskonflikten werden mir vier Dateien angezeigt:

  1. Die aktuelle Branchendatei ($ LOCAL)
  2. Die Datei des anderen Zweigs ($ REMOTE)
  3. Die Datei, die der gemeinsame Vorfahr der beiden Zweige ist ($ BASE)
  4. Die zusammengeführte Ausgabedatei ($ MERGED)

git difftool ruft jedoch nur die beiden Verzweigungsspitzen auf. Ich möchte auch die Basisdatei sehen. Um klar zu sein, möchte ich in der Lage sein, dieses diff vor Zusammenführen durchzuführen, auch für Dateien ohne Zusammenführungskonflikte . (git mergetool zeigt nur die Dreiwege-Unterschiede an, wenn Konflikte bestehen).


Teillösung Nr. 1:

Mit einer einzelnen Datei kann ich die drei Versionen exportieren und das Diff manuell aufrufen:

git show local_branch:filename > localfile
git show remote_branch:filename > remotefile
git show `git merge-base local_branch remote_branch`:filename > basefile

{kdiff3_path}/kdiff3.exe --L1 "Base" --L2 "Local" --L3 "Remote" -o "outputfile" basefile localfile remotefile &

Hierbei gibt es zwei Probleme:

  1. Ich möchte, dass es für das gesamte Projekt funktioniert, nicht nur für eine bestimmte Datei.
  2. Das ist hässlich! Ich kann es skripten, aber ich hoffe, es gibt einen viel saubereren Weg, Standard-Git-Prozesse zu verwenden.

Teillösung Nr. 2:

Vielen Dank an diese Antwort und Kommentar für die Inspiration.

Erstellen Sie einen benutzerdefinierten Zusammenführungstreiber, der immer "false" zurückgibt, wodurch ein in Konflikt stehender Zusammenführungsstatus erstellt wird, ohne dass eine automatische Zusammenführung durchgeführt wird. Führen Sie dann den Vergleich mit git mergetool durch. Brechen Sie dann die Zusammenführung ab, wenn Sie fertig sind.

  1. Zu .git/config hinzufügen:

    [merge "assert_conflict_states"]
        name = assert_conflict_states
        driver = false
    
  2. Erstellen Sie .git/info/attributes (oder hängen Sie ihn an), damit bei allen Zusammenführungen der neue Treiber verwendet wird:

    * merge=assert_conflict_states
    
  3. Führen Sie die Zusammenführung durch, die jetzt keine automatische Zusammenführung mehr vornimmt.

  4. Mach den Unterschied. In meinem Fall: git mergetool, wodurch die Drei-Wege-Zusammenführung von kdiff3 aufgerufen wird.

  5. Wenn Sie fertig sind, brechen Sie die Zusammenführung ab: git merge --abort.

  6. Schritt 2 rückgängig machen.

Dies würde (sorta) funktionieren, mit der Ausnahme, dass kdiff3 beim Aufruf ein Automerge ausführt, sodass ich immer noch nicht die vorab zusammengeführten Diffs sehen kann. Ich kann dies jedoch beheben, indem ich die KDIFF3-Treiberdatei von Git ändere (.../git-core/mergetools/kdiff3 durch Entfernen des Schalters --auto).

Dies hat jedoch die folgenden Probleme:

  1. Dies funktioniert nur, wenn sich beide Dateien geändert haben! Wenn sich nur eine Datei geändert hat, ersetzt die aktualisierte Datei die ältere Datei und die Zusammenführung wird nie aufgerufen.
  2. Ich muss den Git kdiff3-Treiber ändern, der überhaupt nicht portierbar ist.
  3. Ich muss attributes vor und nach dem Diff ändern.
  4. Und natürlich hatte ich gehofft, dies zu tun, ohne zu verschmelzen :)

Informationen zum Kopfgeld:

Laut den gegebenen Antworten ist dies mit Standard-Git nicht möglich. Jetzt suche ich nach einer Out-of-the-Box-Lösung: Wie kann ich Git optimieren, um dies zu erreichen?

Hier ist ein Hinweis: Wenn sich nur eine der drei Dateien geändert hat, wird diese neuere Datei im Zusammenführungsergebnis verwendet, ohne den Zusammenführungstreiber aufzurufen. Dies bedeutet, dass mein benutzerdefinierter "Konflikt erzeugender" Zusammenführungstreiber in diesem Fall niemals aufgerufen wird. Wenn ja, würde meine "Teillösung Nr. 2" tatsächlich funktionieren.

Könnte dieses Verhalten durch Ändern von Dateien oder Konfigurationen geändert werden? Oder gibt es eine Möglichkeit, einen benutzerdefinierten Diff-Treiber zu erstellen? Ich bin nicht bereit, mit dem Git-Quellcode zu spielen ...

Irgendwelche klugen Ideen?

15
bitsmack

Ich möchte in der Lage sein, diese diff before -Mischung durchzuführen, auch bei Dateien ohne Zusammenführungskonflikte.

Sie müssen den Index nur so einrichten, wie Sie möchten, Sie müssen niemals Ergebnisse festlegen. Der Weg für genau das, was gefragt wurde, ist, gerade Unterschiede, da-Basis ohne Zusammenführungsvorbereitung

git merge -s ours --no-ff --no-commit $your_other_tip

ein kompletter Handroll, bei dem git nur Eltern für alles aufstellt, was Sie letztendlich als Ergebnis festlegen möchten, aber es ist wahrscheinlich besser, dies mit einer normalen Verschmelzung zu tun, während Sie trotzdem hineingehen und alles untersuchen können. 

git merge --no-ff --no-commit $your_other_tip

Wählen Sie Ihren Ausgangspunkt und dann

  1. erzwingen Sie einen Zusammenführungsbesuch für alle Einträge, die Änderungen in anzeigen.

    #!/bin/sh
    
    git checkout -m .
    
    # identify paths that show changes in either tip but were automerged
    scratch=`mktemp -t`
    sort <<EOD | uniq -u >"$scratch"
    $(  # paths that show changes at either tip:
        (   git diff --name-only  ...MERGE_HEAD
            git diff --name-only  MERGE_HEAD...
        ) | sort -u )
    $(  # paths that already show a conflict:
        git ls-files -u | cut -f2- )
    EOD
    
    # un-automerge them: strip the resolved-content entry and explicitly 
    # add the base/ours/theirs content entries
    git update-index --force-remove --stdin <"$scratch"
    stage_paths_from () {
            xargs -a "$1" -d\\n git ls-tree -r $2 |
            sed "s/ [^ ]*//;s/\t/ $3\t/" |
            git update-index --index-info
    }
    stage_paths_from "$scratch" $(git merge-base @ MERGE_HEAD) 1 
    stage_paths_from "$scratch" @ 2
    stage_paths_from "$scratch" MERGE_HEAD 3
    
  2. Wenn Sie vimdiff verwenden würden, wäre Schritt 2 nur git mergetool. vimdiff beginnt mit dem, was sich im Worktree befindet, und führt keine eigene automatische Generierung durch. Es sieht so aus, als würde kdiff3 den Worktree ignorieren. Jedenfalls, wenn Sie es einrichten, um ohne --auto zu laufen, sieht auch zu hackig aus:

    # one-time setup:
    wip=~/libexec/my-git-mergetools
    mkdir -p "$wip"
    cp -a "$(git --exec-path)/mergetools/kdiff3" "$wip"
    sed -si 's/--auto //g' "$wip"/kdiff3
    

    und dann kannst du 

    MERGE_TOOLS_DIR=~/libexec/my-git-mergetools git mergetool
    

Zurück von diesem ist nur der übliche git merge --abort oder git reset --hard.

8
jthill

Ich denke nicht, dass es möglich ist.

  1. Die Mischlogik ist eigentlich ziemlich komplex. Die Zusammenführungsbasis ist nicht notwendigerweise eindeutig und der Zusammenführungscode ist umständlich, um mit einer solchen Situation angemessen umzugehen, aber dies wird in keinem Diff-Code dupliziert.

  2. Git macht es leicht, zum vorherigen Zustand zurückzukehren. Bewahren Sie Ihre Änderungen auf, falls Sie welche haben, probieren Sie die Zusammenführung aus und --abort oder reset, wenn Sie genug gesucht haben und das Ergebnis nicht mehr benötigen.

2
Jan Hudec

Ich verwende das folgende grobe bash-Skript und verschmelze, um zu sehen, was geändert wurde - nachdem zwei Zweige zusammengeführt wurden:

#!/bin/bash

filename="$1"

if [ -z "$filename" ] ; then
    echo "Usage: $0 filename"
    exit 1
fi

if [ ! -f "$filename" ] ; then
    echo "No file named \"$filename\""
    exit 1
fi

hashes=$(git log --merges -n1 --parents --format="%P")

hash1=${hashes% *}
hash2=${hashes#* }
if [ -z "$hash1" || -z "$hash2" ] ; then
    echo "Current commit isn't a merge of two branches"
    exit 1
fi

meld <(git show $hash1:"$filename") "$filename" <(git show $hash2:"$filename")

Es kann wahrscheinlich gehackt werden, um die Unterschiede zwischen einer Datei im aktuellen Verzeichnis und zwei Zweigen zu sehen:

!/bin/bash

filename="$1"
hash1=$2
hash2=$3

if [ -z "$filename" ] ; then
    echo "Usage: $0 filename hash1 hash2"
    exit 1
fi

if [ ! -f "$filename" ] ; then
    echo "No file named \"$filename\""
    exit 1
fi

if [ -z "$hash1" || -z "$hash2" ] ; then
    echo "Missing hashes to compare"
    exit 1
fi

meld <(git show $hash1:"$filename") "$filename" <(git show $hash2:"$filename")

Ich habe das Skript nicht getestet. Es zeigt nicht an, wie git die Datei zusammenführen würde, aber es gibt Ihnen eine Vorstellung davon, wo die möglichen Konflikte sind.

1

Wirklich, das git diff3 Befehl sollte existieren. Die in der Antwort von @ FrédérirMarchal gezeigte meld -Lösung ist für eine Datei gut, aber ich möchte, dass sie über ganze Commits hinweg funktioniert. Also habe ich beschlossen, ein Skript zu schreiben, um genau das zu tun. Es ist nicht perfekt, aber es ist ein guter Anfang.

Installation:

  • kopieren Sie das Skript unten in git-diff3 irgendwo auf deinem Weg
  • installiere meld oder setze GIT_DIFF3_TOOL zu Ihrem bevorzugten Drei-Wege-Diff-Programm

Verwendungen:

  • git diff3 branch1 branch2: mache einen Drei-Wege-Unterschied zwischen branch1, die Zusammenführungsbasis von branch1 und branch2, und branch2.
  • git diff3 commit1 commit2 commit3: Machen Sie einen Drei-Wege-Unterschied zwischen den drei angegebenen Commits.
  • git diff3 HEAD^1 HEAD HEAD^2: mache nach dem Zusammenführen einen Drei-Wege-Unterschied zwischen HEAD und seinen beiden Eltern.

Einschränkungen:

  • Ich kann keine Dateien umbenennen. Es ist das Glück der Auslosung, wenn die umbenannte Datei in derselben Reihenfolge ist oder nicht.
  • Nicht wie git diff, mein Diff ist global über alle geänderten Dateien; Ich verankere den Unterschied an den Dateigrenzen. Mein ======== START $file ======== und ... END ... -Markierungen geben dem Diff ein paar Zeilen, die übereinstimmen, aber wenn es große Änderungen gibt, kann es dennoch zu Verwirrung kommen.

Das Drehbuch:

#!/bin/bash

GIT_DIFF3_TOOL=${GIT_DIFF3_TOOL:-meld}

if [[ $# == 2 ]]; then
   c1=$1
   c3=$2
   c2=`git merge-base $c1 $c3`
Elif [[ $# == 3 ]]; then
   c1=$1
   c2=$2
   c3=$3
else
   echo "Usages:
   $0 branch1 branch2 -- compare two branches with their merge bases
   $0 commit1 commit2 commit3 -- compare three commits
   $0 HEAD^1 HEAD HEAD^2 -- compare a merge commit with its two parents" >&2
   exit 1
fi
echo "Comparing $c1 $c2 $c3" >&2


files=$( ( git diff --name-only $c1 $c2 ; git diff --name-only $c1 $c3 ) | sort -u )

show_files() {
   commit=$1
   for file in $files; do
      echo ======== START $file ========
      git show $commit:$file | cat
      echo ======== " END " $file ========
      echo
   done
}

$GIT_DIFF3_TOOL <(show_files $c1) <(show_files $c2) <(show_files $c3)
1
joanis

Soweit ich mich erinnere, ist dies nicht möglich. Sie können die Mergebase gegen local_branch und die Mergebase gegen remote_branch unterscheiden, wie in der Antwort beschrieben, auf die Sie verwiesen haben. Ich denke jedoch, dass es noch keine Möglichkeit gibt, eine 3-Wege-Zusammenführung zu erhalten, wie Sie es von einem Standard-Git-Befehl verlangt haben. Sie können in der Git-Mailingliste anfordern, dass diese Funktion hinzugefügt wird.

1
Vampire