Desanexe muitos subdiretórios em um novo repositório Git separado

135

Esta questão é baseada no subdiretório Desanexar em um repositório Git separado

Em vez de desanexar um único subdiretório, quero desanexar alguns. Por exemplo, minha árvore de diretórios atual se parece com isso:

/apps
  /AAA
  /BBB
  /CCC
/libs
  /XXX
  /YYY
  /ZZZ

E eu gostaria disso:

/apps
  /AAA
/libs
  /XXX

O --subdirectory-filterargumento para git filter-branchnão funcionar porque se livra de tudo, exceto do diretório especificado na primeira vez em que é executado. Eu pensei que usar o --index-filterargumento para todos os arquivos indesejados funcionaria (embora seja entediante), mas se eu tentar executá-lo mais de uma vez, recebo a seguinte mensagem:

Cannot create a new backup.
A previous backup already exists in refs/original/
Force overwriting the backup with -f

Alguma ideia? TIA

prisonerjohn
fonte

Respostas:

155

Em vez de ter que lidar com um subshell e usar ext glob (como sugerido pelo kynan), tente esta abordagem muito mais simples:

git filter-branch --index-filter 'git rm --cached -qr --ignore-unmatch -- . && git reset -q $GIT_COMMIT -- apps/AAA libs/XXX' --prune-empty -- --all

Como mencionado por void.pointer em sua / seu comentário , isso irá remover tudo, exceto apps/AAAe libs/XXXdo repositório atual.

Eliminar confirmações de mesclagem vazia

Isso deixa para trás muitas mesclagens vazias. Estes podem ser removidos por outro passe, conforme descrito por raphinesse em sua resposta :

git filter-branch --prune-empty --parent-filter \
'sed "s/-p //g" | xargs -r git show-branch --independent | sed "s/\</-p /g"'

Warning️ Aviso : O item acima deve usar a versão GNU sede, xargscaso contrário, removeria todos os commits como xargsfalhas. brew install gnu-sed findutilse depois use gsede gxargs:

git filter-branch --prune-empty --parent-filter \
'gsed "s/-p //g" | gxargs git show-branch --independent | gsed "s/\</-p /g"' 
David Smiley
fonte
4
Além disso, a bandeira --ignore-unmatch deve ser passado para rm git, ele falhou pela primeira comprometer para mim de outra forma (o repositório foi criado com git svn clone no meu caso)
Pontomedon
8
Supondo que você tem marcas no mix, você provavelmente deve adicionar --tag-name-filter cataos seus parâmetros
Yonatan
16
Você poderia adicionar mais algumas informações explicando o que esse comando demorado está fazendo?
Burhan Ali
4
Estou agradavelmente surpreso que isso funcione perfeitamente no Windows usando o git bash, ufa!
Dai
3
@BurhanAli Para cada commit no histórico, ele exclui todos os arquivos, exceto os que você deseja manter. Quando tudo estiver pronto, você ficará com apenas a parte da árvore especificada, além desse histórico.
void.pointer
39

Etapas manuais com comandos simples do git

O plano é dividir diretórios individuais em seus próprios repositórios e depois mesclá-los. As etapas manuais a seguir não empregaram scripts geek-to-use, mas comandos fáceis de entender e podem ajudar a mesclar N subpastas extras em outro repositório único.

Dividir

Vamos supor que seu repo original seja: original_repo

1 - Aplicativos divididos:

git clone original_repo apps-repo
cd apps-repo
git filter-branch --prune-empty --subdirectory-filter apps master

2 - Bibliotecas divididas

git clone original_repo libs-repo
cd libs-repo
git filter-branch --prune-empty --subdirectory-filter libs master

Continue se você tiver mais de 2 pastas. Agora você deve ter dois repositórios git novos e temporários.

Conquiste mesclando aplicativos e bibliotecas

3 - Prepare o novo repositório:

mkdir my-desired-repo
cd my-desired-repo
git init

E você precisará fazer pelo menos um commit. Se as três linhas a seguir forem ignoradas, seu primeiro repo aparecerá imediatamente abaixo da raiz do repo:

touch a_file_and_make_a_commit # see user's feedback
git add a_file_and_make_a_commit
git commit -am "at least one commit is needed for it to work"

