题 将(移动)子目录分离到单独的Git存储库中


我有一个 混帐 包含许多子目录的存储库。现在我发现其中一个子目录与另一个子目录无关,应该分离到一个单独的存储库。

如何在将文件的历史记录保存在子目录中的同时执行此操作?

我想我可以制作一个克隆并删除每个克隆的不需要的部分,但我想这会给我一个完整的树,当检查旧版本等。这可能是可以接受的,但我宁愿能够假装两个存储库没有共享历史记录。

为了说清楚,我有以下结构:

XYZ/
    .git/
    XY1/
    ABC/
    XY2/

但我想这样:

XYZ/
    .git/
    XY1/
    XY2/
ABC/
    .git/
    ABC/

1595
2017-12-11 13:57


起源


现在这很简单 git filter-branch 看下面的答案。 - jeremyjjbrown
@jeremyjjbrown是对的。这已经不难做了,但很难在Google上找到正确的答案,因为所有旧的答案都支配着结果。 - Agnel Kurian


答案:


更新:这个过程很常见,git团队用新工具简化了它, git subtree。看这里: 将(移动)子目录分离到单独的Git存储库中


您想要克隆您的存储库然后使用 git filter-branch 标记除了新回购中所需的子目录之外的所有内容都要进行垃圾回收。

  1. 要克隆本地存储库:

    git clone /XYZ /ABC
    

    (注意:存储库将使用硬链接进行克隆,但这不是问题,因为硬链接文件本身不会被修改 - 将创建新的文件。)

  2. 现在,让我们保留我们想要重写的有趣分支,然后删除原点以避免在那里推送并确保原始提交不会被原点引用:

    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
    
  3. 现在您可能还想删除与子项目无关的标记;您也可以稍后再这样做,但您可能需要再次修剪您的仓库。我没有这样做,得到了一个 WARNING: Ref 'refs/tags/v0.1' is unchanged 对于所有标签(因为它们都与子项目无关);此外,在移除此类标签后,将回收更多空间。显然地 git filter-branch 应该能够重写其他标签,但我无法验证这一点。如果要删除所有标记,请使用 git tag -l | xargs git tag -d

  4. 然后使用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
    
  5. 然后删除备份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发现的那样,这使得项目分支和标签包含在新的回购中。

编辑:合并了以下评论中的各种建议,以确保,例如,存储库实际上是缩小的(以前并非总是如此)。


1155
2017-07-25 17:10



很好的答案。谢谢!为了真正得到我想要的东西,我在filter-branch命令中添加了“ - --all”。 - matli
为什么你需要 --no-hardlinks?删除一个硬链接不会影响另一个文件。 Git对象也是不可变的。仅当您更改所需的所有者/文件权限时 --no-hardlinks。 - vdboor
我建议的另一个步骤是“git remote rm origin”。如果我没有弄错的话,这将继续推回原始存储库。 - Tom
要追加的另一个命令 filter-branch 是 --prune-empty,删除now-empty提交。 - Seth Johnson
像保罗一样,我不想在我的新回购中使用项目标签,所以我没有使用 -- --all。我也跑了 git remote rm origin,和 git tag -l | xargs git tag -d 之前 git filter-branch 命令。这缩小了我的意思 .git 目录从60M到~300K。请注意,我需要运行这两个命令才能减小大小。 - saltycrane


Easy Way™

事实证明,这是一个非常普遍和有用的做法,git的霸主使它变得非常容易,但你必须有一个更新版本的git(> = 1.7.11 2012年5月)。见 附录 如何安装最新的git。还有一个 现实世界的例子 在里面 演练 下面。

  1. 准备旧的回购

    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

  2. 创建新的仓库

    mkdir <new-repo>
    pushd <new-repo>
    
    git init
    git pull </path/to/big-repo> <name-of-new-branch>
    
  3. 将新的回购链接链接到Github或任何地方

    git remote add origin <git@github.com:my-user/new-repo.git>
    git push origin -u master
    
  4. 清理, 如果需要的话

    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 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的酿造:

http://brew.sh

关于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  - 然后你又回到了历史的一切。

