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

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

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

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

XYZ/
    .git/
    XY1/
    ABC/
    XY2/

但我想改为:

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

当前回答

原始问题希望XYZ/ABC/(*文件)变为ABC/ABC/“*文件”。在为我自己的代码实现了公认的答案后,我注意到它实际上将XYZ/ABC/(*文件)更改为ABC/(*)文件。过滤器分支手册页甚至说,

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

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

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

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

其他回答

原始问题希望XYZ/ABC/(*文件)变为ABC/ABC/“*文件”。在为我自己的代码实现了公认的答案后,我注意到它实际上将XYZ/ABC/(*文件)更改为ABC/(*)文件。过滤器分支手册页甚至说,

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

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

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

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

在垃圾收集之前,您可能需要像“gitreflog-expire--expire=now--all”这样的内容来实际清理文件。gitfilter分支只删除历史记录中的引用,但不删除保存数据的reflog条目。当然,先测试一下。

尽管我的初始条件有所不同,但我的磁盘使用量在这样做时大幅下降。也许--子目录过滤器否定了这种需要,但我对此表示怀疑。

为了补充Paul的答案,我发现为了最终恢复空间,我必须将HEAD推到一个干净的存储库中,这样可以缩小.git/objects/pack目录的大小。

i.e.

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

在gc修剪之后,还要执行以下操作:

$ git push ...ABC.git HEAD

那你就可以了

$ git clone ...ABC.git

并且减小了ABC/.git的大小

实际上,推送清理存储库不需要一些耗时的步骤(例如gitgc),例如:

$ git clone --no-hardlinks /XYZ /ABC
$ git filter-branch --subdirectory-filter ABC HEAD
$ git reset --hard
$ git push ...ABC.git HEAD

我推荐GitHub将子文件夹拆分为新存储库的指南。步骤与保罗的答案相似,但我发现他们的指示更容易理解。

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


将子文件夹拆分到新存储库中打开Git Bash。将当前工作目录更改为要创建新存储库的位置。克隆包含子文件夹的存储库。git克隆OLD-REPOSTORY-FOLDER NEW-RPOSITORY-FOLDER将当前工作目录更改为克隆的存储库。cd REPOSTORY-NAME(维修人员姓名)要从存储库中的其余文件中筛选出子文件夹,请运行gitfilter branch,提供以下信息:FOLDER-NAME:项目中要从中创建单独存储库的文件夹。提示:Windows用户应使用/分隔文件夹。BRANCH-NAME:当前项目的默认分支,例如master或gh页面。gitfilter branch—修剪空—子目录筛选器FOLDER-NAME branch-NAME#筛选目录中的指定分支并删除空提交重写48dc599c80e20527ed902928085e7861e6b3cbe6(89/89)Ref“refs/heads/BRANCH-NAME”被重写

当使用更新版本的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)