Scollega molte sottodirectory in un nuovo repository Git separato


135

Questa domanda si basa sulla sottodirectory Detach in un repository Git separato

Invece di staccare una singola sottodirectory, voglio staccare un paio. Ad esempio, il mio albero di directory corrente è simile al seguente:

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

E vorrei questo invece:

/apps
  /AAA
/libs
  /XXX

L' --subdirectory-filterargomento git filter-branchnon funzionerà perché si sbarazza di tutto tranne che per la directory data la prima volta che viene eseguito. Ho pensato che usare l' --index-filterargomento per tutti i file indesiderati avrebbe funzionato (anche se noioso), ma se provo a eseguirlo più di una volta, ricevo il seguente messaggio:

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

Qualche idea? TIA

Risposte:


155

Invece di dover gestire una subshell e usare ext glob (come suggerito da Kynan), prova questo approccio molto più semplice:

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

Come menzionato da void.pointer nel suo commento , questo rimuoverà tutto tranne apps/AAAe libs/XXXdal repository corrente.

Elimina le operazioni di unione vuote

Ciò lascia molte fusioni vuote. Questi possono essere rimossi con un altro passaggio come descritto da raphinesse nella sua risposta :

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

⚠️ Avvertenza : quanto sopra deve utilizzare la versione GNU di sede xargsaltrimenti rimuove tutti i commit in quanto xargsfallisce. brew install gnu-sed findutilse quindi usa gsede gxargs:

git filter-branch --prune-empty --parent-filter \
'gsed "s/-p //g" | gxargs git show-branch --independent | gsed "s/\</-p /g"' 

4
inoltre, il flag --ignore-unmatch dovrebbe essere passato a git rm, altrimenti non è riuscito per il primo commit (il repository è stato creato con il clone git svn nel mio caso)
Pontomedon,

8
Supponendo che tu abbia tag nel mix, probabilmente dovresti aggiungere --tag-name-filter catai tuoi parametri
Yonatan,

16
Potresti aggiungere qualche informazione in più spiegando cosa sta facendo questo lungo comando?
Burhan Ali,

4
Sono piacevolmente sorpreso che funzioni perfettamente su Windows usando git bash, eh!
Dai

3
@BurhanAli Per ogni commit nella cronologia, sta eliminando tutti i file tranne quelli che si desidera conservare. Quando tutto è fatto, ti rimane solo la parte dell'albero che hai specificato, insieme solo a quella cronologia.
void.pointer

39

Passaggi manuali con semplici comandi git

Il piano è quello di dividere le singole directory nei propri repository, quindi unirle insieme. I seguenti passaggi manuali non utilizzavano script geek da usare ma comandi di facile comprensione e potevano aiutare a unire N sottocartelle aggiuntive in un altro singolo repository.

Dividere

Supponiamo che il tuo repository originale sia: original_repo

1 - Dividi app:

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

2 - Dividi librerie

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

Continua se hai più di 2 cartelle. Ora avrai due repository git nuovi e temporanei.

Conquista unendo app e librerie

3 - Preparare il nuovissimo repository:

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

E dovrai fare almeno un commit. Se le seguenti tre righe devono essere ignorate, il primo repository apparirà immediatamente sotto la radice del repository:

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"

Dopo aver eseguito il commit del file temporaneo, il mergecomando nella sezione successiva verrà interrotto come previsto.

Prendendo il feedback degli utenti, invece di aggiungere un file casuale come a_file_and_make_a_commit, puoi scegliere di aggiungere un .gitignore, o README.mdecc.

4 - Unisci prima il repository di app:

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"

Ora dovresti vedere la directory delle app nel tuo nuovo repository. git logdovrebbe mostrare tutti i messaggi di commit storici rilevanti.

Nota: come Chris ha osservato di seguito nei commenti, per la versione più recente (> = 2.9) di git, è necessario specificare --allow-unrelated-historiescongit merge

5 - Unisci il repository di librerie libere successivamente allo stesso modo:

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"

Continua se hai più di 2 repository da unire.

Riferimento: unisci una sottodirectory di un altro repository con git


4
Da git 2.9 devi usare --allow-unrelated-histories sui comandi merge. Altrimenti, questo sembra funzionare bene per me.
Chris,

1
Genio! Grazie mille per questo. Le risposte iniziali che avevo esaminato, usando un filtro ad albero su un archivio molto grande, avevano previsto che Git prevedesse di prendere 26 ore per completare le riscritture di GIT. Molto più felice con questo approccio semplice ma ripetibile e ha spostato con successo 4 sottocartelle in un nuovo repository con tutta la cronologia di commit prevista.
chiuso il

