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

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

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

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

XYZ/
    .git/
    XY1/
    ABC/
    XY2/

但我想改为:

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

当前回答

这里是对CoolAJ86的“简单方法”的一个小修改™回答,以便将多个子文件夹(假设sub1和sub2)拆分为一个新的git存储库。

简单的方法™ (多个子文件夹)

准备旧回购推送<大回购>gitfilter分支--树过滤器“mkdir<文件夹名称>;mv<sub1><sub2><文件夹名称>/”HEADgit子树拆分-P<文件夹名称>-b<新分支名称>邻苯二胺注意:<文件夹名称>不能包含前导或尾随字符。例如,名为subject的文件夹必须作为子项目传递,而不是/子项目/windows用户注意:当文件夹深度>1时,<文件夹名称>必须具有*nix样式的文件夹分隔符(/)。例如,名为path1\path2\subject的文件夹必须作为path1/path2/subject传递。此外,不要使用mvcommand,而是移动。最后一点:与基本答案的最大区别是脚本“gitfilter分支…”的第二行创建新回购mkdir<新回购>推送<新回购>初始化git pull</path/to/big repo><新分支的名称>将新回购链接到Github或任何地方git远程添加原点<git@github.com:我的用户/new repo.git>git推送原点-u主清理(如果需要)popd#退出<新回购>推送<大回购>gitrm-rf<文件夹名称>注意:这会将所有历史引用保留在存储库中。如果您确实担心提交了密码或需要减小.git文件夹的文件大小,请参阅原始答案中的附录。

其他回答

我确实遇到了这个问题,但所有基于gitfilter分支的标准解决方案都非常缓慢。如果你有一个小的存储库,那么这可能不是问题,这是我的问题。我基于libgit2编写了另一个git过滤程序,作为第一步,它为主存储库的每个过滤创建分支,然后作为下一步将这些分支推送到清理存储库。在我的存储库中(500Mb100000次提交),标准的gitfilter分支方法花费了几天时间。我的程序需要几分钟来完成相同的过滤。

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

https://github.com/slobobaby/git_filter

在GitHub上。

我希望它对某人有用。

当使用更新版本的git(2.22+可能?)运行gitfilter分支时,它表示要使用这个新工具gitfilter 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进行重命名,但我发现使用--path重命名选项可以获得稍微“更好”的历史记录。我去年(在GitHub UI中)看到的新回购中移动文件的修改时间与原始回购中的修改时间相匹配,而不是5小时前的最后一次修改。

而不是

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
Notes:

我认为Git Rev News博客文章很好地解释了创建另一个回购过滤工具的原因。我最初尝试的路径是在原始存储库中创建一个与目标回购名称匹配的子目录,然后进行过滤(使用gitfilter repo--匹配新回购名称的子目录过滤器dir)。该命令正确地将该子目录转换为复制的本地repo的根目录,但它也只生成了创建子目录所需的三次提交的历史记录。(我没有意识到--路径可以多次指定;因此,不需要在源repo中创建子目录。)由于在我注意到我未能继续执行历史记录时,有人已经提交了源repo,所以我只在subdir move之前使用了git reset commit,在clone命令之后使用了,并在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的扩展模式,我在安装过程中遇到了困难,但最终我克隆了gitfilter repo并将其符号链接到$(git-exec路径):

ln -s ~/github/newren/git-filter-repo/git-filter-repo $(git --exec-path)

Paul的回答创建了一个包含/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--修剪空--子目录筛选器FOLDER_NAME[first_branch][another_branch]

GitHub现在甚至有关于此类案例的小文章。

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

所以你的算法应该是:

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

使用此筛选器命令删除子目录,同时保留标记和分支:

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