Como extrair um subdiretório git e fazer um submódulo dele?

119

Comecei um projeto há alguns meses e armazenei tudo em um diretório principal. No meu diretório principal "Project", existem vários subdiretórios contendo coisas diferentes: Project / paper contém um documento escrito em LaTeX Project / sourcecode / RailsApp contains my app rails.

"Projeto" é GITified e tem havido muitos commits nos diretórios "paper" e "RailsApp". Agora, como gostaria de usar cruisecontrol.rb para meu "RailsApp", gostaria de saber se há uma maneira de fazer um submódulo fora de "RailsApp" sem perder o histórico.

Cœur
fonte
2
Também uma resposta muito boa: stackoverflow.com/questions/359424/…
Rehno Lindeque
Possível duplicata do subdiretório Detach (mover) para um repositório Git separado
Coronel Trinta e Dois

Respostas:

122

Hoje em dia há uma maneira muito mais fácil de fazer isso do que manualmente usando git filter-branch: git subtree

Instalação

NOTE git-subtree agora faz parte git(se você instalar o contrib) a partir de 1.7.11, então você pode já tê-lo instalado. Você pode verificar executando git subtree.


Para instalar git-subtree da fonte (para versões anteriores do git):

git clone https://github.com/apenwarr/git-subtree.git

cd git-subtree
sudo rsync -a ./git-subtree.sh /usr/local/bin/git-subtree

Ou se você quiser as páginas de manual e tudo

make doc
make install

Uso

Divida um maior em pedaços menores:

# Go into the project root
cd ~/my-project

# Create a branch which only contains commits for the children of 'foo'
git subtree split --prefix=foo --branch=foo-only

# Remove 'foo' from the project
git rm -rf ./foo

# Create a git repo for 'foo' (assuming we already created it on github)
mkdir foo
pushd foo
git init
git remote add origin [email protected]:my-user/new-project.git
git pull ../ foo-only
git push origin -u master
popd

# Add 'foo' as a git submodule to `my-project`
git submodule add [email protected]:my-user/new-project.git foo

Para documentação detalhada (página man), por favor leia git-subtree.txt.

Apenwarr
fonte
10
git subtree rocks!
Simon Woodside,
3
Mas não é o objetivo do git-subtree evitar o uso de submódulos? Quer dizer, você é realmente o autor do git-subtree (a menos que haja uma colisão de apelidos), mas parece que git-subtree mudou, embora o comando que você mostra ainda pareça válido. Estou entendendo direito?
Blaisorblade 01 de
17
git-subtree agora faz parte do git (se você instalar o contrib) a partir de 1.7.11
Jeremy
8
Bem git rm -rf ./fooremove fooa partir HEAD, mas não filtra my-project's história completa. Então, git submodule add [email protected]:my-user/new-project.git foosó faz fooum submódulo a partir de HEAD. Nesse aspecto, o script filter-branché superior, pois permite "fazer como se subdir fosse um submódulo desde o início"
Gregory Pakosz
obrigado por isso - git subtree docs um pouco desconcertante, e isso é (para mim) a coisa mais obviamente útil que eu queria fazer com ele ...
hwjp
38

Verifique o git filter-branch .

A Examplesseção da página do manual mostra como extrair um subdiretório em seu próprio projeto, mantendo todo o seu histórico e descartando o histórico de outros arquivos / diretórios (exatamente o que você está procurando).

Para reescrever o repositório para parecer como se foodir/fosse a raiz do projeto e descartar todo o outro histórico:

   git filter-branch --subdirectory-filter foodir -- --all

Assim, você pode, por exemplo, transformar um subdiretório de biblioteca em um repositório próprio.
Observe o --que separa as filter-branchopções das opções de revisão e --allpara reescrever todos os ramos e tags.

Pat Notz
fonte
1
Isto funcionou bem para mim. A única desvantagem que notei foi que o resultado foi um único branch master com todos os commits.
aceofspades
@aceofspades: por que isso é uma desvantagem?
naught101
2
Para mim, o objetivo de extrair commits de um repositório git é que eu quero reter o histórico.
aceofspades
13

Uma maneira de fazer isso é o inverso - remova tudo, exceto o arquivo que deseja manter.

Basicamente, faça uma cópia do repositório e use git filter-branchpara remover tudo, exceto os arquivos / pastas que deseja manter.

Por exemplo, tenho um projeto do qual desejo extrair o arquivo tvnamer.pypara um novo repositório:

git filter-branch --tree-filter 'for f in *; do if [ $f != "tvnamer.py" ]; then rm -rf $f; fi; done' HEAD

Isso costuma git filter-branch --tree-filterpassar por cada confirmação, executar o comando e confirmar novamente o conteúdo do diretório resultante. Isso é extremamente destrutivo (então você só deve fazer isso em uma cópia do seu repositório!), E pode demorar um pouco (cerca de 1 minuto em um repositório com 300 commits e cerca de 20 arquivos)

