如何取消 Git 子模块的订阅?

485

如何最佳实践地取消 Git 子模块并将所有代码合并回核心代码库?


8
注意:使用git1.8.3,现在您可以尝试使用git submodule deinit命令,请参见我的回答 - VonC
8
我可能误解了,但是 git submodule deinit 命令似乎会移除代码。 - Joe Germuska
4
自git 1.8.5(2013年11月)以来,只需执行简单的git submodule deinit asubmodule; git rm asubmodule命令即可,如下面我的回答所示 - VonC
考虑使用 git subtree。 - laplasz
13个回答

653

如果您只想将子模块代码放入主存储库中,则只需删除子模块并将文件重新添加到主存储库中:

git rm --cached submodule_path # delete reference to submodule HEAD (no trailing slash)
git rm .gitmodules             # if you have more than one submodules,
                                # you need to edit this file instead of deleting!
rm -rf submodule_path/.git     # make sure you have backup!!
git add submodule_path         # will add files instead of commit reference
git commit -m "remove submodule"

如果您也想保留子模块的历史记录,可以进行一个小技巧:“合并”子模块到主仓库中,这样结果将与之前相同,只是子模块文件现在在主仓库中。
在主模块中,您需要执行以下操作:
# Fetch the submodule commits into the main repository
git remote add submodule_origin git://url/to/submodule/origin
git fetch submodule_origin

# Start a fake merge (won't change any files, won't commit anything)
git merge -s ours --no-commit submodule_origin/master

# Do the same as in the first solution
git rm --cached submodule_path # delete reference to submodule HEAD
git rm .gitmodules             # if you have more than one submodules,
                                # you need to edit this file instead of deleting!
rm -rf submodule_path/.git     # make sure you have backup!!
git add submodule_path         # will add files instead of commit reference

# Commit and cleanup
git commit -m "removed submodule"
git remote rm submodule_origin

生成的代码库看起来有点奇怪:会有不止一个初始提交。但这对Git不会造成任何问题。
第二种解决方案的一个重要优势是,您仍然可以在最初位于子模块中的文件上运行git blamegit log。实际上,在这里发生的只是将许多文件重命名为一个存储库内的操作,Git应该会自动检测到这一点。如果您仍然遇到git log的问题,请尝试使用一些选项(例如--follow-M-C),这些选项可以更好地进行重命名和复制检测。

5
基本上是这样。Git的技巧在于它不会存储重命名操作:相反,它通过查看父提交来检测它们。如果有一个文件内容在先前的提交中存在,但文件名不同,则被视为重命名(或复制)。在上述步骤中,git merge 确保每个文件都会有一个“先前的提交”(在合并的两个“侧”之一)。 - gyim
6
谢谢gyim,我开始了一个项目,我认为将事物分成几个存储库并使用子模块将它们链接在一起是有意义的。但现在看来这样做过于复杂,我想将它们合并在一起,同时不丢失我的历史记录。 - Bowie Owens
5
@theduke 我也遇到了这个问题。在按照以下步骤之前,可以通过将子模块存储库中的所有文件移动到具有与要合并的存储库相同路径的目录结构中来解决:即,如果您在主存储库中的子模块位于foo/中,则在子模块中执行 mkdir foo && git mv !(foo) foo && git commit - Chris Down
60
需要在虚假合并时添加 --allow-unrelated-histories 以强制合并,因为会收到 fatal: refusing to merge unrelated histories 的错误提示。更多信息请参见此处:https://github.com/git/git/blob/master/Documentation/RelNotes/2.9.0.txt#L58-L68 - vaskort
3
它不保留历史记录,如果我在“git log 子模块所在的子目录”之后键入命令,它不能显示所有的历史记录。 - user1244932
显示剩余15条评论

102
我已经创建了一个脚本,可以将子模块转换为简单的目录,并保留所有文件历史记录。它不会遇到其他解决方案所遇到的git log --follow <file>问题。此外,这是一个非常简单的一行调用,可以为您完成所有工作。祝你好运。
它基于Lucas Jenß的优秀工作,他在他的博客文章“将子模块集成到父存储库中”中描述了这个过程,但自动化整个过程并清理了一些其他边角情况。
最新代码将通过github进行维护,包括错误修复,网址为https://github.com/jeremysears/scripts/blob/master/bin/git-submodule-rewrite,但为了符合stackoverflow答案协议的规定,我在下面完整地提供了解决方案。
用法:
$ git-submodule-rewrite <submodule-name>

git-submodule-rewrite:

#!/usr/bin/env bash

# This script builds on the excellent work by Lucas Jenß, described in his blog
# post "Integrating a submodule into the parent repository", but automates the
# entire process and cleans up a few other corner cases.
# https://x3ro.de/2013/09/01/Integrating-a-submodule-into-the-parent-repository.html

function usage() {
  echo "Merge a submodule into a repo, retaining file history."
  echo "Usage: $0 <submodule-name>"
  echo ""
  echo "options:"
  echo "  -h, --help                Print this message"
  echo "  -v, --verbose             Display verbose output"
}

function abort {
    echo "$(tput setaf 1)$1$(tput sgr0)"
    exit 1
}

function request_confirmation {
    read -p "$(tput setaf 4)$1 (y/n) $(tput sgr0)"
    [ "$REPLY" == "y" ] || abort "Aborted!"
}

function warn() {
  cat << EOF
    This script will convert your "${sub}" git submodule into
    a simple subdirectory in the parent repository while retaining all
    contents and file history.

    The script will:
      * delete the ${sub} submodule configuration from .gitmodules and
        .git/config and commit it.
      * rewrite the entire history of the ${sub} submodule so that all
        paths are prefixed by ${path}.
        This ensures that git log will correctly follow the original file
        history.
      * merge the submodule into its parent repository and commit it.

    NOTE: This script might completely garble your repository, so PLEASE apply
    this only to a fresh clone of the repository where it does not matter if
    the repo is destroyed.  It would be wise to keep a backup clone of your
    repository, so that you can reconstitute it if need be.  You have been
    warned.  Use at your own risk.

EOF

  request_confirmation "Do you want to proceed?"
}

function git_version_lte() {
  OP_VERSION=$(printf "%03d%03d%03d%03d" $(echo "$1" | tr '.' '\n' | head -n 4))
  GIT_VERSION=$(git version)
  GIT_VERSION=$(printf "%03d%03d%03d%03d" $(echo "${GIT_VERSION#git version}" | tr '.' '\n' | head -n 4))
  echo -e "${GIT_VERSION}\n${OP_VERSION}" | sort | head -n1
  [ ${OP_VERSION} -le ${GIT_VERSION} ]
}

function main() {

  warn

  if [ "${verbose}" == "true" ]; then
    set -x
  fi

  # Remove submodule and commit
  git config -f .gitmodules --remove-section "submodule.${sub}"
  if git config -f .git/config --get "submodule.${sub}.url"; then
    git config -f .git/config --remove-section "submodule.${sub}"
  fi
  rm -rf "${path}"
  git add -A .
  git commit -m "Remove submodule ${sub}"
  rm -rf ".git/modules/${sub}"

  # Rewrite submodule history
  local tmpdir="$(mktemp -d -t submodule-rewrite-XXXXXX)"
  git clone "${url}" "${tmpdir}"
  pushd "${tmpdir}"
  local tab="$(printf '\t')"
  local filter="git ls-files -s | sed \"s/${tab}/${tab}${path}\//\" | GIT_INDEX_FILE=\${GIT_INDEX_FILE}.new git update-index --index-info && mv \${GIT_INDEX_FILE}.new \${GIT_INDEX_FILE}"
  git filter-branch --index-filter "${filter}" HEAD
  popd

  # Merge in rewritten submodule history
  git remote add "${sub}" "${tmpdir}"
  git fetch "${sub}"

  if git_version_lte 2.8.4
  then
    # Previous to git 2.9.0 the parameter would yield an error
    ALLOW_UNRELATED_HISTORIES=""
  else
    # From git 2.9.0 this parameter is required
    ALLOW_UNRELATED_HISTORIES="--allow-unrelated-histories"
  fi

  git merge -s ours --no-commit ${ALLOW_UNRELATED_HISTORIES} "${sub}/master"
  rm -rf tmpdir

  # Add submodule content
  git clone "${url}" "${path}"
  rm -rf "${path}/.git"
  git add "${path}"
  git commit -m "Merge submodule contents for ${sub}"
  git config -f .git/config --remove-section "remote.${sub}"

  set +x
  echo "$(tput setaf 2)Submodule merge complete. Push changes after review.$(tput sgr0)"
}

set -euo pipefail

