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

Posted

技术标签:

【中文标题】将子目录分离(移动)到单独的 Git 存储库中【英文标题】:Detach (move) subdirectory into separate Git repository 【发布时间】:2022-01-22 10:50:00 【问题描述】:

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

如何在保留子目录中的文件历史记录的同时做到这一点?

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

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

XYZ/
    .git/
    XY1/
    ABC/
    XY2/

但我想要这个:

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

【问题讨论】:

这对git filter-branch来说现在是微不足道的,请参阅下面的答案。 @jeremyjjbrown 是对的。这不再难做到,但很难在 Google 上找到正确答案,因为所有旧答案都在结果中占主导地位。 不鼓励使用git filter-branch。见warning in docs。 【参考方案1】:

The Easy Way™

事实证明,这是一种非常普遍和有用的做法,Git 的霸主让它变得非常简单,但你必须拥有更新版本的 Git (>= 1.7.11 May 2012)。请参阅附录了解如何安装最新的 Git。此外,下面的演练中有一个真实示例

    准备旧的仓库

     cd <big-repo>
     git subtree split -P <name-of-folder> -b <name-of-new-branch>
    

注意:&lt;name-of-folder&gt; 不得包含前导或尾随字符。例如,名为subproject 的文件夹必须作为subproject 传递,而不是./subproject/

Windows 用户注意事项:当您的文件夹深度大于 1 时,&lt;name-of-folder&gt; 必须具有 *nix 样式的文件夹分隔符 (/)。例如,名为path1\path2\subproject 的文件夹必须作为path1/path2/subproject 传递

    创建新的仓库

     mkdir ~/<new-repo> && cd ~/<new-repo>
     git init
     git pull </path/to/big-repo> <name-of-new-branch>
    

    将新的 repo 链接到 GitHub 或任何地方

     git remote add origin <git@github.com:user/new-repo.git>
     git push -u origin master
    

    清理&lt;big-repo&gt; 内部,如果需要

     git rm -rf <name-of-folder>
    

注意:这会将所有历史引用留在存储库中。如果您确实担心已提交密码或需要减小.git 文件夹的文件大小,请参阅下面的附录


演练

这些是与上述相同的步骤,但遵循我的存储库的确切步骤,而不是使用&lt;meta-named-things&gt;

这是我在节点中实现 javascript 浏览器模块的一个项目:

tree ~/node-browser-compat

node-browser-compat
├── ArrayBuffer
├── Audio
├── Blob
├── FormData
├── atob
├── btoa
├── location
└── navigator

我想将单个文件夹 btoa 拆分为单独的 Git 存储库

cd ~/node-browser-compat/
git subtree split -P btoa -b btoa-only

我现在有一个新分支 btoa-only,它只有 btoa 的提交,我想创建一个新的存储库。

mkdir ~/btoa/ && cd ~/btoa/
git init
git pull ~/node-browser-compat btoa-only

接下来,我在 GitHub 或 Bitbucket 或其他任何地方创建一个新的 repo,并将其添加为 origin

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

快乐的一天!

注意:如果您使用 README.md.gitignoreLICENSE 创建了一个 repo,则需要先拉取:

git pull origin master
git push origin master

最后,我想从更大的仓库中删除该文件夹

git rm -rf btoa

附录

macOS 上的最新 Git

使用Homebrew获取最新版本的Git:

brew install git

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

感谢来自 cmets 的rui.araujo。

清除您的历史记录

默认情况下,从 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 非常友好地帮助您避免意外破坏您的存储库。它最终会在几天和几个月内删除孤立的文件,但它会将它们保留一段时间,以防您意识到您不小心删除了您不想删除的内容。

因此,如果您真的想清空垃圾以立即减小 repo 的克隆大小,您必须做所有这些非常奇怪的事情:

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

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

信用

http://psionides.eu/2010/02/04/sharing-code-between-projects-with-git-subtree/ Remove a directory permanently from git http://blogs.atlassian.com/2013/05/alternatives-to-git-submodule-git-subtree/ How to remove unreferenced blobs from my git repo

【讨论】:

git subtree 仍然是“contrib”文件夹的一部分,并且默认情况下并未安装在所有发行版上。 github.com/git/git/blob/master/contrib/subtree @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 上激活 如果您将密码推送到公共存储库,您应该更改密码,而不是尝试将其从公共存储库中删除并希望没有人看到它。 此解决方案不保留历史记录。 popdpushd 命令使这变得相当隐含,更难理解它打算做什么......【参考方案2】:

更新:这个过程非常普遍,以至于 git 团队使用新工具 git subtree 使其变得更加简单。见这里:Detach (move) subdirectory into separate Git repository


您想要克隆您的存储库,然后使用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。是的,这真的是 --space-- all。这需要是命令的最后一个参数。正如 Matli 发现的那样,这会使项目分支和标签包含在新的 repo 中。

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

【讨论】:

为什么需要--no-hardlinks?删除一个硬链接不会影响另一个文件。 Git 对象也是不可变的。仅当您更改所有者/文件权限时,您才需要 --no-hardlinks 如果您想重写标签以不引用旧结构,请添加--tag-name-filter cat 和 Paul 一样,我不想在我的新仓库中使用项目标签,所以我没有使用 -- --all。我还在git filter-branch 命令之前运行了git remote rm origingit tag -l | xargs git tag -d。这将我的 .git 目录从 60M 缩小到 ~300K。请注意,我需要运行这两个命令才能减小大小。 git 手册页推荐,而不是rm -rf .git/refs/original/git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d;我猜如果 refs 没有存储在正确的位置,后者会更加健壮。此外,我相信还需要“git remote rm origin”来缩小 repo,否则来自 origin 的 refs 将保留引用的对象。 @jonp,我认为这是你的问题。最后,要重写其他分支,必须在克隆后使用git branch 手动设置它们,-- --all 并删除HEAD(这会停止重写其他分支)。 这不是创建 ABC/ 而不是 ABC/ABC/ 吗?【参考方案3】:

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

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

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

【讨论】:

使 git filter-branch --index-filter "git rm -r -f --cached --ignore-unmatch ABC" --prune-empty HEAD 变得更快。 index-filter 对索引起作用,而 tree-filter 必须检查并暂存每次提交的所有内容 在某些情况下,弄乱存储库 XYZ 的历史是矫枉过正的......只是一个简单的“rm -rf ABC; git rm -r ABC; git commit -m'extracted ABC into its own repo' " 对大多数人来说会更好。 如果您多次执行此命令,您可能希望在此命令上使用 -f(强制),例如,在分离两个目录后删除它们。否则,您将收到“无法创建新备份”。 如果您正在使用--index-filter 方法,您可能还希望使用git rm -q -r -f,这样每次调用都不会为它删除的每个文件打印一行。 我建议编辑保罗的答案,只是因为保罗的回答非常彻底。【参考方案4】:

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

    进行克隆和过滤:

    git clone --no-hardlinks foo bar; cd bar
    git filter-branch --subdirectory-filter subdir/you/want
    

    删除所有对旧历史的引用。 “origin” 跟踪您的克隆,“original” 是 filter-branch 保存旧内容的位置:

    git remote rm origin
    git update-ref -d refs/original/refs/heads/master
    git reflog expire --expire=now --all
    

    即使是现在,您的历史记录也可能被困在 fsck 不会触及的包文件中。将其撕成碎片,创建一个新的包文件并删除未使用的对象:

    git repack -ad
    

manual for filter-branch中有an explanation of this。

【讨论】:

我认为像git gc --aggressive --prune=now 这样的想法仍然不见了,不是吗? @Albert repack 命令会解决这个问题,不会有任何松散的对象。 是的,git gc --aggressive --prune=now 减少了很多新的回购 简洁优雅。谢谢! 在这一切之后,我仍然遇到与以前相同的错误。致命:打包对象 xxxxxx(存储在 .git/objects/pack/pack-yyyyyyyy.pack 中)已损坏【参考方案5】:

编辑:添加了 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/$3Clone
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

【讨论】:

【参考方案6】:

这不再那么复杂,您只需在您的 repo 的克隆上使用 git filter-branch 命令来剔除您不想要的子目录,然后推送到新的远程。

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

【讨论】:

这就像一个魅力。上例中的 YOUR_SUBDIR 是你要保留的子目录,其他的都会被删除 根据您的评论更新。 这没有回答问题。从文档中可以看到The result will contain that directory (and only that) as its project root.,实际上这就是您将得到的,即原始项目结构没有被保留。 @NicBright 您能否在问题中说明您对 XYZ 和 ABC 的问题,以说明问题所在? @jeremyjjbrown 是否可以重用克隆的仓库而不使用新的仓库,即我的问题***.com/questions/49269602/…【参考方案7】:

当使用更新版本的git2.22+ 可能?)运行git filter-branch 时,它说使用这个新工具git-filter-repo。这个工具无疑为我简化了事情。

使用 filter-repo 过滤

从原始问题创建 XYZ 存储库的命令:

# create local clone of original repo in directory XYZ
tmp $ git clone git@github.com:user/original.git XYZ

# switch to working in XYZ
tmp $ cd XYZ

# keep subdirectories XY1 and XY2 (dropping ABC)
XYZ $ git filter-repo --path XY1 --path XY2

# note: original remote origin was dropped
# (protecting against accidental pushes overwriting original repo data)

# XYZ $ ls -1
# XY1
# XY2

# XYZ $ git log --oneline
# last commit modifying ./XY1 or ./XY2
# first commit modifying ./XY1 or ./XY2

# point at new hosted, dedicated repo
XYZ $ git remote add origin git@github.com:user/XYZ.git

# push (and track) remote master
XYZ $ git push -u origin master

假设: * 远程 XYZ 仓库在推送前是新的并且是空的

过滤和移动

就我而言,我还想移动几个目录以获得更一致的结构。最初,我运行了简单的filter-repo 命令,然后运行git mv dir-to-rename,但我发现使用--path-rename 选项可以获得稍微“更好”的历史记录。我现在看到 last year(在 GitHub UI 中),而不是在新仓库中看到最后修改的 5 hours ago,它与原始仓库中的修改时间相匹配。

而不是...

git filter-repo --path XY1 --path XY2 --path inconsistent
git mv inconsistent XY3  # which updates last modification time

我最终跑了……

git filter-repo --path XY1 --path XY2 --path inconsistent --path-rename inconsistent:XY3
笔记: 我认为Git Rev News blog post 很好地解释了创建另一个回购过滤工具背后的原因。 我最初尝试了在原始存储库中创建与目标存储库名称匹配的子目录的路径并然后过滤(使用git filter-repo --subdirectory-filter dir-matching-new-repo-name)。该命令正确地将该子目录转换为复制的本地 repo 的根目录,但它也导致仅创建子目录所需的三个提交的历史记录。 (我没有意识到 --path 可以被指定多次;因此,无需在源代码库中创建子目录。)因为当我注意到我没有做到时,有人已经提交了源代码库继承历史,我只是在clone命令之后使用git reset commit-before-subdir-move --hard,并在filter-repo命令中添加--force,让它在稍微修改的本地克隆上运行。
git clone ...
git reset HEAD~7 --hard      # roll back before mistake
git filter-repo ... --force  # tell filter-repo the alterations are expected
由于我不知道git 的扩展模式,我在安装过程中遇到了困难,但最终我克隆了git-filter-repo 并将其符号链接到$(git --exec-path)
ln -s ~/github/newren/git-filter-repo/git-filter-repo $(git --exec-path)

【讨论】:

