436

取消子模块 Git 子模块,将所有代码带回核心存储库的最佳实践是什么?

4

13 回答 13

599

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

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

于 2009-11-24T11:09:34.880 回答
85

我创建了一个脚本,它将子模块转换为一个简单的目录,同时保留所有文件历史记录。它不会受到git log --follow <file>其他解决方案所面临的问题的影响。这也是一个非常简单的单行调用,可以为您完成所有工作。祝你好运。

它建立在 Lucas Jenß 在他的博客文章“将子模块集成到父存储库中”中描述的出色工作的基础上,但自动化了整个过程并清理了其他一些极端情况。

最新的代码将在https://github.com/jeremysears/scripts/blob/master/bin/git-submodule-rewrite的 github 上进行错误修复,但为了正确的 stackoverflow 应答协议,我已经包含了完整的解决方案如下。

用法:

$ git-submodule-rewrite <submodule-name>

git 子模块重写:

#!/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
于 2017-04-22T00:52:35.123 回答
78

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),
  • 并在父 repo ( )的索引中删除表示该子模块 SHA1的特殊条目。rm

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

注意:如果子模块是由旧 Git (< 1.8) 创建的,您可能需要删除.git子模块本身内的嵌套文件夹,正如Simon East评论的那样


如果您需要保留子模块的历史记录,请参阅jsears答案,它使用git filter-branch.

于 2013-04-23T06:13:14.017 回答
41
  1. git rm --cached the_submodule_path
  2. .gitmodules文件中删除子模块部分,或者如果它是唯一的子模块,请删除文件。
  3. 执行提交“删除子模块 xyz”
  4. git add the_submodule_path
  5. 另一个提交“添加了 xyz 的代码库”

我还没有找到更简单的方法。git commit -a您可以根据口味将 3-5 压缩为一步。

于 2009-11-18T23:58:49.170 回答
18

这里有很多答案,但所有答案似乎都过于复杂,并且可能不符合您的要求。我相信大多数人都想保留他们的历史。

对于此示例,主 repo 将是git@site.com:main/main.git,子模块 repo 将是git@site.com:main/child.git. 这假设子模块位于父 repo 的根目录中。根据需要调整说明。

首先克隆父 repo 并删除旧的子模块。

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

下一步假设您要将合并准备分支上的文件移动到与上述子模块相同的位置,尽管您可以通过更改文件路径轻松更改位置。

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您可以查看文件的完整历史记录。

于 2016-03-13T19:37:07.173 回答
12

我们碰巧为 2 个项目创建了 2 个存储库,这些项目耦合度很高,将它们分开没有任何意义,因此我们合并了它们。

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

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

git clone project_uri project_name

在这里,我们做了一个干净的克隆来工作。对于此过程,您不需要初始化或更新子模块,因此只需跳过它。

cd project_name
vim .gitmodules

使用您喜欢的编辑.gitmodules器(或 Vim)进行编辑以删除您计划替换的子模块。您需要删除的行应如下所示:

[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

在这里,我们完全删除了子模块关系,因此我们可以创建将另一个 repo 带到项目中。

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

这里我们将子模块中master的内容发送到在目录名前缀之前所在的目录

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

在这里,我们完成了提交合并中更改的过程。

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

于 2012-01-17T21:01:22.447 回答
6

这是@gyim答案的略微改进版本(恕我直言)。他在主要工作副本中进行了一系列危险的更改,我认为在单独的克隆上进行操作然后在最后将它们合并在一起要容易得多。

在一个单独的目录中(使错误更容易清理并重试)检查顶级 repo 和 subrepo。

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

首先编辑 subrepo 将所有文件移动到所需的子目录中

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

并做了!安全且没有任何魔法。

于 2014-01-30T22:38:09.303 回答
6

我发现的最佳答案在这里:

http://x3ro.de/2013/09/01/Integrating-a-submodule-into-the-parent-repository.html

这篇文章很好地解释了这个过程。

于 2014-08-29T13:04:28.763 回答
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"

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

于 2012-09-20T20:37:47.263 回答
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/**"
于 2016-10-18T18:25:37.973 回答
0

我发现(也?)从子模块获取本地提交数据更方便,因为否则我会丢失它们。(无法推送它们,因为我无法访问该遥控器)。所以我将 submodule/.git 添加为 remote_origin2,获取它提交并从该分支合并。不确定我是否还需要远程子模块作为源,因为我对 git 还不够熟悉。

于 2013-03-19T16:53:50.320 回答
0

这是我发现的最好和最简单的。

在子模块 repo 中,您希望从 HEAD 合并到主 repo:

  • git checkout -b "mergeMe"
  • mkdir "foo/bar/myLib/"(与您希望主仓库上的文件的路径相同)
  • git mv * "foo/bar/myLib/"(将所有移动到路径中)
  • git commit -m "ready to merge into main"

删除子模块并清除路径“foo/bar/myLib”后返回主仓库:

  • git merge --allow-unrelated-histories SubmoduleOriginRemote/mergeMe

繁荣完成

保存的历史

不用担心


请注意,这与其他一些答案几乎相同。但这假设您拥有子模块 repo。这也使得将来为子模块获得上游更改变得容易。

于 2020-02-04T16:54:45.893 回答
0

在主仓库中

  • git rm --cached [submodules_repo]
  • git commit -m "子模块被移除。"
  • git push origin [主]

在子模块回购

  • rm -rf .git

再次主要回购

  • git add [submodules_repo]
  • 混帐添加。
  • git commit -m "子模块 repo 添加到 main."
  • git push origin [主]
于 2021-11-25T19:44:11.963 回答