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


mam Git repozytorium zawierające 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?

Sądzę, że mógłbym zrobić klona i usunąć niechciane części każdego klonu, ale przypuszczam, że dałoby mi to kompletne drzewo podczas sprawdzania starszej wersji itp. To może być do zaakceptowania, ale wolałbym udawać, że dwa repozytoria nie mają wspólnej historii.

Aby to wyjaśnić, mam następującą strukturę:

XYZ/
    .git/
    XY1/
    ABC/
    XY2/

Ale chciałbym tego zamiast:

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

1595
2017-12-11 13:57


pochodzenie


Teraz jest to banalne git filter-branch zobacz moją odpowiedź poniżej. - jeremyjjbrown
@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:


Aktualizacja: Ten proces jest tak powszechny, że zespół git znacznie uprościł go dzięki nowemu narzędziu, git subtree. Spójrz tutaj: Odłącz katalog (przenieś) do osobnego repozytorium Git


Chcesz sklonować swoje repozytorium, a następnie użyć git filter-branch oznaczyć wszystko oprócz podkatalogu, którego chcesz użyć w nowym repo, do zbierania śmieci.

  1. Aby sklonować lokalne repozytorium:

    git clone /XYZ /ABC
    

    (Uwaga: repozytorium będzie klonowane za pomocą twardych linków, ale to nie jest problem, ponieważ pliki z twardym łączem 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ńmy pochodzenie, aby uniknąć popychania i upewnić się, że stare zatwierdzenia nie będą się odnosić do miejsca pochodzenia:

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

    lub dla wszystkich odległych 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ż usunąć znaczniki, które nie mają związku z podprojektem; możesz to zrobić później, ale może być konieczne ponowne przycięcie repo. Nie zrobiłem tego i dostałem WARNING: Ref 'refs/tags/v0.1' is unchanged dla wszystkich tagów (ponieważ wszystkie były niezwiązane z podprojektem); dodatkowo po usunięciu takich tagów odzyskana zostanie większa przestrzeń. Widocznie git filter-branch powinno 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 filtru i zresetuj, aby wykluczyć pozostałe pliki, aby można je było przyciąć. Dodajmy również --tag-name-filter cat --prune-empty aby usunąć puste zatwierdzenia i przepisać znaczniki (pamiętaj, że będzie to musiało usunąć podpis):

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

    lub alternatywnie, tylko przepisać gałąź HEAD i ignorować znaczniki i inne gałęzie:

    git filter-branch --tag-name-filter cat --prune-empty --subdirectory-filter ABC HEAD
    
  5. Następnie usuń archiwa kopii zapasowych, aby można było naprawdę odzyskać miejsce (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 z podkatalogu ABC z zachowaną całą jego historią.

Uwaga: w przypadku większości zastosowań git filter-branch powinien rzeczywiście mieć dodany parametr -- --all. Tak, to naprawdę --przestrzeń--  all. To muszą być ostatnie parametry dla polecenia. Jak odkrył Matli, dzięki temu gałęzie projektu i znaczniki są uwzględnione w nowym repozytorium.

Edycja: różne sugestie z komentarzy poniżej zostały włączone, aby upewnić się, na przykład, że repozytorium faktycznie się skurczyło (co nie zawsze miało miejsce wcześniej).


1155
2017-07-25 17:10



Bardzo dobra odpowiedź. Dzięki! Aby naprawdę uzyskać dokładnie to, co chciałem, dodałem "- --all" do polecenia branch-filter. - matli
Dlaczego potrzebujesz --no-hardlinks? Usunięcie jednego twardego linku nie wpłynie na drugi plik. Obiekty Git są również niezmienne. Tylko wtedy, gdy będziesz potrzebował zmienić uprawnienia właściciela / pliku --no-hardlinks. - vdboor
Dodatkowym krokiem, który poleciłbym, byłoby "git remote rm origin". Gdybym się nie mylił, to pchałoby z powrotem do oryginalnego repozytorium. - Tom
Kolejne polecenie do dołączenia filter-branch jest --prune-empty, aby usunąć teraz puste zatwierdzenia. - Seth Johnson
Tak jak Paul, nie chciałem tagów projektu w moim nowym repo, więc nie korzystałem -- --all. Ja też uciekłem git remote rm origin, i git tag -l | xargs git tag -d przed git filter-branch dowództwo. To mnie skurczyło .git katalog od 60M do ~ 300K. Zwróć uwagę, że aby uzyskać zmniejszenie rozmiaru, muszę uruchomić obie te komendy. - saltycrane


Easy Way ™

Okazuje się, że jest to tak powszechna i użyteczna praktyka, że ​​władcy gita sprawili, że było to naprawdę łatwe, ale musisz mieć nowszą wersję gita (> = 1.7.11 maja 2012). Zobacz dodatek jak zainstalować najnowszy git. Ponadto istnieje przykład z prawdziwego świata w przewodnik poniżej.

  1. Przygotuj stare repozytorium

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

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

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

  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 repo z Githubem lub gdziekolwiek

    git remote add origin <git@github.com:my-user/new-repo.git>
    git push origin -u master
    
  4. Sprzątać, w razie potrzeby

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

    Uwaga: Pozostawia wszystkie historyczne odniesienia w repozytorium. Zobacz dodatek poniżej, jeśli naprawdę martwisz się o to, że popełniłeś hasło lub chcesz zmniejszyć rozmiar pliku .git teczka.

...

Opis przejścia

To są takie same kroki jak powyżej, ale po moich dokładnych krokach dla mojego repozytorium zamiast używać <meta-named-things>.

Oto projekt, który mam do wdrożenia modułów przeglądarki JavaScript w węźle:

tree ~/Code/node-browser-compat

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

Chcę podzielić pojedynczy folder, btoa, do osobnego repozytorium git

pushd ~/Code/node-browser-compat/
git subtree split -P btoa -b btoa-only
popd

Mam teraz nowy oddział, btoa-only, który ma tylko zatwierdzenia dla btoa i chcę utworzyć nowe repozytorium.

mkdir ~/Code/btoa/
pushd ~/Code/btoa/
git init
git pull ~/Code/node-browser-compat btoa-only

Następnie tworzę nowe repo na Github lub bitbucket, lub cokolwiek innego i dodaję to origin (btw, "origin" jest po prostu konwencją, a nie częścią polecenia - możesz nazwać ją "zdalnym serwerem" lub cokolwiek chcesz)

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

Szczęśliwy dzień!

Uwaga: Jeśli utworzono repozytorium z README.md, .gitignore i LICENSE, musisz najpierw wyciągnąć:

git pull origin -u master
git push origin -u master

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

git rm -rf btoa

...

dodatek

Najnowsze git na OS X

Aby uzyskać najnowszą wersję git:

brew install git

Aby zaparzyć dla OS X:

http://brew.sh

Najnowsze git na Ubuntu

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

Jeśli to nie dział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.

czyścisz swoją historię

Domyślnie usuwanie plików z git w rzeczywistości nie usuwa ich z git, po prostu zatwierdza, że ​​już ich tam nie ma. Jeśli chcesz rzeczywiście usunąć odniesienia historyczne (tzn. 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 nie pojawia się już w historii git

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

Jednak ty nie może "push" usuwa do github i tym podobne. Jeśli spróbujesz, dostaniesz błąd i będziesz musiał git pull zanim możesz git push - a potem wracasz do posiadania wszystkiego w swojej historii.

Więc jeśli chcesz usunąć historię z "pochodzenia" - czyli usunąć ją z github, bitbucket, itp. - musisz usunąć repozytorium i ponownie nacisnąć przyciętą kopię repo. Ale poczekaj - jest więcej! - Jeśli naprawdę zależy Ci na usunięciu hasła lub czegoś podobnego, musisz wyczyścić kopię zapasową (patrz poniżej).

zrobienie .git mniejszy

Powyższe polecenie usuwania historii nadal pozostawia po sobie kilka plików kopii zapasowych - ponieważ git jest tak dobry, że nie zrujnuje repozytorium przez przypadek. Ostatecznie usunie on osierocone pliki w ciągu dni i miesięcy, ale pozostawia je tam na jakiś czas, na wypadek, gdy zdasz sobie sprawę, że przypadkowo usunąłeś coś, czego nie chcesz.

Więc jeśli naprawdę chcesz wynieś śmieci do zmniejszyć rozmiar klona repo natychmiast musisz zrobić 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

Powiedziałbym, że nie zalecałbym wykonywania tych kroków, chyba że wiesz, że musisz - na wypadek, gdybyś wyczyścił zły podkatalog, wiesz? Pliki kopii zapasowej nie powinny zostać sklonowane po naciśnięciu repo, będą po prostu w lokalnej kopii.

Kredyt


1124
2018-06-05 13:15



git subtree jest nadal częścią folderu 'contrib' i nie jest domyślnie instalowany we wszystkich dystrybucjach. github.com/git/git/blob/master/contrib/subtree - onionjake
@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
Jeśli przekazałeś hasło do publicznego repozytorium, powinieneś zmienić hasło, nie próbować usunąć go z publicznego repozytorium i mam nadzieję, że nikt go nie widział. - Miles Rout
wydaje się, że nowe repozytorium z zawartością ABC/, ale nowe repozytorium nie zawiera folderu ABC/ sam, jak pytanie zadawane. Jak byś to zrobił? - woojoo666
To rozwiązanie nie chroni historii. - Cœur


Odpowiedź Pawła tworzy nowe repozytorium zawierające / ABC, ale nie usuwa / ABC z wnętrza / XYZ. Następujące polecenie usunie / ABC z wnętrza / XYZ:

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

Oczywiście najpierw przetestuj go w repozytorium "clone --no-hardlinks", a następnie wykonaj polecenia reset, gc i prune, które wymienia Paul.


131
2017-10-19 21:10



sprawiają, że git filter-branch --index-filter "git rm -r -f --cached --ignore-unmatch ABC" --prune-empty HEAD i będzie dużo szybciej. Filtr indeksu działa na indeksie, a filtr drzewa musi się kasa i etap wszystko za każde zatwierdzenie. - fmarc
w niektórych przypadkach zepsucie historii repozytorium XYZ to przesada ... po prostu proste "rm -rf ABC; git rm -r ABC; git commit -m" wyodrębnione ABC do własnego repo "" lepiej by działało dla większości ludzi. - Evgeny
Prawdopodobnie chcesz użyć opcji -f (force) dla tego polecenia, jeśli robisz to więcej niż jeden raz, np. Aby usunąć dwa katalogi po ich rozdzieleniu. W przeciwnym razie otrzymasz komunikat "Nie można utworzyć nowej kopii zapasowej". - Brian Carlton
Jeśli to robisz --index-filter metoda, możesz również chcieć to zrobić git rm -q -r -f, więc każde wywołanie nie wypisze linii dla każdego usuwanego pliku. - Eric Naeseth
Sugerowałbym zredagowanie odpowiedzi Pawła, tylko dlatego, że Paul jest tak dokładny. - Erik Aronesty


Odkryłem, że aby poprawnie usunąć starą historię z nowego repozytorium, musisz zrobić trochę więcej pracy po filter-branch krok.

  1. Zrób klon i filtr:

    git clone --no-hardlinks foo bar; cd bar
    git filter-branch --subdirectory-filter subdir/you/want
    
  2. Usuń wszystkie odniesienia do starej historii. "Origin" śledził twój klon, a "original" to miejsce, w którym gałąź filtru zapisuje stare pliki:

    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 packfile, którego nie dotknie fsck. Rozerwij go na strzępy, tworząc nowy plik pack i usuwając nieużywane obiekty:

    git repack -ad
    

Jest wyjaśnienie tego w instrukcja obsługi filtra.


94
2018-06-09 15:41



Myślę, że jakoś tak git gc --aggressive --prune=now wciąż nie ma, prawda? - Albert
@Albert Polecenie repack zajmuje się tym i nie będzie żadnych luźnych obiektów. - Josh Lee
tylko przepakowanie nie działało dla mnie, potrzebowałem zrobić git gc - jsvnm
Tak, git gc --aggressive --prune=now zmniejszyło dużo nowego repo - Tomek Wyderka
Prosty i elegancki. Dzięki! - Marco Pelegrini


Edytuj: dodano skrypt Bash.

Odpowiedzi podane tutaj działały tylko częściowo dla mnie; Wiele dużych plików pozostało w pamięci podręcznej. Co w końcu dział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ń wielkość repozytorium wynosiła około 100 MB. Ten doprowadził do 1,7 MB. Może to pomaga komuś :)


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

38
2017-08-20 14:11





To nie jest już tak skomplikowane, że możesz po prostu użyć git - gałąź filtru polecenie na klonie z ciebie repo, aby odkurzyć podkatalogi, których nie chcesz, a następnie popchnąć do nowego pilota.

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

21
2018-03-22 20:55



To działało jak czar. YOUR_SUBDIR w powyższym przykładzie to podkatalog, który chcesz KEEP, wszystko inne zostanie usunięte - J.T. Taylor
Aktualizacje na podstawie Twojego komentarza. - jeremyjjbrown
To nie odpowiada na pytanie. Z dokumentów mówi The result will contain that directory (and only that) as its project root. i rzeczywiście to dostaniesz, tzn. oryginalna struktura projektu nie zostanie zachowana. - NicBright
@NicBright Czy możesz zilustrować swój problem z XYZ i ABC, jak w pytaniu, aby pokazać, co jest nie tak? - Adam
@jeremyjjbrown jest możliwe ponowne użycie sklonowanego repo i nie używanie nowego repo, tj. moje pytanie tutaj stackoverflow.com/questions/49269602/... - Qiulang


Aktualizacja: Moduł git-subtree był tak użyteczny, że zespół git wciągnął go do rdzenia i uczynił go git subtree. Spójrz tutaj: Odłącz katalog (przenieś) do osobnego repozytorium Git

git-subtree może być przydatny do tego

http://github.com/apenwarr/git-subtree/blob/master/git-subtree.txt (przestarzałe)

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


19
2017-08-06 15:26



git-subtree jest teraz częścią Git, chociaż jest w drzewie contrib, więc nie zawsze jest instalowane domyślnie. Wiem, że jest zainstalowany przez formułę gith Homebrew, ale bez strony podręcznika. apenwarr w ten sposób nazywa swoją wersję przestarzałą. - echristopherson