如何在 Git 中获取合并文件的列表



对于给定的合并提交,如何找出哪些文件合并了来自两个或多个父项的更改(有或没有冲突(?

而且,这里有一个例子,只是为了更好地衡量:

A -- B -- C -- E-- .. 
      -- D --/  

我有以下文件

  • B 有 f1, f2, f3, f5, f6
  • C 修改 f1 和 f3。删除 f2
  • D 修改 f1、f3 和 f6。添加 f4。
  • E 是合并提交,有 f1、f3、f4、f5 和 f6。

我正在寻找在 E 中返回列表"f1 f3"的 git 命令,因为在 E 中,这是 C 和 D 仅更改的两个文件。所有其他的要么没有被触及,要么只由一个父母更新。

用例如下:公司有一个 SCM(不是 git(,开发人员在其中将变更集(文件列表(提交到临时分支。提交需要通过测试和同行评审的审查,然后才能被主要开发分支接受。 偶尔(我的意思是经常(,开发分支在提交后进行,此时需要合并(并重新合并(一些文件才能被接受到开发分支。

在上面的示例中,底线表示临时分支,D 是我的正在审核的变更集。顶行是主开发分支,C 是在此期间继续的提交。在 E 中,我的更改已获得批准,并已更新并与新的开发分支合并。现在的任务是提出一个我需要推送到公司 SCM 上游的文件列表(请记住,这是我需要提出的手动变更集(。 在 E 中更改的文件包括我在 D 中修改或添加的文件,并且已经向上游推送并且没有对应项或在开发分支(在 C 中(中未触及的文件。在 E 中还有其他人在 dev 分支中修改的文件,与我无关。 这些是具有单个父级的文件。其余的是合并的文件(由 Git 自动合并,或者在发生冲突时由我自己合并(。这就是我需要推上的清单。

(后期编辑:diff-tree的-c只列出了与所有父文件不同的文件,即正是所要求的:

git diff-tree -r -c $commit  # content that doesn't match any parent version

(
(后来编辑:以上内容实际上并不完全正确:请求的内容和下面的脚本打印的是自合并基础以来具有多个父项的所有文件。根据定义,所有此类文件都需要合并解析。差异忽略其合并解析为采用一个父项作为结果的文件。)


好的,从编辑中可以看出,您要生成一个文件列表,以检查合并驱动程序可能的错误合并,这些文件结合了至少两个父级的实际更改。 这将做你:

(编辑:正确处理不包含来自已更改父级的更改的合并;也合并@torek的简化。

substantive-merges-in ()
{
    set -- `git rev-list $1^! --parents`;
    child=$1;
    shift;
    base=$(git merge-base "$@")
    for parent; do
        git diff-tree $base $parent -r --name-only --diff-filter=M
    done 
    | sort 
    | uniq -d
}
substantive-merges-in master

测试:

git init t;cd t
git checkout -b first
# msysgit doesn't install `seq`?
for i in 1 2 3 4 5 6 7 8 9 10; do echo $i >>both; done
cp both justfirst
git add *; git commit -minitial
git branch second
sed -i s/3/3onfirst/ both
sed -i s/3/3onfirst/ justfirst
git commit -amtwochanges
git checkout second
sed -i s/7/7onsecond/ both
git commit -amonechange
git merge first
substantive-merges-in HEAD          # should list 'both'
git checkout -B second second@{1}
git merge --no-commit first
git checkout --ours both
git commit -amstomp
substantive-merges-in HEAD          # should still list 'both'

我想这会做到,也许有人知道更优雅的东西

doit ()
{
    set -- `git rev-list $1^! --parents`;
    child=$1;
    shift;
    for parent; do
        git diff-tree $parent $child -r --raw 
        | awk '$1~/:100/ && $5=="M" {sub(/[^t]*t/,""); print}';
    done 
    | sort -u
}
doit master

假设你的意思是:

  • 提交M是具有两个(或更多(父项(至少M^M^2(的合并提交
  • M的完整树是T
  • 您希望从T排除某些父级中不存在的任何文件

然后,一种简单的方法是从完整列表T开始,然后删除这些文件。 这是一个可以做到这一点的脚本,我认为它没有太多的魔力。 轻测试...

#! /bin/sh
PROG=$(basename $0)
case $# in
1) user_arg="$1";;
*) echo "usage: $PROG <commit>" >&2; exit 1;;
esac
# find full SHA1 of user-specified rev, plus all its parents
args=$(git rev-list --no-walk --parents "$user_arg") || exit 1
set -- $args
# omit this if you want to just list all files in a non-merge commit
case $# in
1|2) echo "$PROG: $user_arg is not a merge commit" >&2; exit 1;;
esac
# make temp file
TF=$(mktemp -t "$PROG") || exit 1
trap "rm -f $TF" 0 1 2 3 15
# save the SHA-1 of the commit, then toss that from arguments
c=$1
shift
# Now look at each parent: if the file was added between that
# parent and commit $c, it was not in that parent, so it's not
# "in common" across all parents to the final commit.  Dump
# such names into a "remove list".
#
# Remove duplicates from "remove" list.  Turn result into series
# of regexp's for "grep -v".  We need to:
#   1) protect any regexp metacharacters: turn . * ^ $ [  into
#      backslash-prefixed versions of same
#   2) add ^ at front and $ at end.
for parent do
    git diff-tree -r --name-only --diff-filter=A $parent $c
done | sort -u | sed -e 's/[.*^$[]/\&/g' -e 's/.*/^&$/' > $TF
# Now just run grep -v with that list, with input being the
# output of the "master list" of files in commit $c.
git ls-tree -r --name-only $c | grep -v -f $TF

如果你的意思是别的,上面的--diff-filter是可调的。

好的,让我们根据问题编辑处理一个不同的"相当精确"的定义。

鉴于:

  • 合并提交M
  • 最终树T包含文件f1f2、...、fn
  • 和直系父母p1p2、...、pn

您希望 - 无论其他可能的祖先1 如何 - 所有文件都fi给定任何两个不同的父级papbfipapb中都被"修改"。

这里"修改"的定义是,对于提交p和文件fp本身有一个单一的父级,p^(所以p既不是合并也不是根提交(,p:f(提交p中的文件f(与p^:f不同(可能甚至不存在p^(。

这表明使用以下明显(且完全未优化(的算法来查找树T中满足此约束的所有fi文件:

# set M = merge commit ID and P to its complete list of parents
# (see previous scripts for how to achieve that)
for f in $(git ls-tree -r $M); do
    found=false twice=false
    for p in $P; do
        $twice && continue # already announced
        if modified_in $p $f; then
           $found && twice=true || found=true
        fi
        $twice && echo $f  # announce if found twice
    done
done

其中modified_in定义为:

modified_in() {
    local p=$1 p_hat=$1^ path="$2"
    assert_single_parent $p # optional: verify neither root commit nor merge
    # (if you want to do this, it would be more efficient to do it once
    # outside the "for f in ..." loop)
    test ! -z "$(git diff-tree -r --diff-filter=AM $p_hat $p -- "$path")"
}

在这里,git diff-tree命令将输出一行,如下所示:

:100644 100644 <sha1_in_p^> <sha1_in_p> M   c

对于在 $p_hat$p 之间修改的文件(sha1值为 blob SHA-1(,以及:

:000000 100644 <null_sha1> <sha1_in_p> A    fgh

对于添加到此处的文件。 --diff-filter=AM确保没有删除的输出(否则你会在这里得到一个R(,并且-- "$path"将检查限制为仅给定的文件名路径。 我很确定(但尚未测试(您不必担心CR(复制编辑和重命名(,并且由于这些是提交树差异,而不是索引差异,U(未合并(不会发生。 因此,我们只需要使用该过滤器运行git diff-tree,并测试该命令是否打印任何内容。

(为了使这[可能]更有效,请在所有"感兴趣的"父级上运行所有可能的git diff-tree命令一次,而不指定路径,保存它们的输出,然后交叉关联列出的所有文件。 那些出现两次或更多次的人是你的候选人。 但这在脚本中要困难得多sh:你需要像awk这样的东西。

[编辑:不,毕竟你不需要awksort | uniq -d会做到这一点。 参见jthill的新答案,它实现了对问题的略有不同的解释的更有效的版本,也许更接近真正的意图,我承认我仍然感到困惑。


1也就是说,如果提交图看起来像这样,例如:

A -- B -- C -- D -- M -- .. 
      -- E -- F --/ 

您只关心DF中与M相关的更改,而不关心CE所做的更改。

如果你确实关心,你可能想要区分提交M,例如,通过将C-and-D和E-and-F分别挤压在一起制成的临时树;或者一直做成对比较,或者诸如此类。 基本上,您需要列出合并基础(提交B,此处(和合并本身(M(之间的 rev,然后弄清楚如何处理它们。

最新更新