web-dev-qa-db-fra.com

Comment effectuer un diff à trois voies dans Git sans fusionner?

Je souhaite effectuer un diff à trois voies entre deux branches git avec une base de fusion commune et l'afficher avec kdiff3.

J'ai trouvé beaucoup de conseils sur SO (et quelques questions très similaires ( 1 , 2 , 3 )) mais je n'ai pas trouvé de réponse directe. réponse. Notamment, un commentaire sur cette réponse implique que ce que je veux est possible, mais cela n’a pas fonctionné pour moi. Espérons que cet utilisateur puisse entrer ici :)

Pour le fond, quand j'effectue des fusions, j'utilise un style de conflit "diff3":

git config --global merge.conflictstyle diff3

Et j'ai git mergetool configuré pour utiliser kdiff3.

Lors de la résolution des conflits de fusion, quatre fichiers sont affichés:

  1. Le fichier de la branche actuelle ($ LOCAL)
  2. Le fichier de l'autre branche ($ REMOTE)
  3. Le fichier qui est l'ancêtre commun des deux branches ($ BASE)
  4. Le fichier de sortie fusionné ($ MERGED)

Cependant, git difftool uniquement extraira les deux conseils de branche. Je veux aussi voir le fichier de base. Pour être clair, je veux pouvoir effectuer cette fusion diff avant, y compris sur des fichiers sans conflits de fusion . (git mergetool affiche uniquement les différences à trois voies s'il y a des conflits).


Solution partielle n ° 1:

Avec un fichier individuel, je peux exporter les trois versions et appeler manuellement le diff:

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 &

Il y a deux problèmes avec ceci:

  1. Je veux que cela fonctionne pour tout le projet, pas seulement pour un fichier spécifique.
  2. C'est moche! Je peux en faire un script, mais j'espère qu'il existe une méthode beaucoup plus propre en utilisant les processus git standard.

Solution partielle n ° 2:

Merci à cette réponse et commentaire pour l'inspiration.

Créez un pilote de fusion personnalisé qui renvoie toujours "false", ce qui crée un état de fusion en conflit sans effectuer de fusion automatique. Puis effectuez le diff en utilisant git mergetool. Puis annulez la fusion lorsque vous avez terminé.

  1. Ajouter à .git/config:

    [merge "assert_conflict_states"]
        name = assert_conflict_states
        driver = false
    
  2. Créez (ou ajoutez à) .git/info/attributes pour que toutes les fusions utilisent le nouveau pilote:

    * merge=assert_conflict_states
    
  3. Effectuez la fusion, ce qui ne permet plus d’automatisation.

  4. Faire le diff. Dans mon cas: git mergetool qui appelle la fusion à trois voies de kdiff3.

  5. Une fois terminé, annulez la fusion: git merge --abort.

  6. Annuler l'étape n ° 2.

Cela fonctionnerait (en quelque sorte) sauf que kdiff3 effectue un automerge à l'appel, donc I ne peut toujours pas voir les diffs pré-fusionnés. Je peux résoudre ce problème en modifiant le fichier de pilote kdiff3 de Git (.../git-core/mergetools/kdiff3 en supprimant le commutateur --auto.

Malgré tout, cela pose les problèmes suivants:

  1. _ {Cela ne fonctionne que lorsque les deux fichiers ont été modifiés! Dans le cas où un seul fichier a été modifié, le fichier mis à jour remplace l'ancien fichier et la fusion n'est jamais appelée.
  2. Je dois modifier le pilote Git kdiff3, qui n'est pas du tout portable.
  3. Je dois modifier attributes avant et après avoir fait le diff.
  4. Et bien sûr, j'espérais le faire sans fusionner :)

Informations sur la prime:

Selon les réponses données, cela n’est pas possible avec Git standard. Alors maintenant, je cherche une solution plus prête à l'emploi: comment puis-je modifier Git pour que cela se produise?

Voici une piste: apparemment, si un seul des trois fichiers a changé, ce fichier plus récent est utilisé dans le résultat de la fusion sans appeler le pilote de fusion. Cela signifie que mon pilote de fusion personnalisé "créant un conflit" n'est jamais appelé dans ce cas. Si c'était le cas, ma "solution partielle n ° 2" fonctionnerait réellement.

Ce comportement pourrait-il être modifié en modifiant des fichiers ou des configurations? Ou peut-être existe-t-il un moyen de créer un pilote de diff personnalisé? Je ne suis pas prêt à jouer avec le code source de Git ...

Des idées intelligentes?

15
bitsmack

Je veux pouvoir effectuer cette diff avant fusion, y compris sur des fichiers sans conflits de fusion.

Il vous suffit de configurer l’index comme vous le souhaitez, vous n’aurez jamais à engager de résultats. La façon de configurer exactement ce qui a été demandé, diffs-puisque-base, sans préparation de fusion, est

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

un handroll complet dans lequel git configure uniquement les parents pour tout ce que vous décidez par la suite de commettre, mais il est probablement préférable de le faire avec une fusion normale tout en pouvant toujours y entrer et tout examiner, 

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

Choisissez votre point de départ, puis

  1. forcer une visite de fusion pour toutes les entrées indiquant toute modification de

    #!/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. ... si vous utilisiez vimdiff, l'étape 2 serait simplement git mergetool. vimdiff part de ce qu'il y a dans l'arbre de travail et ne fait pas son propre automerge. Il semble que kdiff3 veuille ignorer la structure de travail. Anyhoo, le mettre en place pour fonctionner sans --auto ne regarde pas trop trop hacky:

    # 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
    

    et alors vous pouvez 

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

La sauvegarde de ceci est juste le git merge --abort ou git reset --hard habituel.

8
jthill

Je ne pense pas que c'est possible.

  1. La logique de fusion est en réalité assez complexe. La base de fusion n'est pas nécessairement unique et le code de fusion prend beaucoup de temps pour faire face à une telle situation de manière raisonnable, mais cela n'est pas dupliqué dans un code diff.

  2. Git facilite le retour à l'état précédent. Donc, rangez vos modifications si vous en avez, essayez la fusion, puis --abort ou reset lorsque vous avez suffisamment regardé et que vous n'avez plus besoin du résultat.

2
Jan Hudec

J'utilise le script bash brut et fusion suivants pour voir ce qui a été changé après fusionner deux branches:

#!/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")

Il peut probablement être piraté de voir les différences entre un fichier du répertoire courant et deux branches:

!/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")

Je n'ai pas testé ce script. Cela ne vous montrera pas comment git fusionnerait le fichier mais cela vous donnera une idée de l'emplacement des conflits potentiels.

1
Frédéric Marchal

Pour autant que je m'en souvienne, ce n'est pas possible. Vous pouvez différencier mergebase avec local_branch et mergebase contre remote_branch comme décrit dans la réponse que vous avez citée. Mais je pense qu’il n’ya pas encore de possibilité d’obtenir une fusion à trois comme vous l’aviez demandé avec une commande git standard. Vous pouvez demander sur la liste de diffusion Git que cette fonctionnalité soit ajoutée.

1
Vampire