Fusionner deux référentiels Git sans casser l'historique des fichiers

226

J'ai besoin de fusionner deux référentiels Git dans un troisième référentiel flambant neuf. J'ai trouvé de nombreuses descriptions de la façon de procéder en utilisant une fusion de sous-arborescence (par exemple la réponse de Jakub Narębski sur Comment fusionner deux référentiels Git? ) Et en suivant ces instructions fonctionne principalement, sauf que lorsque je valide la sous-arborescence, fusionne tous les fichiers des anciens référentiels sont enregistrés en tant que nouveaux fichiers ajoutés. Je peux voir l'historique des validations des anciens référentiels quand je le fais git log, mais si je le fais, git log <file>il ne montre qu'une seule validation pour ce fichier - la fusion des sous-arbres. À en juger par les commentaires sur la réponse ci-dessus, je ne suis pas le seul à voir ce problème mais je n'ai trouvé aucune solution publiée pour le résoudre.

Existe-t-il un moyen de fusionner les référentiels et de laisser l'historique des fichiers individuels intact?

Eric Lee
la source
Je n'utilise pas Git, mais dans Mercurial, je ferais d'abord une conversion si nécessaire pour corriger les chemins de fichiers du référentiel à fusionner, puis je forcerais un repo dans la cible pour obtenir les changesets, puis ferais un fusion des différentes branches. Ceci est testé et fonctionne;) Peut-être que cela aide à trouver une solution pour Git aussi ... par rapport à l'approche de fusion des sous-arbres, je suppose que l'étape de conversion est différente où l'historique est réécrit au lieu de simplement mapper un chemin (si je comprends bien) correctement). Cela garantit ensuite une fusion en douceur sans aucune manipulation particulière des chemins de fichiers.
Lucero
J'ai également trouvé cette question utile stackoverflow.com/questions/1683531/…
nacross
J'ai créé une question de suivi. Pourrait être intéressant: fusionnez deux référentiels Git et conservez l'historique principal: stackoverflow.com/questions/42161910/…
Dimitri Dewaele
La solution automatisée qui a fonctionné pour moi était stackoverflow.com/a/30781527/239408
xverges

Réponses:

269

Il s'avère que la réponse est beaucoup plus simple si vous essayez simplement de coller deux référentiels ensemble et de faire en sorte que ce soit comme ça tout au long plutôt que de gérer une dépendance externe. Il vous suffit d'ajouter des télécommandes à vos anciens référentiels, de les fusionner avec votre nouveau maître, de déplacer les fichiers et dossiers dans un sous-répertoire, de valider le déplacement et de répéter pour tous les référentiels supplémentaires. Les sous-modules, les fusions de sous-arbres et les rebases sophistiqués sont destinés à résoudre un problème légèrement différent et ne conviennent pas à ce que j'essayais de faire.

Voici un exemple de script Powershell pour coller deux référentiels ensemble:

# Assume the current directory is where we want the new repository to be created
# Create the new repository
git init

# Before we do a merge, we have to have an initial commit, so we'll make a dummy commit
git commit --allow-empty -m "Initial dummy commit"

# Add a remote for and fetch the old repo
git remote add -f old_a <OldA repo URL>

# Merge the files from old_a/master into new/master
git merge old_a/master --allow-unrelated-histories

# Move the old_a repo files and folders into a subdirectory so they don't collide with the other repo coming later
mkdir old_a
dir -exclude old_a | %{git mv $_.Name old_a}

# Commit the move
git commit -m "Move old_a files into subdir"

# Do the same thing for old_b
git remote add -f old_b <OldB repo URL>
git merge old_b/master --allow-unrelated-histories
mkdir old_b
dir exclude old_a,old_b | %{git mv $_.Name old_b}
git commit -m "Move old_b files into subdir"

Évidemment, vous pouvez plutôt fusionner old_b dans old_a (qui devient le nouveau référentiel combiné) si vous préférez le faire - modifier le script en fonction.

Si vous souhaitez également ajouter des branches de fonctionnalités en cours, utilisez ceci:

# Bring over a feature branch from one of the old repos
git checkout -b feature-in-progress
git merge -s recursive -Xsubtree=old_a old_a/feature-in-progress