O comando acima apenas executa o seguinte script de shell em cada revisão, que você teria que modificar, é claro (para excluí-lo do seu subdiretório em vez de tvnamer.py):

for f in *; do
    if [ $f != "tvnamer.py" ]; then
        rm -rf $f;
    fi;
done

O maior problema óbvio é que ele deixa todas as mensagens de commit, mesmo que não estejam relacionadas ao arquivo restante. O script git-remove-empty-commits corrige isso ..

git filter-branch --commit-filter 'if [ z$1 = z`git rev-parse $3^{tree}` ]; then skip_commit "$@"; else git commit-tree "$@"; fi'

Você precisa usar o -fargumento force executado filter-branchnovamente com qualquer coisa em refs/original/(que basicamente é um backup)

Claro que isso nunca será perfeito, por exemplo, se suas mensagens de commit mencionarem outros arquivos, mas é o mais próximo que um git atual permite (pelo menos pelo que eu sei).

Novamente, apenas execute isso em uma cópia do seu repositório! - mas em resumo, para remover todos os arquivos, exceto "thisismyfilename.txt":

git filter-branch --tree-filter 'for f in *; do if [ $f != "thisismyfilename.txt" ]; then rm -rf $f; fi; done' HEAD
git filter-branch -f --commit-filter 'if [ z$1 = z`git rev-parse $3^{tree}` ]; then skip_commit "$@"; else git commit-tree "$@"; fi'
dbr
fonte
4
git filter-branchtem (hoje em dia?) uma opção embutida para remover commits vazios, a saber --prune-empty. Um guia melhor git filter-branchestá nas respostas a esta pergunta: stackoverflow.com/questions/359424/…
Blaisorblade
4

Ambos CoolAJ86 e apenwarr respostas são muito semelhantes. Eu fui para frente e para trás entre os dois tentando entender partes que estavam faltando em qualquer um. Abaixo está uma combinação deles.

Primeiro, navegue no Git Bash até a raiz do repositório git a ser dividido. No meu exemplo aqui, isso é~/Documents/OriginalRepo (master)

# move the folder at prefix to a new branch
git subtree split --prefix=SubFolderName/FolderToBeNewRepo --branch=to-be-new-repo

# create a new repository out of the newly made branch
mkdir ~/Documents/NewRepo
pushd ~/Documents/NewRepo
git init
git pull ~/Documents/OriginalRepo to-be-new-repo

# upload the new repository to a place that should be referenced for submodules
git remote add origin [email protected]:myUsername/newRepo.git
git push -u origin master
popd

# replace the folder with a submodule
git rm -rf ./SubFolderName/FolderToBeNewRepo
git submodule add [email protected]:myUsername/newRepo.git SubFolderName/FolderToBeNewRepo
git branch --delete --force to-be-new-repo

Abaixo está uma cópia de acima com os nomes personalizáveis ​​substituídos e usando https. A pasta raiz é agora~/Documents/_Shawn/UnityProjects/SoProject (master)

# move the folder at prefix to a new branch
git subtree split --prefix=Assets/SoArchitecture --branch=so-package

# create a new repository out of the newly made branch
mkdir ~/Documents/_Shawn/UnityProjects/SoArchitecture
pushd ~/Documents/_Shawn/UnityProjects/SoArchitecture
git init
git pull ~/Documents/_Shawn/UnityProjects/SoProject so-package

# upload the new repository to a place that should be referenced for submodules
git remote add origin https://github.com/Feddas/SoArchitecture.git
git push -u origin master
popd

# replace the folder with a submodule
git rm -rf ./Assets/SoArchitecture
git submodule add https://github.com/Feddas/SoArchitecture.git
git branch --delete --force so-package
ShawnFeatherly
fonte
3

Se você deseja transferir algum subconjunto de arquivos para um novo repositório, mas manter o histórico, basicamente acabará com um histórico completamente novo. A maneira como isso funcionaria é basicamente a seguinte:

  1. Crie um novo repositório.
  2. Para cada revisão de seu antigo repositório, mescle as mudanças em seu módulo no novo repositório. Isso criará uma "cópia" do histórico do projeto existente.

Deve ser um tanto simples automatizar isso se você não se importar em escrever um script pequeno, mas complicado. Simples, sim, mas também doloroso. As pessoas já reescreveram a história no Git no passado, você pode fazer uma busca por isso.

Alternativamente: clone o repositório e exclua o papel do clone, exclua o aplicativo do original. Isso levaria um minuto, é garantido que funcione, e você pode voltar a coisas mais importantes do que tentar purificar seu histórico do git. E não se preocupe com o espaço do disco rígido ocupado por cópias redundantes da história.

Dietrich Epp
fonte