Détacher (déplacer) le sous-répertoire dans un référentiel Git séparé

1758

J'ai un référentiel Git qui contient un certain nombre de sous-répertoires. Maintenant, j'ai constaté que l'un des sous-répertoires n'est pas lié à l'autre et doit être détaché dans un référentiel séparé.

Comment puis-je faire cela tout en conservant l'historique des fichiers dans le sous-répertoire?

Je suppose que je pourrais faire un clone et supprimer les parties indésirables de chaque clone, mais je suppose que cela me donnerait l'arborescence complète lors de la vérification d'une ancienne révision, etc. Cela pourrait être acceptable, mais je préférerais pouvoir prétendre que le deux référentiels n'ont pas d'historique partagé.

Juste pour être clair, j'ai la structure suivante:

XYZ/
    .git/
    XY1/
    ABC/
    XY2/

Mais je voudrais plutôt ceci:

XYZ/
    .git/
    XY1/
    XY2/
ABC/
    .git/
    ABC/
matli
la source
7
C'est trivial maintenant avec git filter-branchvoir ma réponse ci-dessous.
jeremyjjbrown
8
@jeremyjjbrown a raison. Ce n'est plus difficile à faire mais il est difficile de trouver la bonne réponse sur Google car toutes les anciennes réponses dominent les résultats.
Agnel Kurian

Réponses:

1228

Mise à jour : Ce processus est si commun, que l'équipe git fait beaucoup plus simple avec un nouvel outil, git subtree. Voir ici: Détacher (déplacer) le sous-répertoire dans un référentiel Git séparé


Vous souhaitez cloner votre référentiel, puis utiliser git filter-branchpour tout marquer, sauf le sous-répertoire que vous souhaitez dans votre nouveau référentiel pour être récupéré.

  1. Pour cloner votre référentiel local:

    git clone /XYZ /ABC
    

    (Remarque: le référentiel sera cloné à l'aide de liens durs, mais ce n'est pas un problème car les fichiers liés durement ne seront pas modifiés en eux-mêmes - de nouveaux seront créés.)

  2. Maintenant, préservons les branches intéressantes que nous voulons réécrire également, puis supprimons l'origine pour éviter d'y pousser et pour nous assurer que les anciens commits ne seront pas référencés par l'origine:

    cd /ABC
    for i in branch1 br2 br3; do git branch -t $i origin/$i; done
    git remote rm origin
    

    ou pour toutes les succursales distantes:

    cd /ABC
    for i in $(git branch -r | sed "s/.*origin\///"); do git branch -t $i origin/$i; done
    git remote rm origin
    
  3. Maintenant, vous pouvez également supprimer les balises qui n'ont aucun rapport avec le sous-projet; vous pouvez également le faire plus tard, mais vous devrez peut-être à nouveau tailler votre repo. Je ne l'ai pas fait et j'ai obtenu un WARNING: Ref 'refs/tags/v0.1' is unchangedpour toutes les balises (car elles n'étaient pas toutes liées au sous-projet); en outre, après la suppression de ces balises, plus d'espace sera récupéré. Apparemment git filter-branch, je devrais pouvoir réécrire d'autres balises, mais je n'ai pas pu le vérifier. Si vous souhaitez supprimer toutes les balises, utilisez git tag -l | xargs git tag -d.

  4. Utilisez ensuite filter-branch et reset pour exclure les autres fichiers, afin qu'ils puissent être élagués. Ajoutons également --tag-name-filter cat --prune-emptyde supprimer les validations vides et de réécrire les balises (notez que cela devra supprimer leur signature):

    git filter-branch --tag-name-filter cat --prune-empty --subdirectory-filter ABC -- --all
    

    ou bien, pour réécrire uniquement la branche HEAD et ignorer les balises et autres branches:

    git filter-branch --tag-name-filter cat --prune-empty --subdirectory-filter ABC HEAD
    
  5. Ensuite, supprimez les reflogs de sauvegarde afin que l'espace puisse être véritablement récupéré (bien que maintenant l'opération soit destructrice)

    git reset --hard
    git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d
    git reflog expire --expire=now --all
    git gc --aggressive --prune=now
    

    et maintenant vous avez un dépôt git local du sous-répertoire ABC avec toute son histoire préservée.

Remarque: Pour la plupart des utilisations, git filter-branchdevrait en effet avoir le paramètre ajouté -- --all. Oui c'est vraiment --space-- all. Ce doit être les derniers paramètres de la commande. Comme l'a découvert Matli, cela conserve les branches et les balises du projet incluses dans le nouveau référentiel.

Edit: diverses suggestions de commentaires ci-dessous ont été incorporées pour s'assurer, par exemple, que le référentiel est réellement réduit (ce qui n'était pas toujours le cas auparavant).

Paul
la source
29
Très bonne réponse. Merci! Et pour vraiment obtenir exactement ce que je voulais, j'ai ajouté "- --all" à la commande filter-branch.
matli
12
Pourquoi as-tu besoin --no-hardlinks? La suppression d'un lien physique n'affectera pas l'autre fichier. Les objets Git sont immuables aussi. Uniquement si vous souhaitez modifier les autorisations de propriétaire / fichier dont vous avez besoin --no-hardlinks.
vdboor
67
Une étape supplémentaire que je recommanderais serait "git remote rm origin". Cela empêcherait les pressions de revenir au référentiel d'origine, si je ne me trompe pas.
Tom
13
Une autre commande à ajouter filter-branchest --prune-emptyde supprimer les validations maintenant vides.
Seth Johnson
8
Comme Paul, je ne voulais pas de balises de projet dans mon nouveau référentiel, donc je n'ai pas utilisé -- --all. J'ai également couru git remote rm origin, et git tag -l | xargs git tag -davant la git filter-branchcommande. Cela a réduit mon .gitrépertoire de 60M à ~ 300K. Notez que j'avais besoin d'exécuter ces deux commandes pour obtenir la réduction de taille.
saltycrane
1321