1
È possibile utilizzare il primo commit per un "commit iniziale" che aggiunge .gitignoree README.mdfile.
Jack Miller,

2
Sfortunatamente questo approccio sembra spezzare la cronologia di tracciamento per i file aggiunti nel git merge .. git read-treepassaggio, in quanto li registra come file appena aggiunti e tutti i miei git guis non effettuano la connessione ai loro precedenti commit.
Dai

1
@ksadjad, Non ne ho idea, a dire il vero. Il punto centrale dell'unione manuale è selezionare le directory per formare il nuovo repository e conservare le loro cronologie di commit. Non sono sicuro di come gestire una situazione del genere in cui un commit inserisce i file in dirA, dirB, dirDrop e solo dirA e dirB vengono scelti per il nuovo repository, in che modo la cronologia del commit deve essere correlata a quella originale.
chfw

27

Perché dovresti voler correre filter-branchpiù di una volta? Puoi fare tutto in un colpo solo, quindi non è necessario forzarlo (nota che è necessario extglobabilitato nella tua shell perché funzioni):

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

Questo dovrebbe sbarazzarsi di tutti i cambiamenti nelle sottodirectory indesiderate e mantenere tutti i tuoi rami e commit (a meno che non influenzino solo i file nelle sottodirectory potate, in virtù di --prune-empty) - nessun problema con commit duplicati ecc.

Dopo questa operazione le directory indesiderate verranno elencate come non monitorate da git status.

Il $(ls ...)occorre v la extglobviene valutata dalla shell al posto del filtro indice, che utilizza shl'incorporato eval(dove extglobnon è disponibile). Vedi Come abilitare le opzioni di shell in git? per ulteriori dettagli al riguardo.


1
Idea interessante. Ho un problema simile, ma non sono riuscito a farlo funzionare, vedo stackoverflow.com/questions/8050687/...
Manol

Questo è praticamente ciò di cui avevo bisogno, anche se ho avuto una spolverata di file e cartelle nel mio repository ... Grazie :)
notlesh

