Объединить два репозитория Git, не нарушая историю файлов

226

Мне нужно объединить два репозитория Git в совершенно новый, третий репозиторий. Я нашел много описаний того , как сделать это с помощью поддерева слияния (например , ответ Якуба Narębski в на Как объединить два хранилища Git? ) И следуя эти инструкции , в основном работаешь, за исключением того, что , когда я совершить поддерево слияния всех файлов из старых репозиториев записываются как новые добавленные файлы. Когда я это делаю git log, я вижу историю коммитов из старых репозиториев , но если я это сделаю, git log <file>то для этого файла будет показан только один коммит - слияние поддеревьев. Судя по комментариям к ответу выше, я не одинок в этой проблеме, но я не нашел опубликованных решений для нее.

Есть ли способ объединить репозитории и оставить историю отдельных файлов без изменений?

Эрик Ли
источник
Я не использую Git, но в Mercurial я сначала сделал бы преобразование, если необходимо, чтобы исправить пути к файлам репозиториев, которые должны быть объединены, а затем принудительно вытащил один репо в цель, чтобы получить наборы изменений, а затем сделал бы слияние разных веток. Это проверено и работает;) Может быть, это помогает найти решение и для Git ... по сравнению с подходом слияния поддеревьев, я думаю, шаг преобразования отличается, когда история переписывается, а не просто отображает путь (если я понимаю, правильно). Это обеспечивает плавное объединение без какой-либо специальной обработки путей к файлам.
Лусеро
Я также нашел этот вопрос полезным stackoverflow.com/questions/1683531/…
nacross
Я создал дополнительный вопрос. Может быть интересно: объединить два репозитория Git и сохранить историю мастеров: stackoverflow.com/questions/42161910/…
Dimitri Dewaele
Автоматизированное решение, которое работало для меня, было stackoverflow.com/a/30781527/239408
xverges

Ответы:

269

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

Вот пример скрипта Powershell для склеивания двух репозиториев:

# Assume the current directory is where we want the new repository to be created
# Create the new repository
git init

# Before we do a merge, we have to have an initial commit, so we'll make a dummy commit
git commit --allow-empty -m "Initial dummy commit"

# Add a remote for and fetch the old repo
git remote add -f old_a <OldA repo URL>

# Merge the files from old_a/master into new/master
git merge old_a/master --allow-unrelated-histories

# Move the old_a repo files and folders into a subdirectory so they don't collide with the other repo coming later
mkdir old_a
dir -exclude old_a | %{git mv $_.Name old_a}

# Commit the move
git commit -m "Move old_a files into subdir"

# Do the same thing for old_b
git remote add -f old_b <OldB repo URL>
git merge old_b/master --allow-unrelated-histories
mkdir old_b
dir exclude old_a,old_b | %{git mv $_.Name old_b}
git commit -m "Move old_b files into subdir"

Очевидно, что вместо этого вы можете объединить old_b со old_a (который становится новым объединенным репо), если вы предпочитаете это сделать - изменить скрипт так, чтобы он подходил.

Если вы также хотите перенести текущие ветви функций, используйте это:

# Bring over a feature branch from one of the old repos
git checkout -b feature-in-progress
git merge -s recursive -Xsubtree=old_a old_a/feature-in-progress

Это единственная неочевидная часть процесса - это не слияние поддеревьев, а скорее аргумент к обычному рекурсивному слиянию, которое сообщает Git, что мы переименовали цель, и помогает Git правильно выстраивать все.

Я написал чуть более подробное объяснение здесь .

Эрик Ли
источник
16
Использование этого решения git mvне очень хорошо работает. когда вы позже используете a git logдля одного из перемещенных файлов, вы получаете только фиксацию при перемещении. Вся предыдущая история потеряна. это потому, что git mvна самом деле, git rm; git addно в один шаг .
mholm815
15
Это то же самое, что и любая другая операция перемещения / переименования в Git: из командной строки вы можете получить всю историю git log --follow, либо все инструменты GUI сделают это автоматически. Насколько я знаю, с объединением поддерева вы не можете получить историю отдельных файлов, поэтому этот метод лучше.
Эрик Ли,
3
@EricLee Когда репозиторий old_b объединен, я получаю много конфликтов слияния. Это ожидается? Я получаю КОНФЛИКТ (переименовывать / удалять)
Джон
9
Когда я пытаюсь "dir -exclude old_a |% {git mv $ _. Имя old_a}", я получаю sh.exe ": dir: команда не найдена, а sh.exe": git: команда не найдена. Используя это работает: ls -I old_a | xargs -I '{}' git mv '{}' old_a /
Джордж
5
Это 1(номер один) для lsи «глаз» для xargs. Спасибо вам за этот совет!
Доминик Виал
149

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

  1. Добавьте второй репо как удаленный:

    cd firstgitrepo/
    git remote add secondrepo username@servername:andsoon
    
  2. Убедитесь, что вы загрузили все коммиты secondrepo:

    git fetch secondrepo
    
  3. Создайте локальную ветку из ветви второго репо:

    git branch branchfromsecondrepo secondrepo/master
    
  4. Переместите все его файлы в подкаталог:

    git checkout branchfromsecondrepo
    mkdir subdir/
    git ls-tree -z --name-only HEAD | xargs -0 -I {} git mv {} subdir/
    git commit -m "Moved files to subdir/"
    
  5. Объедините вторую ветку с главной веткой первого репо:

    git checkout master
    git merge --allow-unrelated-histories branchfromsecondrepo
    