The Easy Way ™

Il s'avère que c'est une pratique tellement courante et utile que les suzerains de Git ont rendu les choses vraiment faciles, mais vous devez avoir une version plus récente de Git (> = 1.7.11 mai 2012). Voir l' annexe pour savoir comment installer la dernière version de Git. En outre, il y a un exemple réel dans la procédure pas à pas ci-dessous.

  1. Préparer l'ancien repo

    cd <big-repo>
    git subtree split -P <name-of-folder> -b <name-of-new-branch>
    

    Remarque: ne <name-of-folder> doit PAS contenir de caractères de début ou de fin. Par exemple, le dossier nommé subprojectDOIT être transmis en tant que subproject, PAS./subproject/

    Remarque pour les utilisateurs de Windows: lorsque la profondeur de votre dossier est> 1, vous <name-of-folder>devez avoir un séparateur de dossier de style * nix (/). Par exemple, le dossier nommé path1\path2\subprojectDOIT être transmis commepath1/path2/subproject

  2. Créer le nouveau référentiel

    mkdir ~/<new-repo> && cd ~/<new-repo>
    git init
    git pull </path/to/big-repo> <name-of-new-branch>
    
  3. Liez le nouveau dépôt à GitHub ou n'importe où

    git remote add origin <[email protected]:user/new-repo.git>
    git push -u origin master
    
  4. Nettoyage à l'intérieur <big-repo>, si désiré

    git rm -rf <name-of-folder>
    

    Remarque : Cela laisse toutes les références historiques dans le référentiel.Voir l' annexe ci-dessous si vous êtes réellement inquiet d'avoir validé un mot de passe ou si vous devez réduire la taille du fichier de votre .gitdossier.

...

Procédure pas à pas

Ce sont les mêmes étapes que ci - dessus , mais en suivant mes étapes exactes pour mon référentiel au lieu d'utiliser <meta-named-things>.

Voici un projet que j'ai pour implémenter des modules de navigateur JavaScript dans le nœud:

tree ~/node-browser-compat

node-browser-compat
├── ArrayBuffer
├── Audio
├── Blob
├── FormData
├── atob
├── btoa
├── location
└── navigator

Je souhaite diviser un dossier unique btoaen un référentiel Git séparé

cd ~/node-browser-compat/
git subtree split -P btoa -b btoa-only

J'ai maintenant une nouvelle branche, btoa-onlyqui n'a que des commits btoaet je veux créer un nouveau dépôt.

mkdir ~/btoa/ && cd ~/btoa/
git init
git pull ~/node-browser-compat btoa-only

Ensuite, je crée un nouveau dépôt sur GitHub ou Bitbucket, ou autre chose et l'ajoute comme origin

git remote add origin [email protected]:node-browser-compat/btoa.git
git push -u origin master

Bonne journée!

Remarque: Si vous avez créé un référentiel avec un README.md, .gitignoreet LICENSE, vous devrez d'abord tirer:

git pull origin master
git push origin master

Enfin, je veux supprimer le dossier du plus grand dépôt

git rm -rf btoa

...

annexe

Dernier Git sur macOS

Pour obtenir la dernière version de Git en utilisant Homebrew :

brew install git

Dernier Git sur Ubuntu

sudo apt-get update
sudo apt-get install git
git --version

Si cela ne fonctionne pas (vous avez une très ancienne version d'Ubuntu), essayez

sudo add-apt-repository ppa:git-core/ppa
sudo apt-get update
sudo apt-get install git

Si cela ne fonctionne toujours pas, essayez

sudo chmod +x /usr/share/doc/git/contrib/subtree/git-subtree.sh
sudo ln -s \
/usr/share/doc/git/contrib/subtree/git-subtree.sh \
/usr/lib/git-core/git-subtree

Merci à rui.araujo des commentaires.

Effacer votre historique

Par défaut, la suppression de fichiers de Git ne les supprime pas réellement, il confirme simplement qu'ils ne sont plus là. Si vous souhaitez réellement supprimer les références historiques (c'est-à-dire que vous avez un mot de passe validé), vous devez le faire:

git filter-branch --prune-empty --tree-filter 'rm -rf <name-of-folder>' HEAD

Après cela, vous pouvez vérifier que votre fichier ou dossier n'apparaît plus du tout dans l'historique Git

git log -- <name-of-folder> # should show nothing

Cependant, vous ne pouvez pas "pousser" les suppressions vers GitHub et autres. Si vous essayez, vous obtiendrez une erreur et vous devrez le faire git pullavant de pouvoir git push- et vous reviendrez à tout avoir dans votre histoire.

Donc, si vous souhaitez supprimer l'historique de "l'origine" - c'est-à-dire le supprimer de GitHub, Bitbucket, etc. - vous devrez supprimer le dépôt et repousser une copie élaguée du dépôt. Mais attendez - il y a plus ! - Si vous voulez vraiment vous débarrasser d'un mot de passe ou de quelque chose comme ça, vous devrez tailler la sauvegarde (voir ci-dessous).

Rendre .gitplus petit

La commande de suppression de l'historique susmentionnée laisse toujours un tas de fichiers de sauvegarde - parce que Git est trop gentil pour vous aider à ne pas ruiner votre dépôt par accident. Il supprimera éventuellement les fichiers orphelins au fil des jours et des mois, mais il les laissera là pendant un certain temps au cas où vous réaliseriez que vous avez accidentellement supprimé quelque chose que vous ne vouliez pas.

