Mesclar dois repositórios Git sem quebrar o histórico do arquivo

226

Eu preciso mesclar dois repositórios Git em um novo repositório, terceiro. Eu encontrei muitas descrições de como fazer isso usando uma mesclagem de subárvore (por exemplo, a resposta de Jakub Narębski em Como você mescla dois repositórios Git? ) E seguir essas instruções geralmente funciona, exceto que, quando eu submeto a subárvore, mesclamos todos os arquivos dos repositórios antigos são registrados como novos arquivos adicionados. Eu posso ver o histórico de confirmação dos repositórios antigos quando o faço git log, mas se o fizer git log <file>, mostrará apenas uma confirmação para esse arquivo - a mesclagem da subárvore. A julgar pelos comentários na resposta acima, não estou sozinho vendo esse problema, mas não encontrei soluções publicadas para ele.

Existe alguma maneira de mesclar repositórios e deixar intacto o histórico de arquivos individuais?

Eric Lee
fonte
Não estou usando o Git, mas, no Mercurial, eu primeiro faria uma conversão, se necessário, para corrigir os caminhos de arquivo dos repositórios a serem mesclados e, em seguida, force um pull de um repositório no destino para obter os conjuntos de alterações e, em seguida, faça um mesclagem dos diferentes ramos. Isso é testado e funciona;) Talvez isso ajude a encontrar uma solução para o Git também ... Em comparação com a abordagem de subárvore de mesclagem, acho que a etapa de conversão é diferente em que a história é reescrita em vez de apenas mapear um caminho (se eu entender corretamente). Isso garante uma mesclagem suave sem nenhum tratamento especial dos caminhos do arquivo.
Lucero
Eu também achei esta pergunta útil stackoverflow.com/questions/1683531/…
nacross
Eu criei uma pergunta de acompanhamento. Pode ser interessante: Mesclar dois repositórios Git e manter o histórico mestre: stackoverflow.com/questions/42161910/...
Dimitri Dewaele
A solução automatizada que funcionou para mim foi stackoverflow.com/a/30781527/239408
xverges

Respostas:

269

Acontece que a resposta é muito mais simples se você estiver simplesmente tentando colar dois repositórios e parecer que era assim o tempo todo, em vez de gerenciar uma dependência externa. Você só precisa adicionar controles remotos aos seus repositórios antigos, mesclá-los ao seu novo mestre, mover os arquivos e pastas para um subdiretório, confirmar a movimentação e repetir para todos os repositórios adicionais. Sub-módulos, mesclagens de sub-árvores e rebotes sofisticados destinam-se a resolver um problema ligeiramente diferente e não são adequados para o que eu estava tentando fazer.

Aqui está um exemplo de script do Powershell para colar dois repositórios:

# Assume the current directory is where we want the new repository to be created
# Create the new repository
git init

# Before we do a merge, we have to have an initial commit, so we'll make a dummy commit
git commit --allow-empty -m "Initial dummy commit"

# Add a remote for and fetch the old repo
git remote add -f old_a <OldA repo URL>

# Merge the files from old_a/master into new/master
git merge old_a/master --allow-unrelated-histories

# Move the old_a repo files and folders into a subdirectory so they don't collide with the other repo coming later
mkdir old_a
dir -exclude old_a | %{git mv $_.Name old_a}

# Commit the move
git commit -m "Move old_a files into subdir"

# Do the same thing for old_b
git remote add -f old_b <OldB repo URL>
git merge old_b/master --allow-unrelated-histories
mkdir old_b
dir exclude old_a,old_b | %{git mv $_.Name old_b}
git commit -m "Move old_b files into subdir"

Obviamente, você pode mesclar old_b em old_a (que se torna o novo repositório combinado) se preferir fazer isso - modifique o script para se adequar.

Se você deseja trazer ramificações de recursos em andamento também, use o seguinte:

# Bring over a feature branch from one of the old repos
git checkout -b feature-in-progress
git merge -s recursive -Xsubtree=old_a old_a/feature-in-progress

