Jak połączyć dwa repozytoria Git?


1619

Rozważ następujący scenariusz:

Opracowałem mały eksperymentalny projekt A we własnym repozytorium Git. Teraz dojrzał i chciałbym, aby A był częścią większego projektu B, który ma swoje własne duże repozytorium. Chciałbym teraz dodać A jako podkatalog B.

Jak połączyć A w B, nie tracąc historii z żadnej strony?


8
Jeśli próbujesz po prostu połączyć dwa repozytoria w jedno, bez konieczności przechowywania obu repozytoriów, spójrz na to pytanie: stackoverflow.com/questions/13040958/...
Flimm

Aby połączyć repozytorium git z niestandardowym katalogiem z zapisaniem wszystkich poleceń, użyj stackoverflow.com/a/43340714/1772410
Andrey Izman

Odpowiedzi:


436

Pojedynczą gałąź innego repozytorium można łatwo umieścić w podkatalogu zachowującym jego historię. Na przykład:

git subtree add --prefix=rails git://github.com/rails/rails.git master

Pojawi się jako pojedynczy zatwierdzenie, w którym wszystkie pliki gałęzi głównej Rails są dodawane do katalogu „rails”. Jednak tytuł zatwierdzenia zawiera odniesienie do starego drzewa historii:

Dodaj „rails /” z zatwierdzenia <rev>

Gdzie <rev>jest skrót zatwierdzenia SHA-1. Nadal możesz zobaczyć historię, zrzucić winę na niektóre zmiany.

git log <rev>
git blame <rev> -- README.md

Zauważ, że nie widzisz stąd prefiksu katalogu, ponieważ jest to faktycznie stara gałąź pozostawiona nietknięta. Powinieneś traktować to jak zwykły zatwierdzenie przeniesienia pliku: będziesz potrzebował dodatkowego skoku po osiągnięciu go.

# finishes with all files added at once commit
git log rails/README.md

# then continue from original tree
git log <rev> -- README.md

Istnieją bardziej złożone rozwiązania, takie jak robienie tego ręcznie lub przepisywanie historii zgodnie z opisem w innych odpowiedziach.

Polecenie git-subtree jest częścią oficjalnego git-contrib, niektórzy menedżerowie pakietów instalują go domyślnie (OS X Homebrew). Ale może być konieczne samodzielne zainstalowanie go oprócz git.


2
Oto instrukcje instalacji Git SubTree (od czerwca 2013 r.): Stackoverflow.com/a/11613541/694469 (i zastąpiłem git co v1.7.11.3 je ... v1.8.3).
KajMagnus

1
Dzięki za informacje o poniższej odpowiedzi. Począwszy od git 1.8.4 „poddrzewo” nadal nie jest uwzględnione (przynajmniej nie w Ubuntu 12.04 git ppa (ppa: git-core / ppa))
Matt Klein

1
Mogę potwierdzić, że po tym git log rails/somefilenie wyświetla historii zatwierdzeń tego pliku, z wyjątkiem zatwierdzenia scalania. Jak sugerował @artfulrobot, sprawdź odpowiedź Grega Hewgilla . I może być konieczne użycie git filter-branchrepozytorium, które chcesz uwzględnić.
Jifeng Zhang

6
Lub przeczytaj „Scalanie dwóch repozytoriów Git w jedno repozytorium bez utraty historii plików” Erica Lee Lee saintgimp.org/2013/01/22/…
Jifeng Zhang,

4
Jak powiedzieli inni, git subtreemoże nie robić tego, co myślisz! Patrz tutaj dla bardziej kompletnego rozwiązania.
Paul Draper

1906

Jeśli chcesz się połączyć project-aw project-b:

cd path/to/project-b
git remote add project-a path/to/project-a
git fetch project-a --tags
git merge --allow-unrelated-histories project-a/master # or whichever branch you want to merge
git remote remove project-a

Zaczerpnięty z: git scala różne repozytoria?

Ta metoda działała dla mnie całkiem dobrze, jest krótsza i moim zdaniem dużo czystsza.

Jeśli chcesz umieścić project-aw podkatalogu, możesz użyć git-filter-repo( filter-branchjest odradzane ). Uruchom następujące polecenia przed powyższymi poleceniami:

cd path/to/project-a
git filter-repo --to-subdirectory-filter project-a

Przykład połączenia 2 dużych repozytoriów i umieszczenia jednego z nich w podkatalogu: https://gist.github.com/x-yuri/9890ab1079cf4357d6f269d073fd9731

Uwaga:--allow-unrelated-histories parametr istnieje tylko od git> = 2.9. Zobacz Git - git merge Documentation / --allow-nonrelated-histories

Aktualizacja : Dodano --tagszgodnie z sugestią @jstadler, aby zachować tagi.


8
To załatwiło sprawę dla mnie. Po raz pierwszy zadziałał jak urok z jednym konfliktem w pliku .gitignore! Doskonale zachował historię zmian. Dużym plusem w stosunku do innych podejść - oprócz prostoty - jest to, że nie ma potrzeby ciągłego odniesienia do połączonego repozytorium. Jedną rzeczą, na którą należy jednak uważać - jeśli jesteś programistą iOS, takim jak ja - bardzo ostrożnie upuszczaj plik projektu docelowego repozytorium do obszaru roboczego.
Max MacLeod

30
Dzięki. Pracował dla mnie. Musiałem przenieść scalony katalog do podfolderu, więc po wykonaniu powyższych kroków po prostu użyłemgit mv source-dir/ dest/new-source-dir
Sid

13
Ten git mergekrok kończy się niepowodzeniem fatal: refusing to merge unrelated histories; --allow-unrelated-historiesnaprawia to, jak wyjaśniono w dokumentacji .
ssc

19
--allow-unrelated-historieszostał wprowadzony w git 2.9 . We wcześniejszych wersjach było to zachowanie domyślne.
Douglas Royds

11
Krótszy: git fetch /path/to/project-a master; git merge --allow-unrelated-histories FETCH_HEAD.
jthill

614

Oto dwa możliwe rozwiązania:

Submoduły

