Convertir un dossier Git en sous-module rétrospectivement?

115

Très souvent, vous écrivez un projet quelconque, et après un certain temps, il devient clair qu'un composant du projet est en fait utile en tant que composant autonome (une bibliothèque, peut-être). Si vous avez eu cette idée dès le début, il y a de fortes chances que la plupart de ce code se trouve dans son propre dossier.

Existe-t-il un moyen de convertir l'un des sous-répertoires d'un projet Git en sous-module?

Idéalement, cela se produirait de telle sorte que tout le code de ce répertoire soit supprimé du projet parent et que le projet de sous-module soit ajouté à sa place, avec tout l'historique approprié, et de sorte que tous les validations du projet parent pointent vers le sous-module correct. .

rien101
la source
stackoverflow.com/questions/1365541/… peut aider certains :)
Rob Parker
Cela ne fait pas partie de la question initiale, mais ce qui serait encore plus cool serait un moyen de conserver l'historique des fichiers qui ont commencé en dehors du dossier et y ont été déplacés. Pour le moment, toutes les réponses perdent toute l'histoire avant le déménagement.
naught101
2
Le lien de @ ggll est en panne. Voici une copie archivée.
s3cur3

Réponses:

84

Pour isoler un sous-répertoire dans son propre référentiel, utilisez filter-branchsur un clone du référentiel d'origine:

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

Ce n'est alors rien de plus que de supprimer votre répertoire d'origine et d'ajouter le sous-module à votre projet parent.

tricot
la source
18
Vous voudrez probablement aussi git remote rm <name>après la branche de filtre, puis peut-être ajouter une nouvelle télécommande. De plus, s'il y a des fichiers ignorés, un git clean -xd -fpeut être utile
rien101
-- --allpeut être remplacé par le nom d'une branche si le sous-module ne doit être extrait que de cette branche.
adius
git clone <your_project> <your_submodule>Télécharge- t-il uniquement des fichiers pour votre_submodule?
Dominic
@DominicTobias: git clone source destinationindique simplement à Git l'emplacement où placer vos fichiers clonés. La magie réelle pour filtrer les fichiers de votre sous-module se produit alors dans l' filter-branchétape.
knittl
filter-branchest obsolète de nos jours. Vous pouvez utiliser git clone --filter, mais votre serveur Git doit être configuré pour autoriser le filtrage, sinon vous obtiendrez warning: filtering not recognized by server, ignoring.
Matthias Braun
24

Commencez par changer dir en dossier qui sera un sous-module. Ensuite:

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
la source
9
Cela perdra toute l'histoire de ce dossier.
naught101
6
L'historique du dossier sera enregistré dans le référentiel principal et les nouveaux commits enregistreront l'historique dans le sous
zednight
11

Je sais que c'est un vieux fil, mais les réponses ici écrasent tous les commits liés dans d'autres branches.

Un moyen simple de cloner et de conserver toutes ces branches et commits supplémentaires:

1 - Assurez-vous d'avoir cet 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 - Clonez la télécommande, tirez toutes les branches, changez la télécommande, filtrez votre répertoire, poussez

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
la source
1
Mon original avait un lien vers l'essentiel au lieu d'incorporer le code ici sur SO
oodavid
1

Cela peut être fait, mais ce n'est pas simple. Si vous recherchez git filter-branch, subdirectoryet submodule, il y a des comptes rendus décents sur le processus. Cela implique essentiellement la création de deux clones de votre projet, en utilisant git filter-branchpour supprimer tout sauf un sous-répertoire dans l'un, et en supprimant uniquement ce sous-répertoire dans l'autre. Ensuite, vous pouvez établir le deuxième référentiel en tant que sous-module du premier.

Twalberg
la source
0

Status Quo

Supposons que nous ayons un référentiel appelé repo-oldqui contient un sous- répertoire sub que nous aimerions convertir en un sous- module avec son propre dépôt repo-sub.

Il est en outre prévu que le dépôt d'origine repo-oldsoit converti en un dépôt modifié repo-newoù tous les commits touchant le sous sub- répertoire existant précédemment pointeront maintenant vers les commits correspondants de notre repo de sous-module extrait repo-sub.

Changeons

Il est possible d'y parvenir à l'aide d' git filter-branchun processus en deux étapes:

  1. Extraction de sous-répertoire de repo-oldvers repo-sub(déjà mentionné dans la réponse acceptée )
  2. Remplacement du sous-répertoire de repo-oldà repo-new(avec un mappage de validation approprié)

Remarque : je sais que cette question est ancienne et qu'il a déjà été mentionné qu'elle git filter-branchest un peu obsolète et pourrait être dangereuse. Mais d'un autre côté, cela pourrait aider les autres avec des référentiels personnels faciles à valider après la conversion. Alors soyez prévenu ! Et s'il vous plaît laissez-moi savoir s'il existe un autre outil qui fait la même chose sans être obsolète et est sûr à utiliser!

Je vais vous expliquer comment j'ai réalisé les deux étapes sous linux avec la version 2.26.2 de git ci-dessous. Les versions plus anciennes peuvent fonctionner dans une certaine mesure, mais cela doit être testé.

Par souci de simplicité, je me limiterai au cas où il y a juste une masterbranche et une origintélécommande dans le repo d'origine repo-old. Sachez également que je compte sur des balises git temporaires avec le préfixe temp_qui seront supprimées dans le processus. Donc, s'il y a déjà des balises nommées de manière similaire, vous voudrez peut-être ajuster le préfixe ci-dessous. Et enfin, sachez que je n'ai pas testé cela de manière approfondie et qu'il peut y avoir des cas secondaires où la recette échoue. Veuillez donc tout sauvegarder avant de continuer !

Les extraits de code bash suivants peuvent être concaténés en un seul gros script qui doit ensuite être exécuté dans le même dossier où se trouve le dépôt repo-org. Il n'est pas recommandé de tout copier et coller directement dans une fenêtre de commande (même si j'ai testé cela avec succès)!

0. Préparation

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 filtrage

# 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. Extraction de sous-répertoires

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. Remplacement de sous-répertoire

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"

Remarque: si le référentiel nouvellement créé se repo-newbloque pendant, git submodule update --initessayez de re-cloner le référentiel de manière récursive une fois à la place:

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
la source
0

Cela fait la conversion sur place, vous pouvez la sauvegarder comme vous le feriez pour n'importe quelle branche de filtre (j'utilise git fetch . +refs/original/*:*).

J'ai un projet avec une utilsbibliothèque qui a commencé à être utile dans d'autres projets, et je voulais diviser son histoire en sous-modules. Je n'ai pas pensé à regarder SO d'abord, alors j'ai écrit le mien, il construit l'historique localement donc c'est un peu plus rapide, après quoi si vous le souhaitez, vous pouvez configurer le .gitmodulesfichier de la commande d'assistance et autres, et pousser les historiques des sous-modules eux-mêmes n'importe où tu veux.

La commande dépouillée elle-même est ici, la documentation dans les commentaires, dans la commande non rayée qui suit. Exécutez-le comme sa propre commande, avec subdirset, comme subdir=utils git split-submodulesi vous divisez le utilsrépertoire. C'est hacky parce que c'est un one-off, mais je l'ai testé sur le sous-répertoire Documentation de l'historique 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
la source