考虑以下场景:
我在自己的Git repo中开发了一个小型实验项目a。它现在已经成熟,我希望A成为更大的项目B的一部分,该项目有自己的大仓库。现在我想将A添加为B的子目录。
我如何将A合并为B,而不丢失任何方面的历史?
考虑以下场景:
我在自己的Git repo中开发了一个小型实验项目a。它现在已经成熟,我希望A成为更大的项目B的一部分,该项目有自己的大仓库。现在我想将A添加为B的子目录。
我如何将A合并为B,而不丢失任何方面的历史?
当前回答
在我的例子中,我有一个插件存储库和一个主项目存储库,我想假装我的插件一直是在主项目的插件子目录中开发的。
基本上,我重写了我的插件存储库的历史,使其看起来所有的开发都发生在插件/我的插件子目录中。然后,我将插件的开发历史添加到主项目历史中,并将两棵树合并在一起。由于主项目存储库中没有插件/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,过滤所有分支上的分支
其他回答
https://github.com/hraban/tomono作为基于脚本的解决方案的另一个例子。
我不是作者,但使用了它,它完成了任务。
一个积极的方面是,你将所有分支机构和所有历史记录纳入最终回购。对于我的repo(repo中没有重复的文件夹-实际上,它们来自tfs2git迁移),没有冲突,一切都是自动运行的。
它主要用于(参见名称)创建monoreos。
对于Windows用户:gitbash可以执行.sh文件。它带有标准的git安装。
另一个存储库的单个分支可以很容易地放在保留其历史的子目录下。例如:
git subtree add --prefix=rails git://github.com/rails/rails.git master
这将显示为一次提交,其中Rails主分支的所有文件都添加到“Rails”目录中。然而,提交的标题包含对旧历史树的引用:
从提交添加“rails/”<rev>
其中<rev>是SHA-1提交哈希。你仍然可以看到历史,责怪一些变化。
git log <rev>
git blame <rev> -- README.md
注意,从这里看不到目录前缀,因为这是一个完整的旧分支。您应该像通常的文件移动提交一样对待它:当到达它时,您需要额外的跳转。
# finishes with all files added at once commit
git log rails/README.md
# then continue from original tree
git log <rev> -- README.md
还有一些更复杂的解决方案,如手动执行此操作或如其他答案所述重写历史。
git子树命令是git contrib的一部分,一些数据包管理器默认安装它(OS X Homebrew)。但除了git之外,您可能还需要自己安装它。
git子树很好,但它可能不是您想要的。
例如,如果projectA是在B中创建的目录,在git子树之后,
git log projectA
仅列出一个提交:合并。合并项目的提交针对不同的路径,因此不会显示。
格雷格·休吉尔(Greg Hewgill)的答案最接近,但实际上并没有说明如何重写路径。
解决方案出奇地简单。
(1) 在A中,
PREFIX=projectA #adjust this
git filter-branch --index-filter '
git ls-files -s |
sed "s,\t,&'"$PREFIX"'/," |
GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info &&
mv $GIT_INDEX_FILE.new $GIT_INDEX_FILE
' HEAD
注意:这将重写历史;你可能想先备份a。
注意Bene:如果在文件名或路径中使用非ascii字符(或白色字符),则必须修改sed命令中的替代脚本。在这种情况下,“ls files-s”生成的记录中的文件位置以引号开头。
(2) 然后在B中,运行
git pull path/to/A
瞧!在B中有一个projectA目录。如果运行git-log-projectA,您将看到a中的所有提交。
在我的例子中,我需要两个子目录,projectA和projectB。在这种情况下,我也执行了步骤(1)到B。
如果您想单独维护项目,子模块方法是很好的。然而,如果您真的想将两个项目合并到同一个存储库中,那么您还有更多的工作要做。
第一件事是使用gitfilter分支将第二个存储库中所有内容的名称重写到您希望它们结束的子目录中。因此,您将使用projb/foo.c和projb/bar.html代替foo.c和bar.html。
然后,您应该能够执行以下操作:
git remote add projb [wherever]
git pull projb
git pull将执行git fetch,然后执行git merge。如果您要拉到的存储库还没有projb/目录,那么应该不会有冲突。
进一步搜索表明,在将gitk合并为git时也做了类似的操作。Junio C Hamano在这里写道:http://www.mail-archive.com/git@vger.kernel.org/msg03395.html
此函数将远程repo克隆到本地repo目录中,合并后将保存所有提交,git日志将显示原始提交和正确路径:
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
如果进行一些更改,您甚至可以将合并的repo的文件/目录移动到不同的路径中,例如:
repo="https://github.com/example/example"
path="$(pwd)"
tmp="$(mktemp -d)"
remote="$(echo "$tmp" | sed 's/\///g' | sed 's/\./_/g')"
git clone "$repo" "$tmp"
cd "$tmp"
GIT_ADD_STORED=""
function git-mv-store
{
from="$(echo "$1" | sed 's/\./\\./')"
to="$(echo "$2" | sed 's/\./\\./')"
GIT_ADD_STORED+='s,\t'"$from"',\t'"$to"',;'
}
# NOTICE! This paths used for example! Use yours instead!
git-mv-store 'public/index.php' 'public/admin.php'
git-mv-store 'public/data' 'public/x/_data'
git-mv-store 'public/.htaccess' '.htaccess'
git-mv-store 'core/config' 'config/config'
git-mv-store 'core/defines.php' 'defines/defines.php'
git-mv-store 'README.md' 'doc/README.md'
git-mv-store '.gitignore' 'unneeded/.gitignore'
git filter-branch --index-filter '
git ls-files -s |
sed "'"$GIT_ADD_STORED"'" |
GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &&
mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
' HEAD
GIT_ADD_STORED=""
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"
通知路径将通过sed替换,因此请确保在合并后将其移动到正确的路径中。--allow非相关历史参数仅在git>=2.9之后存在。