赞成推荐新的 filter-repo 工具(我上个月在 ***.com/a/58251653/6309 中介绍过) 此时使用git-filter-repo 绝对是首选方法。它比git-filter-branch 快得多、安全得多,并且可以防止在重写一个人的 git 历史时可能遇到的许多问题。希望这个答案能引起更多关注,因为它是针对 git-filter-repo 的答案。 acutally 我目前正在尝试让事情与 git filter-repo 一起工作,但不幸的是,在运行它之后,我丢失了文件,这些文件是在提交中添加的,其中包含被过滤器删除的路径-回购。例如:Foo/ Foo.cs Bar/ Bar.cs 所有都被添加到同一个提交中。我想将 Foo 和 Bar 移动到单独的存储库中。所以我将我的 repo 克隆到一个与新 repo 名称匹配的文件夹中,并且 git filter-repo -path Foo Foo 也被删除了。我说的是一个更大的存储库,它适用于所有其他文件,但如果它是这样的星座,则不是。 如果文件以前被移动/重命名,这不会自动保留移动/重命名之前的历史记录。但是,如果您在命令中包含原始路径/文件名,则不会删除该历史记录。例如,git filter-repo --path CurrentPathAfterRename --path OldPathBeforeRenamegit filter-repo --analyze 生成一个文件 renames.txt 可以帮助确定这些。或者,您可能会发现 script like this 有帮助。【参考方案8】:

更新:git-subtree 模块非常有用,以至于 git 团队将其拉入核心并使其成为 git subtree。见这里:Detach (move) subdirectory into separate Git repository

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/

【讨论】:

git-subtree 现在是 Git 的一部分,虽然它在 contrib 树中,所以并不总是默认安装。我知道它是由 Homebrew git 公式安装的,但没有它的手册页。因此,apenwarr 称他的版本已过时。【参考方案9】:

这里是对CoolAJ86 的"The Easy Way™" answer 的小修改,以便将多个子文件夹(比如说sub1sub2)拆分到一个新的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
    

    注意:&lt;name-of-folder&gt; 不得包含前导或尾随字符。例如,名为subproject 的文件夹必须作为subproject 传递,而不是./subproject/

    windows 用户注意事项:当您的文件夹深度大于 1 时,&lt;name-of-folder&gt; 必须具有 *nix 样式的文件夹分隔符 (/)。例如,名为path1\path2\subproject 的文件夹必须作为path1/path2/subproject 传递。此外,不要使用mvcommand,而是使用move

    最后说明:与基本答案的独特而巨大的区别是脚本的第二行“git filter-branch...

    创建新的仓库

    mkdir <new-repo>
    pushd <new-repo>
    
    git init
    git pull </path/to/big-repo> <name-of-new-branch>
    

    将新的 repo 链接到 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 文件夹的文件大小。

【讨论】:

这对我有用,只需稍作修改。因为我的sub1sub2 文件夹在初始版本中不存在,所以我不得不修改我的--tree-filter 脚本如下:"mkdir &lt;name-of-folder&gt;; if [ -d sub1 ]; then mv &lt;sub1&gt; &lt;name-of-folder&gt;/; fi"。对于第二个 filter-branch 命令,我将 替换为 ,省略了 的创建,并在 filter-branch 之后包含 -f 以覆盖现有备份的警告。 如果任何子目录在 git 的历史记录期间发生了变化,这将不起作用。如何解决? @nietras 看到 rogerdpack 的回答。在阅读并吸收了这些其他答案中的所有信息后,我花了一段时间才找到它。【参考方案10】:

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

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

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

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

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

【讨论】:

我喜欢那个图表的风格。请问你用的是什么工具? Mac 的塔。我很喜欢。换用 Mac 本身几乎是值得的。 是的,尽管在我的情况下,我的子文件夹 targetdir 在某个时候被重命名git filter-branch 只是简单地称之为一天,删除之前所做的所有提交改名!令人震惊的是,考虑到 Git 在跟踪此类事情甚至迁移单个内容块方面有多熟练! 哦,另外,如果有人发现自己在同一条船上,这是我使用的命令。不要忘记git rm 需要多个参数,因此没有理由为每个文件/文件夹运行它: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【参考方案11】:

要添加到Paul's answer,我发现要最终恢复空间,我必须将 HEAD 推送到一个干净的存储库,并减少 .git/objects/pack 目录的大小。

