Scollegare (spostare) la sottodirectory in un repository Git separato


1758

Ho un repository Git che contiene una serie di sottodirectory. Ora ho scoperto che una delle sottodirectory non è correlata all'altra e dovrebbe essere staccata in un repository separato.

Come posso farlo mantenendo la cronologia dei file nella sottodirectory?

Immagino che potrei creare un clone e rimuovere le parti indesiderate di ciascun clone, ma suppongo che questo mi darebbe l'albero completo durante il controllo di una revisione precedente ecc. Ciò potrebbe essere accettabile, ma preferirei poter fingere che il due repository non hanno una cronologia condivisa.

Giusto per chiarire, ho la seguente struttura:

XYZ/
    .git/
    XY1/
    ABC/
    XY2/

Ma vorrei questo invece:

XYZ/
    .git/
    XY1/
    XY2/
ABC/
    .git/
    ABC/

7
Questo è banale ora con git filter-branchvedere la mia risposta qui sotto.
jeremyjjbrown,

8
@jeremyjjbrown ha ragione. Questo non è più difficile da fare, ma è difficile trovare la risposta giusta su Google perché tutte le vecchie risposte dominano i risultati.
Agnel Kurian,

Risposte:


1228

Aggiornamento : Questo processo è così comune, che la squadra git ha reso molto più semplice con un nuovo strumento, git subtree. Vedi qui: Stacca (sposta) la sottodirectory in un repository Git separato


