Odłącz (przenieś) podkatalog do osobnego repozytorium Git

1758

Mam repozytorium Git, które zawiera wiele podkatalogów. Teraz odkryłem, że jeden z podkatalogów nie jest powiązany z drugim i powinien zostać odłączony do osobnego repozytorium.

Jak mogę to zrobić, zachowując historię plików w podkatalogu?

Myślę, że mógłbym utworzyć klon i usunąć niechciane części każdego klonu, ale przypuszczam, że dałoby to pełne drzewo podczas sprawdzania starszej wersji itp. Może to być dopuszczalne, ale wolałbym móc udawać, że dwa repozytoria nie mają wspólnej historii.

Żeby było jasne, mam następującą strukturę:

XYZ/
    .git/
    XY1/
    ABC/
    XY2/

Ale chciałbym to zamiast tego:

XYZ/
    .git/
    XY1/
    XY2/
ABC/
    .git/
    ABC/
matli
źródło
7
To jest teraz trywialne - git filter-branchpatrz moja odpowiedź poniżej.
jeremyjjbrown,
8
@jeremyjjbrown ma rację. Nie jest to już trudne, ale trudno znaleźć właściwą odpowiedź w Google, ponieważ wszystkie stare odpowiedzi dominują w wynikach.
Agnel Kurian

Odpowiedzi:

1228

Aktualizacja : Ten proces jest tak powszechne, że zespół git stało się znacznie prostsze z nowym narzędziu git subtree. Zobacz tutaj: Odłącz (przenieś) podkatalog do osobnego repozytorium Git


Chcesz sklonować swoje repozytorium, a następnie użyć git filter-branchdo zaznaczenia wszystkiego oprócz podkatalogu, który chcesz, aby nowe repozytorium zostało zużyte.

  1. Aby sklonować lokalne repozytorium:

    git clone /XYZ /ABC
    

    (Uwaga: repozytorium zostanie sklonowane za pomocą dowiązań twardych, ale nie stanowi to problemu, ponieważ dowiązane pliki nie będą same w sobie modyfikowane - zostaną utworzone nowe).

  2. Teraz zachowajmy interesujące gałęzie, które również chcemy przepisać, a następnie usuń początek, aby uniknąć pchania się tam i aby upewnić się, że stare zatwierdzenia nie będą odwoływać się do źródła:

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

    lub dla wszystkich zdalnych oddziałów:

    cd /ABC
    for i in $(git branch -r | sed "s/.*origin\///"); do git branch -t $i origin/$i; done
    git remote rm origin
    
  3. Teraz możesz również chcieć usunąć tagi, które nie mają związku z podprojektem; możesz to zrobić później, ale może być konieczne ponowne przycięcie repozytorium. Nie zrobiłem tego i otrzymałem WARNING: Ref 'refs/tags/v0.1' is unchangedtag dla wszystkich (ponieważ wszystkie nie były powiązane z podprojektem); dodatkowo po usunięciu takich tagów zostanie odzyskanych więcej miejsca. Najwyraźniej git filter-branchpowinien być w stanie przepisać inne tagi, ale nie mogłem tego zweryfikować. Jeśli chcesz usunąć wszystkie tagi, użyj git tag -l | xargs git tag -d.

  4. Następnie użyj gałęzi filter i zresetuj, aby wykluczyć inne pliki, aby mogły zostać przycięte. Dodajmy również, --tag-name-filter cat --prune-emptyaby usunąć puste zatwierdzenia i przepisać tagi (pamiętaj, że będzie to musiało usunąć ich podpis):

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

    lub alternatywnie, aby przepisać tylko gałąź HEAD i zignorować tagi i inne gałęzie:

    git filter-branch --tag-name-filter cat --prune-empty --subdirectory-filter ABC HEAD
    
  5. Następnie usuń dzienniki kopii zapasowych, aby przestrzeń mogła zostać naprawdę odzyskana (chociaż teraz operacja jest destrukcyjna)

    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
    

    a teraz masz lokalne repozytorium git podkatalogu ABC z zachowaną całą jego historią.

Uwaga: W przypadku większości zastosowań git filter-branchnależy rzeczywiście dodać parametr -- --all. Tak, to naprawdę --space-- all. To muszą być ostatnie parametry polecenia. Jak odkrył Matli, zachowuje gałęzie projektu i tagi zawarte w nowym repozytorium.

Edycja: uwzględniono różne sugestie z poniższych komentarzy, aby na przykład upewnić się, że repozytorium faktycznie się zmniejszyło (co nie zawsze tak było wcześniej).

Paul
źródło
29
Bardzo dobra odpowiedź. Dzięki! Aby naprawdę uzyskać dokładnie to, czego chciałem, dodałem „- --all” do polecenia filter-branch.
matli,
12
Dlaczego musisz --no-hardlinks? Usunięcie jednego twardego linku nie wpłynie na drugi plik. Obiekty Git również są niezmienne. Tylko jeśli zmienisz uprawnienia właściciela / pliku, których potrzebujesz --no-hardlinks.
vdboor
67
Dodatkowym krokiem, który zaleciłbym, byłoby „git remote rm origin”. Pozwoliłoby to powstrzymać wypychania przed powrotem do oryginalnego repozytorium, jeśli się nie mylę.
Tom
13
Kolejnym poleceniem do dołączenia filter-branchjest --prune-emptyusunięcie teraz pustych zatwierdzeń.
Seth Johnson
8
Podobnie jak Paul, nie chciałem tagów projektu w moim nowym repozytorium, więc nie używałem -- --all. Ja również prowadził git remote rm origin, a git tag -l | xargs git tag -dprzed git filter-branchpoleceniem. To zmniejszyło mój .gitkatalog z 60M do ~ 300K. Zauważ, że musiałem uruchomić oba te polecenia, aby uzyskać zmniejszenie rozmiaru.
saltycrane
1321