因此,如果你想从“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

也就是说,我建议不要执行这些步骤,除非你知道你需要 - 以防万一你修剪了错误的子目录,你知道吗?推送回购时,不应克隆备份文件,它们只是在您的本地副本中。

信用


1124
2018-06-05 13:15



git subtree 仍然是'contrib'文件夹的一部分,默认情况下不会在所有发行版上安装。 github.com/git/git/blob/master/contrib/subtree - onionjake
@krlmlr 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在Ubuntu 13.04上激活 - rui.araujo
如果您已将密码推送到公共存储库,则应更改密码,而不是尝试将其从公共存储库中删除,并希望没有人看到它。 - Miles Rout
这似乎是一个新的回购与内容 ABC/,但新的repo不包含该文件夹 ABC/ 问题就在于问题。你会怎么做? - woojoo666
此解决方案不保留历史记录。 - Cœur


保罗的回答 创建一个包含/ ABC的新存储库,但不从/ XYZ中删除/ ABC。以下命令将从/ XYZ中删除/ ABC:

git filter-branch --tree-filter "rm -rf ABC" --prune-empty HEAD

当然,首先在'clone --no-hardlinks'存储库中测试它,然后使用Paul列出的reset,gc和prune命令进行测试。


131
2017-10-19 21:10



做那个 git filter-branch --index-filter "git rm -r -f --cached --ignore-unmatch ABC" --prune-empty HEAD 它会 许多 更快。 index-filter对索引起作用,而tree-filter必须checkout和stage 每一次提交的一切。 - fmarc
在某些情况下搞乱存储库XYZ的历史是有点过分的......只是一个简单的“rm -rf ABC; git rm -r ABC; git commit -m'extracted ABC into the repo'”对大多数人来说效果会更好。 - Evgeny
如果多次执行此操作,您可能希望对此命令使用-f(强制),例如,在分离后删除两个目录。否则,您将收到“无法创建新备份”。 - Brian Carlton
如果你正在做的话 --index-filter 方法,你可能也想做 git rm -q -r -f,这样每次调用都不会为它删除的每个文件打印一行。 - Eric Naeseth
我建议编辑保罗的答案,只是因为保罗是如此彻底。 - Erik Aronesty


我发现为了从新的存储库中正确删除旧的历史记录,你必须在之后再做一些工作 filter-branch 步。

  1. 做克隆和过滤器:

    git clone --no-hardlinks foo bar; cd bar
    git filter-branch --subdirectory-filter subdir/you/want
    
  2. 删除对旧历史的每个引用。 “origin”跟踪你的克隆,而“original”是filter-branch保存旧东西的地方:

    git remote rm origin
    git update-ref -d refs/original/refs/heads/master
    git reflog expire --expire=now --all
    
  3. 即使是现在,您的历史可能会陷入fsck无法触及的包文件中。撕碎它,创建一个新的packfile并删除未使用的对象:

    git repack -ad
    

对此的解释 在里面 过滤器分支手册


94
2018-06-09 15:41



我想有些想法 git gc --aggressive --prune=now 仍然缺少,不是吗? - Albert
@Albert重新打包命令负责处理,并且不会有任何松散的对象。 - Josh Lee
只是重新包装不适合我,需要做git gc - jsvnm
是啊, git gc --aggressive --prune=now 减少了大量的新回购 - Tomek Wyderka
简单而优雅。谢谢! - Marco Pelegrini


编辑:添加了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

38
2017-08-20 14:11





这不再那么复杂,你可以使用 git filter-branch 命令克隆你的repo以剔除你不想要的子目录,然后推送到新的遥控器。

git filter-branch --prune-empty --subdirectory-filter <YOUR_SUBDIR_TO_KEEP> master
git push <MY_NEW_REMOTE_URL> -f .

21
2018-03-22 20:55