Si desidera clonare il repository e quindi utilizzare git filter-branchper contrassegnare tutto tranne la sottodirectory desiderata nel nuovo repository da raccogliere in modo inutile.

  1. Per clonare il repository locale:

    git clone /XYZ /ABC
    

    (Nota: il repository verrà clonato usando hard link, ma questo non è un problema poiché i file hard link non verranno modificati di per sé - ne verranno creati di nuovi.)

  2. Ora, preserviamo anche i rami interessanti che vogliamo riscrivere, quindi rimuoviamo l'origine per evitare di spingerli lì e per assicurarci che i vecchi commit non vengano referenziati dall'origine:

    cd /ABC
    for i in branch1 br2 br3; do git branch -t $i origin/$i; done
    git remote rm origin
    

    o per tutte le filiali remote:

    cd /ABC
    for i in $(git branch -r | sed "s/.*origin\///"); do git branch -t $i origin/$i; done
    git remote rm origin
    
  3. Ora potresti voler rimuovere anche i tag che non hanno alcuna relazione con il sottoprogetto; puoi farlo anche in seguito, ma potresti dover potare di nuovo il tuo repository. Non l'ho fatto e ho ottenuto un WARNING: Ref 'refs/tags/v0.1' is unchangedtag per tutti (dal momento che erano tutti estranei al sottoprogetto); inoltre, dopo aver rimosso tali tag verrà recuperato più spazio. Apparentemente git filter-branchdovrebbe essere in grado di riscrivere altri tag, ma non sono riuscito a verificarlo. Se vuoi rimuovere tutti i tag, usa git tag -l | xargs git tag -d.

  4. Quindi utilizzare il filtro-ramo e ripristinare per escludere gli altri file, in modo che possano essere eliminati. Aggiungiamo anche --tag-name-filter cat --prune-emptyper rimuovere i commit vuoti e per riscrivere i tag (nota che questo dovrà eliminare la loro firma):

    git filter-branch --tag-name-filter cat --prune-empty --subdirectory-filter ABC -- --all
    

    o in alternativa, per riscrivere solo il ramo HEAD e ignorare tag e altri rami:

    git filter-branch --tag-name-filter cat --prune-empty --subdirectory-filter ABC HEAD
    
  5. Quindi eliminare i reflog di backup in modo che lo spazio possa essere veramente recuperato (anche se ora l'operazione è distruttiva)

    git reset --hard
    git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d
    git reflog expire --expire=now --all
    git gc --aggressive --prune=now
    

    e ora hai un repository git locale della sottodirectory ABC con tutta la sua storia preservata.

Nota: per la maggior parte degli usi, git filter-branchdovrebbe effettivamente avere il parametro aggiunto -- --all. Sì, è davvero --space-- all. Questo deve essere l'ultimo parametro per il comando. Come ha scoperto Matli, ciò mantiene i rami e i tag del progetto inclusi nel nuovo repository.

Modifica: sono stati incorporati vari suggerimenti dai commenti sottostanti per assicurarsi, ad esempio, che il repository sia effettivamente ridotto (cosa che non sempre accadeva prima).


29
Ottima risposta Grazie! E per ottenere esattamente quello che volevo, ho aggiunto "- --all" al comando filtro-ramo.
matli,

12
Perché avete bisogno --no-hardlinks? La rimozione di un collegamento fisico non influirà sull'altro file. Anche gli oggetti Git sono immutabili. Solo se cambiassi le autorizzazioni del proprietario / file di cui hai bisogno --no-hardlinks.
vdboor,

67
Un ulteriore passaggio che consiglierei sarebbe "git remote rm origin". Ciò impedirebbe ai push di tornare al repository originale, se non sbaglio.
Tom,

13
Un altro comando da aggiungere filter-branchè --prune-empty, per rimuovere i commit ora vuoti.
Seth Johnson,

8
Come Paul, non volevo i tag di progetto nel mio nuovo repository, quindi non l'ho usato -- --all. Ho anche corso git remote rm origin, e git tag -l | xargs git tag -dprima del git filter-branchcomando. Questo ha ridotto la mia .gitdirectory da 60M a ~ 300K. Nota che per ottenere la riduzione delle dimensioni avevo bisogno di eseguire entrambi questi comandi.
saltycrane,

1321

The Easy Way ™

Si scopre che questa è una pratica così comune e utile che i signori di Git lo hanno reso davvero facile, ma devi avere una versione più recente di Git (> = 1.7.11 Maggio 2012). Vedi l' appendice per come installare l'ultimo Git. Inoltre, c'è un esempio del mondo reale nella procedura dettagliata di seguito.

  1. Preparare il vecchio repository

    cd <big-repo>
    git subtree split -P <name-of-folder> -b <name-of-new-branch>
    

    Nota: <name-of-folder> NON deve contenere caratteri iniziali o finali. Ad esempio, la cartella denominata subprojectDEVE essere passata come subproject, NOT./subproject/

    Nota per utenti Windows: quando la profondità della cartella è> 1, è <name-of-folder>necessario disporre del separatore di cartelle in stile * nix (/). Ad esempio, la cartella denominata path1\path2\subprojectDEVE essere passata comepath1/path2/subproject

  2. Crea il nuovo repository

    mkdir ~/<new-repo> && cd ~/<new-repo>
    git init
    git pull </path/to/big-repo> <name-of-new-branch>
    
  3. Collega il nuovo repository a GitHub o ovunque

    git remote add origin <git@github.com:user/new-repo.git>
    git push -u origin master
    
  4. Pulizia all'interno <big-repo>, se lo si desidera

    git rm -rf <name-of-folder>
    

    Nota : questo lascia tutti i riferimenti storici nel repository.Vedi l' Appendice di seguito se sei effettivamente preoccupato di aver commesso una password o devi ridurre le dimensioni del file della tua .gitcartella.

...

Procedura dettagliata

Questi sono gli stessi passaggi di cui sopra , ma seguendo i miei esatti passaggi per il mio repository invece di utilizzare <meta-named-things>.

Ecco un progetto che ho per l'implementazione di moduli browser JavaScript nel nodo:

tree ~/node-browser-compat

node-browser-compat
├── ArrayBuffer
├── Audio
├── Blob
├── FormData
├── atob
├── btoa
├── location
└── navigator

Voglio dividere una singola cartella btoa, in un repository Git separato

cd ~/node-browser-compat/
git subtree split -P btoa -b btoa-only

Ora ho una nuova filiale, btoa-onlyche ha solo commit btoae voglio creare un nuovo repository.

mkdir ~/btoa/ && cd ~/btoa/
git init
git pull ~/node-browser-compat btoa-only

Quindi creo un nuovo repository su GitHub o Bitbucket o qualsiasi altra cosa e lo aggiungo come origin

git remote add origin git@github.com:node-browser-compat/btoa.git
git push -u origin master

Giorno felice!

Nota: Se si è creato un pronti contro termine con una README.md, .gitignoree LICENSE, è necessario tirare prima:

git pull origin master
git push origin master

Infine, desidero rimuovere la cartella dal repository più grande

git rm -rf btoa

...

Appendice

Git più recente su macOS

Per ottenere l'ultima versione di Git usando Homebrew :

brew install git

Git più recente su Ubuntu

sudo apt-get update
sudo apt-get install git
git --version

Se non funziona (hai una versione molto vecchia di Ubuntu), prova

sudo add-apt-repository ppa:git-core/ppa
sudo apt-get update
sudo apt-get install git

Se il problema persiste, prova

sudo chmod +x /usr/share/doc/git/contrib/subtree/git-subtree.sh
sudo ln -s \
/usr/share/doc/git/contrib/subtree/git-subtree.sh \
/usr/lib/git-core/git-subtree

Grazie a rui.araujo dai commenti.

Cancella la tua storia

Per impostazione predefinita, la rimozione dei file da Git in realtà non li rimuove, si impegna semplicemente a non essere più lì. Se desideri effettivamente rimuovere i riferimenti storici (ovvero hai una password impegnata), devi farlo:

git filter-branch --prune-empty --tree-filter 'rm -rf <name-of-folder>' HEAD

Dopodiché puoi verificare che il tuo file o la tua cartella non compaiano più nella cronologia di Git

git log -- <name-of-folder> # should show nothing

Tuttavia, non puoi "spingere" le eliminazioni su GitHub e simili. Se provi riceverai un errore e dovrai farlo git pullprima che tu possa farlo git push- e poi tornerai ad avere tutto nella tua storia.

Pertanto, se si desidera eliminare la cronologia da "origine", ovvero eliminarla da GitHub, Bitbucket, ecc., È necessario eliminare il repository e ripetere il push di una copia eliminata del repository. Ma aspetta - c'è di più ! - Se sei davvero preoccupato di eliminare una password o qualcosa del genere, dovrai eliminare il backup (vedi sotto).

rendere .gitpiù piccolo

Il comando di eliminazione della cronologia sopra menzionato lascia ancora un mucchio di file di backup - perché Git è fin troppo gentile nell'aiutarti a non rovinare il tuo repository per caso. Alla fine cancellerà i file orfani nel corso dei giorni e dei mesi, ma li lascerà lì per un po 'nel caso in cui ti rendi conto che hai eliminato accidentalmente qualcosa che non volevi.

Quindi, se vuoi davvero svuotare il cestino per ridurre immediatamente la dimensione del clone di un repository, devi fare tutte queste cose davvero strane:

rm -rf .git/refs/original/ && \
git reflog expire --all && \
git gc --aggressive --prune=now

git reflog expire --all --expire-unreachable=0
git repack -A -d
git prune

Detto questo, consiglierei di non eseguire questi passaggi a meno che tu non sappia che è necessario - nel caso in cui tu abbia potato la sottodirectory sbagliata, sai? I file di backup non dovrebbero essere clonati quando si effettua il push del repository, saranno solo nella copia locale.

Credito


16
git subtreefa ancora parte della cartella 'contrib' e non è installato di default su tutte le distro. github.com/git/git/blob/master/contrib/subtree
onionjake

11
@krlmlr sudo chmod + x /usr/share/doc/git/contrib/subtree/git-subtree.sh sudo ln -s /usr/share/doc/git/contrib/subtree/git-subtree.sh / usr / lib / git-core / git-subtree Per attivare su Ubuntu 13.04
rui.araujo,

41
Se hai inviato una password a un repository pubblico, dovresti cambiare la password, non provare a rimuoverla dal repository pubblico e sperare che nessuno l'abbia vista.
Miles Rout,

8
Questa soluzione non conserva la storia.
Cœur l'

18
Il comando popde pushdrende questo piuttosto implicito e più difficile capire ciò che intende fare ...
jones77

133

La risposta di Paul crea un nuovo repository contenente / ABC, ma non rimuove / ABC da / XYZ. Il seguente comando rimuoverà / ABC da / XYZ:

git filter-branch --tree-filter "rm -rf ABC" --prune-empty HEAD

Naturalmente, prima provalo in un repository 'clone --no-hardlinks', e seguilo con i comandi reset, gc e prune elenchi di Paul.


53
fallo git filter-branch --index-filter "git rm -r -f --cached --ignore-unmatch ABC" --prune-empty HEADe sarà molto più veloce. index-filter funziona sull'indice mentre tree-filter deve effettuare il checkout e mettere in scena tutto per ogni commit .
Fmarc,

51
in alcuni casi confondere la storia del repository XYZ è eccessivo ... solo un semplice "rm -rf ABC; git rm -r ABC; git commit -m'estratto ABC nel suo repository" "funzionerebbe meglio per la maggior parte delle persone.
Evgeny,

2
Probabilmente si desidera utilizzare -f (force) su questo comando se lo si fa più di una volta, ad esempio, per rimuovere due directory dopo che sono state separate. Altrimenti otterrai "Impossibile creare un nuovo backup".
Brian Carlton,

4
Se stai eseguendo il --index-filtermetodo, potresti anche volerlo fare git rm -q -r -f, in modo che ogni invocazione non stampi una riga per ogni file che elimina.
Eric Naeseth,

1
Suggerirei di modificare la risposta di Paul, solo perché quella di Paul è così accurata.
Erik Aronesty,

96

Ho scoperto che per eliminare correttamente la vecchia cronologia dal nuovo repository, devi fare un po 'più di lavoro dopo il filter-branchpassaggio.

  1. Fai il clone e il filtro:

    git clone --no-hardlinks foo bar; cd bar
    git filter-branch --subdirectory-filter subdir/you/want
    
  2. Rimuovi ogni riferimento alla vecchia storia. "Origin" stava tenendo traccia del tuo clone, e "originale" è dove filtro-ramo salva le cose vecchie:

    git remote rm origin
    git update-ref -d refs/original/refs/heads/master
    git reflog expire --expire=now --all
    
  3. Anche ora, la tua storia potrebbe essere bloccata in un file di pacchetto che fsck non toccherà. Strappalo a brandelli, creando un nuovo file di pacchetto ed eliminando gli oggetti inutilizzati:

    git repack -ad
    

C'è una spiegazione di questo nel manuale di filtro-ramo .


3
Penso che git gc --aggressive --prune=nowmanchi ancora qualcosa del genere , no?
Albert,

1
@Albert Il comando repack si occupa di questo e non ci sarebbero oggetti sciolti.
Josh Lee,

Sì, git gc --aggressive --prune=nowridotto gran parte dei nuovi repository
Tomek Wyderka il

Semplice ed elegante. Grazie!
Marco Pelegrini,

40

Modifica: script Bash aggiunto.

Le risposte fornite qui hanno funzionato solo parzialmente per me; Molti file di grandi dimensioni sono rimasti nella cache. Cosa alla fine ha funzionato (dopo ore in #git su freenode):

git clone --no-hardlinks file:///SOURCE /tmp/blubb
cd blubb
git filter-branch --subdirectory-filter ./PATH_TO_EXTRACT  --prune-empty --tag-name-filter cat -- --all
git clone file:///tmp/blubb/ /tmp/blooh
cd /tmp/blooh
git reflog expire --expire=now --all
git repack -ad
git gc --prune=now

Con le soluzioni precedenti, la dimensione del repository era di circa 100 MB. Questo lo ha portato a 1,7 MB. Forse aiuta qualcuno :)


Il seguente script bash automatizza l'attività:

!/bin/bash

if (( $# < 3 ))
then
    echo "Usage:   $0 </path/to/repo/> <directory/to/extract/> <newName>"
    echo
    echo "Example: $0 /Projects/42.git first/answer/ firstAnswer"
    exit 1
fi


clone=/tmp/${3}Clone
newN=/tmp/${3}

git clone --no-hardlinks file://$1 ${clone}
cd ${clone}

git filter-branch --subdirectory-filter $2  --prune-empty --tag-name-filter cat -- --all

git clone file://${clone} ${newN}
cd ${newN}

git reflog expire --expire=now --all
git repack -ad
git gc --prune=now

26

Questo non è più così complesso che puoi semplicemente usare il comando git filter-branch su un clone del tuo repository per eliminare le sottodirectory che non desideri e quindi passare al nuovo telecomando.

git filter-branch --prune-empty --subdirectory-filter <YOUR_SUBDIR_TO_KEEP> master
git push <MY_NEW_REMOTE_URL> -f .

3
Questo ha funzionato come un fascino. YOUR_SUBDIR nell'esempio sopra è la sottodirectory che vuoi tenere, tutto il resto verrà rimosso
JT Taylor

1
Aggiornamenti basati sul tuo commento.
jeremyjjbrown,

2
Questo non risponde alla domanda. Dai documenti che dice The result will contain that directory (and only that) as its project root.e in effetti questo è ciò che otterrai, ovvero la struttura del progetto originale non viene preservata.
NicBright,

2
@NicBright Puoi illustrare il tuo problema con XYZ e ABC come nella domanda, per mostrare cosa c'è che non va?
Adam,

@jeremyjjbrown è possibile riutilizzare il repo clonato e non utilizzare un nuovo pronti contro termine, vale a dire la mia domanda qui stackoverflow.com/questions/49269602/...
Qiulang

19

Aggiornamento : il modulo git-subtree era così utile che il team git lo ha inserito nel core e lo ha reso git subtree. Vedi qui: Stacca (sposta) la sottodirectory in un repository Git separato

git-subtree può essere utile per questo

http://github.com/apenwarr/git-subtree/blob/master/git-subtree.txt (obsoleto)

http://psionides.jogger.pl/2010/02/04/sharing-code-between-projects-with-git-subtree/


1
git-subtree ora fa parte di Git, sebbene sia nella struttura di contrib, quindi non sempre installato di default. So che è installato dalla formula Homebrew git, ma senza la sua pagina man. apenwarr chiama quindi la sua versione obsoleta.
ecristopherson,

19

Ecco una piccola modifica al CoolAJ86 s' 'La via più semplice ™' risposta al fine di dividere più cartelle secondarie (diciamo sub1e sub2) in un nuovo repository git.

The Easy Way ™ (più sottocartelle)

  1. Preparare il vecchio repository

    pushd <big-repo>
    git filter-branch --tree-filter "mkdir <name-of-folder>; mv <sub1> <sub2> <name-of-folder>/" HEAD
    git subtree split -P <name-of-folder> -b <name-of-new-branch>
    popd
    

    Nota: <name-of-folder> NON deve contenere caratteri iniziali o finali. Ad esempio, la cartella denominata subprojectDEVE essere passata come subproject, NOT./subproject/

    Nota per gli utenti di Windows: quando la profondità della cartella è> 1, è <name-of-folder>necessario disporre del separatore di cartelle in stile * nix (/). Ad esempio, la cartella denominata path1\path2\subprojectDEVE essere passata come path1/path2/subproject. Inoltre non usare il mvcomando ma move.

    Nota finale: la differenza unica e grande con la risposta di base è la seconda riga dello script " git filter-branch..."

  2. Crea il nuovo repository

    mkdir <new-repo>
    pushd <new-repo>
    
    git init
    git pull </path/to/big-repo> <name-of-new-branch>
    
  3. Collega il nuovo repository a Github o ovunque

    git remote add origin <git@github.com:my-user/new-repo.git>
    git push origin -u master
    
  4. Pulizia, se lo si desidera

    popd # get out of <new-repo>
    pushd <big-repo>
    
    git rm -rf <name-of-folder>
    

    Nota : questo lascia tutti i riferimenti storici nel repository. Vedi l' Appendice nella risposta originale se sei effettivamente preoccupato di aver impegnato una password o devi ridurre le dimensioni del file della tua .gitcartella.


1
Questo ha funzionato per me con lievi modifiche. Perché i miei sub1e sub2le cartelle non esistevano con la versione iniziale, ho dovuto modificare il mio --tree-filterscript come segue: "mkdir <name-of-folder>; if [ -d sub1 ]; then mv <sub1> <name-of-folder>/; fi". Per il secondo filter-branchcomando ho sostituito <sub1> con <sub2>, ho omesso la creazione di <nome-cartella> e incluso -fdopo filter-branchper sovrascrivere l'avviso di un backup esistente.
pglezen,

Questo non funziona se uno dei sottodir è cambiato durante la cronologia in git. Come può essere risolto?
Nietras,

@nietras vedi la risposta di rogerdpack. Mi ci è voluto un po 'per trovarlo dopo aver letto e assorbito tutte le informazioni in queste altre risposte.
Adam,

12

La domanda originale vuole che XYZ / ABC / (* file) diventino ABC / ABC / (* file). Dopo aver implementato la risposta accettata per il mio codice, ho notato che in realtà cambia XYZ / ABC / (* file) in ABC / (* file). La pagina man del filtro-ramo dice anche,

Il risultato conterrà quella directory (e solo quella) come root del progetto . "

In altre parole, promuove la cartella di livello superiore "su" di un livello. Questa è una distinzione importante perché, ad esempio, nella mia storia avevo rinominato una cartella di livello superiore. Promuovendo le cartelle "su" di un livello, git perde continuità nel commit in cui ho rinominato.

Ho perso la continuità dopo il filtro-ramo

La mia risposta alla domanda è quindi di fare 2 copie del repository ed eliminare manualmente le cartelle che si desidera conservare in ciascuna. La pagina man mi supporta con questo:

[...] evitare di usare [questo comando] se fosse sufficiente un semplice commit per risolvere il problema


1
Mi piace lo stile di quel grafico. Posso chiederti quale strumento stai usando?
Slipp D. Thompson,

3
Tower per Mac. Mi piace veramente. Vale quasi la pena passare a Mac per sé.
MM.

2
Sì, anche se nel mio caso, la mia sottocartella è targetdirstata rinominata ad un certo punto e l' git filter-branchho semplicemente chiamata un giorno, eliminando tutti gli commit effettuati prima della ridenominazione! Scioccante, considerando quanto Git sia abile nel tenere traccia di queste cose e persino nella migrazione di singoli blocchi di contenuti!
Jay Allen,

1
Oh, anche se qualcuno si trova nella stessa barca, ecco il comando che ho usato. Non dimenticare che git rmrichiede più argomenti, quindi non c'è motivo di eseguirlo per ogni file / cartella: BYEBYE="dir/subdir2 dir2 file1 dir/file2"; git filter-branch -f --index-filter "git rm -q -r -f --cached --ignore-unmatch $BYEBYE" --prune-empty -- --all
Jay Allen

7

Da aggiungere alla risposta di Paul , ho scoperto che alla fine per recuperare spazio, devo spingere HEAD in un repository pulito e questo riduce le dimensioni della directory .git / objects / pack.

vale a dire

$ mkdir ... ABC.git
$ cd ... ABC.git
$ git init --bare

Dopo la prugna gc, fai anche:

$ git push ... ABC.git HEAD

Quindi puoi farlo

$ git clone ... ABC.git

e la dimensione di ABC / .git è ridotta

In realtà, alcuni dei passaggi che richiedono tempo (ad esempio git gc) non sono necessari con il push per pulire il repository, ovvero:

$ git clone --no-hardlinks / XYZ / ABC
$ git filter-branch --subdirectory-filter ABC HEAD
$ git reset --hard
$ git push ... ABC.git HEAD

6

Il modo corretto ora è il seguente:

git filter-branch --prune-empty --subdirectory-filter FOLDER_NAME [first_branch] [another_branch]

GitHub ora ha anche un piccolo articolo su tali casi.

Assicurati però di clonare prima il repository originale in una directory separata (in quanto eliminerebbe tutti i file e le altre directory e probabilmente dovrai lavorare con loro).

Quindi il tuo algoritmo dovrebbe essere:

  1. clonare il repository remoto in un'altra directory
  2. usando git filter-branchsolo i file di sinistra in qualche sottodirectory, passa al nuovo telecomando
  3. create commit per rimuovere questa sottodirectory dal repository remoto originale

6

Sembra che la maggior parte (tutte?) Delle risposte qui si basino su una qualche forma di git filter-branch --subdirectory-filtere simili. Questo potrebbe funzionare "la maggior parte delle volte", tuttavia per alcuni casi, ad esempio il caso in cui hai rinominato la cartella, ad esempio:

 ABC/
    /move_this_dir # did some work here, then renamed it to

ABC/
    /move_this_dir_renamed

Se si esegue uno stile di filtro git normale per estrarre "move_me_renamed", si perderà la cronologia delle modifiche ai file che si è verificata da quando era inizialmente move_this_dir ( ref ).

Sembra quindi che l'unico modo per mantenere davvero tutta la cronologia dei cambiamenti (se il tuo è un caso come questo), è, in sostanza, copiare il repository (creare un nuovo repository, impostarlo come origine), quindi annusare tutto il resto e rinominare la sottodirectory nel genitore in questo modo:

  1. Clonare il progetto multi-modulo localmente
  2. Filiali: controlla cosa c'è: git branch -a
  3. Fai un checkout per ogni ramo da includere nella divisione per ottenere una copia locale sulla tua stazione di lavoro: git checkout --track origin/branchABC
  4. Crea una copia in una nuova directory: cp -r oldmultimod simple
  5. Vai nella nuova copia del progetto: cd simple
  6. Sbarazzati degli altri moduli che non sono necessari in questo progetto:
  7. git rm otherModule1 other2 other3
  8. Ora rimane solo il sottodir del modulo target
  9. Elimina il sottodirectory del modulo in modo che la radice del modulo diventi la nuova radice del progetto
  10. git mv moduleSubdir1/* .
  11. Elimina il sottodirectory della reliquia: rmdir moduleSubdir1
  12. Controlla le modifiche in qualsiasi momento: git status
  13. Crea il nuovo repository git e copia il suo URL per puntare questo progetto al suo interno:
  14. git remote set-url origin http://mygithost:8080/git/our-splitted-module-repo
  15. Verifica che sia buono: git remote -v
  16. Invia le modifiche al repository remoto: git push
  17. Vai al repository remoto e controlla che sia tutto lì
  18. Ripetere l'operazione per qualsiasi altro ramo necessario: git checkout branch2

Questo segue il documento github "Dividere una sottocartella in un nuovo repository" passi 6-11 per spingere il modulo in un nuovo repository.

Questo non ti farà risparmiare spazio nella cartella .git, ma manterrà tutta la cronologia delle modifiche per quei file anche attraverso i nomi. E questo potrebbe non valerne la pena se non si perde "molta" storia, ecc. Ma almeno si è certi di non perdere impegni più vecchi!


1
Ho trovato l'ago nel pagliaio git! Ora posso conservare TUTTA la mia cronologia di commit.
Adam,

5

Consiglio la guida di GitHub per suddividere le sottocartelle in un nuovo repository . I passaggi sono simili alla risposta di Paul , ma ho trovato le loro istruzioni più facili da capire.

Ho modificato le istruzioni in modo che si applichino a un repository locale, anziché a quello ospitato su GitHub.


Dividere una sottocartella in un nuovo repository

  1. Apri Git Bash.

  2. Modificare la directory di lavoro corrente nella posizione in cui si desidera creare il nuovo repository.

  3. Clonare il repository che contiene la sottocartella.

git clone OLD-REPOSITORY-FOLDER NEW-REPOSITORY-FOLDER
  1. Cambia la directory di lavoro corrente nel tuo repository clonato.

cd REPOSITORY-NAME
  1. Per filtrare la sottocartella dal resto dei file nel repository, eseguire git filter-branch, fornendo queste informazioni:
    • FOLDER-NAME: La cartella all'interno del tuo progetto da cui desideri creare un repository separato.
      • Suggerimento: gli utenti Windows devono utilizzare /per delimitare le cartelle.
    • BRANCH-NAME: Il ramo predefinito per il tuo progetto attuale, per esempio, mastero gh-pages.

git filter-branch --prune-empty --subdirectory-filter FOLDER-NAME  BRANCH-NAME 
# Filter the specified branch in your directory and remove empty commits
Rewrite 48dc599c80e20527ed902928085e7861e6b3cbe6 (89/89)
Ref 'refs/heads/BRANCH-NAME' was rewritten

Bel post, ma noto che il primo paragrafo del documento che hai collegato dice If you create a new clone of the repository, you won't lose any of your Git history or changes when you split a folder into a separate repository.Eppure, in base ai commenti su tutte le risposte qui, entrambi filter-branche lo subtreescript provocano la perdita della storia ovunque una sottodirectory sia stata rinominata. C'è qualcosa che può essere fatto per affrontare questo?
Adam,

Ho trovato la soluzione per preservare tutti i commit, inclusi quelli che hanno precedentemente rinominato / spostato la directory - è la risposta di rogerdpack a questa domanda.
Adam,

L'unico problema è che non posso più usare il repository clonato
Qiulang

5

Quando viene eseguito git filter-branchutilizzando una versione più recente di git( 2.22+forse?), Si dice di usare questo nuovo strumento git-filter-repo . Questo strumento ha sicuramente semplificato le cose per me.

Filtraggio con filtro repository

Comandi per creare il XYZrepository dalla domanda originale:

# create local clone of original repo in directory XYZ
tmp $ git clone git@github.com:user/original.git XYZ

# switch to working in XYZ
tmp $ cd XYZ

# keep subdirectories XY1 and XY2 (dropping ABC)
XYZ $ git filter-repo --path XY1 --path XY2

# note: original remote origin was dropped
# (protecting against accidental pushes overwriting original repo data)

# XYZ $ ls -1
# XY1
# XY2

# XYZ $ git log --oneline
# last commit modifying ./XY1 or ./XY2
# first commit modifying ./XY1 or ./XY2

# point at new hosted, dedicated repo
XYZ $ git remote add origin git@github.com:user/XYZ.git

# push (and track) remote master
XYZ $ git push -u origin master

ipotesi: * il repository XYZ remoto era nuovo e vuoto prima del push

Filtraggio e spostamento

Nel mio caso, volevo anche spostare un paio di directory per una struttura più coerente. Inizialmente, ho eseguito quel semplice filter-repocomando seguito da git mv dir-to-rename, ma ho scoperto che potevo ottenere una cronologia leggermente "migliore" usando l' --path-renameopzione. Invece di vedere l'ultima modifica 5 hours agosui file spostati nel nuovo repository ora vedo last year(nell'interfaccia utente di GitHub), che corrisponde ai tempi modificati nel repository originale.

Invece di...

git filter-repo --path XY1 --path XY2 --path inconsistent
git mv inconsistent XY3  # which updates last modification time

Alla fine ho corso ...

git filter-repo --path XY1 --path XY2 --path inconsistent --path-rename inconsistent:XY3
Appunti:
  • Pensavo che il post sul blog di Git Rev News spiegasse bene il ragionamento alla base della creazione di un altro strumento di filtro repo.
  • Inizialmente ho provato il percorso di creazione di una sottodirectory che corrispondesse al nome del repository di destinazione nel repository originale e quindi di filtrare (utilizzando git filter-repo --subdirectory-filter dir-matching-new-repo-name). Tale comando ha convertito correttamente quella sottodirectory nella radice del repository locale copiato, ma ha anche portato a una cronologia dei soli tre commit necessari per creare la sottodirectory. (Non mi ero reso conto che --pathpotesse essere specificato più volte; quindi, ovviando alla necessità di creare una sottodirectory nel repository di origine.) Dal momento che qualcuno si era impegnato nel repository di origine nel momento in cui ho notato che non ero riuscito a portare avanti il cronologia, ho appena usato git reset commit-before-subdir-move --harddopo il clonecomando e aggiunto --forceal filter-repocomando per farlo funzionare sul clone locale leggermente modificato.
git clone ...
git reset HEAD~7 --hard      # roll back before mistake
git filter-repo ... --force  # tell filter-repo the alterations are expected
  • Ero sconcertato sull'installazione poiché non ero a conoscenza del modello di estensione con git, ma alla fine ho clonato git-filter-repo e l'ho collegato a $(git --exec-path):
ln -s ~/github/newren/git-filter-repo/git-filter-repo $(git --exec-path)

1
Upvoted per raccomandare il nuovo filter-repostrumento (che ho presentato il mese scorso a stackoverflow.com/a/58251653/6309 )
VonC

L'uso git-filter-repodovrebbe essere sicuramente l'approccio preferito a questo punto. È molto, molto più veloce e più sicuro di git-filter-branch, e salvaguardie contro molti dei problemi che si possono incontrare quando si riscrive la propria storia git. Spero che questa risposta attiri un po 'più di attenzione, poiché è quella a cui rivolgersi git-filter-repo.
Jeremy Caney,

4

Ho avuto esattamente questo problema, ma tutte le soluzioni standard basate su git filter-branch erano estremamente lente. Se hai un piccolo repository, questo potrebbe non essere un problema, è stato per me. Ho scritto un altro programma di filtro git basato su libgit2 che come primo passo crea rami per ogni filtro del repository primario e poi li spinge per pulire i repository come il passo successivo. Sul mio repository (500Mb di 100000 commit) i metodi standard di branch del filtro git hanno richiesto giorni. Il mio programma impiega pochi minuti a fare lo stesso filtro.

Ha il favoloso nome di git_filter e vive qui:

https://github.com/slobobaby/git_filter

su GitHub.

Spero sia utile a qualcuno.


4

Utilizzare questo comando di filtro per rimuovere una sottodirectory, preservando tag e rami:

git filter-branch --index-filter \
"git rm -r -f --cached --ignore-unmatch DIR" --prune-empty \
--tag-name-filter cat -- --all

cos'è il gatto qui?
rogerdpack,

4

Per quello che vale, ecco come usare GitHub su un computer Windows. Supponiamo che tu abbia un repository clonato per risiedere in C:\dir1. La struttura di directory simile a questo: C:\dir1\dir2\dir3. La dir3directory è quella che voglio essere un nuovo repository separato.

Github:

  1. Crea il tuo nuovo repository: MyTeam/mynewrepo

Bash Prompt:

  1. $ cd c:/Dir1
  2. $ git filter-branch --prune-empty --subdirectory-filter dir2/dir3 HEAD
    Restituito: Ref 'refs/heads/master' was rewritten(a proposito: dir2 / dir3 fa distinzione tra maiuscole e minuscole.)

  3. $ git remote add some_name git@github.com:MyTeam/mynewrepo.git
    git remote add origin etc. non ha funzionato, restituito " remote origin already exists"

  4. $ git push --progress some_name master


3

Come ho detto sopra , ho dovuto usare la soluzione inversa (eliminando tutti i commit non toccando il miodir/subdir/targetdir ) che sembrava funzionare abbastanza bene rimuovendo circa il 95% dei commit (come desiderato). Rimangono tuttavia due piccoli problemi.

PRIMO , ha filter-branchfatto un ottimo lavoro di rimozione di commit che introducono o modificano il codice ma apparentemente, i commit di fusione sono sotto la sua stazione nel Gitiverse.

Questo è un problema estetico con cui probabilmente posso convivere (dice ... indietreggiando lentamente con gli occhi distesi) .

SECONDO i pochi commit che rimangono sono praticamente TUTTI duplicati! Mi sembra di aver acquisito una seconda sequenza temporale ridondante che abbraccia quasi l'intera storia del progetto. La cosa interessante (che puoi vedere dalla foto sotto), è che i miei tre rami locali non sono tutti sulla stessa linea temporale (che è, certamente, perché esiste e non è solo la spazzatura raccolta).

L'unica cosa che posso immaginare è che uno dei commit eliminati era, forse, il commit a unione singola che filter-branch effettivamente è stato eliminato e che ha creato la sequenza temporale parallela mentre ogni componente ora non unito ha preso la propria copia dei commit. ( scrollata di spalle Dov'è il mio TARDiS?) Sono abbastanza sicuro di poter risolvere questo problema, anche se mi piacerebbe davvero capire come è successo.

Nel caso del folle mergefest-O-RAMA, probabilmente lascerò quell'uno da solo poiché si è così saldamente radicato nella mia storia di commit - minaccioso con me ogni volta che mi avvicino -, non sembra effettivamente causare eventuali problemi non estetici e perché è abbastanza carino in Tower.app.


3

Il modo più semplice

  1. installa git splits. L'ho creato come estensione git, basato sulla soluzione di jkeating .
  2. Dividi le directory in una filiale locale #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 XY1 XY2

  3. Crea un repository vuoto da qualche parte. Supponiamo di aver creato un repository vuoto chiamato xyzsu GitHub con percorso:git@github.com:simpliwp/xyz.git

  4. Passa al nuovo repository. #add a new remote origin for the empty repo so we can push to the empty repo on GitHub git remote add origin_xyz git@github.com:simpliwp/xyz.git #push the branch to the empty repo's master branch git push origin_xyz XYZ:master

  5. Clonare il repository remoto appena creato in una nuova directory locale
    #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 git@github.com:simpliwp/xyz.git


Un vantaggio di questo metodo rispetto a "The Easy Way" è che il telecomando è già impostato per il nuovo repository, quindi è possibile eseguire immediatamente un'aggiunta di sottostruttura. In effetti in questo modo mi sembra più facile (anche senza git splits)
MM

Props to AndrewD per aver pubblicato questa soluzione. Ho modificato il suo repository per farlo funzionare su OSX ( github.com/ricardoespsanto/git-splits ) se questo è utile a chiunque altro
ricardoespsanto,

2

Potresti aver bisogno di qualcosa come "git reflog expire --expire = now --all" prima della garbage collection per pulire effettivamente i file. git filter-branch rimuove solo i riferimenti nella cronologia, ma non rimuove le voci di reflog che contengono i dati. Certo, prova prima questo.

Il mio utilizzo del disco è diminuito drasticamente nel fare ciò, sebbene le mie condizioni iniziali fossero leggermente diverse. Forse --subdirectory-filter annulla questa necessità, ma ne dubito.


2

Dai un'occhiata al progetto git_split su https://github.com/vangorra/git_split

Trasforma le directory git nei loro repository nella loro posizione. Nessun affare divertente. Questo script prenderà una directory esistente nel tuo repository git e trasformerà quella directory in un repository indipendente. Lungo la strada, copierà l'intera cronologia delle modifiche per la directory che hai fornito.

./git_split.sh <src_repo> <src_branch> <relative_dir_path> <dest_repo>
        src_repo  - The source repo to pull from.
        src_branch - The branch of the source repo to pull from. (usually master)
        relative_dir_path   - Relative path of the directory in the source repo to split.
        dest_repo - The repo to push to.

1

Metti questo nel tuo gitconfig:

reduce-to-subfolder = !sh -c 'git filter-branch --tag-name-filter cat --prune-empty --subdirectory-filter cookbooks/unicorn HEAD && git reset --hard && git for-each-ref refs/original/ | cut -f 2 | xargs -n 1 git update-ref -d && git reflog expire --expire=now --all && git gc --aggressive --prune=now && git remote rm origin'

1

Sono sicuro che git subtree sia tutto perfetto e meraviglioso, ma le mie sottodirectory del codice gestito da git che volevo spostare erano tutte in eclissi. Quindi, se stai usando egit, è dolorosamente facile. Prendi il progetto che vuoi spostare e team-> disconnettilo, quindi team-> condividilo nella nuova posizione. L'impostazione predefinita tenterà di utilizzare la vecchia posizione del repository, ma è possibile deselezionare la selezione esistente e scegliere la nuova posizione per spostarla. Tutti grandine egit.


3
La parte "bella e meravigliosa" della sottostruttura è che la storia della tua sottodirectory arriva per la corsa. Se non hai bisogno della storia, allora il tuo metodo dolorosamente semplice è la strada da percorrere.
pglezen,

0

Puoi facilmente provare il https://help.github.com/enterprise/2.15/user/articles/splitting-a-subfolder-out-into-a-new-repository/

Questo ha funzionato per me. I problemi che ho affrontato nei passaggi sopra indicati sono

  1. in questo comando git filter-branch --prune-empty --subdirectory-filter FOLDER-NAME BRANCH-NAME The BRANCH-NAMEis master

  2. se l'ultimo passaggio fallisce quando si commette a causa di un problema di protezione, seguire - https://docs.gitlab.com/ee/user/project/protected_branches.html


0

Ho trovato una soluzione abbastanza semplice, L'idea è quella di copiare il repository e quindi rimuovere solo le parti non necessarie. Ecco come funziona:

1) Clonare un repository che si desidera dividere

git clone git@git.thehost.io:testrepo/test.git

2) Passa alla cartella git

cd test/

2) Rimuovere le cartelle non necessarie e impegnarlo

rm -r ABC/
git add .
enter code here
git commit -m 'Remove ABC'

3) Rimuovere la cronologia dei moduli non necessari con BFG

cd ..
java -jar bfg.jar --delete-folders "{ABC}" test
cd test/
git reflog expire --expire=now --all && git gc --prune=now --aggressive

per le cartelle multiple puoi usare la virgola

java -jar bfg.jar --delete-folders "{ABC1,ABC2}" metric.git

4) Verifica che la cronologia non contenga i file / le cartelle che hai appena eliminato

git log --diff-filter=D --summary | grep delete

5) Ora hai un repository pulito senza ABC, quindi spingilo nella nuova origine

remote add origin git@github.com:username/new_repo
git push -u origin master

Questo è tutto. Puoi ripetere i passaggi per ottenere un altro repository,

basta rimuovere XY1, XY2 e rinominare XYZ -> ABC al passaggio 3


Quasi perfetto ... ma hai dimenticato "git filter-branch --prune-empty" per rimuovere tutti i vecchi commit che ora sono vuoti. Da fare prima di spingere al master di origine!
ZettaCircl,

Se hai commesso l'errore e vuoi ancora "ripetere" dopo aver rimosso il vecchio commit vuoto, esegui: "git push -u origin master --force-with-lease"
ZettaCircl
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.