C'est la seule partie non évidente du processus - ce n'est pas une fusion de sous-arborescence, mais plutôt un argument pour la fusion récursive normale qui indique à Git que nous avons renommé la cible et qui aide Git à tout aligner correctement.

J'ai écrit une explication un peu plus détaillée ici .

Eric Lee
la source
16
cette solution en utilisant git mvne fonctionne pas si bien. lorsque vous utilisez ultérieurement un git logsur l'un des fichiers déplacés, vous obtenez uniquement la validation du déplacement. toute l'histoire précédente est perdue. c'est parce que git mvc'est vraiment git rm; git addmais en une seule étape .
mholm815
15
C'est la même chose que n'importe quelle autre opération de déplacement / renommage dans Git: à partir de la ligne de commande, vous pouvez obtenir tout l'historique en faisant git log --follow, ou tous les outils GUI le font automatiquement pour vous. Avec une fusion de sous-arborescence, vous ne pouvez pas obtenir l'historique des fichiers individuels, à ma connaissance, donc cette méthode est meilleure.
Eric Lee
3
@EricLee Lorsque le dépôt old_b est fusionné, je reçois beaucoup de conflits de fusion. Est-ce attendu? J'obtiens CONFLICT (renommer / supprimer)
Jon
9
Lorsque j'essaye "dir -exclure old_a |% {git mv $ _. Nom old_a}", j'obtiens sh.exe ": dir: commande introuvable et sh.exe": git: commande introuvable. Utilisation de ceci fonctionne: ls -I old_a | xargs -I '{}' git mv '{}' old_a /
George
5
C'est 1(le numéro un) pour lset le «œil» capital pour xargs. Merci pour cette astuce!
Dominique Vial
149

Voici un moyen qui ne réécrit aucun historique, donc tous les ID de validation resteront valides. Le résultat final est que les fichiers du second dépôt se retrouveront dans un sous-répertoire.

  1. Ajoutez le deuxième dépôt en tant que télécommande:

    cd firstgitrepo/
    git remote add secondrepo username@servername:andsoon
    
  2. Assurez-vous d'avoir téléchargé tous les commits de secondrepo:

    git fetch secondrepo
    
  3. Créez une branche locale à partir de la deuxième branche du référentiel:

    git branch branchfromsecondrepo secondrepo/master
    
  4. Déplacez tous ses fichiers dans un sous-répertoire:

    git checkout branchfromsecondrepo
    mkdir subdir/
    git ls-tree -z --name-only HEAD | xargs -0 -I {} git mv {} subdir/
    git commit -m "Moved files to subdir/"
    
  5. Fusionnez la deuxième branche dans la branche principale du premier dépôt:

    git checkout master
    git merge --allow-unrelated-histories branchfromsecondrepo
    

Votre référentiel aura plus d'un commit racine, mais cela ne devrait pas poser de problème.

Flimm
la source
1
L'étape 2 ne fonctionne pas pour moi: fatal: Pas un nom d'objet valide: 'secondrepo / master'.
Keith
@Keith: assurez-vous que vous avez ajouté le deuxième référentiel en tant que télécommande nommée "secondrepo", et que ce référentiel a une branche nommée "master" (vous pouvez afficher les branches sur un référentiel distant avec la commande git remote show secondrepo)
Flimm
Je devais aussi aller le chercher. Entre 1 et 2, j'ai fait aller chercher secondrepo
sksamuel
@monkjack: J'ai modifié ma réponse pour inclure une étape de récupération de git. N'hésitez pas à modifier la réponse vous-même à l'avenir.
Flimm
4
@MartijnHeemels Pour l'ancienne version de Git, omettez simplement --allow-unrelated-histories. Voir l'historique de ce message de réponse.
Flimm
8

