Separe muchos subdirectorios en un nuevo repositorio Git separado

135

Esta pregunta se basa en el subdirectorio Detach en un repositorio Git separado

En lugar de separar un solo subdirectorio, quiero separar un par. Por ejemplo, mi árbol de directorios actual se ve así:

/apps
  /AAA
  /BBB
  /CCC
/libs
  /XXX
  /YYY
  /ZZZ

Y me gustaría esto en su lugar:

/apps
  /AAA
/libs
  /XXX

El --subdirectory-filterargumento git filter-branchno funcionará porque elimina todo excepto el directorio dado la primera vez que se ejecuta. Pensé que usar el --index-filterargumento para todos los archivos no deseados funcionaría (aunque sea tedioso), pero si intento ejecutarlo más de una vez, recibo el siguiente mensaje:

Cannot create a new backup.
A previous backup already exists in refs/original/
Force overwriting the backup with -f

¿Algunas ideas? TIA

prisionero
fuente

Respuestas:

155

En lugar de tener que lidiar con una subshell y usar ext glob (como sugirió kynan), intente este enfoque mucho más simple:

git filter-branch --index-filter 'git rm --cached -qr --ignore-unmatch -- . && git reset -q $GIT_COMMIT -- apps/AAA libs/XXX' --prune-empty -- --all

Como lo menciona void.pointer en su comentario , esto eliminará todo excepto apps/AAAy libs/XXXdel repositorio actual.

Podar confirmaciones de fusión vacías

Esto deja muchas combinaciones vacías. Estos pueden eliminarse con otro pase como lo describe raphinesse en su respuesta :

git filter-branch --prune-empty --parent-filter \
'sed "s/-p //g" | xargs -r git show-branch --independent | sed "s/\</-p /g"'

⚠️ Advertencia : lo anterior debe usar la versión de GNU sedy, de lo xargscontrario, eliminaría todos los commits como xargsfallidos. brew install gnu-sed findutilsy luego use gsedy gxargs:

git filter-branch --prune-empty --parent-filter \
'gsed "s/-p //g" | gxargs git show-branch --independent | gsed "s/\</-p /g"' 
David Smiley
fuente
44
además, la bandera --ignore-unmatch debe pasarse a git rm, de lo contrario falló para el primer commit para mí (el repositorio fue creado con clon git svn en mi caso)
Pontomedon
8
Suponiendo que tenga etiquetas en la mezcla, probablemente debería agregar --tag-name-filter cata sus parámetros
Yonatan
16
¿Podría agregar más información que explique lo que está haciendo este largo comando?
Burhan Ali
44
Estoy gratamente sorprendido de que esto funcione perfectamente en Windows usando git bash, ¡uf!
Dai
3
@BurhanAli Por cada confirmación en el historial, está eliminando todos los archivos, excepto los que desea conservar. Cuando todo está hecho, solo le queda la parte del árbol que especificó, junto con solo ese historial.
void.pointer
39

Pasos manuales con comandos simples de git

El plan es dividir directorios individuales en sus propios repositorios, luego fusionarlos. Los siguientes pasos manuales no empleaban scripts geek-to-use, sino comandos fáciles de entender y podían ayudar a fusionar subcarpetas N adicionales en otro repositorio único.

Dividir

Supongamos que su repositorio original es: original_repo

1 - Aplicaciones divididas:

git clone original_repo apps-repo
cd apps-repo
git filter-branch --prune-empty --subdirectory-filter apps master

2 - Librerías divididas

git clone original_repo libs-repo
cd libs-repo
git filter-branch --prune-empty --subdirectory-filter libs master

Continúe si tiene más de 2 carpetas. Ahora tendrá dos repositorios git nuevos y temporales.

Conquista mediante la fusión de aplicaciones y bibliotecas

3 - Prepare el nuevo repositorio:

mkdir my-desired-repo
cd my-desired-repo
git init

Y deberá realizar al menos una confirmación. Si se deben omitir las siguientes tres líneas, su primer repositorio aparecerá inmediatamente debajo de la raíz de su repositorio:

touch a_file_and_make_a_commit # see user's feedback
git add a_file_and_make_a_commit
git commit -am "at least one commit is needed for it to work"

