我有一个Git存储库,其中包含许多子目录。现在我发现其中一个子目录与另一个子目录无关,应该分离到一个单独的存储库。
如何在将文件的历史记录保存在子目录中的同时执行此操作?
我想我可以制作一个克隆并删除每个克隆的不需要的部分,但我想这会给我一个完整的树,当检查旧版本等。这可能是可以接受的,但我宁愿能够假装两个存储库没有共享历史记录。
为了说清楚,我有以下结构:
XYZ/
.git/
XY1/
ABC/
XY2/
但我想这样做:
XYZ/
.git/
XY1/
XY2/
ABC/
.git/
ABC/
更新:这个过程是如此常见,以至于git团队使用新工具git subtree
使其变得更加简单。见这里:Detach (move) subdirectory into separate Git repository
您想要克隆您的存储库,然后使用git filter-branch
标记除了您的新存储库中所需的子目录之外的所有内容,以进行垃圾收集。
git clone /XYZ /ABC
(注意:存储库将使用硬链接进行克隆,但这不是问题,因为硬链接文件本身不会被修改 - 将创建新的文件。)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
(因为它们都与子项目无关);此外,在移除此类标签后,将回收更多空间。显然git filter-branch
应该能够重写其他标签,但我无法验证这一点。如果要删除所有标记,请使用git tag -l | xargs git tag -d
。--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
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
。是的,真的是--space-- all
。这需要是命令的最后一个参数。正如Matli发现的那样,这使得项目分支和标签包含在新的仓库中。
编辑:合并了以下评论中的各种建议,以确保,例如,存储库实际上是缩小的(以前并非总是如此)。
要添加到Paul's answer,我发现要最终恢复空间,我必须将HEAD推送到一个干净的存储库,并减少.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现在甚至有关于这种情况的small article。
但请务必先将原始仓库克隆到单独的目录中(因为它会删除所有文件和其他目录,并且您可能需要使用它们)。
所以你的算法应该是:
git filter-branch
只留下一些子目录下的文件,推送到新的远程似乎这里的大多数(全部?)答案都依赖于某种形式的git filter-branch --subdirectory-filter
及其同类。这可能“大多数时间”工作但是对于某些情况,例如重命名文件夹的情况,例如:
ABC/
/move_this_dir # did some work here, then renamed it to
ABC/
/move_this_dir_renamed
如果你使用普通的git过滤器样式来提取“move_me_renamed”,那么当你最初的move_this_dir(ref)时,你将丢失从后面发生的文件更改历史记录。
因此,看来真正保留所有变更历史的唯一方法(如果你的是这样的情况),实质上是复制存储库(创建一个新的存储库,设置它作为原点),然后核对其他一切并将子目录重命名为父目录,如下所示:
git branch -a
git checkout --track origin/branchABC
cp -r oldmultimod simple
cd simple
git rm otherModule1 other2 other3
git mv moduleSubdir1/* .
rmdir moduleSubdir1
git status
git remote set-url origin http://mygithost:8080/git/our-splitted-module-repo
git remote -v
git push
git checkout branch2
这遵循the github doc "Splitting a subfolder out into a new repository"步骤6-11将模块推送到新的仓库。
这不会为.git文件夹中的任何空间节省任何空间,但它会保留这些文件的所有更改历史记录,甚至是重命名文件。如果没有“很多”历史丢失等等,这可能不值得。但至少你保证不会丢失旧的提交!
我确实遇到了这个问题但基于git filter-branch的所有标准解决方案都非常慢。如果你有一个小的存储库,那么这可能不是问题,这对我来说。我编写了另一个基于libgit2的git过滤程序,作为第一步,为主存储库的每次过滤创建分支,然后在下一步将它们推送到清理存储库。在我的存储库(500Mb 100000提交)上,标准的git filter-branch方法需要数天时间。我的程序需要几分钟才能进行相同的过滤。
它有一个神奇的名字git_filter,住在这里:
https://github.com/slobobaby/git_filter
在GitHub上。
我希望它对某人有用。
对于它的价值,这里是如何在Windows机器上使用GitHub。假设您在C:\dir1
居住了克隆回购。目录结构如下所示:C:\dir1\dir2\dir3
。 dir3
目录是我想成为一个新的独立回购的目录。
Github上:
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 [email protected]:MyTeam/mynewrepo.git
git remote add origin etc
。没用,返回“remote origin already exists
”$ git push --progress some_name master
正如我mentioned above,我不得不使用反向解决方案(删除所有提交不接触我的dir/subdir/targetdir
),这似乎很好地删除了大约95%的提交(根据需要)。但是,还有两个小问题。
首先,filter-branch
完成了删除引入或修改代码的提交的工作,但显然,合并提交在Gitiverse的工作站下方。
这是一个我可以忍受的化妆品问题(他说......避免眼睛慢慢退缩)。
第二个剩下的几个提交几乎都是重复的!我似乎已经获得了第二个冗余的时间表,该时间表涵盖了项目的整个历史。有趣的事情(你可以从下图中看到)是,我的三个本地分支并非都在同一时间线上(这就是为什么它存在并且不仅仅是垃圾收集)。
我唯一可以想象的是,其中一个被删除的提交可能是filter-branch
实际上删除的单个合并提交,并且创建了并行时间线,因为每个现在未合并的链都采用了自己的提交副本。 (耸耸肩我的TARDiS在哪里?)我很确定我可以解决这个问题,虽然我真的很想知道它是怎么发生的。
在疯狂的mergefest-O-RAMA的情况下,我可能会单独留下那个,因为它已经在我的提交历史中如此坚定 - 每当我走近时都会对我施加威胁 - 它似乎并没有真正造成任何非美容问题,因为它在Tower.app中相当漂亮。
使用此过滤器命令删除子目录,同时保留标记和分支:
git filter-branch --index-filter \
"git rm -r -f --cached --ignore-unmatch DIR" --prune-empty \
--tag-name-filter cat -- --all
git splits
。我基于jkeating's solution创建了它作为git扩展。
#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
xyz
的空仓库,它有路径:[email protected]: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 [email protected]:simpliwp/xyz.git
#push the branch to the empty repo's master branch
git push origin_xyz XYZ:master
#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's guide to splitting subfolders into a new repository。这些步骤与Paul's answer类似,但我发现他们的说明更容易理解。
我修改了指令,以便它们申请本地存储库,而不是在GitHub上托管的存储库。
Splitting a subfolder out into a new repository
- 打开Git Bash。
- 将当前工作目录更改为要创建新存储库的位置。
- 克隆包含子文件夹的存储库。
git clone OLD-REPOSITORY-FOLDER NEW-REPOSITORY-FOLDER
- 将当前工作目录更改为克隆的存储库。
cd REPOSITORY-NAME
- 要从存储库中的其余文件中过滤掉子文件夹,请运行
git filter-branch
,提供以下信息:FOLDER-NAME
:项目中您要创建单独存储库的文件夹。 提示: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
在垃圾收集之前,您可能需要“git reflog expire --expire = now --all”来实际清理文件。 git filter-branch只删除历史记录中的引用,但不删除保存数据的reflog条目。当然,先测试一下。
尽管我的初始条件有所不同,但我的磁盘使用率却大幅下降。也许--subdirectory-filter否定了这个需求,但我对此表示怀疑。
事实证明,这是一种常见且有用的做法,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
传递mkdir <new-repo>
pushd <new-repo>
git init
git pull </path/to/big-repo> <name-of-new-branch>
git remote add origin <[email protected]: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
的提交,我想创建一个新的存储库。
mkdir ~/Code/btoa/
pushd ~/Code/btoa/
git init
git pull ~/Code/node-browser-compat btoa-only
接下来我在Github或bitbucket上创建一个新的repo,或者其他什么并添加它是origin
(顺便说一句,“origin”只是一个约定,不是命令的一部分 - 你可以称之为“远程服务器”或任何你喜欢的)
git remote add origin [email protected]:node-browser-compat/btoa.git
git push origin -u master
愉快的一天!
注意:如果你用README.md
,.gitignore
和LICENSE
创建了一个repo,你需要先拉:
git pull origin -u master
git push origin -u master
最后,我想从更大的仓库中删除该文件夹
git rm -rf btoa
...
要获取最新版本的git:
brew install git
要获得OS X的酿造:
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历史记录中
git log -- <name-of-folder> # should show nothing
但是,您无法“删除”删除到github等。如果你尝试你会得到一个错误,你必须git pull
才能你git push
- 然后你回到你的历史中的一切。
因此,如果您想从“origin”中删除历史记录 - 意味着从github,bitbucket等删除它 - 您需要删除repo并重新推送repo的修剪副本。但是等等 - 还有更多! - 如果你真的担心要删除密码或类似的东西,你需要修剪备份(见下文)。
.git
更小前面提到的删除历史记录命令仍然留下了一堆备份文件 - 因为git非常友好,可以帮助您不会意外毁坏您的回购。它最终将在几天和几个月内删除孤立的文件,但是如果你意识到你不小心删除了你不想要的东西,它会在那里留下一段时间。
因此,如果你真的想要清空垃圾桶以立即减少回购的克隆大小,你必须做所有这些非常奇怪的事情:
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
也就是说,我建议不要执行这些步骤,除非你知道你需要 - 以防万一你修剪了错误的子目录,你知道吗?推送回购时,不应克隆备份文件,它们只是在本地副本中。
查看https://github.com/vangorra/git_split上的git_split项目
将git目录转换为自己位置的自己的存储库。没有子树有趣的业务。此脚本将获取您的git存储库中的现有目录,并将该目录转换为其自己的独立存储库。在此过程中,它将复制您提供的目录的整个更改历史记录。
./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托管代码的子目录都是eclipse。所以如果你使用egit,那很容易。获取您要移动的项目和团队 - >断开连接,然后团队 - >将其分享到新位置。它将默认尝试使用旧的仓库位置,但您可以取消选中使用现有的选择并选择新位置来移动它。所有人都欢呼。
我发现了很直接的解决方案,想法是复制存储库,然后删除不必要的部分。这是它的工作原理:
1)克隆您要拆分的存储库
git clone [email protected]:testrepo/test.git
2)移动到git文件夹
cd test/
2)删除不必要的文件夹并提交它
rm -r ABC/
git add .
enter code here
git commit -m 'Remove ABC'
3)使用BFG删除不必要的文件夹表单历史记录
cd ..
java -jar bfg.jar --delete-folders "{ABC}" test
cd test/
git reflog expire --expire=now --all && git gc --prune=now --aggressive
对于多个文件夹,您可以使用逗号
java -jar bfg.jar --delete-folders "{ABC1,ABC2}" metric.git
4)检查历史记录是否包含刚刚删除的文件/文件夹
git log --diff-filter=D --summary | grep delete
5)现在你有没有ABC的干净存储库,所以只需将它推入新的原点
remote add origin [email protected]:username/new_repo
git push -u origin master
而已。您可以重复这些步骤以获取另一个存储库,
只需删除XY1,XY2并在步骤3重命名XYZ - > ABC
这对我有用。我在上面给出的步骤中遇到的问题是
git filter-branch --prune-empty --subdirectory-filter FOLDER-NAME BRANCH-NAME
BRANCH-NAME
是主人Paul's answer创建一个包含/ ABC的新存储库,但不从/ XYZ中删除/ ABC。以下命令将从/ XYZ中删除/ ABC:
git filter-branch --tree-filter "rm -rf ABC" --prune-empty HEAD
当然,首先在'clone --no-hardlinks'存储库中测试它,然后使用Paul列出的reset,gc和prune命令进行测试。
我发现为了从新存储库中正确删除旧历史记录,您必须在filter-branch
步骤之后再做一些工作。
git clone --no-hardlinks foo bar; cd bar
git filter-branch --subdirectory-filter subdir/you/want
git remote rm origin
git update-ref -d refs/original/refs/heads/master
git reflog expire --expire=now --all
git repack -ad
编辑:添加了Bash脚本。
这里给出的答案对我来说只是部分起作用;缓存中还有很多大文件。终于有效了(在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
使用以前的解决方案,存储库大小约为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-subtree模块非常有用,git团队将其拉入核心并使其成为git subtree
。见这里:Detach (move) subdirectory into separate Git repository
git-subtree可能对此有用
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™" answer的一个小修改,以便将多个子文件夹(比如说sub1
and sub2
)拆分成一个新的git存储库。
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
command但move
。
最后的注释:基本答案的独特和巨大差异是脚本“git filter-branch...
”的第二行mkdir <new-repo>
pushd <new-repo>
git init
git pull </path/to/big-repo> <name-of-new-branch>
git remote add origin <[email protected]: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-branch手册页甚至说,
结果将包含该目录(并且仅包含该目录)作为其项目根目录。“
换句话说,它将顶级文件夹“升级”一级。这是一个重要的区别,因为,例如,在我的历史中,我已经重命名了一个顶级文件夹。通过将文件夹“提升”到一个级别,git在我进行重命名的提交时失去连续性。
我对问题的回答是制作2个存储库副本并手动删除要保留在每个存储库中的文件夹。该手册页支持我:
[...]如果简单的单一提交足以解决您的问题,请避免使用[此命令]