Cómo mover files de un repository git a otro (no a un clon), preservando el historial

Nuestros repositorys Git comenzaron como parte de un repository SVN de monstruos únicos donde los proyectos individuales tenían cada uno su propio tree, así:

project1/branches /tags /trunk project2/branches /tags /trunk 

Obviamente, era bastante fácil mover files de uno a otro con svn mv . Pero en Git, cada proyecto está en su propio repository, y hoy me pidieron mover un subdirectory de project2 a project1 . Hice algo como esto:

 $ git clone project2 $ cd project2 $ git filter-branch --subdirectory-filter deeply/buried/java/source/directory/A -- --all $ git remote rm origin # so I don't accidentally the repo ;-) $ mkdir -p deeply/buried/different/java/source/directory/B $ for f in *.java; do > git mv $f deeply/buried/different/java/source/directory/B > done $ git commit -m "moved files to new subdirectory" $ cd .. $ $ git clone project1 $ cd project1 $ git remote add p2 ../project2 $ git fetch p2 $ git branch p2 remotes/p2/master $ git merge p2 # --allow-unrelated-histories for git 2.9 $ git remote rm p2 $ git push 

Pero eso parece bastante intrincado. ¿Hay una mejor manera de hacer este tipo de cosas en general? ¿O he adoptado el enfoque correcto?

Sí, golpear en el --subdirectory-filter de filter-branch de filter-branch era la key. El hecho de que lo usó esencialmente demuestra que no hay una manera más fácil: no tuvo más remedio que reescribir el historial, ya que quería terminar con un solo subset (renombrado) de los files, y esto por definición cambia los valores hash. Como ninguno de los commands estándar (por ejemplo, pull ) reescribe el historial, no hay forma de que pueda usarlos para lograr esto.

Podría refinar los detalles, por supuesto, algunas de sus clonaciones y ramificaciones no eran estrictamente necesarias, ¡pero el enfoque general es bueno! Es una pena que sea complicado, pero por supuesto, el objective de git no es facilitar la reescritura de la historia.

Si su historial es sensato, puede tomar las confirmaciones como parche y aplicarlas en el nuevo repository:

 cd repository git log --pretty=email --patch-with-stat --reverse --full-index --binary -- path/to/file_or_folder > patch cd ../another_repository git am < ../repository/patch 

O en una línea

 git log --pretty=email --patch-with-stat --reverse -- path/to/file_or_folder | (cd /path/to/new_repository && git am) 

(Tomado de los documentos de Exherbo )

Después de probar varios enfoques para mover un file o carpeta de un repository de Git a otro, el único que parece funcionar de manera confiable se describe a continuación.

Implica clonar el repository del que desea mover el file o la carpeta, mover ese file o carpeta a la raíz, reescribir el historial de Git, clonar el repository de destino y extraer el file o la carpeta con historial directamente en este repository de destino.

La etapa uno

  1. Haga una copy del repository A ya que los siguientes pasos hacen cambios importantes en esta copy que no debe presionar.

     git clone --branch <branch> --origin origin --progress -v <git repository A url> eg. git clone --branch master --origin origin --progress -v https://username@giturl/scm/projects/myprojects.git 

    (suponiendo que myprojects es el repository del que desea copyr)

  2. cd en él

     cd <git repository A directory> eg. cd /c/Working/GIT/myprojects 
  3. Elimine el enlace al repository original para evitar hacer cambios remotos por error (p. Ej. Presionando)

     git remote rm origin 
  4. Revise su historial y files, eliminando todo lo que no esté en el directory 1. El resultado es el contenido del directory 1 arrojado a la base del repository A.

     git filter-branch --subdirectory-filter <directory> -- --all eg. git filter-branch --subdirectory-filter subfolder1/subfolder2/FOLDER_TO_KEEP -- --all 
  5. Solo para mover un solo file: revise lo que quede y elimine todo, excepto el file deseado. (Puede que necesite eliminar los files que no desea con el mismo nombre y confirmar).

     git filter-branch -f --index-filter \ 'git ls-files -s | grep $'\t'FILE_TO_KEEP$ | GIT_INDEX_FILE=$GIT_INDEX_FILE.new \ git update-index --index-info && \ mv $GIT_INDEX_FILE.new $GIT_INDEX_FILE || echo "Nothing to do"' --prune-empty -- --all 

    p.ej. FILE_TO_KEEP = pom.xml para mantener solo el file pom.xml de FOLDER_TO_KEEP

