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

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

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

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

XYZ/
    .git/
    XY1/
    ABC/
    XY2/

但我想改为:

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

当前回答

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

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--删除文件夹“{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

就是这样。您可以重复这些步骤来获取另一个存储库,

只需在步骤3中删除XY1、XY2并重命名XYZ->ABC

其他回答

更新:这个过程非常常见,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所发现的,这会将项目分支和标记保留在新的回购中。

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

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

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

发现这篇精彩的文章原创参考很容易理解。记录在这里,以防无法访问。

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回到rooot目录,从主存储库中删除文件夹,然后提交并推送更改。现在,我们的主存储库中有一个文件夹,但它链接到一个完全独立的存储库,可以在多个项目中重用。

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