¿Cómo fusionas dos repositorios Git?

1622

Considere el siguiente escenario:

He desarrollado un pequeño proyecto experimental A en su propio repositorio de Git. Ahora ha madurado, y me gustaría que A sea parte de un proyecto más grande B, que tiene su propio gran repositorio. Ahora me gustaría agregar A como un subdirectorio de B.

¿Cómo fusiono A con B, sin perder el historial en ningún lado?

static_rtti
fuente
8
Si solo está tratando de combinar dos repositorios en uno, sin necesidad de mantener ambos repositorios, eche un vistazo a esta pregunta: stackoverflow.com/questions/13040958/…
Flimm
Para fusionar el repositorio de git en el directorio personalizado con guardar todos los comits, use stackoverflow.com/a/43340714/1772410
Andrey Izman el

Respuestas:

437

Una sola rama de otro repositorio se puede colocar fácilmente bajo un subdirectorio que conserva su historial. Por ejemplo:

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

Esto aparecerá como una confirmación única donde todos los archivos de la rama maestra Rails se agregan al directorio "rails". Sin embargo, el título del commit contiene una referencia al antiguo árbol de historia:

Agregue 'rails /' desde commit <rev>

¿Dónde <rev>está un hash SHA-1 commit? Todavía puedes ver la historia, culpar a algunos cambios.

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

Tenga en cuenta que no puede ver el prefijo de directorio desde aquí, ya que esta es una rama antigua real que queda intacta. Debe tratar esto como una confirmación de movimiento de archivo habitual: necesitará un salto adicional al alcanzarlo.

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

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

Hay soluciones más complejas como hacer esto manualmente o reescribir el historial como se describe en otras respuestas.

El comando git-subtree es parte de git-contrib oficial, algunos administradores de paquetes lo instalan por defecto (OS X Homebrew). Pero es posible que deba instalarlo usted mismo además de git.

Simon Perepelitsa
fuente
2
Aquí hay instrucciones sobre cómo instalar Git SubTree (a partir de junio de 2013): stackoverflow.com/a/11613541/694469 (y lo reemplacé git co v1.7.11.3 por ... v1.8.3).
KajMagnus
1
Gracias por el aviso sobre la respuesta a continuación. A partir de git 1.8.4, 'subárbol' todavía no está incluido (al menos no en Ubuntu 12.04 git ppa (ppa: git-core / ppa))
Matt Klein
1
Puedo confirmar que después de esto, git log rails/somefileno se mostrará el historial de confirmaciones de ese archivo, excepto la confirmación de fusión. Como sugirió @artfulrobot, verifique la respuesta de Greg Hewgill . Y es posible que deba usar git filter-branchen el repositorio que desea incluir.
Jifeng Zhang
66
O lea "Combinación de dos repositorios Git de Eric Lee en un repositorio sin perder el historial de archivos" saintgimp.org/2013/01/22/…
Jifeng Zhang
44
Como otros han dicho, ¡ git subtreepuede que no hagas lo que piensas! Vea aquí para una solución más completa.
Paul Draper
1909

Si quieres unirte project-aa 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

Tomado de: git fusionar diferentes repositorios?

Este método funcionó bastante bien para mí, es más corto y, en mi opinión, mucho más limpio.

En caso de que quiera poner project-aen un subdirectorio, puede utilizar git-filter-repo( filter-branchse desanime ). Ejecute los siguientes comandos antes de los comandos anteriores:

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

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

Nota: El --allow-unrelated-historiesparámetro solo existe desde git> = 2.9. Ver Git - Documentación de combinación de git / --allow-non-related-historories

Actualización : Agregado --tagscomo lo sugiere @jstadler para mantener las etiquetas.