Quelques années se sont écoulées et il existe des solutions bien votées mais je veux partager la mienne car elle était un peu différente parce que je voulais fusionner 2 référentiels distants en un nouveau sans supprimer l'historique des référentiels précédents.

  1. Créez un nouveau référentiel dans Github.

    entrez la description de l'image ici

  2. Téléchargez le référentiel nouvellement créé et ajoutez l'ancien référentiel distant.

    git clone https://github.com/alexbr9007/Test.git
    cd Test
    git remote add OldRepo https://github.com/alexbr9007/Django-React.git
    git remote -v
    
  3. Récupère tous les fichiers de l'ancien référentiel pour créer une nouvelle branche.

    git fetch OldRepo
    git branch -a
    

    entrez la description de l'image ici

  4. Dans la branche principale, effectuez une fusion pour combiner l'ancien référentiel avec le nouveau créé.

    git merge remotes/OldRepo/master --allow-unrelated-histories
    

    entrez la description de l'image ici

  5. Créez un nouveau dossier pour stocker tout le nouveau contenu créé qui a été ajouté à partir de OldRepo et déplacez ses fichiers dans ce nouveau dossier.

  6. Enfin, vous pouvez télécharger les fichiers à partir des dépôts combinés et supprimer en toute sécurité OldRepo de GitHub.

J'espère que cela peut être utile pour toute personne confrontée à la fusion de référentiels distants.

abautista
la source
1
C'est la seule solution qui a fonctionné pour moi pour préserver l'histoire de Git. N'oubliez pas de supprimer le lien distant vers l'ancien dépôt avec git remote rm OldRepo.
Harubiyori
7

s'il vous plaît jeter un oeil à l'aide

git rebase --root --preserve-merges --onto

pour relier deux histoires au début de leur vie.

Si vous avez des chemins qui se chevauchent, corrigez-les avec

git filter-branch --index-filter

lorsque vous utilisez le journal, assurez-vous de "trouver des copies plus

git log -CC

de cette façon, vous trouverez tous les mouvements de fichiers dans le chemin.

Adam Dymitruk
la source
La documentation de Git recommande de ne pas rebaser ... git-scm.com/book/en/v2/Git-Branching-Rebasing#_rebase_peril
Stephen Turner
7

J'ai transformé la solution de @Flimm ceci en un git aliascomme ça (ajouté à mon ~/.gitconfig):

[alias]
 mergeRepo = "!mergeRepo() { \
  [ $# -ne 3 ] && echo \"Three parameters required, <remote URI> <new branch> <new dir>\" && exit 1; \
  git remote add newRepo $1; \
  git fetch newRepo; \
  git branch \"$2\" newRepo/master; \
  git checkout \"$2\"; \
  mkdir -vp \"${GIT_PREFIX}$3\"; \
  git ls-tree -z --name-only HEAD | xargs -0 -I {} git mv {} \"${GIT_PREFIX}$3\"/; \
  git commit -m \"Moved files to '${GIT_PREFIX}$3'\"; \
  git checkout master; git merge --allow-unrelated-histories --no-edit -s recursive -X no-renames \"$2\"; \
  git branch -D \"$2\"; git remote remove newRepo; \
}; \
mergeRepo"
Fredrik Erlandsson
la source
12
Juste curieux: faites-vous vraiment cela assez souvent pour avoir besoin d'un alias?
Parker Coates
1
Non, je ne me souviens pas comment le faire, donc un alias est juste un moyen pour moi de m'en souvenir.
Fredrik Erlandsson
1
Oui .. mais essayez de changer d'ordinateurs et oubliez de déplacer vos alias;)
quetzalcoatl
1
Quelle est la valeur de $GIT_PREFIX?
neowulf33
github.com/git/git/blob/… 'GIT_PREFIX' est défini comme retourné en exécutant 'git rev-parse --show-prefix' à partir du répertoire courant d'origine. Voir linkgit: git-rev-parse [1].
Fredrik Erlandsson
3

Cette fonction clone le repo distant dans le répertoire repo local:

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

Remarquer. Ce script peut réécrire les validations, mais enregistrera tous les auteurs et toutes les dates, cela signifie que les nouvelles validations auront un autre hachage, et si vous essayez de pousser les modifications sur le serveur distant, il ne pourra le faire qu'avec la touche de force, il réécrira également les validations sur le serveur. Veuillez donc faire des sauvegardes avant de lancer.

Profit!