Easy Way ™

Okazuje się, że jest to tak powszechna i przydatna praktyka, że ​​zwierzchnicy Gita sprawili, że było to naprawdę łatwe, ale musisz mieć nowszą wersję Git (> = 1.7.11 maja 2012 r.). Zobacz dodatek, aby dowiedzieć się, jak zainstalować najnowszą wersję Git. Ponadto, nie jest przykładem w świecie rzeczywistym w poradniku poniżej.

  1. Przygotuj stare repozytorium

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

    Uwaga: <name-of-folder> NIE może zawierać wiodących ani końcowych znaków. Na przykład folder o nazwie subprojectMUSI zostać przekazany jako subprojectNIE./subproject/

    Uwaga dla użytkowników systemu Windows: gdy głębokość folderu wynosi> 1, <name-of-folder>musi mieć separator folderów w stylu * nix (/). Na przykład folder o nazwie path1\path2\subprojectMUSI zostać przekazany jakopath1/path2/subproject

  2. Utwórz nowe repozytorium

    mkdir ~/<new-repo> && cd ~/<new-repo>
    git init
    git pull </path/to/big-repo> <name-of-new-branch>
    
  3. Połącz nowe repozytorium z GitHub lub gdziekolwiek

    git remote add origin <[email protected]:user/new-repo.git>
    git push -u origin master
    
  4. Oczyszczanie w środku <big-repo>, jeśli jest to pożądane

    git rm -rf <name-of-folder>
    

    Uwaga : pozostawia to wszystkie historyczne odniesienia w repozytorium.Zobacz dodatek poniżej, jeśli naprawdę obawiasz się, że podałeś hasło lub chcesz zmniejszyć rozmiar pliku w .gitfolderze.

...

Przewodnik

Są to te same kroki, co powyżej , ale wykonuję moje dokładne kroki dla mojego repozytorium zamiast używać <meta-named-things>.

Oto projekt, który mam zaimplementować moduły przeglądarki JavaScript w węźle:

tree ~/node-browser-compat

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

Chcę podzielić pojedynczy folder btoana osobne repozytorium Git

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

Mam teraz nowy oddział, btoa-onlyktóry tylko zatwierdza btoai chcę utworzyć nowe repozytorium.

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

Następnie tworzę nowe repozytorium na GitHub lub Bitbucket lub cokolwiek innego i dodam jako origin

git remote add origin [email protected]:node-browser-compat/btoa.git
git push -u origin master

Szczęśliwy dzień!

Uwaga: jeśli utworzyłeś repozytorium za pomocą README.md, .gitignorei LICENSE, musisz najpierw pobrać:

git pull origin master
git push origin master

Na koniec chcę usunąć folder z większego repozytorium

git rm -rf btoa

...

dodatek

Najnowsza wersja Git na macOS

Aby uzyskać najnowszą wersję Git za pomocą Homebrew :

brew install git

Najnowsza wersja Git na Ubuntu

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

Jeśli to nie zadziała (masz bardzo starą wersję Ubuntu), spróbuj

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

Jeśli to nadal nie działa, spróbuj

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

Dzięki rui.araujo z komentarzy.

Wyczyść swoją historię

Domyślnie usuwanie plików z Gita tak naprawdę ich nie usuwa, po prostu potwierdza, że ​​już ich tam nie ma. Jeśli chcesz faktycznie usunąć odniesienia historyczne (tj. Masz zatwierdzone hasło), musisz to zrobić:

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

Następnie możesz sprawdzić, czy Twój plik lub folder w ogóle nie pojawia się w historii Git

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

Nie można jednak „usunąć” operacji usuwania do GitHub i tym podobnych. Jeśli spróbujesz, pojawi się błąd i będziesz musiał to zrobić, git pullzanim będziesz mógł git push- a następnie wrócisz do posiadania wszystkiego w swojej historii.

Więc jeśli chcesz usunąć historię z „pochodzenia” - co oznacza, że ​​chcesz usunąć ją z GitHub, Bitbucket itp. - musisz usunąć repozytorium i ponownie wcisnąć przyciętą kopię repozytorium. Ale czekaj - jest więcej ! - Jeśli naprawdę martwisz się usunięciem hasła lub czegoś takiego, musisz przyciąć kopię zapasową (patrz poniżej).

Dokonywanie .gitmniejszy

Wyżej wspomniane polecenie usuwania historii wciąż pozostawia kilka plików kopii zapasowych - ponieważ Git jest zbyt uprzejmy, pomagając ci nie zepsuć repozytorium przez przypadek. W końcu usunie osierocone pliki w ciągu dni i miesięcy, ale pozostawia je tam na chwilę, na wypadek, gdybyś zdał sobie sprawę, że przypadkowo usunąłeś coś, czego nie chciałeś.

