Como mover arquivos de um repository do git para outro (não um clone), preservando o histórico

Nossos repositorys Git começaram como partes de um único repository SVN de monstros onde os projetos individuais tiveram sua própria tree da seguinte forma:

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

Obviamente, foi muito fácil mover arquivos de um para outro com o svn mv . Mas no Git, cada projeto está em seu próprio repository, e hoje me pediram para mover um subdiretório de project2 para project1 . Eu fiz algo assim:

 $ 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 

Mas isso parece bastante complicado. Existe uma maneira melhor de fazer esse tipo de coisa em geral? Ou adotei a abordagem correta?

Observe que isso envolve mesclar o histórico em um repository existente, em vez de simplesmente criar um novo repository independente de parte de outro ( como em uma pergunta anterior ).

Sim, acertar o --subdirectory-filter do filter-branch de filter-branch era fundamental. O fato de você ter usado essencialmente prova que não há um caminho mais fácil – você não teve escolha senão rewrite o histórico, já que você queria acabar com apenas um subconjunto (renomeado) dos arquivos, e isso, por definição, altera os hashes. Como nenhum dos comandos padrão (por exemplo, pull ) reescreve a história, não há como usá-los para realizar isso.

Você poderia refinar os detalhes, é claro – algumas de suas clonagens e ramificações não eram estritamente necessárias – mas a abordagem geral é boa! É uma pena que é complicado, mas é claro que o objective do git não é facilitar a reescrita da história.

Se o seu histórico é são, você pode pegar os commits como patch e aplicá-los no novo 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 

Ou em uma linha

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

(Extraído dos documentos de Exherbo )

Tendo tentado várias abordagens para mover um arquivo ou pasta de um repository Git para outro, o único que parece funcionar de forma confiável é descrito abaixo.

Envolve a clonagem do repository do qual você deseja mover o arquivo ou a pasta, movendo esse arquivo ou pasta para a raiz, reescrevendo o histórico do Git, clonando o repository de destino e puxando o arquivo ou pasta com histórico diretamente para este repository de destino.

