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

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

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

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

XYZ/
    .git/
    XY1/
    ABC/
    XY2/

但我想改为:

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

当前回答

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

其他回答

我确信git子树很好,很好,但我想移动的git托管代码子目录都在eclipse中。所以,如果你使用egit,这非常容易。将要移动的项目与团队->断开连接,然后团队->将其共享到新位置。默认情况下,它将尝试使用旧的回购位置,但您可以取消选中使用现有选择并选择新位置来移动它。大家都来了。

更新:这个过程非常常见,git团队用一个新工具git子树简化了这个过程。请参阅此处:将子目录分离(移动)到单独的Git存储库中


您希望克隆存储库,然后使用gitfilter分支标记除要在新存储库中进行垃圾收集的子目录之外的所有内容。

要克隆本地存储库,请执行以下操作:gitclone/XYZ/ABC(注意:将使用硬链接克隆存储库,但这不是问题,因为硬链接文件本身不会被修改-将创建新的文件。)现在,让我们保留我们想要重写的有趣分支,然后删除源,以避免推到那里,并确保源不会引用旧的提交:cd/ABC对于分支1 br2 br3中的i;do git branch-t$i原点/$i;完成git远程rm源或对于所有远程分支:cd/ABC对于$中的i(git branch-r|sed“s/.*origin\///”);do git branch-t$i原点/$i;完成git远程rm源现在,您可能还想删除与子项目无关的标记;你也可以稍后再做,但你可能需要再次修剪你的repo。我没有这样做,并得到一个警告:Ref‘refs/tags/v0.1‘对于所有标签都是不变的(因为它们都与子项目无关);此外,在移除这些标签之后,将回收更多的空间。显然,gitfilter分支应该能够重写其他标记,但我无法验证这一点。如果要删除所有标记,请使用git-tag-l|xargs-git-tag-d。然后使用过滤器分支和重置来排除其他文件,以便可以对它们进行修剪。还让我们添加--tag-namefilter-cat--pruneempty以删除空提交并重写标记(注意,这将不得不去掉它们的签名):gitfilter branch--标记名filter cat--修剪空--子目录筛选器ABC----全部或者替代地,只重写HEAD分支并忽略标记和其他分支:gitfilter branch--标记名filter cat--修剪空--子目录筛选器ABC HEAD然后删除备份回流,以便真正回收空间(尽管现在操作是破坏性的)git重置--硬git for each ref--format=“%(refname)”refs/original/| xargs-n 1 git update ref-dgit reflog expire--expire=现在--全部git-gc--aggressive--prune=现在现在您有了ABC子目录的本地git存储库,并保留了其所有历史记录。

注意:对于大多数用途,gitfilter分支确实应该添加参数--all。是的,那真的是——空间——全部。这需要是命令的最后一个参数。正如Matli所发现的,这会将项目分支和标记保留在新的回购中。

编辑:以下评论中的各种建议都被纳入其中,以确保存储库实际上已经缩小(以前并非如此)。

如上所述,我必须使用相反的解决方案(删除所有提交而不触及我的dir/subdr/targetdir),这似乎可以很好地去除大约95%的提交(根据需要)。然而,还有两个小问题。

首先,过滤器分支完成了一项出色的工作,删除了引入或修改代码的提交,但显然,合并提交在Gitiverse的站点之下。

截图:合并疯狂!

这是一个我可能可以忍受的美容问题(他说……慢慢后退,眼睛转向)。

第二,剩下的几个提交几乎都是重复的!我似乎获得了第二个多余的时间线,它几乎涵盖了整个项目的历史。有趣的是(你可以从下面的图片中看到),我的三个本地分支并不都在同一个时间线上(这就是为什么它存在,而不仅仅是垃圾收集)。

尖叫:双双,Git过滤器分支样式

我唯一能想到的是,其中一个被删除的提交可能是过滤器分支实际删除的单个合并提交,并且创建了并行时间线,因为每个现在未合并的链都有自己的提交副本。(耸耸肩,我的TARDiS在哪里?)我很确定我能解决这个问题,尽管我真的很想知道它是怎么发生的。

对于疯狂的mergefest-O-RAMA,我很可能会把它单独放在一边,因为它在我的承诺历史中根深蒂固,每当我走近时,它都会威胁我——它似乎并没有真正引起任何非外观问题,因为在Tower.app中它非常漂亮。

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

这里的大多数答案似乎都依赖于某种形式的gitfilter分支——子目录筛选器及其类似的分支。这可能在“大多数情况下”有效,但在某些情况下,例如重命名文件夹时,例如:

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

ABC/
    /move_this_dir_renamed

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

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

在本地克隆多模块项目分支-检查有什么:gitbranch-a对要包含在拆分中的每个分支进行签出,以在您的工作站上获得本地副本:gitcheckout--trackorigin/branchABC在新目录中创建副本:cp-r oldmultimodsimple进入新项目副本:cd simple删除此项目中不需要的其他模块:git rm other模块1 other2 other3现在只剩下目标模块的子磁盘删除模块子目录,使模块根目录成为新的项目根目录git-mv模块Subdir1/*。删除遗迹子目录:rmdir moduleSubdir1随时检查更改:git状态创建新的git repo并复制其URL以将此项目指向其中:git远程设置url源http://mygithost:8080/git/our-分裂模块回购验证这是否正确:gitremote-v将更改推送到远程存储库:git Push转到远程回购并检查所有内容对所需的任何其他分支重复此操作:git checkout branch2

接下来是github文档“将子文件夹拆分为新存储库”的步骤6-11,以将模块推送到新存储库。

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