Essa é a única parte não óbvia do processo - não é uma mesclagem de subárvore, mas um argumento para a mesclagem recursiva normal que informa ao Git que renomeamos o alvo e ajuda o Git a alinhar tudo corretamente.

Eu escrevi uma explicação um pouco mais detalhada aqui .

Eric Lee
fonte
16
esta solução usando git mvnão funciona tão bem. quando você usa git logum dos arquivos movidos posteriormente, você obtém apenas a confirmação da movimentação. toda a história anterior está perdida. isso é porque git mvé realmente, git rm; git addmas em uma etapa .
precisa saber é o seguinte
15
É o mesmo que qualquer outra operação de mudança / renomeação no Git: na linha de comando, você pode obter todo o histórico fazendo isso git log --follow, ou todas as ferramentas da GUI fazem isso automaticamente. Com uma mesclagem de subárvore, você não pode obter o histórico de arquivos individuais, tanto quanto eu sei, portanto esse método é melhor.
Eric Lee
3
@EricLee Quando o repo old_b é mesclado, recebo muitos conflitos de mesclagem. Isso é esperado? Eu recebo CONFLITO (renomear / excluir)
Jon
9
Quando tento "dir-excluir old_a |% {git mv $ _. Nome old_a}", obtenho sh.exe ": dir: comando não encontrado e sh.exe": git: comando não encontrado. Usando isto funciona: ls -I old_a | xargs -I '{}' git mv '{}' old_a /
George
5
Este é 1(o número um) para lse 'olho' maiúsculo para xargs. Obrigado por esta dica!
Dominique Vial
149

Aqui está uma maneira de não reescrever nenhum histórico, para que todos os IDs de confirmação permaneçam válidos. O resultado final é que os arquivos do segundo repositório acabarão em um subdiretório.

  1. Adicione o segundo repositório como um controle remoto:

    cd firstgitrepo/
    git remote add secondrepo username@servername:andsoon
    
  2. Verifique se você baixou todos os commits do secondrepo:

    git fetch secondrepo
    
  3. Crie uma ramificação local a partir da ramificação do segundo repositório:

    git branch branchfromsecondrepo secondrepo/master
    
  4. Mova todos os seus arquivos para um subdiretório:

    git checkout branchfromsecondrepo
    mkdir subdir/
    git ls-tree -z --name-only HEAD | xargs -0 -I {} git mv {} subdir/
    git commit -m "Moved files to subdir/"
    
  5. Mesclar a segunda ramificação na ramificação principal do primeiro repositório:

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

Seu repositório terá mais de uma confirmação raiz, mas isso não deve representar um problema.

