【问题标题】:git merge multiple copies preserving historygit 合并多个副本保留历史
【发布时间】:2023-01-10 08:32:48
【问题描述】:

我有一个项目,在不同的地方有一些文件的多个副本。 例如:

src/location1/foobar.h
src/location1/foobar.cpp
src/location2/foobar.h
src/location2/foobar.cpp

我正在将这些提取到自己的库中。 所以我希望结束:

src/location3/foobar.h        combining multiple versions of foobar.h
src/location3/foobar.cpp      combining multiple versions of foobar.cpp

我已经通过了使用以下方法删除所有不需要的文件的第一个障碍:

git filter-repo --path-glob \*foobar\*

在此过程中发现 filter-branch 最近已被高级 filter-repo 取代(值得重复,因为 filter-branch 仍然出现在此处的许多最佳答案中)。

我现在想将这些副本合并为一个副本,保留它们的所有历史记录。 这两个候选人是mergemerge-file

merge-file 要求识别每个文件的共同祖先,这可能很痛苦:

src/location3/foobar.h

这在提交历史中是未知的。 我们有 git merge-base 来找到最好的共同祖先。

我不清楚如何为 git merge-file 指定文件版本 我想要做:

git mv src/location1/foobar.h src/newlocation/foobar.h
git commit
git merge-file src/newlocation/foobar.h src/location3/foobar@<commitid> src/location2/foobar.h
...
git merge-file src/newlocation/foobar.h src/location3/foobar@<commitid> src/location3/foobar.h

这是非常费力的,必须对每个文件重复。 另一种方法是创建多个临时分支:

git checkout -b newlibbranch
git mv src/location1/foobar.h src/newlocation/foobar.h
git mv src/location1/foobar.cpp src/newlocation/foobar.cpp
git commit
git checkout oldversion
git checkout -b v2
git mv src/location2/foobar.h src/newlocation/foobar.h
git mv src/location2/foobar.cpp src/newlocation/foobar.cpp
git commit
git checkout newlibbranch
git merge --allow-unrelated-histories v2

这也是相当辛苦的。尽管它可能是可编写脚本的。 还有一个实际问题,因为合并是“重命名/重命名”冲突,而不是实际文件的合并。 这似乎可以通过添加 --allow-unrelated-histories 来解决

所以我的问题是:

关于任务:

  1. 还有更好的方法吗?也许是一个我不知道的合并工具,就像我不知道 filter-repo
  2. 我认为多合并分支方式比 git merge-file 更好是正确的吗?

    关于合并文件:

    1. 如何为 git merge-file 指定文件的特定版本
    2. 是否有自动查找共同祖先的命令或脚本。 就像是:
          git merge-file-wrapper location1 location2   -->
    
          base = `git merge-base location1 location2`
          git merge-file location1 $base location2
    

    难道这不存在是因为有什么隐患?

【问题讨论】:

  • 请注意,Git 没有文件历史。 Git 有提交;提交历史。每个提交都包含每个文件(进入该提交)的完整快照。 Git 的git log 将通过成对检查提交(一次两个:父项和子项)来伪造合成文件历史记录,以查看两个提交中是否存在特定命名的文件并且具有相同的内容(= 提交无趣) (=提交很有趣)。仅此而已。
  • Git 的 git log 确实有 --follow,它通过查看每对提交来增强上述内容,以查看文件路径/到/A 是否消失并且新的/路径/B 是否存在具有足够相似的内容来调用它是一个“重命名”。在这种情况下,git log 将停止查找旧名称并开始查找新名称。这个技巧非常有限(一次只能查看一个名称)并且在很多情况下都失败了(分支和合并可以伪装重命名,就像更改重命名一样)。
  • git merge-file只是合并工作树文件的命令:必须手动找到要合并的三个文件。除了在 .gitattributes 设置中自动化之外,它并不意味着真正的工作。 git merge 基于提交和提交图工作,是真正的合并主力,但它在很大程度上取决于提交图是否正确。对于不相关的历史,图表永远不会正确。
  • 我已经回答了我自己的问题,这可能会使我想问的问题更清楚。

标签: git merge


【解决方案1】:

我还没有找到任何自动化工具来执行此操作,因此生态系统中可能存在缺口。