Andresch Serj
fuente
8
Esto hizo el negocio por mí. ¡Funcionó como un encanto la primera vez con solo un conflicto en el archivo .gitignore! Preserva perfectamente la historia del commit. La gran ventaja sobre otros enfoques, además de la simplicidad, es que con esto no es necesario que haya una referencia continua al repositorio fusionado. Sin embargo, una cosa a tener en cuenta, si es un desarrollador de iOS como yo, es tener mucho cuidado de colocar el archivo de proyecto del repositorio de destino en el espacio de trabajo.
Max MacLeod
30
Gracias. Trabajó para mi. Necesitaba mover el directorio combinado a una subcarpeta, así que después de seguir los pasos anteriores, simplemente utilicégit mv source-dir/ dest/new-source-dir
Sid
13
El git mergepaso falla aquí con fatal: refusing to merge unrelated histories; --allow-unrelated-historiescorrige eso como se explica en los documentos .
ssc
19
--allow-unrelated-historiesfue introducido en git 2.9 . En versiones anteriores era comportamiento predeterminado.
Douglas Royds
11
Más corto: git fetch /path/to/project-a master; git merge --allow-unrelated-histories FETCH_HEAD.
jthill
614

Aquí hay dos posibles soluciones:

Submódulos

Copie el repositorio A en un directorio separado en un proyecto más grande B, o (quizás mejor) clone el repositorio A en un subdirectorio en el proyecto B. Luego use el submódulo git para hacer de este repositorio un submódulo de un repositorio B.

Esta es una buena solución para repositorios poco acoplados, donde el desarrollo en el repositorio A continúa, y la mayor parte del desarrollo es un desarrollo independiente separado en A. Vea también las páginas SubmoduleSupport y GitSubmoduleTutorial en Git Wiki.

Fusión de subárbol

Puede fusionar el repositorio A en un subdirectorio de un proyecto B utilizando la estrategia de fusión de subárbol . Esto se describe en Subtree Merging and You por 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

( --allow-unrelated-historiesSe necesita la opción para Git> = 2.9.0.)

O puede usar la herramienta git subtree ( repositorio en GitHub ) de apenwarr (Avery Pennarun), anunciada, por ejemplo, en su blog Una nueva alternativa a los submódulos Git: git subtree .


Creo que en su caso (A es parte de un proyecto más grande B) la solución correcta sería utilizar la combinación de subárbol .

Jakub Narębski
fuente
1
Esto funciona y parece preservar el historial, pero no de tal manera que pueda usarlo para diferir archivos o dividirlos en la fusión. ¿Me estoy perdiendo un paso?
jettero
56
Esto está incompleto . Sí, obtienes una gran cantidad de confirmaciones, pero ya no se refieren a las rutas correctas. git log dir-B/somefileno mostrará nada excepto la combinación. Ver la respuesta de Greg Hewgill hace referencia a este importante tema.
artfulrobot
2
IMPORTANTE: git pull --no-rebase -s subtree Bproject master Si no lo hace, y ha configurado pull para rebase automáticamente, terminará con "No se pudo analizar el objeto". Ver osdir.com/ml/git/2009-07/msg01576.html
Eric Bowman - abstracto -
44
Esta respuesta puede ser confusa porque tiene B como el subárbol combinado cuando en la pregunta era A. ¿Resultado de copiar y pegar?
vfclists
11
Si está tratando de pegar simplemente dos repositorios juntos, los submódulos y las fusiones de subárbol son la herramienta incorrecta para usar porque no conservan todo el historial de archivos (como han señalado otros comentaristas). Ver stackoverflow.com/questions/13040958/… .
Eric Lee
194

El enfoque de submódulo es bueno si desea mantener el proyecto por separado. Sin embargo, si realmente desea fusionar ambos proyectos en el mismo repositorio, entonces tiene que trabajar un poco más.

Lo primero sería usar git filter-branchpara reescribir los nombres de todo en el segundo repositorio para estar en el subdirectorio donde desea que terminen. Entonces, en lugar de foo.c, bar.htmltendrías projb/foo.cy projb/bar.html.

Entonces, deberías poder hacer algo como lo siguiente:

git remote add projb [wherever]
git pull projb

