如何提取一个git子目录并从中创建一个子模块?


119

我几个月前开始了一个项目,并将所有内容存储在主目录中。在我的主目录“ Project”中,有几个包含不同内容的子目录:Project / paper包含用LaTeX编写的文档Project / sourcecode / RailsApp包含我的rails应用程序。

“项目”已GIT化,并且“纸张”和“ RailsApp”目录中都有很多提交。现在,就像我想对我的“ RailsApp”使用cruisecontrol.rb一样,我想知道是否有一种方法可以在不丢失历史的情况下从“ RailsApp”中制作一个子模块。


2
也是一个很好的答案:stackoverflow.com/questions/359424/...
Rehno Lindeque

Answers:


122

如今,有比手动使用git filter-branch更加容易的方法:git subtree

安装

从1.7.11开始,NOTEgit-subtree现在是git(如果您安装contrib)的一部分,因此您可能已经安装了它。您可以通过执行检查git subtree


要从源代码安装git-subtree(对于git的较早版本):

git clone https://github.com/apenwarr/git-subtree.git

cd git-subtree
sudo rsync -a ./git-subtree.sh /usr/local/bin/git-subtree

或者,如果您想要手册页和所有

make doc
make install

用法

将较大的分成较小的块:

# Go into the project root
cd ~/my-project

# Create a branch which only contains commits for the children of 'foo'
git subtree split --prefix=foo --branch=foo-only

# Remove 'foo' from the project
git rm -rf ./foo

# Create a git repo for 'foo' (assuming we already created it on github)
mkdir foo
pushd foo
git init
git remote add origin git@github.com:my-user/new-project.git
git pull ../ foo-only
git push origin -u master
popd

# Add 'foo' as a git submodule to `my-project`
git submodule add git@github.com:my-user/new-project.git foo

有关详细文档(手册页),请阅读git-subtree.txt


10
git subtree岩石!
西蒙·伍德赛德

3
但是git-subtree避免使用子模块的意义不是吗?我的意思是,您确实是git-subtree的作者(除非有昵称冲突),但看起来git-subtree已更改,即使您显示的命令似乎仍然有效。我说对了吗?
Blaisorblade 2012年

17
从1.7.11开始,git-subtree现在是git的一部分(如果您安装contrib)
Jeremy

8
那么git rm -rf ./foo去除fooHEAD,但不会过滤my-project的全部历史。然后,git submodule add git@github.com:my-user/new-project.git foo仅使foo一个子模块从开始HEAD。在这方面,脚本filter-branch是优越的,因为它允许实现“从一开始就
好像子目录

为此,thx-git subtree docs有点莫名其妙,这(对我来说)是我想用它做的最明显有用的事情……
hwjp

38

检出git filter-branch

手册页的Examples部分显示了如何将子目录提取到其自己的项目中,同时保留其所有历史记录并丢弃其他文件/目录的历史记录(正是您要查找的内容)。

要重写存储库以使其看起来好像foodir/是其项目根目录,并丢弃所有其他历史记录:

   git filter-branch --subdirectory-filter foodir -- --all

因此,您可以,例如,将库子目录变成其自己的存储库。
请注意,--filter-branch选项将选项与修订选项分开,并且--all用来重写所有分支和标签。


1
这对我来说很好。我注意到的唯一缺点是结果是包含所有提交的单个master分支。
aceofspades 2013年

@aceofspades:为什么会有不利之处?
naught101 2013年

2
对我来说,从git repo中提取提交的全部要点是我想保留历史记录。
aceofspades 2013年

13

一种相反的方法是-删除除要保留的文件以外的所有内容。

基本上,制作存储库的副本,然后用于git filter-branch删除除要保留的文件/文件夹以外的所有内容。

例如,我有一个项目,希望从中将文件提取tvnamer.py到新的存储库中:

git filter-branch --tree-filter 'for f in *; do if [ $f != "tvnamer.py" ]; then rm -rf $f; fi; done' HEAD

这用于git filter-branch --tree-filter遍历每次提交,运行命令并重新提交结果目录内容。这是极具破坏性的(因此,您仅应在存储库的副本上执行此操作!),并且可能要花一些时间(在具有300个提交和约20个文件的存储库中大约需要1分钟)。