declare verbose=false
while [ $# -gt 0 ]; do
    case "$1" in
        (-h|--help)
            usage
            exit 0
            ;;
        (-v|--verbose)
            verbose=true
            ;;
        (*)
            break
            ;;
    esac
    shift
done

declare sub="${1:-}"

if [ -z "${sub}" ]; then
  >&2 echo "Error: No submodule specified"
  usage
  exit 1
fi

shift

if [ -n "${1:-}" ]; then
  >&2 echo "Error: Unknown option: ${1:-}"
  usage
  exit 1
fi

if ! [ -d ".git" ]; then
  >&2 echo "Error: No git repository found.  Must be run from the root of a git repository"
  usage
  exit 1
fi

declare path="$(git config -f .gitmodules --get "submodule.${sub}.path")"
declare url="$(git config -f .gitmodules --get "submodule.${sub}.url")"

if [ -z "${path}" ]; then
  >&2 echo "Error: Submodule not found: ${sub}"
  usage
  exit 1
fi

if ! [ -d "${path}" ]; then
  >&2 echo "Error: Submodule path not found: ${path}"
  usage
  exit 1
fi

main

1
很好,@qznc。这已在OSX上测试过了。当它在两个平台上都通过测试时,我将很高兴地合并它。 - jsears
@qznc Ubuntu 16.04支持已合并,答案已更新。 - jsears
非常感谢您!我通过从 GitHub 获取您的最新脚本,并在 WSL 下运行它,成功地在 Windows 10 中使其正常工作。 - Sam
5
这是最好的答案,保留了整个历史。非常棒! - CharlesB
1
在 Windows 10 上使用 Git Bash 2.20.1.1,从 Github 获取最新版本:curl https://raw.githubusercontent.com/jeremysears/scripts/master/bin/git-submodule-rewrite > git-submodule-rewrite.sh,并执行 ./git-submodule-rewrite.sh <submodule-name> 命令,确保所有工作都能在没有错误的情况下完成。 - Alexey
显示剩余11条评论

86

git 1.8.5 (2013年11月) 起(不保留子模块的历史记录):

mv yoursubmodule yoursubmodule_tmp
git submodule deinit yourSubmodule
git rm yourSubmodule
mv yoursubmodule_tmp yoursubmodule
git add yoursubmodule

这将会:

  • 取消注册并卸载(即删除内容)子模块(使用deinit,因此先使用mv),
  • 为您清理.gitmodules文件(使用rm),
  • 并从父仓库的索引中删除代表该子模块 SHA1 的 special entry(使用rm)。

一旦子模块的移除完成(使用deinitgit rm),您可以将文件夹重命名回其原始名称,并将其作为常规文件夹添加到 git 仓库中。

注意: 如果子模块是由旧版本的Git (< 1.8)创建的,则可能需要删除子模块本身的嵌套.git文件夹,如 commented by Simon East 所述。


如果您需要保留子模块的历史记录,请参考jsearsanswer,该方法使用git filter-branch

5
实际上,在1.8.4中,它确实会从工作树中删除它(我的整个子模块目录都被清空了)。 - Chris Down
@CharlesB,它(git submodule deinit)确实清除了它,但是它没有从索引中删除特殊条目,对吗? - VonC
@cottonBallPaws 它确实维护了子模块内文件的历史记录。这只影响父仓库。 - VonC
2
@mschuett 不,你没有错过任何东西:子模块一开始就没有 .git。如果你的情况是这样的话,那么它就是一个嵌套的仓库,而不是子模块。这就解释了为什么上面的答案在你的情况下不适用。关于两者之间的区别,请参见 http://stackoverflow.com/a/34410102/6309。 - VonC
1
@VonC 我目前使用的是2.9.0.windows.1版本,但是子模块可能是在早期版本的git上创建的,可能已经有好几年了,我不确定。我认为只要在最后进行add + commit之前删除那个文件,这些步骤似乎可以正常工作。 - Simon East
显示剩余37条评论

45
  1. git rm --cached the_submodule_path:从Git中删除子模块。
  2. .gitmodules文件中删除子模块部分,或者如果它是唯一的子模块,则删除该文件。
  3. 提交消息为“删除子模块 xyz”的提交操作。
  4. git add the_submodule_path:将更改后的代码库重新添加到Git。
  5. 提交消息为“添加子模块 xyz 的代码库”的提交操作。

目前我还没有找到更简单的方法。您可以通过使用git commit -a将3-5步骤压缩为一个步骤,这取决于个人喜好。


6
应该使用.gitmodules而不是.submodules吗? - imz -- Ivan Zakharyaschev
1
应该是 .gitmodules 而不是 .submodules - Dr. House
4
在对子模块文件夹执行git add之前,我必须先删除子模块中的.git目录。 - Carson
赞成 Carson Evans 的意见,您肯定要删除子模块根目录下的 .git 文件。这应该是步骤 2.5。 - Jamie McLaughlin

19
这里有很多答案,但它们似乎都过于复杂,可能并不符合你的需求。我相信大多数人都想保留他们的历史记录。
对于这个例子,主要仓库是git@site.com:main/main.git,子模块仓库是git@site.com:main/child.git。这假定子模块位于父仓库的根目录中。根据需要调整说明。
首先克隆父仓库并移除旧的子模块。
git clone git@site.com:main/main.git
git submodule deinit child
git rm child
git add --all
git commit -m "remove child submodule"

现在我们将把子存储库上游添加到主存储库中。

git remote add upstream git@site.com:main/child.git
git fetch upstream
git checkout -b merge-prep upstream/master
下一步假定您想将merge-prep分支上的文件移动到与子模块之前相同的位置,尽管您可以通过更改文件路径轻松更改位置。
mkdir child

将除了 .git 文件夹之外的所有文件和文件夹移动到子文件夹中。

git add --all
git commit -m "merge prep"

现在你只需要将你的文件合并回主分支即可。

git checkout master
git merge merge-prep # --allow-unrelated-histories merge-prep flag may be required 

在运行git push之前,四处查看并确保一切看起来都很好。

现在你需要记住的一件事是,默认情况下git log不能跟踪移动的文件,但是通过运行git log --follow filename,你可以查看文件的完整历史记录。


2
我一直进行到最后的 git merge merge-prep,但是收到了错误信息 fatal: refusing to merge unrelated histories。解决方法是:git merge --allow-unrelated-histories merge-prep - humblehacker
1
保留子模块历史记录的最佳答案。谢谢@mschuett - Anton Temchenko
在这个例子中,有没有办法将上游的文件提取到“child”目录中,这样你就不必后来再移动它们了?我在子模块和主仓库中有相同的文件名...所以当它试图合并这两个文件时,我只会得到一个合并冲突。 - Skitterm
可能可以,但我不确定。个人建议先提交一个更改,将你想要移动的文件移动到目标目录中,然后再将它们拉取到仓库中。 - michael.schuett
1
@gianpaolo 如果你将主分支rebase/merge到它们中,那么是的。 - michael.schuett
显示剩余2条评论

12

我们曾经为两个项目创建了两个仓库,但这些项目是如此耦合,以至于将它们分开没有任何意义,所以我们将它们合并了。

我将展示如何首先合并每个仓库的主分支,然后解释如何扩展到您拥有的每个分支,希望对您有所帮助。

如果您的子模块可以工作,并且您想将其转换为当前目录,可以执行以下操作:

git clone project_uri project_name

这里我们进行一次干净的克隆工作。在此过程中,您不需要初始化或更新子模块,因此请跳过它。

cd project_name
vim .gitmodules

使用您喜欢的编辑器(或 Vim)编辑 .gitmodules 文件,以删除您计划要替换的子模块。您需要删除的行应类似于以下内容:

[submodule "lib/asi-http-request"]
    path = lib/asi-http-request
    url = https://github.com/pokeb/asi-http-request.git

保存文件后,

git rm --cached directory_of_submodule
git commit -am "Removed submodule_name as submodule"
rm -rf directory_of_submodule

在这里,我们完全移除子模块关系,以便我们可以将其他存储库就地添加到项目中。

git remote add -f submodule_origin submodule_uri
git fetch submodel_origin/master
在这里,我们获取子模块存储库以进行合并。
git merge -s ours --no-commit submodule_origin/master

我们在这里开始了2个代码库的合并操作,但在提交之前停止。

git read-tree --prefix=directory_of_submodule/ -u submodule_origin/master

在这里,我们将子模块中的主分支内容发送到其之前的目录,然后再添加一个目录名称。

git commit -am "submodule_name is now part of main project"

在这里,我们完成合并所做的更改提交程序。

完成此操作后,您可以推送并开始与任何其他要合并的分支一起工作,只需检出您的存储库中将接收更改的分支,并更改您正在合并和读取树操作中带入的分支。


这似乎没有保留子模块文件的历史记录,我只在git日志中看到了一个提交,针对添加到“directory_of_submodule”下的文件。 - Anentropic
@Anentropic 很抱歉回复晚了。我刚刚重新执行了整个过程(进行了一些小修复)。该过程保留了整个历史记录,但有一个合并点,可能这就是你找不到的原因。 如果您想查看子模块历史记录,请执行“git log”,查找合并提交(在示例中是带有消息“submodule_name is now part of main project”的提交)。 它将有2个父提交(Merge:sdasda asdasd),git log第二个提交,您就可以在那里获得所有子模块/主控历史记录。 - dvicino
我的记忆有些模糊,但我想我能够通过执行 git log original_path_of_file_in_submodule 来获取合并子模块文件的历史记录,即在 git 存储库中为该文件注册的路径(尽管该文件不再存在于文件系统中),即使子模块文件现在位于 submodule_path/new_path_of_file - Anentropic
这样做并不能很好地保留历史记录,而且路径也不正确。我觉得需要像树过滤器之类的东西,但我已经超出了我的能力范围...尝试在这里找到的方法:http://x3ro.de/2013/09/01/Integrating-a-submodule-into-the-parent-repository.html - Luke H
这个答案已经过时了,https://dev59.com/23I-5IYBdhLWcg3wn526#16162228(VonC的回答)做得更好。 - CharlesB
git log --follow $file_from_read_tree 或者使用其他选项都不会起作用。最终我做的与此相同,唯一的例外是在子模块中有一个本地分支,将其所有文件重命名为新的“子树路径”,执行“ours”合并,然后使用空前缀读取树并合并。这样可以使 git log --follow 找到提交记录。您需要暂时删除/添加任何嵌套的子模块。真是麻烦! - joonas

6

这是对 @gyim 回答的稍微改进了一下(在我看来)。他在主要的工作副本中进行了一些危险的更改,我认为在单独的克隆上操作,然后在最后合并它们在一起会更容易。

在一个单独的目录中(使错误更容易清理和重试),检出顶级存储库和子存储库。

git clone ../main_repo main.tmp
git clone ../main_repo/sub_repo sub.tmp

首先编辑子仓库,将所有文件移动到所需的子目录中。

cd sub.tmp
mkdir sub_repo_path
git mv `ls | grep -v sub_repo_path` sub_repo_path/
git commit -m "Moved entire subrepo into sub_repo_path"

注意HEAD标签

SUBREPO_HEAD=`git reflog | awk '{ print $1; exit; }'`

现在从主仓库中删除子仓库

cd ../main.tmp
rmdir sub_repo_path
vi .gitmodules  # remove config for submodule
git add -A
git commit -m "Removed submodule sub_repo_path in preparation for merge"

最后,只需要合并它们即可。
git fetch ../sub.tmp
# remove --allow-unrelated-histories if using git older than 2.9.0
git merge --allow-unrelated-histories $SUBREPO_HEAD

完成了!安全地且没有任何魔法。


那个答案是哪一个?可能需要引用用户名,因为最佳答案随时可能会更改。 - Contango
@Contango的回答已更新。但是顶部答案仍然以400分的领先优势保持着第一名的位置;-) - dataless
如果子仓库已经包含一个名为 subrepo 的目录并且其中有内容,这个代码还能正常工作吗? - detly
在最后一步中,我遇到了以下错误: git merge $SUBREPO_HEAD fatal: refusing to merge unrelated histories 在这种情况下,我应该使用 git merge $SUBREPO_HEAD --allow-unrelated-histories 吗?还是说它可以正常工作,而我犯了一个错误? - Ti-m
1
@Ti-m 是的,这正是合并两个没有共享任何提交记录历史的情况。对无关的历史进行防范似乎是自我回答以来git的新功能;我会更新我的回答。 - dataless

6

3
基于VonC的答案,我创建了一个简单的bash脚本来完成这个任务。结尾处的add必须使用通配符,否则它将撤销子模块本身之前的rm操作。在add命令中,重要的是要添加子模块目录的内容,而不是命名目录本身。在名为git-integrate-submodule的文件中:
#!/usr/bin/env bash
mv "$1" "${1}_"
git submodule deinit "$1"
git rm "$1"
mv "${1}_" "$1"
git add "$1/**"

3

git rm [-r] --cached submodule_path

返回值

fatal: pathspec 'emr/normalizers/' did not match any files

背景:在意识到需要将子模块从刚刚添加它们的主项目中移除之前,我在子模块文件夹中执行了rm -r .git*。当我取消部分子模块时,出现了上述错误。无论如何,在执行rm -r .git*后,我通过运行以下命令来修复它们:

mv submodule_path submodule_path.temp
git add -A .
git commit -m "De-submodulization phase 1/2"
mv submodule_path.temp submodule_path
git add -A .
git commit -m "De-submodulization phase 2/2"

请注意,这不会保留历史记录。

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