Skopiuj repozytorium A do osobnego katalogu w większym projekcie B lub (może lepiej) sklonuj repozytorium A do podkatalogu w projekcie B. Następnie użyj podmoduła git, aby uczynić to repozytorium podmodułem repozytorium B.

Jest to dobre rozwiązanie dla luźno powiązanych repozytoriów, w których rozwój w repozytorium A trwa, a główną część rozwoju stanowi oddzielny samodzielny rozwój w A. Zobacz także strony SubmoduleSupport i GitSubmoduleTutorial na Git Wiki.

Scalanie poddrzewa

Możesz scalić repozytorium A z podkatalogiem projektu B, korzystając ze strategii scalania poddrzewa . Jest to opisane w Markcie Prinz w Subtree Merging and You .

git remote add -f Bproject /path/to/B
git merge -s ours --allow-unrelated-histories --no-commit Bproject/master
git read-tree --prefix=dir-B/ -u Bproject/master
git commit -m "Merge B project as our subdirectory"
git pull -s subtree Bproject master

(Opcja --allow-unrelated-historiesjest wymagana dla Git> = 2.9.0.)

Lub możesz użyć narzędzia git subtree ( repozytorium na GitHub ) autorstwa apenwarr (Avery Pennarun), ogłoszonego na przykład w jego blogu Nowa alternatywa dla submodułów Git: poddrzewo git .


Myślę, że w twoim przypadku (A ma być częścią większego projektu B) poprawnym rozwiązaniem byłoby użycie scalania poddrzewa .


1
Działa to i wydaje się, że zachowuje historię, ale nie jest tak, że można jej użyć do różnicowania plików lub dzielenia przez scalanie. Czy brakuje mi kroku?
jettero

55
to jest niekompletne . Tak, dostajesz mnóstwo zatwierdzeń, ale nie odnoszą się one już do właściwych ścieżek. git log dir-B/somefilenie pokaże niczego oprócz jednego scalenia. Zobacz odpowiedź Grega Hewgilla odnosi się do tego ważnego problemu.
artfulrobot

2
WAŻNE: git pull --no-rebase -s poddrzewo Bproject master Jeśli tego nie zrobisz, a wyciągasz zestaw do automatycznego bazowania, otrzymasz „Nie można przeanalizować obiektu”. Zobacz osdir.com/ml/git/2009-07/msg01576.html
Eric Bowman - abstracto -

4
Ta odpowiedź może być myląca, ponieważ ma B jako scalone poddrzewo, gdy w pytaniu było to A. Wynik kopiowania i wklejania?
vfclists

11
Jeśli próbujesz po prostu skleić ze sobą dwa repozytoria, podmoduły i scalenia poddrzewa są niewłaściwym narzędziem do użycia, ponieważ nie zachowują całej historii plików (jak zauważyli inni komentatorzy). Zobacz stackoverflow.com/questions/13040958/… .
Eric Lee

194

Podmoduł jest dobry, jeśli chcesz zachować projekt osobno. Jeśli jednak naprawdę chcesz połączyć oba projekty w tym samym repozytorium, masz trochę więcej pracy do zrobienia.

Pierwszą rzeczą byłoby użyć git filter-branchdo przepisania nazw wszystkiego w drugim repozytorium, aby znajdowały się w podkatalogu, w którym chciałbyś, aby się skończyły. Więc zamiast foo.c, bar.html, to masz projb/foo.ci projb/bar.html.

Następnie powinieneś być w stanie zrobić coś takiego:

git remote add projb [wherever]
git pull projb

git pullZrobi git fetchnastępnie przez git merge. Nie powinno być żadnych konfliktów, jeśli repozytorium, do którego ciągniesz, nie ma jeszcze projb/katalogu.

Dalsze poszukiwania wskazuje, że coś podobnego miało na celu scalenia gitkw git. Junio ​​C Hamano pisze o tym tutaj: http://www.mail-archive.com/git@vger.kernel.org/msg03395.html


4
scalenie poddrzewa byłoby lepszym rozwiązaniem i nie wymagałoby przepisywania historii dołączonego projektu
Jakub Narębski

8
Chciałbym wiedzieć, jak git filter-branchtego dokonać. Na stronie podręcznika jest napisane o odwrotnej sytuacji: uczynienie subdir / rootem, ale nie na odwrót.
artfulrobot

31
ta odpowiedź byłaby świetna, gdyby wyjaśniła, jak użyć odgałęzienia filtru, aby osiągnąć pożądany rezultat
Anentropic

14
Znalazłem tutaj sposób użycia odgałęzienia filtru: stackoverflow.com/questions/4042816/…
David Minor

3
Zobacz tę odpowiedź dotyczącą realizacji konspektu Grega.
Paul Draper

75

git-subtree jest fajny, ale prawdopodobnie nie jest to ten, którego chcesz.

Na przykład jeśli projectAkatalog jest utworzony w B, po git subtree,

git log projectA

wyświetla tylko jedno zatwierdzenie: scalenie. Zatwierdzenia ze scalonego projektu są dla różnych ścieżek, więc się nie wyświetlają.

Odpowiedź Grega Hewgilla jest najbliższa, chociaż tak naprawdę nie mówi, jak przepisać ścieżki.


Rozwiązanie jest zaskakująco proste.

(1) w A

PREFIX=projectA #adjust this

git filter-branch --index-filter '
    git ls-files -s |
    sed "s,\t,&'"$PREFIX"'/," |
    GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info &&
    mv $GIT_INDEX_FILE.new $GIT_INDEX_FILE
' HEAD

Uwaga: Ta historia przepisuje historię, więc jeśli zamierzasz nadal korzystać z tego repozytorium A, możesz najpierw sklonować (skopiować) jego wyrzuconą kopię.

Uwaga Bene: Musisz zmodyfikować skrypt zastępczy w komendzie sed, jeśli używasz znaków innych niż ascii (lub białych) w nazwach plików lub ścieżce. W takim przypadku lokalizacja pliku w rekordzie utworzonym przez „ls-files -s” zaczyna się od cudzysłowu.

(2) Następnie w B, uruchom

git pull path/to/A

