将子目录分离(移动)到单独的Git存储库中
我有一个Git仓库,其中包含一些子目录。 现在我发现其中一个子目录与另一个不相关,应该分离到一个单独的存储库。
我怎么能这样做,而保持在子目录中的文件的历史?
我想我可以做一个克隆,并删除每个克隆不需要的部分,但我想这会给我一个完整的树,当检查一个较旧的修订等。这可能是可以接受的,但我宁愿能够假装两个存储库没有共享的历史logging。
为了说清楚,我有以下结构:
XYZ/ .git/ XY1/ ABC/ XY2/
但是我想代之以:
XYZ/ .git/ XY1/ XY2/ ABC/ .git/ ABC/
更新 :这个过程非常常见,git团队使用一个新的工具git subtree
更简单。 看到这里: 分离(移动)到不同的Git仓库的子目录
你想克隆你的仓库,然后使用git filter-branch
来标记所有东西,但是你想在你的新仓库中的子目录被垃圾收集。
-
克隆你的本地仓库:
git clone /XYZ /ABC
(注意:版本库将使用硬链接进行克隆,但这不是问题,因为硬链接文件本身不会被修改 – 将会创build新文件。)
-
现在,让我们保留我们想要重写的有趣的分支,然后删除原点以避免推到那里,并确保旧的提交不会被原点引用:
cd /ABC for i in branch1 br2 br3; do git branch -t $i origin/$i; done git remote rm origin
或所有远程分支机构:
cd /ABC for i in $(git branch -r | sed "s/.*origin\///"); do git branch -t $i origin/$i; done git remote rm origin
-
现在您可能还需要删除与子项目无关的标签; 您也可以稍后再做,但您可能需要再次修剪您的回购。 我没有这样做,得到了一个
WARNING: Ref 'refs/tags/v0.1' is unchanged
对所有标签WARNING: Ref 'refs/tags/v0.1' is unchanged
(因为它们都与子项目无关)。 此外,删除这样的标签后,将回收更多的空间。 显然git filter-branch
应该可以重写其他标签,但是我无法validation这一点。 如果你想删除所有标签,请使用git tag -l | xargs git tag -d
git tag -l | xargs git tag -d
。 -
然后使用filter-branch并重置来排除其他文件,这样它们可以被修剪。 我们还添加
--tag-name-filter cat --prune-empty
来删除空提交并重写标签(注意,这将不得不删除它们的签名):git filter-branch --tag-name-filter cat --prune-empty --subdirectory-filter ABC -- --all
或者也可以只重写HEAD分支并忽略标签和其他分支:
git filter-branch --tag-name-filter cat --prune-empty --subdirectory-filter ABC HEAD
-
然后删除备份reflogs,这样可以真正回收空间(虽然现在操作是破坏性的)
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仓库。
注意:对于大多数用途, git filter-branch
确实应该有添加的参数-- --all
。 是的,这是真正的冲刺冲刺空间冲刺冲刺all
。 这需要是该命令的最后一个参数。 正如Matli发现的,这样可以将项目分支和标签包含在新的回购中。
编辑:从下面的评论意见中的各种build议被合并,以确保,例如,存储库实际上收缩(这并不总是如此)。
简单的方法™
事实certificate,这是一个常见的,有用的做法,git的主人使它真的很容易,但你必须有一个更新版本的git(> = 1.7.11 2012年5月)。 请参阅附录了解如何安装最新的git。 另外,下面的演练中还有一个真实的例子 。
-
准备旧的回购
pushd <big-repo> git subtree split -P <name-of-folder> -b <name-of-new-branch> popd
注意:
<name-of-folder>
不能包含前导字符或结尾字符。 例如,名为subproject
的文件夹务必作为subproject
传递,而不是./subproject/
Windows用户注意:当文件夹深度> 1时,
<name-of-folder>
必须包含* nix样式的文件夹分隔符(/)。 例如,名为path1\path2\subproject
的文件夹务必作为path1/path2/subproject
传递 -
创build新的回购
mkdir <new-repo> pushd <new-repo> git init git pull </path/to/big-repo> <name-of-new-branch>
-
将新的回购链接链接到Github或任何地方
git remote add origin <git@github.com:my-user/new-repo.git> git push origin -u master
-
清理, 如果需要的话
popd # get out of <new-repo> pushd <big-repo> git rm -rf <name-of-folder>
注意 :这会在资源库中留下所有历史引用。如果您确实担心提交了密码或者需要减小
.git
文件夹的文件大小,请参阅下面的附录 。
…
演练
这些步骤与上面的步骤相同 ,但遵循我的存储库的确切步骤,而不是使用<meta-named-things>
。
下面是我在节点中实现JavaScript浏览器模块的一个项目:
tree ~/Code/node-browser-compat node-browser-compat ├── ArrayBuffer ├── Audio ├── Blob ├── FormData ├── atob ├── btoa ├── location └── navigator
我想将一个文件夹btoa
拆分成一个单独的git存储库
pushd ~/Code/node-browser-compat/ git subtree split -P btoa -b btoa-only popd
我现在有一个新的分支, btoa-only
,只有提交btoa
,我想创build一个新的存储库。
mkdir ~/Code/btoa/ pushd ~/Code/btoa/ git init git pull ~/Code/node-browser-compat btoa-only
接下来,我在Github或bitbucket上创build一个新的回购协议,并添加它是origin
(顺便说一句,“起源”只是一个约定,不是命令的一部分 – 你可以称之为“远程服务器”或任何你喜欢的东西)
git remote add origin git@github.com:node-browser-compat/btoa.git git push origin -u master
快乐的一天!
注意:如果您使用README.md
, .gitignore
和LICENSE
创build了一个回购,您需要先README.md
:
git pull origin -u master git push origin -u master
最后,我想从更大的回购中删除文件夹
git rm -rf btoa
…
附录
OS X上最新的git
要获得最新版本的git:
brew install git
为了酿造OS X:
Ubuntu上最新的git
sudo apt-get update sudo apt-get install git git --version
如果这不起作用(你有一个非常旧的版本的Ubuntu),请尝试
sudo add-apt-repository ppa:git-core/ppa sudo apt-get update sudo apt-get install git
如果仍然不起作用,请尝试
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中删除它,它只是提交,他们不在那里了。 如果你想实际删除历史引用(即你有一个提交的密码),你需要这样做:
git filter-branch --prune-empty --tree-filter 'rm -rf <name-of-folder>' HEAD
之后,你可以检查你的文件或文件夹不再出现在git历史logging中
git log -- <name-of-folder> # should show nothing
但是,你不能“推”删除github等。 如果你尝试,你会得到一个错误,你必须在你能够git push
之前,你必须先git pull
– 然后你回到历史上的一切。
所以,如果你想从“origin”中删除历史logging – 意味着从github,bitbucket等中删除它,你需要删除repo并重新推回修剪后的repo。 但是等等 – 还有更多 ! – 如果你真的担心摆脱密码或类似的东西,你需要修剪备份(见下文)。
使得.git
变小
前面提到的删除历史logging命令仍然留下了一堆备份文件 – 因为git非常善意的帮助你不会意外毁掉你的回购。 它最终会在几天和几个月内删除孤立的文件,但是如果你意识到你不小心删除了一些你不想要的东西,它会在那里留下一段时间。
所以如果你真的想清空垃圾来立即减less回购的克隆大小 ,你必须做所有这些很奇怪的事情:
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
这就是说,我build议不要执行这些步骤,除非你知道你需要 – 以防万一你修剪了错误的子目录,你知道吗? 备份文件不应该克隆,当你推回购,他们只会在你的本地副本。
信用
Paul的答案创build了一个包含/ ABC的新存储库,但不会从/ XYZ中删除/ ABC。 以下命令将从/ XYZ中删除/ ABC:
git filter-branch --tree-filter "rm -rf ABC" --prune-empty HEAD
当然,首先在“clone – no-hardlinks”存储库中进行testing,然后按照Paul列出的reset,gc和prune命令进行testing。
我发现为了正确删除旧版本库中的旧版历史logging,你必须在filter-branch
步骤之后做更多的工作。
-
做克隆和filter:
git clone --no-hardlinks foo bar; cd bar git filter-branch --subdirectory-filter subdir/you/want
-
删除所有对旧历史的引用。 “origin”跟踪你的克隆,而“original”是filter-branch保存旧内容的地方:
git remote rm origin git update-ref -d refs/original/refs/heads/master git reflog expire --expire=now --all
-
即使现在,你的历史可能会卡在fsck不会触及的包文件中。 撕碎它,创build一个新的packfile并删除未使用的对象:
git repack -ad
在滤波器分支手册中有对此的解释 。
编辑:添加了Bash脚本。
这里给出的答案对我来说只是部分的工作。 大量的大文件仍然在caching中。 什么终于工作(在freenode的#git后小时):
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
使用以前的解决scheme,存储库大小大约为100 MB。 这一个把它降到1.7 MB。 也许它有助于某人:)
以下bash脚本自动执行任务:
!/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
这已经不再那么复杂了,你可以在repo的克隆上使用git filter-branch命令来挑选你不想要的子目录,然后推送到新的远程。
git filter-branch --prune-empty --subdirectory-filter <YOUR_SUBDIR_TO_KEEP> master git push <MY_NEW_REMOTE_URL> -f .
更新 :git子树模块是非常有用的,git团队将其拉入核心,并使其git subtree
。 看到这里: 分离(移动)到不同的Git仓库的子目录
git-subtree可能对此有用
http://github.com/apenwarr/git-subtree/blob/master/git-subtree.txt (不build议使用)
http://psionides.jogger.pl/2010/02/04/sharing-code-between-projects-with-git-subtree/
为了将多个子文件夹 (比如说sub1
和sub2
)分割成一个新的git仓库,下面是对CoolAJ86的“The Easy Way™”答案的一个小修改。
Easy Way™(多个子文件夹)
-
准备旧的回购
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
注意:
<name-of-folder>
不得包含前导字符或结尾字符。 例如,名为subproject
的文件夹务必作为subproject
传递,而不是./subproject/
Windows用户注意:当文件夹深度> 1时,
<name-of-folder>
必须包含* nix样式的文件夹分隔符(/)。 例如,名为path1\path2\subproject
的文件夹务必作为path1/path2/subproject
传递。 另外不要使用mv
命令而是move
。最后说明:与基本答案的独特和巨大的差异是脚本的第二行“
git filter-branch...
” -
创build新的回购
mkdir <new-repo> pushd <new-repo> git init git pull </path/to/big-repo> <name-of-new-branch>
-
将新的回购链接链接到Github或任何地方
git remote add origin <git@github.com:my-user/new-repo.git> git push origin -u master
-
清理, 如果需要的话
popd # get out of <new-repo> pushd <big-repo> git rm -rf <name-of-folder>
注意 :这会在资源库中留下所有历史引用。如果您确实担心提交了密码,或者需要减小
.git
文件夹的文件大小,请参阅原始答案中的附录 。
原来的问题是希望XYZ / ABC /(*文件)成为ABC / ABC /(*文件)。 在为我自己的代码实现接受的答案之后,我注意到它实际上将XYZ / ABC /(*文件)更改为ABC /(*文件)。 filter分支手册甚至说,
结果将包含该目录(只有那个) 作为它的项目根目录 。“
换句话说,它提升了顶层文件夹“向上”的一个层次。 这是一个重要的区别,因为,例如,在我的历史中,我已经重新命名了一个顶级文件夹。 通过将文件夹提升到一个级别,git在我重命名的提交中失去了连续性。
我接下来的答案是制作2个版本库,然后手动删除每个文件夹。 手册页支持我:
[…]避免使用[这个命令],如果一个简单的单一提交就足以解决您的问题
要添加到Paul的答案 ,我发现最终要恢复空间,我必须将HEAD推到一个干净的存储库,并减less.git / objects / pack目录的大小。
即
$ mkdir ... ABC.git $ cd ... ABC.git $ git init --bare
gc修剪完之后,还要做:
$ git push ... ABC.git HEAD
那你可以做
$ git clone ... ABC.git
并且ABC / .git的大小减小了
实际上,一些耗时的步骤(例如git gc)在推送到清理仓库时并不需要,即:
$ git clone --no-hardlinks / XYZ / ABC $ git filter-branch --subdirectory-filter ABC HEAD $ git reset --hard $ git push ... ABC.git HEAD
正确的方法是:
git filter-branch --prune-empty --subdirectory-filter FOLDER_NAME [first_branch] [another_branch]
GitHub现在甚至有关于这种情况的小文章 。
但一定要克隆你原来的回购单独目录第一(因为它会删除所有的文件和其他目录,你可能需要与他们合作)。
所以你的algorithm应该是:
- 克隆你的远程回购到另一个目录
- 使用
git filter-branch
只留下一些子目录下的文件,推送到新的远程 - 创build提交从您的原始远程回购中删除此子目录
看来,大多数(所有?)的答案在这里依赖于某种forms的git filter-branch --subdirectory-filter
及其git filter-branch --subdirectory-filter
。 这可能会工作“最多次”,但是在某些情况下,例如,当您重命名文件夹的情况下,例如:
ABC/ /move_this_dir # did some work here, then renamed it to ABC/ /move_this_dir_renamed
如果您使用正常的gitfilter样式来提取“move_me_renamed”,那么您将丢失最初从move_this_dir( ref )开始的文件更改历史logging。
因此,看起来,真正保留所有变更历史的唯一方法(如果你是这样的情况)本质上是复制存储库(创build一个新的回购,将其设置为原点),然后核对其他所有并将子目录重命名为父级,如下所示:
- 在本地克隆多模块项目
- 分支 – 检查有什么:
git branch -a
- 做一个签出到每个分支被包括在拆分,以获得您的工作站上的本地副本:
git checkout --track origin/branchABC
- 在新的目录下复制:
cp -r oldmultimod simple
- 进入新的项目副本:
cd simple
- 摆脱这个项目中不需要的其他模块:
-
git rm otherModule1 other2 other3
- 现在只剩下目标模块的子目录
- 摆脱模块subdir,使模块根成为新的项目根
-
git mv moduleSubdir1/* .
- 删除
rmdir moduleSubdir1
:rmdir moduleSubdir1
- 在任何时候检查更改:
git status
- 创build新的git仓库并复制它的URL来指向这个项目:
-
git remote set-url origin http://mygithost:8080/git/our-splitted-module-repo
- validation这是好的:
git remote -v
- 将更改推送到远程回购:
git push
- 去远程回购,并检查它在那里
- 重复它所需的任何其他分支:
git checkout branch2
这遵循github文档“将子文件夹拆分到新的存储库”步骤6-11,将模块推到新的仓库 。
这不会为您保存.git文件夹中的任何空间,但即使在重命名时,它也会保留这些文件的所有更改历史logging。 如果没有“很多”的历史遗失等,这可能不值得,但至less你保证不会失去旧的承诺!
对于什么是值得的,这里是如何在Windows机器上使用GitHub。 假设你有一个克隆的回购站在C:\dir1
。 目录结构如下所示: C:\dir1\dir2\dir3
。 目录dir3
是我想成为一个新的单独的回购。
Github上:
- 创build您的新存储库:
MyTeam/mynewrepo
Bash提示:
-
$ cd c:/Dir1
-
$ git filter-branch --prune-empty --subdirectory-filter dir2/dir3 HEAD
返回:Ref 'refs/heads/master' was rewritten
(fyi:dir2 / dir3区分大小写) -
$ git remote add some_name git@github.com:MyTeam/mynewrepo.git
git remote add origin etc
。 没有工作,返回“remote origin already exists
” -
$ git push --progress some_name master
正如我上面提到的 ,我必须使用相反的解决scheme(删除所有未触及我的dir/subdir/targetdir
),这似乎工作很好,去除了大约95%的提交(根据需要)。 但是,还有两个小问题。
首先 , filter-branch
在删除引入或修改代码的提交方面做了很多工作,但显然合并提交在Gitiverse的工作站之下。
- 截图:合并疯狂!
这是一个我可能可以忍受的化妆品问题(他说…避开眼睛慢慢退去) 。
第二 ,几乎所有的提交都是重复的! 我似乎已经获得了第二个冗长的时间表,几乎涵盖了整个项目的历史。 有趣的事情(你可以从下面的图中看到)是我的三个地方分支并不在同一时间线上(这当然是为什么它存在,而不是垃圾收集)。
- Screnshot:Double-double,Git滤镜分支样式
我能想象的唯一的事情就是,其中一个被删除的提交可能是filter-branch
实际上删除的单个合并提交,并且创build了并行时间线,因为每个现在未合并的链都提交了它自己的提交副本。 ( 耸肩我的TARDiS在哪里?)我很确定我能解决这个问题,但我真的很想知道它是如何发生的。
在疯狂的mergefest-O-RAMA的情况下,我很可能会独自离开那个人,因为它已经牢牢地固定在我的提交历史中 – 每当我走近时就对我咄咄逼人,似乎并没有真正造成任何非美容问题,因为它是相当漂亮的Tower.app。
我有这个问题,但基于git filter-branch的所有标准解决scheme都非常慢。 如果你有一个小的存储库,那么这可能不是一个问题,这是我的。 我编写了另一个基于libgit2的git过滤程序,第一步是为主存储库的每个过滤创build分支,然后将这些分支推送到清理存储库作为下一步。 在我的仓库(500Mb 100000提交)标准的git filter-branch方法花了几天时间。 我的程序需要几分钟来完成相同的过滤。
它有git_filter的神话般的名字,住在这里:
https://github.com/slobobaby/git_filter
在GitHub上。
我希望对某人有用。
使用此过滤命令删除一个子目录,同时保留您的标签和分支:
git filter-branch --index-filter \ "git rm -r -f --cached --ignore-unmatch DIR" --prune-empty \ --tag-name-filter cat -- --all
在垃圾回收之前,你可能需要像“git reflog expire –expire = now –all”这样的文件来清理文件。 git filter-branch只删除历史logging中的引用,但不会删除保存数据的引用日志条目。 当然,先testing一下。
尽pipe我的初始条件有所不同,但是我的磁盘使用率却大幅下降。 也许–subdirectoryfilter否定了这种需要,但我怀疑它。
更简单的方法
- 安装
git splits
。 我创build它作为git扩展,基于jkeating的解决scheme 。 -
拆分目录到本地分支
#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#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 -
在某处创build一个空的回购。 我们假设我们在GitHub上创build了一个名为
xyz
的空回购,其path为:git@github.com:simpliwp/xyz.git
-
推到新的回购。
#add a new remote origin for the empty repo so we can push to the empty repo on GitHub git remote add origin_xyz git@github.com:simpliwp/xyz.git #push the branch to the empty repo's master branch git push origin_xyz XYZ:master
-
将新创build的远程仓库克隆到新的本地目录中
#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 git@github.com:simpliwp/xyz.git
在https://github.com/vangorra/git_split上查看git_split项目;
把git目录转到他们自己的仓库中。 没有子树有趣的业务。 这个脚本将会在你的git仓库中获得一个现有的目录,并把这个目录转换成一个独立的仓库。 一路上,它将复制您提供的目录的整个更改历史logging。
./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.
把这个放到你的gitconfig中:
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'
我敢肯定Git子树是好的,但是我的git托pipe代码的子目录全部都是在eclipse中。 所以,如果你使用egit,那很容易。 以你想移动的项目和团队 – >断开它,然后团队 – >分享到新的位置。 它将默认尝试使用旧的回购地点,但您可以取消选中使用现有的select,并select新的地方来移动它。 所有的冰雹。
我build议GitHub的指南将子文件夹拆分成一个新的存储库 。 这些步骤与Paul的答案类似,但是我发现他们的说明更容易理解。
我修改了说明,以便申请本地存储库,而不是托pipe在GitHub上。
将一个子文件夹拆分成一个新的存储库
打开Git Bash。
将当前工作目录更改为您要创build新存储库的位置。
克隆包含子文件夹的存储库。
git clone OLD-REPOSITORY-FOLDER NEW-REPOSITORY-FOLDER
- 将当前工作目录更改为您的克隆存储库。
cd REPOSITORY-NAME
- 要从存储库中的其余文件中过滤掉子文件夹,请运行
git filter-branch
,提供以下信息:
FOLDER-NAME
:您希望从中创build单独存储库的项目文件夹。
- 提示:Windows用户应该使用
/
来分隔文件夹。BRANCH-NAME
:您当前项目的默认分支,例如master
或gh-pages
。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