Con el archivo temporal comprometido, el mergecomando en la sección posterior se detendrá como se esperaba.

Tomando de los comentarios del usuario, en lugar de agregar un archivo aleatorio como a_file_and_make_a_commit, puede elegir agregar un .gitignore, o README.mdetc.

4 - Combinar repositorio de aplicaciones primero:

git remote add apps-repo ../apps-repo
git fetch apps-repo
git merge -s ours --no-commit apps-repo/master # see below note.
git read-tree --prefix=apps -u apps-repo/master
git commit -m "import apps"

Ahora debería ver el directorio de aplicaciones dentro de su nuevo repositorio. git logdebería mostrar todos los mensajes de confirmación históricos relevantes.

Nota: como Chris señaló a continuación en los comentarios, para la versión más nueva (> = 2.9) de git, debe especificar --allow-unrelated-historiescongit merge

5 - Fusionar libs repo a continuación de la misma manera:

git remote add libs-repo ../libs-repo
git fetch libs-repo
git merge -s ours --no-commit libs-repo/master # see above note.
git read-tree --prefix=libs -u libs-repo/master
git commit -m "import libs"

Continúe si tiene más de 2 repositorios para fusionar.

Referencia: fusionar un subdirectorio de otro repositorio con git

chfw
fuente
44
Desde git 2.9 necesita usar --allow-non-related-historories en los comandos de fusión. De lo contrario, esto parece haber funcionado bien para mí.
Chris
1
¡Genio! Muchas gracias por esto. Las respuestas iniciales que había visto, usando un filtro de árbol en un repositorio muy grande, tenían a git prediciendo tomar más de 26 horas para completar las reescrituras de git. Mucho más feliz con este enfoque simple pero repetible, y he movido con éxito 4 subcarpetas a un nuevo repositorio con todo el historial de confirmación esperado.
Shuttsy
1
Puede usar la primera confirmación para una "Confirmación inicial" que agrega .gitignorey README.mdarchivos.
Jack Miller
2
Desafortunadamente, este enfoque parece romper el historial de seguimiento de los archivos agregados en el git merge .. git read-treepaso, ya que los registra como archivos recientemente agregados y todas mis git guis no hacen la conexión a sus confirmaciones anteriores.
Dai
1
@ksadjad, no tengo idea, para ser honesto. El punto central de la fusión manual es seleccionar los directorios para formar el nuevo repositorio y mantener sus historiales de confirmación. No estoy seguro de cómo manejar tal situación en la que un commit coloca los archivos en dirA, dirB, dirDrop y solo se eligen dirA y dirB para el nuevo repositorio, cómo debe relacionarse el historial de commit con el original.
chfw
27

¿Por qué querrías correr filter-branchmás de una vez? Puede hacerlo todo en un barrido, por lo que no es necesario forzarlo (tenga en cuenta que debe extglobhabilitarlo en su shell para que esto funcione):

git filter-branch --index-filter "git rm -r -f --cached --ignore-unmatch $(ls -xd apps/!(AAA) libs/!(XXX))" --prune-empty -- --all

Esto debería deshacerse de todos los cambios en los subdirectorios no deseados y mantener todas sus ramas y confirmaciones (a menos que solo afecten a los archivos en los subdirectorios eliminados, en virtud de --prune-empty) - no hay problema con confirmaciones duplicadas, etc.

Después de esta operación, los directorios no deseados se enumerarán como no seguidos por git status.

El $(ls ...)es necesario st el extglobse evalúa por su shell en lugar del filtro de índice, que utiliza la shorden interna eval(donde extglobno está disponible). Consulte ¿Cómo habilito las opciones de shell en git? para más detalles sobre eso.

