如何在不合并的情况下在 Git 中执行三向差异? [英] How to perform a three-way diff in Git without merging?

查看:16
本文介绍了如何在不合并的情况下在 Git 中执行三向差异?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想在具有公共合并基础的两个 git 分支之间执行三向差异,并使用 kdiff3 进行查看.

我找到了很多关于 SO 的指导(以及一些非常相似的问题(123)) 但是我还没有找到直接的答案.值得注意的是,对 this answer 的评论暗示我想要的是可能的,但它对我不起作用.希望该用户可以在这里发言:)

对于背景,当我执行合并时,我使用diff3"冲突样式:

git config --global merge.conflictstyle diff3

我已经将 git mergetool 配置为使用 kdiff3.

解决合并冲突时,这会显示四个文件:

  1. 当前分支的文件($LOCAL)
  2. 另一个分支的文件 ($REMOTE)
  3. 作为两个分支的共同祖先的文件($BASE)
  4. 合并后的输出文件 ($MERGED)

然而,git difftool 只会拉起这两个分支提示.我也想看基础文件.明确地说,我希望能够在合并之前执行此差异,包括在没有合并冲突的文件上.(git mergetool 仅在存在冲突时显示三向差异).

<小时>

部分解决方案 #1:

使用单个文件,我可以导出三个版本并手动调用差异:

git show local_branch:filename >本地文件git show remote_branch:filename >远程文件git show `git merge-base local_branch remote_branch`:filename >基本文件{kdiff3_path}/kdiff3.exe --L1 "Base" --L2 "Local" --L3 "Remote" -o "outputfile" basefile localfile remotefile &

这有两个问题:

  1. 我希望它适用于整个项目,而不仅仅是特定文件.
  2. 这太丑了!我可以编写脚本,但我希望有一种使用标准 git 进程的更简洁的方法.

<小时>

部分解决方案 #2:

感谢这个答案评论以获得灵感.