Estágio um

  1. Faça uma cópia do repository A conforme as etapas a seguir fizerem grandes alterações nessa cópia que você não deve enviar!

     git clone --branch  --origin origin --progress -v  eg. git clone --branch master --origin origin --progress -v https://username@giturl/scm/projects/myprojects.git 

    (assumindo que myprojects é o repository do qual você deseja copiar)

  2. cd para ele

     cd  eg. cd /c/Working/GIT/myprojects 
  3. Exclua o link para o repository original para evitar fazer alterações remotas acidentalmente (por exemplo, empurrando)

     git remote rm origin 
  4. Percorra seu histórico e arquivos, removendo qualquer coisa que não esteja no diretório 1. O resultado é o conteúdo do diretório 1 expelido para a base do repository A.

     git filter-branch --subdirectory-filter  -- --all eg. git filter-branch --subdirectory-filter subfolder1/subfolder2/FOLDER_TO_KEEP -- --all 
  5. Apenas para movimentação de arquivo único: passe pelo que resta e remova tudo, exceto o arquivo desejado. (Pode ser necessário excluir arquivos que você não deseja com o mesmo nome e 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 

    por exemplo. FILE_TO_KEEP = pom.xml para manter apenas o arquivo pom.xml de FOLDER_TO_KEEP

Estágio Dois

  1. Etapa de limpeza

     git reset --hard 
  2. Etapa de limpeza

     git gc --aggressive 
  3. Etapa de limpeza

     git prune 

Você pode querer importar esses arquivos para o repository B dentro de um diretório que não seja a raiz:

  1. Faça esse diretório

     mkdir  eg. mkdir FOLDER_TO_KEEP 
  2. Mover arquivos para esse diretório

     git mv *  eg. git mv * FOLDER_TO_KEEP 
  3. Adicione arquivos a esse diretório

     git add . 
  4. Confirme suas alterações e estamos prontos para mesclar esses arquivos no novo repository

     git commit 

Estágio três

  1. Faça uma cópia do repository B se você não tiver um já

     git clone  eg. git clone https://username@giturl/scm/projects/FOLDER_TO_KEEP.git 

    (assumindo que FOLDER_TO_KEEP é o nome do novo repository para o qual você está copiando)

  2. cd para ele

     cd  eg. cd /c/Working/GIT/FOLDER_TO_KEEP 
  3. Crie uma conexão remota para o repository A como uma ramificação no repository B

     git remote add repo-A-branch  

    (repo-A-branch pode ser qualquer coisa – é apenas um nome arbitrário)

     eg. git remote add repo-A-branch /c/Working/GIT/myprojects 
  4. Puxe desta ramificação (contendo apenas o diretório que você deseja mover) para o repository B.

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

    O pull copia os arquivos e o histórico. Nota: Você pode usar uma mesclagem em vez de um pull, mas o pull funciona melhor.

  5. Finalmente, você provavelmente quer limpar um pouco removendo a conexão remota para o repository A

     git remote rm repo-A-branch 
  6. Empurre e está tudo pronto.

     git push 

Eu achei isso muito útil. É uma abordagem muito simples, na qual você cria patches que são aplicados ao novo repository. Veja a página vinculada para mais detalhes.

Ele contém apenas três etapas (copiadas do blog):

 # Setup a directory to hold the patches mkdir  # Create the patches git format-patch -o  --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 

O único problema que tive foi que eu não podia aplicar todos os patches de uma só vez usando

 git am --3way /*.patch 

No Windows, recebi um erro InvalidArgument. Então eu tive que aplicar todos os patches um após o outro.

MANTENDO O NOME DO DIRETÓRIO

O subdiretório-filter (ou o menor comando git subtree) funciona bem, mas não funcionou para mim, já que eles removem o nome do diretório da informação de commit. No meu cenário, apenas quero mesclar partes de um repository em outro e manter o histórico com o nome do caminho completo.

Minha solução foi usar o filtro de tree e simplesmente remover os arquivos e diretórios indesejados de um clone temporário do repository de origem e, em seguida, extrair desse clone para o meu repository de destino em 5 etapas simples.

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

Esta resposta fornece comandos interessantes baseados no git am e apresentados usando exemplos, passo a passo.

Objetivo

  • Você quer mover alguns ou todos os arquivos de um repository para outro.
  • Você quer manter sua história.
  • Mas você não se importa em manter tags e branches.
  • Você aceita histórico limitado para arquivos renomeados (e arquivos em diretórios renomeados).

Procedimento

  1. Extraia o histórico no formato de email usando
    git log --pretty=email -p --reverse --full-index --binary
  2. Reorganize a tree de arquivos e atualize a alteração do nome de arquivo no histórico [opcional]
  3. Aplicar novo histórico usando o git am

1. Extraia o histórico no formato de email

Exemplo: extrair histórico de file3 , file4 e file5

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

Limpe o destino do diretório temporário

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

Limpe sua fonte de recompra

 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 

Extraia o histórico de cada arquivo no formato de email

 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"' {} ';' 

Infelizmente a opção – --follow ou --find-copies-harder não pode ser combinada com --reverse . É por isso que o histórico é cortado quando o arquivo é renomeado (ou quando um diretório pai é renomeado).

Depois: Histórico temporário no formato de email

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

2. Reorganize a tree de arquivos e atualize a mudança de nome de arquivo no histórico [opcional]

Suponha que você queira mover esses três arquivos neste outro repo (pode ser o mesmo repo).

 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 

Portanto, reorganize seus arquivos:

 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 

Sua história temporária é agora:

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

Altere também nomes de arquivos dentro do histórico:

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

Nota: Isso reescreve o histórico para refletir a mudança de caminho e nome de arquivo.
(ou seja, a mudança da nova localização / nome dentro do novo repo)


3. Aplicar novo histórico

Seu outro repo é:

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

Aplique commits de arquivos históricos temporários:

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

Seu outro repo é agora:

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

Use o git status para ver a quantidade de commits prontos para serem enviados 🙂

Nota: Como o histórico foi reescrito para refletir a alteração do caminho e do nome do arquivo:
(ou seja, em comparação com a localização / nome no repo anterior)

  • Não há necessidade de git mv para alterar o local / nome do arquivo.
  • Não há necessidade de git log --follow para acessar o histórico completo.

Truque extra: Detectar arquivos renomeados / movidos dentro de seu repo

Para listar os arquivos que foram renomeados:

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

Mais personalizações: você pode concluir o comando git log usando as opções --find-copies-harder ou --reverse . Você também pode remover as duas primeiras colunas usando cut -f3- e grepping complete pattern ‘{. * =>. *}’.

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

Tendo tido uma coceira semelhante ao scratch (por apenas alguns arquivos de um determinado repository) este script provou ser muito útil: git-import

A versão curta é que cria arquivos de correção do arquivo ou diretório fornecido ( $object ) do repository existente:

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

que então são aplicados a um novo repository:

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

Para mais detalhes, por favor, procure:

  • a fonte documentada
  • git format-patch
  • git am

O que eu sempre uso está aqui http://blog.neutrino.es/2012/git-copy-a-file-or-directory-frother-repository-preserving-history/ . Simples e rápido

Para conformidade com os padrões do stackoverflow, aqui está o procedimento:

 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 

Usando inspiração do http://blog.neutrino.es/2012/git-copy-a-file-or-directory-from-other-repository-preserving-history/ , criei essa function do Powershell para fazer o mesmo, que tem funcionou muito bem para mim até agora:

 # 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. # Inspired 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 restore the history of $fileOrDir ..." -ForegroundColor Cyan ls $tempDir -Filter *.patch ` | foreach { git am $_.FullName } } 

Uso para este exemplo:

 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" 

Depois de fazer isso, você pode reorganizar os arquivos na ramificação migrate-from-project2 antes de mesclá-los.

Eu queria algo robusto e reutilizável (um-comando-e-ir + desfazer function) então eu escrevi o seguinte script bash. Trabalhei para mim em várias ocasiões, então pensei em compartilhá-lo aqui.

É capaz de mover uma pasta arbitrária /path/to/foo de repo1 em /some/other/folder/bar para repo2 (caminhos de pastas podem ser iguais ou diferentes, a distância da pasta raiz pode ser diferente).

Uma vez que ele apenas ultrapassa os commits que tocam os arquivos na pasta de input (não sobre todos os commits do repository de origem), ele deve ser bastante rápido mesmo em grandes repositorys de fonts, se você apenas extrair uma subpasta profundamente aninhada que não foi tocada em cada commit.

Uma vez que isto é para criar uma ramificação órfã com todo o histórico do repo antigo e então mesclá-lo ao HEAD, ele irá até funcionar em caso de conflitos de nome de arquivo (então você teria que resolver uma fusão no final do curso) .

Se não houver confrontos no nome do arquivo, você só precisa git commit o git commit no final para finalizar a mesclagem.

A desvantagem é que ele provavelmente não seguirá renomeações de arquivos (fora da pasta REWRITE_FROM ) nas REWRITE_FROM de REWRITE_FROM reporte de origem bem-vindas no GitHub para acomodar isso.

Link do GitHub: git-move-folder-entre-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 

No meu caso, não precisei preservar o repository do qual eu estava migrando ou preservar qualquer histórico anterior. Eu tinha um patch do mesmo ramo, de um controle remoto diferente

 #Source directory git remote rm origin #Target directory git remote add branch-name-from-old-repo ../source_directory 

Nesses dois passos, consegui que a ramificação do outro repo aparecesse no mesmo repo.

Finalmente, eu configurei este branch (que eu importei do outro repository) para seguir a linha principal do repository alvo (para que eu pudesse diferenciá-los com precisão)

 git br --set-upstream-to=origin/mainline 

Agora ele se comportou como se fosse apenas outro ramo que eu havia empurrado contra o mesmo repository.

Se os caminhos para os arquivos em questão são os mesmos nos dois repos e você está querendo trazer apenas um arquivo ou um pequeno conjunto de arquivos relacionados, uma maneira fácil de fazer isso é usar o git cherry-pick .

O primeiro passo é trazer os commits do outro repository para o seu repository local usando git fetch . Isso deixará FETCH_HEAD apontando para o head commit do outro repo; Se você quiser preservar uma referência a esse commit depois de ter feito outras buscas, você pode querer marcá-lo com o git tag other-head FETCH_HEAD .

Você precisará então criar um commit inicial para aquele arquivo (se ele não existir) ou um commit para trazer o arquivo para um estado que possa ser corrigido com o primeiro commit do outro repository que você quer trazer. ser capaz de fazer isso com um git cherry-pick se commit-0 introduziu os arquivos que você quer, ou você pode precisar construir o commit ‘manualmente’. Adicione -n às opções cherry-pick se você precisar modificar o commit inicial para, por exemplo, descartar arquivos daquele commit que você não quer trazer.

Depois disso, você pode continuar a git cherry-pick confirmações subseqüentes, usando novamente -n quando necessário. No caso mais simples (todos os commits são exatamente o que você quer e aplica de forma limpa) você pode dar a lista completa de commits na linha de comando cherry-pick: git cherry-pick ...