Voila! Masz projectAkatalog w B. Jeśli uruchomisz git log projectA, zobaczysz wszystkie zatwierdzenia z A.


W moim przypadku chciałem dwa podkatalogi projectAi projectB. W takim przypadku zrobiłem również krok (1) do B.


1
Wygląda na to, że skopiowałeś odpowiedź z stackoverflow.com/a/618113/586086 ?
Andrew Mao,

1
@AndrewMao, tak myślę ... Właściwie nie pamiętam. Sporo używałem tego skryptu.
Paul Draper,

6
Dodam, że \ t nie działa w systemie OS X i musisz wpisać <tab>
Muneeb Ali

2
"$GIT_INDEX_FILE"musi być cytowany (dwa razy), w przeciwnym razie metoda zawiedzie, jeśli np. ścieżka zawiera spacje.
Rob W

4
Jeśli zastanawiasz się, aby wstawić <tab> w OSX, musiszCtrl-V <tab>
Casey

48

Jeśli oba repozytoria mają ten sam rodzaj plików (jak dwa repozytoria Rails dla różnych projektów), możesz pobrać dane z repozytorium wtórnego do bieżącego repozytorium:

git fetch git://repository.url/repo.git master:branch_name

a następnie scal je z bieżącym repozytorium:

git merge --allow-unrelated-histories branch_name

Jeśli Twoja wersja Git jest mniejsza niż 2.9, usuń --allow-unrelated-histories.

Następnie mogą wystąpić konflikty. Możesz rozwiązać je na przykład za pomocą git mergetool. kdiff3może być używany wyłącznie z klawiaturą, więc 5 konfliktów zajmuje tylko kilka minut po przeczytaniu kodu.

Pamiętaj, aby zakończyć scalanie:

git commit

25

Ciągle traciłem historię, używając scalania, więc skończyło się na rebase, ponieważ w moim przypadku dwa repozytoria są na tyle różne, że nie łączą się przy każdym zatwierdzeniu:

git clone git@gitorious/projA.git projA
git clone git@gitorious/projB.git projB

cd projB
git remote add projA ../projA/
git fetch projA 
git rebase projA/master HEAD

=> rozwiąż konflikty, a następnie kontynuuj tyle razy, ile potrzeba ...

git rebase --continue

Powoduje to, że jeden projekt ma wszystkie zatwierdzenia z projA, a następnie zatwierdzenia z projB


25

W moim przypadku miałem my-pluginrepozytorium i main-projectrepozytorium i chciałem udawać, że my-pluginzawsze było rozwijane w pluginspodkatalogu main-project.

Zasadniczo przepisałem historię my-pluginrepozytorium, aby wyglądało na to, że cały rozwój odbywał się w plugins/my-pluginpodkatalogu. Następnie dodałem historię rozwoju my-plugindo main-projecthistorii i połączyłem oba drzewa. Ponieważ plugins/my-pluginw main-projectrepozytorium nie było już katalogu , było to trywialne połączenie bez konfliktów. Powstałe repozytorium zawierało całą historię obu oryginalnych projektów i miało dwa źródła.

TL; DR

$ cp -R my-plugin my-plugin-dirty
$ cd my-plugin-dirty
$ git filter-branch -f --tree-filter "zsh -c 'setopt extended_glob && setopt glob_dots && mkdir -p plugins/my-plugin && (mv ^(.git|plugins) plugins/my-plugin || true)'" -- --all
$ cd ../main-project
$ git checkout master
$ git remote add --fetch my-plugin ../my-plugin-dirty
$ git merge my-plugin/master --allow-unrelated-histories
$ cd ..
$ rm -rf my-plugin-dirty

Długa wersja

Najpierw utwórz kopię my-pluginrepozytorium, ponieważ zamierzamy przepisać historię tego repozytorium.

Teraz przejdź do katalogu głównego my-pluginrepozytorium, sprawdź swoją główną gałąź (prawdopodobnie master) i uruchom następującą komendę. Oczywiście powinieneś zastąpić my-plugini pluginsjakikolwiek jest twój rzeczywisty imion.

$ git filter-branch -f --tree-filter "zsh -c 'setopt extended_glob && setopt glob_dots && mkdir -p plugins/my-plugin && (mv ^(.git|plugins) plugins/my-plugin || true)'" -- --all

Teraz wyjaśnienie. git filter-branch --tree-filter (...) HEADuruchamia (...)polecenie przy każdym dostępnym zatwierdzeniu HEAD. Zauważ, że działa to bezpośrednio na dane przechowywane dla każdego zatwierdzenia, więc nie musimy się martwić o pojęcia „katalogu roboczego”, „indeksu”, „przemieszczania” i tak dalej.

Jeśli uruchomisz filter-branchpolecenie, które się nie powiedzie, pozostawi on niektóre pliki w .gitkatalogu, a przy następnej próbie filter-branchbędzie narzekał na to, chyba że podasz -fopcję filter-branch.

Jak dla rzeczywistego polecenia, nie mam dużo szczęścia z dostaniem bashsię do tego, co chciałem, więc zamiast tego użyć zsh -c, aby zshwykonać polecenie. Najpierw ustawiam extended_globopcję, która umożliwia ^(...)składnię mvpolecenia, a także glob_dotsopcję, która pozwala mi wybierać pliki dot (np. .gitignore) Za pomocą glob ( ^(...)).

Następnie używam mkdir -ppolecenia do tworzenia zarówno pluginsi plugins/my-pluginw tym samym czasie.

Na koniec używam funkcji zsh„negatywny glob”, ^(.git|plugins)aby dopasować wszystkie pliki w katalogu głównym repozytorium oprócz .giti nowo utworzonego my-pluginfolderu. (Wykluczenie .gitmoże nie być tutaj konieczne, ale próba przeniesienia katalogu do siebie jest błędem).

W moim repozytorium początkowe zatwierdzenie nie zawierało żadnych plików, więc mvpolecenie zwróciło błąd przy początkowym zatwierdzeniu (ponieważ nic nie było dostępne do przeniesienia). Dlatego dodałem || truetak, git filter-branchżeby się nie przerywało.