Com o arquivo temporário confirmado, o mergecomando na seção posterior será interrompido conforme o esperado.

Partindo do feedback do usuário, em vez de adicionar um arquivo aleatório como a_file_and_make_a_commit, você pode optar por adicionar um .gitignore, ou README.mdetc.

4 - Mesclar aplicativos repo primeiro:

git remote add apps-repo ../apps-repo
git fetch apps-repo
git merge -s ours --no-commit apps-repo/master # see below note.
git read-tree --prefix=apps -u apps-repo/master
git commit -m "import apps"

Agora você deve ver o diretório de aplicativos dentro do seu novo repositório. git logdeve mostrar todas as mensagens de confirmação históricas relevantes.

Nota: como Chris observou abaixo nos comentários, para a versão mais recente (> = 2.9) do git, é necessário especificar --allow-unrelated-historiescomgit merge

5 - Mesclar repositório de libs a seguir da mesma maneira:

git remote add libs-repo ../libs-repo
git fetch libs-repo
git merge -s ours --no-commit libs-repo/master # see above note.
git read-tree --prefix=libs -u libs-repo/master
git commit -m "import libs"

Continue se você tiver mais de 2 repositórios para mesclar.

Referência: Mesclar um subdiretório de outro repositório com o git

chfw
fonte
4
Desde o git 2.9, você precisa usar --allow-unrelated-histories nos comandos de mesclagem. Caso contrário, isso parece ter funcionado bem para mim.
Chris
1
Gênio! Muito obrigado por isso. As respostas iniciais que eu olhei, usando um filtro de árvore em um repositório muito grande, previram o git levando mais de 26 horas para concluir as reescritas do git. Muito mais feliz com essa abordagem simples, mas repetível, e moveu com êxito quatro subpastas para um novo repositório com todo o histórico de confirmação esperado.
shuttsy
1
Você pode usar o primeiro commit para um "Initial commit" que adiciona .gitignoree README.mdarquiva.
21318 Jack Miller
2
Infelizmente, essa abordagem parece quebrar o histórico de rastreamento dos arquivos adicionados na git merge .. git read-treeetapa, pois os registra como arquivos adicionados recentemente e todos os meus git guis não fazem a conexão com os commits anteriores.
Dai
1
@ksadjad, Não faço ideia, para ser sincero. O ponto central da mesclagem manual é selecionar os diretórios para formar o novo repositório e manter seus históricos de consolidação. Não tenho certeza de como lidar com essa situação em que um commit coloca arquivos em dirA, dirB, dirDrop e apenas dirA e dirB são escolhidos para o novo repositório, como o histórico do commit deve se relacionar com o original.
Chfw
27

Por que você quer correr filter-branchmais de uma vez? Você pode fazer tudo de uma só vez, portanto, não é necessário forçá-lo (observe que você precisa extglobhabilitar no seu shell para que isso funcione):

git filter-branch --index-filter "git rm -r -f --cached --ignore-unmatch $(ls -xd apps/!(AAA) libs/!(XXX))" --prune-empty -- --all

Isso deve livrar-se de todas as alterações nos subdiretórios indesejados e manter todas as suas ramificações e confirmações (a menos que elas afetem apenas os arquivos nos subdiretórios removidos, em virtude de --prune-empty) - sem problemas com confirmações duplicadas etc.

Após esta operação, os diretórios indesejados serão listados como não rastreados por git status.

O $(ls ...)necessário extglobé avaliado pelo seu shell em vez do filtro de índice, que usa o shbuiltin eval(onde extglobnão está disponível). Consulte Como habilito as opções de shell no git? para mais detalhes sobre isso.