Ваш репозиторий будет иметь более одного корневого коммита, но это не должно создавать проблем.

Флимм
источник
1
Шаг 2 не работает для меня: fatal: Недопустимое имя объекта: 'secondrepo / master'.
Кит
@Keith: убедитесь, что вы добавили второе репо в качестве удаленного с именем «secondrepo» и что в этом репо есть ветка с именем «master» (вы можете просматривать ветки в удаленном репо с помощью команды git remote show secondrepo)
Flimm
Я должен был сделать выборку, чтобы сломать это также. Между 1 и 2 я сделал git fetch secondrepo
sksamuel
@monkjack: я отредактировал свой ответ, включив в него шаг git fetch. Не стесняйтесь редактировать ответ самостоятельно в будущем.
Flimm
4
@MartijnHeemels Для более старой версии Git просто опустите --allow-unrelated-histories. Смотрите историю этого ответного поста.
Flimm
8

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

  1. Создайте новый репозиторий в Github.

    введите описание изображения здесь

  2. Загрузите вновь созданный репозиторий и добавьте старый удаленный репозиторий.

    git clone https://github.com/alexbr9007/Test.git
    cd Test
    git remote add OldRepo https://github.com/alexbr9007/Django-React.git
    git remote -v
    
  3. Получить все файлы из старого репозитория, чтобы создать новую ветку.

    git fetch OldRepo
    git branch -a
    

    введите описание изображения здесь

  4. В основной ветке выполните слияние, чтобы объединить старое репо с вновь созданным.

    git merge remotes/OldRepo/master --allow-unrelated-histories
    

    введите описание изображения здесь

  5. Создайте новую папку для хранения всего нового созданного контента, который был добавлен из OldRepo, и переместите его файлы в эту новую папку.

  6. Наконец, вы можете загрузить файлы из объединенных репозиториев и безопасно удалить OldRepo из GitHub.

Надеюсь, что это может быть полезно для всех, кто занимается слиянием удаленных репозиториев.

abautista
источник
1
Это единственное решение, которое помогло мне сохранить историю git. Не забудьте удалить удаленную ссылку на старый репозиторий git remote rm OldRepo.
Харубиёри
7

пожалуйста, посмотрите на использование

git rebase --root --preserve-merges --onto

связать две истории в начале своей жизни.

Если у вас есть пути, которые перекрываются, исправьте их

git filter-branch --index-filter

при использовании журнала убедитесь, что вы «находите копии труднее» с

git log -CC

Таким образом, вы найдете любые движения файлов в пути.

Адам Димитрук
источник
Документация Git рекомендует не перебазировать ... git-scm.com/book/en/v2/Git-Branching-Rebasing#_rebase_peril
Стивен Тернер
7

Я превратил решение из @Flimm в git aliasподобное (добавлено в мой ~/.gitconfig):

[alias]
 mergeRepo = "!mergeRepo() { \
  [ $# -ne 3 ] && echo \"Three parameters required, <remote URI> <new branch> <new dir>\" && exit 1; \
  git remote add newRepo $1; \
  git fetch newRepo; \
  git branch \"$2\" newRepo/master; \
  git checkout \"$2\"; \
  mkdir -vp \"${GIT_PREFIX}$3\"; \
  git ls-tree -z --name-only HEAD | xargs -0 -I {} git mv {} \"${GIT_PREFIX}$3\"/; \
  git commit -m \"Moved files to '${GIT_PREFIX}$3'\"; \
  git checkout master; git merge --allow-unrelated-histories --no-edit -s recursive -X no-renames \"$2\"; \
  git branch -D \"$2\"; git remote remove newRepo; \
}; \
mergeRepo"
Фредрик Эрландссон
источник
12
Просто любопытно: вы действительно делаете это достаточно часто, чтобы использовать псевдоним?
Паркер Коутс
1
Нет, я не знаю, но никогда не помню, как это сделать, так что псевдоним - это просто способ запомнить это.
Фредрик Эрландссон
1
Да .. но попробуйте изменить компьютеры и забыть , чтобы переместить псевдонимы;)
Кетцалькоатля
1
Какова стоимость $GIT_PREFIX?
neowulf33
github.com/git/git/blob/… 'GIT_PREFIX' устанавливается как возвращаемое путем запуска 'git rev-parse --show-prefix' из исходного текущего каталога. Смотрите linkgit: git-rev-parse [1].
Фредрик Эрландссон
3

