Détachez de nombreux sous-répertoires dans un nouveau référentiel Git séparé

135

Cette question est basée sur le sous-répertoire Detach dans un référentiel Git séparé

Au lieu de détacher un seul sous-répertoire, je veux en détacher un couple. Par exemple, mon arborescence de répertoires actuelle ressemble à ceci:

/apps
  /AAA
  /BBB
  /CCC
/libs
  /XXX
  /YYY
  /ZZZ

Et j'aimerais plutôt ceci:

/apps
  /AAA
/libs
  /XXX

L' --subdirectory-filterargument to git filter-branchne fonctionnera pas car il supprime tout sauf le répertoire donné la première fois qu'il est exécuté. Je pensais que l'utilisation de l' --index-filterargument pour tous les fichiers indésirables fonctionnerait (bien que fastidieux), mais si j'essaie de l'exécuter plus d'une fois, j'obtiens le message suivant:

Cannot create a new backup.
A previous backup already exists in refs/original/
Force overwriting the backup with -f

Des idées? TIA

prisonnierjohn
la source

Réponses:

155

Au lieu d'avoir à gérer un sous-shell et d'utiliser ext glob (comme l'a suggéré kynan), essayez cette approche beaucoup plus simple:

git filter-branch --index-filter 'git rm --cached -qr --ignore-unmatch -- . && git reset -q $GIT_COMMIT -- apps/AAA libs/XXX' --prune-empty -- --all

Comme mentionné par void.pointer dans son commentaire , cela supprimera tout sauf apps/AAAet libs/XXXdu référentiel actuel.

Prune vide les commits de fusion

Cela laisse derrière lui de nombreuses fusions vides. Ceux-ci peuvent être supprimés par un autre passage comme décrit par raphinesse dans sa réponse :

git filter-branch --prune-empty --parent-filter \
'sed "s/-p //g" | xargs -r git show-branch --independent | sed "s/\</-p /g"'

⚠️ Attention : ce qui précède doit utiliser la version GNU de sed, xargssinon il supprimerait tous les commits en cas d' xargséchec. brew install gnu-sed findutilspuis utilisez gsedet gxargs:

git filter-branch --prune-empty --parent-filter \
'gsed "s/-p //g" | gxargs git show-branch --independent | gsed "s/\</-p /g"' 
David Smiley
la source
4
de plus, l'indicateur --ignore-unmatch devrait être passé à git rm, il a échoué pour le tout premier commit pour moi sinon (le dépôt a été créé avec git svn clone dans mon cas)
Pontomedon
8
En supposant que vous ayez des balises dans le mix, vous devriez probablement ajouter --tag-name-filter catà vos paramètres
Yonatan
16
Pourriez-vous ajouter des informations supplémentaires expliquant ce que fait cette longue commande?
Burhan Ali
4
Je suis agréablement surpris que cela fonctionne parfaitement sous Windows en utilisant git bash, ouf!
Dai
3
@BurhanAli Pour chaque commit de l'historique, il supprime tous les fichiers sauf ceux que vous souhaitez conserver. Lorsque tout est terminé, il ne vous reste que la partie de l'arborescence que vous avez spécifiée, avec uniquement cet historique.
void.pointer
39

Étapes manuelles avec de simples commandes git

Le plan est de diviser les répertoires individuels en ses propres dépôts, puis de les fusionner. Les étapes manuelles suivantes n'utilisaient pas de scripts de geek mais des commandes faciles à comprendre et pourraient aider à fusionner N sous-dossiers supplémentaires dans un autre référentiel unique.

Diviser

Supposons que votre dépôt d'origine est: original_repo

1 - Applications fractionnées:

git clone original_repo apps-repo
cd apps-repo
git filter-branch --prune-empty --subdirectory-filter apps master

2 - Diviser les bibliothèques

git clone original_repo libs-repo
cd libs-repo
git filter-branch --prune-empty --subdirectory-filter libs master

Continuez si vous avez plus de 2 dossiers. Vous allez maintenant avoir deux nouveaux référentiels git temporaires.

Conquérir en fusionnant des applications et des bibliothèques

3 - Préparez le tout nouveau repo:

mkdir my-desired-repo
cd my-desired-repo
git init

Et vous devrez faire au moins un commit. Si les trois lignes suivantes doivent être ignorées, votre premier dépôt apparaîtra immédiatement sous la racine de votre dépôt:

touch a_file_and_make_a_commit # see user's feedback
git add a_file_and_make_a_commit
git commit -am "at least one commit is needed for it to work"

Avec le fichier temporaire validé, la mergecommande dans la section suivante s'arrêtera comme prévu.

En vous basant sur les commentaires des utilisateurs, au lieu d'ajouter un fichier aléatoire comme a_file_and_make_a_commit, vous pouvez choisir d'ajouter un .gitignore, ou README.mdetc.

4 - Fusionner d'abord le repo des applications:

git remote add apps-repo ../apps-repo
git fetch apps-repo
git merge -s ours --no-commit apps-repo/master # see below note.
git read-tree --prefix=apps -u apps-repo/master
git commit -m "import apps"

Vous devriez maintenant voir le répertoire des applications dans votre nouveau référentiel. git logdevrait afficher tous les messages de validation historiques pertinents.

Remarque: comme Chris l'a noté ci-dessous dans les commentaires, pour la version plus récente (> = 2.9) de git, vous devez spécifier --allow-unrelated-historiesavecgit merge

5 - Fusionnez ensuite libs repo de la même manière:

git remote add libs-repo ../libs-repo
git fetch libs-repo
git merge -s ours --no-commit libs-repo/master # see above note.
git read-tree --prefix=libs -u libs-repo/master
git commit -m "import libs"

Continuez si vous avez plus de 2 dépôts à fusionner.

Référence: Fusionner un sous-répertoire d'un autre référentiel avec git

chfw
la source
4
Depuis git 2.9, vous devez utiliser --allow-unrelated-histories sur les commandes de fusion. Sinon, cela semble avoir bien fonctionné pour moi.
Chris du
1
Génie! Merci beaucoup pour cela. Les premières réponses que j'avais examinées, en utilisant un filtre d'arbre sur un très grand référentiel, avaient git prédisant qu'il fallait plus de 26 heures pour terminer les réécritures git. Beaucoup plus satisfait de cette approche simple mais répétable et ont réussi à déplacer 4 sous-dossiers dans un nouveau dépôt avec tout l'historique de validation attendu.
shuttsy
1
Vous pouvez utiliser la première commettras pour un « engagement initial » , qui ajoute .gitignoreet README.mdfichiers.
Jack Miller
2
Malheureusement, cette approche semble casser l'historique de suivi des fichiers ajoutés à l' git merge .. git read-treeétape, car elle les enregistre comme des fichiers nouvellement ajoutés et tous mes git guis ne font pas la connexion à leurs commits précédents.
Dai
1
@ksadjad, Aucune idée, pour être honnête. Le point central de la fusion manuelle est de sélectionner les répertoires pour former le nouveau dépôt et de conserver leurs historiques de validation. Je ne sais pas comment gérer une telle situation où un commit met des fichiers dans dirA, dirB, dirDrop et seuls dirA et dirB sont choisis pour le nouveau dépôt, comment l'historique des commit devrait-il se rapporter à l'original.
chfw
27

Pourquoi voudriez-vous courir filter-branchplus d'une fois? Vous pouvez tout faire en un seul balayage, donc pas besoin de le forcer (notez que vous devez extglobactiver dans votre shell pour que cela fonctionne):

git filter-branch --index-filter "git rm -r -f --cached --ignore-unmatch $(ls -xd apps/!(AAA) libs/!(XXX))" --prune-empty -- --all

Cela devrait éliminer tous les changements dans les sous-répertoires indésirables et conserver toutes vos branches et commits (à moins qu'ils n'affectent uniquement les fichiers dans les sous-répertoires élagués, en vertu de --prune-empty) - aucun problème avec les commits en double, etc.

Après cette opération, les répertoires indésirables seront répertoriés comme non suivis par git status.

Le $(ls ...)st nécessaire extglobest évalué par votre shell au lieu du filtre d'index, qui utilise le shbuiltin eval(où extglobn'est pas disponible). Voir Comment activer les options du shell dans git? pour plus de détails à ce sujet.

