No meu caso, eu tinha um my-pluginrepositório e um main-projectrepositório e queria fingir que my-pluginsempre havia sido desenvolvido no pluginssubdiretório de main-project.
Basicamente, reescrevi o histórico do my-pluginrepositório para que parecesse que todo o desenvolvimento ocorreu no plugins/my-pluginsubdiretório. Em seguida, adicionei o histórico de desenvolvimento my-pluginao main-projecthistórico e mesclei as duas árvores. Como não havia plugins/my-plugindiretório já presente no main-projectrepositório, essa foi uma mesclagem trivial sem conflitos. O repositório resultante continha todo o histórico dos dois projetos originais e tinha duas raízes.
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
Versão longa
Primeiro, crie uma cópia do my-pluginrepositório, porque vamos reescrever o histórico desse repositório.
Agora, navegue até a raiz do my-pluginrepositório, verifique sua ramificação principal (provavelmente master) e execute o seguinte comando. Obviamente, você deve substituir my-plugine pluginsquaisquer que sejam seus nomes reais.
$ 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
Agora, para uma explicação. git filter-branch --tree-filter (...) HEADexecuta o (...)comando em todas as confirmações acessíveis HEAD. Observe que isso opera diretamente nos dados armazenados para cada confirmação, portanto, não precisamos nos preocupar com noções de "diretório de trabalho", "índice", "preparo" e assim por diante.
Se você executar um filter-branchcomando que falhar, ele deixará para trás alguns arquivos no .gitdiretório e, na próxima vez em que tentar filter-branch, reclamará disso, a menos que você forneça a -fopção filter-branch.
Quanto ao comando real, não tive muita sorte bashem fazer o que queria, então, em vez disso, uso zsh -cpara zshexecutar um comando. Primeiro, defino a extended_globopção, que é o que habilita a ^(...)sintaxe no mvcomando, bem como a glob_dotsopção, que permite selecionar arquivos de ponto (como .gitignore) com um globo ( ^(...)).
Em seguida, uso o mkdir -pcomando para criar os dois pluginse plugins/my-pluginao mesmo tempo.
Por fim, uso o zshrecurso "glob negativo" ^(.git|plugins)para corresponder a todos os arquivos no diretório raiz do repositório, exceto .gita my-pluginpasta recém-criada . (A exclusão .gitpode não ser necessária aqui, mas tentar mover um diretório para si mesmo é um erro.)
No meu repositório, o commit inicial não incluía nenhum arquivo; portanto, o mvcomando retornou um erro no commit inicial (já que nada estava disponível para mover). Portanto, adicionei um || truepara que git filter-branchnão abortasse.
A --allopção diz filter-branchpara reescrever o histórico de todas as ramificações no repositório, e o extra --é necessário dizer gitpara interpretá-la como parte da lista de opções para reescrever ramificações, em vez de ser uma opção para filter-branchsi mesma.
Agora, navegue até o seu main-projectrepositório e verifique em qual filial você deseja mesclar. Adicione sua cópia local do my-pluginrepositório (com seu histórico modificado) como um controle remoto de main-projectcom:
$ git remote add --fetch my-plugin $PATH_TO_MY_PLUGIN_REPOSITORY
Agora você terá duas árvores não relacionadas em seu histórico de consolidação, que podem ser visualizadas com bom uso:
$ git log --color --graph --decorate --all
Para mesclá-los, use:
$ git merge my-plugin/master --allow-unrelated-histories
Observe que no Git anterior à 2.9.0, a --allow-unrelated-historiesopção não existe. Se você estiver usando uma dessas versões, apenas omita a opção: a mensagem de erro que --allow-unrelated-historiesimpede também foi adicionada na 2.9.0.
Você não deve ter nenhum conflito de mesclagem. Se o fizer, provavelmente significa que o filter-branchcomando não funcionou corretamente ou já havia um plugins/my-plugindiretório main-project.
Certifique-se de inserir uma mensagem de confirmação explicativa para qualquer colaborador futuro, imaginando que hackery estava acontecendo para criar um repositório com duas raízes.
Você pode visualizar o novo gráfico de confirmação, que deve ter duas confirmações raiz, usando o git logcomando acima . Observe que apenas a masterramificação será mesclada . Isso significa que, se você tiver um trabalho importante em outros my-pluginramos que deseja mesclar na main-projectárvore, evite excluir o my-plugincontrole remoto até fazer essas mesclagens. Caso contrário, as confirmações dessas ramificações ainda estarão no main-projectrepositório, mas algumas estarão inacessíveis e suscetíveis a eventual coleta de lixo. (Além disso, você precisará consultá-los pelo SHA, porque a exclusão de um controle remoto remove suas ramificações de rastreamento remoto.)
Opcionalmente, depois de mesclar tudo o que você deseja impedir my-plugin, você pode remover o my-plugincontrole remoto usando:
$ git remote remove my-plugin
Agora você pode excluir com segurança a cópia do my-pluginrepositório cujo histórico foi alterado. No meu caso, também adicionei um aviso de descontinuação ao my-pluginrepositório real depois que a mesclagem foi concluída e enviada por push.
Testado no Mac OS X El Capitan com git --version 2.9.0e zsh --version 5.2. Sua milhagem pode variar.
Referências: