合并两个 Git 存储库而不破坏文件历史记录

Posted

技术标签:

【中文标题】合并两个 Git 存储库而不破坏文件历史记录【英文标题】:Merge two Git repositories without breaking file history 【发布时间】:2012-10-14 00:08:14 【问题描述】:

我需要将两个 Git 存储库合并到一个全新的第三个存储库中。我发现了许多关于如何使用子树合并(例如How do you merge two Git repositories? 上的Jakub Narębski's answer)的描述,并且遵循这些说明大部分都有效,除了当我提交子树合并时,旧存储库中的所有文件都是记录为新添加的文件。当我执行git log 时,我可以从旧存储库中看到提交历史记录,但如果我执行git log <file>,它只会显示该文件的一个提交——子树合并。从上述答案的 cmets 来看,我并不是唯一一个看到这个问题的人,但我没有找到任何已发布的解决方案。

有什么方法可以合并存储库并保留单个文件历史记录吗?

【问题讨论】:

我没有使用 Git,但在 Mercurial 中,如果需要,我会先进行转换以修复要合并的存储库的文件路径,然后将一个存储库强制拉入目标以获取变更集,然后合并不同的分支。这是经过测试并且有效的;)也许这也有助于为Git找到解决方案......与子树合并方法相比,我猜转换步骤在重写历史而不是仅仅映射路径时是不同的(如果我理解正确)。这样可以确保顺利合并,而无需对文件路径进行任何特殊处理。 我也发现这个问题很有帮助***.com/questions/1683531/… 我创建了一个后续问题。可能很有趣:合并两个 Git 存储库并保留主历史记录:***.com/questions/42161910/… 对我有用的自动化解决方案是***.com/a/30781527/239408 【参考方案1】:

事实证明,如果您只是尝试将两个存储库粘合在一起并使其看起来一直都是这样,而不是管理外部依赖项,那么答案会简单得多。您只需将遥控器添加到旧存储库,将它们合并到新主库,将文件和文件夹移动到子目录,提交移动,然后重复所有其他存储库。子模块、子树合并和花哨的变基旨在解决稍微不同的问题,并不适合我想要做的事情。

这是一个将两个存储库粘合在一起的示例 Powershell 脚本:

# Assume the current directory is where we want the new repository to be created
# Create the new repository
git init

# Before we do a merge, we have to have an initial commit, so we'll make a dummy commit
git commit --allow-empty -m "Initial dummy commit"

# Add a remote for and fetch the old repo
# (the '--fetch' (or '-f') option will make git immediately fetch commits to the local repo after adding the remote)
git remote add --fetch old_a <OldA repo URL>

# Merge the files from old_a/master into new/master
git merge old_a/master --allow-unrelated-histories

# Move the old_a repo files and folders into a subdirectory so they don't collide with the other repo coming later
mkdir old_a
dir -exclude old_a | %git mv $_.Name old_a

# Commit the move
git commit -m "Move old_a files into subdir"

# Do the same thing for old_b
git remote add -f old_b <OldB repo URL>
git merge old_b/master --allow-unrelated-histories
mkdir old_b
dir –exclude old_a,old_b | %git mv $_.Name old_b
git commit -m "Move old_b files into subdir"

如果您愿意,显然您可以将 old_b 合并到 old_a(这将成为新的组合存储库)——修改脚本以适应。

如果您还想引入正在进行的功能分支,请使用:

# Bring over a feature branch from one of the old repos
git checkout -b feature-in-progress
git merge -s recursive -Xsubtree=old_a old_a/feature-in-progress

这是该过程中唯一不明显的部分 - 这不是子树合并,而是正常递归合并的一个参数,它告诉 Git 我们重命名了目标并帮助 Git 正确排列所有内容。

我写了一个稍微详细一点的解释here。

【讨论】:

这个使用git mv 的解决方案效果不佳。当您稍后在其中一个移动文件上使用 git log 时,您只能从移动中获得提交。所有以前的历史都丢失了。这是因为 git mv 实际上是 git rm; git add 但 in one step。 它与 Git 中的任何其他移动/重命名操作相同:在命令行中,您可以通过 git log --follow 获取所有历史记录,或者所有 GUI 工具都会自动为您执行此操作。据我所知,通过子树合并,您无法获取单个文件的历史记录,因此这种方法更好。 @EricLee 合并 old_b 存储库时,我遇到了很多合并冲突。这是预期的吗?我得到冲突(重命名/删除) 当我尝试 "dir -exclude old_a | %git mv $_.Name old_a" 时,我得到 sh.exe": dir: command not found and sh.exe": git: command未找到。使用这个作品: ls -I old_a | xargs -I '' git mv '' old_a/ 这是1(第一号)代表ls,大写“眼睛”代表xargs。谢谢你的提示!【参考方案2】:

