将子目录分离(移动)到单独的 Git 存储库中
-
21-08-2019 - |
题
我有一个 git 存储库,其中包含许多子目录。现在我发现其中一个子目录与另一个子目录无关,应该分离到一个单独的存储库。
如何在保留子目录中文件的历史记录的同时执行此操作?
我想我可以制作一个克隆并删除每个克隆中不需要的部分,但我想这会在检查旧版本等时为我提供完整的树。这可能是可以接受的,但我更希望能够假装这两个存储库没有共享的历史记录。
为了清楚起见,我有以下结构:
XYZ/
.git/
XY1/
ABC/
XY2/
但我想要这个:
XYZ/
.git/
XY1/
XY2/
ABC/
.git/
ABC/
解决方案
更新: :这个过程非常常见,以至于 git 团队使用新工具使其变得更加简单, git subtree
. 。看这里: 将子目录分离(移动)到单独的 Git 存储库中
您想要克隆您的存储库,然后使用 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
.然后使用filter-branch和reset来排除其他文件,这样它们就可以被修剪。我们还添加一下
--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 发现的那样,这会将项目分支和标签保留在新存储库中。
编辑:以下评论中的各种建议被纳入其中,以确保存储库实际上已缩小(以前的情况并非总是如此)。
其他提示
简单的方法™
事实证明,这是一个如此常见且有用的做法,以至于 git 的霸主使它变得非常简单,但你必须拥有较新版本的 git(> = 1.7.11 May 2012)。请参阅 附录 了解如何安装最新的 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>
将新存储库链接到 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>
.
这是我在 Node 中实现 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 等上创建一个新的存储库,并将其添加为 origin
(顺便说一句,“origin”只是一个约定,不是命令的一部分 - 您可以将其称为“远程服务器”或任何您喜欢的名称)
git remote add origin git@github.com:node-browser-compat/btoa.git
git push origin -u master
快乐的一天!
笔记: 如果您创建了一个带有 README.md
, .gitignore
和 LICENSE
, ,你需要先拉:
git pull origin -u master
git push origin -u master
最后,我想从更大的存储库中删除该文件夹
git rm -rf btoa
...
附录
OS X 上的最新 git
要获取最新版本的 git:
brew install git
要获取适用于 OS X 的 brew:
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 历史记录中
git log -- <name-of-folder> # should show nothing
然而,你 无法将删除内容“推送”到 github 等等。如果你尝试,你会得到一个错误,你必须 git pull
在你可以之前 git push
- 然后你又回到了拥有你历史中的一切。
因此,如果您想从“源”删除历史记录(即从 github、bitbucket 等删除历史记录),您需要删除该存储库并重新推送该存储库的修剪副本。可是等等 - 还有更多!- 如果您真的担心删除密码或类似的东西,您需要修剪备份(见下文)。
制作 .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
也就是说,我建议不要执行这些步骤,除非您知道需要这样做 - 以防万一您确实修剪了错误的子目录,您知道吗?当您推送存储库时,备份文件不应被克隆,它们只会位于您的本地副本中。
信用
保罗的回答创建包含/ ABC新的存储库,但不从/ XYZ内删除/ ABC。
:下面的命令将从/ XYZ内取出/ ABCgit filter-branch --tree-filter "rm -rf ABC" --prune-empty HEAD
当然,在一个“克隆--no-硬链接”测试它储存库第一,并与复位,GC按照它和剪枝命令保罗列出。
我发现为了从新存储库中正确删除旧历史记录,您必须在 filter-branch
步。
进行克隆和过滤:
git clone --no-hardlinks foo bar; cd bar git filter-branch --subdirectory-filter subdir/you/want
删除所有对旧历史的引用。“origin”是跟踪你的克隆,“original”是过滤器分支保存旧东西的地方:
git remote rm origin git update-ref -d refs/original/refs/heads/master git reflog expire --expire=now --all
即使现在,您的历史记录也可能被困在 fsck 无法触及的包文件中。将其撕成碎片,创建一个新的包文件并删除未使用的对象:
git repack -ad
编辑:bash脚本添加
。下面给出的答案的工作只是部分地为我;大文件很多留在缓存中。什么终于研究出(小时后在#git freenode上):
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
此不再那么复杂可以只使用 git的滤波器分支一>在克隆的你回购剔除你不想要的子目录,然后推到新的远程命令。
git filter-branch --prune-empty --subdirectory-filter <YOUR_SUBDIR_TO_KEEP> master
git push <MY_NEW_REMOTE_URL> -f .
更新: :git-subtree 模块非常有用,以至于 git 团队将其拉入核心并使其成为可能 git subtree
. 。看这里: 将子目录分离(移动)到单独的 Git 存储库中
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/
这是一个小修改 酷AJ86的 “简单方法™”的答案 为了分裂 多个子文件夹 (假设 sub1
和 sub2
) 到一个新的 git 存储库中。
The 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...
"创建新的存储库
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 /(*文件)。该过滤器分支手册页甚至说,
的结果将包含该目录(并且只有)作为它的项目根“。
在换句话说,它促进了顶级文件夹“向上”一个级别。这是一个重要的区别,因为,例如,在我的历史我已经改名为一个顶层文件夹。通过促进夹“涨”声一片的水平,混帐在失去连续性承诺在那里我做了重命名。
我的问题的答案则是使仓库的2份,并手动删除您希望保留在每个文件夹(S)。该名男子页背我了这样的:
[...]避免使用[此命令]如果简单单一提交就足以解决您的问题
要添加到保罗的回答,我发现,最终恢复空间,我要推的头一个干净库和修剪下来的.git /对象/包目录的大小。
即
$ 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 现在甚至有 小文章 关于此类案例。
但请务必首先将原始存储库克隆到单独的目录(因为它会删除所有文件和其他目录,并且您可能需要使用它们)。
所以你的算法应该是:
- 将远程存储库克隆到另一个目录
- 使用
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 时发生的文件更改历史记录(参考).
因此看来,真正保持 全部 更改历史记录(如果您的情况是这样的),本质上是复制存储库(创建一个新的存储库,将其设置为源),然后删除其他所有内容并将子目录重命名为父目录,如下所示:
- 将多模块项目克隆到本地
- 分支机构 - 检查那里有什么:
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 存储库并复制其 URL 以将该项目指向其中:
git remote set-url origin http://mygithost:8080/git/our-splitted-module-repo
- 验证这是否良好:
git remote -v
- 将更改推送到远程存储库:
git push
- 转到远程仓库并检查它是否全部存在
- 对所需的任何其他分支重复此操作:
git checkout branch2
这如下 github 文档“将子文件夹拆分到新存储库中” 步骤 6-11 将模块推送到新的存储库。
这不会为您节省 .git 文件夹中的任何空间,但它会保留这些文件的所有更改历史记录,即使是在重命名期间也是如此。如果没有“大量”历史丢失等,这可能不值得。但至少保证你不会丢失旧的提交!
我正是这个问题,但都基于git的过滤分支的标准溶液是极其缓慢。如果你有一个小仓库,那么这可能不是一个问题,这是对我来说。我写基于libgit2另一个GIT中的过滤程序,其作为第一步骤,用于在主存储库中的每个滤波建立分支并压入这些清洁库作为下一步骤。在我的仓库(500MB的100000提交)标准git的过滤分支方法需要数天。我的节目只需要几分钟做同样的滤波。
它git_filter神话般的名字和住在这里:
https://github.com/slobobaby/git_filter
在GitHub。
我希望这是有用的人。
无论如何,以下是如何在 Windows 计算机上使用 GitHub。假设您有一个克隆存储库位于 C:\dir1
. 。目录结构如下所示: C:\dir1\dir2\dir3
. 。这 dir3
目录是我想成为一个新的单独存储库的目录。
GitHub:
- 创建您的新存储库:
MyTeam/mynewrepo
重击提示:
$ cd c:/Dir1
$ git filter-branch --prune-empty --subdirectory-filter dir2/dir3 HEAD
回到:Ref 'refs/heads/master' was rewritten
(供参考: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
正如我 上文提到的, ,我不得不使用相反的解决方案(删除所有不触及我的提交 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
. 。我将它创建为 git 扩展,基于 jkeating的解决方案. 将目录拆分到本地分支
#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
在 GitHub 上有路径: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
将新创建的远程存储库克隆到新的本地目录中
#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
我建议 GitHub 将子文件夹拆分到新存储库的指南. 。步骤类似于 保罗的回答, ,但我发现他们的说明更容易理解。
我修改了说明,以便它们申请本地存储库,而不是托管在 GitHub 上的存储库。
将子文件夹拆分到新存储库中
打开 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
您可能需要像“混帐引用日志到期--expire =现在--all”垃圾收集之前,实际清理的文件出来。 git的过滤分支只是删除引用的历史,但不会删除保存数据的引用日志条目。当然,首先进行测试。
我的磁盘使用情况在做这个急剧下降,虽然我的初始条件有所不同。也许--subdirectory滤波器否定这种需要,但我怀疑。
查核git_split项目在 https://github.com/vangorra/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代码。 所以,如果你使用的例如:It,这是痛苦容易。 就拿你要移动和team->断开连接的项目,然后team->分享给新的位置。它会默认尝试使用老回购的位置,但你可以取消使用现有的选择和挑选移动它新的地方。 所有雹例如:It。
我发现很直接的解决方案, 我们的想法是复制存储库,然后只需删除不必要的部分。 这是如何工作的:
1)克隆一个仓库要分割
git clone git@git.thehost.io:testrepo/test.git
2)移动到GIT中的文件夹
cd test/
2)删除不必要的文件夹和提交它
rm -r ABC/
git add .
enter code here
git commit -m 'Remove ABC'
3) BFG 删除不必要的文件夹(S)形式的历史p>
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 git@github.com:username/new_repo
git push -u origin master
就是这样。您可以重复步骤获得另一个仓库,
只是删除XY1,XY2和重命名XYZ - > ABC上的步骤3
这对我有用。我在上述步骤中遇到的问题是
在这个命令中
git filter-branch --prune-empty --subdirectory-filter FOLDER-NAME BRANCH-NAME
这BRANCH-NAME
是 掌握如果由于保护问题而提交时最后一步失败,请遵循 - https://docs.gitlab.com/ee/user/project/protected_branches.html