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

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

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

我想我可以制作一个克隆并删除每个克隆中不需要的部分,但是我想这会在检出较旧的修订版时为我提供完整的树。这可能是可以接受的,但我希望能够假装两个存储库没有共享的历史记录。

为了清楚起见,我具有以下结构:

XYZ/
    .git/
    XY1/
    ABC/
    XY2/

但是我想这样:

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

答案

Easy Way™

事实证明,这是一种通用且有用的做法,使得 git 的霸主确实很容易,但是您必须拥有 git 的较新版本(> = 2012 年 5 月 1.7.11)。请参阅附录以了解如何安装最新的 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>

这是我要在 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” 只是一个约定,不是命令的一部分 - 您可以将其称为 “remote-server” 或任何您喜欢的东西)

git remote add origin git@github.com:node-browser-compat/btoa.git
git push origin -u master

愉快的一天!

注意:如果使用README.md.gitignoreLICENSE创建了一个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 的 brew:

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然后,您将返回历史中的所有内容。

因此,如果您想从 “来源” 中删除历史记录(即从 github,bitbucket 等中删除历史记录),则需要删除该存储库并重新推送该存储库的修剪后的副本。但是,等等 - 还有更多 ! - 如果您确实担心要删除密码或类似的东西,则需要修剪备份(请参见下文)。

使.git变小

前面提到的 delete history 命令仍然留下了许多备份文件 - 因为 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

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

信用

更新 :这个过程非常普遍,以至于 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所有标签的WARNING: Ref 'refs/tags/v0.1' is unchanged (因为它们都与子项目无关)。此外,删除此类标签后,将回收更多空间。显然git filter-branch应该能够重写其他标签,但是我无法验证这一点。如果要删除所有标签,请使用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. 然后删除备份引用日志,以便可以真正回收空间(尽管现在该操作具有破坏性)

    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 所发现的,这将使项目分支和标签保留在新仓库中。

编辑:合并了以下注释中的各种建议,以确保例如存储库实际上已缩小(以前并不总是这样)。

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

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

当然,首先要在 “clone --no-hardlinks” 存储库中对其进行测试,然后使用保罗列出的 reset,gc 和 prune 命令对其进行跟踪。

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

  1. 做克隆和过滤器:

    git clone --no-hardlinks foo bar; cd bar
    git filter-branch --subdirectory-filter subdir/you/want
  2. 删除所有对旧历史的引用。 “原始” 记录了您的克隆,“原始” 记录是过滤分支保存旧内容的地方:

    git remote rm origin
    git update-ref -d refs/original/refs/heads/master
    git reflog expire --expire=now --all
  3. 即使是现在,您的历史记录也可能停留在 fsck 不会触及的 packfile 中。将其撕成碎片,创建一个新的 packfile 并删除未使用的对象:

    git repack -ad

filter-branch 手册 中对此一个解释

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

这不再那么复杂,您只需在回购的克隆上使用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 。参见此处: 将子目录分离(移动)到单独的 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/

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

最初的问题是希望 XYZ / ABC /(* files)成为 ABC / ABC /(* files)。在为我自己的代码实现可接受的答案之后,我注意到它实际上将 XYZ / ABC /(* files)更改为 ABC /(* files)。过滤分支的手册页甚至说:

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

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

过滤器分支后我失去了连续性

然后,我对这个问题的答案是制作 2 个存储库副本,并手动删除要保留在每个存储库中的文件夹。手册页对此提供了支持:

[...] 如果只需要一次简单的提交就可以解决您的问题,请避免使用 [此命令]

为了补充Paul 的答案 ,我发现要最终恢复空间,我必须将 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