在我的例子中,我有多个文件要移动,其中一些文件比其他文件有更多的副本,这增加了一些有趣的复杂性,但在重构以删除重复时并不少见。

我最后做的是:

  • 编写一个脚本来创建一个新分支,其中每个变体都被移动到它的新位置。

  • 我的脚本首先识别要移动的文件。

  • 查找副本最多的文件并创建多个分支。

  • 对于每个分支,它尝试将每个文件的一个副本移动到其新位置

  • 然后我手动合并每个分支。

    这些合并中的大多数都是微不足道的事情,例如更改每个子项目的名称空间。

结果是一组文件,其中包含我想要的所有更改以及每个文件的所有更改历史记录。

为了使这个更具体一点:

  • 第 1 步:使用 filter-repo 创建一个只包含感兴趣文件的项目

    (注意这应该在项目的新克隆上完成)

     git filter-repo --path-glob *ThingIWant1* --path-glob *AnotherThingIWant* 
     git filter-repo --invert --path-glob *ThingIDontWant*
  • 第二步:创建分支
    #!/bin/bash
    
    # find unique filenames
    MAXLOCS=0
    FILES=`find . -not -path '*/.*' -type f | grep -v makebranch | xargs -ifile basename file | sort -u`
    for FILE in $FILES; do
        echo FILE=$FILE
        # find number of locations for each filename
        NUMLOCS=`find . -not -path '*/.*' -name $FILE | wc -l`
        if [ $NUMLOCS -gt $MAXLOCS ]; then
        MAXLOCS=$NUMLOCS
        fi
    done
    echo "$MAXLOCS branches required"
    
    # for each branch
    #  move one location of each file to its final destination
    L=0
    while [ $L -lt $MAXLOCS ]; do
        git checkout develop
        git checkout -b ps$L
        for FILE in $FILES; do
        echo FILE=$FILE
        LOCS=( $(find . -not -path '*/.*' -name $FILE) )
        NUMLOCS=${#LOCS[@]}
        if [ $L -lt $NUMLOCS ]; then
            LOC=${LOCS[$L]}
            echo "mv $LOC"
            # Move source files to one place and test files to another
            # In my case we have src and test
            echo $LOC | grep -q /src/
            if [ $? ]; then
                mkdir -p FinalDestinationForSource
                git mv $LOC FinalDestinationForSource/$FILE
                if [ $? -ne 0 ];then
                   echo "BAD: git mv $LOC FinalDestinationForSource/$FILE"
                fi
            else
                mkdir -p FinalDestinationForTests
                git mv $LOC FinalDestinationForTests/$FILE
                if [ $? -ne 0 ];then
                   echo "BAD: git mv $LOC FinalDestinationForTests/$FILE"
                fi
            fi
        fi 
        done
        git add -u
        git status
        git commit -m "#Ticket: move Things to new location $L"
        ((L = L + 1))
    done
  • 第三步:合并各个分支
    git checkout ps0
    git merge ps1 -X rename-threshold=5%
    # resolve manually... then
    git commit
    git merge ps2 -X rename-threshold=5%
    # resolve manually... then
    git commit

重命名阈值有助于让 git 相信这些文件具有相同的来源。 否则,一个版本可能会简单地替换另一个版本,而不会保留链接它们的更改历史记录。 我认为结果等同于使用 git commit-tree 链接多个提交 这将是解决这个问题的另一种方法。

您可以使用git blame 验证历史记录以查看每个文件中每一行的来源,并使用git log 查看实际提交。

Raymond Chen 对此有一个series of blogs,您可能对此感兴趣。他使用提交树来处理这个任务。我认为这可行,但我认为它对我的案例来说有点太低级了。

  • 第 4 步:将您的库合并到它所属的项目中

    这是为了完整性而包括在内,因为您可能将文件移动到另一个项目。 有关详细信息,请参阅“How do you merge two Git repositories?

    cd targetProject
    git remote add sourceProject /path/to/sourceProject
    git fetch sourceProject
    git merge --allow-unrelated-histories sourceProject/ps0

我认为这个领域已经成熟,可以贡献一个脚本来向 git 添加一个新的合并工具。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-04-17
    • 2017-06-28
    • 1970-01-01
    • 2014-12-17
    • 1970-01-01
    • 2021-12-26
    • 1970-01-01
    相关资源
    最近更新 更多