创建一个总是返回false"的自定义合并驱动程序,这会创建一个冲突的合并状态,而实际上没有进行任何自动合并.然后使用 git mergetool 执行差异.然后在完成后中止合并.

  1. 添加到.git/config:

    [合并assert_conflict_states"]名称 = assert_conflict_states司机=假

  2. 创建(或附加到).git/info/attributes 使所有合并使用新驱动程序:

    * merge=assert_conflict_states

  3. 执行合并,现在不执行任何自动合并.

  4. 做差异.在我的例子中:git mergetool 它带来了 kdiff3 三路合并.

  5. 完成后,中止合并:git merge --abort.

  6. 撤消第 2 步.

除了 kdiff3 在调用时执行自动合并之外,这会(有点)工作,所以我仍然无法看到预合并的差异.不过,我可以通过更改 Git 的库存 kdiff3 驱动程序文件(.../git-core/mergetools/kdiff3 通过删除 --auto 开关来解决这个问题.>

即便如此,这也存在以下问题:

  1. 这仅在两个文件都已更改时有效!在只有一个文件更改的情况下,更新后的文件将替换旧文件并且永远不会调用合并.
  2. 我必须修改 Git kdiff3 驱动程序,它根本不可移植.
  3. 我必须在做差异之前和之后修改 attributes.
  4. 当然,我希望在不合并的情况下做到这一点:)

<小时>

赏金信息:

根据给出的答案,这在标准 Git 中是不可能的.所以现在我正在寻找一种更现成的解决方案:我如何调整 Git 来实现这一点?

这里有一个线索:显然,如果三个文件中只有一个发生了变化,这个较新的文件将用于合并结果中,而不实际调用合并驱动程序.这意味着在这种情况下永远不会调用我的自定义冲突创建"合并驱动程序.如果是这样,那么我的部分解决方案#2"实际上会起作用.

可以通过调整文件或配置来改变这种行为吗?或者也许有一种方法可以创建自定义差异驱动程序?我还没准备好开始玩 Git 源代码...

有什么聪明的想法吗?

解决方案

我希望能够在合并之前 执行此差异,包括没有合并冲突的文件.

你只需要设置你喜欢的索引,你永远不必提交结果.准确设置所要求的内容的方法,即没有合并准备的直接 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/shgit checkout -m .# 识别在任一提示中显示更改但已自动合并的路径划痕=`mktemp -t`排序 <<EOD |uniq -u >"$scratch"$( # 显示任一提示更改的路径:( git diff --name-only ...MERGE_HEADgit diff --name-only MERGE_HEAD...) |排序 -u )$( # 已经显示冲突的路径:git ls-files -u |切 -f2- )排爆# 取消自动合并它们:剥离已解析的内容条目并显式# 添加基本/我们的/他们的内容条目git update-index --force-remove --stdin <"$scratch"stage_paths_from () {xargs -a "$1" -d\n git ls-tree -r $2 |sed "s/[^ ]*//;s/	/$3	/" |git update-index --index-info}stage_paths_from "$scratch" $(git merge-base @ MERGE_HEAD) 1stage_paths_from "$scratch" @ 2stage_paths_from "$scratch" MERGE_HEAD 3

  2. ... 如果您使用 vimdiff,第 2 步将只是 git mergetool.vimdiff 从工作树中的内容开始,不进行自己的自动合并.看起来 kdiff3 想要忽略工作树.Anyhoo,将它设置为在没有 --auto 的情况下运行不会看起来太hacky:

    # 一次性设置:wip=~/libexec/my-git-mergetoolsmkdir -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 --abortgit reset --hard.

I want to perform a three-way diff between two git branches with a common merge base, and view it with kdiff3.

I've found lots of guidance on SO (and a few very similar questions (1, 2, 3) ) but I haven't found a direct answer. Notably, a comment on this answer implies that what I want is possible, but it didn't work for me. Hopefully that user might chime in here :)

For background, when I perform merges I use a "diff3" conflict style:

git config --global merge.conflictstyle diff3

And I have git mergetool configured to use kdiff3.

When resolving merge conflicts this shows me four files:

  1. The current branch's file ($LOCAL)
  2. The other branch's file ($REMOTE)
  3. The file which is the common ancestor of the two branches ($BASE)
  4. The merged output file ($MERGED)

However, git difftool only will pull up the two branch tips. I want to see the base file, too. To be clear, I want to be able to perform this diff before merging, including on files without merge conflicts. (git mergetool only shows the three-way diffs if there are conflicts).


Partial Solution #1:

With an individual file, I can export the three versions and manually call the 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 &

There are two problems with this:

  1. I want it to work for the whole project, not just a specific file.
  2. This is ugly! I can script it, but I hope there's a much cleaner way using standard git processes.


Partial Solution #2:

Thanks to this answer and comment for the inspiration.

Create a custom merge driver that always returns "false", which creates a conflicted merge state without actually doing any auto-merging. Then perform the diff using git mergetool. Then abort the merge when you're finished.

  1. Add to .git/config:

    [merge "assert_conflict_states"]
        name = assert_conflict_states
        driver = false
    

  2. Create (or append to) .git/info/attributes to cause all merges to use the new driver:

    * merge=assert_conflict_states
    

  3. Perform the merge, which now doesn't do any automerging.

  4. Do the diff. In my case: git mergetool which brings up the kdiff3 three-way merge.

  5. When done, abort the merge: git merge --abort.

  6. Undo step #2.

This would (sorta) work except that kdiff3 performs an automerge when called, so I still can't see the pre-merged diffs. I can fix this, though, by changing Git's stock kdiff3 driver file (.../git-core/mergetools/kdiff3 by removing the --auto switch.

Even so, this has the following show-stopping problems:

  1. This only works when both files have changed! In the case where only one file changed, the updated file replaces the older file and the merge is never called.
  2. I have to modify the Git kdiff3 driver, which isn't portable at all.
  3. I have to modify attributes before and after doing the diff.
  4. And, of course, I was hoping to do this without merging :)


Information for the bounty:

According to answers given, this isn't possible with standard Git. So now I'm looking for a more out-of-the-box solution: How can I tweak Git to make this happen?

Here's one lead: Apparently, if only one of the three files has changed, this newer file is used in the merge result without actually calling the merge driver. This means that my custom "conflict-creating" merge driver is never called in this case. If it was, then my "Partial Solution #2" would actually function.

Could this behavior be changed by tweaking files or configurations? Or perhaps there's a way to create a custom diff driver? I'm not ready to start playing with the Git source code...

Any clever ideas?

解决方案

I want to be able to perform this diff before merging, including on files without merge conflicts.

You just have to set up the index as you like, you never have to commit results. The way to set up for exactly what was asked, straight diffs-since-base with no merge prep, is

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

a complete handroll in which git only sets up parents for whatever you eventually decide to commit as the a result, but it's probably better to do this with a normal merge while still being able to get in there and examine everything,

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

Pick your starting point, and then

  1. force a merge visit for all entries that show any changes in either tip:

    #!/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/	/ $3	/" |
            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. ... if you were using vimdiff, step 2 would be just git mergetool. vimdiff starts from what's in the worktree and doesn't do its own automerge. It looks like kdiff3 wants to ignore the worktree. Anyhoo, setting it up to run without --auto doesn't look too too 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
    

    and then you can

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

Backout from this is just the usual git merge --abort or git reset --hard.

这篇关于如何在不合并的情况下在 Git 中执行三向差异?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