【问题标题】:Convert a Git folder to a submodule retrospectively?回顾性地将 Git 文件夹转换为子模块?
【发布时间】:2012-09-12 21:53:33
【问题描述】:

通常情况下,您正在编写某种类型的项目,但一段时间后,您会发现项目的某些组件实际上可用作独立组件(也许是库)。如果您从一开始就有这样的想法,那么很有可能大部分代码都在它自己的文件夹中。

有没有办法将 Git 项目中的一个子目录转换为子模块?

理想情况下,该目录中的所有代码都会从父项目中删除,并在其位置添加子模块项目,并带有所有适当的历史记录,并且所有父项目提交都指向正确的子模块提交。

【问题讨论】:

  • stackoverflow.com/questions/1365541/… 可能会有所帮助:)
  • 这不是原始问题的一部分,但更酷的方法是保留从文件夹外部开始并移入其中的文件的历史记录。目前,所有答案都丢失了移动之前的所有历史记录。
  • @ggll 的链接已关闭。 Here's an archived copy.

标签: git git-submodules


【解决方案1】:

要将子目录隔离到其自己的存储库中,请在原始存储库的克隆上使用 filter-branch

git clone <your_project> <your_submodule>
cd <your_submodule>
git filter-branch --subdirectory-filter 'path/to/your/submodule' --prune-empty -- --all

然后就是删除你原来的目录,把子模块添加到你的父项目中。

【讨论】:

  • 您可能还想在过滤器分支之后git remote rm &lt;name&gt;,然后可能添加一个新的遥控器。此外,如果有被忽略的文件,git clean -xd -f 可能会有用
  • -- --all 可以替换为分支的名称,如果子模块应该只从该分支中​​提取。
  • git clone &lt;your_project&gt; &lt;your_submodule&gt; 是否只为 your_submodule 下载文件?
  • @DominicTobias: git clone source destination 只是告诉 Git 放置克隆文件的位置。过滤子模块文件的真正魔法发生在filter-branch 步骤中。
  • filter-branch 现在是 deprecated。你可以使用git clone --filter,但你的Git服务器必须配置为允许过滤,否则你会得到warning: filtering not recognized by server, ignoring
【解决方案2】:

首先将目录更改为将成为子模块的文件夹。那么:

git init
git remote add origin <repourl>
git add .
git commit -am 'first commit in submodule'
git push -u origin master
cd ..
rm -rf <folder> # the folder which will be a submodule
git commit -am 'deleting folder'
git submodule add <repourl> <folder> # add the submodule
git commit -am 'adding submodule'

【讨论】:

  • 这将丢失该文件夹的所有历史记录。
  • 文件夹的历史记录将保存在主存储库中,新提交将保存历史记录在子模块中
【解决方案3】:

我知道这是一个旧线程,但这里的答案会压制其他分支中的任何相关提交。

克隆和保留所有这些额外分支和提交的简单方法:

1 - 确保你有这个 git 别名

git config --global alias.clone-branches '! git branch -a | sed -n "/\/HEAD /d; /\/master$/d; /remotes/p;" | xargs -L1 git checkout -t'

2 - 克隆远程,拉取所有分支,更改远程,过滤目录,推送

git clone git@github.com:user/existing-repo.git new-repo
cd new-repo
git clone-branches
git remote rm origin
git remote add origin git@github.com:user/new-repo.git
git remote -v
git filter-branch --subdirectory-filter my_directory/ -- --all
git push --all
git push --tags

【讨论】:

  • 我的原件有一个指向要点的链接,而不是在 SO 上嵌入代码
【解决方案4】:

现状

假设我们有一个名为 repo-old 的存储库,其中包含一个子目录 sub,我们希望将其转换为具有自己的 repo @ 的子模块 987654324@.

还打算将原始 repo repo-old 转换为修改后的 repo repo-new,其中所有涉及先前存在的子目录 sub 的提交现在应指向我们提取的子模块 repo repo-sub 的相应提交.

让我们改变

可以在git filter-branch 的帮助下分两步实现:

  1. repo-oldrepo-sub 的子目录提取(已在接受的answer 中提及)
  2. repo-oldrepo-new 的子目录替换(使用正确的提交映射)

备注:我知道这个问题已经过时了,并且已经提到 git filter-branch 有点过时并且可能很危险。但另一方面,它可能会帮助其他人使用转换后易于验证的个人存储库。所以要警告!如果有任何其他工具可以做同样的事情而不会被弃用并且可以安全使用,请告诉我!

