考虑以下场景:

我在自己的Git repo中开发了一个小型实验项目a。它现在已经成熟,我希望A成为更大的项目B的一部分,该项目有自己的大仓库。现在我想将A添加为B的子目录。

我如何将A合并为B,而不丢失任何方面的历史?


当前回答

如果要将项目a合并到项目b中:

cd path/to/project-b
git remote add project-a /path/to/project-a
git fetch project-a --tags
git merge --allow-unrelated-histories project-a/master # or whichever branch you want to merge
git remote remove project-a

摘自:git合并不同的存储库?

这种方法对我来说效果很好,它更短,在我看来更干净。

如果您想将project-a放到子目录中,可以使用gitfilter repo(不建议使用过滤器分支)。在上述命令之前运行以下命令:

cd path/to/project-a
git filter-repo --to-subdirectory-filter project-a

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

注意:--allow非相关历史参数仅在git>=2.9之后存在。请参阅Git-Git merge文档/-允许不相关的历史记录

更新:按照@jstadler的建议添加了--tags,以便保留标签。

其他回答

如果要将项目a合并到项目b中:

cd path/to/project-b
git remote add project-a /path/to/project-a
git fetch project-a --tags
git merge --allow-unrelated-histories project-a/master # or whichever branch you want to merge
git remote remove project-a

摘自:git合并不同的存储库?

这种方法对我来说效果很好,它更短,在我看来更干净。

如果您想将project-a放到子目录中,可以使用gitfilter repo(不建议使用过滤器分支)。在上述命令之前运行以下命令:

cd path/to/project-a
git filter-repo --to-subdirectory-filter project-a

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

注意:--allow非相关历史参数仅在git>=2.9之后存在。请参阅Git-Git merge文档/-允许不相关的历史记录

更新:按照@jstadler的建议添加了--tags,以便保留标签。

在我的例子中,我有一个插件存储库和一个主项目存储库,我想假装我的插件一直是在主项目的插件子目录中开发的。

基本上,我重写了我的插件存储库的历史,使其看起来所有的开发都发生在插件/我的插件子目录中。然后,我将插件的开发历史添加到主项目历史中,并将两棵树合并在一起。由于主项目存储库中没有插件/my插件目录,所以这是一个简单的无冲突合并。生成的存储库包含两个原始项目的所有历史,并且有两个根。

TL;博士

$ cp -R my-plugin my-plugin-dirty
$ cd my-plugin-dirty
$ git filter-branch -f --tree-filter "zsh -c 'setopt extended_glob && setopt glob_dots && mkdir -p plugins/my-plugin && (mv ^(.git|plugins) plugins/my-plugin || true)'" -- --all
$ cd ../main-project
$ git checkout master
$ git remote add --fetch my-plugin ../my-plugin-dirty
$ git merge my-plugin/master --allow-unrelated-histories
$ cd ..
$ rm -rf my-plugin-dirty

长版本

首先,创建我的插件存储库的副本,因为我们将重写这个存储库的历史。

现在,导航到我的插件库的根目录,检查您的主分支(可能是主分支),然后运行以下命令。当然,你应该替换我的插件和插件,无论你的实际名称是什么。

$ git filter-branch -f --tree-filter "zsh -c 'setopt extended_glob && setopt glob_dots && mkdir -p plugins/my-plugin && (mv ^(.git|plugins) plugins/my-plugin || true)'" -- --all

现在来解释一下。git-filter-branch--tree-filter(…)HEAD对可以从HEAD访问的每个提交运行(…)命令。请注意,这直接对为每次提交存储的数据进行操作,因此我们不必担心“工作目录”、“索引”、“暂存”等概念。

如果您运行的filter branch命令失败,它将在.git目录中留下一些文件,下次尝试filter branch时,它将对此进行投诉,除非您为filter branch提供-f选项。

至于实际的命令,我没有太多的运气让bash执行我想要的,所以我使用zsh-c来让zsh执行一个命令。首先,我设置了extended_glob选项,这是启用mv命令中的^(…)语法的选项,以及glob_dots选项,它允许我使用glob(^(……))选择点文件(例如.gitignore)。

接下来,我使用mkdir-p命令同时创建插件和plugins/my插件。

最后,我使用zsh“negative glob”特性^(.git |插件)来匹配存储库根目录中的所有文件,但.git和新创建的插件文件夹除外。(此处可能不需要排除.git,但尝试将目录移动到自身是错误的。)

在我的存储库中,初始提交不包含任何文件,因此mv命令在初始提交时返回了一个错误(因为没有可移动的内容)。因此,我添加了一个||true,这样gitfilter分支就不会中止。

-all选项告诉filter-branch重写存储库中所有分支的历史记录,而额外的--则需要告诉git将其解释为分支重写选项列表的一部分,而不是filter-branch本身的一个选项。

现在,导航到您的主项目存储库并检查您要合并到的任何分支。添加我的插件存储库的本地副本(已修改其历史记录)作为主项目的远程副本:

$ git remote add --fetch my-plugin $PATH_TO_MY_PLUGIN_REPOSITORY

现在,您的提交历史中将有两个不相关的树,您可以使用以下方法很好地可视化它们:

$ git log --color --graph --decorate --all

要合并它们,请使用:

$ git merge my-plugin/master --allow-unrelated-histories

注意,在2.9.0之前的Git中,--allow unrelated history选项不存在。如果您使用的是这些版本中的一个,只需省略选项:2.9.0中还添加了--allow unrelated histories prevent的错误消息。

您不应该有任何合并冲突。如果您这样做了,这可能意味着filter branch命令无法正常工作,或者主项目中已经存在plugins/my插件目录。

确保为任何未来的贡献者输入一个解释性的提交消息,让他们知道如何进行黑客操作来创建一个具有两个根的存储库。

您可以使用上面的gitlog命令可视化新的提交图,它应该有两个根提交。请注意,只有主分支将被合并。这意味着,如果你在其他我的插件分支上有重要的工作要合并到主项目树中,那么在完成这些合并之前,你应该避免删除我的插件远程。如果您不这样做,那么来自这些分支的提交仍将在主项目存储库中,但有些将无法访问,并且容易受到最终垃圾收集的影响。(此外,您必须通过SHA引用它们,因为删除远程会删除其远程跟踪分支。)

可选地,在您合并了我的插件中要保留的所有内容后,您可以使用以下方法删除我的插件远程:

$ git remote remove my-plugin

现在,您可以安全地删除您更改了其历史记录的插件存储库的副本。在我的例子中,在合并完成并推送后,我还向真正的插件存储库添加了一个弃用通知。


在Mac OS X El Capitan上测试了git版本2.9.0和zsh版本5.2。您的里程数可能有所不同。

参考文献:

https://git-scm.com/docs/git-filter-branchhttps://unix.stackexchange.com/questions/6393/how-do-you-move-all-files-including-hidden-from-one-directory-to-anotherhttp://www.refining-linux.org/archives/37/ZSH-Gem-2-Extended-globbing-and-expansion/从Git repo清除文件失败,无法创建新备份git,过滤所有分支上的分支

除了使用远程add->fetch->merge策略的所有答案之外:如果您想保留其他存储库中的标记,但不想将它们全部泄漏到一个公共命名空间中(可能会发生冲突),您可能需要稍微更改fetch命令:

git fetch --no-tags other_repo
git fetch --no-tags other_repo 'refs/tags/*:refs/tags/other_repo/*'

第一个命令像往常一样获取所有分支,但省略了附加到提交的标记,第二个命令也省略了通常的标记获取机制(git help fetch了解更多信息),并使用git的refspec功能获取将它们从X映射到other_repo/X的所有标记。

引用(分支、标记)只是git中的文件,您可以使用目录来命名。上面的两个命令将保留第一个存储库中的标记,而另一个存储库的标记将以other_repo作为前缀/

操作完成后,最好移除另一个遥控器,这样您就不会意外地以正常方式获取标签并造成混乱。

我知道事情已经过去很久了,但我对我在这里找到的其他答案并不满意,所以我写了这样一篇文章:

me=$(basename $0)

TMP=$(mktemp -d /tmp/$me.XXXXXXXX)
echo 
echo "building new repo in $TMP"
echo
sleep 1

set -e

cd $TMP
mkdir new-repo
cd new-repo
    git init
    cd ..

x=0
while [ -n "$1" ]; do
    repo="$1"; shift
    git clone "$repo"
    dirname=$(basename $repo | sed -e 's/\s/-/g')
    if [[ $dirname =~ ^git:.*\.git$ ]]; then
        dirname=$(echo $dirname | sed s/.git$//)
    fi

    cd $dirname
        git remote rm origin
        git filter-branch --tree-filter \
            "(mkdir -p $dirname; find . -maxdepth 1 ! -name . ! -name .git ! -name $dirname -exec mv {} $dirname/ \;)"
        cd ..

    cd new-repo
        git pull --no-commit ../$dirname
        [ $x -gt 0 ] && git commit -m "merge made by $me"
        cd ..

    x=$(( x + 1 ))
done

当您希望在一次提交中合并三个或更多项目时,请执行其他答案中所述的步骤(远程添加-f,合并)。然后,(软)将索引重置为旧头(没有合并)。添加所有文件(git-Add-A)并提交它们(消息“将项目A、B、C和D合并到一个项目中”)。这现在是master的提交id。

现在,使用以下内容创建.git/info/places:

<commit-id of master> <list of commit ids of all parents>

运行gitfilter分支--head^。。头头^2.头头^3.头。如果你有三个以上的分支,就加上同样多的头。。当你有树枝的时候,就把头伸过去。要更新标记,请追加--tag-namefilter cat。不要总是添加,因为这可能会导致某些提交的重写。有关详细信息,请参阅过滤器分支的手册页,搜索“移植物”。

现在,你的最后一次承诺与正确的父母相关。