El git pullhará un git fetchseguido de un git merge. No debería haber conflictos, si el repositorio al que está llegando todavía no tiene un projb/directorio.

Una búsqueda adicional indica que algo similar se hizo para la fusión gitken git. Junio ​​C Hamano escribe sobre esto aquí: http://www.mail-archive.com/[email protected]/msg03395.html

Greg Hewgill
fuente
44
subárbol de combinación sería mejor solución, y no requieren volver a escribir la historia del proyecto incluida
Jakub Narębski
8
Me gustaría saber cómo usar git filter-branchpara lograr esto. En la página de manual dice lo contrario: hacer que subdir / se convierta en la raíz, pero no al revés.
artfulrobot
31
esta respuesta sería genial si explicara cómo usar filter-branch para lograr el resultado deseado
Anentropic
14
Encontré cómo usar filter-branch aquí: stackoverflow.com/questions/4042816/…
David Minor
3
Vea esta respuesta para la implementación del esquema de Greg.
Paul Draper
75

git-subtree es agradable, pero probablemente no sea el que quieres.

Por ejemplo, si projectAes el directorio creado en B, después git subtree,

git log projectA

enumera solo una confirmación: la fusión. Los commits del proyecto fusionado son para diferentes caminos, por lo que no se muestran.

La respuesta de Greg Hewgill se acerca más, aunque en realidad no dice cómo reescribir los caminos.


La solución es sorprendentemente simple.

(1) En 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

Nota: Esto reescribe el historial, por lo que si tiene la intención de continuar usando este repositorio A, es posible que desee clonar (copiar) primero una copia desechable.

Nota Bene: debe modificar la secuencia de comandos de sustitución dentro del comando sed en el caso de que utilice caracteres no ascii (o caracteres blancos) en los nombres de archivo o en la ruta. En ese caso, la ubicación del archivo dentro de un registro producido por "ls-files -s" comienza con comillas.

(2) Luego en B, corre

git pull path/to/A

Voila! Tiene un projectAdirectorio en B. Si ejecuta git log projectA, verá todas las confirmaciones de A.


En mi caso, quería dos subdirectorios, projectAy projectB. En ese caso, hice el paso (1) a B también.

Paul Draper
fuente
1
Parece que copió su respuesta de stackoverflow.com/a/618113/586086 ?
Andrew Mao
1
@ AndrewMao, creo que sí ... En realidad no puedo recordar. He usado este script bastante.
Paul Draper
66
Agregaría que \ t no funciona en OS X y que debe ingresar <tab>
Muneeb Ali
2
"$GIT_INDEX_FILE"debe ser citado (dos veces), de lo contrario su método fallará si, por ejemplo, la ruta contiene espacios.
Rob W
44
Si se está preguntando, para insertar una <tab> en osx, debe hacerloCtrl-V <tab>
casey
48

Si ambos repositorios tienen el mismo tipo de archivos (como dos repositorios Rails para diferentes proyectos), puede buscar datos del repositorio secundario en su repositorio actual:

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

y luego fusionarlo con el repositorio actual:

git merge --allow-unrelated-histories branch_name

Si su versión de Git es menor a 2.9, elimínela --allow-unrelated-histories.

Después de esto, pueden ocurrir conflictos. Puede resolverlos, por ejemplo, con git mergetool. kdiff3se puede usar únicamente con el teclado, por lo que se requieren 5 archivos de conflicto al leer el código solo unos minutos.

Recuerda terminar la fusión:

git commit
Smar
fuente
25

Seguí perdiendo el historial al usar la combinación, así que terminé usando rebase ya que en mi caso los dos repositorios son lo suficientemente diferentes como para no terminar fusionándose en cada confirmación:

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

=> resolver conflictos, luego continuar, tantas veces como sea necesario ...

git rebase --continue

Hacer esto lleva a un proyecto que tiene todos los commits de projA seguidos de commits de projB

Calahad
fuente
25