Jeśli więc naprawdę chcesz opróżnić kosz, aby natychmiast zmniejszyć rozmiar klonu repozytorium, musisz wykonać wszystkie te naprawdę dziwne rzeczy:

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

To powiedziawszy, nie zalecałbym wykonywania tych kroków, chyba że wiesz, że musisz - na wypadek, gdybyś wyciął niewłaściwy podkatalog, wiesz? Pliki kopii zapasowej nie powinny zostać sklonowane po naciśnięciu repozytorium, będą one tylko w lokalnej kopii.

Kredyt

CoolAJ86
źródło
16
git subtreejest nadal częścią folderu „contrib” i nie jest domyślnie instalowany na wszystkich dystrybucjach. 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 Aby aktywować na Ubuntu 13.04
rui.araujo
41
Jeśli przekazałeś hasło do publicznego repozytorium, powinieneś zmienić hasło, nie próbuj usuwać go z publicznego repozytorium i mam nadzieję, że nikt go nie widział.
Miles Rout
8
To rozwiązanie nie zachowuje historii.
Cœur
18
Polecenia popdi pushdsprawiają, że jest to raczej niejawne i trudniejsze do zrozumienia, co zamierza zrobić ...
jones77
133

Odpowiedź Paula tworzy nowe repozytorium zawierające / ABC, ale nie usuwa / ABC z / XYZ. Następujące polecenie usunie / ABC z / XYZ:

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

Oczywiście najpierw przetestuj go w repozytorium „klon-no-hardlinks” i postępuj zgodnie z poleceniami resetowania, gc i przycinania, które znajdują się na listach Paula.

pgs
źródło
53
zrób to git filter-branch --index-filter "git rm -r -f --cached --ignore-unmatch ABC" --prune-empty HEADi będzie znacznie szybciej. filtr indeksu działa na indeksie, podczas gdy filtr drzewa musi sprawdzać i ustawiać wszystko dla każdego zatwierdzenia .
fmarc,
51
w niektórych przypadkach zepsucie historii repozytorium XYZ to przesada ... po prostu proste „rm -rf ABC; git rm -r ABC; git commit -m'ekstrakowano ABC do własnego repozytorium” ”działałoby lepiej dla większości ludzi.
Evgeny
2
Prawdopodobnie zechcesz użyć opcji -f (force) w tym poleceniu, jeśli zrobisz to więcej niż jeden raz, np. W celu usunięcia dwóch katalogów po ich rozdzieleniu. W przeciwnym razie pojawi się komunikat „Nie można utworzyć nowej kopii zapasowej”.
Brian Carlton,
4
Jeśli korzystasz z tej --index-filtermetody, możesz to zrobić git rm -q -r -f, aby każde wywołanie nie drukowało wiersza dla każdego usuwanego pliku.
Eric Naeseth,
1
Sugerowałbym zredagowanie odpowiedzi Paula, tylko dlatego, że jest on tak dokładny.
Erik Aronesty
96

Przekonałem się, że aby poprawnie usunąć starą historię z nowego repozytorium, musisz wykonać trochę więcej pracy po filter-branchkroku.

  1. Wykonaj klon i filtr:

    git clone --no-hardlinks foo bar; cd bar
    git filter-branch --subdirectory-filter subdir/you/want
    
  2. Usuń każde odniesienie do starej historii. „Origin” śledził twój klon, a „oryginał” to miejsce, w którym gałąź filtra zapisuje stare rzeczy:

    git remote rm origin
    git update-ref -d refs/original/refs/heads/master
    git reflog expire --expire=now --all
    
  3. Nawet teraz twoja historia może utknąć w pliku pakietu, którego fsck nie dotknie. Rozerwij go na strzępy, tworząc nowy plik pakietu i usuwając nieużywane obiekty:

    git repack -ad
    

Istnieje wyjaśnienie tego w instrukcji dla branży filtracyjnej .

Josh Lee
źródło
3
Wydaje mi się, że czegoś takiego git gc --aggressive --prune=nowwciąż brakuje, prawda?
Albert
1
@Albert Polecenie przepakowania zajmuje się tym i nie będzie żadnych luźnych obiektów.
Josh Lee,
tak, git gc --aggressive --prune=nowzmniejszyłem wiele nowych repo
Tomek Wyderka
Prosty i elegancki. Dzięki!
Marco Pelegrini,
40

Edycja: dodano skrypt Bash.

Odpowiedzi podane tutaj działały dla mnie tylko częściowo; Wiele dużych plików pozostało w pamięci podręcznej. Co w końcu zadziałało (po godzinach w #git na 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

W przypadku poprzednich rozwiązań rozmiar repozytorium wynosił około 100 MB. Ten obniżył go do 1,7 MB. Może to komuś pomaga :)


Poniższy skrypt bash automatyzuje zadanie:

!/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
Simon A. Eugster
źródło
26

To nie jest już tak skomplikowane, że możesz po prostu użyć polecenia git filter-branch na klonie repozytorium, aby wyrzucić podkatalogi, których nie chcesz, a następnie przełączyć na nowy pilot.