--allOpcja mówi filter-branchprzepisać historię dla wszystkich oddziałów w repozytorium, a dodatkowo --trzeba powiedzieć gitinterpretować go jako części listy opcji dla oddziałów przepisać, zamiast jako opcja do filter-branchsiebie.

Teraz przejdź do swojego main-projectrepozytorium i sprawdź gałąź, z którą chcesz się połączyć. Dodaj lokalną kopię my-pluginrepozytorium (ze zmodyfikowaną historią) jako zdalną main-projectz:

$ git remote add --fetch my-plugin $PATH_TO_MY_PLUGIN_REPOSITORY

Będziesz teraz mieć dwa niezwiązane drzewa w swojej historii zmian, które możesz ładnie wizualizować za pomocą:

$ git log --color --graph --decorate --all

Aby je połączyć, użyj:

$ git merge my-plugin/master --allow-unrelated-histories

Zauważ, że w wersji wcześniejszej niż 2.9.0 --allow-unrelated-historiesopcja nie istnieje. Jeśli używasz jednej z tych wersji, po prostu pomiń tę opcję: komunikat o błędzie, który --allow-unrelated-historieszapobiega, został również dodany w wersji 2.9.0.

Nie powinieneś mieć żadnych konfliktów scalania. Jeśli to zrobisz, prawdopodobnie oznacza to, że albo filter-branchpolecenie nie działało poprawnie, albo w plugins/my-pluginkatalogu już był main-project.

Upewnij się, że wprowadzasz wyjaśniający komunikat zatwierdzenia dla wszystkich przyszłych autorów, którzy zastanawiają się, co się dzieje, aby stworzyć repozytorium z dwoma źródłami.

Za pomocą powyższego git logpolecenia możesz wizualizować nowy wykres zatwierdzania, który powinien mieć dwa główne zatwierdzenia . Pamiętaj, że tylko mastergałąź zostanie scalona . Oznacza to, że jeśli masz ważną pracę nad innymi my-plugingałęziami, które chcesz scalić z main-projectdrzewem, powinieneś powstrzymać się od usuwania my-pluginpilota, dopóki nie wykonasz tych scaleń. Jeśli tego nie zrobisz, zatwierdzenia z tych gałęzi nadal będą w main-projectrepozytorium, ale niektóre będą nieosiągalne i podatne na ewentualne wyrzucanie elementów bezużytecznych. (Ponadto będziesz musiał odwoływać się do nich przez SHA, ponieważ usunięcie pilota usuwa jego gałęzie zdalnego śledzenia.)

Opcjonalnie po scaleniu wszystkiego, co chcesz zachować my-plugin, możesz usunąć my-pluginpilota za pomocą:

$ git remote remove my-plugin

Możesz teraz bezpiecznie usunąć kopię my-pluginrepozytorium, którego historię zmieniłeś. W moim przypadku dodałem również informację o wycofaniu do prawdziwego my-pluginrepozytorium po zakończeniu scalania i wypchnięciu.


Testowane na Mac OS X El Capitan z git --version 2.9.0i zsh --version 5.2. Twój przebieg może się różnić.

Bibliografia:


1
Skąd --allow-unrelated-historiespochodzisz
xpto

3
@MarceloFilho Check man git-merge. Domyślnie polecenie git merge odmawia scalenia historii, które nie mają wspólnego przodka. Tej opcji można użyć, aby zastąpić to bezpieczeństwo podczas łączenia historii dwóch projektów, które rozpoczęły swoje życie niezależnie. Ponieważ jest to bardzo rzadka okazja, żadna zmienna konfiguracyjna domyślnie nie istnieje i nie zostanie dodana.
Radon Rosborough,

Powinny być dostępne git version 2.7.2.windows.1?
xpto

2
@MarceloFilho Zostało to dodane w wersji 2.9.0, ale w starszych wersjach nie powinieneś przekazywać opcji (to po prostu zadziała). github.com/git/git/blob/…
Radon Rosborough,

To działało dobrze. Byłem w stanie użyć gałęzi filtru do przepisania nazw plików tam, gdzie chciałem w drzewie przed scaleniem. Przypuszczam, że jest więcej pracy, jeśli musisz przenieść historię poza gałąź główną.
codeDr

9

Próbowałem robić to samo od kilku dni, używam git 2.7.2. Pod poddrzewo nie zachowuje historii.

Możesz użyć tej metody, jeśli nie będziesz ponownie używać starego projektu.

Sugerowałbym, aby najpierw rozgałęzić B i pracować w oddziale.

Oto kroki bez rozgałęziania:

cd B

# You are going to merge A into B, so first move all of B's files into a sub dir
mkdir B

# Move all files to B, till there is nothing in the dir but .git and B
git mv <files> B

git add .

git commit -m "Moving content of project B in preparation for merge from A"


# Now merge A into B
git remote add -f A <A repo url>

git merge A/<branch>

mkdir A

# move all the files into subdir A, excluding .git
git mv <files> A

git commit -m "Moved A into subdir"


