Odłącz wiele podkatalogów do nowego, oddzielnego repozytorium Git


135

To pytanie jest oparte na podkatalogu Odłącz do osobnego repozytorium Git

Zamiast odłączać pojedynczy podkatalog, chcę odłączyć kilka. Na przykład moje bieżące drzewo katalogów wygląda następująco:

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

Zamiast tego chciałbym:

/apps
  /AAA
/libs
  /XXX

--subdirectory-filterArgument git filter-branchnie będzie działać, ponieważ pozbywa się wszystkiego z wyjątkiem podanym katalogu po raz pierwszy jest prowadzony. Myślałem, że użycie --index-filterargumentu dla wszystkich niechcianych plików zadziała (choćby nużące), ale jeśli spróbuję go uruchomić więcej niż raz, otrzymuję następujący komunikat:

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

Jakieś pomysły? TIA

Odpowiedzi:


155

Zamiast zajmować się podpowłoką i używać ext glob (jak zasugerował kynan), wypróbuj to znacznie prostsze podejście:

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

Jak wspomniał void.pointer w swoim komentarzu , spowoduje to usunięcie wszystkiego oprócz apps/AAAiz libs/XXXbieżącego repozytorium.

Usuń puste zatwierdzenia scalania

To pozostawia wiele pustych połączeń. Można je usunąć inną przepustką, jak to opisuje raphinesse w swojej odpowiedzi :

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

⚠️ Uwaga : Powyższe stosowanie musi Wersja GNU sedi xargsinaczej byłoby usunąć wszystkie rewizje jak xargssię nie powiedzie. brew install gnu-sed findutilsa następnie użyj gsedi gxargs:

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

4
dodatkowo flaga --ignore-unmatch powinna zostać przekazana do git rm, w przeciwnym razie nie powiodło się przy pierwszym zatwierdzeniu dla mnie (repozytorium zostało utworzone za pomocą klonu git svn w moim przypadku)
Pontomedon

8
Zakładając, że masz tagi w miksie, prawdopodobnie powinieneś dodać --tag-name-filter catdo swoich parametrów
Yonatan

16
Czy mógłbyś dodać więcej informacji wyjaśniających, co robi to długie polecenie?
Burhan Ali

4
Jestem mile zaskoczony, że działa to doskonale w systemie Windows przy użyciu git bash, uff!
Dai,

3
@BurhanAli Każde zatwierdzenie w historii powoduje usunięcie wszystkich plików oprócz tych, które chcesz zachować. Kiedy wszystko jest zrobione, pozostaje tylko część drzewa, którą określiłeś, wraz z tylko tą historią.
void.pointer

39

Ręczne kroki za pomocą prostych poleceń git

Plan polega na podzieleniu poszczególnych katalogów na własne repozytoria, a następnie scaleniu ich razem. Poniższe ręczne kroki nie wykorzystywały łatwych w użyciu skryptów, ale łatwe do zrozumienia polecenia i mogą pomóc w scaleniu dodatkowych N podfolderów w inne pojedyncze repozytorium.

Podzielić

Załóżmy, że Twoje oryginalne repozytorium to: original_repo

1 - Podziel aplikacje:

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

2 - Podziel biblioteki

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

Kontynuuj, jeśli masz więcej niż 2 foldery. Teraz będziesz mieć dwa nowe i tymczasowe repozytorium git.

Pokonaj , łącząc aplikacje i biblioteki

3 - Przygotuj nowe repozytorium:

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

Będziesz musiał wykonać co najmniej jedno zatwierdzenie. Jeśli poniższe trzy wiersze powinny zostać pominięte, pierwsze repozytorium pojawi się natychmiast w katalogu głównym repozytorium:

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"

Po zatwierdzeniu pliku tymczasowego mergepolecenie w późniejszej sekcji zostanie zatrzymane zgodnie z oczekiwaniami.

Biorąc pod uwagę opinie użytkowników, zamiast dodawać losowy plik, taki jak a_file_and_make_a_commit, możesz dodać .gitignore, lub README.mditp.

4 - Najpierw scal repozytorium aplikacji:

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"

Teraz powinieneś zobaczyć katalog aplikacji w nowym repozytorium. git logpowinien pokazywać wszystkie istotne historyczne komunikaty o zatwierdzeniach.

Uwaga: jak Chris zauważył poniżej w komentarzach, dla nowszej wersji (> = 2.9) git, musisz określić za --allow-unrelated-historiespomocągit merge

5 - Następnie scal repozytorium bibliotek w ten sam sposób:

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"

Kontynuuj, jeśli masz więcej niż 2 repozytoria do scalenia.

Odniesienie: Scal podkatalog innego repozytorium z git


4
Od wersji 2.9 git musisz używać --allow-unrelated-histories w poleceniach merge. W przeciwnym razie wydaje mi się, że działa to dobrze.
Chris,