En mi caso, tenía un my-pluginrepositorio y un main-projectrepositorio, y quería fingir que my-pluginsiempre se había desarrollado en el pluginssubdirectorio de main-project.

Básicamente, reescribí la historia del my-pluginrepositorio para que pareciera que todo el desarrollo tuvo lugar en el plugins/my-pluginsubdirectorio. Luego, agregué el historial de desarrollo de my-pluginla main-projecthistoria y fusioné los dos árboles. Como no había ningún plugins/my-plugindirectorio ya presente en el main-projectrepositorio, esta fue una fusión trivial sin conflictos. El repositorio resultante contenía toda la historia de ambos proyectos originales y tenía dos raíces.

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

Versión larga

Primero, cree una copia del my-pluginrepositorio, porque vamos a reescribir la historia de este repositorio.

Ahora, navegue hasta la raíz del my-pluginrepositorio, revise su rama principal (probablemente master) y ejecute el siguiente comando. Por supuesto, debe sustituir my-pluginy pluginscualesquiera que sean sus nombres reales.

$ 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

Ahora para una explicación. git filter-branch --tree-filter (...) HEADejecuta el (...)comando en cada confirmación a la que se puede acceder HEAD. Tenga en cuenta que esto opera directamente en los datos almacenados para cada confirmación, por lo que no tenemos que preocuparnos por las nociones de "directorio de trabajo", "índice", "puesta en escena", etc.

Si ejecuta un filter-branchcomando que falla, dejará algunos archivos en el .gitdirectorio y la próxima vez que lo intente filter-branchse quejará de esto, a menos que proporcione la -fopción filter-branch.

En cuanto al comando real, no tuve mucha suerte bashpara hacer lo que quería, así que en lugar de eso utilizo zsh -cpara zshejecutar un comando. Primero configuro la extended_globopción, que es lo que habilita la ^(...)sintaxis en el mvcomando, así como la glob_dotsopción, que me permite seleccionar archivos de puntos (como .gitignore) con un globo ( ^(...)).

A continuación, uso el mkdir -pcomando para crear ambos pluginsy plugins/my-pluginal mismo tiempo.

Finalmente, utilizo la función zsh"glob negativo" ^(.git|plugins)para hacer coincidir todos los archivos en el directorio raíz del repositorio, excepto para .gitla my-plugincarpeta recién creada . (Es .gitposible que no sea necesario excluir aquí, pero intentar mover un directorio en sí mismo es un error).

En mi repositorio, la confirmación inicial no incluía ningún archivo, por lo que el mvcomando devolvió un error en la confirmación inicial (ya que no había nada disponible para mover). Por lo tanto, agregué un || truepara que git filter-branchno abortara.

La --allopción le dice filter-branchque reescriba el historial de todas las ramas en el repositorio, y el extra --es necesario para gitque lo interprete como parte de la lista de opciones para que las ramas se reescriban, en lugar de ser una opción para filter-branchsí mismo.

Ahora, navegue a su main-projectrepositorio y revise en qué rama desea fusionarse. Agregue su copia local del my-pluginrepositorio (con su historial modificado) como un control remoto main-projectcon:

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

Ahora tendrá dos árboles no relacionados en su historial de confirmación, que puede visualizar muy bien usando:

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

Para fusionarlos, use:

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

Tenga en cuenta que en Git anterior a 2.9.0, la --allow-unrelated-historiesopción no existe. Si está utilizando una de estas versiones, simplemente omita la opción: el mensaje de error que --allow-unrelated-historiesimpide también se agregó en 2.9.0.

No debe tener ningún conflicto de fusión. Si lo hace, probablemente significa que el filter-branchcomando no funcionó correctamente o que ya había un plugins/my-plugindirectorio main-project.

Asegúrate de ingresar un mensaje de confirmación explicativo para cualquier contribuyente futuro que se pregunte qué hacker está sucediendo para hacer un repositorio con dos raíces.