Donc, si vous voulez vraiment vider la corbeille pour réduire la taille du clone d'un dépôt immédiatement, vous devez faire toutes ces choses vraiment étranges:

rm -rf .git/refs/original/ && \
git reflog expire --all && \
git gc --aggressive --prune=now

git reflog expire --all --expire-unreachable=0
git repack -A -d
git prune

Cela dit, je vous recommande de ne pas effectuer ces étapes à moins que vous ne sachiez que vous devez - juste au cas où vous auriez élagué le mauvais sous-répertoire, vous savez? Les fichiers de sauvegarde ne doivent pas être clonés lorsque vous appuyez sur le dépôt, ils seront simplement dans votre copie locale.

Crédit

CoolAJ86
la source
16
git subtreefait toujours partie du dossier «contrib» et n'est pas installé par défaut sur toutes les distributions. github.com/git/git/blob/master/contrib/subtree
onionjake
11
@krlmlr sudo chmod + x /usr/share/doc/git/contrib/subtree/git-subtree.sh sudo ln -s /usr/share/doc/git/contrib/subtree/git-subtree.sh / usr / lib / git-core / git-subtree À activer sur Ubuntu 13.04
rui.araujo
41
Si vous avez poussé un mot de passe vers un référentiel public, vous devez changer le mot de passe, ne pas essayer de le supprimer du dépôt public et espérer que personne ne l'a vu.
Miles Rout
8
Cette solution ne préserve pas l'histoire.
Cœur
18
La commande popdet pushdrend cela plutôt implicite et plus difficile à comprendre ce qu'elle a l'intention de faire ...
jones77
133

La réponse de Paul crée un nouveau référentiel contenant / ABC, mais ne supprime pas / ABC de / XYZ. La commande suivante supprimera / ABC de / XYZ:

git filter-branch --tree-filter "rm -rf ABC" --prune-empty HEAD

Bien sûr, testez-le d'abord dans un référentiel «clone --no-hardlinks» et suivez-le avec les commandes Paul de reset, gc et prune.

pgs
la source
53
faites cela git filter-branch --index-filter "git rm -r -f --cached --ignore-unmatch ABC" --prune-empty HEADet ce sera beaucoup plus rapide. le filtre d'index fonctionne sur l'index tandis que le filtre d'arbre doit tout extraire et tout mettre en scène pour chaque commit .
fmarc
51
dans certains cas, gâcher l'historique du référentiel XYZ est exagéré ... juste un simple "rm -rf ABC; git rm -r ABC; git commit -m'extracted ABC dans son propre référentiel '" fonctionnerait mieux pour la plupart des gens.
Evgeny
2
Vous souhaiterez probablement utiliser -f (force) sur cette commande si vous le faites plus d'une fois, par exemple, pour supprimer deux répertoires après qu'ils ont été séparés. Sinon, vous obtiendrez «Impossible de créer une nouvelle sauvegarde».
Brian Carlton
4
Si vous effectuez la --index-filterméthode, vous pouvez également le faire git rm -q -r -f, de sorte que chaque appel n'imprime pas de ligne pour chaque fichier qu'il supprime.
Eric Naeseth
1
Je suggérerais de modifier la réponse de Paul, uniquement parce que celle-ci est si approfondie.
Erik Aronesty
96

J'ai constaté que pour supprimer correctement l'ancien historique du nouveau référentiel, vous devez faire un peu plus de travail après l' filter-branchétape.

  1. Faites le clone et le filtre:

    git clone --no-hardlinks foo bar; cd bar
    git filter-branch --subdirectory-filter subdir/you/want
    
  2. Supprimez toutes les références à l'ancienne histoire. "Origin" gardait une trace de votre clone, et "original" est l'endroit où la branche de filtre enregistre les vieux trucs:

    git remote rm origin
    git update-ref -d refs/original/refs/heads/master
    git reflog expire --expire=now --all
    
  3. Même maintenant, votre historique peut être coincé dans un fichier pack que fsck ne touchera pas. Déchirez-le en lambeaux, créant un nouveau packfile et supprimant les objets inutilisés:

    git repack -ad
    

Il y a une explication à cela dans le manuel de la branche de filtre .

Josh Lee
la source
3
Je pense git gc --aggressive --prune=nowqu'il manque quelque chose comme ça , non?
Albert
1
@Albert La commande repack s'occupe de cela, et il n'y aurait pas d'objets lâches.
Josh Lee
ouais, git gc --aggressive --prune=nowréduit une grande partie du nouveau repo
Tomek Wyderka
Simple et élégant. Merci!
Marco Pelegrini
40

Edit: script Bash ajouté.

Les réponses données ici n'ont fonctionné que partiellement pour moi; Beaucoup de gros fichiers sont restés dans le cache. Ce qui a finalement fonctionné (après des heures en #git sur freenode):

git clone --no-hardlinks file:///SOURCE /tmp/blubb
cd blubb
git filter-branch --subdirectory-filter ./PATH_TO_EXTRACT  --prune-empty --tag-name-filter cat -- --all
git clone file:///tmp/blubb/ /tmp/blooh
cd /tmp/blooh
git reflog expire --expire=now --all
git repack -ad
git gc --prune=now

Avec les solutions précédentes, la taille du référentiel était d'environ 100 Mo. Celui-ci l'a ramené à 1,7 Mo. Peut-être que cela aide quelqu'un :)


Le script bash suivant automatise la tâche:

!/bin/bash

