Comment extraire un sous-répertoire git et en faire un sous-module?

120

J'ai commencé un projet il y a quelques mois et tout stocké dans un répertoire principal. Dans mon répertoire principal "Project", il y a plusieurs sous-répertoires contenant différentes choses: Project / paper contient un document écrit en LaTeX Project / sourcecode / RailsApp contient mon application rails.

"Project" est GITified et il y a eu beaucoup de commits dans les répertoires "paper" et "RailsApp". Maintenant, comme j'aimerais utiliser cruisecontrol.rb pour mon "RailsApp", je me demande s'il existe un moyen de créer un sous-module à partir de "RailsApp" sans perdre l'historique.

Cœur
la source
2
Aussi une très bonne réponse: stackoverflow.com/questions/359424/…
Rehno Lindeque

Réponses:

123

De nos jours, il existe un moyen beaucoup plus simple de le faire que d'utiliser manuellement git filter-branch: git subtree

Installation

NOTE git-subtree fait maintenant partie de git(si vous installez contrib) à partir de 1.7.11, donc vous l'avez peut-être déjà installé. Vous pouvez vérifier en exécutant git subtree.


Pour installer git-subtree à partir des sources (pour les anciennes versions de git):

git clone https://github.com/apenwarr/git-subtree.git

cd git-subtree
sudo rsync -a ./git-subtree.sh /usr/local/bin/git-subtree

Ou si vous voulez les pages de manuel et tout

make doc
make install

Usage

Divisez un plus grand en petits morceaux:

# Go into the project root
cd ~/my-project

# Create a branch which only contains commits for the children of 'foo'
git subtree split --prefix=foo --branch=foo-only

# Remove 'foo' from the project
git rm -rf ./foo

# Create a git repo for 'foo' (assuming we already created it on github)
mkdir foo
pushd foo
git init
git remote add origin [email protected]:my-user/new-project.git
git pull ../ foo-only
git push origin -u master
popd

# Add 'foo' as a git submodule to `my-project`
git submodule add [email protected]:my-user/new-project.git foo

Pour une documentation détaillée (page de manuel), veuillez lire git-subtree.txt.

Apenwarr
la source
10
git sous-arbre roches!
Simon Woodside
3
Mais l'intérêt de git-subtree n'est-il pas d'éviter d'utiliser des sous-modules? Je veux dire, vous êtes en effet l'auteur de git-subtree (sauf s'il y a une collision de pseudonymes), mais il semble que git-subtree ait changé, même si la commande que vous affichez semble toujours valide. Est-ce que je comprends bien?
Blaisorblade
18
git-subtree fait maintenant partie de git (si vous installez contrib) à partir du 1.7.11
Jeremy
8
Bien git rm -rf ./foosupprime foode HEADmais ne filtre pas l' my-projecthistorique de. Ensuite, git submodule add [email protected]:my-user/new-project.git foone crée fooun sous-module qu'à partir de HEAD. À cet égard, le script filter-branchest supérieur car il permet de réaliser "faire comme si subdir était un sous-module depuis le tout début"
Gregory Pakosz
merci pour cela - git subtree docs juste un peu déroutant, et c'est (pour moi) la chose la plus évidemment utile que je voulais en faire ...
hwjp
38

Checkout git filter-branch .

La Examplessection de la page de manuel montre comment extraire un sous-répertoire dans son propre projet tout en conservant toute son histoire et en supprimant l'historique des autres fichiers / répertoires (exactement ce que vous recherchez).

Pour réécrire le référentiel pour qu'il ressemble à foodir/sa racine de projet et supprimer tout autre historique:

   git filter-branch --subdirectory-filter foodir -- --all

Ainsi, vous pouvez, par exemple, transformer un sous-répertoire de bibliothèque en un référentiel à part entière.
Notez le --qui sépare les filter-branchoptions des options de révision et le --allpour réécrire toutes les branches et balises.

Pat Notz
la source
1
Cela a bien fonctionné pour moi. Le seul inconvénient que j'ai remarqué était que le résultat était une seule branche principale avec tous les commits.
aceofspades
@aceofspades: pourquoi est-ce un inconvénient?
naught101
2
Pour moi, le but de l'extraction des commits d'un dépôt git est que je veux conserver l'historique.
aceofspades
13

Une façon de faire est l'inverse: supprimez tout sauf le fichier que vous souhaitez conserver.

En gros, faites une copie du référentiel, puis utilisez git filter-branchpour supprimer tout sauf le fichier / les dossiers que vous souhaitez conserver.

Par exemple, j'ai un projet dont je souhaite extraire le fichier tvnamer.pyvers un nouveau référentiel:

git filter-branch --tree-filter 'for f in *; do if [ $f != "tvnamer.py" ]; then rm -rf $f; fi; done' HEAD

Cela permet git filter-branch --tree-filterde parcourir chaque commit, d'exécuter la commande et de réitérer le contenu des répertoires résultant. Ceci est extrêmement destructeur (vous ne devriez donc le faire que sur une copie de votre référentiel!), Et peut prendre un certain temps (environ 1 minute sur un référentiel avec 300 commits et environ 20 fichiers)