这就像一个魅力。上例中的YOUR_SUBDIR是您要保留的子目录,其他所有内容都将被删除 - J.T. Taylor
更新基于您的评论。 - jeremyjjbrown
这不回答这个问题。从它说的文档 The result will contain that directory (and only that) as its project root. 事实上,这就是你将得到的,即不保留原始项目结构。 - NicBright
@NicBright你可以用问题来说明XYZ和ABC的问题,以显示出错了吗? - Adam
@jeremyjjbrown是否有可能重用克隆的回购,而不是使用新的回购,即我的问题在这里 stackoverflow.com/questions/49269602/... - Qiulang


更新: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/


19
2017-08-06 15:26



git-subtree现在是Git的一部分,虽然它位于contrib树中,因此默认情况下并不总是安装。我知道它是由Homebrew git公式安装的,但没有手册页。因此,apenwarr称他的版本已经过时了。 - echristopherson


这是一个小修改 CoolAJ86“Easy Way™”答案 为了分裂 多个子文件夹 (让我们说吧 sub1sub2)进入一个新的git存储库。

Easy Way™(多个子文件夹)

  1. 准备旧的回购

    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...

  2. 创建新的仓库

    mkdir <new-repo>
    pushd <new-repo>
    
    git init
    git pull </path/to/big-repo> <name-of-new-branch>
    
  3. 将新的回购链接链接到Github或任何地方

    git remote add origin <git@github.com:my-user/new-repo.git>
    git push origin -u master
    
  4. 清理, 如果需要的话

    popd # get out of <new-repo>
    pushd <big-repo>
    
    git rm -rf <name-of-folder>
    

    注意:这将保留存储库中的所有历史引用。请参阅 附录 在原始答案中,如果你真的担心提交密码或者你需要减少你的文件大小 .git 夹。


13
2018-04-17 05:12



这对我有所改变。因为我的 sub1 和 sub2初始版本不存在文件夹,我不得不修改我的 --tree-filter 脚本如下: "mkdir <name-of-folder>; if [ -d sub1 ]; then mv <sub1> <name-of-folder>/; fi"。第二个 filter-branch 命令我将<sub1>替换为<sub2>,省略了<name-of-folder>的创建,并包含在内 -f 后 filter-branch 覆盖现有备份的警告。 - pglezen
如果任何子目录在git中的历史记录中发生了变化,则不起作用。怎么解决这个问题? - nietras
@nietras看到rogerdpack的回答。在阅读并吸收了其他答案中的所有信息后,我花了一段时间才找到它。 - Adam


原始问题想要XYZ / ABC /(*文件)成为ABC / ABC /(*文件)。在为我自己的代码实现接受的答案后,我注意到它实际上将XYZ / ABC /(*文件)更改为ABC /(*文件)。 filter-branch手册页甚至说,

结果将包含该目录(并且仅包含该目录) 作为其项目根“。

换句话说,它将顶层文件夹“提升”一级。这是一个重要的区别,因为,例如,在我的历史中,我已经重命名了一个顶级文件夹。通过将文件夹“提升”到一个级别,git在我进行重命名的提交时失去连续性。

I lost contiuity after filter-branch

我对问题的回答是制作2个存储库副本并手动删除要保留在每个存储库中的文件夹。该手册页支持我:

[...]如果简单的单一提交足以解决您的问题,请避免使用[此命令]


11
2017-07-25 10:01



我喜欢那张图的风格。请问您使用的是什么工具? - Slipp D. Thompson
适用于Mac的塔。我很喜欢。几乎值得切换到Mac本身。 - MM.
是的,虽然在我的情况下,我的子文件夹 targetdir 曾经 改名 在某些时候和 git filter-branch 简单地称它为一天,删除重命名之前的所有提交!令人震惊的是,考虑到Git在跟踪这些事情以及甚至是单个内容块的迁移方面有多么娴熟! - Jay Allen
哦,如果有人发现自己在同一条船上,这就是我使用的命令。别忘了 git rm 需要多个args,所以没有理由为每个文件/文件夹运行它: BYEBYE="dir/subdir2 dir2 file1 dir/file2"; git filter-branch -f --index-filter "git rm -q -r -f --cached --ignore-unmatch $BYEBYE" --prune-empty -- --all - Jay Allen


要添加到 保罗的回答,我发现要最终恢复空间,我必须将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

7
2017-11-12 13:22