¿Convertir una carpeta Git en un submódulo retrospectivamente?

115

Muy a menudo ocurre que estás escribiendo un proyecto de algún tipo, y después de un tiempo queda claro que algún componente del proyecto es realmente útil como un componente independiente (una biblioteca, quizás). Si ha tenido esa idea desde el principio, es muy probable que la mayor parte de ese código esté en su propia carpeta.

¿Hay alguna forma de convertir uno de los subdirectorios en un proyecto Git en un submódulo?

Idealmente, esto sucedería de manera que todo el código en ese directorio se elimine del proyecto principal y el proyecto del submódulo se agregue en su lugar, con todo el historial apropiado, y de manera que todas las confirmaciones del proyecto principal apunten a las confirmaciones del submódulo correctas. .

nada101
fuente
stackoverflow.com/questions/1365541/ ... puede ayudar a algunos :)
Rob Parker
Esto no es parte de la pregunta original, pero lo que sería aún mejor sería una forma de mantener el historial de archivos que comenzaron fuera de la carpeta y se movieron a ella. Por el momento, todas las respuestas pierden todo el historial previo a la mudanza.
naught101
2
El enlace de @ ggll está caído. Aquí hay una copia archivada.
s3cur3

Respuestas:

84

Para aislar un subdirectorio en su propio repositorio, utilice filter-branchen un clon del repositorio original:

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

Entonces no es más que eliminar su directorio original y agregar el submódulo a su proyecto principal.

Knittl
fuente
18
Probablemente también desee git remote rm <name>después de la rama del filtro y luego quizás agregar un nuevo control remoto. Además, si hay archivos ignorados, a git clean -xd -fpuede ser útil
naught101
-- --allse puede reemplazar con el nombre de una rama si el submódulo solo debe extraerse de esta rama.
adius
¿ git clone <your_project> <your_submodule>Solo descarga archivos para your_submodule?
Dominic
@DominicTobias: git clone source destinationsimplemente le dice a Git la ubicación de dónde colocar sus archivos clonados. La magia real para filtrar los archivos de su submódulo ocurre en el filter-branchpaso.
Knittl
filter-branchestá en desuso hoy en día. Puede usar git clone --filter, pero su servidor Git debe estar configurado para permitir el filtrado, de lo contrario obtendrá warning: filtering not recognized by server, ignoring.
Matthias Braun
24

Primero cambie dir a la carpeta que será un submódulo. Luego:

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'
zednight
fuente
9
Esto perderá todo el historial de esa carpeta.
naught101
6
el historial de la carpeta se guardará en el repositorio principal y las nuevas confirmaciones guardarán el historial en el submódulo
zednight
11

Sé que este es un hilo antiguo, pero las respuestas aquí aplastan cualquier confirmación relacionada en otras ramas.

Una forma sencilla de clonar y mantener todas esas ramas y confirmaciones adicionales:

1 - Asegúrate de tener este alias de 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 el control remoto, tire de todas las ramas, cambie el control remoto, filtre su directorio, presione

git clone [email protected]:user/existing-repo.git new-repo
cd new-repo
git clone-branches
git remote rm origin
git remote add origin [email protected]:user/new-repo.git
git remote -v
git filter-branch --subdirectory-filter my_directory/ -- --all
git push --all
git push --tags
oodavid
fuente
1
Mi original tenía un enlace a una esencia en lugar de incrustar el código aquí en SO
oodavid
1

Se puede hacer, pero no es sencillo. Si busca git filter-branch, subdirectoryy submodule, hay algunos escritos decentes sobre el proceso. Básicamente, implica la creación de dos clones de su proyecto, utilizando git filter-branchpara eliminar todo excepto un subdirectorio en uno y eliminando solo ese subdirectorio en el otro. Luego, puede establecer el segundo repositorio como un submódulo del primero.

Twalberg
fuente
0

Status quo

Vamos a suponer que tenemos un repositorio llamada repo-oldque contiene un sub directorio sub que nos gustaría convertir en un sub módulo con su propio repo repo-sub.

Además, se pretende que el repositorio original repo-oldse convierta en un repositorio modificado repo-newdonde todas las confirmaciones que toquen el subdirectorio existente previamente apuntarán suba las confirmaciones correspondientes de nuestro repositorio de submódulos extraído repo-sub.

Cambiemos

Es posible lograr esto con la ayuda de git filter-branchun proceso de dos pasos:

  1. Extracción de subdirectorio de repo-olda repo-sub(ya mencionado en la respuesta aceptada )
  2. Reemplazo de subdirectorio de repo-olda repo-new(con el mapeo de compromiso adecuado)

Observación : Sé que esta pregunta es antigua y ya se ha mencionado que git filter-branchestá en desuso y podría ser peligrosa. Pero, por otro lado, podría ayudar a otros con repositorios personales que son fáciles de validar después de la conversión. ¡Así que ten cuidado ! ¡Y avíseme si hay alguna otra herramienta que haga lo mismo sin ser obsoleta y que sea segura de usar!

Explicaré cómo me di cuenta de ambos pasos en Linux con la versión 2.26.2 de git a continuación. Las versiones anteriores pueden funcionar hasta cierto punto, pero eso debe probarse.

En aras de la simplicidad, me limitaré al caso en el que solo hay una masterrama y un origincontrol remoto en el repositorio original repo-old. También tenga en cuenta que confío en las etiquetas git temporales con el prefijo temp_que se eliminarán en el proceso. Entonces, si ya hay etiquetas con nombres similares, es posible que desee ajustar el prefijo a continuación. Y, por último, tenga en cuenta que no he probado esto de forma exhaustiva y puede haber casos en los que la receta falle. ¡Así que haga una copia de seguridad de todo antes de continuar !

Los siguientes fragmentos de bash se pueden concatenar en un gran script que luego se debe ejecutar en la misma carpeta donde repo-orgreside el repositorio . ¡No se recomienda copiar y pegar todo directamente en una ventana de comando (aunque lo he probado con éxito)!

0. Preparación

Variables

# 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. Extracción de subdirectorios

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. Reemplazo de subdirectorio

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"

Observación: si el repositorio recién creado se repo-newcuelga durante git submodule update --init, intente volver a clonar el repositorio de forma recursiva una vez en su lugar:

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"
PB
fuente
0

Esto hace la conversión en el lugar, puede retroceder como lo haría con cualquier rama de filtro (yo uso git fetch . +refs/original/*:*).

Tengo un proyecto con un utils biblioteca que comenzó a ser útil en otros proyectos y quería dividir su historial en submódulos. No pensé en mirar SO primero, así que escribí el mío, crea el historial localmente, por lo que es un poco más rápido, después de lo cual, si lo desea, puede configurar el .gitmodulesarchivo del comando auxiliar y demás, y enviar los historiales del submódulo a cualquier lugar usted quiere.

El comando despojado en sí está aquí, el documento en los comentarios, en el que sigue sin despojar. Ejecútelo como su propio comando, con subdirset, como subdir=utils git split-submodulesi estuviera dividiendo el utilsdirectorio. Es hacky porque es único, pero lo probé en el subdirectorio Documentation en el historial de 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}
jthill
fuente