Flimm
fonte
1
A etapa 2 não funciona para mim: fatal: Não é um nome de objeto válido: 'secondrepo / master'.
Keith
@ Keith: verifique se você adicionou o segundo repositório como um controle remoto chamado "secondrepo", e se esse repositório possui uma ramificação chamada "master" (você pode exibir ramificações em um git remote show secondrepo
repositório
Eu tive que fazer uma busca para derrubá-lo também. Em entre 1 e 2 eu fiz git fetch secondrepo
sksamuel
@monkjack: editei minha resposta para incluir uma etapa de busca do git. Sinta-se livre para editar a resposta você mesmo no futuro.
Flimm 17/03/14
4
@MartijnHeemels Para a versão mais antiga do Git, basta omitir --allow-unrelated-histories. Veja o histórico desta resposta.
Flimm
8

Alguns anos se passaram e há soluções bem votadas bem fundamentadas, mas quero compartilhar as minhas porque era um pouco diferente, porque eu queria mesclar 2 repositórios remotos em um novo, sem excluir o histórico dos repositórios anteriores.

  1. Crie um novo repositório no Github.

    insira a descrição da imagem aqui

  2. Faça o download do repositório recém-criado e adicione o repositório remoto antigo.

    git clone https://github.com/alexbr9007/Test.git
    cd Test
    git remote add OldRepo https://github.com/alexbr9007/Django-React.git
    git remote -v
    
  3. Busque todos os arquivos do repositório antigo para que uma nova ramificação seja criada.

    git fetch OldRepo
    git branch -a
    

    insira a descrição da imagem aqui

  4. Na ramificação mestre, faça uma mesclagem para combinar o repositório antigo com o recém-criado.

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

    insira a descrição da imagem aqui

  5. Crie uma nova pasta para armazenar todo o novo conteúdo criado que foi adicionado no OldRepo e mover seus arquivos para essa nova pasta.

  6. Por fim, você pode fazer o upload dos arquivos dos repositórios combinados e excluir com segurança o OldRepo do GitHub.

Espero que isso possa ser útil para quem lida com a fusão de repositórios remotos.

abautista
fonte
1
Esta é a única solução que funcionou para mim para preservar a história do git. Não se esqueça de remover o link remoto para o repositório antigo git remote rm OldRepo.
Harubiyori 23/01
7

por favor, dê uma olhada no uso

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

para ligar duas histórias no início de suas vidas.

Se você tiver caminhos sobrepostos, corrija-os com

git filter-branch --index-filter

ao usar o log, verifique se "encontra cópias mais difíceis" com

git log -CC

Dessa forma, você encontrará movimentos de arquivos no caminho.

Adam Dymitruk
fonte
A documentação do Git recomenda não rebasear
Stephen Turner
7

Eu transformei a solução do @Flimm em um git aliasassim (adicionado ao meu ~/.gitconfig):

[alias]
 mergeRepo = "!mergeRepo() { \
  [ $# -ne 3 ] && echo \"Three parameters required, <remote URI> <new branch> <new dir>\" && exit 1; \
  git remote add newRepo $1; \
  git fetch newRepo; \
  git branch \"$2\" newRepo/master; \
  git checkout \"$2\"; \
  mkdir -vp \"${GIT_PREFIX}$3\"; \
  git ls-tree -z --name-only HEAD | xargs -0 -I {} git mv {} \"${GIT_PREFIX}$3\"/; \
  git commit -m \"Moved files to '${GIT_PREFIX}$3'\"; \
  git checkout master; git merge --allow-unrelated-histories --no-edit -s recursive -X no-renames \"$2\"; \
  git branch -D \"$2\"; git remote remove newRepo; \
}; \
mergeRepo"
Fredrik Erlandsson
fonte
12
Apenas curioso: você realmente faz isso com frequência suficiente para precisar de um apelido?
Parker Coates
1
Não, eu não, mas nunca me lembro de como fazê-lo, então um pseudônimo é apenas uma maneira de eu me lembrar.
Fredrik Erlandsson
1
Sim .. mas tente mudar computadores e esquecendo-se de mover seus pseudónimos;)
quetzalcoatl
1
Qual é o valor de $GIT_PREFIX?
neowulf33
github.com/git/git/blob/… 'GIT_PREFIX' é definido como retornado executando 'git rev-parse --show-prefix' no diretório atual original. Veja linkgit: git-rev-parse [1].
Fredrik Erlandsson
3

Esta função clonará o repositório remoto no diretório de repositório local:

function git-add-repo
{
    repo="$1"
    dir="$(echo "$2" | sed 's/\/$//')"
    path="$(pwd)"

    tmp="$(mktemp -d)"
    remote="$(echo "$tmp" | sed 's/\///g'| sed 's/\./_/g')"

    git clone "$repo" "$tmp"
    cd "$tmp"

    git filter-branch --index-filter '
        git ls-files -s |
        sed "s,\t,&'"$dir"'/," |
        GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &&
        mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
    ' HEAD

    cd "$path"
    git remote add -f "$remote" "file://$tmp/.git"
    git pull "$remote/master"
    git merge --allow-unrelated-histories -m "Merge repo $repo into master" --edit "$remote/master"
    git remote remove "$remote"
    rm -rf "$tmp"
}

Como usar:

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

Aviso prévio. Esse script pode reescrever confirmações, mas salvará todos os autores e datas, significa que novas confirmações terão outros hashes, e se você tentar enviar alterações para o servidor remoto, ele poderá ser capaz apenas com a força de chave, e também reescreverá confirmações no servidor. Portanto, faça backups antes do lançamento.

