如何提取Git子目录并将其制作成子模块?

125

我几个月前开始了一个项目,并将所有东西存储在一个主目录中。

在我的主目录“Project”中,有几个子目录包含不同的内容:

  • Project/paper 包含使用LaTeX编写的文档
  • Project/sourcecode/RailsApp 包含我的Rails应用程序

“Project”已经GIT化,"paper"和"RailsApp"目录中有许多提交。现在,由于我想为我的“RailsApp”使用cruisecontrol.rb,我想知道是否有一种方法可以将“RailsApp”制作成子模块而不会失去历史记录。


2
还有一个非常好的答案:https://dev59.com/cXRC5IYBdhLWcg3wROpQ - Rehno Lindeque
可能是将子目录分离(移动)到单独的Git存储库中的重复问题。 - Colonel Thirty Two
5个回答

129

现在有一种比手动使用git filter-branch更容易的方法: git subtree

安装

注意:自从1.7.11版本开始,git-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

或者如果你想要获取所有的man页面

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 获取详细的文档(man页面)。


4
但是 git-subtree 的目的不就是避免使用子模块吗?我的意思是,你确实是 git-subtree 的作者(除非有昵称冲突),但看起来 git-subtree 已经发生了变化,尽管你展示的命令仍然有效。我理解得对吗? - Blaisorblade
18
自1.7.11版本起,git-subtree已经成为git的一部分(如果您安装了contrib)。 - Jeremy
8
git rm -rf ./foo会从当前分支中移除foo,但不会过滤掉my-project的完整历史记录。然后,git submodule add git@github.com:my-user/new-project.git foo 只会将foo作为子模块添加到当前分支。在这方面,脚本化的filter-branch更加优越,因为它可以实现"假装子目录从一开始就是一个子模块"的效果。 - Gregory Pakosz
谢谢这个——Git子树文档有点令人困惑,而这对我来说是最明显有用的事情... - hwjp
请注意,子模块甚至可能不是必要的步骤。例如,如果您想将其提取为单独的软件包以在当前项目中使用,则应使用适当的软件包管理器,例如 npm link && cd ../orig-proj && npm link extracted-module - OJFord
显示剩余2条评论

41

请查看git filter-branch

该命令的man页面中的Examples部分展示了如何将子目录提取到单独的项目中,并保留其所有历史记录,同时丢弃其他文件/目录的历史记录(正是您要寻找的内容)。

为使仓库看起来好像foodir/是其项目根目录,并丢弃所有其他历史记录:

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

因此,您可以将库的子目录转换为其自己的存储库。
请注意,--用于分隔filter-branch选项和修订选项,--all用于重写所有分支和标签。


1
这对我来说效果很好。唯一的缺点是结果是一个包含所有提交记录的单个主分支。 - aceofspades
@aceofspades:为什么这是一个缺点? - naught101
2
对我来说,从Git存储库中提取提交的整个意义在于我想保留历史记录。 - aceofspades

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'

你需要使用-f强制参数再次运行filter-branch,并将refs/original/中的任何内容都过滤掉(这基本上是一个备份)。

当然,这永远不会完美,例如如果您的提交消息提到其他文件,但就我所知,这是 git 目前允许的最接近的方式。

请务必只在存储库的副本上运行此命令! - 但总之,要删除除"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 更好的指南可在以下问题的答案中找到:https://dev59.com/cXRC5IYBdhLWcg3wROpQ - Blaisorblade

4

以下是关于it技术的翻译:

两位回答者CoolAJ86apenwarr的答案非常相似。我来回查看这两个答案,试图理解其中缺失的部分。下面是它们的结合体。

首先将 Git Bash 导航到要拆分的 git 仓库的根目录。在我的示例中,这是~/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历史记录更重要的事情。不要担心冗余历史记录占用的硬盘空间。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接