La commande ci-dessus exécute simplement le script shell suivant sur chaque révision, que vous devrez bien sûr modifier (pour le faire exclure votre sous-répertoire au lieu de tvnamer.py):

for f in *; do
    if [ $f != "tvnamer.py" ]; then
        rm -rf $f;
    fi;
done

Le plus gros problème évident est qu'il laisse tous les messages de validation, même s'ils ne sont pas liés au fichier restant. Le script git-remove-empty-commits corrige ce problème.

git filter-branch --commit-filter 'if [ z$1 = z`git rev-parse $3^{tree}` ]; then skip_commit "$@"; else git commit-tree "$@"; fi'

Vous devez utiliser l' -fargument force run à filter-branchnouveau avec tout ce qui se trouve dans refs/original/(qui essentiellement une sauvegarde)

Bien sûr, ce ne sera jamais parfait, par exemple si vos messages de commit mentionnent d'autres fichiers, mais c'est à peu près aussi proche qu'un git current le permet (pour autant que je sache de toute façon).

Encore une fois, ne l'exécutez que sur une copie de votre référentiel! - mais en résumé, pour supprimer tous les fichiers sauf "thisismyfilename.txt":

git filter-branch --tree-filter 'for f in *; do if [ $f != "thisismyfilename.txt" ]; then rm -rf $f; fi; done' HEAD
git filter-branch -f --commit-filter 'if [ z$1 = z`git rev-parse $3^{tree}` ]; then skip_commit "$@"; else git commit-tree "$@"; fi'
dbr
la source
4
git filter-brancha (de nos jours?) une option intégrée pour supprimer les commits vides, à savoir --prune-empty. Un meilleur guide pour se git filter-branchtrouve dans les réponses à cette question: stackoverflow.com/questions/359424/…
Blaisorblade
4

Les réponses CoolAJ86 et apenwarr sont très similaires. J'ai fait des allers-retours entre les deux en essayant de comprendre les éléments qui manquaient à l'un ou l'autre. Voici une combinaison d'entre eux.

Naviguez d'abord dans Git Bash jusqu'à la racine du dépôt git à fractionner. Dans mon exemple ici, c'est~/Documents/OriginalRepo (master)

# move the folder at prefix to a new branch
git subtree split --prefix=SubFolderName/FolderToBeNewRepo --branch=to-be-new-repo

# create a new repository out of the newly made branch
mkdir ~/Documents/NewRepo
pushd ~/Documents/NewRepo
git init
git pull ~/Documents/OriginalRepo to-be-new-repo

# upload the new repository to a place that should be referenced for submodules
git remote add origin [email protected]:myUsername/newRepo.git
git push -u origin master
popd

# replace the folder with a submodule
git rm -rf ./SubFolderName/FolderToBeNewRepo
git submodule add [email protected]:myUsername/newRepo.git SubFolderName/FolderToBeNewRepo
git branch --delete --force to-be-new-repo

Vous trouverez ci-dessous une copie de ce qui précède avec les noms personnalisables remplacés et utilisant plutôt https. Le dossier racine est maintenant~/Documents/_Shawn/UnityProjects/SoProject (master)

# move the folder at prefix to a new branch
git subtree split --prefix=Assets/SoArchitecture --branch=so-package

# create a new repository out of the newly made branch
mkdir ~/Documents/_Shawn/UnityProjects/SoArchitecture
pushd ~/Documents/_Shawn/UnityProjects/SoArchitecture
git init
git pull ~/Documents/_Shawn/UnityProjects/SoProject so-package

# upload the new repository to a place that should be referenced for submodules
git remote add origin https://github.com/Feddas/SoArchitecture.git
git push -u origin master
popd

# replace the folder with a submodule
git rm -rf ./Assets/SoArchitecture
git submodule add https://github.com/Feddas/SoArchitecture.git
git branch --delete --force so-package
ShawnFeatherly
la source
3

Si vous souhaitez transférer un sous-ensemble de fichiers vers un nouveau référentiel mais conserver l'historique, vous allez essentiellement vous retrouver avec un historique complètement nouveau. La façon dont cela fonctionnerait est essentiellement la suivante:

  1. Créez un nouveau référentiel.
  2. Pour chaque révision de votre ancien référentiel, fusionnez les modifications apportées à votre module dans le nouveau référentiel. Cela créera une "copie" de votre historique de projet existant.

Il devrait être assez simple d'automatiser cela si cela ne vous dérange pas d'écrire un petit script mais poilu. Simple, oui, mais aussi douloureux. Les gens ont réécrit l'histoire dans Git dans le passé, vous pouvez faire une recherche pour cela.

Alternativement: clonez le référentiel et supprimez le papier dans le clone, supprimez l'application dans l'original. Cela prendrait une minute, il est garanti de fonctionner, et vous pouvez revenir à des choses plus importantes que d'essayer de purifier votre histoire git. Et ne vous inquiétez pas de l'espace disque occupé par les copies redondantes de l'historique.

Dietrich Epp
la source