# Move B's files back to root    
git mv B/* ./

rm -rf B

git commit -m "Reset B to original state"

git push

Jeśli teraz zalogujesz któryś z plików w podkatalogu A, otrzymasz pełną historię

git log --follow A/<file>

Ten post pomógł mi to zrobić:

http://saintgimp.org/2013/01/22/merging-two-git-repositories-into-one-repository-without-losing-file-history/


8

Jeśli chcesz umieścić pliki z gałęzi w repozytorium B w poddrzewie repozytorium A, a także zachować historię, czytaj dalej. (W poniższym przykładzie zakładam, że chcemy scalić gałąź master repo B w gałąź master repo A.)

W repozytorium A wykonaj najpierw następujące czynności, aby udostępnić repozytorium B:

git remote add B ../B # Add repo B as a new remote.
git fetch B

Teraz tworzymy nowy oddział (z tylko jednym zatwierdzeniem) w repozytorium A, które nazywamy new_b_root. Wynikowe zatwierdzenie będzie miało pliki, które zostały zatwierdzone w pierwszym zatwierdzeniu w gałęzi master repo B, ale umieszczone w podkatalogu o nazwie path/to/b-files/.

git checkout --orphan new_b_root master
git rm -rf . # Remove all files.
git cherry-pick -n `git rev-list --max-parents=0 B/master`
mkdir -p path/to/b-files
git mv README path/to/b-files/
git commit --date="$(git log --format='%ai' $(git rev-list --max-parents=0 B/master))"

Objaśnienie: --orphanOpcja polecenia checkout pobiera pliki z gałęzi master A, ale nie tworzy żadnego zatwierdzenia. Mogliśmy wybrać dowolny zatwierdzenie, ponieważ i tak usuwamy wszystkie pliki. Następnie, nie popełniając jeszcze ( -n), wybieramy pierwszy zatwierdzenie z gałęzi master B. (Cherry-pick zachowuje oryginalny komunikat zatwierdzenia, którego wydaje się, że nie wykonuje go proste pobranie). Następnie tworzymy poddrzewo, w którym chcemy umieścić wszystkie pliki z repozytorium B. Następnie musimy przenieść wszystkie pliki, które zostały wprowadzone w cherry-pick do poddrzewa. W powyższym przykładzie jest tylko READMEplik do przeniesienia. Następnie zatwierdzamy nasz główny zatwierdzenie B repo i jednocześnie zachowujemy znacznik czasu oryginalnego zatwierdzenia.

Teraz utworzymy nowy B/masteroddział na nowo utworzonym new_b_root. Nazywamy nowy oddział b:

git checkout -b b B/master
git rebase -s recursive -Xsubtree=path/to/b-files/ new_b_root

Teraz łączymy nasz boddział w A/master:

git checkout master
git merge --allow-unrelated-histories --no-commit b
git commit -m 'Merge repo B into repo A.'

Wreszcie możesz usunąć Bgałęzie zdalne i tymczasowe:

git remote remove B
git branch -D new_b_root b

Ostateczny wykres będzie miał następującą strukturę:

wprowadź opis zdjęcia tutaj


Świetna odpowiedź, dzięki! Naprawdę brakowało mi w innych odpowiedziach z „git subtree” lub „merge --allow-nonrelated-histories” z Andresch Serj, że podkatalog nie ma dziennika.
Ilendir

8

Zebrałem tutaj wiele informacji na temat Stack OverFlow itp. I udało mi się stworzyć skrypt, który rozwiązuje problem.

Zastrzeżenie polega na tym, że bierze ono pod uwagę tylko gałąź „rozwijania” każdego repozytorium i łączy je w osobny katalog w całkowicie nowym repozytorium.

Tagi i inne gałęzie są ignorowane - to może nie być to, czego chcesz.

Skrypt obsługuje nawet gałęzie funkcji i tagi - zmieniając ich nazwy w nowym projekcie, abyś wiedział, skąd pochodzą.

#!/bin/bash
#
################################################################################
## Script to merge multiple git repositories into a new repository
## - The new repository will contain a folder for every merged repository
## - The script adds remotes for every project and then merges in every branch
##   and tag. These are renamed to have the origin project name as a prefix
##
## Usage: mergeGitRepositories.sh <new_project> <my_repo_urls.lst>
## - where <new_project> is the name of the new project to create
## - and <my_repo_urls.lst> is a file contaning the URLs to the respositories
##   which are to be merged on separate lines.
##
## Author: Robert von Burg
##            eitch@eitchnet.ch
##
## Version: 0.3.2
## Created: 2018-02-05
##
################################################################################
#

# disallow using undefined variables
shopt -s -o nounset

# Script variables
declare SCRIPT_NAME="${0##*/}"
declare SCRIPT_DIR="$(cd ${0%/*} ; pwd)"
declare ROOT_DIR="$PWD"
IFS=$'\n'

# Detect proper usage
if [ "$#" -ne "2" ] ; then
  echo -e "ERROR: Usage: $0 <new_project> <my_repo_urls.lst>"
  exit 1
fi


## Script variables
PROJECT_NAME="${1}"
PROJECT_PATH="${ROOT_DIR}/${PROJECT_NAME}"
TIMESTAMP="$(date +%s)"
LOG_FILE="${ROOT_DIR}/${PROJECT_NAME}_merge.${TIMESTAMP}.log"
REPO_FILE="${2}"
REPO_URL_FILE="${ROOT_DIR}/${REPO_FILE}"


# Script functions
function failed() {
  echo -e "ERROR: Merging of projects failed:"
  echo -e "ERROR: Merging of projects failed:" >>${LOG_FILE} 2>&1
  echo -e "$1"
  exit 1
}

function commit_merge() {
  current_branch="$(git symbolic-ref HEAD 2>/dev/null)"
  if [[ ! -f ".git/MERGE_HEAD" ]] ; then
    echo -e "INFO:   No commit required."
    echo -e "INFO:   No commit required." >>${LOG_FILE} 2>&1
  else
    echo -e "INFO:   Committing ${sub_project}..."
    echo -e "INFO:   Committing ${sub_project}..." >>${LOG_FILE} 2>&1
    if ! git commit -m "[Project] Merged branch '$1' of ${sub_project}" >>${LOG_FILE} 2>&1 ; then
      failed "Failed to commit merge of branch '$1' of ${sub_project} into ${current_branch}"
    fi
  fi
}


# Make sure the REPO_URL_FILE exists
if [ ! -e "${REPO_URL_FILE}" ] ; then
  echo -e "ERROR: Repo file ${REPO_URL_FILE} does not exist!"
  exit 1
fi


# Make sure the required directories don't exist
if [ -e "${PROJECT_PATH}" ] ; then
  echo -e "ERROR: Project ${PROJECT_NAME} already exists!"
  exit 1
fi


# create the new project
echo -e "INFO: Logging to ${LOG_FILE}"
echo -e "INFO: Creating new git repository ${PROJECT_NAME}..."
echo -e "INFO: Creating new git repository ${PROJECT_NAME}..." >>${LOG_FILE} 2>&1
echo -e "===================================================="
echo -e "====================================================" >>${LOG_FILE} 2>&1
cd ${ROOT_DIR}
mkdir ${PROJECT_NAME}
cd ${PROJECT_NAME}
git init
echo "Initial Commit" > initial_commit
# Since this is a new repository we need to have at least one commit
# thus were we create temporary file, but we delete it again.
# Deleting it guarantees we don't have conflicts later when merging
git add initial_commit
git commit --quiet -m "[Project] Initial Master Repo Commit"
git rm --quiet initial_commit
git commit --quiet -m "[Project] Initial Master Repo Commit"
echo


