所有文件重命名后的git合并

3

有其他答案涉及如何处理重命名的合并,但我的情况比较复杂,我认为这需要一个单独的问题。

我们有一个Git项目,最初由20多个仓库组成。我们使用包装脚本来处理许多标准的Git操作。因为我们现在要转移到GitHub,所以我们不能以这种方式处理项目。

因此,我们将所有存储库移动到单个存储库中,基本上使用saintgimp描述的方法。当然,这意味着所有文件现在都已被重命名,但历史上的SHAs是相同的。

好的,现在我想将分支source合并到分支target中,注意我确保在最后一次切换之前,两者是同步的。我的第一次尝试使用git merge <source>导致数千个冲突,关于在一侧或另一侧更改/删除的文件的投诉等。

然后我在高级合并页面上找到了一个宝石:

如果您想要做类似于此的事情,但不希望Git尝试合并另一侧的更改,则有一种更严格的选项,即“ours”合并策略。这与“ours”递归合并选项不同。

啊,这听起来就是我需要的。好的,我执行了以下操作:

$ git merge -s ours SHA

其中SHA是从合并中最后提交的。换句话说,我希望所有历史记录,包括SHA在内,都被视为已经合并到target中。我的希望是这将是一次性合并,并且将修复所有未来的合并。

现在,当我尝试合并来自source的第一个新提交时,效果是正确的,但我仍然会收到以下警告:

[user@host src] git merge --no-commit next_unmerged_commit
Auto-merging /path/to/file/changed/foo.c
warning: inexact rename detection was skipped due to too many files.
warning: you may want to set your merge.renamelimit variable to at least 5384 and retry the command.
Automatic merge went well; stopped before committing as requested

事实上,如果我将renamelimit设置为10000,那么下一个合并(称之为B)将在没有警告的情况下执行,但代价是性能明显变慢。再次强调,一次性的代价是可以接受的,如果我的后续合并恢复到正常状态,我会支付这个代价。
下一个合并C使用默认的renamelimit,再次出现警告。
因此,我的问题是:如何说服git目标分支与源分支同步,以便它停止尝试在重新统一之前回溯历史记录?我希望能够进行合并,而不需要增加renamelimit,以避免性能下降。

我注意到你在第二个例子中使用了 --no-commit。如果你在第一个例子中也这样做了(git merge -s ours),那么这就可以解释问题了。如果不是这样,提供更多关于你如何陷入这种情况的细节可能会有所帮助(你发布的第一个链接谈到了使用子树合并,因此更多细节可能很重要)。 - torek
我的问题是:你将来要提交的新分支,是在最初的大合并之前还是之后创建的分支?在你描述的情况下,我预计这样的未来合并会很快,只有来自于大合并之前的分支合并才会很慢。你的工作流程是否允许放弃先于大合并的分支,或者至少逐步淘汰它们? - joanis
第二个问题,旨在更直接地回答您的问题:git merge-base master target 指向超级合并之前还是之后的提交?第三个问题:sourcetarget中的文件名是否相同,还是重命名前后的名称? - joanis
@joanis 在大合并之前,sourcetarget都已经创建了。是的,我们最终会放弃source,但至少还需要几个月的时间。 - tanager
问题3:我认为初始的git merge -s ours之后的所有合并都应该是针对发生在重新统一之后的提交,这意味着文件名不应更改。我故意选择了SHA(如上面的示例),以便git merge -s ours SHA将包括作为重新统一一部分的所有活动。 - tanager
显示剩余4条评论
1个回答

2
这个回答并不是很好,因为它更多地涉及到你使用的脚本 - 或者我应该说,你没有使用的脚本,因为你在评论中说你使用了基于链接脚本的脚本。但是我将展示一下在某些原始仓库的假设脚本转换中得到的相当混乱的图形。请注意,这个特定的脚本让所有的转换都有一个合并基础提交,实际上是提交B,而提交B本身是空的。
你的问题是:
现在我想将分支source合并到分支target中,注意在最后的切换之前我确保两者同步。
如下所示,所有新的分支都以它们来自的项目命名 - 没有明确的方法将sourcetarget映射到P或Q等。但如果你运行:
git checkout P/master
git merge Q/master

在下面所示的过程之后,此git merge的合并基础将是空提交-B,合并将顺利进行:Git会将我绘制的提交视为分别是DH,跟踪它们的祖先,找到提交B作为它们的合并基础,并运行两个git diff


