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

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

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

11 ответов

Решение

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

Вот пример скрипта 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 правильно выстроить все.

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

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

  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
    

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

Скажем, вы хотите объединить репозиторий 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

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

Прошло несколько лет, и есть хорошо обоснованные решения, за которые проголосовали, но я хочу поделиться своим, потому что это было немного по-другому, потому что я хотел объединить 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.

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

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

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

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

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

git filter-branch --index-filter

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

git log -CC

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

Я превратил решение из @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"

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

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

Прибыль!

Как объединить 1, 2 или более локальных (или удаленных, но клонированных локально) репозиториев в другой репозиторий, сохраняя при этом историю git

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

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

В настоящее время у вас есть такая структура каталогов:

а вот независимые, автономные, локально хранящиеся или клонированные репозитории git.


Что вы хотите, так это получить следующее:

а здесь только папки (не репозитории и не подрепозитории/подмодули) внутри .

      repo1/      # will be deleted when done
    ...

repo2/      # will be deleted when done
    ...

new_repo/
    .git/
    .gitignore
    repo1/
        (other files and folders)
    repo2/
        (other files and folders)

Подробные инструкции

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

    Итак, будем исходить из этого:

            repo1/
        .git/
        .gitignore
        (other files and folders)
    
    repo2/
        .git/
        .gitignore
        (other files and folders)
    

    к этому:

            repo1/
        .git/
        repo1/
            .gitignore
            (other files and folders)
    
    repo2/
        .git/
        repo2/
            .gitignore
            (other files and folders)
    

    Команды, которые нужно запустить для этого:

            # 1. Fix up repo1
    
    cd path/to/repo1
    mkdir repo1
    # move all non-hidden files and folders into `repo1/`
    mv * repo1/
    # move all hidden files and folders into `repo1/`
    mv .* repo1/
    # Now move the .git dir back to where it belongs, since it was moved by the
    # command just above
    mv repo1/.git .
    # commit all these changes into this repo
    git add -A
    git status
    git commit -m "Move all files & folders into a subdir"
    
    
    # 2. Fix up repo2 (same process as just above, except use `repo2` instead of
    # `repo1`)
    
    cd path/to/repo2
    mkdir repo2
    mv * repo2/
    mv .* repo2/
    mv repo2/.git .
    git add -A
    git status
    git commit -m "Move all files & folders into a subdir"
    
  2. Создайте файл , если необходимо. Если этот репозиторий уже существует, ничего страшного, используйте его как есть и пропустите этот шаг. Если вам нужно создать это как совершенно новый репозиторий, вот как это сделать:

            # Create `new_repo`
    
    cd path/to/parentdir_of_repo1_and_repo2
    mkdir new_repo
    cd new_repo
    
    git init
    # (Optional, but recommended) rename the main branch from `master` to `main`
    git branch -m main
    # set your name and email for just this repo if you haven't done this
    # globally previously
    git config user.name "First Last"
    git config user.email firstlast@gmail.com
    
    # create an empty first commit to start a git history
    git commit --allow-empty -m "Initial empty commit"
    
  3. Объединить наши фиксированные иrepo2истории и содержимое репо в файлы .

            cd path/to/new_repo
    
    # --------------------------------------------------------------------------
    # 1. Merge repo1, with all files and folders and git history, into new_repo
    # --------------------------------------------------------------------------
    
    # Add repo1 as a local "remote" named `repo1`
    # - Note: this assumes that new_repo, repo1, and repo2 are all at the same
    #   directory level and inside the same parent folder. If this is *not* the
    #   case, no problem. Simply change `"../repo1"` below to the proper
    #   relative *or* absolute path to that repo! Ex: `"path/to/repo1"`. 
    git remote add repo1 "../repo1"
    # View all of your remotes. 
    # - You'll now see `repo1` as a remote which points to the local "URL"
    #   of "../repo1"
    git remote -v
    
    # Fetch all of repo1's files and history into new_repo's .git dir. 
    # - Note that `repo1` here is the name of the remote alias that you just
    #   added above.
    git fetch repo1 
    # View the new locally-stored, remote-tracking hidden branch that was just
    # created for you. 
    # - `git branch -r` will now show a new hidden branch named `repo1/main` or
    #   `repo1/master` or whatever your main branch was named there.
    git branch -r
    
    # Assuming that your new hidden branch that you just fetched is called
    # `repo1/main`, let's merge that into our currently-checked-out branch now.
    # - This merges repo1's files and folders and git history into new_repo.
    # - change `repo1/main` to `repo1/some_other_branch` if you want to merge in
    #   `some_other_branch` instead.
    # - Immediately after running this command, you will now see a new folder,
    #   `repo1`, with all of its files and folders within it, created inside
    #   the `new_repo` directory.
    git merge --allow-unrelated-histories repo1/main
    # Since you have independent sub-folders prepared inside repo1 and repo2,
    # there will be no conflicts whatsoever. When the git merge editor opens
    # up, just save and close it to complete the merge. Optionally, add any
    # comments you wish before saving and closing it.
    
    # Now, remove the remote named "repo1", since we no longer need it.
    git remote remove repo1
    # View all remotes to ensure it is now gone
    git remote -v
    
    # See the new commit status. If `repo1` had 100 commits in it, for instance,
    # `git status` will now show this, since `new_repo` now has those 100
    # commits plus this new merge commit in it:
    #       
    #       $ git status
    #       On branch main
    #       Your branch is ahead of 'origin/main' by 101 commits.
    #         (use "git push" to publish your local commits)
    #       
    #       nothing to commit, working tree clean
    #       
    git status 
    # Push to a remote, if you have one configured
    git push
    
    # You may now optionally manually delete the original `repo1` since it is
    # merged into `new_repo`. Here's what that command would look like:
    # - WARNING: get this path correct or you'll delete a whole lot of stuff you
    #   don't mean to!
    # 
    #       rm -r ../repo1
    #
    
    # --------------------------------------------------------------------------
    # 2. Merge repo2 in too. 
    # - This is the exact same process as above, except we use `repo2` instead
    #   of `repo1`.
    # --------------------------------------------------------------------------
    
    git remote add repo2 "../repo2"
    git remote -v
    
    git fetch repo2 
    git branch -r
    
    git merge --allow-unrelated-histories repo2/main
    
    git remote remove repo2
    git remote -v
    
    git status 
    git push
    # Optional: `rm -r ../repo2`
    
  4. (Необязательно) просмотрите новую объединенную историю внутриnew_repoс :

            cd path/to/new_repo
    
    # Add this really great `git lg` alias from here:
    # https://coderwall.com/p/euwpig/a-better-git-log
    git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
    
    # now view the new git history; it will cleanly show you the branch pattern,
    # and your new contents, including their branch lines
    git lg
    

    Если ты бежишьgit lgтолько после слияния, вот что вы можете увидеть, предполагая, чтоrepo1в нем было всего 7 коммитов:

            $ git lg
    *   32c1d05 - (HEAD -> main, origin/main, origin/HEAD) Merge remote-tracking branch 'repo1/main' (x minutes ago) <Gabriel Staples>
    |\  
    | * eca4e6c - Move all files & folders into a subdir (x minutes ago) <Gabriel Staples>
    | * 92853ac - my repo1 commit message <Gabriel Staples>
    | * 07fe88e - my repo1 commit message <Gabriel Staples>
    | * f3b0847 - my repo1 commit message <Gabriel Staples>
    | * bcb0ca5 - my repo1 commit message <Gabriel Staples>
    | * 712ec94 - my repo1 commit message <Gabriel Staples>
    | * 1103fe1 - Initial commit (x days ago) <Gabriel Staples>
    * 47a663b - my new_repo commit message <Gabriel Staples>
    * 5d865d0 - my new_repo commit message <Gabriel Staples>
    * d47c4e1 - Initial commit (x hours ago) <Gabriel Staples>
    

Рекомендации

  1. Основная часть моих инструкций была получена здесь: https://blog.jdriven.com/2021/04/how-to-merge-multiple-git-repositories/
  2. это очень похожий ответ от @x-yuri
  3. Суперпользователь: Как скопировать с помощью cp, чтобы включить скрытые файлы, скрытые каталоги и их содержимое?

Смотрите также

Помимо ссылок выше, см.:

  1. https://gfscott.com/blog/merge-git-repos-and-keep-commit-history
  2. основной ответ от @Eric Lee

Выполните шаги для встраивания одного репо в другое репо, имея одну историю 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/

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

Я создал на основе ответа x-yuri , который использует filter-repo . С помощью репозиторий с некоторыми скриптамимоих скриптов вы можете легко переместить все ветки и теги в ваш новый репозиторий без конфликтов слияния, если вы укажете разные подкаталоги.

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

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

У меня есть эта суть , которая помогает с этим.

Другие вопросы по тегам