kynan
la source
1
Idée intéressante. J'ai un problème similaire mais je n'ai pas pu le faire fonctionner, voir stackoverflow.com/questions/8050687/…
manol
C'est à peu près ce dont j'avais besoin, même si j'avais saupoudré à la fois des fichiers et des dossiers dans mon dépôt ... Merci :)
notlesh
1
hm. même avec extglob activé, j'obtiens une erreur près de ma parenthèse: erreur de syntaxe près du jeton inattendu `('ma commande ressemble à: git filter-branch -f --index-filter" git rm -r -f --cached - -ignore-unmatch src / css / themes /! (some_theme *) "--prune-empty - --all an ls avec src / css / themes /! (some_theme *) renvoie tous les autres thèmes ainsi extglob apparaîtra be working ...
robdodson
2
@MikeGraf Je ne pense pas que cela donnera le résultat souhaité: échapper correspondrait à un littéral "!" etc. sur votre chemin.
kynan
1
La réponse (plus récente) de @ david-smiley utilise une approche très similaire, mais a l'avantage de s'appuyer exclusivement sur des gitcommandes, et n'est donc pas aussi sensible aux différences d' lsinterprétation de l 'entre les systèmes d'exploitation, comme l'a découvert @Bae.
Jeremy Caney
20

Répondre à ma propre question ici ... après de nombreux essais et erreurs.