# Merge all projects into the branches of this project
echo -e "INFO: Merging projects into new repository..."
echo -e "INFO: Merging projects into new repository..." >>${LOG_FILE} 2>&1
echo -e "===================================================="
echo -e "====================================================" >>${LOG_FILE} 2>&1
for url in $(cat ${REPO_URL_FILE}) ; do

  if [[ "${url:0:1}" == '#' ]] ; then
    continue
  fi

  # extract the name of this project
  export sub_project=${url##*/}
  sub_project=${sub_project%*.git}

  echo -e "INFO: Project ${sub_project}"
  echo -e "INFO: Project ${sub_project}" >>${LOG_FILE} 2>&1
  echo -e "----------------------------------------------------"
  echo -e "----------------------------------------------------" >>${LOG_FILE} 2>&1

  # Fetch the project
  echo -e "INFO:   Fetching ${sub_project}..."
  echo -e "INFO:   Fetching ${sub_project}..." >>${LOG_FILE} 2>&1
  git remote add "${sub_project}" "${url}"
  if ! git fetch --tags --quiet ${sub_project} >>${LOG_FILE} 2>&1 ; then
    failed "Failed to fetch project ${sub_project}"
  fi

  # add remote branches
  echo -e "INFO:   Creating local branches for ${sub_project}..."
  echo -e "INFO:   Creating local branches for ${sub_project}..." >>${LOG_FILE} 2>&1
  while read branch ; do
    branch_ref=$(echo $branch | tr " " "\t" | cut -f 1)
    branch_name=$(echo $branch | tr " " "\t" | cut -f 2 | cut -d / -f 3-)

    echo -e "INFO:   Creating branch ${branch_name}..."
    echo -e "INFO:   Creating branch ${branch_name}..." >>${LOG_FILE} 2>&1

    # create and checkout new merge branch off of master
    if ! git checkout -b "${sub_project}/${branch_name}" master >>${LOG_FILE} 2>&1 ; then failed "Failed preparing ${branch_name}" ; fi
    if ! git reset --hard ; then failed "Failed preparing ${branch_name}" >>${LOG_FILE} 2>&1 ; fi
    if ! git clean -d --force ; then failed "Failed preparing ${branch_name}" >>${LOG_FILE} 2>&1 ; fi

    # Merge the project
    echo -e "INFO:   Merging ${sub_project}..."
    echo -e "INFO:   Merging ${sub_project}..." >>${LOG_FILE} 2>&1
    if ! git merge --allow-unrelated-histories --no-commit "remotes/${sub_project}/${branch_name}" >>${LOG_FILE} 2>&1 ; then
      failed "Failed to merge branch 'remotes/${sub_project}/${branch_name}' from ${sub_project}"
    fi

    # And now see if we need to commit (maybe there was a merge)
    commit_merge "${sub_project}/${branch_name}"

    # relocate projects files into own directory
    if [ "$(ls)" == "${sub_project}" ] ; then
      echo -e "WARN:   Not moving files in branch ${branch_name} of ${sub_project} as already only one root level."
      echo -e "WARN:   Not moving files in branch ${branch_name} of ${sub_project} as already only one root level." >>${LOG_FILE} 2>&1
    else
      echo -e "INFO:   Moving files in branch ${branch_name} of ${sub_project} so we have a single directory..."
      echo -e "INFO:   Moving files in branch ${branch_name} of ${sub_project} so we have a single directory..." >>${LOG_FILE} 2>&1
      mkdir ${sub_project}
      for f in $(ls -a) ; do
        if  [[ "$f" == "${sub_project}" ]] ||
            [[ "$f" == "." ]] ||
            [[ "$f" == ".." ]] ; then
          continue
        fi
        git mv -k "$f" "${sub_project}/"
      done

      # commit the moving
      if ! git commit --quiet -m  "[Project] Move ${sub_project} files into sub directory" ; then
        failed "Failed to commit moving of ${sub_project} files into sub directory"
      fi
    fi
    echo
  done < <(git ls-remote --heads ${sub_project})


  # checkout master of sub probject
  if ! git checkout "${sub_project}/master" >>${LOG_FILE} 2>&1 ; then
    failed "sub_project ${sub_project} is missing master branch!"
  fi

  # copy remote tags
  echo -e "INFO:   Copying tags for ${sub_project}..."
  echo -e "INFO:   Copying tags for ${sub_project}..." >>${LOG_FILE} 2>&1
  while read tag ; do
    tag_ref=$(echo $tag | tr " " "\t" | cut -f 1)
    tag_name_unfixed=$(echo $tag | tr " " "\t" | cut -f 2 | cut -d / -f 3)

    # hack for broken tag names where they are like 1.2.0^{} instead of just 1.2.0
    tag_name="${tag_name_unfixed%%^*}"

    tag_new_name="${sub_project}/${tag_name}"
    echo -e "INFO:     Copying tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}..."
    echo -e "INFO:     Copying tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}..." >>${LOG_FILE} 2>&1
    if ! git tag "${tag_new_name}" "${tag_ref}" >>${LOG_FILE} 2>&1 ; then
      echo -e "WARN:     Could not copy tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}"
      echo -e "WARN:     Could not copy tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}" >>${LOG_FILE} 2>&1
    fi
  done < <(git ls-remote --tags --refs ${sub_project})

  # Remove the remote to the old project
  echo -e "INFO:   Removing remote ${sub_project}..."
  echo -e "INFO:   Removing remote ${sub_project}..." >>${LOG_FILE} 2>&1
  git remote rm ${sub_project}

  echo
done


# Now merge all project master branches into new master
git checkout --quiet master
echo -e "INFO: Merging projects master branches into new repository..."
echo -e "INFO: Merging projects master branches into new repository..." >>${LOG_FILE} 2>&1
echo -e "===================================================="
echo -e "====================================================" >>${LOG_FILE} 2>&1
for url in $(cat ${REPO_URL_FILE}) ; do

  if [[ ${url:0:1} == '#' ]] ; then
    continue
  fi

  # extract the name of this project
  export sub_project=${url##*/}
  sub_project=${sub_project%*.git}

  echo -e "INFO:   Merging ${sub_project}..."
  echo -e "INFO:   Merging ${sub_project}..." >>${LOG_FILE} 2>&1
  if ! git merge --allow-unrelated-histories --no-commit "${sub_project}/master" >>${LOG_FILE} 2>&1 ; then
    failed "Failed to merge branch ${sub_project}/master into master"
  fi

  # And now see if we need to commit (maybe there was a merge)
  commit_merge "${sub_project}/master"

  echo
done


# Done
cd ${ROOT_DIR}
echo -e "INFO: Done."
echo -e "INFO: Done." >>${LOG_FILE} 2>&1
echo

exit 0

Możesz go również uzyskać ze strony http://paste.ubuntu.com/11732805

Najpierw utwórz plik z adresem URL do każdego repozytorium, np .:

git@github.com:eitchnet/ch.eitchnet.parent.git
git@github.com:eitchnet/ch.eitchnet.utils.git
git@github.com:eitchnet/ch.eitchnet.privilege.git

Następnie wywołaj skrypt podając nazwę projektu i ścieżkę do skryptu:

./mergeGitRepositories.sh eitchnet_test eitchnet.lst

Sam skrypt ma wiele komentarzy, które powinny wyjaśniać, co robi.


Zamiast kierować czytelników do odpowiedzi, prosimy o opublikowanie odpowiedzi tutaj (inaczej edytuj to, co powiedziałeś w tym komentarzu, w tej odpowiedzi).
josliber

1
Jasne, po prostu pomyślałem, że lepiej się nie powtarzać ... =)
eitch

Jeśli uważasz, że to pytanie jest identyczne z drugim, możesz oflagować je jako duplikat, używając linku „flaga” pod samym pytaniem i wskazując drugie pytanie. Jeśli nie jest to zduplikowane pytanie, ale uważasz, że do rozwiązania obu problemów można użyć tej samej odpowiedzi, po prostu opublikuj tę samą odpowiedź na oba problemy (jak teraz). Dziękujemy za pomoc!
josliber

Niesamowity! Nie działał na Windows bash, ale działał bezbłędnie z Vagrant box z Ubuntu. Co za oszczędność czasu!
xverges


7

Wiem, że minęło wiele czasu, ale nie byłem zadowolony z innych odpowiedzi, które tu znalazłem, więc napisałem:

me=$(basename $0)

TMP=$(mktemp -d /tmp/$me.XXXXXXXX)
echo 
echo "building new repo in $TMP"
echo
sleep 1

set -e

cd $TMP
mkdir new-repo
cd new-repo
    git init
    cd ..

x=0
while [ -n "$1" ]; do
    repo="$1"; shift
    git clone "$repo"
    dirname=$(basename $repo | sed -e 's/\s/-/g')
    if [[ $dirname =~ ^git:.*\.git$ ]]; then
        dirname=$(echo $dirname | sed s/.git$//)
    fi

    cd $dirname
        git remote rm origin
        git filter-branch --tree-filter \
            "(mkdir -p $dirname; find . -maxdepth 1 ! -name . ! -name .git ! -name $dirname -exec mv {} $dirname/ \;)"
        cd ..

    cd new-repo
        git pull --no-commit ../$dirname
        [ $x -gt 0 ] && git commit -m "merge made by $me"
        cd ..

    x=$(( x + 1 ))
done

2
Właśnie tego szukałem. Dzięki! Musiałem jednak zmienić linię 22 na:if [[ $dirname =~ ^.*\.git$ ]]; then
heyman

2
^. * blarg $ jest marnotrawnie chciwym RE. Lepiej powiedzieć .blarg $ i pominąć przednią kotwicę.
jettero

7

Jeśli próbujesz po prostu skleić ze sobą dwa repozytoria, podmoduły i scalenia poddrzewa są niewłaściwym narzędziem do użycia, ponieważ nie zachowują całej historii plików (jak zauważyli inni w innych odpowiedziach). Zobacz tę odpowiedź tutaj, aby uzyskać prosty i poprawny sposób na zrobienie tego.


1
Twoje rozwiązanie działa dobrze tylko w przypadku nowego repozytorium, ale co powiesz na połączenie repozytorium w innym z konfliktami plików?
Andrey Izman,

6

Podjąłem podobne wyzwanie, ale w moim przypadku opracowaliśmy jedną wersję bazy kodu w repozytorium A, a następnie sklonowaliśmy ją w nowej repozytorium, repo B, dla nowej wersji produktu. Po naprawieniu kilku błędów w repozytorium A, musieliśmy przesłać FI zmian w repozytorium B. Skończyło się na tym, że:

  1. Dodanie pilota do repo B, który wskazywał na repo A (git remote add ...)
  2. Wyciąganie bieżącej gałęzi (nie używaliśmy mastera do naprawiania błędów) (git pull remoteForRepoA bugFixBranch)
  3. Pushing scala się do github

Pracowałem przysmak :)


5

Podobne do @Smar, ale korzysta ze ścieżek systemu plików, ustawionych w PODSTAWOWYM i WTÓRNYM:

PRIMARY=~/Code/project1
SECONDARY=~/Code/project2
cd $PRIMARY
git remote add test $SECONDARY && git fetch test
git merge test/master

Następnie ręcznie scalasz.

(dostosowano z postu Anar Manafov )


5

Scalanie 2 repozytoriów

git clone ssh://<project-repo> project1
cd project1
git remote add -f project2 project2
git merge --allow-unrelated-histories project2/master
git remote rm project2

delete the ref to avoid errors
git update-ref -d refs/remotes/project2/master

4

Jeśli chcesz scalić trzy lub więcej projektów w jednym zatwierdzeniu, wykonaj kroki opisane w innych odpowiedziach ( remote add -f, merge). Następnie (miękki) zresetuj indeks do starej głowy (gdzie nie nastąpiło scalenie). Dodaj wszystkie pliki ( git add -A) i zatwierdź je (komunikat „Scalanie projektów A, B, C i D w jeden projekt). To jest teraz identyfikator zatwierdzenia master.

Teraz utwórz .git/info/graftsz następującą zawartością:

<commit-id of master> <list of commit ids of all parents>

Uruchom git filter-branch -- head^..head head^2..head head^3..head. Jeśli masz więcej niż trzy gałęzie, po prostu dodaj tyle, head^n..headile masz gałęzi. Aby zaktualizować tagi, dołącz --tag-name-filter cat. Nie zawsze dodawaj to, ponieważ może to spowodować przepisanie niektórych zmian. Aby uzyskać szczegółowe informacje, zobacz stronę man gałęzi filter- search, wyszukaj „przeszczepy”.

Teraz do ostatniego zatwierdzenia przypisani są odpowiedni rodzice.


1
Zaraz, dlaczego miałbyś chcieć połączyć trzy projekty w jednym zatwierdzeniu?
Steve Bennett

Zacząłem od repozytorium, repozytorium-klienta i modelera jako oddzielnych projektów git. Było to trudne dla współpracowników, więc dołączyłem do nich w jednym projekcie git. Aby mieć możliwość, że „root” nowego projektu pochodzi z trzech innych projektów, chciałem mieć zatwierdzenie pojedynczej korespondencji seryjnej.
koppor

4

Aby scalić A w obrębie B:

1) W projekcie A

