Comment fusionnez-vous deux référentiels Git?

1622

Considérez le scénario suivant:

J'ai développé un petit projet expérimental A dans son propre référentiel Git. Il a maintenant mûri et j'aimerais que A fasse partie d'un plus grand projet B, qui a son propre grand référentiel. Je voudrais maintenant ajouter A comme sous-répertoire de B.

Comment puis-je fusionner A en B, sans perdre l'historique de quelque côté que ce soit?

static_rtti
la source
8
Si vous essayez simplement de combiner deux référentiels en un seul, sans avoir besoin de conserver les deux référentiels, jetez un œil à cette question: stackoverflow.com/questions/13040958/…
Flimm
Pour fusionner git repo dans un répertoire personnalisé avec sauvegarde de tous les comits, utilisez stackoverflow.com/a/43340714/1772410
Andrey Izman

Réponses:

437

Une seule branche d'un autre référentiel peut être facilement placée sous un sous-répertoire conservant son historique. Par exemple:

git subtree add --prefix=rails git://github.com/rails/rails.git master

Cela apparaîtra comme un commit unique où tous les fichiers de la branche principale de Rails sont ajoutés dans le répertoire "rails". Cependant, le titre du commit contient une référence à l'ancien arbre d'historique:

Ajouter 'rails /' à partir de la validation <rev>

Où se <rev>trouve un hachage de validation SHA-1. Vous pouvez toujours voir l'historique, blâmer certains changements.

git log <rev>
git blame <rev> -- README.md

Notez que vous ne pouvez pas voir le préfixe de répertoire d'ici car il s'agit d'une ancienne branche réelle laissée intacte. Vous devez traiter cela comme un commit de déplacement de fichier habituel: vous aurez besoin d'un saut supplémentaire pour l'atteindre.

# finishes with all files added at once commit
git log rails/README.md

# then continue from original tree
git log <rev> -- README.md

Il existe des solutions plus complexes comme le faire manuellement ou réécrire l'historique comme décrit dans d'autres réponses.

La commande git-subtree fait partie de git-contrib officielle, certains gestionnaires de paquets l'installent par défaut (OS X Homebrew). Mais vous devrez peut-être l'installer vous-même en plus de git.

Simon Perepelitsa
la source
2
Voici les instructions sur la façon d'installer Git SubTree (à partir de juin 2013): stackoverflow.com/a/11613541/694469 (et j'ai remplacé git co v1.7.11.3 par ... v1.8.3).
KajMagnus
1
Merci pour les informations concernant la réponse ci-dessous. À partir de git 1.8.4, 'subtree' n'est toujours pas inclus (du moins pas sur Ubuntu 12.04 git ppa (ppa: git-core / ppa))
Matt Klein
1
Je peux confirmer qu'après cela, git log rails/somefilen'affichera pas l'historique des validations de ce fichier, sauf la validation de la fusion. Comme l'a suggéré @artfulrobot, vérifiez la réponse de Greg Hewgill . Et vous devrez peut-être utiliser git filter-branchle référentiel que vous souhaitez inclure.
Jifeng Zhang
6
Ou lisez "Fusionner deux référentiels Git en un seul référentiel sans perdre l'historique des fichiers" d'Eric Lee saintgimp.org/2013/01/22/…
Jifeng Zhang
4
Comme d'autres l'ont dit, git subtreene faites pas ce que vous pensez! Voir ici pour une solution plus complète.
Paul Draper
1910

Si vous souhaitez fusionner project-aen project-b:

cd path/to/project-b
git remote add project-a path/to/project-a
git fetch project-a --tags
git merge --allow-unrelated-histories project-a/master # or whichever branch you want to merge
git remote remove project-a

Tiré de: git fusionner différents référentiels?

Cette méthode a plutôt bien fonctionné pour moi, elle est plus courte et à mon avis beaucoup plus propre.

Dans le cas où vous souhaitez mettre project-adans un sous-répertoire, vous pouvez utiliser git-filter-repo( filter-branchest déconseillé ). Exécutez les commandes suivantes avant les commandes ci-dessus:

cd path/to/project-a
git filter-repo --to-subdirectory-filter project-a

Un exemple de fusion de 2 grands référentiels, en plaçant l'un d'entre eux dans un sous-répertoire: https://gist.github.com/x-yuri/9890ab1079cf4357d6f269d073fd9731

Remarque: Le --allow-unrelated-historiesparamètre existe uniquement depuis git> = 2.9. Voir Git - git merge Documentation / --allow-unrelated-histories

Mise à jour : ajoutée --tagscomme suggéré par @jstadler afin de conserver les tags.

Andresch Serj
la source
8
Cela a fait l'affaire pour moi. A fonctionné comme un charme la première fois avec un seul conflit dans le fichier .gitignore! Il a parfaitement conservé l'historique des commit. Le gros plus par rapport aux autres approches - en plus de la simplicité - est qu'avec cela, il n'est pas nécessaire d'avoir une référence continue au référentiel fusionné. Cependant, une chose à surveiller - si vous êtes un développeur iOS comme moi - est de faire très attention de déposer le fichier de projet du référentiel cible dans l'espace de travail.
Max MacLeod
30
Merci. A travaillé pour moi. J'avais besoin de déplacer le répertoire fusionné dans un sous-dossier, donc après avoir suivi les étapes ci-dessus, j'ai simplement utiliségit mv source-dir/ dest/new-source-dir
Sid
13
L' git mergeétape échoue ici avec fatal: refusing to merge unrelated histories; --allow-unrelated-historiescorrige cela comme expliqué dans la documentation .
ssc
19
--allow-unrelated-historiesa été introduit dans git 2.9 . Dans les versions antérieures, c'était un comportement par défaut.
Douglas Royds
11
Shorter: git fetch /path/to/project-a master; git merge --allow-unrelated-histories FETCH_HEAD.
2017
614

Voici deux solutions possibles:

Sous-modules

