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


1758

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

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

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

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

XYZ/
    .git/
    XY1/
    ABC/
    XY2/

但我想这样:

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

7
现在这很简单,git filter-branch请参阅下面的答案。
jeremyjjbrown 2014年

8
@jeremyjjbrown是正确的。这样做不再困难,但由于所有旧答案主导结果,因此很难在Google上找到正确答案。
Agnel Kurian 2014年

Answers:


1228

更新:此过程非常普遍,以至于git团队使用新工具简化了工作git subtree。参见此处:将子目录分离(移动)到单独的Git存储库中


您想要克隆存储库,然后使用git filter-branch它标记所有内容,但要在新存储库中垃圾存储的子目录除外。

  1. 克隆本地存储库:

    git clone /XYZ /ABC
    

    (注意:将使用硬链接克隆存储库,但这不是问题,因为硬链接文件本身不会被修改-会创建新文件。)

  2. 现在,让我们保留同样要重写的有趣分支,然后删除原点以避免将其压入并确保原点不会引用旧提交:

    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
    
  3. 现在,您可能还希望删除与子项目无关的标签。您也可以稍后再执行此操作,但是您可能需要再次修剪您的存储库。我没有这样做,并且WARNING: Ref 'refs/tags/v0.1' is unchanged对所有标签都使用了“ a ”(因为它们都与子项目无关);此外,删除此类标签后,将回收更多空间。显然git filter-branch应该能够重写其他标签,但是我无法验证这一点。如果要删除所有标签,请使用git tag -l | xargs git tag -d

  4. 然后使用filter-branch并重置以排除其他文件,以便可以对其进行修剪。我们还要添加--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
    
  5. 然后删除备份引用日志,以便可以真正回收空间(尽管现在该操作具有破坏性)

    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所发现的,这将使项目分支和标签保留在新仓库中。

编辑:合并了以下注释中的各种建议,以确保例如存储库实际上已缩小(以前并不总是这样)。


29
很好的答案。谢谢!为了真正得到我想要的,我在filter-branch命令中添加了“---all”。
matli

12
为什么需要--no-hardlinks?删除一个硬链接不会影响另一个文件。Git对象也是不可变的。仅当您要更改所有者/文件权限时,才需要--no-hardlinks
vdboor'2

67
我建议的另一个步骤是“ git remote rm origin”。如果我没有记错的话,这将阻止推送回到原始存储库。
汤姆(Tom)2010年

13
附加到的另一个命令filter-branch--prune-empty,以删除当前为空的提交。
塞斯·约翰逊

8
和Paul一样,我不想在新的仓库中使用项目标签,所以我没有使用-- --all。我也跑了git remote rm origingit tag -l | xargs git tag -dgit filter-branch命令之前。这使我的.git目录从60M 缩减到300K。请注意,我需要同时运行这两个命令以减小尺寸。
saltycrane

1321

Easy Way™

事实证明,这是一种通用且有用的做法,Git的霸主确实使这变得非常容易,但是您必须拥有更新版本的Git(> = 2012年5月1.7.11)。有关如何安装最新版本的Git的信息,请参阅附录。另外,下面的演练中有一个真实的示例

  1. 准备旧的仓库

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

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

    Windows用户注意事项:当文件夹深度> 1时,<name-of-folder>必须具有* nix样式文件夹分隔符(/)。例如,名为path1\path2\subprojectMUST 的文件夹必须以path1/path2/subproject

  2. 创建新的仓库

    mkdir ~/<new-repo> && cd ~/<new-repo>
    git init
    git pull </path/to/big-repo> <name-of-new-branch>
    
  3. 将新仓库链接到GitHub或任何地方

    git remote add origin <git@github.com:user/new-repo.git>
    git push -u origin master
    
  4. 如果需要<big-repo>可以在内部清理

    git rm -rf <name-of-folder>
    

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

...

演练

这些与上面的步骤相同,但是遵循了我对于存储库的确切步骤,而不是使用<meta-named-things>

这是我要在node中实现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上创建一个新的仓库,或将其添加为 origin

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