git fast-export --all --date-order > /tmp/ProjectAExport

2) W projekcie B

git checkout -b projectA
git fast-import --force < /tmp/ProjectAExport

W tej gałęzi wykonaj wszystkie operacje, które musisz wykonać, i zatwierdź je.

C) Następnie z powrotem do mistrza i klasyczne połączenie dwóch gałęzi:

git checkout master
git merge projectA

2

Ta funkcja sklonuje zdalne repo do lokalnego repozytorium, po scaleniu wszystkie commity zostaną zapisane, git logpokaże oryginalne commity i odpowiednie ścieżki:

function git-add-repo
{
    repo="$1"
    dir="$(echo "$2" | sed 's/\/$//')"
    path="$(pwd)"

    tmp="$(mktemp -d)"
    remote="$(echo "$tmp" | sed 's/\///g'| sed 's/\./_/g')"

    git clone "$repo" "$tmp"
    cd "$tmp"

    git filter-branch --index-filter '
        git ls-files -s |
        sed "s,\t,&'"$dir"'/," |
        GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &&
        mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
    ' HEAD

    cd "$path"
    git remote add -f "$remote" "file://$tmp/.git"
    git pull "$remote/master"
    git merge --allow-unrelated-histories -m "Merge repo $repo into master" --edit "$remote/master"
    git remote remove "$remote"
    rm -rf "$tmp"
}