kynan
fuente
1
Idea interesante. Tengo un problema similar pero no pude hacerlo funcionar, consulte stackoverflow.com/questions/8050687/…
manol
Esto es más o menos lo que necesitaba, aunque tenía un poco de archivos y carpetas en mi repositorio ... Gracias :)
notlesh
1
hm. incluso con extglob activado, aparece un error cerca de mi paréntesis: error de sintaxis cerca del token inesperado `('mi comando se parece a: git filter-branch -f --index-filter" git rm -r -f --cached - -ignore-unmatch src / css / themes /! (some_theme *) "--prune-empty - --todos los ls con src / css / themes /! (some_theme *) devuelve todos los otros temas para que extglob parezca estar trabajando ...
robdodson 02 de
2
@MikeGraf No creo que eso dé el resultado deseado: el escape coincidiría con un literal "!" etc. en tu camino.
kynan
1
La respuesta de @ david-smiley (más reciente) utiliza un enfoque muy similar, pero tiene la ventaja de depender exclusivamente de gitcomandos y, por lo tanto, no es tan susceptible a las diferencias en cómo lsse interpreta en los sistemas operativos, como descubrió @Bae.
Jeremy Caney
20

Respondiendo mi propia pregunta aquí ... después de muchas pruebas y errores.

Logré hacer esto usando una combinación de git subtreey git-stitch-repo. Estas instrucciones se basan en:

Primero, saqué los directorios que quería mantener en su propio repositorio separado:

cd origRepo
git subtree split -P apps/AAA -b aaa
git subtree split -P libs/XXX -b xxx

cd ..
mkdir aaaRepo
cd aaaRepo
git init
git fetch ../origRepo aaa
git checkout -b master FETCH_HEAD

cd ..
mkdir xxxRepo
cd xxxRepo
git init
git fetch ../origRepo xxx
git checkout -b master FETCH_HEAD

Luego creé un nuevo repositorio vacío e importé / cosí los dos últimos:

cd ..
mkdir newRepo
cd newRepo
git init
git-stitch-repo ../aaaRepo:apps/AAA ../xxxRepo:libs/XXX | git fast-import

Esto crea dos ramas master-Ay master-Bcada una contiene el contenido de uno de los repositorios cosidos. Para combinarlos y limpiarlos:

git checkout master-A
git pull . master-B
git checkout master
git branch -d master-A 
git branch -d master-B

Ahora no estoy muy seguro de cómo / cuándo sucede esto, pero después del primero checkouty el pull, el código se fusiona mágicamente en la rama maestra (¡se aprecia cualquier idea de lo que está sucediendo aquí!)

Todo parece haber funcionado como se esperaba, excepto que si miro a través del newRepohistorial de confirmación, hay duplicados cuando el conjunto de cambios afectó a ambos apps/AAAy libs/XXX. Si hay una manera de eliminar duplicados, entonces sería perfecto.

prisionero
fuente
Herramientas ordenadas que encontraste aquí. Información sobre "pago": "git pull" es lo mismo que "git fetch && git merge". La parte "buscar" es inocuo ya que está "buscando localmente". Así que creo que este comando de pago es el mismo que "git merge master-B", que es un poco más evidente. Ver kernel.org/pub/software/scm/git/docs/git-pull.html
phord
1
Desafortunadamente, la herramienta git-stitch-repo está rota debido a malas dependencias hoy en día.
Henrik
@Henrik ¿Qué problema estabas experimentando exactamente? Funciona para mí, aunque tuve que agregar export PERL5LIB="$PERL5LIB:/usr/local/git/lib/perl5/site_perl/"a mi configuración de bash para que pudiera encontrar Git.pm. Luego lo instalé con cpan.
Es posible utilizar git subtree addpara realizar esta tarea. Ver stackoverflow.com/a/58253979/1894803
laconbass el
7

He escrito un filtro git para resolver exactamente este problema. Tiene el fantástico nombre de git_filter y se encuentra en github aquí:

https://github.com/slobobaby/git_filter

Se basa en el excelente libgit2.

Necesitaba dividir un repositorio grande con muchos commits (~ 100000) y las soluciones basadas en git filter-branch tardaron varios días en ejecutarse. git_filter tarda un minuto en hacer lo mismo.

slobobaby
fuente
7

Use la extensión git 'git splits'

git splitses un script bash que es un contenedor git branch-filterque creé como una extensión git, basado en la solución de jkeating .

Fue hecho exactamente para esta situación. Para su error, intente usar la git splits -fopción para forzar la eliminación de la copia de seguridad. Debido a que git splitsopera en una nueva rama, no reescribirá su rama actual, por lo que la copia de seguridad es extraña. Consulte el archivo Léame para obtener más detalles y asegúrese de usarlo en una copia / clon de su repositorio (¡por las dudas!) .

  1. instalar git splits.
  2. Dividir los directorios en una sucursal local. #change into your repo's directory cd /path/to/repo #checkout the branch git checkout XYZ
    #split multiple directories into new branch XYZ git splits -b XYZ apps/AAA libs/ZZZ

  3. Crea un repositorio vacío en alguna parte. Asumiremos que hemos creado un repositorio vacío llamado xyzen GitHub que tiene ruta:[email protected]:simpliwp/xyz.git

  4. Empuje al nuevo repositorio. #add a new remote origin for the empty repo so we can push to the empty repo on GitHub git remote add origin_xyz [email protected]:simpliwp/xyz.git #push the branch to the empty repo's master branch git push origin_xyz XYZ:master

  5. Clone el repositorio remoto recién creado en un nuevo directorio local
    #change current directory out of the old repo cd /path/to/where/you/want/the/new/local/repo #clone the remote repo you just pushed to git clone [email protected]:simpliwp/xyz.git

AndrewD
fuente
No parece posible agregar archivos a la división y actualizarlos más tarde, ¿verdad?
Alex
Esto parece retrasarse en mi repositorio con toneladas de commits
Shinta Smith
git-split parece usar git --index filter que es extremadamente lento en comparación con --subdirectory-filter. Para algunos repositorios, puede ser una opción viable, pero para repositorios grandes (múltiples gigabytes, confirmaciones de 6 dígitos): el filtro de índice tarda semanas en ejecutarse, incluso en hardware en la nube dedicado.
Jostein Kjønigsen
6
git clone [email protected]:thing.git
cd thing
git fetch
for originBranch in `git branch -r | grep -v master`; do
    branch=${originBranch:7:${#originBranch}}
    git checkout $branch
done
git checkout master

git filter-branch --index-filter 'git rm --cached -qr --ignore-unmatch -- . && git reset -q $GIT_COMMIT -- dir1 dir2 .gitignore' --prune-empty -- --all

git remote set-url origin [email protected]:newthing.git
git push --all
Richard Barraclough
fuente
Leer todos los otros comentarios me llevó por el buen camino. Sin embargo, su solución simplemente funciona. Importa todas las ramas y funciona con múltiples directorios. ¡Excelente!
jschober
1
El forbucle es digno de reconocer, ya que otras respuestas similares no lo incluyen. Si no tiene una copia local de cada rama en su clon, filter-branchno las tomará en cuenta como parte de su reescritura, lo que podría excluir los archivos introducidos en otras ramas, pero que aún no se fusionaron con su rama actual. (Aunque también vale la pena hacer una visita git fetcha cualquier sucursal que haya verificado previamente para asegurarse de que permanezca actualizada).
Jeremy Caney,
5

Una solución fácil: git-filter-repo

Tuve un problema similar y, después de revisar los diversos enfoques enumerados aquí, descubrí git-filter-repo . Se recomienda como alternativa a git-filter-branch en la documentación oficial de git aquí .

Para crear un nuevo repositorio a partir de un subconjunto de directorios en un repositorio existente, puede usar el comando:

git filter-repo --path <file_to_remove>

Filtra varios archivos / carpetas encadenándolos:

git filter-repo --path keepthisfile --path keepthisfolder/

Entonces, para responder la pregunta original , con git-filter-repo solo necesitarías el siguiente comando:

git filter-repo --path apps/AAA/ --path libs/XXX/
elmo
fuente
Esta es definitivamente una gran respuesta. El problema con todas las demás soluciones es que no pude extraer el contenido de TODAS las ramas de un directorio. Sin embargo, git filter-repo recuperó la carpeta de todas las ramas y reescribió el historial perfectamente, como limpiar todo el árbol de todo lo que no necesitaba.
Teodoro
3

Si. Forzar la sobrescritura de la copia de seguridad mediante el uso del -findicador en llamadas posteriores a filter-branchpara anular esa advertencia. :) De lo contrario, creo que tiene la solución (es decir, erradicar un directorio no deseado a la vez con filter-branch).

Jakob Borg
fuente
-4

Elimine la copia de seguridad presente en el directorio .git en refs / original como sugiere el mensaje. El directorio está oculto.

usuario5200576
fuente