git filter-branch --prune-empty --subdirectory-filter <YOUR_SUBDIR_TO_KEEP> master
git push <MY_NEW_REMOTE_URL> -f .
jeremyjjbrown
źródło
3
To działało jak urok. YOUR_SUBDIR w powyższym przykładzie jest podkatalogiem, który chcesz PRZECHOWYWAĆ, wszystko inne zostanie usunięte
JT Taylor
1
Aktualizacje na podstawie Twojego komentarza.
jeremyjjbrown
2
To nie odpowiada na pytanie. Z dokumentów jest napisane The result will contain that directory (and only that) as its project root.i rzeczywiście to otrzymasz, tj. Oryginalna struktura projektu nie zostanie zachowana.
NicBright,
2
@NicBright Czy możesz zilustrować swój problem z XYZ i ABC jak w pytaniu, aby pokazać, co jest nie tak?
Adam
@jeremyjjbrown czy można ponownie użyć sklonowanego repozytorium i nie używać nowej repozytorium, tj. moje pytanie tutaj stackoverflow.com/questions/49269602/...
Qiulang
19

Aktualizacja : moduł git-poddrzewo był tak przydatny, że zespół git wciągnął go do rdzenia i stworzył git subtree. Zobacz tutaj: Odłącz (przenieś) podkatalog do osobnego repozytorium Git

git-subtree może być do tego użyteczny

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

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

DW
źródło
1
git-subtree jest teraz częścią Git, chociaż jest w drzewie contrib, więc nie zawsze jest instalowane domyślnie. Wiem, że jest instalowany według formuły git Homebrew, ale bez strony podręcznika. apenwarr określa swoją wersję jako przestarzałą.
echristopherson
19

Oto mała modyfikacja CoolAJ86 „s «Easy Way ™»odpowiedź aby rozdzielić wiele podfoldery (powiedzmy sub1a sub2) do nowego repozytorium git.

The Easy Way ™ (wiele podfolderów)

  1. Przygotuj stare repozytorium

    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
    

    Uwaga: <name-of-folder> NIE może zawierać wiodących ani końcowych znaków. Na przykład folder o nazwie subprojectMUSI zostać przekazany jako subprojectNIE./subproject/

    Uwaga dla użytkowników systemu Windows: gdy głębokość folderu wynosi> 1, <name-of-folder>musi mieć separator folderów w stylu * nix (/). Na przykład folder o nazwie path1\path2\subprojectMUSI zostać przekazany jako path1/path2/subproject. Ponadto nie używaj mvpolecenia, ale move.

    Ostatnia uwaga: wyjątkowa i duża różnica w stosunku do odpowiedzi podstawowej to druga linia skryptu „ git filter-branch...

  2. Utwórz nowe repozytorium

    mkdir <new-repo>
    pushd <new-repo>
    
    git init
    git pull </path/to/big-repo> <name-of-new-branch>
    
  3. Połącz nowe repozytorium z Githubem lub gdziekolwiek

    git remote add origin <[email protected]:my-user/new-repo.git>
    git push origin -u master
    
  4. Oczyszczanie, w razie potrzeby

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

    Uwaga : pozostawia to wszystkie historyczne odniesienia w repozytorium. Zobacz załącznik w oryginalnej odpowiedzi, jeśli naprawdę obawiasz się, że podałeś hasło lub chcesz zmniejszyć rozmiar pliku w .gitfolderze.

Anthony O.
źródło
1
To zadziałało dla mnie z niewielką modyfikacją. Ponieważ moi sub1and sub2foldery nie istnieją z pierwotnej wersji, musiałem zmodyfikować mój --tree-filterskrypt w następujący sposób: "mkdir <name-of-folder>; if [ -d sub1 ]; then mv <sub1> <name-of-folder>/; fi". W drugim filter-branchpoleceniu zastąpiłem <sub1> <sub2>, pominąłem tworzenie <nazwa-folderu> i dodałem -fpo, filter-branchaby zastąpić ostrzeżenie o istniejącej kopii zapasowej.
pglezen
To nie działa, jeśli którykolwiek z podkatalogów zmienił się podczas historii w git. Jak można to rozwiązać?
nietras
@nietras zobacz odpowiedź Rogerdpack. Zajęło mi trochę czasu, aby go znaleźć po przeczytaniu i przyswojeniu wszystkich informacji zawartych w tych innych odpowiedziach.
Adam
12

Pierwotne pytanie chce, aby pliki XYZ / ABC / (*) stały się plikami ABC / ABC / (*). Po zaimplementowaniu zaakceptowanej odpowiedzi na mój własny kod zauważyłem, że faktycznie zmienia on pliki XYZ / ABC / (*) na pliki ABC / (*). Strona man gałęzi filter mówi nawet:

Wynik będzie zawierał ten katalog (i tylko ten) jako katalog główny projektu . ”

Innymi słowy, promuje folder najwyższego poziomu „w górę” o jeden poziom. To ważne rozróżnienie, ponieważ na przykład w mojej historii zmieniłem nazwę folderu najwyższego poziomu. Promując foldery „wyżej” o jeden poziom, git traci ciągłość przy zatwierdzeniu, w którym zmieniłem nazwę.

Straciłem ciągłość po odgałęzieniu filtra