Etapa dos

  1. Paso de limpieza

     git reset --hard 
  2. Paso de limpieza

     git gc --aggressive 
  3. Paso de limpieza

     git prune 

Es posible que desee importar estos files en el repository B dentro de un directory, no en la raíz:

  1. Hacer ese directory

     mkdir <base directory> eg. mkdir FOLDER_TO_KEEP 
  2. Mover files a ese directory

     git mv * <base directory> eg. git mv * FOLDER_TO_KEEP 
  3. Agregar files a ese directory

     git add . 
  4. Confirme sus cambios y estamos listos para fusionar estos files en el nuevo repository

     git commit 

Etapa tres

  1. Haga una copy del repository B si no tiene uno ya

     git clone <git repository B url> eg. git clone https://username@giturl/scm/projects/FOLDER_TO_KEEP.git 

    (suponiendo que FOLDER_TO_KEEP es el nombre del nuevo repository al que está copyndo)

  2. cd en él

     cd <git repository B directory> eg. cd /c/Working/GIT/FOLDER_TO_KEEP 
  3. Cree una connection remota al repository A como una bifurcación en el repository B

     git remote add repo-A-branch <git repository A directory> 

    (repo-A-branch puede ser cualquier cosa, es solo un nombre arbitrario)

     eg. git remote add repo-A-branch /c/Working/GIT/myprojects 
  4. Extraiga de esta twig (que contiene solo el directory que desea mover) en el repository B.

     git pull repo-A-branch master --allow-unrelated-histories 

    La extracción copy los files y el historial. Nota: Puede usar una fusión en lugar de una extracción, pero la extracción funciona mejor.

  5. Finalmente, es probable que desee limpiar un poco eliminando la connection remota al repository A

     git remote rm repo-A-branch 
  6. Presiona y estás listo.

     git push 

Encontré esto muy útil. Es un enfoque muy simple si crea parches que se aplican al nuevo repository. Vea la página vinculada para más detalles.

Solo contiene tres pasos (copydos del blog):

 # Setup a directory to hold the patches mkdir <patch-directory> # Create the patches git format-patch -o <patch-directory> --root /path/to/copy # Apply the patches in the new repo using a 3 way merge in case of conflicts # (merges from the other repo are not turned into patches). # The 3way can be omitted. git am --3way <patch-directory>/*.patch 