快乐的一天!

注意:如果你创建了一个带有回购协议README.md.gitignore并且LICENSE,你需要先拉:

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

感谢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

前面提到的delete history命令仍然留下了许多备份文件-因为Git太善于帮助您避免意外损坏您的存储库。它最终将在几天和几个月内删除孤立的文件,但是会保留一段时间,以防万一您意外删除了不想删除的文件。

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

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

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

信用


16
git subtree仍然是“ contrib”文件夹的一部分,并且默认情况下未在所有发行版中安装。 github.com/git/git/blob/master/contrib/subtree
onionjake 2013年

11
@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上激活
rui.araujo 2013年

41
如果您已将密码推送到公共存储库,则应更改密码,而不要尝试从公共存储库中删除密码,并希望没人能看到。
Miles Rout

8
此解决方案不会保留历史记录。
心教堂

18
popdpushd命令使这个隐而难神交什么打算做...
jones77

133

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

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

当然,首先要在“ clone --no-hardlinks”存储库中对其进行测试,然后使用保罗列出的reset,gc和prune命令对其进行跟踪。


53
作出这样的 git filter-branch --index-filter "git rm -r -f --cached --ignore-unmatch ABC" --prune-empty HEAD,这将是快。index-filter对索引起作用,而tree-filter必须为每个commit检出并暂存所有内容
fmarc

51
在某些情况下,弄乱存储库XYZ的历史记录是过大的事情……对于大多数人来说,简单的“ rm -rf ABC; git rm -r ABC; git commit -m'将ABC提取到自己的存储库中”会更好。
Evgeny

2
如果您多次执行此命令,则可能希望在该命令上使用-f(force),例如,将两个目录分开后将其删除。否则,您将收到“无法创建新备份”。
布莱恩·卡尔顿

4
如果您正在执行该--index-filter方法,则可能还需要创建该方法,以使git rm -q -r -f每次调用都不会为其删除的每个文件打印一行。
埃里克·内塞斯

1
我建议编辑保罗的答案,仅是因为保罗的答案如此详尽。
Erik Aronesty

96

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

  1. 做克隆和过滤器:

    git clone --no-hardlinks foo bar; cd bar
    git filter-branch --subdirectory-filter subdir/you/want
    
  2. 删除所有对旧历史的引用。“原始”记录了您的克隆,“原始”记录是过滤分支保存旧内容的地方:

    git remote rm origin
    git update-ref -d refs/original/refs/heads/master
    git reflog expire --expire=now --all
    
  3. 即使是现在,您的历史记录也可能停留在fsck不会触及的packfile中。将其撕成碎片,创建一个新的packfile并删除未使用的对象:

    git repack -ad
    

一个这样的解释手册过滤分支


3
我认为git gc --aggressive --prune=now仍然缺少某种想法,不是吗?
艾伯特

1
@Albert repack命令可以解决这个问题,并且不会有任何松散的对象。
Josh Lee

是的,git gc --aggressive --prune=now减少了很多新的回购交易
Tomek Wyderka

简洁大方。谢谢!
Marco Pelegrini

40

编辑:添加了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

26

这不再那么复杂,您只需在仓库的克隆上使用git filter-branch命令就可以删除不需要的子目录,然后推送到新的远程目录。

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

3
这就像一个魅力。上例中的YOUR_SUBDIR是您要保留的子目录,所有其他内容都将被删除
JT Taylor

1
根据您的评论进行更新。
jeremyjjbrown

2
这不能回答问题。它从文档中说出了The result will contain that directory (and only that) as its project root.,的确如此,这就是您将得到的,即,未保留原始项目结构。
NicBright

2
@NicBright您可以在问题中说明XYZ和ABC的问题,以说明问题所在吗?
亚当

@jeremyjjbrown是否可以重复使用克隆的存储库而不使用新的存储库,即我的问题在这里stackoverflow.com/questions/49269602/…–
Qiulang

19

更新:git-subtree模块非常有用,以至于git团队将其拉入了核心并制成了它git subtree。参见此处:将子目录分离(移动)到单独的Git存储库中

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/