Moja odpowiedź na to pytanie polega zatem na utworzeniu 2 kopii repozytorium i ręcznym usunięciu folderów, które chcesz w nich przechowywać. Strona podręcznika wspiera mnie następującymi informacjami:

[...] unikaj używania [tej komendy], jeśli wystarczy pojedyncze zatwierdzenie, aby rozwiązać problem

MM.
źródło
1
Podoba mi się styl tego wykresu. Czy mogę zapytać, jakiego narzędzia używasz?
Slipp D. Thompson
3
Tower dla komputerów Mac. Naprawdę to lubie. Już prawie warto przejść na komputer Mac.
MM.
2
Tak, choć w moim przypadku nazwa mojego podfoldera targetdirzostała w pewnym momencie zmieniona i git filter-branchpo prostu nazwała go dniem, usuwając wszystkie zatwierdzenia dokonane przed zmianą nazwy! Szokujące, biorąc pod uwagę, jak biegły Git śledzi takie rzeczy, a nawet migruje poszczególne fragmenty treści!
Jay Allen
1
Och, jeśli ktoś znajdzie się na tej samej łodzi, oto polecenie, którego użyłem. Nie zapominaj, że git rmwymaga wielu argumentów, więc nie ma powodu, aby uruchamiać go dla każdego pliku / folderu: 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

Aby dodać do odpowiedzi Pawła , odkryłem, że aby ostatecznie odzyskać miejsce, muszę pchnąć HEAD do czystego repozytorium, a to zmniejsza rozmiar katalogu .git / objects / pack.

to znaczy

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

Po przycięciu gc wykonaj również:

$ git push ... ABC.git HEAD

To możesz zrobić

$ git clone ... ABC.git

i rozmiar ABC / .git jest zmniejszony

W rzeczywistości niektóre czasochłonne kroki (np. Git gc) nie są potrzebne w przypadku repozytorium wypychania do czyszczenia, tj .:

$ git clone --no-hardlinks / XYZ / ABC
$ git filter-branch - subdirectory-filter ABC HEAD
$ git reset --hard
$ git push ... ABC.git HEAD
Case Larsen
źródło
6

Właściwy sposób jest teraz następujący:

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

GitHub ma teraz nawet mały artykuł na temat takich przypadków.

Ale pamiętaj, aby najpierw sklonować oryginalne repozytorium do osobnego katalogu (ponieważ spowoduje to usunięcie wszystkich plików i innych katalogów i prawdopodobnie będziesz musiał z nimi pracować).

Twój algorytm powinien więc:

  1. sklonuj swoje zdalne repozytorium do innego katalogu
  2. używając git filter-branchtylko pozostawionych plików w pewnym podkatalogu, pchnij do nowego pilota
  3. utwórz zatwierdzenie, aby usunąć ten podkatalog z oryginalnego zdalnego repozytorium
Oleksandr Shapovalov
źródło
6

Wydaje się, że większość (wszystkich?) Odpowiedzi tutaj opiera się na jakiejś formie git filter-branch --subdirectory-filteri podobnej. Może to działać „przez większość czasu”, jednak w niektórych przypadkach, na przykład w przypadku zmiany nazwy folderu, np .:

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

ABC/
    /move_this_dir_renamed

Jeśli wykonasz normalny styl filtru git, aby wyodrębnić plik „move_me_renamed”, utracisz historię zmian plików, która nastąpiła od tyłu, gdy początkowo był to katalog move_this_dir ( zob. ).