$ mkdir ...ABC.git $ cd ...ABC.git $ git init --bare

gc prune 之后,还要做:

$ 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

【讨论】:

【参考方案12】:

现在正确的方法如下:

git filter-branch --prune-empty --subdirectory-filter FOLDER_NAME [first_branch] [another_branch]

GitHub 现在甚至有small article 关于此类案例。

但请务必先将原始存储库克隆到单独的目录(因为它会删除所有文件和其他目录,您可能需要使用它们)。

所以你的算法应该是:

    将您的远程仓库克隆到另一个目录 使用git filter-branch 只在某个子目录下留下文件,推送到新的远程 创建提交以从原始远程存储库中删除此子目录

【讨论】:

【参考方案13】:

我推荐GitHub's guide to splitting subfolders into a new repository。步骤类似于Paul's answer,但我发现他们的说明更容易理解。

我已修改说明,使其适用于本地存储库,而不是托管在 GitHub 上的存储库。


Splitting a subfolder out into a new repository

    打开 Git Bash。

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

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

git clone OLD-REPOSITORY-FOLDER NEW-REPOSITORY-FOLDER
    将当前工作目录更改为您的克隆存储库。

cd REPOSITORY-NAME
    要从存储库中的其余文件中过滤掉子文件夹,请运行 git filter-branch,并提供以下信息: FOLDER-NAME:项目中您希望从中创建单独存储库的文件夹。 提示: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

【讨论】:

不错的帖子,但我注意到您链接的文档的第一段说 If you create a new clone of the repository, you won't lose any of your Git history or changes when you split a folder into a separate repository. 但是根据 cmets 对这里所有答案的所有答案 filter-branchsubtree 脚本导致丢失重命名子目录的历史记录。有什么办法可以解决这个问题? 找到了保留所有提交的解决方案,包括之前的目录重命名/移动——这是 rogerdpack 对这个问题的回答。 唯一的问题是我不能再使用克隆的repo了【参考方案14】:

看来这里的大多数(全部?)答案都依赖于某种形式的git filter-branch --subdirectory-filter 及其同类。这可能在“大多数情况下”有效,但在某些情况下,例如您重命名文件夹的情况,例如:

 ABC/
    /move_this_dir # did some work here, then renamed it to

ABC/
    /move_this_dir_renamed

如果您使用普通的 git 过滤器样式来提取“move_this_dir_renamed”,您将丢失最初为“move_this_dir”时发生的文件更改历史记录 (ref)。