1
git-subtree现在是Git的一部分,尽管它在contrib树中,所以默认情况下并不总是安装。我知道它是由Homebrew git公式安装的,但是没有手册页。因此,apenwarr称他的版本已过时。
echristopherson

19

这是对CoolAJ86“ The Easy Way™”答案的一个小修改,目的是将多个子文件夹(比如sub1sub2)拆分为一个新的git存储库。

Easy Way™(多个子文件夹)

  1. 准备旧的仓库

    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
    

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

    Windows用户注意事项:当文件夹深度> 1时,<name-of-folder>必须具有* nix样式的文件夹分隔符(/)。例如,名为的文件夹path1\path2\subproject必须以形式传递path1/path2/subproject。而且不要使用mv命令而是move

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

  2. 创建新的仓库

    mkdir <new-repo>
    pushd <new-repo>
    
    git init
    git pull </path/to/big-repo> <name-of-new-branch>
    
  3. 将新仓库链接到Github或任何地方

    git remote add origin <git@github.com:my-user/new-repo.git>
    git push origin -u master
    
  4. 清理(如果需要)

    popd # get out of <new-repo>
    pushd <big-repo>
    
    git rm -rf <name-of-folder>
    

    注意:这将所有历史记录保留在存储库中。如果您实际上担心已提交密码或需要减小文件夹的文件大小,请参阅原始答案中的附录.git


1
稍加修改,这对我有用。由于初始版本不存在我的sub1sub2文件夹,因此我必须--tree-filter按如下所示修改脚本"mkdir <name-of-folder>; if [ -d sub1 ]; then mv <sub1> <name-of-folder>/; fi"。对于第二个filter-branch命令,我用<sub2>替换了<sub1>,省略了<name-of-folder>的创建,并-f在之后包含filter-branch了该命令以覆盖现有备份的警告。
pglezen '16

如果在git的历史记录中任何子目录都发生了更改,则此方法将无效。如何解决呢?
nietras '16

@nietras看到rogerdpack的答案。在阅读并吸收了其他答案中的所有信息之后,花了我一段时间才能找到它。
亚当

12

最初的问题是希望XYZ / ABC /(* files)成为ABC / ABC /(* files)。在为我自己的代码实现可接受的答案后,我注意到它实际上将XYZ / ABC /(* files)更改为ABC /(* files)。过滤分支的手册页甚至说:

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

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

过滤器分支后我失去了连续性

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

[...]如果仅需一次简单的提交即可解决您的问题,请避免使用[此命令]


1
我喜欢该图的样式。请问您使用什么工具?
Slipp D. Thompson

3
Mac版塔。我很喜欢。本身就值得切换到Mac,这几乎是值得的。
MM。

2
是的,尽管在我的情况下,我的子文件夹在某个时候targetdir已经被重命名git filter-branch只是将其命名为一天,删除了重命名之前所做的所有提交!令人震惊的是,考虑到Git在跟踪此类事情甚至单个内容块的迁移方面多么熟练!
杰·艾伦

1
哦,同样,如果有人发现自己在同一条船上,这就是我使用的命令。别忘了它git rm需要多个arg,因此没有理由为每个文件/文件夹运行它: 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
Jay Allen

7

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

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

gc修剪后,还可以执行以下操作:

$ git push ... ABC.git头

那你可以做

$ git clone ... ABC.git

并减小ABC / .git的大小

实际上,推送到清理存储库不需要某些耗时的步骤(例如git gc),即:

$ git clone-无硬链接/ XYZ / ABC
$ git filter-branch --subdirectory-filter ABC HEAD
$ git reset --hard
$ git push ... ABC.git头

6

现在正确的方法如下:

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

GitHub现在甚至有关于此类情况的小文章

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

因此,您的算法应为:

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

6

似乎这里的大多数(全部?)答案都取决于某种形式git filter-branch --subdirectory-filter及其形式。这可能在“大多数时候”有效,但是在某些情况下,例如,当您重命名文件夹时,例如:

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