J'ai réussi à le faire en utilisant une combinaison de git subtreeet git-stitch-repo. Ces instructions sont basées sur:

Tout d'abord, j'ai sorti les répertoires que je voulais conserver dans leur propre référentiel séparé:

cd origRepo
git subtree split -P apps/AAA -b aaa
git subtree split -P libs/XXX -b xxx

cd ..
mkdir aaaRepo
cd aaaRepo
git init
git fetch ../origRepo aaa
git checkout -b master FETCH_HEAD

cd ..
mkdir xxxRepo
cd xxxRepo
git init
git fetch ../origRepo xxx
git checkout -b master FETCH_HEAD

J'ai ensuite créé un nouveau référentiel vide et y ai importé / assemblé les deux derniers:

cd ..
mkdir newRepo
cd newRepo
git init
git-stitch-repo ../aaaRepo:apps/AAA ../xxxRepo:libs/XXX | git fast-import

Cela crée deux branches master-Aet master-B, chacune contenant le contenu de l'un des dépôts cousus. Pour les combiner et nettoyer:

git checkout master-A
git pull . master-B
git checkout master
git branch -d master-A 
git branch -d master-B

Maintenant, je ne sais pas trop comment / quand cela se produit, mais après le premier checkoutet le pull, le code fusionne comme par magie dans la branche master (toute idée de ce qui se passe ici est appréciée!)

Tout semble avoir fonctionné comme prévu, sauf que si je regarde l' newRepohistorique des commit, il y a des doublons lorsque le changeset a affecté à la fois apps/AAAet libs/XXX. S'il existe un moyen de supprimer les doublons, ce serait parfait.

prisonnierjohn
la source
Des outils soignés que vous avez trouvés ici. Aperçu de "checkout": "git pull" est le même que "git fetch && git merge". La partie «chercher» est inoffensive puisque vous «récupérez localement». Je pense donc que cette commande d'extraction est la même que "git merge master-B", ce qui est un peu plus évident. Voir kernel.org/pub/software/scm/git/docs/git-pull.html
phord
1
Malheureusement, l'outil git-stitch-repo est cassé en raison de mauvaises dépendances de nos jours.
Henrik
@Henrik Quel problème avez-vous rencontré exactement? Cela fonctionne pour moi, même si j'ai dû ajouter export PERL5LIB="$PERL5LIB:/usr/local/git/lib/perl5/site_perl/"à ma configuration bash pour qu'elle puisse trouver Git.pm. Ensuite, je l'ai installé avec cpan.
Il est possible d'utiliser git subtree addpour effectuer cette tâche. Voir stackoverflow.com/a/58253979/1894803
laconbass
7

J'ai écrit un filtre git pour résoudre exactement ce problème. Il porte le nom fantastique de git_filter et se trouve sur github ici:

https://github.com/slobobaby/git_filter

Il est basé sur l'excellent libgit2.

J'avais besoin de diviser un grand référentiel avec de nombreux commits (~ 100000) et les solutions basées sur git filter-branch ont pris plusieurs jours à s'exécuter. git_filter prend une minute pour faire la même chose.

slobobaby
la source
7

Utiliser l'extension git 'git splits'

git splitsest un script bash qui est un wrapper autour git branch-filterque j'ai créé en tant qu'extension git, basé sur la solution de jkeating .