这是一种不会重写任何历史记录的方法,因此所有提交 ID 都将保持有效。最终结果是第二个 repo 的文件最终会放在一个子目录中。

    将第二个 repo 添加为远程:

    cd firstgitrepo/
    git remote add secondrepo username@servername:andsoon
    

    确保您已下载所有 secondrepo 的提交:

    git fetch secondrepo
    

    从第二个 repo 的分支创建一个本地分支:

    git branch branchfromsecondrepo secondrepo/master
    

    将其所有文件移动到子目录中:

    git checkout branchfromsecondrepo
    mkdir subdir/
    git ls-tree -z --name-only HEAD | xargs -0 -I  git mv  subdir/
    git commit -m "Moved files to subdir/"
    

    将第二个分支合并到第一个 repo 的 master 分支:

    git checkout master
    git merge --allow-unrelated-histories branchfromsecondrepo
    

您的存储库将有多个根提交,但这不构成问题。

【讨论】:

第 2 步对我不起作用:致命:不是有效的对象名称:'secondrepo/master'。 @Keith:确保您已将第二个仓库添加为名为“secondrepo”的远程仓库,并且该仓库有一个名为“master”的分支(您可以使用命令git remote show secondrepo) 我也必须进行取回才能将其关闭。在 1 到 2 之间,我做了 git fetch secondrepo @monkjack:我已经编辑了我的答案以包含一个 git fetch 步骤。以后随时可以自己编辑答案。 @MartijnHeemels 对于旧版本的 Git,只需省略 --allow-unrelated-histories。查看此回答帖子的历史记录。【参考方案3】:

几年过去了,有一些基于良好的投票解决方案,但我想分享我的,因为它有点不同,因为 我想在不删除历史记录的情况下将 2 个远程存储库合并到一个新存储库中以前的存储库。

    在 Github 中创建一个新的存储库。

    下载新创建的仓库并添加旧的远程仓库。

    git clone https://github.com/alexbr9007/Test.git
    cd Test
    git remote add OldRepo https://github.com/alexbr9007/Django-React.git
    git remote -v
    

    从旧仓库中获取所有文件,以便创建一个新分支。

    git fetch OldRepo
    git branch -a
    

    在 master 分支中,进行合并以将旧的 repo 与新创建的 repo 合并。

    git merge remotes/OldRepo/master --allow-unrelated-histories
    

    创建一个新文件夹来存储从 OldRepo 添加的所有新创建的内容,并将其文件移动到这个新文件夹中。

    最后,您可以从合并的 repos 上传文件并从 GitHub 安全删除 OldRepo。

希望这对任何处理合并远程存储库的人有用。

【讨论】:

这是唯一对我有用的保存 git 历史的解决方案。不要忘记使用git remote rm OldRepo 删除旧仓库的远程链接。 我对此赞不绝口。一个非常简单、成功、明智的解决方案。谢谢!感谢@Harubiyori 最后的润色。【参考方案4】:

假设您要将存储库 a 合并到 b(我假设它们并排放置):

cd b
git remote add a ../a
git fetch a
git merge --allow-unrelated-histories a/master
git remote remove a

如果您想将a 放入子目录,请在上述命令之前执行以下操作:

cd a
git filter-repo --to-subdirectory-filter a
cd ..

为此,您需要安装 git-filter-repofilter-branch 是 discouraged)。

合并 2 个大存储库的示例,将其中一个放入子目录:https://gist.github.com/x-yuri/9890ab1079cf4357d6f269d073fd9731

更多信息here。

【讨论】:

是否可以做到不发生合并冲突? @Mikhail 是的,有可能,您是否看到要点中的合并冲突?如果您遇到合并冲突,这意味着您有例如两个存储库中的文件a/b/c。在合并之前重命名文件,或者合并到子目录,或者解决冲突。 好的。谢谢。解决它的冲突 这是保存文件历史而不依赖于--follow的完美解决方案,谢谢!【参考方案5】:

请看使用

git rebase --root --preserve-merges --onto

在他们生命的早期将两个历史联系起来。

如果您有重叠的路径,请使用

修复它们
git filter-branch --index-filter

当你使用日志时,确保你“更难找到副本”

git log -CC

这样你会发现路径中文件的任何移动。

【讨论】:

Git 文档建议不要变基...git-scm.com/book/en/v2/Git-Branching-Rebasing#_rebase_peril【参考方案6】:

我把solution从@Flimm变成了git alias这样的(添加到我的~/.gitconfig):

[alias]
 mergeRepo = "!mergeRepo()  \
  [ $# -ne 3 ] && echo \"Three parameters required, <remote URI> <new branch> <new dir>\" && exit 1; \
  git remote add newRepo $1; \
  git fetch newRepo; \
  git branch \"$2\" newRepo/master; \
  git checkout \"$2\"; \
  mkdir -vp \"$GIT_PREFIX$3\"; \
  git ls-tree -z --name-only HEAD | xargs -0 -I  git mv  \"$GIT_PREFIX$3\"/; \
  git commit -m \"Moved files to '$GIT_PREFIX$3'\"; \
  git checkout master; git merge --allow-unrelated-histories --no-edit -s recursive -X no-renames \"$2\"; \
  git branch -D \"$2\"; git remote remove newRepo; \
; \
mergeRepo"

【讨论】:

只是好奇:你真的经常这样做以至于需要别名吗? 不,我不记得但不记得怎么做,所以别名只是我记住它的一种方式。 是的.. 但是尝试更换计算机并忘记移动您的别名 ;) $GIT_PREFIX 的值是多少? github.com/git/git/blob/… 'GIT_PREFIX' 设置为通过从原始当前目录运行 'git rev-parse --show-prefix' 返回。见链接git:git-rev-parse[1]。【参考方案7】:

这个函数会将远程仓库克隆到本地仓库目录:

function git-add-repo

    repo="$1"
    dir="$(echo "$2" | sed 's/\/$//')"
    path="$(pwd)"

    tmp="$(mktemp -d)"
    remote="$(echo "$tmp" | sed 's/\///g'| sed 's/\./_/g')"

    git clone "$repo" "$tmp"
    cd "$tmp"

    git filter-branch --index-filter '
        git ls-files -s |
        sed "s,\t,&'"$dir"'/," |
        GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &&
        mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
    ' HEAD

    cd "$path"
    git remote add -f "$remote" "file://$tmp/.git"
    git pull "$remote/master"
    git merge --allow-unrelated-histories -m "Merge repo $repo into master" --edit "$remote/master"
    git remote remove "$remote"
    rm -rf "$tmp"

使用方法:

cd current/package
git-add-repo https://github.com/example/example dir/to/save

注意。此脚本可以重写提交,但会保存所有作者和日期,这意味着新提交将有另一个哈希值,如果您尝试将更改推送到远程服务器,它只能使用强制密钥,它也会重写服务器上的提交。所以请在启动前做好备份。

利润!

【讨论】:

我使用的是 zsh 而不是 bash,以及 v2.13.0 的 git。无论我尝试了什么,我都无法让git filter-branch --index-filter 工作。通常我会收到一条错误消息,指出 .new 索引文件不存在。有没有敲响警钟? @PatrickBeard 我不知道 zsh,你可以用上面的函数创建单独的文件git-add-repo.sh,在文件的末尾放上这一行git-add-repo "$@"。之后,您可以从 zsh 中使用它,例如 cd current/git/packagebash path/to/git-add-repo.sh https://github.com/example/example dir/to/save 这里讨论了这个问题:***.com/questions/7798142/… mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE" 有时会失败,所以你必须添加一个if test 我不会用这个方法!我天真地逐字逐句地尝试了这个脚本(我只能为此责备自己),它破坏了我本地的 git repo。历史看起来基本正确,但是将 git push 回 Github 会导致可怕的“RPC 失败;curl 55 SSL_write() 返回 SYSCALL,errno = 32”错误。我试图修复它,但它已经无法修复地损坏了。我最终不得不在一个新的本地仓库中重建东西。 @MasonFreed 这个脚本创建了一个新的 git 历史,混合了两个 repos,所以它不能被推送到旧的 repo,它需要创建一个新的或者用 force 键推送,意味着它重写你在服务器上的仓库【参考方案8】:

按照步骤将一个 repo 嵌入到另一个 repo 中,通过合并两个 git 历史记录来拥有一个单一的 git 历史记录。

    克隆您要合并的两个存储库。

git clone git@github.com:user/parent-repo.git

git clone git@github.com:user/child-repo.git

    转到子回购

cd child-repo/

    运行以下命令,将路径 my/new/subdir(出现 3 次)替换为您想要拥有子存储库的目录结构。

git filter-branch --prune-empty --tree-filter ' 如果 [ ! -e 我的/新的/子目录];然后 mkdir -p 我的/新的/子目录 git ls-tree --name-only $GIT_COMMIT | xargs -I 文件 mv 文件 my/new/subdir 呵呵

    转到父仓库

cd ../parent-repo/

    向父仓库添加远程,指向子仓库的路径

git remote add child-remote ../child-repo/

    获取子仓库

git 获取子远程

    合并历史记录

git merge --allow-unrelated-histories child-remote/master

如果您现在检查父 repo 中的 git 日志,它应该合并了子 repo 提交。您还可以看到来自提交源的标记。

下面的文章帮助我将一个存储库嵌入到另一个存储库中,通过合并两个 git 历史记录来拥有一个单一的 git 历史记录。

http://ericlathrop.com/2014/01/combining-git-repositories/

希望这会有所帮助。 快乐编码!

【讨论】:

第 3 步因语法错误而失败。缺少分号。修复git filter-branch --prune-empty --tree-filter ' if [ ! -e my/new/subdir ]; then mkdir -p my/new/subdir; git ls-tree --name-only $GIT_COMMIT | xargs -I files mv files my/new/subdir; fi'

以上是关于合并两个 Git 存储库而不破坏文件历史记录的主要内容,如果未能解决你的问题,请参考以下文章

如何合并两个 Git 存储库?

如何合并两个 Git 存储库?

如何将多个 Git 存储库合并为一个并交错历史

撤消已发布的合并并重新应用原始更改而不重写历史记录

将 git repo 分支(具有不同的历史记录)合并到主分支 [重复]

致命:添加 git remote 后拒绝合并不相关的历史记录