Wygląda więc na to, że jedynym sposobem, aby naprawdę zachować całą historię zmian (jeśli tak jest w twoim przypadku), jest w istocie skopiowanie repozytorium (utworzenie nowego repozytorium, ustawienie tego jako źródła), a następnie usunięcie wszystkich pozostałych i zmień nazwę podkatalogu na nadrzędny w następujący sposób:

  1. Klonuj projekt wielomodułowy lokalnie
  2. Oddziały - sprawdź, co tam jest: git branch -a
  3. Zrób kasę do każdego oddziału, który ma zostać uwzględniony w podziale, aby uzyskać lokalną kopię na stacji roboczej: git checkout --track origin/branchABC
  4. Utwórz kopię w nowym katalogu: cp -r oldmultimod simple
  5. Przejdź do nowej kopii projektu: cd simple
  6. Pozbądź się innych modułów, które nie są potrzebne w tym projekcie:
  7. git rm otherModule1 other2 other3
  8. Teraz pozostaje tylko podkatalog modułu docelowego
  9. Pozbądź się podkatalogu modułu, aby katalog główny modułu stał się nowym katalogiem głównym projektu
  10. git mv moduleSubdir1/* .
  11. Usuń podkatalog relikwii: rmdir moduleSubdir1
  12. Sprawdź zmiany w dowolnym momencie: git status
  13. Utwórz nowe repozytorium git i skopiuj jego adres URL, aby skierować do niego ten projekt:
  14. git remote set-url origin http://mygithost:8080/git/our-splitted-module-repo
  15. Sprawdź, czy to dobrze: git remote -v
  16. Przekaż zmiany do zdalnego repozytorium: git push
  17. Przejdź do zdalnego repozytorium i sprawdź, czy wszystko tam jest
  18. Powtórz to dla każdej innej potrzebnej gałęzi: git checkout branch2

Jest to zgodne z dokumentem github „Dzielenie podfolderu na nowe repozytorium” kroki 6-11, aby wypchnąć moduł do nowego repozytorium.

Nie pozwoli ci to zaoszczędzić miejsca w folderze .git, ale zachowa całą historię zmian tych plików, nawet przy różnych nazwach. I może to nie być tego warte, jeśli nie ma „zbyt wielu” utraconych historii itp. Ale przynajmniej masz gwarancję, że nie stracisz starszych zobowiązań!

rogerdpack
źródło
1
Znalazłem igłę w stogu siana gita! Teraz mogę zachować Całą moją historię zatwierdzeń.
Adam
5

Polecam przewodnik GitHub dotyczący podziału podfolderów na nowe repozytorium . Kroki są podobne do odpowiedzi Paula , ale dla mnie instrukcje były łatwiejsze do zrozumienia.

Zmodyfikowałem instrukcje, aby dotyczyły lokalnego repozytorium, a nie jednego hostowanego w GitHub.


Podział podfolderu na nowe repozytorium

  1. Otwórz Git Bash.

  2. Zmień bieżący katalog roboczy na lokalizację, w której chcesz utworzyć nowe repozytorium.

  3. Sklonuj repozytorium zawierające podfolder.

git clone OLD-REPOSITORY-FOLDER NEW-REPOSITORY-FOLDER
  1. Zmień bieżący katalog roboczy na sklonowane repozytorium.

cd REPOSITORY-NAME
  1. Aby odfiltrować podfolder od reszty plików w repozytorium, uruchom git filter-branch , podając następujące informacje:
    • FOLDER-NAME: Folder w projekcie, z którego chcesz utworzyć osobne repozytorium.
      • Wskazówka: użytkownicy systemu Windows powinni używać /do rozgraniczania folderów.
    • BRANCH-NAME: Domyślna gałąź dla bieżącego projektu, na przykład, masterlub 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
Stevoisiak
źródło
Fajny post, ale zauważam, że pierwszy akapit dokumentu, który dowiązałeś, mówi: 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.Jednak zgodnie z komentarzami do wszystkich odpowiedzi tutaj zarówno skrypt, jak filter-branchi subtreeskrypt powodują utratę historii wszędzie tam, gdzie zmienia się nazwa podkatalogu. Czy można coś zrobić, aby rozwiązać ten problem?
Adam
Znalazłem rozwiązanie dla zachowania wszystkich zatwierdzeń, w tym poprzednich zmian nazw / ruchów katalogów - to odpowiedź Rogerdpack na to pytanie.
Adam
Jedyny problem polega na tym, że nie mogę już używać sklonowanego repozytorium
Qiulang
5

Podczas git filter-branchkorzystania z nowszej wersji git( 2.22+może?), Mówi o użyciu tego nowego narzędzia git-filter-repo . To narzędzie z pewnością uprościło mi sprawy.

Filtrowanie z repozytorium filtrów

Polecenia do utworzenia XYZrepozytorium z pierwotnego pytania:

# create local clone of original repo in directory XYZ
tmp $ git clone [email protected]: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 [email protected]:user/XYZ.git

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

założenia: * zdalne repozytorium XYZ było nowe i puste przed wypchnięciem

Filtrowanie i przenoszenie

W moim przypadku chciałem również przenieść kilka katalogów, aby uzyskać bardziej spójną strukturę. Początkowo uruchomiłem to proste filter-repopolecenie git mv dir-to-rename, ale potem odkryłem, że mogę uzyskać nieco „lepszą” historię przy użyciu tej --path-renameopcji. Zamiast zobaczyć ostatnio zmodyfikowane 5 hours agoprzeniesione pliki w nowym repozytorium, teraz widzęlast year (w interfejsie GitHub), który pasuje do zmodyfikowanych czasów w oryginalnym repozytorium.

Zamiast...

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

Ostatecznie pobiegłem ...

git filter-repo --path XY1 --path XY2 --path inconsistent --path-rename inconsistent:XY3
Uwagi:
  • Pomyślałem, że post na blogu Git Rev News dobrze wyjaśnia powody tworzenia kolejnego narzędzia do filtrowania repo.
  • Początkowo próbowałem ścieżki tworzenia podkatalogu pasującego do nazwy docelowego repozytorium w oryginalnym repozytorium, a następnie filtrowania (za pomocą git filter-repo --subdirectory-filter dir-matching-new-repo-name). To polecenie poprawnie przekonwertowało ten podkatalog na katalog główny skopiowanego lokalnego repozytorium, ale zaowocowało to także historią tylko trzech zatwierdzeń, które zajęły utworzenie podkatalogu. (Nie zdawałem sobie sprawy, że --pathmożna to określić wiele razy; tym samym eliminuje potrzebę tworzenia podkatalogu w repozytorium źródłowym). Ponieważ do czasu, gdy zauważyłem, że ktoś zaangażował się w repozytorium źródłowe, zauważyłem, że nie udało mi się przenieść historię, której użyłem git reset commit-before-subdir-move --hardpo clonepoleceniu i dodałem --forcedo filter-repopolecenia, aby działało na nieco zmodyfikowanym klonie lokalnym.
git clone ...
git reset HEAD~7 --hard      # roll back before mistake
git filter-repo ... --force  # tell filter-repo the alterations are expected
  • Byłem zaskoczony instalacją, ponieważ nie byłem świadomy wzoru rozszerzenia git, ale ostatecznie sklonowałem git-filter-repo i dowiązałem go symbolicznie do $(git --exec-path):
ln -s ~/github/newren/git-filter-repo/git-filter-repo $(git --exec-path)
LPearson
źródło
1
Wybitny za zalecenie nowego filter-reponarzędzia (które przedstawiłem w zeszłym miesiącu w stackoverflow.com/a/58251653/6309 )
VonC
Korzystanie git-filter-repozdecydowanie powinien być preferowanym podejściem w tym momencie. Jest o wiele, dużo szybszy i bezpieczniejszy niż git-filter-branchi chroni przed wieloma problemami, na które można natknąć się podczas przepisywania historii gitów. Mam nadzieję, że ta odpowiedź przyciąga więcej uwagi, ponieważ jest to jedna z nich git-filter-repo.
Jeremy Caney
4

Miałem dokładnie ten problem, ale wszystkie standardowe rozwiązania oparte na gałęzi filter git były bardzo wolne. Jeśli masz małe repozytorium, to może nie być problem, to było dla mnie. Napisałem inny program do filtrowania git oparty na libgit2, który jako pierwszy krok tworzy gałęzie dla każdego filtrowania podstawowego repozytorium, a następnie wypycha je do czyszczenia repozytoriów jako następny krok. W moim repozytorium (500 Mb 100 000 zatwierdzeń) standardowe metody git filter-branch trwały kilka dni. Mój program zajmuje kilka minut, aby wykonać to samo filtrowanie.

Ma wspaniałą nazwę git_filter i mieszka tutaj:

https://github.com/slobobaby/git_filter

na GitHub.

Mam nadzieję, że komuś się przyda.

slobobaby
źródło
4

Użyj tego polecenia filtru, aby usunąć podkatalog, zachowując jednocześnie tagi i gałęzie:

git filter-branch --index-filter \
"git rm -r -f --cached --ignore-unmatch DIR" --prune-empty \
--tag-name-filter cat -- --all
cmcginty
źródło
co to jest kot?
rogerdpack,
4

Oto, co warto, oto jak używać GitHub na komputerze z systemem Windows. Załóżmy, że masz sklonowane repozytorium w miejscu zamieszkania C:\dir1. Struktura katalogów wygląda następująco: C:\dir1\dir2\dir3. dir3Katalog jest jeden Chcę być nowy oddzielny repo.

Github:

  1. Utwórz nowe repozytorium: MyTeam/mynewrepo

Podpowiedź Bash:

  1. $ cd c:/Dir1
  2. $ git filter-branch --prune-empty --subdirectory-filter dir2/dir3 HEAD
    Zwrócono: Ref 'refs/heads/master' was rewritten(fyi: dir2 / dir3 rozróżnia małe i duże litery).

  3. $ git remote add some_name [email protected]:MyTeam/mynewrepo.git
    git remote add origin etc. nie działał, zwrócił „ remote origin already exists

  4. $ git push --progress some_name master

James Lawruk
źródło
3

Jak wspomniałem powyżej , musiałem użyć rozwiązania odwrotnego (usuwając wszystkie zatwierdzenia nie dotykające mojego dir/subdir/targetdir), które wydawało się działać całkiem dobrze, usuwając około 95% zatwierdzeń (zgodnie z życzeniem). Pozostają jednak dwa małe problemy.

Po pierwsze , filter-branchwykonał świetną robotę usuwając zatwierdzenia, które wprowadzają lub modyfikują kod, ale najwyraźniej zatwierdzenia scalania znajdują się poniżej jego stacji w Gitiverse.

Jest to kwestia kosmetyczna, z którą prawdopodobnie mógłbym żyć (mówi ... powoli wycofując się z odwróconymi oczami) .

PO DRUGI, kilka pozostałych zmian jest prawie WSZYSTKIE zduplikowanych! Wydaje mi się, że uzyskałem drugą, zbędną oś czasu, która obejmuje prawie całą historię projektu. Interesującą rzeczą (którą widać na poniższym obrazku) jest to, że moje trzy lokalne oddziały nie znajdują się na tej samej osi czasu (z pewnością dlatego istnieje i nie są po prostu śmieciami).

Jedyne, co mogę sobie wyobrazić, to to, że jednym z usuniętych zatwierdzeń był być może pojedynczy zatwierdzenie scalania, które filter-branch faktycznie usunęło , i które stworzyło równoległą oś czasu, ponieważ każdy nie połączony nić wziął własną kopię zatwierdzeń. ( wzrusza ramionami Gdzie są moje TARDiS?) Jestem prawie pewien, że mogę rozwiązać ten problem, chociaż naprawdę chciałbym zrozumieć, jak to się stało.

W przypadku szalonego mergefest-O-RAMA prawdopodobnie zostawię go w spokoju, ponieważ tak mocno zakorzenił się w mojej historii popełnień - grozi mi za każdym razem, gdy się zbliżam - nie wydaje się, aby faktycznie powodował wszelkie nie-kosmetyczne problemy i ponieważ jest dość ładna w Tower.app.

Jay Allen
źródło
3

Łatwiejsza droga

  1. zainstaluj git splits. Stworzyłem go jako rozszerzenie git, oparte na rozwiązaniu Jkeating .
  2. Podziel katalogi na oddział lokalny #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. Utwórz gdzieś puste repozytorium. Załóżmy, że utworzyliśmy puste repozytorium o nazwie xyzGitHub, które ma ścieżkę:[email protected]: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 [email protected]:simpliwp/xyz.git #push the branch to the empty repo's master branch git push origin_xyz XYZ:master

  5. Sklonuj nowo utworzone zdalne repozytorium 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 [email protected]:simpliwp/xyz.git

AndrewD
źródło
Zaletą tej metody w porównaniu z „łatwym sposobem” jest to, że pilot jest już skonfigurowany do nowego repozytorium, dzięki czemu można natychmiast wykonać dodawanie poddrzewa. W rzeczywistości wydaje mi się to łatwiejsze (nawet bez git splits)
MM
Wspiera AndrewD za opublikowanie tego rozwiązania. Rozwiesiłem jego repozytorium, aby działało na OSX ( github.com/ricardoespsanto/git-splits ), jeśli jest to przydatne dla kogokolwiek innego
ricardoespsanto
2

Może być potrzebne coś takiego jak „git reflog expire --expire = now - all” przed odśmiecaniem, aby faktycznie wyczyścić pliki. git filter-branch po prostu usuwa odniesienia w historii, ale nie usuwa wpisów z rejestru, które przechowują dane. Oczywiście najpierw przetestuj.

Moje użycie dysku dramatycznie spadło, chociaż moje początkowe warunki były nieco inne. Być może --subdirectory-filter neguje tę potrzebę, ale wątpię w to.


źródło
2

Sprawdź projekt git_split na https://github.com/vangorra/git_split

Zamień katalogi git we własne repozytoria w ich własnej lokalizacji. Brak poddrzewa zabawnego biznesu. Ten skrypt zajmie istniejący katalog w twoim repozytorium git i zamieni ten katalog w niezależne repozytorium. Po drodze skopiuje całą historię zmian dla podanego katalogu.

./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.
Vangora
źródło
1

Umieść to w swoim 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'
grosser
źródło
1

Jestem pewien, że poddrzewo git jest w porządku i wspaniałe, ale moje podkatalogi kodu zarządzanego przez git, które chciałem przenieść, były w całości zaćmione. Więc jeśli używasz egit, jest to boleśnie łatwe. Weź projekt, który chcesz przenieść, i połącz go -> odłącz, a następnie zespół -> udostępnij go w nowej lokalizacji. Domyślnie spróbuje użyć starej lokalizacji repozytorium, ale możesz odznaczyć istniejącą opcję użycia i wybrać nowe miejsce, aby ją przenieść. Cały grad egit.

stu
źródło
3
„Dobra i cudowna” część poddrzewa polega na tym, że historia twojego podkatalogu przychodzi na przejażdżkę. Jeśli nie potrzebujesz historii, to droga do bólu jest łatwa.
pglezen
0

Możesz łatwo wypróbować https://help.github.com/enterprise/2.15/user/articles/splitting-a-subfolder-out-into-a-new-repository/

To zadziałało dla mnie. Problemy, które napotkałem w powyższych krokach, to:

  1. W tym poleceniu jest mistrzemgit filter-branch --prune-empty --subdirectory-filter FOLDER-NAME BRANCH-NAMEBRANCH-NAME

  2. jeśli ostatni krok nie powiedzie się podczas zatwierdzania z powodu problemu z ochroną, wykonaj - https://docs.gitlab.com/ee/user/project/protected_branches.html

Barath Ravichander
źródło
0

Znalazłem dość proste rozwiązanie. Pomysł polega na skopiowaniu repozytorium, a następnie usunięciu niepotrzebnej części. Tak to działa:

1) Sklonuj repozytorium, które chcesz podzielić

git clone [email protected]:testrepo/test.git

2) Przejdź do folderu git

cd test/

2) Usuń niepotrzebne foldery i zatwierdź je

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

3) Usuń niepotrzebne foldery z historii za pomocą BFG

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

w przypadku wielu folderów możesz użyć przecinka

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

4) Sprawdź, czy historia nie zawiera właśnie usuniętych plików / folderów

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

5) Teraz masz czyste repozytorium bez ABC, więc po prostu weź je do nowego źródła

remote add origin [email protected]:username/new_repo
git push -u origin master

Otóż ​​to. Możesz powtórzyć kroki, aby uzyskać inne repozytorium,

po prostu usuń XY1, XY2 i zmień nazwę XYZ -> ABC w kroku 3

Vladislav Troyan
źródło
Prawie idealnie ... ale zapomniałeś "git filter-branch --prune-empty", aby usunąć wszystkie stare zatwierdzenia, które są teraz puste. Do zrobienia przed mistrzem push to origin!
ZettaCircl
Jeśli popełniłeś błąd i nadal chcesz „odświeżyć” po usunięciu starego pustego zatwierdzenia, wykonaj: „git push -u origin master --force-with-lease”
ZettaCircl