Как объединить хранилища Git в линейную историю?

У меня есть два репозитория git R1 и R2, которые содержат коммиты от двух периодов разработки продукта: 1995-1997 и 1999-2013 гг. (Я создал их путем преобразования существующих репозиториев RCS и CVS в Git.)

R1:
A---B---C---D

R2:
K---L---M---N

Как я могу объединить два репозитория в один, содержащий точное представление о линейной истории проекта?

A---B---C---D---K---L---M---N

Обратите внимание, что между R1 и R2 файлами были добавлены, удалены и переименованы.

Я попытался создать пустой репозиторий и затем слить их содержимое на него.

git remote add R1 /vol/R1.git
git fetch R1

git remote add R2 /vol/R2.git
git fetch R2

git merge --strategy=recursive --strategy-option=theirs R1
git merge --strategy=recursive --strategy-option=theirs R2

Однако это оставляет в конце файлы, которые были в редакции D, но не в редакции K. Я мог бы синтезировать фиксацию для удаления дополнительных файлов между слияниями, но это кажется мне неэлегантным. Кроме того, благодаря такому подходу конечный результат содержит слияния, которые на самом деле не произошло.

Ответ 1

Использование git ветки фильтра

Используя трюк прямо с git-filter-branch man page:

Сначала создайте новый репозиторий с двумя оригинальными в качестве пультов, как и раньше. Я предполагаю, что оба используют имя ветки "master" .

git init repo
cd repo
git remote add R1 /vol/R1.git
git fetch R1
git remote add R2 /vol/R2.git
git fetch R2

Далее, укажите "ведущий" (текущая ветвь) на вершину R2 "master" .

git reset --hard R2/master

Теперь мы можем перенести историю R1 "master" в начало.

git filter-branch --parent-filter 'sed "s_^\$_-p R1/master_"' HEAD

Другими словами, мы вставляем фальшивое родительское соглашение между D и K, чтобы новая история выглядела следующим образом:

A---B---C---D---K---L---M---N

Единственное изменение в K через N заключается в том, что указатель родительского указателя K изменяется, и, следовательно, все идентификаторы SHA-1 меняются. Сообщение фиксации, автор, метка времени и т.д. Остаются неизменными.

Объединение более двух репозиториев вместе с ветвью фильтров

Если у вас есть более двух репозиториев, скажите R1 (самый старый) через R5 (самый новый), просто повторяйте команды git reset и git filter-branch в хронологическом порядке.

PARENT_REPO=R1
for CHILD_REPO in R2 R3 R4 R5; do
    git reset --hard $CHILD_REPO/master
    git filter-branch --parent-filter 'sed "s_^\$_-p '$PARENT_REPO/master'"' HEAD
    PARENT_REPO=$CHILD_REPO
done

Использование трансплантатов

В качестве альтернативы использованию опции --parent-filter для filter-branch вместо этого вы можете использовать механизм трансплантат.

Рассмотрим исходную ситуацию добавления R2/master в качестве дочернего элемента (то есть более нового, чем) R1/master. Как и раньше, начните с указания текущей ветки (master) на вершину R2/master.

git reset --hard R2/master

Теперь вместо запуска команды filter-branch создайте в .git/info/grafts "трансплантат" (фальшивый родитель), который связывает "root" (самый старый) commit R2/master (K) на вершине (новейшей) фиксации в R1/master (D). (Если есть несколько корней из R2/master, следующее будет связывать только одно из них.)

ROOT_OF_R2=$(git rev-list R2/master | tail -n 1)
TIP_OF_R1=$(git rev-parse R1/master)
echo $ROOT_OF_R2 $TIP_OF_R1 >> .git/info/grafts

На этом этапе вы можете посмотреть свою историю (скажем, через gitk), чтобы увидеть, правильно ли она выглядит. Если это так, вы можете сделать изменения постоянными через:

git filter-branch

Наконец, вы можете очистить все, удалив файл трансплантата.

rm .git/info/grafts