kynan
fonte
1
Idéia interessante. Eu tenho um problema semelhante, mas não consegui fazê-lo funcionar, consulte stackoverflow.com/questions/8050687/…
manol
Isso é muito bonito o que eu precisava, embora eu tinha aspersão de ambos os arquivos e pastas em toda a minha repo ... Thanks :)
notlesh
1
hm. mesmo com o extglob ativado, estou recebendo um erro próximo ao parênteses: erro de sintaxe próximo ao token inesperado `('meu comando se parece com: git filter-branch -f --index-filter" git rm -r -f --cached - -ignore-unmatch src / css / themes /! (some_theme *) "--une-empty - - todos os ls com src / css / themes /! (some_theme *) retornam todos os outros temas para que o extglob pareça estar trabalhando ...
robdodson 2/12/12
2
@ MikeGraf Eu não acho que isso dará o resultado desejado: escapar corresponderia a um literal "!" etc. no seu caminho.
Kynan
1
A resposta (mais recente) da @ david-smiley usa uma abordagem muito semelhante, mas tem a vantagem de depender exclusivamente de gitcomandos e, portanto, não é tão suscetível a diferenças na maneira como a lsinterpretação é interpretada nos sistemas operacionais, como a @Bae descobriu.
Jeremy Caney
20

Respondendo a minha própria pergunta aqui ... depois de muitas tentativas e erros.

Consegui fazer isso usando uma combinação de git subtreee git-stitch-repo. Estas instruções são baseadas em:

Primeiro, peguei os diretórios que queria manter em seu próprio repositório separado:

cd origRepo
git subtree split -P apps/AAA -b aaa
git subtree split -P libs/XXX -b xxx

cd ..
mkdir aaaRepo
cd aaaRepo
git init
git fetch ../origRepo aaa
git checkout -b master FETCH_HEAD

cd ..
mkdir xxxRepo
cd xxxRepo
git init
git fetch ../origRepo xxx
git checkout -b master FETCH_HEAD

Criei um novo repositório vazio e importei / costurei os dois últimos nele:

cd ..
mkdir newRepo
cd newRepo
git init
git-stitch-repo ../aaaRepo:apps/AAA ../xxxRepo:libs/XXX | git fast-import

Isso cria dois ramos, master-Ae master-B, cada um segurando o conteúdo de um dos repos costurados. Para combiná-los e limpar:

git checkout master-A
git pull . master-B
git checkout master
git branch -d master-A 
git branch -d master-B

Agora não tenho muita certeza de como / quando isso acontece, mas após o primeiro checkoute o pull, o código se funde magicamente no ramo mestre (qualquer percepção sobre o que está acontecendo aqui é apreciada!)

Tudo parece ter funcionado conforme o esperado, exceto que, se eu examinar o newRepohistórico de consolidação, haverá duplicatas quando o conjunto de alterações afetou tanto apps/AAAe libs/XXX. Se houver uma maneira de remover duplicatas, seria perfeito.

prisonerjohn
fonte
Ferramentas legais que você encontrou aqui. Informações sobre "checkout": "git pull" são iguais a "git fetch && git merge". A parte "buscar" é inócua, pois você está "buscando localmente". Então, acho que esse comando checkout é o mesmo que "git merge master-B", que é um pouco mais evidente. Veja kernel.org/pub/software/scm/git/docs/git-pull.html
phord
1
Infelizmente, a ferramenta git-stitch-repo está quebrada devido a dependências ruins nos dias de hoje.
Henrik
@ Henrik Que problema você estava enfrentando exatamente? Funciona para mim, embora eu tenha que adicionar export PERL5LIB="$PERL5LIB:/usr/local/git/lib/perl5/site_perl/"à minha configuração do bash para encontrar o Git.pm. Então eu instalei com cpan.
É possível usar git subtree addpara executar esta tarefa. Veja stackoverflow.com/a/58253979/1894803
laconbass
7

Eu escrevi um filtro git para resolver exatamente esse problema. Ele tem o nome fantástico de git_filter e está localizado no github aqui:

https://github.com/slobobaby/git_filter

É baseado no excelente libgit2.

Eu precisava dividir um repositório grande com muitos commits (~ 100000) e as soluções baseadas no ramo de filtro git levaram vários dias para serem executadas. O git_filter leva um minuto para fazer a mesma coisa.

slobobaby
fonte
7

Use a extensão git 'git splits'

git splitsé um script bash que é um wrapper git branch-filterque eu criei como uma extensão git, com base na solução do jkeating .

Foi feito exatamente para esta situação. Para o seu erro, tente usar a git splits -fopção para forçar a remoção do backup. Como git splitsopera em uma nova ramificação, ela não reescreverá sua ramificação atual; portanto, o backup é estranho. Consulte o leia-me para obter mais detalhes e use-o em uma cópia / clone do seu repositório (apenas no caso!) .

  1. instalar git splits.
  2. Dividir os diretórios em uma ramificação local #change into your repo's directory cd /path/to/repo #checkout the branch git checkout XYZ
    #split multiple directories into new branch XYZ git splits -b XYZ apps/AAA libs/ZZZ

  3. Crie um repositório vazio em algum lugar. Vamos assumir que criamos um repositório vazio chamado xyzno GitHub que possui o caminho:[email protected]:simpliwp/xyz.git

  4. Envie para o novo repositório. #add a new remote origin for the empty repo so we can push to the empty repo on GitHub git remote add origin_xyz [email protected]:simpliwp/xyz.git #push the branch to the empty repo's master branch git push origin_xyz XYZ:master

  5. Clone o repositório remoto recém-criado em um novo diretório local
    #change current directory out of the old repo cd /path/to/where/you/want/the/new/local/repo #clone the remote repo you just pushed to git clone [email protected]:simpliwp/xyz.git

AndrewD
fonte
Não parece possível adicionar arquivos à divisão e atualizá-los mais tarde, certo?
21417 Alex
Este parece retardar a correr no meu repo com toneladas de commits
Shinta Smith
O git-split parece usar o filtro git --index, que é extremamente lento comparado ao --subdirectory-filter. Para alguns repositórios, ainda pode ser uma opção viável, mas para grandes repositórios (vários gigabytes, confirmações de 6 dígitos) - o filtro de índice leva semanas para ser executado, mesmo em hardware de nuvem dedicado.
Jostein Kjønigsen 14/03/19
6
git clone [email protected]:thing.git
cd thing
git fetch
for originBranch in `git branch -r | grep -v master`; do
    branch=${originBranch:7:${#originBranch}}
    git checkout $branch
done
git checkout master

git filter-branch --index-filter 'git rm --cached -qr --ignore-unmatch -- . && git reset -q $GIT_COMMIT -- dir1 dir2 .gitignore' --prune-empty -- --all

git remote set-url origin [email protected]:newthing.git
git push --all
Richard Barraclough
fonte
Ler todos os outros comentários me colocou no caminho certo. No entanto, sua solução simplesmente funciona. Importa todas as ramificações e trabalha com vários diretórios! Ótimo!
Jschober
1
O forcircuito vale a pena reconhecer, uma vez que outras respostas semelhantes não incluí-lo. Se você não tiver uma cópia local de cada ramificação no seu clone, filter-branchnão as contabilizará como parte de sua reescrita, o que poderia excluir os arquivos introduzidos em outras ramificações, mas ainda não foram mesclados à sua ramificação atual. (Apesar de ser também vale a pena fazer um git fetchem quaisquer ramos você já verificados para garantir que eles permaneçam atual.)
Jeremy Caney
5

Uma solução fácil: git-filter-repo

Eu tive um problema semelhante e, depois de revisar as várias abordagens listadas aqui, descobri o git-filter-repo . É recomendado como uma alternativa ao git-filter-branch na documentação oficial do git aqui .

Para criar um novo repositório a partir de um subconjunto de diretórios em um repositório existente, você pode usar o comando:

git filter-repo --path <file_to_remove>

Filtre vários arquivos / pastas encadeando-os:

git filter-repo --path keepthisfile --path keepthisfolder/

Portanto, para responder à pergunta original , com o git-filter-repo, você precisaria apenas do seguinte comando:

git filter-repo --path apps/AAA/ --path libs/XXX/
elmo
fonte
Esta é definitivamente uma ótima resposta. O problema com todas as outras soluções é que não consegui extrair o conteúdo de TODAS as ramificações de um diretório. No entanto, o git filter-repo recuperou a pasta de todos os ramos e reescreveu o histórico perfeitamente, como limpar toda a árvore de tudo que eu não precisava.
Teodoro
3

Sim. Forçar a substituição do backup usando o -fsinalizador nas chamadas subseqüentes filter-branchpara substituir esse aviso. :) Caso contrário, acho que você tem a solução (ou seja, erradique um diretório indesejado de cada vez filter-branch).

Jakob Borg
fonte
-4

Exclua o backup presente no diretório .git em refs / original como a mensagem sugere. O diretório está oculto.

user5200576
fonte