Puede visualizar el nuevo gráfico de confirmación, que debe tener dos confirmaciones de raíz, utilizando el git logcomando anterior . Tenga en cuenta que solo masterse fusionará la rama . Esto significa que si tiene un trabajo importante en otras my-pluginramas que desea fusionar en el main-projectárbol, debe abstenerse de eliminar el my-plugincontrol remoto hasta que haya realizado estas fusiones. Si no lo hace, entonces los commits de esas ramas aún estarán en el main-projectrepositorio, pero algunos serán inalcanzables y susceptibles a una eventual recolección de basura. (Además, tendrá que referirse a ellos por SHA, porque al eliminar un control remoto se eliminan sus ramas de seguimiento remoto).

Opcionalmente, después de haber fusionado todo lo que desea evitar my-plugin, puede eliminar el my-plugincontrol remoto utilizando:

$ git remote remove my-plugin

Ahora puede eliminar de forma segura la copia del my-pluginrepositorio cuyo historial ha cambiado. En mi caso, también agregué un aviso de desaprobación al my-pluginrepositorio real después de que la fusión se completó y se empujó.


Probado en Mac OS X El Capitan con git --version 2.9.0y zsh --version 5.2. Su experiencia puede ser diferente.

Referencias

Radon Rosborough
fuente
1
De donde --allow-unrelated-historiesvienen
xpto
3
@MarceloFilho Check man git-merge. Por defecto, el comando git merge se niega a fusionar historias que no comparten un antepasado común. Esta opción se puede utilizar para anular esta seguridad al fusionar historias de dos proyectos que comenzaron sus vidas de forma independiente. Como es una ocasión muy rara, no existe ninguna variable de configuración para habilitar esto de manera predeterminada y no se agregará.
Radon Rosborough
¿Debería estar disponible el git version 2.7.2.windows.1?
xpto
2
@MarceloFilho Esto se agregó en 2.9.0, pero en versiones anteriores no debería tener que pasar la opción (simplemente funcionará). github.com/git/git/blob/…
Radon Rosborough
Esto funcionó bien. Y pude usar la rama de filtro para reescribir los nombres de archivo donde quería en el árbol antes de la fusión. Supongo que hay más trabajo involucrado si necesita mover el historial además de la rama maestra.
codeDr
9

He estado tratando de hacer lo mismo durante días, estoy usando git 2.7.2. Subtree no conserva la historia.

Puede usar este método si no volverá a usar el proyecto anterior.

Te sugiero que ramifiques B primero y trabajes en la rama.

Estos son los pasos sin ramificación:

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 ahora registra alguno de los archivos en el subdirectorio A, obtendrá el historial completo

git log --follow A/<file>

Esta fue la publicación que me ayudó a hacer esto:

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

Rian
fuente
8

Si desea colocar los archivos de una rama en el repositorio B en un subárbol del repositorio A y también preservar el historial, siga leyendo. (En el ejemplo a continuación, supongo que queremos fusionar la rama maestra del repo B en la rama maestra del repo A).

En el repositorio A, primero haga lo siguiente para que el repositorio B esté disponible:

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

Ahora creamos una nueva sucursal (con solo una confirmación) en el repositorio A que llamamos new_b_root. La confirmación resultante tendrá los archivos que se confirmaron en la primera confirmación de la rama maestra del repositorio B pero que se colocaron en un subdirectorio llamado 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))"

Explicación: La --orphanopción para el comando de pago extrae los archivos de la rama maestra de A pero no crea ninguna confirmación. Podríamos haber seleccionado cualquier confirmación porque de todos modos borramos todos los archivos. Luego, sin comprometernos aún ( -n), seleccionamos la primera confirmación de la rama maestra de B. (La selección de cereza conserva el mensaje de confirmación original que no parece hacer un pago directo). Luego creamos el subárbol donde queremos colocar todos los archivos del repositorio B. Luego tenemos que mover todos los archivos que se introdujeron en el selección de cereza al subárbol. En el ejemplo anterior, solo hay un READMEarchivo para mover. Luego confirmamos nuestra confirmación de raíz B-repo y, al mismo tiempo, también conservamos la marca de tiempo de la confirmación original.