我将解释我是如何使用 git 版本 2.26.2 在 linux 上实现这两个步骤的。旧版本可能会在一定程度上起作用,但需要进行测试。

为了简单起见,我将自己限制在原始存储库repo-old 中只有一个master 分支和一个origin 远程的情况。另请注意,我依赖带有前缀 temp_ 的临时 git 标签,这些标签将在此过程中被删除。因此,如果已经有类似名称的标签,您可能需要调整下面的前缀。最后请注意,我没有对此进行广泛的测试,并且可能存在配方失败的极端情况。所以请在继续之前备份所有内容

以下 bash sn-ps 可以连接成一个大脚本,然后应该在 repo repo-org 所在的同一文件夹中执行该脚本。不建议将所有内容直接复制粘贴到命令窗口中(即使我已经成功测试过)!

0。准备

变量

# Root directory where repo-org lives
# and a temporary location for git filter-branch
root="$PWD"
temp='/dev/shm/tmp'

# The old repository and the subdirectory we'd like to extract
repo_old="$root/repo-old"
repo_old_directory='sub'

# The new submodule repository, its url
# and a hash map folder which will be populated
# and later used in the filter script below
repo_sub="$root/repo-sub"
repo_sub_url='https://github.com/somewhere/repo-sub.git'
repo_sub_hashmap="$root/repo-sub.map"

# The new modified repository, its url
# and a filter script which is created as heredoc below
repo_new="$root/repo-new"
repo_new_url='https://github.com/somewhere/repo-new.git'
repo_new_filter="$root/repo-new.sh"

过滤脚本

# The index filter script which converts our subdirectory into a submodule
cat << EOF > "$repo_new_filter"
#!/bin/bash

# Submodule hash map function
sub ()
{
    local old_commit=\$(git rev-list -1 \$1 -- '$repo_old_directory')

    if [ ! -z "\$old_commit" ]
    then
        echo \$(cat "$repo_sub_hashmap/\$old_commit")
    fi
}

# Submodule config
SUB_COMMIT=\$(sub \$GIT_COMMIT)
SUB_DIR='$repo_old_directory'
SUB_URL='$repo_sub_url'

# Submodule replacement
if [ ! -z "\$SUB_COMMIT" ]
then
    touch '.gitmodules'
    git config --file='.gitmodules' "submodule.\$SUB_DIR.path" "\$SUB_DIR"
    git config --file='.gitmodules' "submodule.\$SUB_DIR.url" "\$SUB_URL"
    git config --file='.gitmodules' "submodule.\$SUB_DIR.branch" 'master'
    git add '.gitmodules'

    git rm --cached -qrf "\$SUB_DIR"
    git update-index --add --cacheinfo 160000 \$SUB_COMMIT "\$SUB_DIR"
fi
EOF
chmod +x "$repo_new_filter"

1。子目录提取

cd "$root"

# Create a new clone for our new submodule repo
git clone "$repo_old" "$repo_sub"

# Enter the new submodule repo
cd "$repo_sub"

# Remove the old origin remote
git remote remove origin

# Loop over all commits and create temporary tags
for commit in $(git rev-list --all)
do
    git tag "temp_$commit" $commit
done

# Extract the subdirectory and slice commits
mkdir -p "$temp"
git filter-branch --subdirectory-filter "$repo_old_directory" \
                  --tag-name-filter 'cat' \
                  --prune-empty --force -d "$temp" -- --all