1
hm. anche con extglob attivato sto ricevendo un errore vicino alla mia parentesi: errore di sintassi vicino token inaspettato `('il mio comando assomiglia a: git filter-branch -f --index-filter" git rm -r -f --cached - -ignore-unmatch src / css / themes /! (some_theme *) "--prune-empty - - all ans with src / css / themes /! (some_theme *) restituisce tutti gli altri temi così extglob sembra lavorare ...
robdodson,

2
@MikeGraf Non credo che darà il risultato desiderato: la fuga corrisponderebbe a un letterale "!" ecc. nel tuo percorso.
Kynan,

1
La risposta (più recente) di @ david-smiley utilizza un approccio molto simile, ma ha il vantaggio di affidarsi esclusivamente ai gitcomandi, e quindi non è sensibile alle differenze nel modo in cui lsviene interpretata tra i sistemi operativi, come ha scoperto @Bae.
Jeremy Caney,

20

Rispondere alla mia domanda qui ... dopo molte prove ed errori.

Sono riuscito a farlo usando una combinazione di git subtreee git-stitch-repo. Queste istruzioni si basano su:

Innanzitutto, ho estratto le directory che volevo tenere nel loro repository separato:

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

Ho quindi creato un nuovo repository vuoto e importato / cucito gli ultimi due in esso:

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

Questo crea due rami master-Ae master-B, ciascuno contenente il contenuto di uno dei repository cuciti. Per combinarli e ripulire:

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

Ora non sono del tutto sicuro di come / quando ciò accadrà, ma dopo il primo checkoute il pull, il codice si fonde magicamente nel ramo principale (qualsiasi comprensione di ciò che sta accadendo qui è apprezzata!)

Sembra che tutto abbia funzionato come previsto, tranne per il fatto che se guardo attraverso la newRepocronologia del commit, ci sono duplicati quando il changeset ha influenzato entrambi apps/AAAe libs/XXX. Se c'è un modo per rimuovere i duplicati, sarebbe perfetto.


Strumenti accurati che hai trovato qui. Approfondimento su "checkout": "git pull" è lo stesso di "git fetch && git merge". La parte "recupero" è innocua poiché si sta "recuperando localmente". Quindi penso che questo comando di checkout sia lo stesso di "git merge master-B", che è un po 'più evidente. Vedi kernel.org/pub/software/scm/git/docs/git-pull.html
phord

1
Purtroppo lo strumento git-stitch-repo è rotto a causa di cattive dipendenze al giorno d'oggi.
Henrik,

@Henrik Che problema hai riscontrato esattamente? Funziona per me, anche se ho dovuto aggiungere export PERL5LIB="$PERL5LIB:/usr/local/git/lib/perl5/site_perl/"alla mia configurazione bash in modo che potesse trovare Git.pm. Quindi l'ho installato con cpan.

È possibile utilizzare git subtree addper eseguire questa attività. Vedere stackoverflow.com/a/58253979/1894803
laconbass

7

Ho scritto un filtro git per risolvere esattamente questo problema. Ha il fantastico nome di git_filter e si trova su github qui:

https://github.com/slobobaby/git_filter

Si basa sull'eccellente libgit2.

Avevo bisogno di dividere un grande repository con molti commit (~ 100000) e le soluzioni basate su git filter-branch richiedevano diversi giorni per essere eseguite. git_filter impiega un minuto per fare la stessa cosa.


7

Usa l'estensione git 'git splits'

git splitsè uno script bash che è un wrapper git branch-filterche ho creato come estensione git, basato sulla soluzione di jkeating .

È stato creato esattamente per questa situazione. Per il tuo errore, prova a utilizzare l' git splits -fopzione per forzare la rimozione del backup. Poiché git splitsopera su un nuovo ramo, non riscriverà il ramo corrente, quindi il backup è estraneo. Vedi il readme per maggiori dettagli e assicurati di usarlo su una copia / clone del tuo repository (per ogni evenienza!) .

  1. installa git splits.
  2. Dividi le directory in un ramo 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 apps/AAA libs/ZZZ

  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


Non sembra possibile aggiungere file alla divisione e aggiornarli in seguito, giusto?
Alex,

Questo sembra rallentare nel mio repository con tonnellate di commit
Shinta Smith,

git-split sembra usare git --index filter che è estremamente lento rispetto a --subdirectory-filter. Per alcuni repository potrebbe essere ancora un'opzione praticabile, ma per repository di grandi dimensioni (più gigabyte, commit a 6 cifre) - il filtro indice richiede effettivamente settimane per l'esecuzione, anche su hardware cloud dedicato.
Jostein Kjønigsen,

6
git clone git@example.com: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 git@example.com:newthing.git
git push --all

Leggere tutti gli altri commenti mi ha portato sulla strada giusta. Tuttavia, la tua soluzione funziona. Importa tutti i rami e funziona con più directory! Grande!
jschober,

1
Il forciclo è la pena di riconoscere, dal momento che altre risposte simili non comprendono esso. Se non hai una copia locale di ciascun ramo nel tuo clone, filter-branchnon li terrà conto come parte della sua riscrittura, che potrebbe potenzialmente escludere i file introdotti in altri rami, ma non ancora uniti al tuo ramo attuale. (Anche se vale anche la pena fare un git fetchsu tutti i rami che hai controllato in precedenza per assicurarti che rimangano aggiornati.)
Jeremy Caney,

5

Una soluzione semplice: git-filter-repo

Ho avuto un problema simile e, dopo aver esaminato i vari approcci elencati qui, ho scoperto git-filter-repo . È raccomandato come alternativa a git-filter-branch nella documentazione ufficiale di git qui .

Per creare un nuovo repository da un sottoinsieme di directory in un repository esistente, è possibile utilizzare il comando:

git filter-repo --path <file_to_remove>

Filtra più file / cartelle concatenandoli:

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

Quindi, per rispondere alla domanda originale , con git-filter-repo avresti solo bisogno del seguente comando:

git filter-repo --path apps/AAA/ --path libs/XXX/

Questa è sicuramente un'ottima risposta. Il problema con tutte le altre soluzioni è che non sono riuscito a estrarre il contenuto di TUTTI i rami di una directory. Tuttavia, git filter-repo ha recuperato la cartella da tutti i rami e ha riscritto perfettamente la cronologia, come pulire l'intero albero di tutto ciò di cui non avevo bisogno.
Teodoro,

3

Si. Forza la sovrascrittura del backup utilizzando il -fflag nelle chiamate successive filter-branchper sovrascrivere tale avviso. :) Altrimenti penso che tu abbia la soluzione (cioè sradicare una directory indesiderata alla volta con filter-branch).


-4

Elimina il backup presente nella directory .git in refs / original come suggerisce il messaggio. La directory è nascosta.

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.