ABC/
    /move_this_dir_renamed

如果您使用普通的git过滤器样式来提取“ move_me_renamed”,则将丢失文件更改历史记录,该文件更改历史记录最初是从move_this_dir(ref)开始的。

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

  1. 在本地克隆多模块项目
  2. 分支机构-检查那里: git branch -a
  3. 对要包含在拆分中的每个分支进行检出,以在工作站上获取本地副本: git checkout --track origin/branchABC
  4. 在新目录中制作副本: cp -r oldmultimod simple
  5. 进入新的项目副本: cd simple
  6. 摆脱该项目中不需要的其他模块:
  7. git rm otherModule1 other2 other3
  8. 现在仅保留目标模块的子目录
  9. 摆脱模块子目录,使模块根成为新项目根
  10. git mv moduleSubdir1/* .
  11. 删除遗物子目录: rmdir moduleSubdir1
  12. 随时检查更改: git status
  13. 创建新的git repo并复制其URL,以将该项目指向其中:
  14. git remote set-url origin http://mygithost:8080/git/our-splitted-module-repo
  15. 验证这是好的: git remote -v
  16. 将更改推送到远程存储库: git push
  17. 转到远程仓库并检查所有内容
  18. 对所需的任何其他分支重复此操作: git checkout branch2

这遵循github doc“将子文件夹拆分到新的存储库中”的步骤6-11,将模块推送到新的存储库

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


1
在git haystack中找到了针!现在,我可以保留所有提交历史记录。
亚当

5

我建议使用GitHub的将子文件夹拆分为新存储库的指南。步骤类似于Paul的回答,但我发现它们的说明更容易理解。

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


将子文件夹拆分到新的存储库中

  1. 打开Git Bash。

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

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

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

cd REPOSITORY-NAME
  1. 要从存储库中的其余文件中过滤出该子文件夹,请运行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.然而,根据此处所有答案的注释,无论子目录已重命名filter-branch,该subtree脚本都会导致历史记录丢失。有什么可以解决的吗?
亚当

找到了保留所有提交的解决方案,包括先前目录重命名/移动的所有提交-这是rogerdpack对这个问题的解答。
亚当

唯一的问题是,我不能用克隆的回购更多
Qiulang

5

当运行git filter-branch使用的新版本git2.22+也许?),它说,使用这种新工具git的过滤器回购。这个工具无疑为我简化了事情。

用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选项可以得到稍微更好的历史记录。5 hours ago现在,我没有看到在新仓库中对移动文件进行的最后修改,而是last year在GitHub UI中看到了,它与原始仓库中的修改时间匹配。

代替...

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博客文章很好地解释了创建另一个回购过滤工具的原因。
  • 我最初尝试了在原始存储库中创建与目标存储库名称匹配的子目录,然后进行过滤(使用git filter-repo --subdirectory-filter dir-matching-new-repo-name)的路径。该命令正确地将该子目录转换为复制的本地存储库的根目录,但是它还导致创建该子目录只进行了三次提交的历史记录。(我没有意识到--path可以多次指定它;因此,无需在源存储库中创建子目录。)由于有人在提交时已提交到源存储库,所以我注意到我无法继续进行历史,我只是git reset commit-before-subdir-move --hardclone命令之后使用过,并将其添加--forcefilter-repo命令中以使其在经过稍微修改的本地克隆上运行。
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)

1
推荐使用新filter-repo工具(我上个月在stackoverflow.com/a/58251653/6309上推荐了此书)
VonC

使用git-filter-repo一定要在这一点上的首选方法。它远比快得多,并且更安全git-filter-branch,并且可以防止人们在重写git历史记录时遇到很多陷阱。希望这个答案能引起更多关注,因为它是解决的方法git-filter-repo
杰里米·卡尼

4

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

它的名字叫git_filter,它住在这里:

https://github.com/slobobaby/git_filter

在GitHub上。

我希望它对某人有用。


4

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

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

这里的猫是什么?
rogerdpack

4

值得一提的是,这是在Windows计算机上使用GitHub的方式。假设您有一个克隆的仓库位于中C:\dir1。目录结构如下:C:\dir1\dir2\dir3。该dir3目录是我要成为新的单独存储库的目录。

Github:

  1. 创建新的存储库: MyTeam/mynewrepo

重击提示:

  1. $ cd c:/Dir1
  2. $ git filter-branch --prune-empty --subdirectory-filter dir2/dir3 HEAD
    返回值:(Ref 'refs/heads/master' was rewritten仅供参考:dir2 / dir3区分大小写。)

  3. $ git remote add some_name git@github.com:MyTeam/mynewrepo.git
    git remote add origin etc。无效,返回了“ remote origin already exists

  4. $ git push --progress some_name master


3

正如我上面提到的,我不得不使用反向解决方案(删除所有未触及我的提交dir/subdir/targetdir),这似乎效果很好,可以删除约95%的提交(按需)。但是,还有两个小问题。

第一filter-branch做了一声却取消其引入或修改代码,但很显然,提交的工作合并提交是其在Gitiverse台站下方。

这可能是我可以忍受的装饰性问题(他说...眼睛移开时慢慢退后)

第二,几乎所有剩余的提交都是重复的!我似乎已经获得了第二条冗余的时间表,该时间表几乎涵盖了项目的整个历史。有趣的事情(您可以从下面的图片中看到)是,我的三个本地分支并不都在同一时间轴上(这当然是它存在的原因,而不仅仅是垃圾回收)。

我唯一可以想象的是,其中一个删除的提交可能是filter-branch 实际上确实删除过的单个合并提交,并且由于每个现在未合并的链都使用了自己的提交副本,因此创建了并行的时间轴。(耸耸肩,我的TARDiS在哪里?)我很确定可以解决此问题,尽管我真的很想了解它是如何发生的。

在疯狂的mergefest-O-RAMA的情况下,我很可能会把它搁置一旁,因为它在我的提交历史中已经牢牢地扎根了自己-每当我接近时都会威胁我-它似乎并没有真正引起任何非美容方面的问题,因为它在Tower.app中非常漂亮。


3

更简单的方法

  1. 安装git splits。我基于jkeating的解决方案将其创建为git扩展。
  2. 将目录拆分到本地分支 #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

  3. 在某处创建一个空的仓库。我们假设我们xyz在GitHub上创建了一个空的仓库,该仓库具有path:git@github.com:simpliwp/xyz.git

  4. 推送到新的仓库。 #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

  5. 将新创建的远程仓库克隆到新的本地目录中
    #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


与“简易方式”相比,此方法的优点是已经为新的存储库设置了远程服务器,因此您可以立即添加子树。实际上,这种方式对我来说似乎更容易(即使没有git splits
MM

提议AndrewD发布此解决方案。如果这对其他人有用,我已分叉他的存储库以使其可用于OSX(github.com/ricardoespsanto/git-splits
ricardoespsanto

2

在垃圾回收之前,您可能需要类似“ git reflog expire --expire = now --all”之类的东西,才能真正清除文件。git filter-branch只会删除历史记录中的引用,但不会删除保存数据的reflog条目。当然,先测试一下。

尽管我的初始条件有所不同,但我的磁盘使用率却急剧下降。也许--subdirectory-filter否定了这种需求,但我对此表示怀疑。


2

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.

1

将其放入您的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'

1

我确定git子树都很好,但我想移动的git托管代码的子目录全部在eclipse中。因此,如果您使用egit,它非常容易。拿出您要移动的项目,并团队->断开连接,然后团队->将其共享到新位置。默认情况下,它将尝试使用旧的存储库位置,但是您可以取消选中现有的使用选择并选择新的位置来移动它。全部冰雹。


3
子树的“精妙”部分是您子目录的历史记录。如果您不需要历史记录,那么走痛苦的简单方法就是路。
pglezen '16


0

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

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”来删除所有现在为空的旧提交。在推向原产地大师之前要做!
ZettaCircl

如果您犯了错误并且在删除了旧的空提交后仍然想“重推”,请执行:“ git push -u origin master
–force
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.