Combina dos repositorios Git sin romper el historial de archivos

226

Necesito fusionar dos repositorios de Git en un tercer repositorio completamente nuevo. He encontrado muchas descripciones de cómo hacer esto usando una combinación de subárbol (por ejemplo, la respuesta de Jakub Narębski en ¿Cómo se combinan dos repositorios Git? ) Y siguiendo esas instrucciones en su mayoría funciona, excepto que cuando confirmo el subárbol fusionar todos los archivos de los repositorios antiguos se registran como nuevos archivos agregados. Puedo ver el historial de confirmación de los repositorios antiguos cuando lo hago git log, pero si lo hago git log <file>, solo muestra una confirmación para ese archivo: la fusión de subárbol. A juzgar por los comentarios sobre la respuesta anterior, no estoy solo al ver este problema, pero no he encontrado soluciones publicadas para ello.

¿Hay alguna forma de fusionar repositorios y dejar intacto el historial de archivos individuales?

Eric Lee
fuente
No estoy usando Git, pero en Mercurial primero haría una conversión si fuera necesario para arreglar las rutas de archivo de los repositorios a fusionar, y luego forzaría un repositorio al objetivo para obtener los conjuntos de cambios, y luego haría un fusión de las diferentes ramas. Esto se prueba y funciona;) Quizás esto también ayude a encontrar una solución para Git ... en comparación con el enfoque de combinación de subárbol, supongo que el paso de conversión es diferente cuando se reescribe el historial en lugar de simplemente mapear una ruta (si entiendo correctamente). Esto asegura una fusión suave sin ningún manejo especial de las rutas de archivo.
Lucero
También encontré esta pregunta útil stackoverflow.com/questions/1683531/…
nacross
Creé una pregunta de seguimiento. Puede ser interesante: combinar dos repositorios Git y mantener el historial maestro: stackoverflow.com/questions/42161910/…
Dimitri Dewaele
La solución automatizada que funcionó para mí fue stackoverflow.com/a/30781527/239408
xverges

Respuestas:

269

Resulta que la respuesta es mucho más simple si simplemente está tratando de pegar dos repositorios y hacer que parezca que fue así todo el tiempo en lugar de administrar una dependencia externa. Simplemente necesita agregar controles remotos a sus repositorios antiguos, fusionarlos con su nuevo maestro, mover los archivos y carpetas a un subdirectorio, confirmar el movimiento y repetir para todos los repositorios adicionales. Los submódulos, las fusiones de subárboles y las refinadas modificaciones tienen la intención de resolver un problema ligeramente diferente y no son adecuados para lo que estaba tratando de hacer.

Aquí hay un script de ejemplo de Powershell para pegar dos repositorios:

# 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"

Obviamente, podría fusionar old_b en old_a (que se convierte en el nuevo repositorio combinado) si prefiere hacerlo: modifique el script para adaptarlo.

Si desea traer también ramas de características en progreso, use esto:

# 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

Esa es la única parte no obvia del proceso: no es una fusión de subárbol, sino un argumento de la fusión recursiva normal que le dice a Git que cambiamos el nombre del objetivo y que ayuda a Git a alinear todo correctamente.

Escribí una explicación un poco más detallada aquí .

Eric Lee
fuente
16
Esta solución git mvno funciona tan bien. cuando más tarde use un git logen uno de los archivos movidos, solo obtendrá la confirmación del movimiento. Se pierde toda la historia previa. Esto es porque git mves realmente, git rm; git addpero en un solo paso .
mholm815
15
Es lo mismo que cualquier otra operación de mover / renombrar en Git: desde la línea de comando puedes obtener todo el historial haciendo git log --follow, o todas las herramientas de GUI lo hacen automáticamente. Con una combinación de subárbol no puede obtener el historial de archivos individuales, que yo sepa, por lo que este método es mejor.
Eric Lee
3
@EricLee Cuando se fusiona el repositorio old_b obtengo muchos conflictos de fusión. ¿Es eso esperado? Me sale CONFLICT (renombrar / eliminar)
Jon
99
Cuando intento "dir -exclude old_a |% {git mv $ _. Name old_a}", obtengo sh.exe ": dir: comando no encontrado y sh.exe": git: comando no encontrado. Usando esto funciona: ls -I old_a | xargs -I '{}' git mv '{}' old_a /
George
55
Este es 1(el número uno) para lsy 'ojo' capital para xargs. Gracias por este consejo!
Dominique Vial
149

Aquí hay una manera que no reescribe ningún historial, por lo que todas las ID de confirmación seguirán siendo válidas. El resultado final es que los archivos del segundo repositorio terminarán en un subdirectorio.

  1. Agregue el segundo repositorio como control remoto:

    cd firstgitrepo/
    git remote add secondrepo username@servername:andsoon
    
  2. Asegúrese de haber descargado todos los commits de secondrepo:

    git fetch secondrepo
    
  3. Cree una rama local a partir de la rama del segundo repositorio:

    git branch branchfromsecondrepo secondrepo/master
    
  4. Mueva todos sus archivos a un subdirectorio:

    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. Combina la segunda rama en la rama principal del primer repositorio:

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

Su repositorio tendrá más de una confirmación de raíz, pero eso no debería suponer un problema.

Flimm
fuente
1
El paso 2 no me funciona: fatal: no es un nombre de objeto válido: 'secondrepo / master'.
Keith
@Keith: asegúrese de haber agregado el segundo repositorio como un control remoto llamado "secondrepo", y que ese repositorio tenga una rama llamada "master" (puede ver sucursales en un repositorio remoto con el comando git remote show secondrepo)
Flimm
Tuve que ir a buscarlo para derribarlo también. Entre 1 y 2 hice git fetch secondrepo
sksamuel
@monkjack: he editado mi respuesta para incluir un paso git fetch. Siéntase libre de editar la respuesta usted mismo en el futuro.
Flimm
44
@MartijnHeemels Para la versión anterior de Git, simplemente omita --allow-unrelated-histories. Vea la historia de esta publicación de respuestas.
Flimm
8

Han pasado algunos años y hay soluciones bien votadas, pero quiero compartir la mía porque era un poco diferente porque quería fusionar 2 repositorios remotos en uno nuevo sin eliminar el historial de los repositorios anteriores.

  1. Crea un nuevo repositorio en Github.

    ingrese la descripción de la imagen aquí

  2. Descargue el repositorio recién creado y agregue el antiguo repositorio remoto.

    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. Obtenga todos los archivos del repositorio anterior para crear una nueva rama.

    git fetch OldRepo
    git branch -a
    

    ingrese la descripción de la imagen aquí

  4. En la rama maestra, combine para combinar el repositorio antiguo con el recién creado.

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

    ingrese la descripción de la imagen aquí

  5. Cree una nueva carpeta para almacenar todo el nuevo contenido creado que se agregó desde OldRepo y mueva sus archivos a esta nueva carpeta.

  6. Por último, puede cargar los archivos de los repositorios combinados y eliminar de forma segura el OldRepo de GitHub.

Espero que esto pueda ser útil para cualquiera que trabaje con la fusión de repositorios remotos.

abautista
fuente
1
Esta es la única solución que me funcionó para preservar el historial de git. No olvide eliminar el enlace remoto al repositorio anterior con git remote rm OldRepo.
Harubiyori
7

por favor, eche un vistazo al uso

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

para vincular dos historias al principio de sus vidas.

Si tienes rutas que se superponen, arréglalas con

git filter-branch --index-filter

cuando utilice el registro, asegúrese de "encontrar copias más difíciles" con

git log -CC

de esa manera encontrará cualquier movimiento de archivos en la ruta.

Adam Dymitruk
fuente
La documentación de Git recomienda no volver a crear
Stephen Turner
7

Convertí la solución de @Flimm this en algo git aliasasí (agregado a mi ~/.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
fuente
12
Solo curiosidad: ¿realmente haces esto con la frecuencia suficiente para necesitar un alias?
Parker Coates
1
No, pero nunca recuerdo cómo hacerlo, así que un alias es solo una forma de recordarlo.
Fredrik Erlandsson
1
Sí ... pero intenta cambiar las computadoras y olvidarte de mover tus alias;)
quetzalcoatl
1
¿Cuál es el valor de $GIT_PREFIX?
neowulf33
github.com/git/git/blob/… 'GIT_PREFIX' se configura como devuelto al ejecutar 'git rev-parse --show-prefix' desde el directorio actual original. Ver linkgit: git-rev-parse [1].
Fredrik Erlandsson
3