1
Geniusz! Dziękuję bardzo za to. Początkowe odpowiedzi, na które patrzyłem, używając filtru drzewa w bardzo dużym repozytorium, przewidywały, że git przewidywał, że zajmie ponad 26 godzin, aby zakończyć przepisywanie gita. O wiele szczęśliwsi dzięki temu prostemu, ale powtarzalnemu podejściu i pomyślnie przenieśliśmy 4 podfoldery do nowego repozytorium z całą oczekiwaną historią zatwierdzania.
shuttsy

1
Możesz użyć pierwszego zatwierdzenia dla „Wstępnego zatwierdzenia”, które dodaje .gitignorei README.mdpliki.
Jack Miller,

2
Niestety takie podejście wydaje się przerywać historię śledzenia plików dodanych w git merge .. git read-treekroku, ponieważ rejestruje je jako nowo dodane pliki, a wszystkie moje git guis nie łączą się z ich wcześniejszymi zatwierdzeniami.
Dai

1
@ksadjad, nie mam pojęcia, szczerze mówiąc. Centralnym punktem ręcznego scalania jest wybranie katalogów w celu utworzenia nowego repozytorium i zachowanie ich historii zatwierdzeń. Nie jestem pewien, jak sobie poradzić z taką sytuacją, gdy commita umieszcza pliki w dirA, dirB, dirDrop i tylko dirA i dirB są wybierane do nowego repozytorium, jak historia zmian powinna odnosić się do oryginalnego.
chfw

27

Dlaczego miałbyś chcieć biegać filter-branchwięcej niż raz? Możesz to wszystko zrobić za jednym zamachem, więc nie musisz tego wymuszać (pamiętaj, że musisz extglobwłączyć w powłoce, aby to zadziałało):

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

Powinno to pozbyć się wszystkich zmian w niechcianych podkatalogach i zachować wszystkie twoje gałęzie i zatwierdzenia (chyba że wpływają one tylko na pliki w przyciętych podkatalogach --prune-empty) - nie ma problemu z powielonymi zatwierdzeniami itp.

Po tej operacji niechciane katalogi zostaną wymienione jako nieśledzone przez git status.

To $(ls ...)jest konieczne, extglobjest oceniane przez twoją powłokę zamiast przez filtr indeksu, który używa shwbudowanego eval(gdzie extglobnie jest dostępny). Zobacz Jak włączyć opcje powłoki w git? po dalsze szczegóły na ten temat.


1
Ciekawy pomysł. Mam podobny problem, ale nie mogłem go uruchomić, patrz stackoverflow.com/questions/8050687/ ...
manol

To jest prawie to, czego potrzebowałem, chociaż
posypałem