Использование графтов скорее всего будет работать, чем при использовании --parent-filter, но у него есть то преимущество, что он может трансформировать более двух историй с помощью одного filter-branch. (Вы можете сделать то же самое с --parent-filter, но script станет очень уродливым очень быстро.) Это также имеет то преимущество, что позволяет вам видеть ваши изменения до того, как они станут постоянными; если он выглядит плохо, просто удалите файл трансплантата, чтобы прервать его.

Объединение более двух репозиториев вместе с трансплантатами

Чтобы использовать метод трансплантата с R1 (самый старый) через R5 (новейший), просто добавьте несколько строк в файл трансплантата. (Порядок, в котором вы выполняете команды echo, не имеет значения.)

git reset --hard R5/master

PARENT_REPO=R1
for CHILD_REPO in R2 R3 R4 R5; do
    ROOT_OF_CHILD=$(git rev-list $CHILD_REPO/master | tail -n 1)
    TIP_OF_PARENT=$(git rev-parse $PARENT_REPO/master)
    echo "$ROOT_OF_CHILD" "$TIP_OF_PARENT" >> .git/info/grafts
    PARENT_REPO=$CHILD_REPO
done

Как насчет git rebase?

Несколько других предложили использовать git rebase R1/master вместо команды git filter-branch выше. Это приведет к различию между пустым фиксацией и K, а затем попытается применить его к D, в результате чего:

A---B---C---D---K'---L'---M'---N'

Это скорее всего вызовет конфликт слиянием и может привести к созданию ложных файлов в K', если файл был удален между D и K. Единственный случай, когда это будет работать, - это то, что деревья D и K идентичны.

(Еще одно небольшое отличие состоит в том, что git rebase изменяет информацию коммиттера для K' через N', тогда как git filter-branch не делает.)

Ответ 2

Вот что я сделал, что сработало:

git init
git remote add R1 /vol/R1.git
git fetch R1
git remote add R2 /vol/R2.git
git fetch R2
git co -B master R2/master
git rebase R1/master
git push -f

Ответ 3

Оригинальный плакат гласит:

R1:
A---B---C---D

R2:
K---L---M---N

Как я могу объединить два репозитория в один, содержащий точное представление о линейной истории проекта?

Как я могу объединить два репозитория в один, содержащий точное представление о линейной истории проекта?

A---B---C---D---K---L---M---N

Обратите внимание, что между файлами R1 и R2 были добавлены, удалены и переименованы.

Поэтому я точно знаю, что если первая фиксация нового репо, K, была идентичной или слегка измененной из последнего фиксации более старого репо, D, тогда вы могли бы просто извлечь историю R1 в R2, затем переустановите граф фиксации R2 на график из R1:

# From R2
git fetch R1
git checkout master
git rebase --onto R1/master --root

Нелинейные истории (когда у вас есть слияния)

Предположим, что граф R2 является линейным. Если у него есть слияния, вы можете попытаться сделать то же самое, указав, что хотите сохранить комманды слияния,

git rebase --preserve-merges --onto R1/master --root

Однако, если вам когда-либо приходилось разрешать конфликты в любом из этих слияний, которые вы перезагружаете, вам, вероятно, придется снова их повторно разрешить, что, вероятно, будет проблемой.

Сочетание двух радикально разных историй?

Оригинальный плакат сказал:

Обратите внимание, что между файлами R1 и R2 были добавлены, удалены и переименованы.

Как я уже указывал выше, простая переформатация должна работать, если первая фиксация нового репо, K, будет одинаковой или немного отличается от последней фиксации более старого репо, D. Я не уверен, что тот же rebase будет работать чисто, если K на самом деле существенно отличается от D. Я полагаю, что в худшем случае вам придется решить множество конфликтов во время первого применения K во время rebase.

Документация

Ответ 4

Все, что вам нужно, это: git rebase следует за тем, какая ветка вы перегружаете.

В двух словах, rebase перематывает все коммиты ветки и объединяет их с фиксацией ветки, которую вы перегружаете.

В зависимости от того, сколько разностей существует между двумя ветвями, вы можете столкнуться с конфликтами. Но избежать каких-либо конфликтов можно, используя любой другой метод.

Удачи!