Andrey Izman
la source
J'utilise zsh plutôt que bash et v2.13.0 de git. Peu importe ce que j'ai essayé, je n'ai pas pu me mettre git filter-branch --index-filterau travail. En général, je reçois un message d'erreur indiquant que le fichier d'index .new n'existe pas. Cela vous rappelle-t-il quelque chose?
Patrick Beard
@PatrickBeard Je ne sais pas zsh, vous pouvez créer un fichier séparé git-add-repo.shavec la fonction ci-dessus, à la fin du fichier mettez cette ligne git-add-repo "$@". Après cela, vous pouvez l'utiliser à partir de zsh comme cd current/git/packageetbash path/to/git-add-repo.sh https://github.com/example/example dir/to/save
Andrey Izman
Le problème a été discuté ici: stackoverflow.com/questions/7798142/… mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE" échoue parfois, vous devez donc ajouter un if test.
Patrick Beard
1
Je n'utiliserais pas cette méthode! J'ai essayé le script, naïvement et textuellement (je ne peux que me blâmer pour cette partie), et il a encombré mon dépôt git local. L'historique semblait globalement correct, mais en faisant un push git vers Github, le redouté "RPC a échoué; curl 55 SSL_write () a renvoyé l'erreur SYSCALL, errno = 32". J'ai essayé de le réparer, mais il était irréparablement cassé. J'ai fini par devoir reconstruire des choses dans un nouveau dépôt local.
Mason libéré le
@MasonFreed ce script crée un nouvel historique git avec un mélange des deux dépôts, donc il ne peut pas être poussé vers l'ancien dépôt, il nécessite d'en créer un nouveau ou de pousser avec la touche force, cela signifie qu'il réécrit votre dépôt sur le serveur
Andrey Izman
2

Suivez les étapes pour intégrer un référentiel dans un autre référentiel, en ayant un seul historique git en fusionnant les deux historiques git.

  1. Clonez les deux référentiels que vous souhaitez fusionner.

git clone [email protected]: utilisateur / parent-repo.git

git clone [email protected]: user / child-repo.git

  1. Aller au repo enfant

cd repo-enfant /

  1. exécutez la commande ci-dessous, remplacez le chemin d'accès my/new/subdir(3 occurrences) par la structure de répertoires où vous souhaitez avoir le dépôt enfant.

git filter-branch --prune-empty --tree-filter 'if [! -e mon / nouveau / sous-répertoire]; puis mkdir -p mon / nouveau / sous-répertoire git ls-tree --name-only $ GIT_COMMIT | xargs -I files mv files my / new / subdir fi '

  1. Aller au dépôt parent

cd ../parent-repo/

  1. Ajouter une télécommande au référentiel parent, pointant le chemin vers le référentiel enfant

git remote ajouter child-remote ../child-repo/

  1. Récupérer le repo enfant

git fetch child-remote

  1. Fusionner les histoires

git merge --allow-unrelated-histories child-remote / master

Si vous vérifiez le journal git dans le référentiel parent maintenant, il devrait fusionner les validations du référentiel enfant. Vous pouvez également voir la balise indiquant la source de validation.

L'article ci-dessous m'a aidé à intégrer un référentiel dans un autre référentiel, à avoir un seul historique git en fusionnant les deux historiques git.

http://ericlathrop.com/2014/01/combining-git-repositories/

J'espère que cela t'aides. Codage heureux!

AnoopGoudar
la source
L'étape 3 a échoué pour moi avec une erreur de syntaxe. Les points-virgules sont manquants. Fixgit filter-branch --prune-empty --tree-filter ' if [ ! -e my/new/subdir ]; then mkdir -p my/new/subdir; git ls-tree --name-only $GIT_COMMIT | xargs -I files mv files my/new/subdir; fi'
Yuri L
1

Dites que vous souhaitez fusionner dépôt adans b(je suppose qu'ils sont situés à côté de l'autre):

cd b
git remote add a ../a
git fetch a
git merge --allow-unrelated-histories a/master
git remote remove a

Si vous souhaitez placer adans un sous-répertoire, procédez comme suit avant les commandes ci-dessus:

cd a
git filter-repo --to-subdirectory-filter a
cd ..

Pour cela, vous devez git-filter-repoinstaller ( filter-branchest déconseillé ).

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

Plus d'informations ici .

x-yuri
la source