Scal repozytorium git w podkatalogu

85

Chciałbym scalić zdalne repozytorium git w moim działającym repozytorium git jako jego podkatalog. Chciałbym, aby powstałe repozytorium zawierało połączoną historię dwóch repozytoriów, a także, aby każdy plik scalonego repozytorium zachował swoją historię tak, jak w zdalnym repozytorium. Próbowałem użyć strategii poddrzewa, jak wspomniano w Jak używać strategii scalania poddrzewa , ale po wykonaniu tej procedury, chociaż wynikowe repozytorium rzeczywiście zawiera scaloną historię dwóch repozytoriów, pojedyncze pliki pochodzące ze zdalnego nie zachowały swojej historii (`git log 'na którymkolwiek z nich pokazuje po prostu komunikat" Scalona gałąź ... ").

Nie chcę też używać modułów podrzędnych, ponieważ nie chcę, aby dwa połączone repozytoria git były już oddzielne.

Czy możliwe jest połączenie zdalnego repozytorium git w innym jako podkatalog z pojedynczymi plikami pochodzącymi ze zdalnego repozytorium, zachowując ich historię?

Bardzo dziękuję za pomoc.

EDYCJA: Obecnie wypróbowuję rozwiązanie, które używa git filter-branch do przepisania historii scalonego repozytorium. Wygląda na to, że działa, ale muszę to jeszcze przetestować. Wrócę do raportu z moich ustaleń.

EDYCJA 2: W nadziei, że wyjaśnię siebie, podaję dokładne polecenia, których użyłem w strategii poddrzewa git, co powoduje widoczną utratę historii plików zdalnego repozytorium. Niech A będzie repozytorium git, w którym obecnie pracuję, a B repozytorium git, które chciałbym włączyć do A jako jego podkatalog. Wykonał następujące czynności:

git remote add -f B <url-of-B>
git merge -s ours --no-commit B/master
git read-tree --prefix=subdir/Iwant/to/put/B/in/ -u B/master
git commit -m "Merge B as subdirectory in subdir/Iwant/to/put/B/in."

Po tych poleceniach i przejściu do katalogu subdir / Iwant / to / put / B / in, widzę wszystkie pliki B, ale git logna każdym z nich wyświetla się tylko komunikat zatwierdzenia „Scal B jako podkatalog w podkatalogu / Iwant / to / put /Kosz." Historia ich plików, jaka jest w B, zostaje utracona.

To, co wydaje się działać (ponieważ jestem początkującym w git, mogę się mylić) jest następujące:

git remote add -f B <url-of-B>
git checkout -b B_branch B/master  # make a local branch following B's master
git filter-branch --index-filter \ 
   'git ls-files -s | sed "s-\t\"*-&subdir/Iwant/to/put/B/in/-" |
        GIT_INDEX_FILE=$GIT_INDEX_FILE.new \
                git update-index --index-info &&
        mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"' HEAD 
git checkout master
git merge B_branch

Powyższe polecenie dla filter-branch pochodzi z git help filter-branch, w którym zmieniłem tylko ścieżkę subdir.

christosc
źródło
Co gitkmówi o historii? W przeszłości z powodzeniem korzystałem z funkcji scalania poddrzewa git. Być może możesz ujawnić swoje dokładne polecenia? Nie jestem pewien, czy git-filter-branch to właściwe podejście. Mogę polecić wypróbowanie git-fast-export i git-fast-import, aby zsyntetyzować nową historię.
Seth Robertson
Po wykonaniu procedury poddrzewa gitkpokazuje dwa repozytoria połączone na swoich końcówkach i niepowiązane w ich początkowych zatwierdzeniach. (Czy pomogłoby, gdybym opublikował zrzuty ekranu widoku historii gitk? Czy mogę?) Niestety, poszczególne pliki zdalnego repozytorium nie zachowały swojej historii, jeśli to zrobiłem w terminalu git log <file-from-remote-repo>. Patrzę na git-fast-exporti git-fast-import; Jestem nowy w git. Zmienię moje pytanie, aby dokładnie pokazać, jakich poleceń użyłem w poddrzewie git. Bardzo dziękuję za odpowiedź.
Christosc
@christosc: Twoja druga metoda zadziałała pięknie i bardzo prosto. Dziękuję bardzo! Po prostu musiałem zmienić subdir / Iwant / to / put / B / in / i uczynić go onelinerem (ponieważ msysgit w systemie Windows wydaje się nie obsługiwać zwrotów linii w poleceniach z): git filter-branch --index-filter 'git ls-files -s | sed "s- \ t \" * - & subdir / Iwant / to / put / B / in / - "| GIT_INDEX_FILE = $ GIT_INDEX_FILE.new git update-index --index-info && mv" $ GIT_INDEX_FILE.new "" $ GIT_INDEX_FILE "'HEAD
wylewny
@ user1121352 Cieszę się, że mogłem ci pomóc.
christosc
Zwykle podążam za tą odpowiedzią: stackoverflow.com/a/1684694/207791
Victor Sergienko

Odpowiedzi:

40

Po uzyskaniu pełniejszego wyjaśnienia tego, co się dzieje, myślę, że to rozumiem, aw każdym razie na dole mam obejście. W szczególności uważam, że to, co się dzieje, polega na tym, że wykrywanie zmiany nazwy jest oszukiwane przez scalanie poddrzewa z --prefix. Oto mój przypadek testowy:

mkdir -p z/a z/b
cd z/a
git init
echo A>A
git add A
git commit -m A
echo AA>>A
git commit -a -m AA
cd ../b
git init
echo B>B
git add B
git commit -m B
echo BB>>B
git commit -a -m BB
cd ../a
git remote add -f B ../b
git merge -s ours --no-commit B/master
git read-tree --prefix=bdir -u B/master
git commit -m "subtree merge B into bdir"
cd bdir
echo BBB>>B
git commit -a -m BBB

Tworzymy katalogi git aib z kilkoma zatwierdzeniami w każdym. Dokonujemy scalenia poddrzewa, a następnie wykonujemy ostateczne zatwierdzenie w nowym poddrzewie.

Bieg gitk(w z / a) pokazuje, że historia się pojawia, możemy ją zobaczyć. Bieg git logpokazuje, że historia się pojawia. Jednak spojrzenie na konkretny plik ma problem: git log bdir/B

Cóż, jest sztuczka, którą możemy zagrać. Możemy przyjrzeć się historii przed zmianą nazwy konkretnego pliku za pomocą --follow. git log --follow -- B. Jest to dobre, ale nie świetne, ponieważ nie łączy historii przed scaleniem z po scaleniu.

Próbowałem grać z -M i -C, ale nie byłem w stanie zmusić go do śledzenia jednego konkretnego pliku.

Wydaje mi się, że rozwiązaniem jest poinformowanie gita o zmianie nazwy, która będzie miała miejsce w ramach scalania poddrzewa. Niestety drzewo git-read-tree jest dość wybredne, jeśli chodzi o scalanie poddrzewów, więc musimy przejść przez katalog tymczasowy, ale może to zniknąć przed zatwierdzeniem. Następnie możemy zobaczyć pełną historię.

Najpierw utwórz repozytorium „A” i wykonaj kilka zatwierdzeń:

mkdir -p z/a z/b
cd z/a
git init
echo A>A
git add A
git commit -m A
echo AA>>A
git commit -a -m AA

Po drugie, utwórz repozytorium „B” i wykonaj kilka zatwierdzeń:

cd ../b
git init
echo B>B
git add B
git commit -m B
echo BB>>B
git commit -a -m BB

I sztuczka, aby to zadziałało : zmusić Gita do rozpoznania zmiany nazwy, tworząc podkatalog i przenosząc do niego zawartość.

mkdir bdir
git mv B bdir
git commit -a -m bdir-rename

Wróć do repozytorium „A” i pobierz i scal zawartość „B”:

cd ../a
git remote add -f B ../b
git merge -s ours --no-commit B/master
# According to Alex Brown and pjvandehaar, newer versions of git need --allow-unrelated-histories
# git merge -s ours --allow-unrelated-histories --no-commit B/master
git read-tree --prefix= -u B/master
git commit -m "subtree merge B into bdir"

Aby pokazać, że są teraz połączone:

cd bdir
echo BBB>>B
git commit -a -m BBB

Aby udowodnić, że pełna historia jest zachowana w połączonym łańcuchu:

git log --follow B

Po wykonaniu tej czynności uzyskujemy historię, ale problem polega na tym, że jeśli faktycznie przechowujesz stare repozytorium „b” i od czasu do czasu łączysz się z nim (powiedzmy, że jest to w rzeczywistości oddzielnie obsługiwane repozytorium strony trzeciej), masz kłopoty, ponieważ ta strona trzecia nie dokonał zmiany nazwy. Musisz spróbować scalić nowe zmiany ze swoją wersją b ze zmianą nazwy i obawiam się, że to nie pójdzie gładko. Ale jeśli b odchodzi, wygrywasz.

Seth Robertson
źródło
Rzeczywiście, to działa @Seth! I nie musiałem uciekać się do przepisywania historii, tak jak w przypadku filter-branch, co tworzy nieco zwodniczą historię (np. Podczas przeglądania git log --stat). Nie zauważyłem też --followzmiany w dokumentacji git log; wydaje się bardzo przydatne przy zmianie nazw. Bardzo dziękuję za tak szczegółową i pouczającą odpowiedź!
christosc
2
Ta odpowiedź byłaby znacznie bardziej pomocna, gdyby przykładowy kod został podzielony na czytelne wiersze zamiast pojedynczej, rozdzielonej średnikami, linijki. ;)
jwadsack
Chciałbym połączyć „b” w „a” z zachowaniem pełnej historii. Jak mogłem to zrobić?
emeraldhieu
3
Zobacz stackoverflow.com/questions/37937984/…, aby znaleźć poprawkę
Alex Brown
3
Jak wspomniano w @AlexBrown, w nowych wersjach gittego programu fatal: refusing to merge unrelated historiesnależy się uruchomić git merge -s ours --allow-unrelated-histories --no-commit B/master.
pjvandehaar
61

git-subtreejest skryptem zaprojektowanym do dokładnie tego przypadku użycia, polegającego na scalaniu wielu repozytoriów w jedno przy jednoczesnym zachowaniu historii (i / lub dzieleniu historii poddrzew, chociaż wydaje się to nie mieć związku z tym pytaniem). Jest dystrybuowany jako część drzewa git od wersji 1.7.11 .

Aby scalić repozytorium <repo>w rewizji <rev>jako podkatalog <prefix>, użyj git subtree addw następujący sposób:

git subtree add -P <prefix> <repo> <rev>

git-subtree implementuje strategię scalania poddrzewa w bardziej przyjazny dla użytkownika sposób.

Minusem jest to, że w historii połączonego pliki są prefiksu (nie w podkatalogu). Powiedzmy, że scalasz repozytorium az b. W rezultacie git log a/f1pokażą Ci wszystkie zmiany (jeśli istnieją) oprócz tych w połączonej historii. Możesz to zrobić:

git log --follow -- f1

ale to nie pokaże zmian innych niż w połączonej historii.

Innymi słowy, jeśli nie zmieniasz aplików w repozytorium b, musisz to określić--follow ścieżkę bez prefiksu. Jeśli zmienisz je w obu repozytoriach, masz 2 polecenia, z których żadne nie pokazuje wszystkich zmian.

Więcej na ten temat tutaj .

kynan
źródło
Miły! To jest dokładnie to, czego potrzebowałem w jednej linii. Dzięki, przyszłość!
iameli
Jest to idealne rozwiązanie do scalenia innego repozytorium z moim repozytorium w podrzędnym kierunku.
eitch
1
Zauważ, że to nie zadziała z istniejącymi podkatalogami w <prefix>. Np. W celu scalenia podkatalogu, który został kiedyś ręcznie przeniesiony do własnego repozytorium i chcesz go ponownie scalić.
Richard Kiefer
8

Chciałbym

  1. zachować liniową historię bez jawnego scalania i
  2. sprawiają, że wygląda na to, że pliki scalonego repozytorium zawsze istniały w podkatalogu, a jako efekt uboczny git log -- filedziałają bez --follow.

Krok 1 : Przepisz historię w repozytorium źródłowym, aby wyglądało na to, że wszystkie pliki zawsze istniały poniżej podkatalogu.

Utwórz tymczasową gałąź dla przepisanej historii.

git checkout -b tmp_subdir

Następnie użyj git filter-branchzgodnie z opisem w Jak mogę przepisać historię, aby wszystkie pliki, z wyjątkiem tych, które zostały już przeniesione, znajdowały się w podkatalogu? :

git filter-branch --prune-empty --tree-filter '
if [ ! -e foo/bar ]; then
    mkdir -p foo/bar
    git ls-tree --name-only $GIT_COMMIT | xargs -I files mv files foo/bar
fi'

Krok 2 : Przełącz się do repozytorium docelowego. Dodaj repozytorium źródłowe jako zdalne w repozytorium docelowym i pobierz jego zawartość.

git remote add sourcerepo .../path/to/sourcerepo
git fetch sourcerepo

Krok 3 : Użyj, merge --ontoaby dodać zatwierdzenia przepisanego repozytorium źródłowego na górze repozytorium docelowego.

git rebase --preserve-merges --onto master --root sourcerepo/tmp_subdir

Możesz sprawdzić dziennik, aby zobaczyć, że naprawdę dostałeś to, czego chciałeś.

git log --stat

Krok 4 : Po rebase jesteś w stanie „odłączonej głowy”. Możesz przewinąć mastera do nowej głowy.

git checkout -b tmp_merged
git checkout master
git merge tmp_merged
git branch -d tmp_merged

Krok 5 : Na koniec trochę porządkowania: Usuń tymczasowego pilota.

git remote rm sourcerepo
hfs
źródło
git rebasewygląda na to, że nie zezwala razem na określone opcje: "błąd: nie można łączyć opcji interaktywnych (--interactive, --exec, --rebase-scales, --preserve-scales, --keep-empty, --root + - -onto) z opcjami am (--committer-date-is-author-date) "
Sam
Ciekawy! Spróbuj upuścić --committer-date-is-author-date. Sprawdzanie niezgodnych opcji zostało ostatnio dodane w git v2.19.0 ( github.com/git/git/commit/ ... ). Z opisu wynika, że ​​i tak --committer-date-is-author-datezostało po cichu zignorowane.
hfs
Zamiast używać starego filter-branchpolecenia, użyj git filter-repo --to-subdirectory-filter <dir>, jest to znacznie szybsze i łatwiejsze.
Willem
5

Jeśli naprawdę chcesz zszyć rzeczy razem, spójrz na przeszczepianie. Powinieneś także używać git rebase --preserve-merges --onto. Istnieje również możliwość zachowania daty autora dla informacji o autorach.

Adam Dymitruk
źródło
@adymitruk Dziękuję za odpowiedź. Jestem naprawdę nowy w git, więc przyjrzę się rozwiązaniu, które proponujesz. Próbowałem git filter-branchi wydaje się, że działa, ale może twój jest lepszy. Wypróbuję to.
Christosc
@adymitruk Czy mogę używać rebase z dwoma repozytoriami, które nie są ze sobą powiązane jako gałęzie? Chodzi mi o to, że dwa repozytoria, które chcę scalić, nie mają wspólnych początkowych zatwierdzeń ...
christosc
Dzięki @adymitruk. Nie byłem pewien, czy ponowne bazowanie można wykonać za pomocą dwóch niepowiązanych repozytoriów. Na pewno się przyda…
christosc
Ale nie bój się gałęzi filtra. To nas uratowało wiele razy. Po prostu utwórz wcześniej inną gałąź i zawsze możesz wrócić. To lub użyj reflogu.
Adam Dymitruk
Rozumiem… W każdym razie lepiej poczytajmy trochę dokumentacji na temat tych koncepcji i poleceń gita. Mając tylko niewielkie doświadczenie w VCS, a mianowicie svn, jestem trochę przytłoczony przez git. Jednak jego moc wydaje się być tego warta.
christosc
4

Uważam, że następujące rozwiązanie jest dla mnie wykonalne. Najpierw przechodzę do projektu B, tworzę nową gałąź, w której już wszystkie pliki zostaną przeniesione do nowego podkatalogu. Następnie pcham tę nową gałąź do początku. Następnie idę do projektu A, dodaję i pobieram pilota B, potem wyewidencjonuję przeniesioną gałąź, wracam do mastera i scalam:

# in local copy of project B
git checkout -b prepare_move
mkdir subdir
git mv <files_to_move> subdir/
git commit -m 'move files to subdir'
git push origin prepare_move

# in local copy of project A
git remote add -f B_origin <remote-url>
git checkout -b from_B B_origin/prepare_move
git checkout master
git merge from_B

Jeśli przejdę do podkatalogu subdir, mogę użyć git log --followi nadal mam historię.

Nie jestem ekspertem od git, więc nie mogę komentować, czy jest to szczególnie dobre rozwiązanie, czy ma zastrzeżenia, ale na razie wydaje się wszystko w porządku.

0__
źródło
Wygląda na to, że ludzie popierają to podejście tutaj: stackoverflow.com/questions/1683531/…
nacross
3

Czy próbowałeś dodać dodatkowe repozytorium jako moduł podrzędny git? Nie połączy historii z repozytorium zawierającym, w rzeczywistości będzie to niezależne repozytorium.

Wspominam o tym, bo tego nie zrobiłeś.

Abizern
źródło
1
Dzięki za odpowiedź Abizern. Właściwie chcę, aby dwie historie repozytoriów zostały połączone w jedną; Nie chcę, aby były już oddzielne, dlatego nie wspomniałem o modułach podrzędnych.
Christosc
1

Powiedzmy, że chcesz scalić repozytorium az b(zakładam, że znajdują się obok siebie):

cd a
git filter-repo --to-subdirectory-filter a
cd ..
cd b
git remote add a ../a
git fetch a
git merge --allow-unrelated-histories a/master
git remote remove a

Do tego trzeba git-filter-repozainstalować ( filter-branchjest zniechęcony ).

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

Więcej na ten temat tutaj .

x-yuri
źródło
0

Podobnie jak odpowiedź hfs, której chciałem

  • zachować liniową historię bez jawnego scalania i
  • sprawiają, że wygląda na to, że pliki scalonego repozytorium zawsze istniały w podkatalogu, a jako efekt uboczny git log -- filedziałają bez --follow.

Jednak wybrałem nowocześniejszy filter-repo(zakładając, że newrepozytorium istnieje i jest sprawdzone):

git clone git@host/repo/old.git
cd old
git checkout -b tmp_subdir
git filter-repo --to-subdirectory-filter old

cd ../new
git remote add old ../old
git fetch old
git rebase --rebase-merges --onto main --root old/tmp_subdir --committer-date-is-author-date

możesz potrzebować naprawić konflikty (ręcznie) lub zmienić polecenie rebase, aby zawierało, --merge -s recursive -X theirsjeśli chcesz spróbować rozwiązać problem za pomocą theirswersji:

git rebase --rebase-merges --onto main --root old/tmp_subdir --committer-
date-is-author-date --merge -s recursive -X theirs

kończysz na odłączonym HEAD, więc stwórz nową gałąź i połącz ją z główną uwagą, że nowoczesne repozytoria nie powinny używać gałęzi „master”, ale „main”

branch for a more inclusive language.
git checkout -b old_merge
git checkout main
git merge old_merge

sprzątać

git branch -d old_merge
git remote rm old
Oblepiony
źródło