1
hm. nawet przy włączonym extglob pojawia się błąd w pobliżu mojego nawiasu: błąd składni w pobliżu nieoczekiwanego tokenu `('moje polecenie wygląda tak: git filter-branch -f --index-filter" git rm -r -f --cached - -ignore-unmatch src / css / themes /! (some_theme *) "--prune-empty - --all an ls with src / css / themes /! (some_theme *) zwraca wszystkie inne motywy, więc extglob wydaje się pracować ...
robdodson

2
@MikeGraf Nie sądzę, aby to przyniosło pożądany rezultat: ucieczka byłaby dopasowana do dosłownego „!” itp. na twojej drodze.
kynan

1
Odpowiedź @david-smiley (nowsza) używa bardzo podobnego podejścia, ale ma tę zaletę, że polega wyłącznie na gitpoleceniach, a zatem nie jest tak podatna na różnice w sposobie lsinterpretowania znaków w różnych systemach operacyjnych, jak odkrył @Bae.
Jeremy Caney

20

Odpowiadając tutaj na moje własne pytanie ... po wielu próbach i błędach.

Udało mi się to zrobić za pomocą kombinacji git subtreei git-stitch-repo. Te instrukcje są oparte na:

Najpierw wyciągnąłem katalogi, które chciałem zachować, w ich oddzielnym repozytorium:

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

Następnie utworzyłem nowe, puste repozytorium i zaimportowałem / zszyłem do niego ostatnie dwa:

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

Stwarza to dwa oddziały, master-Aa master-Bkażdy trzyma zawartość jednej ze zszytych repo. Aby je połączyć i posprzątać:

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

Teraz nie jestem do końca pewien, jak / kiedy to się dzieje, ale po pierwszej checkouti drugiej pull, kod magicznie łączy się z gałęzią master (każdy wgląd w to, co się tutaj dzieje, jest mile widziany!)

Wygląda na to, że wszystko działało zgodnie z oczekiwaniami, z wyjątkiem tego, że jeśli newRepoprzejrzę historię zmian, istnieją duplikaty, gdy zestaw zmian wpłynął na oba apps/AAAi libs/XXX. Jeśli istnieje sposób na usunięcie duplikatów, byłby idealny.


Zgrabne narzędzia, które znalazłeś tutaj. Wgląd w „checkout”: „git pull” to to samo, co „git fetch && git merge”. Część „pobieranie” jest nieszkodliwa, ponieważ „pobierasz lokalnie”. Więc myślę, że to polecenie checkout jest tym samym, co „git merge master-B”, co jest nieco bardziej oczywiste. Zobacz kernel.org/pub/software/scm/git/docs/git-pull.html
phord

1
Niestety narzędzie git-stitch-repo jest obecnie zepsute z powodu złych zależności.
Henrik

@Henrik Jaki dokładnie problem miałeś / aś? U mnie działa, chociaż musiałem dodać export PERL5LIB="$PERL5LIB:/usr/local/git/lib/perl5/site_perl/"do mojej konfiguracji basha, aby mógł znaleźć Git.pm. Następnie zainstalowałem go z cpan.

Można go użyć git subtree adddo wykonania tego zadania. Zobacz stackoverflow.com/a/58253979/1894803
laconbass

7

Napisałem filtr git, aby rozwiązać dokładnie ten problem. Ma fantastyczną nazwę git_filter i znajduje się na github tutaj:

https://github.com/slobobaby/git_filter

Opiera się na doskonałej bibliotece libgit2.

Musiałem podzielić duże repozytorium z wieloma zatwierdzeniami (~ 100000), a rozwiązania oparte na git filter-branch zajęły kilka dni. git_filter zajmuje minutę, aby zrobić to samo.


7

Użyj rozszerzenia git „git splits”

git splitsto skrypt bash będący opakowaniem, git branch-filterktóry stworzyłem jako rozszerzenie git, w oparciu o rozwiązanie jkeating .

Został stworzony dokładnie do tej sytuacji. W przypadku błędu spróbuj użyć git splits -fopcji wymuszenia usunięcia kopii zapasowej. Ponieważ git splitsdziała na nowej gałęzi, nie przepisuje bieżącej gałęzi, więc kopia zapasowa jest zbędna. Zobacz plik readme, aby uzyskać więcej szczegółów i pamiętaj, aby użyć go na kopii / klonie repozytorium (na wszelki wypadek!) .

  1. zainstalować git splits.
  2. Podziel katalogi na lokalny oddział #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. Utwórz gdzieś puste repozytorium. Zakładamy, że utworzyliśmy puste repozytorium wywołane xyzna GitHub, które ma ścieżkę:git@github.com:simpliwp/xyz.git

  4. Przejdź do nowego repozytorium. #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. Sklonuj nowo utworzone repozytorium zdalne do nowego katalogu lokalnego
    #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


Wydaje się, że nie można dodać plików do podziału i zaktualizować je później, prawda?
Alex

Wydaje się, że działa wolno w moim repozytorium z mnóstwem zatwierdzeń
Shinta Smith

git-split wydaje się używać filtru git --index, który jest niezwykle powolny w porównaniu z --subdirectory-filter. W przypadku niektórych repozytoriów może nadal być realną opcją, ale w przypadku dużych repozytoriów (wiele gigabajtów, 6-cyfrowe zatwierdzenia) - index-filter skutecznie działa tygodniami, nawet na dedykowanym sprzęcie w chmurze.
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

Przeczytanie wszystkich innych komentarzy doprowadziło mnie do właściwej ścieżki. Jednak Twoje rozwiązanie po prostu działa. Importuje wszystkie gałęzie i działa z wieloma katalogami! Wspaniały!
jschober

1
forPętla jest warta uznania, gdyż inne podobne odpowiedzi nie zawierają go. Jeśli nie masz lokalnej kopii każdej gałęzi w swoim klonie, filter-branchnie uwzględnisz ich w ramach przepisywania, co może potencjalnie wykluczyć pliki wprowadzone w innych gałęziach, ale jeszcze nie połączone z bieżącą gałęzią. (Choć warto też robi git fetchna wszystkich oddziałach zostały wcześniej sprawdzone w celu zapewnienia, że pozostają one aktualne.)
Jeremy Caney

5

Proste rozwiązanie: git-filter-repo

Miałem podobny problem i po przejrzeniu różnych wymienionych tutaj podejść odkryłem git-filter-repo . Jest zalecany jako alternatywa dla git-filter-branch w oficjalnej dokumentacji git tutaj .

Aby utworzyć nowe repozytorium z podzbioru katalogów w istniejącym repozytorium, możesz użyć polecenia:

git filter-repo --path <file_to_remove>

Filtruj wiele plików / folderów, łącząc je:

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

Tak więc, aby odpowiedzieć na pierwotne pytanie , z git-filter-repo potrzebujesz tylko następującego polecenia:

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

To zdecydowanie świetna odpowiedź. Problem ze wszystkimi innymi rozwiązaniami polega na tym, że nie udało mi się wyodrębnić zawartości WSZYSTKICH gałęzi katalogu. Jednak git filter-repo pobrał folder ze wszystkich gałęzi i doskonale przepisał historię, na przykład wyczyścił całe drzewo ze wszystkiego, czego nie potrzebowałem.
Teodoro

3

Tak. Wymuś zastąpienie kopii zapasowej, używając -fflagi przy kolejnych wywołaniach do, filter-branchaby zastąpić to ostrzeżenie. :) W przeciwnym razie myślę, że masz rozwiązanie (to znaczy usunąć niechciany katalog na raz za pomocą filter-branch).


-4

Usuń kopię zapasową obecną w katalogu .git w refs / original, jak sugeruje wiadomość. Katalog jest ukryty.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.