Ahora, crearemos una nueva B/masterrama encima de la recién creada new_b_root. Llamamos a la nueva sucursal b:

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

Ahora, fusionamos nuestra bsucursal en A/master:

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

Finalmente, puede eliminar las Bramas remotas y temporales:

git remote remove B
git branch -D new_b_root b

El gráfico final tendrá una estructura como esta:

ingrese la descripción de la imagen aquí

Finn Haakansson
fuente
Gran respuesta, gracias! Realmente extrañé en las otras respuestas con "git subtree" o "merge --allow-non-related-historories" de Andresch Serj que el subdirectorio no tenía el registro.
Ilendir
8

He reunido mucha información aquí sobre Stack OverFlow, etc., y he logrado armar un script que resuelve el problema para mí.

La advertencia es que solo tiene en cuenta la rama 'desarrollar' de cada repositorio y la fusiona en un directorio separado en un repositorio completamente nuevo.

Las etiquetas y otras ramas se ignoran; esto podría no ser lo que desea.

El script incluso maneja ramas y etiquetas de características, renombrándolas en el nuevo proyecto para que sepa de dónde provienen.

#!/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

También puede obtenerlo en http://paste.ubuntu.com/11732805

Primero cree un archivo con la URL a cada repositorio, por ejemplo:

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

Luego llame al script dando un nombre del proyecto y la ruta al script:

./mergeGitRepositories.sh eitchnet_test eitchnet.lst

El script en sí tiene muchos comentarios que deberían explicar lo que hace.

eitch
fuente
En lugar de dirigir a los lectores a una respuesta, publique la respuesta aquí (es decir, edite lo que dijo en ese comentario en esta respuesta).
josliber
1
Claro, solo pensé que era mejor no repetirme ... =)
eitch
Si cree que esta pregunta es idéntica a la otra, puede marcarla como un duplicado utilizando el enlace "marcar" debajo de la pregunta e indicando la otra pregunta. Si no se trata de una pregunta duplicada pero cree que se puede usar la misma respuesta exacta para resolver ambos problemas, simplemente publique la misma respuesta a ambos problemas (como ya lo ha hecho). ¡Gracias por contribuir!
josliber
¡Asombroso! No funcionó en Windows bash prompt, pero se ejecutó sin problemas desde un cuadro Vagrant que ejecuta ubuntu. ¡Qué ahorro de tiempo!
xverges
Happy to be of service =)
eitch
7

Sé que es mucho después del hecho, pero no estaba contento con las otras respuestas que encontré aquí, así que escribí esto:

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
fuente
2
Esto era exactamente lo que estaba buscando. ¡Gracias! Sin embargo, tuve que cambiar la línea 22 para:if [[ $dirname =~ ^.*\.git$ ]]; then
Heyman
2
^. * blarg $ es derrochador codicioso RE. Es mejor decir .blarg $ y omitir el ancla delantera.
jettero
7

Si está tratando de pegar simplemente dos repositorios juntos, los submódulos y las fusiones de subárbol son la herramienta incorrecta para usar porque no conservan todo el historial de archivos (como la gente ha notado en otras respuestas). Vea esta respuesta aquí para ver la forma simple y correcta de hacer esto.

Eric Lee
fuente
1
Su solución funciona bien solo para un nuevo repositorio, pero ¿qué tal si combina el repositorio en otro con conflictos de archivos?
Andrey Izman
6

Tuve un desafío similar, pero en mi caso, desarrollamos una versión de la base de código en el repositorio A, luego la clonamos en un nuevo repositorio, repositorio B, para la nueva versión del producto. Después de corregir algunos errores en el repositorio A, necesitábamos FI los cambios en el repositorio B. Terminamos haciendo lo siguiente:

  1. Agregar un control remoto al repositorio B que apuntaba al repositorio A (git remote add ...)
  2. Extrayendo la rama actual (no estábamos usando el maestro para la corrección de errores) (git pull remoteForRepoA bugFixBranch)
  3. Empujando fusiones a github