if (( $# < 3 ))
then
    echo "Usage:   $0 </path/to/repo/> <directory/to/extract/> <newName>"
    echo
    echo "Example: $0 /Projects/42.git first/answer/ firstAnswer"
    exit 1
fi


clone=/tmp/${3}Clone
newN=/tmp/${3}

git clone --no-hardlinks file://$1 ${clone}
cd ${clone}

git filter-branch --subdirectory-filter $2  --prune-empty --tag-name-filter cat -- --all

git clone file://${clone} ${newN}
cd ${newN}

git reflog expire --expire=now --all
git repack -ad
git gc --prune=now
Simon A. Eugster
la source
26

Ce n'est plus si complexe que vous pouvez simplement utiliser la commande git filter-branch sur un clone de votre référentiel pour éliminer les sous-répertoires que vous ne voulez pas, puis pousser vers la nouvelle télécommande.

git filter-branch --prune-empty --subdirectory-filter <YOUR_SUBDIR_TO_KEEP> master
git push <MY_NEW_REMOTE_URL> -f .
jeremyjjbrown
la source
3
Cela a fonctionné comme un charme. YOUR_SUBDIR dans l'exemple ci-dessus est le sous-répertoire que vous souhaitez GARDER, tout le reste sera supprimé
JT Taylor
1
Mises à jour basées sur vos commentaires.
jeremyjjbrown
2
Cela ne répond pas à la question. D'après les documents, il est dit The result will contain that directory (and only that) as its project root.et c'est ce que vous obtiendrez, c'est-à-dire que la structure du projet d'origine n'est pas conservée.
NicBright
2
@NicBright Pouvez-vous illustrer votre problème avec XYZ et ABC comme dans la question, pour montrer ce qui ne va pas?
Adam
@jeremyjjbrown est-il possible de réutiliser le dépôt cloné et de ne pas utiliser un nouveau dépôt, c'est-à-dire ma question ici stackoverflow.com/questions/49269602/…
Qiulang
19

Mise à jour : Le module git-subtree était si utile que l'équipe git l'a intégré dans le noyau et l'a créé git subtree. Voir ici: Détacher (déplacer) le sous-répertoire dans un référentiel Git séparé

git-subtree peut être utile pour cela

http://github.com/apenwarr/git-subtree/blob/master/git-subtree.txt (obsolète)

http://psionides.jogger.pl/2010/02/04/sharing-code-between-projects-with-git-subtree/

DW
la source
1
git-subtree fait maintenant partie de Git, bien qu'il soit dans l'arborescence contrib, donc pas toujours installé par défaut. Je sais qu'il est installé par la formule Homebrew git, mais sans sa page de manuel. apenwarr appelle ainsi sa version obsolète.
echristopherson
19

Voici une petite modification à CoolAJ86 de « The Easy Way ™ » réponse afin de diviser plusieurs sous - dossiers (disons sub1et sub2) dans un nouveau dépôt git.

The Easy Way ™ (plusieurs sous-dossiers)

  1. Préparer l'ancien repo

    pushd <big-repo>
    git filter-branch --tree-filter "mkdir <name-of-folder>; mv <sub1> <sub2> <name-of-folder>/" HEAD
    git subtree split -P <name-of-folder> -b <name-of-new-branch>
    popd
    

    Remarque: ne <name-of-folder> doit PAS contenir de caractères de début ou de fin. Par exemple, le dossier nommé subprojectDOIT être transmis en tant que subproject, PAS./subproject/

    Remarque pour les utilisateurs de Windows: lorsque la profondeur de votre dossier est> 1, vous <name-of-folder>devez avoir un séparateur de dossier de style * nix (/). Par exemple, le dossier nommé path1\path2\subprojectDOIT être passé en tant que path1/path2/subproject. De plus n'utilisez pas de mvcommande mais move.

    Note finale: la différence unique et grande avec la réponse de base est la deuxième ligne du script " git filter-branch..."

  2. Créer le nouveau référentiel

    mkdir <new-repo>
    pushd <new-repo>
    
    git init
    git pull </path/to/big-repo> <name-of-new-branch>
    
  3. Liez le nouveau dépôt à Github ou n'importe où

    git remote add origin <[email protected]:my-user/new-repo.git>
    git push origin -u master
    
  4. Nettoyage, si désiré

    popd # get out of <new-repo>
    pushd <big-repo>
    
    git rm -rf <name-of-folder>
    

    Remarque : Cela laisse toutes les références historiques dans le référentiel.Voir l' annexe dans la réponse d'origine si vous êtes réellement inquiet d'avoir validé un mot de passe ou si vous devez réduire la taille du fichier de votre .gitdossier.

Anthony O.
la source
1
Cela a fonctionné pour moi avec une légère modification. Parce que mes sub1et sub2dossiers n'existaient pas avec la version initiale, je devais modifier mon --tree-filterscript comme suit: "mkdir <name-of-folder>; if [ -d sub1 ]; then mv <sub1> <name-of-folder>/; fi". Pour la deuxième filter-branchcommande, j'ai remplacé <sub1> par <sub2>, omis la création de <name-of-folder>, et inclus -faprès filter-branchpour remplacer l'avertissement d'une sauvegarde existante.
pglezen
Cela ne fonctionne pas si l'un des sous-répertoires a changé au cours de l'historique dans git. Comment résoudre ce problème?
nietras
@nietras voir la réponse de rogerdpack. Il m'a fallu un certain temps pour le trouver après avoir lu et absorbé toutes les informations dans ces autres réponses.
Adam
12

La question d'origine veut que XYZ / ABC / (* fichiers) devienne ABC / ABC / (* fichiers). Après avoir implémenté la réponse acceptée pour mon propre code, j'ai remarqué qu'il change en fait XYZ / ABC / (* fichiers) en ABC / (* fichiers). La page de manuel filter-branch dit même:

Le résultat contiendra ce répertoire (et seulement cela) comme racine de projet . "

En d'autres termes, il promeut le dossier de niveau supérieur "vers le haut" d'un niveau. C'est une distinction importante parce que, par exemple, dans mon histoire, j'avais renommé un dossier de niveau supérieur. En faisant la promotion des dossiers "up" d'un niveau, git perd la continuité au commit où j'ai fait le renommage.

J'ai perdu la contiuité après la branche de filtrage

Ma réponse à la question est alors de faire 2 copies du référentiel et de supprimer manuellement le ou les dossiers que vous souhaitez conserver dans chacun. La page de manuel me soutient avec ceci:

[...] évitez d'utiliser [cette commande] si un simple commit suffit pour résoudre votre problème

MM.
la source
1
J'aime le style de ce graphique. Puis-je demander quel outil vous utilisez?
Slipp D. Thompson
3
Tour pour Mac. J'aime vraiment ça. Cela vaut presque la peine de passer à Mac en soi.
MM.
2
Oui, bien que dans mon cas, mon sous-dossier targetdirait été renommé à un moment donné et l'a git filter-branchsimplement appelé un jour, supprimant toutes les validations effectuées avant le renommage! Choquant, compte tenu de la capacité de Git à suivre de telles choses et même à migrer des morceaux de contenu individuels!
Jay Allen
1
Oh, aussi, si quelqu'un se retrouve dans le même bateau, voici la commande que j'ai utilisée. N'oubliez pas que cela git rmprend plusieurs arguments, il n'y a donc aucune raison de l'exécuter pour chaque fichier / dossier: BYEBYE="dir/subdir2 dir2 file1 dir/file2"; git filter-branch -f --index-filter "git rm -q -r -f --cached --ignore-unmatch $BYEBYE" --prune-empty -- --all
Jay Allen
7

Pour ajouter à la réponse de Paul , j'ai trouvé que pour récupérer de l'espace, je dois pousser HEAD vers un référentiel propre et qui réduit la taille du répertoire .git / objects / pack.

c'est à dire

$ mkdir ... ABC.git
$ cd ... ABC.git
$ git init --bare

Après le pruneau gc, faites également:

$ git push ... ABC.git HEAD

Ensuite, vous pouvez faire

$ git clone ... ABC.git

et la taille de ABC / .git est réduite

En fait, certaines des étapes fastidieuses (par exemple git gc) ne sont pas nécessaires avec le push to clean repository, c'est-à-dire:

$ git clone - no-hardlinks / XYZ / ABC
$ git filtre-branche - sous-répertoire-filtre ABC HEAD
$ git reset --hard
$ git push ... ABC.git HEAD
Case Larsen
la source
6

La bonne façon est maintenant la suivante:

git filter-branch --prune-empty --subdirectory-filter FOLDER_NAME [first_branch] [another_branch]

GitHub a maintenant même un petit article sur de tels cas.

Mais assurez-vous de cloner votre dépôt d'origine dans un répertoire séparé en premier (car cela supprimerait tous les fichiers et autres répertoires et vous devrez probablement travailler avec eux).

Votre algorithme devrait donc être:

  1. clonez votre référentiel distant dans un autre répertoire
  2. en utilisant git filter-branchuniquement des fichiers de gauche dans un sous-répertoire, pousser vers une nouvelle télécommande
  3. créer un commit pour supprimer ce sous-répertoire de votre dépôt distant d'origine
Olexandr Shapovalov
la source
6

Il semble que la plupart (toutes?) Des réponses ici reposent sur une certaine forme de git filter-branch --subdirectory-filteret ses semblables. Cela peut fonctionner "la plupart du temps" cependant pour certains cas, par exemple le cas où vous avez renommé le dossier, ex:

 ABC/
    /move_this_dir # did some work here, then renamed it to

ABC/
    /move_this_dir_renamed

Si vous effectuez un style de filtre git normal pour extraire "move_me_renamed", vous perdrez l'historique des modifications de fichiers survenues de l'arrière quand il était initialement move_this_dir ( ref ).

Il apparaît donc que la seule façon de vraiment garder tout l' historique des modifications (si le vôtre est un cas comme celui-ci), est, par essence, de copier le référentiel (créer un nouveau référentiel, définir cela comme étant l'origine), puis nuke tout le reste et renommez le sous-répertoire en parent comme ceci:

  1. Clonez le projet multi-module localement
  2. Succursales - vérifiez ce qui s'y trouve: git branch -a
  3. Effectuez une vérification pour chaque branche à inclure dans la division pour obtenir une copie locale sur votre poste de travail: git checkout --track origin/branchABC
  4. Faites une copie dans un nouveau répertoire: cp -r oldmultimod simple
  5. Allez dans la nouvelle copie du projet: cd simple
  6. Débarrassez-vous des autres modules qui ne sont pas nécessaires dans ce projet:
  7. git rm otherModule1 other2 other3
  8. Maintenant, seul le sous-répertoire du module cible reste
  9. Débarrassez-vous du sous-répertoire du module pour que la racine du module devienne la nouvelle racine du projet
  10. git mv moduleSubdir1/* .
  11. Supprimez le sous-répertoire relique: rmdir moduleSubdir1
  12. Vérifiez les modifications à tout moment: git status
  13. Créez le nouveau dépôt git et copiez son URL pour y pointer ce projet:
  14. git remote set-url origin http://mygithost:8080/git/our-splitted-module-repo
  15. Vérifiez que c'est bon: git remote -v
  16. Poussez les modifications vers le référentiel distant: git push
  17. Accédez au dépôt à distance et vérifiez que tout est là
  18. Répétez-le pour toute autre branche nécessaire: git checkout branch2

Cela suit le doc github "Fractionner un sous-dossier dans un nouveau référentiel" étapes 6-11 pour pousser le module vers un nouveau référentiel .

Cela ne vous permettra pas d'économiser de l'espace dans votre dossier .git, mais cela préservera tout votre historique des modifications pour ces fichiers, même à travers les renommages. Et cela ne vaut peut-être pas la peine s'il n'y a pas "beaucoup" d'histoire perdue, etc. Mais au moins, vous êtes assuré de ne pas perdre les commits plus anciens!

rogerdpack
la source
1
Trouvé l'aiguille dans la botte de foin de git! Maintenant, je peux garder TOUT mon historique de commit.
Adam
5

Je recommande le guide de GitHub pour diviser les sous-dossiers en un nouveau référentiel . Les étapes sont similaires à la réponse de Paul , mais j'ai trouvé leurs instructions plus faciles à comprendre.

J'ai modifié les instructions afin qu'elles s'appliquent à un référentiel local, plutôt qu'à un hébergé sur GitHub.


Division d'un sous-dossier dans un nouveau référentiel

  1. Ouvrez Git Bash.

  2. Modifiez le répertoire de travail actuel à l'emplacement où vous souhaitez créer votre nouveau référentiel.

  3. Clonez le référentiel qui contient le sous-dossier.

git clone OLD-REPOSITORY-FOLDER NEW-REPOSITORY-FOLDER
  1. Remplacez le répertoire de travail actuel par votre référentiel cloné.

cd REPOSITORY-NAME
  1. Pour filtrer le sous-dossier du reste des fichiers du référentiel, exécutez git filter-branch, en fournissant ces informations:
    • FOLDER-NAME: Le dossier de votre projet à partir duquel vous souhaitez créer un référentiel distinct.
      • Conseil: les utilisateurs de Windows doivent utiliser /pour délimiter les dossiers.
    • BRANCH-NAME: La branche par défaut de votre projet en cours, par exemple, masterou gh-pages.

git filter-branch --prune-empty --subdirectory-filter FOLDER-NAME  BRANCH-NAME 
# Filter the specified branch in your directory and remove empty commits
Rewrite 48dc599c80e20527ed902928085e7861e6b3cbe6 (89/89)
Ref 'refs/heads/BRANCH-NAME' was rewritten
Stevoisiak
la source
Bon article, mais je remarque que le premier paragraphe du document que vous avez lié dit If you create a new clone of the repository, you won't lose any of your Git history or changes when you split a folder into a separate repository.Pourtant, selon les commentaires sur toutes les réponses ici filter-branchet le subtreescript entraînent la perte de l'historique partout où un sous-répertoire a été renommé. Y a-t-il quelque chose qui puisse être fait pour résoudre ce problème?
Adam
J'ai trouvé la solution pour conserver toutes les validations, y compris les précédents renommages / déplacements de répertoires - c'est la réponse de rogerdpack à cette même question.
Adam
Le seul problème est que je ne peux plus utiliser le
dépôt
5

Lors de l'exécution git filter-branchavec une version plus récente de git( 2.22+peut-être?), Il indique d'utiliser ce nouvel outil git-filter-repo . Cet outil m'a certainement simplifié les choses.

Filtrage avec Filter-Repo

Commandes pour créer le XYZréférentiel à partir de la question d'origine:

# create local clone of original repo in directory XYZ
tmp $ git clone [email protected]:user/original.git XYZ

# switch to working in XYZ
tmp $ cd XYZ

# keep subdirectories XY1 and XY2 (dropping ABC)
XYZ $ git filter-repo --path XY1 --path XY2

# note: original remote origin was dropped
# (protecting against accidental pushes overwriting original repo data)

# XYZ $ ls -1
# XY1
# XY2

# XYZ $ git log --oneline
# last commit modifying ./XY1 or ./XY2
# first commit modifying ./XY1 or ./XY2

# point at new hosted, dedicated repo
XYZ $ git remote add origin [email protected]:user/XYZ.git

# push (and track) remote master
XYZ $ git push -u origin master

hypothèses: * le repo XYZ distant était nouveau et vide avant le push

Filtrage et déplacement

Dans mon cas, je voulais également déplacer quelques répertoires pour une structure plus cohérente. Au début, j'ai exécuté cette filter-repocommande simple, suivie par git mv dir-to-rename, mais j'ai trouvé que je pouvais obtenir un historique légèrement "meilleur" en utilisant l' --path-renameoption. Au lieu de voir la dernière modification des 5 hours agofichiers déplacés dans le nouveau référentiel, je vois maintenant last year(dans l'interface utilisateur de GitHub), qui correspond aux heures modifiées dans le référentiel d'origine.

Au lieu de...

git filter-repo --path XY1 --path XY2 --path inconsistent
git mv inconsistent XY3  # which updates last modification time

J'ai finalement couru ...

git filter-repo --path XY1 --path XY2 --path inconsistent --path-rename inconsistent:XY3
Remarques:
  • Je pensais que le blog de Git Rev News expliquait bien le raisonnement derrière la création d'un autre outil de filtrage des repo.
  • J'ai d'abord essayé le chemin de la création d'un sous-répertoire correspondant au nom du référentiel cible dans le référentiel d'origine, puis le filtrage (à l'aide git filter-repo --subdirectory-filter dir-matching-new-repo-name). Cette commande a correctement converti ce sous-répertoire à la racine du référentiel local copié, mais elle a également abouti à un historique des trois validations uniquement nécessaires pour créer le sous-répertoire. (Je n'avais pas réalisé que cela --pathpouvait être spécifié plusieurs fois; évitant ainsi la nécessité de créer un sous-répertoire dans le référentiel source.) Étant donné que quelqu'un s'était engagé dans le référentiel source au moment où j'ai remarqué que je n'avais pas réussi à poursuivre la histoire, j'ai juste utilisé git reset commit-before-subdir-move --hardaprès la clonecommande, et ajouté --forceà la filter-repocommande pour le faire fonctionner sur le clone local légèrement modifié.
git clone ...
git reset HEAD~7 --hard      # roll back before mistake
git filter-repo ... --force  # tell filter-repo the alterations are expected
  • J'étais perplexe sur l'installation car je n'étais pas au courant du modèle d'extension avec git, mais finalement j'ai cloné git-filter-repo et l'ai lié à $(git --exec-path):
ln -s ~/github/newren/git-filter-repo/git-filter-repo $(git --exec-path)
lpearson
la source
1
A voté pour avoir recommandé le nouvel filter-repooutil (que j'ai présenté le mois dernier dans stackoverflow.com/a/58251653/6309 )
VonC
L'utilisation git-filter-repodevrait certainement être l'approche préférée à ce stade. C'est beaucoup, beaucoup plus rapide et plus sûr que git-filter-branch, et des protections contre de nombreux pièges que l'on peut rencontrer lors de la réécriture de l'historique de Git. J'espère que cette réponse attirera davantage l'attention, car c'est celle à laquelle il faut répondre git-filter-repo.
Jeremy Caney
4

J'ai eu exactement ce problème mais toutes les solutions standard basées sur git filter-branch étaient extrêmement lentes. Si vous avez un petit dépôt, ce n'est peut-être pas un problème, c'était pour moi. J'ai écrit un autre programme de filtrage git basé sur libgit2 qui, dans un premier temps, crée des branches pour chaque filtrage du référentiel principal, puis les pousse à nettoyer les référentiels comme étape suivante. Sur mon référentiel (500 Mo 100 000 commits), les méthodes standard de filtrage de branche git ont pris des jours. Mon programme prend quelques minutes pour effectuer le même filtrage.

Il a le nom fabuleux de git_filter et vit ici:

https://github.com/slobobaby/git_filter

sur GitHub.

J'espère que c'est utile à quelqu'un.

slobobaby
la source
4

Utilisez cette commande de filtre pour supprimer un sous-répertoire, tout en préservant vos balises et branches:

git filter-branch --index-filter \
"git rm -r -f --cached --ignore-unmatch DIR" --prune-empty \
--tag-name-filter cat -- --all
cmcginty
la source
qu'est-ce que le chat ici?
rogerdpack
4

Pour ce que ça vaut, voici comment utiliser GitHub sur une machine Windows. Supposons que vous ayez un dépôt cloné dans votre domicile C:\dir1. La structure du répertoire ressemble à ceci: C:\dir1\dir2\dir3. ledir3 répertoire est celui que je veux être un nouveau référentiel séparé.

Github:

  1. Créez votre nouveau référentiel: MyTeam/mynewrepo

Invite bash:

  1. $ cd c:/Dir1
  2. $ git filter-branch --prune-empty --subdirectory-filter dir2/dir3 HEAD
    Renvoyé: Ref 'refs/heads/master' was rewritten(fyi: dir2 / dir3 est sensible à la casse.)

  3. $ git remote add some_name [email protected]:MyTeam/mynewrepo.git
    git remote add origin etc. n'a pas fonctionné, est revenu " remote origin already exists"

  4. $ git push --progress some_name master

James Lawruk
la source
3

Comme je l'ai mentionné ci-dessus , j'ai dû utiliser la solution inverse (supprimer tous les commits ne touchant pas le mien dir/subdir/targetdir) qui semblait assez bien supprimer environ 95% des commits (comme souhaité). Il reste cependant deux petits problèmes.

D'abord , a filter-branchfait un travail de suppression des commits qui introduisent ou modifient du code mais apparemment, les commits de fusion sont sous sa station dans le Gitiverse.

C'est un problème cosmétique avec lequel je peux probablement vivre (dit-il ... reculer lentement les yeux détournés) .

DEUXIÈMEMENT, les quelques commits qui restent sont à peu près TOUS dupliqués! Il me semble avoir acquis une seconde chronologie redondante qui couvre à peu près toute l'histoire du projet. La chose intéressante (que vous pouvez voir sur l'image ci-dessous), c'est que mes trois succursales locales ne sont pas toutes sur la même chronologie (ce qui est certainement la raison pour laquelle elle existe et n'est pas seulement récupérée).

La seule chose que je peux imaginer est que l'une des validations supprimées était, peut-être, la validation de fusion unique qui a filter-branch effectivement été supprimée , et qui a créé la chronologie parallèle car chaque brin non fusionné a pris sa propre copie des validations. ( haussement d'épaules Où est mon TARDiS?) Je suis presque sûr de pouvoir résoudre ce problème, même si j'aimerais vraiment comprendre comment cela s'est produit.

Dans le cas du mergefest-O-RAMA fou, je vais probablement laisser celui-là seul car il s'est si solidement ancré dans mon histoire de commit - me menaçant à chaque fois que je m'approche -, il ne semble pas vraiment causer tous les problèmes non cosmétiques et parce que c'est assez joli dans Tower.app.

Jay Allen
la source
3

La manière la plus simple

  1. installer git splits. Je l'ai créé comme une extension git, basée sur la solution de jkeating .
  2. Fractionner 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 XY1 XY2

  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 un chemin:[email protected]:simpliwp/xyz.git

  4. Poussez vers le nouveau dépôt. #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. Clonez 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
Un avantage de cette méthode par rapport à "The Easy Way" est que la télécommande est déjà configurée pour le nouveau dépôt, vous pouvez donc immédiatement ajouter un sous-arbre. En fait, cela me semble plus facile (même sans git splits)
MM
Props à AndrewD pour avoir publié cette solution. J'ai fourché son dépôt pour le faire fonctionner sur OSX ( github.com/ricardoespsanto/git-splits ) si cela est utile à quelqu'un d'autre
ricardoespsanto
2

Vous pourriez avoir besoin de quelque chose comme "git reflog expire --expire = now --all" avant le garbage collection pour réellement nettoyer les fichiers. git filter-branch supprime simplement les références dans l'historique, mais ne supprime pas les entrées reflog qui contiennent les données. Bien sûr, testez ceci en premier.

Mon utilisation du disque a chuté de façon spectaculaire, même si mes conditions initiales étaient quelque peu différentes. Peut-être que --subdirectory-filter annule ce besoin, mais j'en doute.


la source
2

Découvrez le projet git_split sur https://github.com/vangorra/git_split

Transformez les répertoires git en leurs propres référentiels dans leur propre emplacement. Aucune entreprise drôle de sous-arbre. Ce script prendra un répertoire existant dans votre référentiel git et transformera ce répertoire en un référentiel indépendant. En cours de route, il copiera tout l'historique des modifications du répertoire que vous avez fourni.

./git_split.sh <src_repo> <src_branch> <relative_dir_path> <dest_repo>
        src_repo  - The source repo to pull from.
        src_branch - The branch of the source repo to pull from. (usually master)
        relative_dir_path   - Relative path of the directory in the source repo to split.
        dest_repo - The repo to push to.
vangorra
la source
1

Mettez ceci dans votre gitconfig:

reduce-to-subfolder = !sh -c 'git filter-branch --tag-name-filter cat --prune-empty --subdirectory-filter cookbooks/unicorn HEAD && git reset --hard && git for-each-ref refs/original/ | cut -f 2 | xargs -n 1 git update-ref -d && git reflog expire --expire=now --all && git gc --aggressive --prune=now && git remote rm origin'
plus grossier
la source
1

Je suis sûr que le sous-arbre git est très bien et merveilleux, mais mes sous-répertoires de code géré par git que je voulais déplacer étaient tous en éclipse. Donc, si vous utilisez egit, c'est extrêmement simple. Prenez le projet que vous souhaitez déplacer et équipe-> déconnectez-le, puis équipe-> partagez-le au nouvel emplacement. Par défaut, il tentera d'utiliser l'ancien emplacement du référentiel, mais vous pouvez décocher la sélection existante et choisir le nouvel emplacement pour le déplacer. Salut à tous.

stu
la source
3
La partie "fine et merveilleuse" de la sous-arborescence est que l'histoire de votre sous-répertoire vient pour la balade. Si vous n'avez pas besoin de l'historique, alors votre méthode douloureusement simple est la voie à suivre.
pglezen
0

Vous pouvez facilement essayer https://help.github.com/enterprise/2.15/user/articles/splitting-a-subfolder-out-into-a-new-repository/

Cela a fonctionné pour moi. Les problèmes que j'ai rencontrés dans les étapes ci-dessus sont

  1. Dans cette commande git filter-branch --prune-empty --subdirectory-filter FOLDER-NAME BRANCH-NAME L' BRANCH-NAMEest maître

  2. si la dernière étape échoue lors de la validation en raison d'un problème de protection, suivez - https://docs.gitlab.com/ee/user/project/protected_branches.html

Barath Ravichander
la source
0

J'ai trouvé une solution assez simple, L'idée est de copier le référentiel puis de supprimer simplement la partie inutile. Voilà comment cela fonctionne:

1) Clonez un référentiel que vous souhaitez diviser

git clone [email protected]:testrepo/test.git

2) Déplacer vers le dossier git

cd test/

2) Supprimez les dossiers inutiles et validez-le

rm -r ABC/
git add .
enter code here
git commit -m 'Remove ABC'

3) Supprimer l'historique des formulaires de dossiers inutiles avec BFG

cd ..
java -jar bfg.jar --delete-folders "{ABC}" test
cd test/
git reflog expire --expire=now --all && git gc --prune=now --aggressive

pour multiplier les dossiers, vous pouvez utiliser une virgule

java -jar bfg.jar --delete-folders "{ABC1,ABC2}" metric.git

4) Vérifiez que l'historique ne contient pas les fichiers / dossiers que vous venez de supprimer

git log --diff-filter=D --summary | grep delete

5) Vous avez maintenant un référentiel propre sans ABC, il vous suffit donc de le pousser vers une nouvelle origine

remote add origin [email protected]:username/new_repo
git push -u origin master

C'est ça. Vous pouvez répéter les étapes pour obtenir un autre référentiel,

supprimez simplement XY1, XY2 et renommez XYZ -> ABC à l'étape 3

Vladislav Troyan
la source
Presque parfait ... mais vous avez oublié "git filter-branch --prune-empty" pour supprimer tous les anciens commits qui sont maintenant vides. A faire avant de pousser au master d'origine!
ZettaCircl
Si vous avez fait l'erreur et que vous souhaitez toujours "repousser" après avoir supprimé l'ancien commit vide, effectuez: "git push -u origin master --force-with-lease"
ZettaCircl