Jak używać:

cd current/package
git-add-repo https://github.com/example/example dir/to/save

Jeśli wprowadzisz małe zmiany, możesz nawet przenieść pliki / katalogi scalonego repozytorium na różne ścieżki, na przykład:

repo="https://github.com/example/example"
path="$(pwd)"

tmp="$(mktemp -d)"
remote="$(echo "$tmp" | sed 's/\///g' | sed 's/\./_/g')"

git clone "$repo" "$tmp"
cd "$tmp"

GIT_ADD_STORED=""

function git-mv-store
{
    from="$(echo "$1" | sed 's/\./\\./')"
    to="$(echo "$2" | sed 's/\./\\./')"

    GIT_ADD_STORED+='s,\t'"$from"',\t'"$to"',;'
}

# NOTICE! This paths used for example! Use yours instead!
git-mv-store 'public/index.php' 'public/admin.php'
git-mv-store 'public/data' 'public/x/_data'
git-mv-store 'public/.htaccess' '.htaccess'
git-mv-store 'core/config' 'config/config'
git-mv-store 'core/defines.php' 'defines/defines.php'
git-mv-store 'README.md' 'doc/README.md'
git-mv-store '.gitignore' 'unneeded/.gitignore'

git filter-branch --index-filter '
    git ls-files -s |
    sed "'"$GIT_ADD_STORED"'" |
    GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &&
    mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
' HEAD

GIT_ADD_STORED=""

cd "$path"
git remote add -f "$remote" "file://$tmp/.git"
git pull "$remote/master"
git merge --allow-unrelated-histories -m "Merge repo $repo into master" --edit "$remote/master"
git remote remove "$remote"
rm -rf "$tmp"

Powiadomienia
Ścieżki zamieniają się przez sed, więc upewnij się, że po scaleniu poruszały się we właściwych ścieżkach.
Ten --allow-unrelated-historiesparametr istnieje tylko od git> = 2,9.


1

Podane polecenie jest najlepszym możliwym rozwiązaniem, które sugeruję.

git subtree add --prefix=MY_PROJECT git://github.com/project/my_project.git master

1

Scalam projekty lekko ręcznie, co pozwala mi uniknąć konieczności radzenia sobie z konfliktami scalania.

najpierw skopiuj pliki z innego projektu, tak jak chcesz.

cp -R myotherproject newdirectory
git add newdirectory

następny ciąg historii

git fetch path_or_url_to_other_repo

powiedz git, aby połączył się w historii ostatnio pobranej rzeczy

echo 'FETCH_HEAD' > .git/MERGE_HEAD

teraz zatwierdzaj, ale normalnie to robisz

git commit

0

Chciałem przenieść mały projekt do podkatalogu większego. Ponieważ mój mały projekt nie miał wielu zobowiązań, skorzystałem z niego git format-patch --output-directory /path/to/patch-dir. Potem wykorzystałem większy projekt git am --directory=dir/in/project /path/to/patch-dir/*.

Ten czuje sposób mniej przerażające i sposób bardziej czystsze niż w branży filtracyjnej. To prawda, że ​​może nie mieć zastosowania do wszystkich przypadków.

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.