Trabajó un placer :)

David Lemphers
fuente
5

Similar a @Smar pero usa rutas de sistema de archivos, establecidas en PRIMARIO y SECUNDARIO:

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

Luego se fusionan manualmente.

(Adaptado de la publicación de Anar Manafov )

Turadg
fuente
5

Fusionando 2 repositorios

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
fuente
4

Cuando desee fusionar tres o más proyectos en una sola confirmación, siga los pasos que se describen en las otras respuestas ( remote add -f, merge). Luego, (suave) restablece el índice a la cabeza anterior (donde no ocurrió fusión). Agregar todos los archivos (git add -A ) y confírmelos (mensaje "Fusionar proyectos A, B, C y D en un proyecto). Este es ahora el id de confirmación del maestro.

Ahora, cree .git/info/graftscon el siguiente contenido:

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

Ejecutar git filter-branch -- head^..head head^2..head head^3..head. Si tiene más de tres ramas, solo agregue tanto head^n..headcomo tenga ramas. Para actualizar etiquetas, agregue --tag-name-filter cat. No siempre agregue eso, porque esto podría causar una reescritura de algunas confirmaciones. Para más detalles, vea la página del manual de filter-branch , busque "grafts".

Ahora, su último compromiso tiene los padres correctos asociados.

koppor
fuente
1
Espera, ¿por qué quieres fusionar tres proyectos en una sola confirmación?
Steve Bennett
Comencé con repositorio, repositorio-cliente y modelador como proyectos separados de git. Esto fue difícil para los compañeros de trabajo, por lo que me uní a ellos en un solo proyecto git. Para poder que la "raíz" del nuevo proyecto se origine en otros tres proyectos, quería tener un solo compromiso de fusión.
koppor
4

Para fusionar una A dentro de B:

1) En el proyecto A

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

2) En el proyecto B

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

En esta rama, haga todas las operaciones que necesita hacer y comprométalas.

C) Luego de vuelta al maestro y una fusión clásica entre las dos ramas:

git checkout master
git merge projectA
usuario123568943685
fuente
2

Esta función clonará el repositorio remoto en el directorio de repositorio local, después de fusionar se guardarán todas las confirmaciones, git logse mostrarán las confirmaciones originales y las rutas adecuadas:

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

Si realiza pequeños cambios, incluso puede mover archivos / directorios de repositorio fusionado a diferentes rutas, por ejemplo:

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"

Notices
Paths reemplaza via sed, así que asegúrese de que se movió en las rutas adecuadas después de la fusión.
El --allow-unrelated-historiesparámetro solo existe desde git> = 2.9.

Andrey Izman
fuente
1

El comando dado es la mejor solución posible, sugiero.

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

Fusiono proyectos ligeramente de forma manual, lo que me permite evitar tener que lidiar con conflictos de fusión.

primero, copie los archivos del otro proyecto como desee.

cp -R myotherproject newdirectory
git add newdirectory

siguiente tirón en la historia

git fetch path_or_url_to_other_repo

dile a git que se fusione en la historia de la última cosa traída

echo 'FETCH_HEAD' > .git/MERGE_HEAD

ahora se compromete sin embargo normalmente lo haría

git commit
Collin Anderson
fuente
0

Quería mover un proyecto pequeño a un subdirectorio de uno más grande. Como mi pequeño proyecto no tenía muchos commits, solía hacerlo git format-patch --output-directory /path/to/patch-dir. Luego, en el proyecto más grande, solía git am --directory=dir/in/project /path/to/patch-dir/*.

Esto se siente mucho menos aterrador y mucho más limpio que una rama de filtro. Por supuesto, puede no ser aplicable a todos los casos.

Miguel
fuente