web-dev-qa-db-ja.com

マージせずにGitで3者間差分を実行する方法は?

共通のマージベースを持つ2つのgitブランチ間で3者間差分を実行し、kdiff3でそれを表示したいと思います。

SO(およびいくつかの非常に類似した質問( 12 、))しかし、直接的な答えは見つかりませんでした。特に、 この答え に関するコメントは、私が望むことは可能であることを意味しますが、私にとってはうまくいきませんでした。ここに :)

背景として、マージを実行するときは、「diff3」の競合スタイルを使用します。

git config --global merge.conflictstyle diff3

また、kdiff3を使用するようにgit mergetoolを構成しています。

マージの競合を解決すると、4つのファイルが表示されます。

  1. 現在のブランチのファイル($ LOCAL)
  2. 他のブランチのファイル($ REMOTE)
  3. 2つのブランチの共通の祖先であるファイル($ BASE)
  4. マージされた出力ファイル($ MERGED)

ただし、git difftoolは2つのブランチのヒントをプルアップするだけです。ベースファイルも見たいです。 明確にするために、私はこの差分を実行できるようにしたいbeforeマージの競合のないファイルも含めて。 (git mergetoolは、競合がある場合にのみ、3方向の差分を表示します)。


部分的なソリューション#1:

個別のファイルを使用して、3つのバージョンをエクスポートし、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 &

これには2つの問題があります。

  1. 特定のファイルだけでなく、プロジェクト全体で機能させたい。
  2. これは醜いです!スクリプトを作成することはできますが、標準のgitプロセスを使用したよりクリーンな方法があることを願っています。

部分的なソリューション#2:

この答えコメント にインスピレーションを与えてくれてありがとう。

常に「false」を返すカスタムマージドライバーを作成します。これは、実際には自動マージを行わずに競合するマージ状態を作成します。次に、git mergetoolを使用してdiffを実行します。その後、完了したらマージを中止します。

  1. .git/configに追加:

    [merge "assert_conflict_states"]
        name = assert_conflict_states
        driver = false
    
  2. .git/info/attributesを作成(または追加)して、すべてのマージで新しいドライバーを使用するようにします。

    * merge=assert_conflict_states
    
  3. 自動マージを行わないマージを実行します。

  4. 差分をとります。私の場合:git mergetoolは、kdiff3の3者間マージを表示します。

  5. 完了したら、マージを中止します:git merge --abort

  6. 手順2を元に戻します。

これは、(ソート)機能しますが、kdiff3が呼び出されたときに自動マージを実行するため、マージ済みの差分を確認できません。ただし、Gitのストックkdiff3ドライバーファイル(.../git-core/mergetools/kdiff3スイッチを削除して--autoを変更することで、これを修正できます。

それでも、これには次のショー停止の問題があります。

  1. これは両方のファイルが変更された場合にのみ機能します!1つのファイルだけが変更された場合、更新されたファイルが古いファイルを置き換え、マージが呼び出されることはありません。
  2. Git kdiff3ドライバーを変更する必要がありますが、これはまったく移植性がありません。
  3. 差分を行う前後にattributesを変更する必要があります。
  4. そしてもちろん、私はマージせずにこれを行うことを望んでいました:)

バウンティの情報:

与えられた回答によると、これは標準のGitでは不可能です。だから今、私はもっと独創的なソリューションを探しています:これを実現するためにGitをどのように調整できますか?

ここに1つのリードがあります。どうやら、3つのファイルのうち1つだけが変更された場合、実際にはマージドライバーを呼び出さずに、この新しいファイルがマージ結果で使用されます。つまり、この場合、カスタムの「競合を作成する」マージドライバが呼び出されることはありません。もしそうなら、私の「部分的ソリューション#2」は実際に機能します。

この動作は、ファイルまたは構成を調整することで変更できますか?または、カスタムの差分ドライバーを作成する方法はありますか? Gitのソースコードを試す準備ができていません...

賢いアイデアはありますか?

25
bitsmack

この差分を実行できるようにしたいbeforeマージ(マージの競合のないファイルを含む)。

好きなようにインデックスを設定するだけで、結果をコミットする必要はありません。要求されたもの、マージ準備なしのストレートdiffs-since-baseを正確にセットアップする方法は、

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

結果としてコミットすることを最終的に決定したものに対してのみgitが親を設定する完全なハンドロールですが、通常のマージでこれを実行し、そこにアクセスしてすべてを調べることができる方がおそらく良いでしょう。

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

出発点を選択してから

  1. いずれかのヒントの変更を示すすべてのエントリに対してマージアクセスを強制します。

    #!/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. ... vimdiffを使用している場合、ステップ2はgit mergetool。 vimdiffは、ワークツリーにあるものから始まり、独自の自動マージを行いません。 kdiff3がワークツリーを無視したいようです。 Anyhoo、それを--autoなしで実行するように設定すると、見えませんtooあまりにもハッキーです:

    # 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
    

    そして、あなたはできる

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

これからのバックアウトは通常のgit merge --abortまたはgit reset --hard

10
jthill

それは可能ではないと思います。

  1. マージロジックは実際には非常に複雑です。マージベースは必ずしも一意である必要はなく、マージコードはそのような状況に合理的に対処するために非常に長くなりますが、これはどのdiffコードでも複製されません。

  2. Gitを使用すると、以前の状態に簡単に戻ることができます。変更がある場合は隠して、マージしてから--abort itまたはreset十分に調べて、結果を必要としなくなった場合。

2
Jan Hudec

次の粗いbashスクリプトとmeldを使用して、何が変更されたかを確認しますafter 2つのブランチをマージします。

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

現在のディレクトリのファイルと2つのブランチの違いを確認するためにハッキングされる可能性があります。

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

そのスクリプトはテストしていません。 gitがファイルをマージする方法は示されませんが、潜在的な競合がどこにあるかがわかります。

1

本当に、git diff3コマンドは存在するはずです。 @FrédérirMarchalの回答に示されているmeldソリューションは1つのファイルに適していますが、コミット全体で機能するようにしたいと思います。だから私はそれを行うためのスクリプトを書くことにしました。完璧ではありませんが、良いスタートです。

インストール:

  • 以下のスクリプトをパスのどこかにgit-diff3にコピーします
  • meldをインストールするか、GIT_DIFF3_TOOLをお気に入りの3ウェイdiffプログラムに設定します

使用法:

  • git diff3 branch1 branch2branch1branch1branch2のマージベース、およびbranch2の間で3者間比較を行います。
  • git diff3 commit1 commit2 commit3:与えられた3つのコミットを3方向で比較します。
  • git diff3 HEAD^1 HEAD HEAD^2:マージを行った後、HEADとその2つの親の間で3方向の差分を行います。

制限:

  • ファイル名の変更は行いません。名前が変更されたファイルが同じ順序であるかどうかは、ドローの運です。
  • git diffとは異なり、私のdiffは変更されたすべてのファイルに対してグローバルです。ファイルの境界で差分を固定しています。私の======== START $file ========マーカーと... END ...マーカーは、一致する2行にdiffを提供しますが、大きな変更がある場合でも混乱する可能性があります。

スクリプト:

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

私が覚えている限り、これは不可能です。参照した回答で説明されているように、local_branchに対してmergebaseとremote_branchに対してmergebaseを比較できます。しかし、標準のgitコマンドで要求したような3者間マージを取得する機能はまだないと思います。この機能の追加をGitメーリングリストでリクエストする場合があります。

1
Vampire