Il a été fait exactement pour cette situation. Pour votre erreur, essayez d'utiliser l' git splits -foption pour forcer la suppression de la sauvegarde. Comme il git splitsfonctionne sur une nouvelle branche, il ne réécrira pas votre branche actuelle, donc la sauvegarde est superflue. Voir le readme pour plus de détails et assurez-vous de l'utiliser sur une copie / clone de votre dépôt (juste au cas où!) .

  1. installer git splits.
  2. Divisez les répertoires en une branche locale #change into your repo's directory cd /path/to/repo #checkout the branch git checkout XYZ
    #split multiple directories into new branch XYZ git splits -b XYZ apps/AAA libs/ZZZ

  3. Créez un dépôt vide quelque part. Nous supposerons que nous avons créé un dépôt vide appelé xyzsur GitHub qui a le chemin:[email protected]:simpliwp/xyz.git

  4. Poussez vers le nouveau repo. #add a new remote origin for the empty repo so we can push to the empty repo on GitHub git remote add origin_xyz [email protected]:simpliwp/xyz.git #push the branch to the empty repo's master branch git push origin_xyz XYZ:master

  5. Cloner le référentiel distant nouvellement créé dans un nouveau répertoire local
    #change current directory out of the old repo cd /path/to/where/you/want/the/new/local/repo #clone the remote repo you just pushed to git clone [email protected]:simpliwp/xyz.git

AndrewD
la source
Il ne semble pas possible d'ajouter des fichiers à la division et de les mettre à jour plus tard, non?
Alex
Cela semble lent à s'exécuter sur mon repo avec des tonnes de commits
Shinta Smith
git-split semble utiliser git --index filter qui est extrêmement lent par rapport à --subdirectory-filter. Pour certains dépôts, cela peut encore être une option viable, mais pour les gros dépôts (plusieurs gigaoctets, commits à 6 chiffres) --index-filter prend effectivement des semaines à s'exécuter, même sur du matériel cloud dédié.
Jostein Kjønigsen
6
git clone [email protected]:thing.git
cd thing
git fetch
for originBranch in `git branch -r | grep -v master`; do
    branch=${originBranch:7:${#originBranch}}
    git checkout $branch
done
git checkout master

git filter-branch --index-filter 'git rm --cached -qr --ignore-unmatch -- . && git reset -q $GIT_COMMIT -- dir1 dir2 .gitignore' --prune-empty -- --all

git remote set-url origin [email protected]:newthing.git
git push --all
Richard Barraclough
la source
La lecture de tous les autres commentaires m'a mis sur la bonne voie. Cependant, votre solution fonctionne. Il importe toutes les branches et fonctionne avec plusieurs répertoires! Génial!
jschober
1
La forboucle vaut la peine d'être reconnue, car d'autres réponses similaires ne l'incluent pas. Si vous ne disposez pas d'une copie locale de chaque branche dans votre clone, vous ne les comptez pas filter-branchdans le cadre de sa réécriture, ce qui pourrait potentiellement exclure les fichiers introduits dans d'autres branches, mais pas encore fusionnés avec votre branche actuelle. (Bien que cela vaut également la peine de faire un git fetchsur toutes les branches que vous avez déjà vérifiées pour vous assurer qu'elles restent à jour.)
Jeremy Caney
5

Une solution simple: git-filter-repo

J'ai eu un problème similaire et, après avoir examiné les différentes approches répertoriées ici, j'ai découvert git-filter-repo . Il est recommandé comme alternative à git-filter-branch dans la documentation officielle de git ici .

Pour créer un nouveau référentiel à partir d'un sous-ensemble de répertoires dans un référentiel existant, vous pouvez utiliser la commande:

git filter-repo --path <file_to_remove>

Filtrez plusieurs fichiers / dossiers en les chaînant:

git filter-repo --path keepthisfile --path keepthisfolder/

Donc, pour répondre à la question d'origine , avec git-filter-repo, vous auriez juste besoin de la commande suivante:

git filter-repo --path apps/AAA/ --path libs/XXX/
elmo
la source
C'est certainement une excellente réponse. Le problème avec toutes les autres solutions est que je n'arrivais pas à extraire le contenu de TOUTES les branches d'un répertoire. Cependant, git filter-repo a récupéré le dossier de toutes les branches et réécrit parfaitement l'historique, comme nettoyer l'arbre entier de tout ce dont je n'avais pas besoin.
Teodoro
3

Ouais. Forcer le remplacement de la sauvegarde en utilisant l' -findicateur sur les appels suivants à filter-branchpour remplacer cet avertissement. :) Sinon, je pense que vous avez la solution (c'est-à-dire éradiquer un répertoire indésirable à la fois avec filter-branch).

Jakob Borg
la source
-4

Supprimez la sauvegarde présente sous le répertoire .git dans refs / original comme le suggère le message. Le répertoire est masqué.

user5200576
la source