Esta función clonará el repositorio remoto en el directorio de repositorio 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"
}

Cómo utilizar:

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

Aviso. Este script puede reescribir confirmaciones, pero guardará todos los autores y fechas, significa que las nuevas confirmaciones tendrán otros valores hash, y si intenta enviar cambios al servidor remoto, solo podrá presionar la tecla forzar, también reescribirá confirmaciones en el servidor. Así que haga copias de seguridad antes de iniciar.

¡Lucro!

Andrey Izman
fuente
Estoy usando zsh en lugar de bash, y v2.13.0 de git. No importa lo que he intentado, no he podido ir git filter-branch --index-filtera trabajar. Por lo general, recibo un mensaje de error de que el archivo de índice .new no existe. ¿Suena eso?
Patrick Beard
@PatrickBeard No sé zsh, puedes crear un archivo separado git-add-repo.shcon la función anterior, al final del archivo pon esta línea git-add-repo "$@". Después de eso, puede usarlo desde zsh like cd current/git/packageybash path/to/git-add-repo.sh https://github.com/example/example dir/to/save
Andrey Izman
El problema se discutió aquí: stackoverflow.com/questions/7798142/… mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE" falla a veces, por lo que debe agregar un if test.
Patrick Beard
1
¡No usaría este método! Probé el guión, ingenua y literalmente (solo puedo culparme por esa parte), y golpeó mi repositorio local de git. La historia parecía en su mayoría correcta, pero al hacer un git push a Github resultó el temido "RPC falló; curl 55 SSL_write () devolvió el error SYSCALL, errno = 32". Traté de repararlo, pero estaba irreparablemente roto. Terminé teniendo que reconstruir cosas en un nuevo repositorio local.
Mason liberado
@MasonFreed, este script crea un nuevo historial de git con la combinación de ambos repositorios, por lo que no se puede empujar al repositorio anterior, requiere crear uno nuevo o presionar con la tecla de fuerza, lo que significa que reescribirá su repositorio en el servidor
Andrey Izman
2

Siga los pasos para integrar un repositorio en otro repositorio, teniendo un solo historial de git fusionando ambos historiales de git.

  1. Clone los repositorios que desea fusionar.

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

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

  1. Ir al repositorio de niños

cd child-repo /

  1. ejecute el siguiente comando, reemplace la ruta my/new/subdir(3 veces) con la estructura del directorio donde desea tener el repositorio secundario.

git filter-branch --prune-empty --tree-filter 'if [! -e mi / nuevo / subdir]; luego mkdir -p my / new / subdir git ls-tree --name-only $ GIT_COMMIT | xargs -I archivos mv archivos my / new / subdir fi '

  1. Ir al repositorio principal

cd ../parent-repo/

  1. Agregue un remoto al repositorio principal, señalando la ruta al repositorio secundario

git remote add child-remote ../child-repo/

  1. Fetch the child repo

git fetch child-remote

  1. Fusionar las historias

git merge --allow-non-related-historories child-remote / master

Si marca el registro de git en el repositorio principal ahora, debería hacer que se fusionen los compromisos del repositorio secundario. También puede ver la etiqueta que indica desde la fuente de confirmación.

El siguiente artículo me ayudó a incrustar un repositorio en otro repositorio, teniendo un solo historial de git al fusionar ambos historiales de git.

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

Espero que esto ayude. ¡Feliz codificación!

AnoopGoudar
fuente
El paso 3 falló para mí con un error de sintaxis. Faltan los semicolones. Fijargit 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

Digamos que quiere fusionar repositorio aen b(estoy asumiendo que están situados uno junto al otro):

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

En caso de que desee colocar aen un subdirectorio, haga lo siguiente antes de los comandos anteriores:

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

Para esto necesita git-filter-repoinstalado ( filter-branchse desaconseja ).

Un ejemplo de fusión de 2 grandes repositorios, colocando uno de ellos en un subdirectorio: https://gist.github.com/x-yuri/9890ab1079cf4357d6f269d073fd9731

Más sobre esto aquí .

x-yuri
fuente