Lucro!

Andrey Izman
fonte
Estou usando o zsh em vez do bash e a v2.13.0 do git. Não importa o que eu tentei, não consegui git filter-branch --index-filtertrabalhar. Normalmente, recebo uma mensagem de erro informando que o arquivo de índice .new não existe. Isso soa sinos?
22617 Patrick Beard
@PatrickBeard Não sei zsh, você pode criar um arquivo separado git-add-repo.shcom a função acima, no final do arquivo coloque esta linha git-add-repo "$@". Depois disso, você pode usá-lo a partir do zsh como cd current/git/packageebash path/to/git-add-repo.sh https://github.com/example/example dir/to/save
Andrey Izman
O problema foi discutido aqui: stackoverflow.com/questions/7798142/… mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE" falha às vezes, então você deve adicionar um if test.
Patrick Beard
1
Eu não usaria esse método! Eu tentei o script, ingenuamente e literalmente (só posso me culpar por essa parte), e isso acabou com meu repo git local. O histórico parecia quase certo, mas fazer um push do git de volta ao Github resultou no temido "RPC falhou; o curl 55 SSL_write () retornou o erro SYSCALL, errno = 32". Tentei consertá-lo, mas estava irreparavelmente quebrado. Acabei tendo que reconstruir as coisas em um novo repositório local.
Mason Freed
@MasonFreed Esse script cria um novo histórico do git com a mistura de ambos os repositórios, por isso não pode ser enviado para um repositório antigo, é necessário criar um novo ou pressionado com a tecla force, significa que ele reescreve seu
repositório
2

Siga as etapas para incorporar um repositório em outro repositório, tendo um único histórico git mesclando os dois históricos git.

  1. Clone os dois repositórios que você deseja mesclar.

git clone [email protected]: usuário / pai-repo.git

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

  1. Ir para o repo filho

cd repositório-filho /

  1. execute o comando abaixo, substitua o caminho my/new/subdir(3 ocorrências) pela estrutura de diretórios na qual você deseja que o repo filho seja executado.

git filter-branch --une-empty - --tree-filter 'se [! -e meu / novo / subdir]; então mkdir -p my / new / subdir git ls-tree --name-only $ GIT_COMMIT | xargs -I arquivos mv arquivos my / new / subdir fi '

  1. Ir para o repo pai

cd ../parent-repo/

  1. Adicionar um controle remoto ao repositório pai, apontando o caminho para o repositório filho

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

  1. Buscar o repositório filho

git fetch child-remote

  1. Mesclar as histórias

git merge --allow-unrelated-historories filho-remoto / mestre

Se você verificar o log do git no repositório pai agora, ele deverá fazer com que o repositório filho confirme a fusão. Você também pode ver a tag indicando na fonte de confirmação.

O artigo abaixo me ajudou a incorporar um repositório em outro repositório, tendo um único histórico do git ao mesclar os dois.

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

Espero que isto ajude. Feliz codificação!

AnoopGoudar
fonte
Falha na etapa 3 para mim com erro de sintaxe. Faltam dois pontos e vírgulas. Fixgit filter-branch --prune-empty --tree-filter ' if [ ! -e my/new/subdir ]; then mkdir -p my/new/subdir; git ls-tree --name-only $GIT_COMMIT | xargs -I files mv files my/new/subdir; fi'
Yuri L
1

Digamos que você deseja mesclar repositório apara b(eu estou supondo que eles estão localizados ao lado do outro):

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

Caso queira colocar aem um subdiretório, faça o seguinte antes dos comandos acima:

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

Para isso, você precisa git-filter-repoinstalar ( filter-branché desencorajado ).

Um exemplo de mesclagem de 2 grandes repositórios, colocando um deles em um subdiretório: https://gist.github.com/x-yuri/9890ab1079cf4357d6f269d073fd9731

Mais sobre isso aqui .

x-yuri
fonte