Soit copier le référentiel A dans un répertoire séparé dans le projet B plus grand, ou (peut-être mieux) cloner le référentiel A dans un sous-répertoire du projet B.Utilisez ensuite le sous-module git pour faire de ce référentiel un sous - module d'un référentiel B.

Ceci est une bonne solution pour les dépôts faiblement couplés, où le développement dans le référentiel A continue, et la majeure partie du développement est un développement autonome séparé A. Voir aussi SubmoduleSupport et GitSubmoduleTutorial pages Wiki Git.

Fusion de sous-arborescence

Vous pouvez fusionner le référentiel A dans un sous-répertoire d'un projet B à l'aide de la stratégie de fusion des sous-arbres . Ceci est décrit dans Subtree Merging and You par Markus Prinz.

git remote add -f Bproject /path/to/B
git merge -s ours --allow-unrelated-histories --no-commit Bproject/master
git read-tree --prefix=dir-B/ -u Bproject/master
git commit -m "Merge B project as our subdirectory"
git pull -s subtree Bproject master

(L'option --allow-unrelated-historiesest nécessaire pour Git> = 2.9.0.)

Ou vous pouvez utiliser l' outil git subtree ( référentiel sur GitHub ) par apenwarr (Avery Pennarun), annoncé par exemple dans son article de blog Une nouvelle alternative aux sous-modules Git: git subtree .


Je pense que dans votre cas (A doit faire partie d'un plus grand projet B), la bonne solution serait d'utiliser la fusion des sous-arbres .

Jakub Narębski
la source
1
Cela fonctionne et semble conserver l'historique, mais pas de telle sorte que vous puissiez l'utiliser pour différencier des fichiers ou diviser en deux lors de la fusion. Suis-je en train de manquer une étape?
jettero
56
c'est incomplet . Oui, vous obtenez une charge de commits, mais ils ne font plus référence aux bons chemins. git log dir-B/somefilene montrera rien sauf la fusion. Voir la réponse de Greg Hewgill fait référence à ce problème important.
artfulrobot
2
IMPORTANT: git pull --no-rebase -s subtree Bproject master Si vous ne le faites pas et que vous avez défini pull pour rebaser automatiquement, vous vous retrouvez avec "Impossible d'analyser l'objet". Voir osdir.com/ml/git/2009-07/msg01576.html
Eric Bowman - abstracto -
4
Cette réponse peut prêter à confusion car elle a B comme sous-arbre fusionné alors que dans la question c'était A. Résultat d'un copier-coller?
vfclists
11
Si vous essayez simplement de coller deux référentiels ensemble, les fusions de sous-modules et de sous-arborescences ne sont pas le bon outil à utiliser car elles ne préservent pas tout l'historique des fichiers (comme d'autres commentateurs l'ont noté). Voir stackoverflow.com/questions/13040958/… .
Eric Lee
194

L'approche par sous-module est bonne si vous souhaitez gérer le projet séparément. Cependant, si vous voulez vraiment fusionner les deux projets dans le même référentiel, vous avez encore un peu de travail à faire.

La première chose serait d'utiliser git filter-branchpour réécrire les noms de tout dans le deuxième référentiel pour être dans le sous-répertoire où vous souhaitez qu'ils se retrouvent. Donc au lieu de foo.c, bar.htmlvous auriez projb/foo.cet projb/bar.html.

Ensuite, vous devriez pouvoir faire quelque chose comme ceci:

git remote add projb [wherever]
git pull projb

Le git pullfera un git fetchsuivi d'un git merge. Il ne devrait pas y avoir de conflits, si le référentiel vers lequel vous tirez n'a pas encore de projb/répertoire.

En outre la recherche indique que quelque chose de semblable a été fait pour la fusion gitken git. Junio ​​C Hamano écrit à ce sujet ici: http://www.mail-archive.com/[email protected]/msg03395.html

Greg Hewgill
la source
4
la fusion des sous-arbres serait une meilleure solution et ne nécessite pas de réécrire l'historique du projet inclus
Jakub Narębski
8
Je voudrais savoir comment l'utiliser git filter-branchpour y parvenir. Dans la page de manuel, il est question du contraire: faire de subdir / devenir la racine, mais pas l'inverse.
artfulrobot
31
cette réponse serait formidable si elle expliquait comment utiliser la branche de filtre pour obtenir le résultat souhaité
Anentropic
14
J'ai trouvé comment utiliser filter-branch ici: stackoverflow.com/questions/4042816/…
David Minor
3
Voir cette réponse pour la mise en œuvre du plan de Greg.
Paul Draper
75

git-subtree c'est bien, mais ce n'est probablement pas celui que vous voulez.

Par exemple, si projectAle répertoire est créé en B, après git subtree,

git log projectA

répertorie un seul commit: la fusion. Les validations du projet fusionné sont pour des chemins différents, donc elles n'apparaissent pas.

La réponse de Greg Hewgill est la plus proche, même si elle ne dit pas comment réécrire les chemins.


La solution est étonnamment simple.

(1) Dans A,

PREFIX=projectA #adjust this

git filter-branch --index-filter '
    git ls-files -s |
    sed "s,\t,&'"$PREFIX"'/," |
    GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info &&
    mv $GIT_INDEX_FILE.new $GIT_INDEX_FILE
' HEAD

Remarque: Cela réécrit l'historique, donc si vous avez l'intention de continuer à utiliser ce dépôt A, vous pouvez d'abord en cloner (copier) une copie jetable.

Remarque Bene: vous devez modifier le script de remplacement à l'intérieur de la commande sed dans le cas où vous utilisez des caractères non ascii (ou des caractères blancs) dans les noms de fichier ou le chemin. Dans ce cas, l'emplacement du fichier dans un enregistrement produit par "ls-files -s" commence par un guillemet.

(2) Puis en B, exécutez

git pull path/to/A

Voila! Vous avez un projectArépertoire en B. Si vous exécutez git log projectA, vous verrez toutes les validations de A.


Dans mon cas, je voulais deux sous-répertoires, projectAet projectB. Dans ce cas, j'ai également effectué l'étape (1) vers B.

Paul Draper
la source
1
Il semble que vous ayez copié votre réponse à partir de stackoverflow.com/a/618113/586086 ?
Andrew Mao
1
@AndrewMao, je pense que oui ... Je ne m'en souviens pas. J'ai beaucoup utilisé ce script.
Paul Draper
6
J'ajouterais que \ t ne fonctionne pas sur OS X et vous devez entrer <tab>
Muneeb Ali
2
"$GIT_INDEX_FILE"doit être cité (deux fois), sinon votre méthode échouera si par exemple le chemin contient des espaces.
Rob W
4
Si vous vous demandez, pour insérer un <tab> dans osx, vous devezCtrl-V <tab>
casey
48

Si les deux référentiels ont le même type de fichiers (comme deux référentiels Rails pour des projets différents), vous pouvez récupérer les données du référentiel secondaire dans votre référentiel actuel:

git fetch git://repository.url/repo.git master:branch_name

puis fusionnez-le dans le référentiel actuel:

git merge --allow-unrelated-histories branch_name

Si votre version de Git est inférieure à 2,9, supprimez --allow-unrelated-histories.

Après cela, des conflits peuvent survenir. Vous pouvez les résoudre par exemple avec git mergetool. kdiff3peut être utilisé uniquement avec le clavier, donc 5 fichiers de conflit prennent lors de la lecture du code quelques minutes seulement.

N'oubliez pas de terminer la fusion:

git commit
Smar
la source
25

J'ai continué à perdre l'historique lors de l'utilisation de la fusion, j'ai donc fini par utiliser le rebase car dans mon cas, les deux référentiels sont suffisamment différents pour ne pas finir par fusionner à chaque validation:

git clone git@gitorious/projA.git projA
git clone git@gitorious/projB.git projB

cd projB
git remote add projA ../projA/
git fetch projA 
git rebase projA/master HEAD

=> résoudre les conflits, puis continuer, autant de fois que nécessaire ...

git rebase --continue

Faire ceci conduit à un projet ayant toutes les validations de projA suivies de validations de projB

Calahad
la source
25

Dans mon cas, j'avais un my-pluginréférentiel et un main-projectréférentiel, et je voulais faire comme si my-pluginj'avais toujours été développé dans le pluginssous - répertoire de main-project.

Fondamentalement, j'ai réécrit l'histoire du my-pluginréférentiel afin qu'il apparaisse que tout le développement a eu lieu dans le plugins/my-pluginsous - répertoire. Ensuite, j'ai ajouté l'historique de développement de my-plugindans l' main-projecthistoire et j'ai fusionné les deux arbres ensemble. Puisqu'aucun plugins/my-pluginrépertoire n'était déjà présent dans le main-projectréférentiel, il s'agissait d'une fusion triviale sans conflits. Le référentiel résultant contenait toute l'histoire des deux projets originaux et avait deux racines.

TL; DR

$ cp -R my-plugin my-plugin-dirty
$ cd my-plugin-dirty
$ git filter-branch -f --tree-filter "zsh -c 'setopt extended_glob && setopt glob_dots && mkdir -p plugins/my-plugin && (mv ^(.git|plugins) plugins/my-plugin || true)'" -- --all
$ cd ../main-project
$ git checkout master
$ git remote add --fetch my-plugin ../my-plugin-dirty
$ git merge my-plugin/master --allow-unrelated-histories
$ cd ..
$ rm -rf my-plugin-dirty

Version longue

Tout d'abord, créez une copie du my-pluginréférentiel, car nous allons réécrire l'historique de ce référentiel.

Maintenant, accédez à la racine du my-pluginréférentiel, consultez votre branche principale (probablement master) et exécutez la commande suivante. Bien sûr, vous devez remplacer my-pluginet pluginsquels que soient vos noms réels.

$ git filter-branch -f --tree-filter "zsh -c 'setopt extended_glob && setopt glob_dots && mkdir -p plugins/my-plugin && (mv ^(.git|plugins) plugins/my-plugin || true)'" -- --all

Maintenant pour une explication. git filter-branch --tree-filter (...) HEADexécute la (...)commande sur chaque commit accessible depuis HEAD. Notez que cela fonctionne directement sur les données stockées pour chaque commit, nous n'avons donc pas à nous soucier des notions de "répertoire de travail", "index", "staging", etc.

Si vous exécutez une filter-branchcommande qui échoue, elle laissera des fichiers dans le .gitrépertoire et la prochaine fois que vous l'essayerez, filter-branchelle s'en plaindra, sauf si vous fournissez l' -foption à filter-branch.

Quant à la commande proprement dite, je n'ai pas eu beaucoup de chance bashpour faire ce que je voulais, donc j'utilise plutôt zsh -cpour faire zshexécuter une commande. J'ai d'abord défini l' extended_globoption, qui active la ^(...)syntaxe dans la mvcommande, ainsi que l' glob_dotsoption, qui me permet de sélectionner des fichiers dot (tels que .gitignore) avec un glob ( ^(...)).

Ensuite, j'utilise la mkdir -pcommande pour créer les deux pluginset plugins/my-pluginen même temps.

Enfin, j'utilise la fonction zsh"glob négatif" pour ^(.git|plugins)faire correspondre tous les fichiers du répertoire racine du référentiel à l'exception de .gitet du my-plugindossier nouvellement créé . (L'exclusion .gitpeut ne pas être nécessaire ici, mais essayer de déplacer un répertoire en lui-même est une erreur.)

Dans mon référentiel, la validation initiale ne comprenait aucun fichier, donc la mvcommande a renvoyé une erreur sur la validation initiale (car rien n'était disponible pour se déplacer). Par conséquent, j'ai ajouté un || trueafin de git filter-branchne pas abandonner.

L' --alloption indique filter-branchde réécrire l'historique de toutes les branches dans le référentiel, et le supplément --est nécessaire de dire gitde l'interpréter comme une partie de la liste d'options pour les branches à réécrire, plutôt que comme une option pour filter-branchlui-même.

Maintenant, accédez à votre main-projectréférentiel et découvrez la branche dans laquelle vous souhaitez fusionner. Ajoutez votre copie locale du my-pluginréférentiel (avec son historique modifié) en tant que télécommande de main-projectavec:

$ git remote add --fetch my-plugin $PATH_TO_MY_PLUGIN_REPOSITORY

Vous aurez maintenant deux arborescences indépendantes dans votre historique de commit, que vous pouvez visualiser correctement en utilisant:

$ git log --color --graph --decorate --all

Pour les fusionner, utilisez:

$ git merge my-plugin/master --allow-unrelated-histories

Notez que dans la version antérieure à 2.9.0 Git, l' --allow-unrelated-historiesoption n'existe pas. Si vous utilisez l'une de ces versions, omettez simplement l'option: le message d'erreur qui --allow-unrelated-historiesempêche a également été ajouté dans 2.9.0.

Vous ne devriez pas avoir de conflits de fusion. Si vous le faites, cela signifie probablement que la filter-branchcommande n'a pas fonctionné correctement ou qu'il y avait déjà un plugins/my-pluginrépertoire dans main-project.

Assurez-vous d'entrer un message de validation explicatif pour tous les futurs contributeurs se demandant quel piratage était en cours pour créer un référentiel à deux racines.

Vous pouvez visualiser le nouveau graphique de validation, qui devrait avoir deux validations racine, à l'aide de la git logcommande ci-dessus . Notez que seule la masterbranche sera fusionnée . Cela signifie que si vous avez un travail important sur d'autres my-pluginbranches que vous souhaitez fusionner dans l' main-projectarborescence, vous devez vous abstenir de supprimer la my-plugintélécommande jusqu'à ce que vous ayez effectué ces fusions. Si vous ne le faites pas, les validations de ces branches seront toujours dans le main-projectréférentiel, mais certaines seront inaccessibles et susceptibles d'être éventuellement récupérées. (De plus, vous devrez vous y référer par SHA, car la suppression d'une télécommande supprime ses branches de suivi à distance.)

Facultativement, après avoir fusionné tout ce que vous souhaitez conserver my-plugin, vous pouvez supprimer la my-plugintélécommande en utilisant:

$ git remote remove my-plugin

Vous pouvez désormais supprimer en toute sécurité la copie du my-pluginréférentiel dont vous avez modifié l'historique. Dans mon cas, j'ai également ajouté un avis de dépréciation au my-pluginréférentiel réel une fois la fusion terminée et poussée.


Testé sur Mac OS X El Capitan avec git --version 2.9.0et zsh --version 5.2. Votre kilométrage peut varier.

Références:

Radon Rosborough
la source
1
D'où --allow-unrelated-historiesviens-tu?
xpto
3
@MarceloFilho Check man git-merge. Par défaut, la commande git merge refuse de fusionner les historiques qui ne partagent pas un ancêtre commun. Cette option peut être utilisée pour remplacer cette sécurité lors de la fusion des historiques de deux projets qui ont commencé leur vie de manière indépendante. Comme il s'agit d'une occasion très rare, aucune variable de configuration pour l'activer par défaut n'existe et ne sera pas ajoutée.
Radon Rosborough
Devrait être disponible sur git version 2.7.2.windows.1?
xpto
2
@MarceloFilho Cela a été ajouté dans 2.9.0, mais dans les anciennes versions, vous ne devriez pas avoir à passer l'option (cela fonctionnera juste). github.com/git/git/blob/…
Radon Rosborough
Cela a bien fonctionné. Et j'ai pu utiliser la branche de filtre pour réécrire les noms de fichiers à l'endroit où je voulais dans l'arborescence avant la fusion. Je suppose qu'il faut plus de travail si vous devez déplacer l'historique en plus de la branche principale.
codeDr
9

J'essaie de faire la même chose depuis des jours, j'utilise git 2.7.2. Subtree ne conserve pas l'histoire.

Vous pouvez utiliser cette méthode si vous n'utilisez plus l'ancien projet.

Je vous suggère de commencer par la branche B et de travailler dans la branche.

Voici les étapes sans branchement:

cd B

# You are going to merge A into B, so first move all of B's files into a sub dir
mkdir B

# Move all files to B, till there is nothing in the dir but .git and B
git mv <files> B

git add .

git commit -m "Moving content of project B in preparation for merge from A"


# Now merge A into B
git remote add -f A <A repo url>

git merge A/<branch>

mkdir A

# move all the files into subdir A, excluding .git
git mv <files> A

git commit -m "Moved A into subdir"


# Move B's files back to root    
git mv B/* ./

rm -rf B

git commit -m "Reset B to original state"

git push

Si vous enregistrez maintenant l'un des fichiers dans le sous-répertoire A, vous obtiendrez l'historique complet

git log --follow A/<file>

Ce fut le poste qui m'aide à le faire:

http://saintgimp.org/2013/01/22/merging-two-git-repositories-into-one-repository-without-losing-file-history/

Rian
la source
8

Si vous souhaitez placer les fichiers d'une branche dans le référentiel B dans un sous - arbre du référentiel A et également conserver l'historique, continuez à lire. (Dans l'exemple ci-dessous, je suppose que nous voulons fusionner la branche principale du repo B dans la branche principale du repo A.)

Dans le référentiel A, procédez d'abord comme suit pour rendre le référentiel B disponible:

git remote add B ../B # Add repo B as a new remote.
git fetch B

Nous créons maintenant une toute nouvelle branche (avec un seul commit) dans le repo A que nous appelons new_b_root. Le commit résultant aura les fichiers qui ont été validés dans le premier commit de la branche master du repo B mais placés dans un sous-répertoire appelé path/to/b-files/.

git checkout --orphan new_b_root master
git rm -rf . # Remove all files.
git cherry-pick -n `git rev-list --max-parents=0 B/master`
mkdir -p path/to/b-files
git mv README path/to/b-files/
git commit --date="$(git log --format='%ai' $(git rev-list --max-parents=0 B/master))"

Explication: L' --orphanoption de la commande d'extraction extrait les fichiers de la branche principale de A mais ne crée aucun commit. Nous aurions pu sélectionner n'importe quel commit car nous effacerons tous les fichiers de toute façon. Ensuite, sans commettre encore ( -n), nous sélectionnons le premier commit de la branche principale de B. (La cerise sur le gâteau préserve le message de validation d'origine, ce qu'une extraction directe ne semble pas faire.) Ensuite, nous créons le sous-arbre où nous voulons placer tous les fichiers du repo B. Nous devons ensuite déplacer tous les fichiers qui ont été introduits dans le cueillir le sous-arbre. Dans l'exemple ci-dessus, il n'y a qu'un READMEfichier à déplacer. Ensuite, nous validons notre commit racine B-repo et, en même temps, nous préservons également l'horodatage du commit d'origine.

Maintenant, nous allons créer une nouvelle B/masterbranche au-dessus de la nouvellement créée new_b_root. Nous appelons la nouvelle branche b:

git checkout -b b B/master
git rebase -s recursive -Xsubtree=path/to/b-files/ new_b_root

Maintenant, nous fusionnons notre bbranche en A/master:

git checkout master
git merge --allow-unrelated-histories --no-commit b
git commit -m 'Merge repo B into repo A.'

Enfin, vous pouvez supprimer les Bbranches distantes et temporaires:

git remote remove B
git branch -D new_b_root b

Le graphique final aura une structure comme celle-ci:

entrez la description de l'image ici

Finn Haakansson
la source
Excellente réponse, merci! J'ai vraiment manqué dans les autres réponses avec "git subtree" ou "merge --allow-unrelated-histories" d'Andresch Serj que le sous-répertoire n'avait pas le journal.
Ilendir
8

J'ai rassemblé beaucoup d'informations ici sur Stack OverFlow, etc., et j'ai réussi à créer un script qui résout le problème pour moi.

La mise en garde est qu'elle ne prend en compte que la branche «développer» de chaque référentiel et la fusionne dans un répertoire séparé dans un référentiel complètement nouveau.

Les balises et autres branches sont ignorées - ce n'est peut-être pas ce que vous voulez.

Le script gère même les branches de fonctionnalités et les balises - en les renommant dans le nouveau projet afin que vous sachiez d'où elles viennent.

#!/bin/bash
#
################################################################################
## Script to merge multiple git repositories into a new repository
## - The new repository will contain a folder for every merged repository
## - The script adds remotes for every project and then merges in every branch
##   and tag. These are renamed to have the origin project name as a prefix
##
## Usage: mergeGitRepositories.sh <new_project> <my_repo_urls.lst>
## - where <new_project> is the name of the new project to create
## - and <my_repo_urls.lst> is a file contaning the URLs to the respositories
##   which are to be merged on separate lines.
##
## Author: Robert von Burg
##            [email protected]
##
## Version: 0.3.2
## Created: 2018-02-05
##
################################################################################
#

# disallow using undefined variables
shopt -s -o nounset

# Script variables
declare SCRIPT_NAME="${0##*/}"
declare SCRIPT_DIR="$(cd ${0%/*} ; pwd)"
declare ROOT_DIR="$PWD"
IFS=$'\n'

# Detect proper usage
if [ "$#" -ne "2" ] ; then
  echo -e "ERROR: Usage: $0 <new_project> <my_repo_urls.lst>"
  exit 1
fi


## Script variables
PROJECT_NAME="${1}"
PROJECT_PATH="${ROOT_DIR}/${PROJECT_NAME}"
TIMESTAMP="$(date +%s)"
LOG_FILE="${ROOT_DIR}/${PROJECT_NAME}_merge.${TIMESTAMP}.log"
REPO_FILE="${2}"
REPO_URL_FILE="${ROOT_DIR}/${REPO_FILE}"


# Script functions
function failed() {
  echo -e "ERROR: Merging of projects failed:"
  echo -e "ERROR: Merging of projects failed:" >>${LOG_FILE} 2>&1
  echo -e "$1"
  exit 1
}

function commit_merge() {
  current_branch="$(git symbolic-ref HEAD 2>/dev/null)"
  if [[ ! -f ".git/MERGE_HEAD" ]] ; then
    echo -e "INFO:   No commit required."
    echo -e "INFO:   No commit required." >>${LOG_FILE} 2>&1
  else
    echo -e "INFO:   Committing ${sub_project}..."
    echo -e "INFO:   Committing ${sub_project}..." >>${LOG_FILE} 2>&1
    if ! git commit -m "[Project] Merged branch '$1' of ${sub_project}" >>${LOG_FILE} 2>&1 ; then
      failed "Failed to commit merge of branch '$1' of ${sub_project} into ${current_branch}"
    fi
  fi
}


# Make sure the REPO_URL_FILE exists
if [ ! -e "${REPO_URL_FILE}" ] ; then
  echo -e "ERROR: Repo file ${REPO_URL_FILE} does not exist!"
  exit 1
fi


# Make sure the required directories don't exist
if [ -e "${PROJECT_PATH}" ] ; then
  echo -e "ERROR: Project ${PROJECT_NAME} already exists!"
  exit 1
fi


# create the new project
echo -e "INFO: Logging to ${LOG_FILE}"
echo -e "INFO: Creating new git repository ${PROJECT_NAME}..."
echo -e "INFO: Creating new git repository ${PROJECT_NAME}..." >>${LOG_FILE} 2>&1
echo -e "===================================================="
echo -e "====================================================" >>${LOG_FILE} 2>&1
cd ${ROOT_DIR}
mkdir ${PROJECT_NAME}
cd ${PROJECT_NAME}
git init
echo "Initial Commit" > initial_commit
# Since this is a new repository we need to have at least one commit
# thus were we create temporary file, but we delete it again.
# Deleting it guarantees we don't have conflicts later when merging
git add initial_commit
git commit --quiet -m "[Project] Initial Master Repo Commit"
git rm --quiet initial_commit
git commit --quiet -m "[Project] Initial Master Repo Commit"
echo


# Merge all projects into the branches of this project
echo -e "INFO: Merging projects into new repository..."
echo -e "INFO: Merging projects into new repository..." >>${LOG_FILE} 2>&1
echo -e "===================================================="
echo -e "====================================================" >>${LOG_FILE} 2>&1
for url in $(cat ${REPO_URL_FILE}) ; do

  if [[ "${url:0:1}" == '#' ]] ; then
    continue
  fi

  # extract the name of this project
  export sub_project=${url##*/}
  sub_project=${sub_project%*.git}

  echo -e "INFO: Project ${sub_project}"
  echo -e "INFO: Project ${sub_project}" >>${LOG_FILE} 2>&1
  echo -e "----------------------------------------------------"
  echo -e "----------------------------------------------------" >>${LOG_FILE} 2>&1

  # Fetch the project
  echo -e "INFO:   Fetching ${sub_project}..."
  echo -e "INFO:   Fetching ${sub_project}..." >>${LOG_FILE} 2>&1
  git remote add "${sub_project}" "${url}"
  if ! git fetch --tags --quiet ${sub_project} >>${LOG_FILE} 2>&1 ; then
    failed "Failed to fetch project ${sub_project}"
  fi

  # add remote branches
  echo -e "INFO:   Creating local branches for ${sub_project}..."
  echo -e "INFO:   Creating local branches for ${sub_project}..." >>${LOG_FILE} 2>&1
  while read branch ; do
    branch_ref=$(echo $branch | tr " " "\t" | cut -f 1)
    branch_name=$(echo $branch | tr " " "\t" | cut -f 2 | cut -d / -f 3-)

    echo -e "INFO:   Creating branch ${branch_name}..."
    echo -e "INFO:   Creating branch ${branch_name}..." >>${LOG_FILE} 2>&1

    # create and checkout new merge branch off of master
    if ! git checkout -b "${sub_project}/${branch_name}" master >>${LOG_FILE} 2>&1 ; then failed "Failed preparing ${branch_name}" ; fi
    if ! git reset --hard ; then failed "Failed preparing ${branch_name}" >>${LOG_FILE} 2>&1 ; fi
    if ! git clean -d --force ; then failed "Failed preparing ${branch_name}" >>${LOG_FILE} 2>&1 ; fi

    # Merge the project
    echo -e "INFO:   Merging ${sub_project}..."
    echo -e "INFO:   Merging ${sub_project}..." >>${LOG_FILE} 2>&1
    if ! git merge --allow-unrelated-histories --no-commit "remotes/${sub_project}/${branch_name}" >>${LOG_FILE} 2>&1 ; then
      failed "Failed to merge branch 'remotes/${sub_project}/${branch_name}' from ${sub_project}"
    fi

    # And now see if we need to commit (maybe there was a merge)
    commit_merge "${sub_project}/${branch_name}"

    # relocate projects files into own directory
    if [ "$(ls)" == "${sub_project}" ] ; then
      echo -e "WARN:   Not moving files in branch ${branch_name} of ${sub_project} as already only one root level."
      echo -e "WARN:   Not moving files in branch ${branch_name} of ${sub_project} as already only one root level." >>${LOG_FILE} 2>&1
    else
      echo -e "INFO:   Moving files in branch ${branch_name} of ${sub_project} so we have a single directory..."
      echo -e "INFO:   Moving files in branch ${branch_name} of ${sub_project} so we have a single directory..." >>${LOG_FILE} 2>&1
      mkdir ${sub_project}
      for f in $(ls -a) ; do
        if  [[ "$f" == "${sub_project}" ]] ||
            [[ "$f" == "." ]] ||
            [[ "$f" == ".." ]] ; then
          continue
        fi
        git mv -k "$f" "${sub_project}/"
      done

      # commit the moving
      if ! git commit --quiet -m  "[Project] Move ${sub_project} files into sub directory" ; then
        failed "Failed to commit moving of ${sub_project} files into sub directory"
      fi
    fi
    echo
  done < <(git ls-remote --heads ${sub_project})


  # checkout master of sub probject
  if ! git checkout "${sub_project}/master" >>${LOG_FILE} 2>&1 ; then
    failed "sub_project ${sub_project} is missing master branch!"
  fi

  # copy remote tags
  echo -e "INFO:   Copying tags for ${sub_project}..."
  echo -e "INFO:   Copying tags for ${sub_project}..." >>${LOG_FILE} 2>&1
  while read tag ; do
    tag_ref=$(echo $tag | tr " " "\t" | cut -f 1)
    tag_name_unfixed=$(echo $tag | tr " " "\t" | cut -f 2 | cut -d / -f 3)

    # hack for broken tag names where they are like 1.2.0^{} instead of just 1.2.0
    tag_name="${tag_name_unfixed%%^*}"

    tag_new_name="${sub_project}/${tag_name}"
    echo -e "INFO:     Copying tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}..."
    echo -e "INFO:     Copying tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}..." >>${LOG_FILE} 2>&1
    if ! git tag "${tag_new_name}" "${tag_ref}" >>${LOG_FILE} 2>&1 ; then
      echo -e "WARN:     Could not copy tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}"
      echo -e "WARN:     Could not copy tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}" >>${LOG_FILE} 2>&1
    fi
  done < <(git ls-remote --tags --refs ${sub_project})

  # Remove the remote to the old project
  echo -e "INFO:   Removing remote ${sub_project}..."
  echo -e "INFO:   Removing remote ${sub_project}..." >>${LOG_FILE} 2>&1
  git remote rm ${sub_project}

  echo
done


# Now merge all project master branches into new master
git checkout --quiet master
echo -e "INFO: Merging projects master branches into new repository..."
echo -e "INFO: Merging projects master branches into new repository..." >>${LOG_FILE} 2>&1
echo -e "===================================================="
echo -e "====================================================" >>${LOG_FILE} 2>&1
for url in $(cat ${REPO_URL_FILE}) ; do

  if [[ ${url:0:1} == '#' ]] ; then
    continue
  fi

  # extract the name of this project
  export sub_project=${url##*/}
  sub_project=${sub_project%*.git}

  echo -e "INFO:   Merging ${sub_project}..."
  echo -e "INFO:   Merging ${sub_project}..." >>${LOG_FILE} 2>&1
  if ! git merge --allow-unrelated-histories --no-commit "${sub_project}/master" >>${LOG_FILE} 2>&1 ; then
    failed "Failed to merge branch ${sub_project}/master into master"
  fi

  # And now see if we need to commit (maybe there was a merge)
  commit_merge "${sub_project}/master"

  echo
done


# Done
cd ${ROOT_DIR}
echo -e "INFO: Done."
echo -e "INFO: Done." >>${LOG_FILE} 2>&1
echo

exit 0

Vous pouvez également l'obtenir sur http://paste.ubuntu.com/11732805

Créez d'abord un fichier avec l'URL de chaque référentiel, par exemple:

[email protected]:eitchnet/ch.eitchnet.parent.git
[email protected]:eitchnet/ch.eitchnet.utils.git
[email protected]:eitchnet/ch.eitchnet.privilege.git

Appelez ensuite le script en donnant un nom au projet et le chemin d'accès au script:

./mergeGitRepositories.sh eitchnet_test eitchnet.lst

Le script lui-même contient beaucoup de commentaires qui devraient expliquer ce qu'il fait.

eitch
la source
Au lieu de diriger les lecteurs vers une réponse, veuillez poster la réponse ici (alias éditez ce que vous avez dit dans ce commentaire dans cette réponse).
josliber
1
Bien sûr, je pensais juste qu'il
valait
Si vous pensez que cette question est identique à l'autre, vous pouvez la signaler comme doublon en utilisant le lien "signaler" sous la question elle-même et en indiquant l'autre question. Si ce n'est pas une question en double mais que vous pensez que la même réponse exacte peut être utilisée pour résoudre les deux problèmes, il vous suffit de poster la même réponse aux deux problèmes (comme vous l'avez maintenant fait). Merci d'avoir contribué!
josliber
Incroyable! Ne fonctionnait pas sur l'invite bash de Windows, mais il fonctionnait parfaitement sous forme d'une boîte Vagrant exécutant ubuntu. Quel gain de temps!
xverges
Heureux d'être au service =)
eitch
7

Je sais que c'est longtemps après le fait, mais je n'étais pas satisfait des autres réponses que j'ai trouvées ici, alors j'ai écrit ceci:

me=$(basename $0)

TMP=$(mktemp -d /tmp/$me.XXXXXXXX)
echo 
echo "building new repo in $TMP"
echo
sleep 1

set -e

cd $TMP
mkdir new-repo
cd new-repo
    git init
    cd ..

x=0
while [ -n "$1" ]; do
    repo="$1"; shift
    git clone "$repo"
    dirname=$(basename $repo | sed -e 's/\s/-/g')
    if [[ $dirname =~ ^git:.*\.git$ ]]; then
        dirname=$(echo $dirname | sed s/.git$//)
    fi

    cd $dirname
        git remote rm origin
        git filter-branch --tree-filter \
            "(mkdir -p $dirname; find . -maxdepth 1 ! -name . ! -name .git ! -name $dirname -exec mv {} $dirname/ \;)"
        cd ..

    cd new-repo
        git pull --no-commit ../$dirname
        [ $x -gt 0 ] && git commit -m "merge made by $me"
        cd ..

    x=$(( x + 1 ))
done
jettero
la source
2
C'était exactement ce que je cherchais. Merci! Cependant, j'ai dû changer la ligne 22 pour:if [[ $dirname =~ ^.*\.git$ ]]; then
heyman
2
^. * blarg $ est RE super gourmand. Mieux vaut dire .blarg $ et sauter l'ancre avant.
jettero
7

Si vous essayez simplement de coller deux référentiels ensemble, les fusions de sous-modules et de sous-arborescences ne sont pas le bon outil à utiliser car elles ne préservent pas tout l'historique des fichiers (comme les gens l'ont noté dans d'autres réponses). Voir cette réponse ici pour la manière simple et correcte de le faire.

Eric Lee
la source
1
Votre solution ne fonctionne bien que pour un nouveau référentiel, mais que diriez-vous de fusionner le référentiel dans un autre avec des conflits de fichiers?
Andrey Izman
6

J'avais un défi similaire, mais dans mon cas, nous avions développé une version de la base de code dans le référentiel A, puis cloné cela dans un nouveau référentiel, le référentiel B, pour la nouvelle version du produit. Après avoir corrigé quelques bugs dans le référentiel A, nous avions besoin de FI les modifications dans le référentiel B. Nous avons fini par faire ce qui suit:

  1. Ajout d'une télécommande au repo B qui pointait vers le repo A (git remote add ...)
  2. Tirer la branche actuelle (nous n'utilisions pas master pour corriger les bugs) (git pull remoteForRepoA bugFixBranch)
  3. Pousser fusionne avec github

A travaillé un régal :)

David Lemphers
la source
5

Similaire à @Smar mais utilise des chemins de système de fichiers, définis dans PRIMARY et SECONDARY:

PRIMARY=~/Code/project1
SECONDARY=~/Code/project2
cd $PRIMARY
git remote add test $SECONDARY && git fetch test
git merge test/master

Ensuite, vous fusionnez manuellement.

(adapté de l' article d'Anar Manafov )

Turadg
la source
5

Fusion de 2 dépôts

git clone ssh://<project-repo> project1
cd project1
git remote add -f project2 project2
git merge --allow-unrelated-histories project2/master
git remote rm project2

delete the ref to avoid errors
git update-ref -d refs/remotes/project2/master
RahulMohan Kolakandy
la source
4

Lorsque vous souhaitez fusionner trois projets ou plus en une seule validation, procédez comme décrit dans les autres réponses ( remote add -f, merge). Ensuite, (soft) réinitialise l'index à l'ancienne tête (où aucune fusion n'a eu lieu). Ajoutez tous les fichiers ( git add -A) et validez-les (message "Fusion des projets A, B, C et D en un seul projet). Il s'agit désormais de l'ID de validation de master.

Maintenant, créez .git/info/graftsavec le contenu suivant:

<commit-id of master> <list of commit ids of all parents>

Courez git filter-branch -- head^..head head^2..head head^3..head. Si vous avez plus de trois branches, ajoutez simplement autant head^n..headque vous avez de branches. Pour mettre à jour les balises, ajoutez --tag-name-filter cat. N'ajoutez pas toujours cela, car cela pourrait entraîner une réécriture de certains commits. Pour plus de détails, voir la page de manuel de filter-branch , recherchez "greffe".

Maintenant, votre dernier commit a les bons parents associés.

koppor
la source
1
Attendez, pourquoi voudriez-vous fusionner trois projets en un seul commit?
Steve Bennett
J'ai commencé avec le référentiel, le référentiel client et le modeleur en tant que projets git séparés. Cela a été difficile pour les collègues, alors je les ai rejoints dans un seul projet git. Pour pouvoir que la "racine" du nouveau projet provienne de trois autres projets, je voulais avoir un commit de fusion unique .
koppor
4

Pour fusionner un A dans B:

1) Dans le projet A

git fast-export --all --date-order > /tmp/ProjectAExport

2) Dans le projet B

git checkout -b projectA
git fast-import --force < /tmp/ProjectAExport

Dans cette branche, effectuez toutes les opérations nécessaires et validez-les.

C) Puis revenons au master et une fusion classique entre les deux branches:

git checkout master
git merge projectA
user123568943685
la source
2

Cette fonction clonera le repo distant dans le répertoire repo local, après la fusion de toutes les validations seront enregistrées, git logaffichera les validations d'origine et les chemins d'accès appropriés:

function git-add-repo
{
    repo="$1"
    dir="$(echo "$2" | sed 's/\/$//')"
    path="$(pwd)"

    tmp="$(mktemp -d)"
    remote="$(echo "$tmp" | sed 's/\///g'| sed 's/\./_/g')"

    git clone "$repo" "$tmp"
    cd "$tmp"

    git filter-branch --index-filter '
        git ls-files -s |
        sed "s,\t,&'"$dir"'/," |
        GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &&
        mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
    ' HEAD

    cd "$path"
    git remote add -f "$remote" "file://$tmp/.git"
    git pull "$remote/master"
    git merge --allow-unrelated-histories -m "Merge repo $repo into master" --edit "$remote/master"
    git remote remove "$remote"
    rm -rf "$tmp"
}

Comment utiliser:

cd current/package
git-add-repo https://github.com/example/example dir/to/save

Si vous apportez quelques modifications, vous pouvez même déplacer des fichiers / répertoires du référentiel fusionné dans différents chemins, par exemple:

repo="https://github.com/example/example"
path="$(pwd)"

tmp="$(mktemp -d)"
remote="$(echo "$tmp" | sed 's/\///g' | sed 's/\./_/g')"

git clone "$repo" "$tmp"
cd "$tmp"

GIT_ADD_STORED=""

function git-mv-store
{
    from="$(echo "$1" | sed 's/\./\\./')"
    to="$(echo "$2" | sed 's/\./\\./')"

    GIT_ADD_STORED+='s,\t'"$from"',\t'"$to"',;'
}

# NOTICE! This paths used for example! Use yours instead!
git-mv-store 'public/index.php' 'public/admin.php'
git-mv-store 'public/data' 'public/x/_data'
git-mv-store 'public/.htaccess' '.htaccess'
git-mv-store 'core/config' 'config/config'
git-mv-store 'core/defines.php' 'defines/defines.php'
git-mv-store 'README.md' 'doc/README.md'
git-mv-store '.gitignore' 'unneeded/.gitignore'

git filter-branch --index-filter '
    git ls-files -s |
    sed "'"$GIT_ADD_STORED"'" |
    GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &&
    mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
' HEAD

GIT_ADD_STORED=""

cd "$path"
git remote add -f "$remote" "file://$tmp/.git"
git pull "$remote/master"
git merge --allow-unrelated-histories -m "Merge repo $repo into master" --edit "$remote/master"
git remote remove "$remote"
rm -rf "$tmp"

Avis Les
chemins remplacent via sed, assurez-vous donc qu'il s'est déplacé dans les bons chemins après la fusion.
Le --allow-unrelated-historiesparamètre n'existe que depuis git> = 2.9.

Andrey Izman
la source
1

La commande donnée est la meilleure solution possible que je suggère.

git subtree add --prefix=MY_PROJECT git://github.com/project/my_project.git master
Praveen Kumar
la source
1

Je fusionne les projets légèrement manuellement, ce qui me permet d'éviter d'avoir à gérer les conflits de fusion.

copiez d'abord les fichiers de l'autre projet comme vous le souhaitez.

cp -R myotherproject newdirectory
git add newdirectory

prochaine traction dans l'histoire

git fetch path_or_url_to_other_repo

dire à git de fusionner dans l'histoire de la dernière chose récupérée

echo 'FETCH_HEAD' > .git/MERGE_HEAD

engagez maintenant mais vous vous engagez normalement

git commit
Collin Anderson
la source
0

Je voulais déplacer un petit projet vers un sous-répertoire d'un plus grand. Comme mon petit projet n'avait pas beaucoup de commits, j'ai utilisé git format-patch --output-directory /path/to/patch-dir. Ensuite, sur le plus grand projet, j'ai utilisé git am --directory=dir/in/project /path/to/patch-dir/*.

Cela semble beaucoup moins effrayant et beaucoup plus propre qu'une branche de filtre. Certes, elle peut ne pas s'appliquer à tous les cas.

Mike
la source