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

我有一个Git仓库,其中包含一些子目录。 现在我发现其中一个子目录与另一个不相关,应该分离到一个单独的存储库。

我怎么能这样做,而保持在子目录中的文件的历史?

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

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

XYZ/ .git/ XY1/ ABC/ XY2/ 

但是我想代之以:

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

更新 :这个过程非常常见,git团队使用一个新的工具git subtree更简单。 看到这里: 分离(移动)到不同的Git仓库的子目录


你想克隆你的仓库,然后使用git filter-branch来标记所有东西,但是你想在你的新仓库中的子目录被垃圾收集。

  1. 克隆你的本地仓库:

     git clone /XYZ /ABC 

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

  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对所有标签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

  4. 然后使用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 
  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发现的,这样可以将项目分支和标签包含在新的回购中。

编辑:从下面的评论意见中的各种build议被合并,以确保,例如,存储库实际上收缩(这并不总是如此)。

简单的方法™

事实certificate,这是一个常见的,有用的做法,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. 创build新的回购

     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 ,我想创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.gitignoreLICENSE创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:

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历史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步骤之后做更多的工作。

  1. 做克隆和filter:

     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不会触及的包文件中。 撕碎它,创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/

为了将多个子文件夹 (比如说sub1sub2 )分割成一个新的git仓库,下面是对CoolAJ86的“The Easy Way™”答案的一个小修改。

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. 创build新的回购

     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文件夹的文件大小,请参阅原始答案中的附录

原来的问题是希望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应该是:

  1. 克隆你的远程回购到另一个目录
  2. 使用git filter-branch只留下一些子目录下的文件,推送到新的远程
  3. 创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一个新的回购,将其设置为原点),然后核对其他所有并将子目录重命名为父级,如下所示:

  1. 在本地克隆多模块项目
  2. 分支 – 检查有什么: git branch -a
  3. 做一个签出到每个分支被包括在拆分,以获得您的工作站上的本地副本: git checkout --track origin/branchABC
  4. 在新的目录下复制: cp -r oldmultimod simple
  5. 进入新的项目副本: cd simple
  6. 摆脱这个项目中不需要的其他模块:
  7. git rm otherModule1 other2 other3
  8. 现在只剩下目标模块的子目录
  9. 摆脱模块subdir,使模块根成为新的项目根
  10. git mv moduleSubdir1/* .
  11. 删除rmdir moduleSubdir1rmdir moduleSubdir1
  12. 在任何时候检查更改: git status
  13. 创build新的git仓库并复制它的URL来指向这个项目:
  14. git remote set-url origin http://mygithost:8080/git/our-splitted-module-repo
  15. validation这是好的: git remote -v
  16. 将更改推送到远程回购: git push
  17. 去远程回购,并检查它在那里
  18. 重复它所需的任何其他分支: git checkout branch2

这遵循github文档“将子文件夹拆分到新的存储库”步骤6-11,将模块推到新的仓库 。

这不会为您保存.git文件夹中的任何空间,但即使在重命名时,它也会保留这些文件的所有更改历史logging。 如果没有“很多”的历史遗失等,这可能不值得,但至less你保证不会失去旧的承诺!

对于什么是值得的,这里是如何在Windows机器上使用GitHub。 假设你有一个克隆的回购站在C:\dir1 。 目录结构如下所示: C:\dir1\dir2\dir3 。 目录dir3是我想成为一个新的单独的回购。

Github上:

  1. 创build您的新存储库: MyTeam/mynewrepo

Bash提示:

  1. $ cd c:/Dir1
  2. $ git filter-branch --prune-empty --subdirectory-filter dir2/dir3 HEAD
    返回: Ref 'refs/heads/master' was rewritten (fyi:dir2 / dir3区分大小写)

  3. $ git remote add some_name git@github.com:MyTeam/mynewrepo.git
    git remote add origin etc 。 没有工作,返回“ remote origin already exists

  4. $ 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否定了这种需要,但我怀疑它。

更简单的方法

  1. 安装git splits 。 我创build它作为git扩展,基于jkeating的解决scheme 。
  2. 拆分目录到本地分支#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

  3. 在某处创build一个空的回购。 我们假设我们在GitHub上创build了一个名为xyz的空回购,其path为: git@github.com:simpliwp/xyz.git

  4. 推到新的回购。 #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

  5. 将新创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上。


将一个子文件夹拆分成一个新的存储库

  1. 打开Git Bash。

  2. 将当前工作目录更改为您要创build新存储库的位置。

  3. 克隆包含子文件夹的存储库。

 git clone OLD-REPOSITORY-FOLDER NEW-REPOSITORY-FOLDER 
  1. 将当前工作目录更改为您的克隆存储库。
 cd REPOSITORY-NAME 
  1. 要从存储库中的其余文件中过滤掉子文件夹,请运行git filter-branch ,提供以下信息:
    • FOLDER-NAME :您希望从中创build单独存储库的项目文件夹。
      • 提示:Windows用户应该使用/来分隔文件夹。
    • BRANCH-NAME :您当前项目的默认分支,例如mastergh-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