Converter uma pasta Git em um submódulo retrospectivamente?


115

Freqüentemente, você está escrevendo um projeto de algum tipo e, depois de um tempo, fica claro que algum componente do projeto é realmente útil como um componente independente (uma biblioteca, talvez). Se você teve essa ideia desde o início, então há uma boa chance de que a maior parte desse código esteja em sua própria pasta.

Existe uma maneira de converter um dos subdiretórios em um projeto Git em um submódulo?

Idealmente, isso aconteceria de forma que todo o código nesse diretório fosse removido do projeto pai e o projeto do submódulo fosse adicionado em seu lugar, com todo o histórico apropriado, e de forma que todos os commits do projeto pai apontassem para os commits do submódulo corretos .

Respostas:


84

Para isolar um subdiretório em seu próprio repositório, use filter-branchem um clone do repositório original:

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

Portanto, nada mais é do que excluir o diretório original e adicionar o submódulo ao projeto pai.


24

Primeiro mude o diretório para a pasta que será um submódulo. Então:

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 wich will be a submodule
git commit -am'deleting folder'
git submodule add repourl folder wich will be a submodule
git commit -am'adding submodule'

11

Eu sei que este é um tópico antigo, mas as respostas aqui eliminam quaisquer commits relacionados em outros branches.

Uma maneira simples de clonar e manter todos os branches e commits extras:

1 - Certifique-se de ter este alias git

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

2 - Clone o remoto, puxe todos os branches, mude o remoto, filtre seu diretório, empurre

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

1

Isso pode ser feito, mas não é simples. Se você procurar git filter-branch, subdirectorye submodule, há alguns write-ups decentes sobre o processo. Basicamente, envolve a criação de dois clones de seu projeto, usando git filter-branchpara remover tudo, exceto um subdiretório em um, e removendo apenas esse subdiretório no outro. Então você pode estabelecer o segundo repositório como um submódulo do primeiro.


0

Status quo

Vamos supor que temos um repositório chamado repo-oldque contém um sub diretório sub que gostaria de converter em uma sub módulo com seu próprio repo repo-sub.

Pretende-se ainda que o repo original repo-oldseja convertido em um repo modificado, repo-newonde todos os commits que tocam no subdiretório existente subdevem agora apontar para os commits correspondentes de nosso repo de submódulo extraído repo-sub.

Vamos mudar

É possível conseguir isso com a ajuda de git filter-branchum processo de duas etapas:

  1. Extração de subdiretório de repo-oldpara repo-sub(já mencionado na resposta aceita )
  2. Substituição de subdiretório de repo-oldpara repo-new(com mapeamento de confirmação adequado)

Observação : Eu sei que esta pergunta é antiga e já foi mencionado que git filter-branchestá meio obsoleta e pode ser perigosa. Mas, por outro lado, pode ajudar outras pessoas com repositórios pessoais que são fáceis de validar após a conversão. Portanto, esteja avisado ! E, por favor, deixe-me saber se há alguma outra ferramenta que faz a mesma coisa sem ser descontinuada e é segura de usar!

Explicarei como realizei as duas etapas no linux com git versão 2.26.2 abaixo. Versões mais antigas podem funcionar até certo ponto, mas isso precisa ser testado.

Para simplificar, vou me restringir ao caso em que há apenas uma masterfilial e um origincontrole remoto no repo original repo-old. Também esteja avisado que eu confio em tags git temporárias com o prefixo temp_que serão removidas no processo. Portanto, se já houver tags com nomes semelhantes, você pode querer ajustar o prefixo abaixo. E, finalmente, esteja ciente de que não testei extensivamente isso e pode haver casos em que a receita falha. Portanto, faça backup de tudo antes de continuar !

Os seguintes fragmentos de bash podem ser concatenados em um grande script que deve ser executado na mesma pasta onde repo-orgreside o repositório . Não é recomendado copiar e colar tudo diretamente em uma janela de comando (embora eu tenha testado isso com sucesso)!

0. Preparação

Variáveis

# 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"

Script de filtro

# 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. Extração de subdiretório

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. Substituição de subdiretório

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"

Observação: se o repo recém-criado repo-newtravar durante git submodule update --init, tente clonar novamente o repositório recursivamente uma vez:

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"

0

Isso faz a conversão no local, você pode recuperá-la como faria com qualquer filtro-branch (eu uso git fetch . +refs/original/*:*).

Tenho um projeto com uma utilsbiblioteca que começou a ser útil em outros projetos e queria dividir seu histórico em submódulos. Não pensei em olhar o SO primeiro, então escrevi meu próprio, ele constrói o histórico localmente, então é um pouco mais rápido, depois do qual se você quiser, pode configurar o .gitmodulesarquivo do comando auxiliar e tal, e enviar os próprios históricos de submódulo em qualquer lugar você quer.

O comando removido está aqui, o documento está nos comentários, no não removido que se segue. Execute-o como seu próprio comando, com subdirset, como subdir=utils git split-submodulese você estivesse dividindo o utilsdiretório. É hacky porque é único, mas testei no subdiretório Documentation na história do Git.

#!/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}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.