El único problema que tuve fue que no podía aplicar todos los parches al mismo time

 git am --3way <patch-directory>/*.patch 

En Windows obtuve un error InvalidArgument. Entonces tuve que aplicar todos los parches uno tras otro.

MANTENIENDO EL NOMBRE DEL DIRECTORIO

El subdirectory-filter (o el subtree de command más corto git) funciona bien, pero no funcionó para mí, ya que eliminan el nombre del directory de la información de confirmación. En mi caso, solo deseo fusionar partes de un repository en otro y conservar el historial CON el nombre de ruta completo.

Mi solución fue utilizar el filter de tree y simplemente eliminar los files y directorys no deseados de un clon temporal del repository de origen, luego extraer de ese clon en mi repository de destino en 5 sencillos pasos.

 # 1. clone the source git clone ssh://<user>@<source-repo url> cd <source-repo> # 2. remove the stuff we want to exclude git filter-branch --tree-filter "rm -rf <files to exclude>" --prune-empty HEAD # 3. move to target repo and create a merge branch (for safety) cd <path to target-repo> git checkout -b <merge branch> # 4. Add the source-repo as remote git remote add source-repo <path to source-repo> # 5. fetch it git pull source-repo master # 6. check that you got it right (better safe than sorry, right?) gitk 

Esta respuesta proporciona commands interesantes basados ​​en git am y presentados usando ejemplos, paso a paso.

Objetivo

  • Desea mover algunos o todos los files de un repository a otro.
  • Quieres mantener su historial.
  • Pero no te importa mantener tags y twigs.
  • Acepta historial limitado para files renombrados (y files en directorys renombrados).

Procedimiento

  1. Extrae el historial en formatting de correo electrónico usando
    git log --pretty=email -p --reverse --full-index --binary
  2. Reorganizar el tree de files y actualizar el cambio de nombre de file en el historial [opcional]
  3. Aplicar nueva historia con git am

1. Extracto del historial en formatting de correo electrónico

Ejemplo: extraer el historial de file4 , file4 y file5

 my_repo ├── dirA │ ├── file1 │ └── file2 ├── dirB ^ │ ├── subdir | To be moved │ │ ├── file3 | with history │ │ └── file4 | │ └── file5 v └── dirC ├── file6 └── file7 

Limpiar el destino del directory temporal

 export historydir=/tmp/mail/dir # Absolute path rm -rf "$historydir" # Caution when cleaning 

Limpia tu fuente de repository

 git commit ... # Commit your working files rm .gitignore # Disable gitignore git clean -n # Simulate removal git clean -f # Remove untracked file git checkout .gitignore # Restore gitignore 

Extrae el historial de cada file en formatting de correo electrónico

 cd my_repo/dirB find -name .git -prune -o -type d -o -exec bash -c 'mkdir -p "$historydir/${0%/*}" && git log --pretty=email -p --stat --reverse --full-index --binary -- "$0" > "$historydir/$0"' {} ';' 

Lamentablemente, la opción --follow o --find-copies-harder no se puede combinar con --reverse . Esta es la razón por la que el historial se corta cuando se cambia el nombre del file (o cuando se cambia el nombre de un directory principal).

Después: Historial temporal en formatting de correo electrónico

 /tmp/mail/dir ├── subdir │ ├── file3 │ └── file4 └── file5 

2. Reorganizar el tree de files y actualizar el cambio de nombre de file en el historial [opcional]

Supongamos que desea mover estos tres files en este otro repository (puede ser el mismo repository).

 my_other_repo ├── dirF │ ├── file55 │ └── file56 ├── dirB # New tree │ ├── dirB1 # was subdir │ │ ├── file33 # was file3 │ │ └── file44 # was file4 │ └── dirB2 # new dir │ └── file5 # = file5 └── dirH └── file77 

Por lo tanto, reorganice sus files:

 cd /tmp/mail/dir mkdir dirB mv subdir dirB/dirB1 mv dirB/dirB1/file3 dirB/dirB1/file33 mv dirB/dirB1/file4 dirB/dirB1/file44 mkdir dirB/dirB2 mv file5 dirB/dirB2 

Tu historial temporal es ahora:

 /tmp/mail/dir └── dirB ├── dirB1 │ ├── file33 │ └── file44 └── dirB2 └── file5 

Cambiar también los nombres de file dentro de la historia:

 cd "$historydir" find * -type f -exec bash -c 'sed "/^diff --git a\|^--- a\|^+++ b/s:\( [ab]\)/[^ ]*:\1/$0:g" -i "$0"' {} ';' 

Nota: Esto reescribe el historial para reflejar el cambio de ruta y nombre de file.
(es decir, el cambio de la nueva location / nombre dentro del nuevo repository)


3. Aplicar nueva historia

Su otro repository es:

 my_other_repo ├── dirF │ ├── file55 │ └── file56 └── dirH └── file77 

Aplique confirmaciones de files de historial temporal:

 cd my_other_repo find "$historydir" -type f -exec cat {} + | git am 

Su otro repository es ahora:

 my_other_repo ├── dirF │ ├── file55 │ └── file56 ├── dirB ^ │ ├── dirB1 | New files │ │ ├── file33 | with │ │ └── file44 | history │ └── dirB2 | kept │ └── file5 v └── dirH └── file77 

Usa el git status para ver la cantidad de commits listos para ser empujados 🙂

Nota: Como el historial se ha reescrito para reflejar la ruta y el cambio de nombre de file:
(es decir, en comparación con la location / nombre dentro del repository anterior)

  • No es necesario que git mv cambie la location / nombre del file.
  • No es necesario git log --follow para acceder al historial completo.

Truco adicional: detectar files renombrados / movidos dentro de su repository

Para enumerar los files que han sido renombrados:

 find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow {} ';' | grep '=>' 

Más personalizaciones: puede completar el command git log usando las opciones --find-copies-harder o --reverse . También puede eliminar las dos primeras columnas usando cut -f3- y grepping complete pattern '{. * =>. *}'.

 find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}' 

Habiendo tenido un picor similar a cero (aunque solo para algunos files de un repository dado), este script resultó ser realmente útil: git-import

La versión corta es que crea files de parche del file o directory dado ( $object ) del repository existente:

 cd old_repo git format-patch --thread -o "$temp" --root -- "$object" 

que luego se aplica a un nuevo repository:

 cd new_repo git am "$temp"/*.patch 

Para más detalles, mira hacia arriba:

  • la fuente documentada
  • git formatting-parche
  • git am

El que uso siempre está aquí http://blog.neutrino.es/2012/git-copy-a-file-or-directory-from-another-repository-preserving-history/ . Simple y rápido.

Para cumplir con los estándares de stackoverflow, este es el procedimiento:

 mkdir /tmp/mergepatchs cd ~/repo/org export reposrc=myfile.c #or mydir git format-patch -o /tmp/mergepatchs $(git log $reposrc|grep ^commit|tail -1|awk '{print $2}')^..HEAD $reposrc cd ~/repo/dest git am /tmp/mergepatchs/*.patch 

Utilizando la inspiración de http://blog.neutrino.es/2012/git-copy-a-file-or-directory-from-another-repository-preserving-history/ , creé esta function Powershell para hacer lo mismo, que tiene funcionó muy bien para mí hasta ahora:

 # Migrates the git history of a file or directory from one Git repo to another. # Start in the root directory of the source repo. # Also, before running this, I recommended that $destRepoDir be on a new branch that the history will be migrated to. # Inspinetworking by: http://blog.neutrino.es/2012/git-copy-a-file-or-directory-from-another-repository-preserving-history/ function Migrate-GitHistory { # The file or directory within the current Git repo to migrate. param([string] $fileOrDir) # Path to the destination repo param([string] $destRepoDir) # A temp directory to use for storing the patch file (optional) param([string] $tempDir = "\temp\migrateGit") mkdir $tempDir # git log $fileOrDir -- to list commits that will be migrated Write-Host "Generating patch files for the history of $fileOrDir ..." -ForegroundColor Cyan git format-patch -o $tempDir --root -- $fileOrDir cd $destRepoDir Write-Host "Applying patch files to restre the history of $fileOrDir ..." -ForegroundColor Cyan ls $tempDir -Filter *.patch ` | foreach { git am $_.FullName } } 

Uso para este ejemplo:

 git clone project2 git clone project1 cd project1 # Create a new branch to migrate to git checkout -b migrate-from-project2 cd ..\project2 Migrate-GitHistory "deeply\buried\java\source\directory\A" "..\project1" 

Después de hacer esto, puede reorganizar los files en la twig migrate-from-project2 antes de fusionarla.

Quería algo robusto y reutilizable (function one-command-and-go + undo) así que escribí el siguiente script bash. Trabajó para mí en varias ocasiones, así que pensé en compartirlo aquí.

Puede mover una carpeta arbitraria /path/to/foo desde repo1 a /some/other/folder/bar a repo2 (las routes de carpeta pueden ser iguales o diferentes, la distancia desde la carpeta raíz puede ser diferente).

Como solo va más allá de las confirmaciones que tocan los files en la carpeta de input (no en todas las confirmaciones del repository de origen), debería ser bastante rápido incluso en grandes repositorys fuente, si solo extraes una subcarpeta profundamente anidada que no se tocó en cada cometer.

Como lo que hace es crear una sucursal huérfana con todo el historial del repository anterior y luego fusionarla con HEAD, incluso funcionará en caso de conflictos de nombre de file (entonces tendría que resolver una fusión al final del curso) .

Si no hay conflictos de nombre de file, solo necesita hacer git commit al final para finalizar la fusión.

La desventaja es que probablemente no siga el cambio de nombre de los files (fuera de la carpeta REWRITE_FROM ) en el repository fuente: las requestes de extracción son bienvenidas en GitHub para acomodarse a eso.

Enlace de GitHub: git-move-folder-between-repos-keep-history

 #!/bin/bash # Copy a folder from one git repo to another git repo, # preserving full history of the folder. SRC_GIT_REPO='/d/git-experimental/your-old-webapp' DST_GIT_REPO='/d/git-experimental/your-new-webapp' SRC_BRANCH_NAME='master' DST_BRANCH_NAME='import-stuff-from-old-webapp' # Most likely you want the REWRITE_FROM and REWRITE_TO to have a trailing slash! REWRITE_FROM='app/src/main/static/' REWRITE_TO='app/src/main/static/' verifyPreconditions() { #echo 'Checking if SRC_GIT_REPO is a git repo...' && { test -d "${SRC_GIT_REPO}/.git" || { echo "Fatal: SRC_GIT_REPO is not a git repo"; exit; } } && #echo 'Checking if DST_GIT_REPO is a git repo...' && { test -d "${DST_GIT_REPO}/.git" || { echo "Fatal: DST_GIT_REPO is not a git repo"; exit; } } && #echo 'Checking if REWRITE_FROM is not empty...' && { test -n "${REWRITE_FROM}" || { echo "Fatal: REWRITE_FROM is empty"; exit; } } && #echo 'Checking if REWRITE_TO is not empty...' && { test -n "${REWRITE_TO}" || { echo "Fatal: REWRITE_TO is empty"; exit; } } && #echo 'Checking if REWRITE_FROM folder exists in SRC_GIT_REPO' && { test -d "${SRC_GIT_REPO}/${REWRITE_FROM}" || { echo "Fatal: REWRITE_FROM does not exist inside SRC_GIT_REPO"; exit; } } && #echo 'Checking if SRC_GIT_REPO has a branch SRC_BRANCH_NAME' && { cd "${SRC_GIT_REPO}"; git rev-parse --verify "${SRC_BRANCH_NAME}" || { echo "Fatal: SRC_BRANCH_NAME does not exist inside SRC_GIT_REPO"; exit; } } && #echo 'Checking if DST_GIT_REPO has a branch DST_BRANCH_NAME' && { cd "${DST_GIT_REPO}"; git rev-parse --verify "${DST_BRANCH_NAME}" || { echo "Fatal: DST_BRANCH_NAME does not exist inside DST_GIT_REPO"; exit; } } && echo '[OK] All preconditions met' } # Import folder from one git repo to another git repo, including full history. # # Internally, it rewrites the history of the src repo (by creating # a temporary orphaned branch; isolating all the files from REWRITE_FROM path # to the root of the repo, commit by commit; and rewriting them again # to the original path). # # Then it creates another temporary branch in the dest repo, # fetches the commits from the rewritten src repo, and does a merge. # # Before any work is done, all the preconditions are verified: all folders # and branches must exist (except REWRITE_TO folder in dest repo, which # can exist, but does not have to). # # The code should work reasonably on repos with reasonable git history. # I did not test pathological cases, like folder being created, deleted, # created again etc. but probably it will work fine in that case too. # # In case you realize something went wrong, you should be able to reverse # the changes by calling `undoImportFolderFromAnotherGitRepo` function. # However, to be safe, please back up your repos just in case, before running # the script. `git filter-branch` is a powerful but dangerous command. importFolderFromAnotherGitRepo(){ SED_COMMAND='s-\t\"*-\t'${REWRITE_TO}'-' verifyPreconditions && cd "${SRC_GIT_REPO}" && echo "Current working directory: ${SRC_GIT_REPO}" && git checkout "${SRC_BRANCH_NAME}" && echo 'Backing up current branch as FILTER_BRANCH_BACKUP' && git branch -f FILTER_BRANCH_BACKUP && SRC_BRANCH_NAME_EXPORTED="${SRC_BRANCH_NAME}-exported" && echo "Creating temporary branch '${SRC_BRANCH_NAME_EXPORTED}'..." && git checkout -b "${SRC_BRANCH_NAME_EXPORTED}" && echo 'Rewriting history, step 1/2...' && git filter-branch -f --prune-empty --subdirectory-filter ${REWRITE_FROM} && echo 'Rewriting history, step 2/2...' && git filter-branch -f --index-filter \ "git ls-files -s | sed \"$SED_COMMAND\" | GIT_INDEX_FILE=\$GIT_INDEX_FILE.new git update-index --index-info && mv \$GIT_INDEX_FILE.new \$GIT_INDEX_FILE" HEAD && cd - && cd "${DST_GIT_REPO}" && echo "Current working directory: ${DST_GIT_REPO}" && echo "Adding git remote pointing to SRC_GIT_REPO..." && git remote add old-repo ${SRC_GIT_REPO} && echo "Fetching from SRC_GIT_REPO..." && git fetch old-repo "${SRC_BRANCH_NAME_EXPORTED}" && echo "Checking out DST_BRANCH_NAME..." && git checkout "${DST_BRANCH_NAME}" && echo "Merging SRC_GIT_REPO/" && git merge "old-repo/${SRC_BRANCH_NAME}-exported" --no-commit && cd - } # If something didn't work as you'd expect, you can undo, tune the params, and try again undoImportFolderFromAnotherGitRepo(){ cd "${SRC_GIT_REPO}" && SRC_BRANCH_NAME_EXPORTED="${SRC_BRANCH_NAME}-exported" && git checkout "${SRC_BRANCH_NAME}" && git branch -D "${SRC_BRANCH_NAME_EXPORTED}" && cd - && cd "${DST_GIT_REPO}" && git remote rm old-repo && git merge --abort cd - } importFolderFromAnotherGitRepo #undoImportFolderFromAnotherGitRepo