Эта функция клонирует удаленное репо в локальный каталог репо:

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"
}

Как пользоваться:

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

Примечание. Этот сценарий может перезаписывать коммиты, но сохранит всех авторов и даты, это означает, что новые коммиты будут иметь другие хэши, и если вы попытаетесь отправить изменения на удаленный сервер, он сможет это сделать только с помощью клавиши Force, также он перезапишет коммиты на сервере. Поэтому, пожалуйста, сделайте резервные копии перед запуском.

Прибыль!

Андрей Изман
источник
Я использую zsh, а не bash и v2.13.0 из git. Что бы я ни пробовал, я не смог добраться git filter-branch --index-filterдо работы. Обычно я получаю сообщение об ошибке, что файл индекса .new не существует. Это звонит в какие-нибудь колокола?
Патрик Борода
@PatrickBeard Я не знаю, zsh, вы можете создать отдельный файл git-add-repo.shс функцией выше, в конце файла поставьте эту строку git-add-repo "$@". После этого вы можете использовать его из Zsh, как cd current/git/packageиbash path/to/git-add-repo.sh https://github.com/example/example dir/to/save
Андрей Изман
Проблема обсуждалась здесь: stackoverflow.com/questions/7798142/… mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE" иногда терпит неудачу, поэтому вы должны добавить if test.
Патрик Борода
1
Я бы не использовал этот метод! Я попробовал сценарий, наивно и дословно (я могу винить себя только за эту часть), и он забил мой локальный репозиторий. История выглядела в основном правильно, но выполнение git push обратно на Github привело к страшному «RPC не удалось; curl 55 SSL_write () вернул ошибку SYSCALL, errno = 32». Я пытался починить его, но он был непоправимо сломан. В итоге мне пришлось реконструировать вещи в новом локальном репо.
Мейсон Фрид
@MasonFreed этот скрипт создает новую историю git с сочетанием обоих репо, поэтому его нельзя перенести в старое репо, ему нужно создать новое или нажать форсированную клавишу, значит, он переписывает ваш репо на сервере
Андрей Изман
2

Выполните шаги для встраивания одного репо в другое репо, имея одну историю Git, объединяя обе истории Git.

  1. Клонируйте оба репозитория, которые вы хотите объединить.

git clone git@github.com: пользователь / parent-repo.git

git clone git@github.com: пользователь / child-repo.git

  1. Перейти на репо

cd child-repo /

  1. выполните приведенную ниже команду, замените путь my/new/subdir(3 случая) структурой каталогов, в которой вы хотите иметь дочернее хранилище.

git filter-branch --prune-empty --tree-filter 'if [! -e мой / новый / subdir]; затем mkdir -p my / new / subdir git ls-tree - только для имени $ GIT_COMMIT | xargs -I файлы mv файлы my / new / subdir fi '

  1. Перейти к родительскому репо

cd ../parent-repo/

  1. Добавить удаленное к родительскому репо, указав путь к дочернему репо

git remote добавить child-remote ../child-repo/

  1. Получить репо ребенка

git fetch child-remote

  1. Слить истории

git merge --allow-unrelated-историй child-remote / master

Если вы сейчас проверите журнал git в родительском репо, он должен объединить коммиты дочернего репо. Вы также можете увидеть тег, указывающий из источника фиксации.

Приведенная ниже статья помогла мне встроить одно хранилище в другое, создав одну историю Git, объединив обе истории Git.

http://ericlathrop.com/2014/01/combining-git-repositories/

Надеюсь это поможет. Удачного кодирования!

AnoopGoudar
источник
Шаг 3 не удался для меня с синтаксической ошибкой. Точки с запятой отсутствуют. Fixgit filter-branch --prune-empty --tree-filter ' if [ ! -e my/new/subdir ]; then mkdir -p my/new/subdir; git ls-tree --name-only $GIT_COMMIT | xargs -I files mv files my/new/subdir; fi'
Юрий Л
1

Допустим, вы хотите объединить хранилище aс b(я предполагаю, что они расположены рядом друг с другом):

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

В случае, если вы хотите поместить aв подкаталог, сделайте следующее перед командами выше:

cd a
git filter-repo --to-subdirectory-filter a
cd ..

Для этого вам нужно git-filter-repoустановить ( filter-branchне рекомендуется ).

Пример объединения двух больших репозиториев и помещения одного из них в подкаталог: https://gist.github.com/x-yuri/9890ab1079cf4357d6f269d073fd9731

Подробнее об этом здесь .

х-юри
источник