上面的命令仅在每个修订版上运行以下shell脚本,您当然必须对其进行修改(以使其排除子目录而不是tvnamer.py):

for f in *; do
    if [ $f != "tvnamer.py" ]; then
        rm -rf $f;
    fi;
done

最大的明显问题是它保留所有提交消息,即使它们与其余文件无关。脚本git-remove-empty-commits,解决了这个问题。

git filter-branch --commit-filter 'if [ z$1 = z`git rev-parse $3^{tree}` ]; then skip_commit "$@"; else git commit-tree "$@"; fi'

您需要再次使用-fforce参数运行filter-branch任何内容refs/original/(基本上是备份)

当然,这将永远不会是完美的,例如,如果您的提交消息中提到了其他文件,但是它与git current所允许的接近(据我所知)。

同样,请只在存储库的副本上运行它!-但总的来说,要删除除“ thisismyfilename.txt”以外的所有文件:

git filter-branch --tree-filter 'for f in *; do if [ $f != "thisismyfilename.txt" ]; then rm -rf $f; fi; done' HEAD
git filter-branch -f --commit-filter 'if [ z$1 = z`git rev-parse $3^{tree}` ]; then skip_commit "$@"; else git commit-tree "$@"; fi'

4
git filter-branch具有(如今?)有一个内置选项来删除空提交,即--prune-empty。一个更好的指导,git filter-branch在这个问题的答案:stackoverflow.com/questions/359424/...
Blaisorblade

4

无论CoolAJ86apenwarr答案都非常相似。我在两个对象之间来回走动,试图了解两个对象中缺少的部分。以下是它们的组合。

首先,将Git Bash导航到要拆分的git repo的根目录。在我的示例中,这是~/Documents/OriginalRepo (master)

# move the folder at prefix to a new branch
git subtree split --prefix=SubFolderName/FolderToBeNewRepo --branch=to-be-new-repo

# create a new repository out of the newly made branch
mkdir ~/Documents/NewRepo
pushd ~/Documents/NewRepo
git init
git pull ~/Documents/OriginalRepo to-be-new-repo

# upload the new repository to a place that should be referenced for submodules
git remote add origin git@github.com:myUsername/newRepo.git
git push -u origin master
popd

# replace the folder with a submodule
git rm -rf ./SubFolderName/FolderToBeNewRepo
git submodule add git@github.com:myUsername/newRepo.git SubFolderName/FolderToBeNewRepo
git branch --delete --force to-be-new-repo

下面是上面的副本,其中替换了可自定义的名称,并改用https。现在是根文件夹~/Documents/_Shawn/UnityProjects/SoProject (master)

# move the folder at prefix to a new branch
git subtree split --prefix=Assets/SoArchitecture --branch=so-package

# create a new repository out of the newly made branch
mkdir ~/Documents/_Shawn/UnityProjects/SoArchitecture
pushd ~/Documents/_Shawn/UnityProjects/SoArchitecture
git init
git pull ~/Documents/_Shawn/UnityProjects/SoProject so-package

# upload the new repository to a place that should be referenced for submodules
git remote add origin https://github.com/Feddas/SoArchitecture.git
git push -u origin master
popd

# replace the folder with a submodule
git rm -rf ./Assets/SoArchitecture
git submodule add https://github.com/Feddas/SoArchitecture.git
git branch --delete --force so-package

3

如果您想将文件的某些子集转移到新的存储库中但保留历史记录,那么您基本上将获得一个全新的历史记录。这种工作方式基本上如下:

  1. 创建新的存储库。
  2. 对于旧存储库的每个修订版,将对模块的更改合并到新存储库中。这将创建现有项目历史记录的“副本”。

如果您不介意编写一个小的但冗长的脚本,则自动化该过程应该有点简单。坦率地说,是的,但也很痛苦。人们过去曾经在Git中进行过历史重写,您可以进行搜索。

或者:克隆存储库,然后删除克隆中的文件,然后删除原始应用程序。这将花费一分钟,保证可以正常工作,并且比起尝试净化git历史,您可以回到更重要的事情上。不必担心冗余历史记录副本占用的硬盘空间。

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.