# Populate hash map folder from our previously created tag names
mkdir -p "$repo_sub_hashmap"
for tag in $(git tag | grep "^temp_")
do
    old_commit=${tag#'temp_'}
    sub_commit=$(git rev-list -1 $tag)

    echo $sub_commit > "$repo_sub_hashmap/$old_commit"
done
git tag | grep "^temp_" | xargs -d '\n' git tag -d 2>&1 > /dev/null

# Add the new url for this repository (and e.g. push)
git remote add origin "$repo_sub_url"
# git push -u origin master

2。子目录替换

cd "$root"

# Create a clone for our modified repo
git clone "$repo_old" "$repo_new"

# Enter the new modified repo
cd "$repo_new"

# Remove the old origin remote
git remote remove origin

# Replace the subdirectory and map all sliced submodule commits using
# the filter script from above
mkdir -p "$temp"
git filter-branch --index-filter "$repo_new_filter" \
                  --tag-name-filter 'cat' --force -d "$temp" -- --all

# Add the new url for this repository (and e.g. push)
git remote add origin "$repo_new_url"
# git push -u origin master

# Cleanup (commented for safety reasons)
# rm -rf "$repo_sub_hashmap"
# rm -f "$repo_new_filter"

备注:如果新创建的 repo repo-newgit submodule update --init 期间挂起,则尝试以递归方式重新克隆存储库一次:

cd "$root"

# Clone the new modified repo recursively
git clone --recursive "$repo_new" "$repo_new-tmp"

# Now use the newly cloned one
mv "$repo_new" "$repo_new-bak"
mv "$repo_new-tmp" "$repo_new"

# Cleanup (commented for safety reasons)
# rm -rf "$repo_new-bak"

【讨论】:

  • 这是一个了不起的答案。正是我所需要的,并以易于遵循的格式清楚地概述了所需的步骤。
  • 很棒的指南 - 比你好多了。
【解决方案5】:

可以做到,但并不简单。如果你搜索git filter-branchsubdirectorysubmodule,就会有一些关于这个过程的不错的文章。它本质上需要创建项目的两个克隆,使用git filter-branch 删除一个中的一个子目录以外的所有内容,并仅删除另一个中的那个子目录。然后您可以将第二个存储库建立为第一个存储库的子模块。

【讨论】:

    【解决方案6】:

    这会就地进行转换,您可以像使用任何过滤器分支一样将其退出(我使用git fetch . +refs/original/*:*)。

    我有一个带有 utils 库的项目,该库已开始在其他项目中有用,并希望将其历史拆分为子模块。没想到先看 SO,所以我自己写了,它在本地构建历史记录,所以速度要快一些,之后如果你愿意,可以设置辅助命令的 .gitmodules 文件等,然后推送子模块随时随地记录历史。

    剥离的命令本身在这里,文档在 cmets 中,在后面的未剥离的命令中。将其作为自己的命令运行,并设置subdir,例如subdir=utils git split-submodule,如果您要拆分utils 目录。这很 hacky,因为它是一次性的,但我在 Git 历史记录的 Documentation 子目录中对其进行了测试。

    #!/bin/bash
    # put this or the commented version below in e.g. ~/bin/git-split-submodule
    ${GIT_COMMIT-exec git filter-branch --index-filter "subdir=$subdir; ${debug+debug=$debug;} $(sed 1,/SNIP/d "$0")" "$@"}
    ${debug+set -x}
    fam=(`git rev-list --no-walk --parents $GIT_COMMIT`)
    pathcheck=(`printf "%s:$subdir\\n" ${fam[@]} \
        | git cat-file --batch-check='%(objectname)' | uniq`)
    [[ $pathcheck = *:* ]] || {
        subfam=($( set -- ${fam[@]}; shift;
            for par; do tpar=`map $par`; [[ $tpar != $par ]] &&
                git rev-parse -q --verify $tpar:"$subdir"
            done
        ))
        git rm -rq --cached --ignore-unmatch  "$subdir"
        if (( ${#pathcheck[@]} == 1 && ${#fam[@]} > 1 && ${#subfam[@]} > 0)); then
            git update-index --add --cacheinfo 160000,$subfam,"$subdir"
        else
            subnew=`git cat-file -p $GIT_COMMIT | sed 1,/^$/d \
                | git commit-tree $GIT_COMMIT:"$subdir" $(
                    ${subfam:+printf ' -p %s' ${subfam[@]}}) 2>&-
                ` &&
            git update-index --add --cacheinfo 160000,$subnew,"$subdir"
        fi
    }
    ${debug+set +x}
    

    #!/bin/bash
    # Git filter-branch to split a subdirectory into a submodule history.
    
    # In each commit, the subdirectory tree is replaced in the index with an
    # appropriate submodule commit.
    # * If the subdirectory tree has changed from any parent, or there are
    #   no parents, a new submodule commit is made for the subdirectory (with
    #   the current commit's message, which should presumably say something
    #   about the change). The new submodule commit's parents are the
    #   submodule commits in any rewrites of the current commit's parents.
    # * Otherwise, the submodule commit is copied from a parent.
    
    # Since the new history includes references to the new submodule
    # history, the new submodule history isn't dangling, it's incorporated.
    # Branches for any part of it can be made casually and pushed into any
    # other repo as desired, so hooking up the `git submodule` helper
    # command's conveniences is easy, e.g.
    #     subdir=utils git split-submodule master
    #     git branch utils $(git rev-parse master:utils)
    #     git clone -sb utils . ../utilsrepo
    # and you can then submodule add from there in other repos, but really,
    # for small utility libraries and such, just fetching the submodule
    # histories into your own repo is easiest. Setup on cloning a
    # project using "incorporated" submodules like this is:
    #   setup:  utils/.git
    #
    #   utils/.git:
    #       @if _=`git rev-parse -q --verify utils`; then \
    #           git config submodule.utils.active true \
    #           && git config submodule.utils.url "`pwd -P`" \
    #           && git clone -s . utils -nb utils \
    #           && git submodule absorbgitdirs utils \
    #           && git -C utils checkout $$(git rev-parse :utils); \
    #       fi
    # with `git config -f .gitmodules submodule.utils.path utils` and
    # `git config -f .gitmodules submodule.utils.url ./`; cloners don't
    # have to do anything but `make setup`, and `setup` should be a prereq
    # on most things anyway.
    
    # You can test that a commit and its rewrite put the same tree in the
    # same place with this function:
    # testit ()
    # {
    #     tree=($(git rev-parse `git rev-parse $1`: refs/original/refs/heads/$1));
    #     echo $tree `test $tree != ${tree[1]} && echo ${tree[1]}`
    # }
    # so e.g. `testit make~95^2:t` will print the `t` tree there and if
    # the `t` tree at ~95^2 from the original differs it'll print that too.
    
    # To run it, say `subdir=path/to/it git split-submodule` with whatever
    # filter-branch args you want.
    
    # $GIT_COMMIT is set if we're already in filter-branch, if not, get there:
    ${GIT_COMMIT-exec git filter-branch --index-filter "subdir=$subdir; ${debug+debug=$debug;} $(sed 1,/SNIP/d "$0")" "$@"}
    
    ${debug+set -x}
    fam=(`git rev-list --no-walk --parents $GIT_COMMIT`)
    pathcheck=(`printf "%s:$subdir\\n" ${fam[@]} \
        | git cat-file --batch-check='%(objectname)' | uniq`)
    
    [[ $pathcheck = *:* ]] || {
        subfam=($( set -- ${fam[@]}; shift;
            for par; do tpar=`map $par`; [[ $tpar != $par ]] &&
                git rev-parse -q --verify $tpar:"$subdir"
            done
        ))
    
        git rm -rq --cached --ignore-unmatch  "$subdir"
        if (( ${#pathcheck[@]} == 1 && ${#fam[@]} > 1 && ${#subfam[@]} > 0)); then
            # one id same for all entries, copy mapped mom's submod commit
            git update-index --add --cacheinfo 160000,$subfam,"$subdir"
        else
            # no mapped parents or something changed somewhere, make new
            # submod commit for current subdir content.  The new submod
            # commit has all mapped parents' submodule commits as parents:
            subnew=`git cat-file -p $GIT_COMMIT | sed 1,/^$/d \
                | git commit-tree $GIT_COMMIT:"$subdir" $(
                    ${subfam:+printf ' -p %s' ${subfam[@]}}) 2>&-
                ` &&
            git update-index --add --cacheinfo 160000,$subnew,"$subdir"
        fi
    }
    ${debug+set +x}
    

    【讨论】:

      【解决方案7】:

      @knittl 使用filter-branch 的当前答案让我们非常接近预期的效果,但是在尝试时,Git 向我发出了警告:

      WARNING: git-filter-branch has a glut of gotchas generating mangled history
               rewrites.  Hit Ctrl-C before proceeding to abort, then use an
               alternative filtering tool such as 'git filter-repo'
               (https://github.com/newren/git-filter-repo/) instead.  See the
               filter-branch manual page for more details; to squelch this warning,
               set FILTER_BRANCH_SQUELCH_WARNING=1.
      

      在首次提出并回答此问题 9 年后,filter-branch 已被弃用,取而代之的是 git filter-repo。事实上,当我使用 git log --all --oneline --graph 查看我的 git 历史记录时,它充满了不相关的提交。

      git filter-repo怎么用呢? Github 有一篇很好的文章概述了 here。 (请注意,您需要独立于 git 安装它。我使用的是带有 pip3 install git-filter-repo 的 python 版本)

      如果他们决定移动/删除文章,我将在下面总结和概括他们的程序:

      git clone <your_old_project_remote> <your_submodule>
      cd <your_submodule>
      git filter-repo --path path/to/your/submodule
      git remote set-url origin <your_new_submodule_remote>
      git push -u origin <branch_name>
      

      从那里,您只需要将新存储库注册为您想要的子模块:

      cd <path/to/your/parent/module>
      git submodule add <your_new_submodule_remote>
      git submodule update
      git commit
      

      【讨论】:

        猜你喜欢
        • 2015-03-28
        • 1970-01-01
        • 1970-01-01
        • 2015-07-31
        • 2020-03-14
        • 2011-05-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多