Detach (move) subdirectory into separate Git repository
我有一个Git存储库,其中包含许多子目录。现在我发现其中一个子目录与另一个子目录无关,应该分离到单独的存储库中。
如何在子目录中保存文件的历史记录的同时做到这一点?
我想我可以制作一个克隆并删除每个克隆不需要的部分,但我想这会在签出旧版本等时提供完整的树。这可能是可以接受的,但我更愿意假装两个存储库没有共享的历史。
为了清楚起见,我有以下结构:
1 2 3 4 5 | XYZ/ .git/ XY1/ ABC/ XY2/ |
但我想改为:
1 2 3 4 5 6 7 | XYZ/ .git/ XY1/ XY2/ ABC/ .git/ ABC/ |
简单的方式和贸易;
事实证明,这是一个非常常见和有用的实践,Git的霸主让它变得非常容易,但你必须有一个新版本的Git(>1.7.11 2012年5月)。有关如何安装最新Git的信息,请参阅附录。此外,下面的演练中还有一个真实的示例。好的。
准备旧回购好的。
1 2 3 | pushd <big-repo> git subtree split -P <name-of-folder> -b <name-of-new-branch> popd |
注意:
Windows用户注意:当文件夹深度大于1时,
创建新回购好的。
1 2 3 4 5 | mkdir <new-repo> pushd <new-repo> git init git pull </path/to/big-repo> <name-of-new-branch> |
将新回购协议链接到Github或任何地方好的。
1 2 | git remote add origin <[email protected]:my-user/new-repo.git> git push origin -u master |
清除(如果需要)好的。
1 2 3 4 | popd # get out of <new-repo> pushd <big-repo> git rm -rf <name-of-folder> |
注意:这会将所有历史引用保留在存储库中。如果您确实担心提交了密码,或者需要减小
…好的。演练
这些步骤与上面的步骤相同,但是遵循我的存储库的确切步骤,而不是使用
下面是我在节点中实现JavaScript浏览器模块的一个项目:好的。
1 2 3 4 5 6 7 8 9 10 11 | tree ~/Code/node-browser-compat node-browser-compat ├── ArrayBuffer ├── Audio ├── Blob ├── FormData ├── atob ├── btoa ├── location └── navigator |
我想把一个文件夹
1 2 3 | pushd ~/Code/node-browser-compat/ git subtree split -P btoa -b btoa-only popd |
我现在有了一个新的分支,
1 2 3 4 | mkdir ~/Code/btoa/ pushd ~/Code/btoa/ git init git pull ~/Code/node-browser-compat btoa-only |
接下来,我在GitHub或BitBucket上创建一个新的repo,或者添加它是
1 2 | git remote add origin [email protected]:node-browser-compat/btoa.git git push origin -u master |
快乐的一天!好的。
注:如果您使用
1 2 | git pull origin -u master git push origin -u master |
最后,我要从更大的报告中删除文件夹好的。
1 | git rm -rf btoa |
…好的。附录OS X上的最新Git
要获取最新版本的Git:好的。
1 | brew install git |
要获取BREW for OS X:好的。
HTTP//BURW.SH好的。Ubuntu上的最新Git
1 2 3 | sudo apt-get update sudo apt-get install git git --version |
如果这不起作用(你有一个非常老的Ubuntu版本),试试看。好的。
1 2 3 | sudo add-apt-repository ppa:git-core/ppa sudo apt-get update sudo apt-get install git |
如果还是不行,试试看好的。
1 2 3 4 | sudo chmod +x /usr/share/doc/git/contrib/subtree/git-subtree.sh sudo ln -s \ /usr/share/doc/git/contrib/subtree/git-subtree.sh \ /usr/lib/git-core/git-subtree |
感谢Rui.Araujo的评论。好的。清除您的历史记录
默认情况下,从Git中删除文件实际上并没有从Git中删除它们,只是承诺不再存在。如果要实际删除历史引用(即您已提交密码),则需要执行以下操作:好的。
1 | git filter-branch --prune-empty --tree-filter 'rm -rf <name-of-folder>' HEAD |
之后,您可以检查您的文件或文件夹是否不再显示在Git历史记录中。好的。
1 | git log -- <name-of-folder> # should show nothing |
但是,您不能将删除"推送"到GitHub等。如果你尝试的话,你会得到一个错误,你必须在你能做到之前,先到江户×1〔16〕,然后你就可以恢复你历史上的一切。好的。
因此,如果您想从"原始"中删除历史记录(即从GitHub、BitBucket等中删除历史记录),则需要删除repo并重新推送修剪过的repo副本。但是等等-还有更多!-如果你真的很担心删除密码或类似的东西,你需要修剪备份(见下文)。好的。使
前面提到的删除历史记录命令仍然会留下一堆备份文件——因为Git太仁慈了,无法帮助您意外破坏您的repo。它最终会在数天和数月内删除孤立的文件,但会将它们保留一段时间,以防您意识到您意外删除了不想删除的内容。好的。
因此,如果您真的想清空垃圾箱以立即减少回购的克隆大小,那么您必须执行所有这些非常奇怪的操作:好的。
1 2 3 4 5 6 7 | rm -rf .git/refs/original/ && \ git reflog expire --all && \ git gc --aggressive --prune=now git reflog expire --all --expire-unreachable=0 git repack -A -d git prune |
也就是说,我建议不要执行这些步骤,除非你知道你需要-以防万一你删掉了错误的子目录,你知道吗?当您按下repo时,备份文件不应该被克隆,它们只会出现在您的本地副本中。好的。信用卡
- http://psionides.eu/2010/02/04/sharing-code-between-projects-with-git-subtree/
- 从git中永久删除目录
- http://blogs.atlassian.com/2013/05/alternations-to-git-submodule-git-subtree/
- 如何从我的git repo中删除未引用的blob
好啊。
更新:这个过程很常见,Git团队使用一个新工具
您想克隆您的存储库,然后使用
要克隆本地存储库,请执行以下操作:
1 | git clone /XYZ /ABC |
(注意:存储库将使用硬链接进行克隆,但这不是问题,因为硬链接文件本身不会被修改-将创建新的文件。)
现在,让我们保留要重写的有趣分支,然后删除源代码以避免将其推到那里,并确保源代码不会引用旧的提交:
1 2 3 | cd /ABC for i in branch1 br2 br3; do git branch -t $i origin/$i; done git remote rm origin |
或者对于所有远程分支:
1 2 3 | cd /ABC for i in $(git branch -r | sed"s/.*origin\///"); do git branch -t $i origin/$i; done git remote rm origin |
现在,您可能还需要删除与子项目无关的标记;您也可以稍后执行,但可能需要再次修剪您的repo。我没有这样做,所有标签都得到了一个
然后使用filter branch和reset排除其他文件,以便修剪它们。我们还要添加
1 | git filter-branch --tag-name-filter cat --prune-empty --subdirectory-filter ABC -- --all |
或者,只重写头分支并忽略标记和其他分支:
1 | git filter-branch --tag-name-filter cat --prune-empty --subdirectory-filter ABC HEAD |
然后删除备份刷新,以便真正回收空间(尽管现在操作具有破坏性)
1 2 3 4 | git reset --hard git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d git reflog expire --expire=now --all git gc --aggressive --prune=now |
现在您有了ABC子目录的本地Git存储库,它的所有历史都保存了下来。
注:在大多数情况下,
编辑:下面评论中的各种建议都被合并进来,以确保,例如,存储库实际上被缩小了(以前并不总是这样)。
Paul的答案创建了一个包含/abc的新存储库,但没有从/xyz中删除/abc。以下命令将从/xyz中删除/abc:
1 | git filter-branch --tree-filter"rm -rf ABC" --prune-empty HEAD |
当然,首先在"clone——no hardlinks"存储库中测试它,然后按照paul列出的reset、gc和prune命令进行测试。
我发现,为了正确地从新存储库中删除旧的历史记录,您必须在执行
执行克隆和筛选:
1 2 | git clone --no-hardlinks foo bar; cd bar git filter-branch --subdirectory-filter subdir/you/want |
删除对旧历史的所有引用。"origin"跟踪您的克隆,"original"是筛选分支保存旧内容的位置:
1 2 3 | git remote rm origin git update-ref -d refs/original/refs/heads/master git reflog expire --expire=now --all |
即使现在,您的历史可能会被卡在一个文件包文件,而fsck不会接触。将其撕成碎片,创建新的packfile并删除未使用的对象:
1 | git repack -ad |
在过滤器分支手册中对此有一个解释。
编辑:添加了bash脚本。
这里给出的答案对我来说只起了一部分作用;很多大文件仍在缓存中。最终的效果(在freenode上以git格式工作数小时后):
1 2 3 4 5 6 7 8 | git clone --no-hardlinks file:///SOURCE /tmp/blubb cd blubb git filter-branch --subdirectory-filter ./PATH_TO_EXTRACT --prune-empty --tag-name-filter cat -- --all git clone file:///tmp/blubb/ /tmp/blooh cd /tmp/blooh git reflog expire --expire=now --all git repack -ad git gc --prune=now |
在以前的解决方案中,存储库大小约为100MB。这个使它降到了1.7MB。也许这对某人有帮助:)
以下bash脚本自动执行任务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | !/bin/bash if (( $# < 3 )) then echo"Usage: $0 </path/to/repo/> <directory/to/extract/> <newName>" echo echo"Example: $0 /Projects/42.git first/answer/ firstAnswer" exit 1 fi clone=/tmp/${3}Clone newN=/tmp/${3} git clone --no-hardlinks file://$1 ${clone} cd ${clone} git filter-branch --subdirectory-filter $2 --prune-empty --tag-name-filter cat -- --all git clone file://${clone} ${newN} cd ${newN} git reflog expire --expire=now --all git repack -ad git gc --prune=now |
这不再是那么复杂了,你只需在你的复制品上使用git filter branch命令,剔除你不想要的子目录,然后推到新的远程。
1 2 | git filter-branch --prune-empty --subdirectory-filter <YOUR_SUBDIR_TO_KEEP> master git push <MY_NEW_REMOTE_URL> -f . |
更新:Git子树模块非常有用,以至于Git团队将其放入核心,并使其成为
Git子树可能对此有用
http://github.com/apenwarr/git-subtree/blob/master/git-subtree.txt(已弃用)
http://psionides.jogger.pl/2010/02/04/sharing-code-between-projects-with-git-subtree/
下面是对CoolAJ86的"The Easy Way&Trade";答案的一个小修改,以便将多个子文件夹(例如,
准备旧回购
1 2 3 4 | pushd <big-repo> git filter-branch --tree-filter"mkdir <name-of-folder>; mv <sub1> <sub2> <name-of-folder>/" HEAD git subtree split -P <name-of-folder> -b <name-of-new-branch> popd |
注意:
Windows用户注意:当文件夹深度大于1时,
最后一点:与基础答案的独特和巨大区别是脚本"
创建新回购
1 2 3 4 5 | mkdir <new-repo> pushd <new-repo> git init git pull </path/to/big-repo> <name-of-new-branch> |
将新回购协议链接到Github或任何地方
1 2 | git remote add origin <[email protected]:my-user/new-repo.git> git push origin -u master |
清除(如果需要)
1 2 3 4 | popd # get out of <new-repo> pushd <big-repo> git rm -rf <name-of-folder> |
注意:这会将所有的历史引用保留在存储库中。如果您确实担心已提交密码或需要减小
原始问题希望xyz/abc/(*文件)成为abc/abc/(*文件)。在为自己的代码实现了接受的答案之后,我注意到它实际上将xyz/abc/(*文件)更改为abc/(*文件)。过滤器分支手册页甚至说,
The result will contain that directory (and only that) as its project root."
换句话说,它将顶级文件夹"向上"提升一级。这是一个重要的区别,因为,例如,在我的历史中,我重命名了一个顶级文件夹。通过将文件夹"向上"提升一个级别,Git在执行重命名的提交时会失去连续性。
我对这个问题的答案是复制两份存储库,然后手动删除每个存储库中要保存的文件夹。手册页支持我这样做:
[...] avoid using [this command] if a simple single commit would suffice to fix your problem
为了补充保罗的答案,我发现为了最终恢复空间,我必须将头部推到一个干净的存储库中,这样可以缩小.git/objects/pack目录的大小。
即
1 2 3 | $ mkdir ...ABC.git $ cd ...ABC.git $ git init --bare |
在GC修剪之后,还要执行以下操作:
1 | $ git push ...ABC.git HEAD |
然后你可以做
1 | $ git clone ...ABC.git |
abc/.git的大小减小了
实际上,推送清理存储库不需要一些耗时的步骤(例如g i t-gc),即:
1 2 3 4 | $ git clone --no-hardlinks /XYZ /ABC $ git filter-branch --subdirectory-filter ABC HEAD $ git reset --hard $ git push ...ABC.git HEAD |
正确的方法如下:
Github现在甚至有关于此类案例的小文章。
但一定要先将原始repo克隆到单独的目录(因为它会删除所有文件和其他目录,您可能需要使用它们)。
所以你的算法应该是:
似乎大多数(全部?)这里的答案依赖于某种形式的
1 2 3 4 5 | ABC/ /move_this_dir # did some work here, then renamed it to ABC/ /move_this_dir_renamed |
如果使用普通的git过滤器样式提取"move-me-rename",则会丢失最初移动该目录(ref)时从后面发生的文件更改历史记录。
因此,似乎真正保留所有更改历史记录(如果您的情况是这样的话)的唯一方法,实质上是复制存储库(创建一个新的repo,将其设置为原点),然后对其他所有内容进行核处理,并将子目录重命名为父目录,如下所示:
这遵循Github文档"将子文件夹拆分为新存储库"的步骤6-11,将模块推送到新的repo。
这不会在.git文件夹中为您节省任何空间,但它会保留这些文件的所有更改历史记录,甚至在重命名时也是如此。如果没有"大量"的历史损失等等,这可能不值得,但至少保证你不会失去更老的承诺!
我确实遇到过这个问题,但是所有基于Git过滤分支的标准解决方案都非常慢。如果你有一个小的存储库,那么这可能不是问题,它是为我准备的。我编写了另一个基于libgit2的git过滤程序,第一步是为主存储库的每个过滤创建分支,然后将这些分支作为下一步推送到清理存储库。在我的存储库(500MB 100000提交)上,标准的Git过滤器分支方法花费了几天时间。我的程序需要几分钟来做同样的过滤。
它有一个极好的名字,Git_过滤器,住在这里:
https://github.com/slobaby/git_过滤器
在吉瑟布上。
我希望它对某人有用。
值得一提的是,下面是如何在Windows计算机上使用GitHub。假设您在
github:
BASH提示:
使用此筛选命令删除子目录,同时保留标记和分支:
1 2 3 | git filter-branch --index-filter \ "git rm -r -f --cached --ignore-unmatch DIR" --prune-empty \ --tag-name-filter cat -- --all |
正如我上面提到的,我不得不使用相反的解决方案(删除所有不涉及我的
首先,
- 截图:合并疯狂!
这是一个表面的问题,我可能可以忍受(他说……慢慢后退,避开眼睛)。
第二,剩下的几个承诺几乎都是重复的!我似乎已经获得了第二个多余的时间表,它跨越了项目的整个历史。有趣的是(您可以从下面的图片中看到),我的三个本地分支并不都在同一时间线上(这当然是它存在的原因,而且不仅仅是垃圾收集)。
- screnshot:双双精度,git过滤分支样式
我唯一能想到的是,其中一个已删除的提交可能是
在疯狂并购的情况下,我可能会把它单独留下,因为它已经牢牢地扎根在我的承诺历史中,每当我靠近的时候,它就威胁着我-它似乎并没有真正造成任何非化妆品的问题,因为它在tower.app中相当漂亮。
更简单的方法
将目录拆分为本地分支
#change into your repo's directory
cd /path/to/repo
#checkout the branch
git checkout XYZ
#split multiple directories into new branch XYZ
git splits -b XYZ XY1 XY2
在某处创建一个空回购。我们假设我们在Github上创建了一个名为
推动新回购。
#add a new remote origin for the empty repo so we can push to the empty repo on GitHub
git remote add origin_xyz [email protected]:simpliwp/xyz.git
#push the branch to the empty repo's master branch
git push origin_xyz XYZ:master
将新创建的远程repo克隆到新的本地目录中
#change current directory out of the old repo
cd /path/to/where/you/want/the/new/local/repo
#clone the remote repo you just pushed to
git clone [email protected]:simpliwp/xyz.git
我推荐Github将子文件夹拆分为新存储库的指南。这些步骤与保罗的答案相似,但我发现它们的说明更容易理解。
我已经修改了这些指令,以便它们适用于本地存储库,而不是一个托管在GitHub上的存储库。
Splitting a subfolder out into a new repository
Open Git Bash.
Change the current working directory to the location where you want to create your new repository.
Clone the repository that contains the subfolder.
1 git clone OLD-REPOSITORY-FOLDER NEW-REPOSITORY-FOLDERChange the current working directory to your cloned repository.
1 cd REPOSITORY-NAMETo filter out the subfolder from the rest of the files in the repository, run git filter-branch , supplying this information:
FOLDER-NAME : The folder within your project that you'd like to create a separate repository from.
- Tip: Windows users should use
/ to delimit folders.BRANCH-NAME : The default branch for your current project, for example,master orgh-pages .
1
2
3
4 git filter-branch --prune-empty --subdirectory-filter FOLDER-NAME BRANCH-NAME
# Filter the specified branch in your directory and remove empty commits
Rewrite 48dc599c80e20527ed902928085e7861e6b3cbe6 (89/89)
Ref 'refs/heads/BRANCH-NAME' was rewritten
在https://github.com/vangorra/git_split上查看git_split项目
把git目录转换成它们自己的存储库。没有子树有趣的事。此脚本将获取Git存储库中的一个现有目录,并将该目录转换为它自己的独立存储库。在此过程中,它将复制您提供的目录的整个更改历史记录。
1 2 3 4 5 | ./git_split.sh <src_repo> <src_branch> <relative_dir_path> <dest_repo> src_repo - The source repo to pull from. src_branch - The branch of the source repo to pull from. (usually master) relative_dir_path - Relative path of the directory in the source repo to split. dest_repo - The repo to push to. |
在垃圾收集之前,您可能需要"git reflog expire--expire=now--all"之类的东西来实际清理文件。git filter分支只删除历史记录中的引用,但不删除保存数据的reflog条目。当然,先测试一下。
在这样做时,我的磁盘使用率急剧下降,尽管我的初始条件有所不同。也许——子目录过滤器否定了这个需求,但我对此表示怀疑。
我确信Git子树是非常好的,但是我想要移动的Git托管代码的子目录都在Eclipse中。所以如果你用的是egit,那就很容易了。拿着你想要移动的项目,团队->断开它,然后团队->共享到新的位置。默认情况下,它将尝试使用旧的回购位置,但您可以取消选中"使用现有选择"并选择新位置来移动它。万岁。
把这个放到你的Gitconfig中:
1 | reduce-to-subfolder = !sh -c 'git filter-branch --tag-name-filter cat --prune-empty --subdirectory-filter cookbooks/unicorn HEAD && git reset --hard && git for-each-ref refs/original/ | cut -f 2 | xargs -n 1 git update-ref -d && git reflog expire --expire=now --all && git gc --aggressive --prune=now && git remote rm origin' |
我找到了非常直接的解决方案,其思想是复制存储库,然后删除不必要的部分。这就是它的工作原理:
1)克隆要拆分的存储库
1 | git clone [email protected]:testrepo/test.git |
2)移动到Git文件夹
1 | cd test/ |
2)删除不必要的文件夹并提交
1 2 3 4 | rm -r ABC/ git add . enter code here git commit -m 'Remove ABC' |
3)用bfg删除不必要的文件夹表单历史记录
1 2 3 4 | cd .. java -jar bfg.jar --delete-folders"{ABC}" test cd test/ git reflog expire --expire=now --all && git gc --prune=now --aggressive |
for multiply folders you can use comma
1 java -jar bfg.jar --delete-folders"{ABC1,ABC2}" metric.git
4)检查历史记录是否不包含您刚删除的文件/文件夹。
1 | git log --diff-filter=D --summary | grep delete |
5)现在你有了没有ABC的干净仓库,所以把它推到新的起点
1 2 | remote add origin [email protected]:username/new_repo git push -u origin master |
就是这样。您可以重复这些步骤以获取另一个存储库,
只需删除xy1、xy2并在步骤3中重命名xyz->abc
您可以轻松地尝试https://help.github.com/enterprise/2.15/user/articles/spliting-a-subfolder-out-into-a-new-repository/
这对我有用。我在上述步骤中面临的问题是
在这个命令中,
如果由于保护问题提交时最后一步失败,请执行-https://docs.gitlab.com/ee/user/project/protected_branches.html