协慌网

登录 贡献 社区

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

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

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

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

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

XYZ/
    .git/
    XY1/
    ABC/
    XY2/

但我想这样:

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

答案

更新 :这个过程非常普遍,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 和 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 发现的那样,这使得项目分支和标签包含在新的回购中。

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

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 <[email protected]: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 [email protected]:node-browser-compat/btoa.git
git push origin -u master

快乐的一天!

注意:如果您使用README.md.gitignoreLICENSE创建了一个 repo,则需要先执行以下操作:

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

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

信用

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

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

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