Слияние Репозиториев В Git
Есть два репозитория. Задача - слить их так, чтобы файлы из одного репозитория попали в папку в другом, и при этом сохранилась вся история.
Я не нашёл в сети полного описания всех шагов (может быть, плохо искал), и поэтому решил набить памятку, скомпилированную из разных источников и собственных экспериментов.
Суть проблемы
Допустим, у нас есть два проекта, каждый из которых живёт в отдельном репозитории.
Назовём первый проект libfoo. Это архиполезная библиотека с долгой и знатной историей.
zloddey@zlosung:~/projects/libfoo$ git log --pretty=oneline
2e0560e5c43ec63096f975ea534c23d58274a341 Fix critical bug
f0d99354dd3140d826b7e77cfb4e72f251dcf81e Awesome Foo library
В ней много файлов, все из которых нам очень важны.
libfoo
∟ include
∟ foo.h
∟ src
∟ foo.c
Второй проект - это mainproject, убийца Facebook, Twitter и (внезапно) Wikipedia.
zloddey@zlosung:~/projects/mainproject$ git log --pretty=oneline
88ed3223758ea3171ba4fccaf0098c85db38d348 Kill'em all!
Он тоже очень большой
mainproject
∟ src
∟ main.c
И после трёх лет разработки мы поняли, что нам жизненно необходимо слить оба репозитория в один. А именно, переместить библиотеку в репозиторий проекта (в новую папку libfoo). Разумеется, при этом хочется сохранить всю историю изменений.
Disclaimer
В этой записи я провожу все изменения прямо в самих репозиториях. Но при работе с реальными репозиториями я крайне рекомендую проводить подобные манипуляции исключительно с копиями Ваших репозиториев. Несмотря на то, что git в большинстве случаев старается сохранить пользовательские данные, некоторые из используемых нами команд способны бесследно удалить содержимое репозитория.
Поэтому проводите все эксперименты на копиях рабочих репозиториев. Тем более, что клоны в git создаются очень быстро. А когда все манипуляции будут завершены, то репозитории можно синхронизировать через push
/pull
.
В общем, я считаю, что предупредил Вас. Берегите свои данные!
Переписываем путь к файлам
Первый шаг на пути к слиянию репозиториев в сладостном экстазе - это изменение пути к файлам в репозитории с библиотекой. Если сейчас файлы лежат в корне, то после наших манипуляций они должны переместиться в папку libfoo (которая будет расположена внутри репозитория libfoo).
Добиться этого нам поможет термоядерная команда git filter-branch
. Она предназначена для активного редактирования истории в духе “1984”. История изменяется непосредственно через изменение (точнее, пересоздание) коммитов, поэтому все правки необратимы. Неудачное применение filter-branch
может отправить Ваш репозиторий на свалку.
Поэтому внимательно следите за руками:
zloddey@zlosung:~/projects/libfoo$ git filter-branch --tree-filter 'mkdir libfoo; ls | grep -v libfoo | xargs -I{} mv {} libfoo' master
Rewrite 2e0560e5c43ec63096f975ea534c23d58274a341 (2/2)
Ref 'refs/heads/master' was rewritten
Флаг --tree-filter
командует git-у, что следующая за ним команда должна быть применена к каждому коммиту в цепочке (в нашем случае ко всей ветке master). Фильтрация пройдёт успешно только в том случае, когда команда завершается со статусом 0, и все коммиты удаётся наложить.
Сама команда шелла тоже состоит из двух частей. Сначала мы создаём папку libfoo. Затем перемещаем в неё все файлы, за исключением самой libfoo. Типичный пример мощи шелла: с помощью пайпа и чьей-то матери составляем из базовых команд сложное действие, которое сделает за человека всю монотонную работу.
Если мы не ошиблись в наборе команды, то git перепишет все коммиты репозитория и явит нам новую, улучшенную библиотеку libfoo:
libfoo
∟ libfoo
∟ include
∟ foo.h
∟ src
∟ foo.c
При этом сохранится вся история изменений (хотя, разумеется, изменятся хэши коммитов):
zloddey@zlosung:~/projects/libfoo$ git log --pretty=o
a98d0b1b6196831bf85f24517ece58a74317827b Fix critical bug
0286c0c223c611c35b8df9049a33189ab2e3d72c Awesome Foo library
При желании можно скастовать git log
на любой конкретный файл и убедиться, что с его историей тоже всё в порядке.
Теперь можно заняться и самим слиянием.
Объединяем репозитории
Осталось совсем немного. Переходим в наш уберпроект mainproject и вытягиваем все коммиты из изменённого репозитория библиотеки:
zloddey@zlosung:~/projects/mainproject$ git remote add libfoo-source ../libfoo/
zloddey@zlosung:~/projects/mainproject$ git fetch libfoo-source
warning: no common commits
remote: Counting objects: 11, done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 11 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (11/11), done.
From ../libfoo
* [new branch] master -> libfoo-source/master
git будет удивлён, что удалённый репозиторий не имеет с нашим общих коммитов, и предупредит нас об этом. Не стоит беспокоиться - всё идёт по плану.
Однако сам план зависит от того, как именно Вы хотите слить две ветки. Это зависит, главным образом, от принятой в Вашей команде политики управления ветками в репозитории.
Можно пойти по Linus-way и слить две ветки через обычный merge. В таком случае создастся коммит, у которого будет два предка: один из libfoo, и один из mainproject:
zloddey@zlosung:~/projects/mainproject$ git branch
* master
zloddey@zlosung:~/projects/mainproject$ git merge libfoo-source/master
Merge made by the 'recursive' strategy.
libfoo/include/foo.h | 1 +
1 file changed, 1 insertion(+)
create mode 100644 libfoo/include/foo.h
create mode 100644 libfoo/src/foo.c
zloddey@zlosung:~/projects/mainproject$ git log --graph --pretty=oneline
* 6c15d2bbaab1ee5751f345ac67a9a1b2400b8476 Merge remote-tracking branch 'libfoo-source/master'
|\
| * a98d0b1b6196831bf85f24517ece58a74317827b Fix critical bug
| * 0286c0c223c611c35b8df9049a33189ab2e3d72c Awesome Foo library
* 88ed3223758ea3171ba4fccaf0098c85db38d348 Kill'em all!
Есть и другой вариант. Например, мы на своём рабочем проекте запретили коммиты с несколькими родителями (ради простой линейной истории коммитов). В таком случае надо сливать ветки через rebase
.
zloddey@zlosung:~/projects/mainproject$ git branch
* master
zloddey@zlosung:~/projects/mainproject$ git checkout libfoo-source/master
Note: checking out 'libfoo-source/master'.
(всякая болтовня git)
HEAD is now at a98d0b1... Fix critical bug
zloddey@zlosung:~/projects/mainproject$ git checkout -b new-master
Switched to a new branch 'new-master'
zloddey@zlosung:~/projects/mainproject$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: Awesome Foo library
Applying: Fix critical bug
zloddey@zlosung:~/projects/mainproject$ git checkout master
Switched to branch 'master'
zloddey@zlosung:~/projects/mainproject$ git merge new-master
Updating 88ed322..3641759
Fast-forward
libfoo/include/foo.h | 1 +
1 file changed, 1 insertion(+)
create mode 100644 libfoo/include/foo.h
create mode 100644 libfoo/src/foo.c
zloddey@zlosung:~/projects/mainproject$ git branch -d new-master
Deleted branch new-master (was 3641759).
zloddey@zlosung:~/projects/mainproject$ git log --graph --pretty=o
* 36417598b9e390c15f9671e8940fc42392b5a4fa Fix critical bug
* d0934618c8316f5190ad57b6ebc67def61a0c096 Awesome Foo library
* 88ed3223758ea3171ba4fccaf0098c85db38d348 Kill'em all!
И в любом случае не забываем подчистить удалённый репозиторий - он нам больше не нужен!
zloddey@zlosung:~/projects/mainproject$ git remote rm libfoo-source
Что в итоге
Мы получили, что хотели: в репозитории mainproject появилась папка libfoo со всеми файлами из проекта libfoo (надеюсь, Вы действительно хотели именно этого!)
mainproject
∟ libfoo
∟ include
∟ foo.h
∟ src
∟ foo.c
∟ src
∟ main.c
При этом libfoo появился у нас не одним большим блобом, а в виде оригинальной последовательности коммитов. Так что, когда найдётся следующий баг в этой библиотеке, поиск через git blame
укажет на реального автора косяка, а не на человека, который сливал репозитории (впрочем, если это одно и то же лицо, я умываю руки).
Мораль всей басни: переписывайте свою историю и упорядочивайте репозитории на здоровье, но только помните:
(Картинка отсюда)