git diff <hash-of-B> <hash-of-D>   # what we did on P/master
git diff <hash-of-B> <hash-of-H>   # what they did on H/master

这些git diff的输出会说每个文件都是从头开始创建的,并且它们的名称都不同:在P/master中的所有内容都被命名为P/*,而在H/master中的所有内容都被命名为Q/*。没有名称冲突,合并将自行完成。
显然,这不是你正在做的事情。但是你正在做的事情,以及哪个提交是合并基础,仍然很神秘。看起来你正在挑选出两个尖端提交,使得这两个提交的合并基础是一个具有文件的提交,并且这些文件还没有从基线到尖端重命名。
链接的脚本的目的是设置一切,使得每个不相关项目的合并基础都是一个空的提交。也许,在该脚本之后或替代该脚本,最好进行一次所有最终提交的大章鱼合并(NB:这未经测试,可能很明显)。
git checkout P/master                 # just to be somewhere that's not master
git branch -d master                  # discard existing master branch name
git checkout --orphan master          # arrange to create new master
git merge P/master Q/master R/master  # make big octopus merge to marry all projects

这个章鱼合并的合并基础将再次是提交B,结果将是一个合并,将所有项目都带入它们的新的project/*名称下。原始存储库现在大多无用,但如果它们有新的提交,您可以从中提取,添加重命名提交,并从重命名提交合并(如果导入脚本没有删除已添加的远程,则此操作将更容易)。
关于链接脚本的工作观察:
我从未面对过这个特定的问题,但脚本中的方法似乎是一个不错的起点。我可能会以稍微不同的方式做,不需要空的合并基础,并使用git read-treegit commit-tree来构建和创建最终的章鱼合并。主要关键是在下面的草图中在每个传入的项目分支(P/*Q/*等)末尾添加一个重命名提交。
脚本似乎是这样工作的。它的输入为P、Q、R三个项目(URL的最后一个组成部分被视为项目名称)。
  1. Make empty repo.
  2. Make two iniial commits:

    A--B   <-- master
    

提交A有一个文件,提交B没有文件(为什么不直接将空树作为B提交?但是无论如何)。

最初的回答:

  1. Loop, for all three projects. Here I have expanded the loop to view what's happening.

  2. (loop iteration 1) git remote add P <url> and git fetch P (with --tags!?). We're going to assume here that P has master and dev.

    A--B   <-- master
    
    P1-P2-...-Pm   <-- origin/P/master
           \
            Pd   <-- origin/P/dev
    
  3. Use git ls-remote --heads to find names for commits in P, i.e., the same set of names we have in refs/remotes/P/*. (Assumes the remote hsa not changed during fetch -- unwise but probably OK.)

    Loop over these names. Result again expanded in line for illustration...

  4. Run git checkout -b P/master master. Effect:

    A--B   <-- master, P/master
    
    P1-P2-...-Pm   <-- origin/P/master
           \
            Pd   <-- origin/P/dev
    
  5. Run git reset --hard for no apparent reason: no effect. Perhaps this might have some effect on some later step.

  6. Run git clean -d --force for no apparent reason: no effect.

  7. Run git merge --allow-unrelated-histories --no-commit remotes/P/master" (does merge, but does not commit yet) and then rungit commit -m ...`. Effect:

    A--B   <-- master
        \
         \-------C   <-- P/master
                /
    P1-P2-...-Pm   <-- origin/P/master
           \
            Pd   <-- origin/P/dev
    
  8. Maybe rename files, with somewhat squirrelly code (lines 160-180): if project P has one top level directory named P, do nothing, otherwise create directory named P (with no check to see if this fails) and then in effect:

    git mv all-but-P P/
    git commit -m "[Project] Move ${sub_project} files into sub directory"
    

    giving:

    A--B   <-- master
        \
         \-------C--D   <-- P/master
                /
    P1-P2-...-Pm   <-- origin/P/master
           \
            Pd   <-- origin/P/dev
    

    Note that the git mv is given -k so that it does nothing if one of the git mv operations would have failed. However, except for subdirectory P and .git itself, all files in the top level of the work-tree should be in the index and the git mv should succeed unless one of them is named P (in which case, yikes!).

    I assume here that we did the mv, otherwise commit D does not exist.

  9. Repeat loop (see step 5) for dev. Run git checkout -b P/dev master:

    A--B   <-- master, P/dev
        \
         \-------C--D   <-- P/master
                /
    P1-P2-...-Pm   <-- origin/P/master
           \
            Pd   <-- origin/P/dev
    
  10. Presumably-ineffectual git reset and git clean again as in steps 7 and 8. (This might do something if the git mv in step 10 went really badly?) Do a funky two step merge as in step 9, giving:

    A--B   <-- master
       |\
       | \-------C--D   <-- P/master
                /
    P1-P2-...-Pm   <-- origin/P/master
           \
         \  Pd   <-- origin/P/dev
          \   \
           \---E   <-- P/dev
    

    where the line down from B connects to the one up from E. The graph has gotten rather out of hand at this point.

  11. Rename and commit as in step 10. I assume here that the project isn't already in a subdirectory, in both master, as already assumed, and dev.

    A--B   <-- master
       |\
       | \-------C--D   <-- P/master
                /
    P1-P2-...-Pm   <-- origin/P/master
           \
         \  Pd   <-- origin/P/dev
          \   \
           \---E--F   <-- P/dev
    
  12. Really ugly attempt to rename tags, at lines 190-207. This should have been done at fetch time, using a clever refspec. Whoever wrote this probably was not aware of annotated vs lightweight tags. It is not clear to me whether this works correctly and I did not look closely. Let's just assume no tags for now.

  13. Remove remote P. This removes the origin/P/* names too, but of course the commits stick around as they're retained by the new P/* branches:

    A--B   <-- master
       |\
       | \-------C--D   <-- P/master
                /
    P1-P2-...-Pm
           \
         \  Pd
          \   \
           \---E--F   <-- P/dev
    
  14. Repeat outer loop (step 3) with remote Q. We'll add Q and fetch (again with --tags, not a good plan as noted in step 14, but let's just assume no tags). So now we get another disjoint subgraph with origin/Q/* names. For simplicity let's just assume that only origin/Q/master exists this time:

    A--B   <-- master
       |\
       | \-------C--D   <-- P/master
                /
    P1-P2-...-Pm
           \
         \  Pd
          \   \
           \---E--F   <-- P/dev
    
    Q1-Q2-...-Qm   <-- origin/Q/master
    
  15. Run git checkout -b Q/master master:

    A--B   <-- master, Q/master
       |\
       | \-------C--D   <-- P/master
                /
    P1-P2-...-Pm
           \
         \  Pd
          \   \
           \---E--F   <-- P/dev
    
    Q1-Q2-...-Qm   <-- origin/Q/master
    
  16. Run the (probably ineffectual and still mysterious) git reset --hard and git clean steps.

  17. Use the funky two step merge with --allow-unrelated-histories to create new commit G like this:

         ---------------G   <-- Q/master
        /               |
    A--B   <-- master   | (down to Qm)
       |\
       | \-------C--D   <-- P/master
                /
    P1-P2-...-Pm
           \
         \  Pd
          \   \
           \---E--F   <-- P/dev
    
                 / (up to G)
                /
    Q1-Q2-...-Qm   <-- origin/Q/master
    
  18. Again, optional: rename all files in G to live in Q/ and commit. Again let's assume this does happen:

         ---------------G--H   <-- Q/master
        /               |
    A--B   <-- master   | (down to Qm)
       |\
       | \-------C--D   <-- P/master
                /
    P1-P2-...-Pm
           \
         \  Pd
          \   \
           \---E--F   <-- P/dev
    
                 / (up to G)
                /
    Q1-Q2-...-Qm   <-- origin/Q/master
    
  19. Ugly attempt to rename tags; we'll ignore this.

  20. Remove remote Q and origin/Q/* names. (No need to draw this.)

  21. Repeat outer loop for repository R. Assuming it has only its own master, we'll get a tangled graph like this:

         --------------------I--J   <-- R/master
        /                    | (down to Rm)
       /
       | ---------------G--H   <-- Q/master
       |/               |
    A--B   <-- master   | (down to Qm)
       |\
       | \-------C--D   <-- P/master
                /
    P1-P2-...-Pm
           \
         \  Pd
          \   \
           \---E--F   <-- P/dev
    
                 / (up to G)
                /
    Q1-Q2-...-Qm
                    / (up to I)
                   /
    R1-R2-...----Rm
    
最初答案

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接