因此看来,真正保留 all 更改历史记录的唯一方法(如果您的情况是这样的话)本质上是复制存储库(创建一个新的存储库,设置成为原点),然后核对其他所有内容并将子目录重命名为父目录,如下所示:

    在本地克隆多模块项目 分支机构 - 检查那里有什么:git branch -a 检查要包含在拆分中的每个分支,以在您的工作站上获取本地副本:git checkout --track origin/branchABC 在新目录中制作副本:cp -r oldmultimod simple 进入新项目副本:cd simple 去掉此项目中不需要的其他模块: git rm otherModule1 other2 other3 现在只剩下目标模块的子目录了 去掉模块子目录,让模块根目录成为新的项目根目录 git mv moduleSubdir1/* . 删除relic子目录: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

这遵循the github doc "Splitting a subfolder out into a new repository" 步骤 6-11 将模块推送到新的存储库。

这不会为您节省 .git 文件夹中的任何空间,但它会保留这些文件的所有更改历史记录,即使在重命名时也是如此。如果没有“很多”历史丢失等,这可能不值得。但至少可以保证您不会丢失较旧的提交!

【讨论】:

大海捞针!现在我可以保留 ALL 我的提交历史记录了。【参考方案15】:

我确实遇到了这个问题,但是所有基于 git filter-branch 的标准解决方案都非常慢。如果您有一个小型存储库,那么这可能不是问题,它适合我。我编写了另一个基于 libgit2 的 git 过滤程序,它首先为主存储库的每个过滤创建分支,然后将它们推送到清理存储库作为下一步。在我的存储库(500Mb 100000 次提交)上,标准的 git filter-branch 方法需要几天时间。我的程序需要几分钟来执行相同的过滤。

它有一个神话般的名字 git_filter 并住在这里:

https://github.com/slobobaby/git_filter

在 GitHub 上。

我希望它对某人有用。

【讨论】:

【参考方案16】:

使用此过滤器命令删除子目录,同时保留您的标签和分支:

git filter-branch --index-filter \
"git rm -r -f --cached --ignore-unmatch DIR" --prune-empty \
--tag-name-filter cat -- --all

【讨论】:

这里的猫是什么?【参考方案17】:

对于它的价值,这里是在 Windows 机器上使用 GitHub 的方法。假设您有一个位于C:\dir1 的克隆存储库。目录结构如下所示:C:\dir1\dir2\dir3dir3 目录是我想成为一个新的独立仓库的目录。

Github:

    创建您的新存储库:MyTeam/mynewrepo

Bash 提示:

    $ 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.gitgit remote add origin etc。没用,返回“remote origin already exists

    $ git push --progress some_name master

【讨论】:

【参考方案18】:

当我 mentioned above 时,我不得不使用反向解决方案(删除所有未触及我的 dir/subdir/targetdir 的提交),这似乎可以很好地删除大约 95% 的提交(根据需要)。但是,还有两个小问题。

FIRSTfilter-branch 在删除引入或修改代码的提交方面做得非常出色,但显然,合并提交位于其 Gitiverse 的位置之下。

Screenshot: Merge Madness!

这是一个我可能可以忍受的外观问题(他说...慢慢后退,眼睛移开)

SECOND 剩下的几个提交几乎是 ALL 重复的!我似乎获得了第二条冗余的时间线,它几乎涵盖了该项目的整个历史。有趣的是(你可以从下图看到),我的三个本地分支并不都在同一个时间线上(这当然是它存在的原因,而且不仅仅是垃圾收集)。

Screnshot: Double-double, Git filter-branch style

我唯一能想到的是,其中一个被删除的提交可能是 filter-branch实际上删除的单个合并提交,它创建了每个现在未合并的并行时间线strand 获取了自己的提交副本。 (耸耸肩我的 TARDiS 在哪里?)我很确定我可以解决这个问题,尽管我真的很想了解它是如何发生的。

在疯狂的 mergefest-O-RAMA 的情况下,我可能会不理会它,因为它已经在我的提交历史中牢牢占据了地位——每当我靠近时都会威胁我——它似乎并没有实际上会导致任何非外观问题,因为它在 Tower.app 中非常漂亮。

【讨论】:

【参考方案19】:

更简单的方法

    安装git splits。我基于jkeating's solution 将它创建为一个 git 扩展。

    将目录拆分为本地分支 #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

    在某处创建一个空仓库。我们假设我们在 GitHub 上创建了一个名为 xyz 的空仓库,其路径为: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

【讨论】:

与“简单方法”相比,这种方法的一个优势是远程已经为新的 repo 设置好了,所以你可以立即添加子树。事实上,这种方式对我来说似乎更容易(即使没有git splits 向 AndrewD 推荐以发布此解决方案。如果这对其他人有用的话,我已经分叉了他的 repo 以使其在 OSX (github.com/ricardoespsanto/git-splits) 上运行【参考方案20】:

在垃圾收集之前,您可能需要“git reflog expire --expire=now --all”之类的东西来实际清除文件。 git filter-branch 仅删除历史记录中的引用,但不会删除包含数据的 reflog 条目。当然,先测试一下。

在执行此操作时,我的磁盘使用量急剧下降,尽管我的初始条件有些不同。也许 --subdirectory-filter 否定了这种需要,但我对此表示怀疑。

【讨论】:

【参考方案21】:

在https://github.com/vangorra/git_split查看 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.

【讨论】:

【参考方案22】:

把它放到你的 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'

【讨论】:

【参考方案23】:

我确信 git subtree 一切都很好,但是我想要移动的 git 托管代码的子目录都在 eclipse 中。 因此,如果您使用的是 egit,则非常容易。 获取您要移动的项目并组队->断开连接,然后组队->将其共享到新位置。它将默认尝试使用旧的 repo 位置,但您可以取消选中 use-existing 选择并选择新的位置来移动它。 万岁。

【讨论】:

子树的“精巧”部分是您的子目录的历史记录随顺。如果您不需要历史记录,那么您的痛苦简单的方法就是要走的路。【参考方案24】:

您可以轻松尝试https://help.github.com/enterprise/2.15/user/articles/splitting-a-subfolder-out-into-a-new-repository/

这对我有用。我在上面给出的步骤中遇到的问题是

    在这个命令中git filter-branch --prune-empty --subdirectory-filter FOLDER-NAME BRANCH-NAME BRANCH-NAMEma​​ster

    如果由于保护问题导致最后一步提交失败,请关注 - https://docs.gitlab.com/ee/user/project/protected_branches.html

【讨论】:

【参考方案25】:

我找到了非常直接的解决方案, 这个想法是复制存储库,然后删除不必要的部分。 它是这样工作的:

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 删除不必要的文件夹表单历史记录

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 并在第 3 步重命名 XYZ -> ABC

【讨论】:

近乎完美......但您忘记了“git filter-branch --prune-empty”来删除所有现在为空的旧提交。推送到原始主机之前要做的事情! 如果你犯了错误并且在删除旧的空提交后仍然想“repush”,执行:“git push -u origin master --force-with-lease”【参考方案26】:

发现这篇精彩的文章Original reference 易于理解。如果无法访问,请在此处记录。

1.准备当前存储库

$ cd path/to/repository
$ git subtree split -P my-folder -b my-folder
Created branch 'my-folder'
aecbdc3c8fe2932529658f5ed40d95c135352eff

文件夹的名称必须是相对路径,从存储库的根目录开始。

2。创建新存储库

$ cd my-folder
$ git init
Initialized empty Git repository in /Users/adamwest/Projects/learngit/shop/my-folder/.git/
$ git add .
$ git commit -m "initial commit"
[master (root-commit) 192c10b] initial commit
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 file

这里我们只需要 cd 到新文件夹,初始化新存储库,并提交任何内容。

3.添加新的远程仓库并推送

$ git remote add origin git@github.com:robertlyall/my-folder.git
$ git push origin -u master
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 199 bytes | 199.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To github.com:robertlyall/my-folder.git
 * [new branch]      master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.

我们从 GitHub 远程添加新的存储库,然后将我们的第一个提交推送到它。

4.从主存储库中删除文件夹并推送

$ cd ../
$ git rm -rf my-folder
rm 'my-folder/file'
$ git commit -m "Remove old folder"
[master 56aedbe] remove old folder
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 my-folder/file
$ git push
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Delta compression using up to 4 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 217 bytes | 217.00 KiB/s, done.
Total 2 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To github.com:robertlyall/shop.git
   74dd8b3..56aedbe  master -> master

最后,我们 cd 回到根目录,从我们的主存储库中删除该文件夹,然后提交并推送更改。 现在,我们的主存储库中有该文件夹,但链接到一个完全独立的存储库,可以跨多个项目重用。

【讨论】:

以上是关于将子目录分离(移动)到单独的 Git 存储库中的主要内容,如果未能解决你的问题,请参考以下文章

sh 将子目录从Git存储库分离为单独的存储库

将 git 存储库移动到 nfs 会导致权限错误

将 Mercurial 存储库移动到现有 git 存储库的子目录中

sh 将子目录从一个git存储库移动到另一个git存储库的子目录,而不会丢失提交历史记录。

如何在将一个巨大的存储库拆分为单独的存储库时将 SVN 转换为 GIT?

如何将分支内容移动到另一个存储库保留历史记录并避免复制原始存储库的完整历史记录?