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

наши репозитории Git начинались как части одного репозитория Monster SVN, где у каждого отдельного проекта было свое собственное дерево:

project1/branches
        /tags
        /trunk
project2/branches
        /tags
        /trunk

очевидно, было довольно легко перемещать файлы с одного на другой с помощью svn mv. Но в Git каждый проект находится в своем репозитории, и сегодня меня попросили переместить подкаталог из project2 to project1. Я сделал что-то вроде этого:--6-->

$ git clone project2 
$ cd project2
$ git filter-branch --subdirectory-filter deeply/buried/java/source/directory/A -- --all
$ git remote rm origin  # so I don't accidentally the repo ;-)
$ mkdir -p deeply/buried/different/java/source/directory/B
$ for f in *.java; do 
>  git mv $f deeply/buried/different/java/source/directory/B
>  done
$ git commit -m "moved files to new subdirectory"
$ cd ..
$
$ git clone project1
$ cd project1
$ git remote add p2 ../project2
$ git fetch p2
$ git branch p2 remotes/p2/master
$ git merge p2 # --allow-unrelated-histories for git 2.9
$ git remote rm p2
$ git push

но это кажется довольно запутанным. Есть ли лучший способ сделать что-то в этом роде? Или я выбрал правильный подход?

обратите внимание, что это включает в себя объединение истории в существующий репозиторий, а не просто создание нового автономного репозитория из части другого (как и в предыдущем вопросе).

12 ответов


Да, нажимая на --subdirectory-filter of filter-branch был ключ. Тот факт, что вы использовали его, по сути, доказывает, что нет более простого способа - у вас не было выбора, кроме как переписать историю, так как вы хотели получить только (переименованное) подмножество файлов, и это по определению изменяет хэши. Поскольку ни одна из стандартных команд (например,pull) перепишите историю, вы не можете использовать их для этого.

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


Если ваша история вменяема, вы можете взять коммиты как патч и применить их в новом репозитории:

cd repository
git log --pretty=email --patch-with-stat --reverse --full-index --binary -- path/to/file_or_folder > patch
cd ../another_repository
git am < ../repository/patch 

или в одной строке

git log --pretty=email --patch-with-stat --reverse -- path/to/file_or_folder | (cd /path/to/new_repository && git am)

(взято из документы Exherbo)


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

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

этап Один

  1. сделайте копию репозитория A, так как следующие шаги делают major изменения в этой копии, которые вы не должны нажимать!

    git clone --branch <branch> --origin origin --progress -v <git repository A url>
    eg. git clone --branch master --origin origin --progress -v https://username@giturl/scm/projects/myprojects.git
    

    (предполагая, что myprojects-это репозиторий, из которого вы хотите скопировать)

  2. cd в него

    cd <git repository A directory>          eg. cd /c/Working/GIT/myprojects
    
  3. удалите ссылку на исходный репозиторий, чтобы избежать случайного внесение любых удаленных изменений (например. от pushing)

    git remote rm origin
    
  4. просмотрите свою историю и файлы, удалив все, чего нет в каталог 1. В результате содержимое каталога 1 извергается в базу репозитория A.

    git filter-branch --subdirectory-filter <directory> -- --all
    eg. git filter-branch --subdirectory-filter subfolder1/subfolder2/FOLDER_TO_KEEP -- --all
    
  5. только для перемещения одного файла: пройдите через то, что осталось, и удалите все, кроме нужного файла. (Возможно, потребуется удалить файлы вы не хотите с тем же именем и совершить.)

    git filter-branch -f --index-filter \
    'git ls-files -s | grep $'\t'FILE_TO_KEEP$ |
    GIT_INDEX_FILE=$GIT_INDEX_FILE.new \
    git update-index --index-info && \
    mv $GIT_INDEX_FILE.new $GIT_INDEX_FILE || echo "Nothing to do"' --prune-empty -- --all
    

    например. FILE_TO_KEEP = ПФЛ.xml, чтобы сохранить только pom.xml-файл из FOLDER_TO_KEEP

Второй Этап

  1. стек

    git reset --hard
    
  2. стек

    git gc --aggressive
    
  3. стек

    git prune
    

вы можете импортировать эти файлы в репозиторий B в каталоге, не являющемся корневым:

  1. сделать это каталог

    mkdir <base directory>             eg. mkdir FOLDER_TO_KEEP
    
  2. переместить файлы в этот каталог

    git mv * <base directory>          eg. git mv * FOLDER_TO_KEEP
    
  3. добавить файлы в этот каталог

    git add .
    
  4. сохранить изменения и мы готовы объединить эти файлы в новый репозиторий

    git commit
    

Третий Этап

  1. сделайте копию репозитория B, если у вас его еще нет

    git clone <git repository B url>
    eg. git clone https://username@giturl/scm/projects/FOLDER_TO_KEEP.git
    

    (предполагая, что FOLDER_TO_KEEP-это имя нового репозитория, в который вы копируете)

  2. cd в него

    cd <git repository B directory>          eg. cd /c/Working/GIT/FOLDER_TO_KEEP
    
  3. создайте удаленное соединение с репозиторием A как ветвь в репозитории Б

    git remote add repo-A-branch <git repository A directory>
    

    (repo-a-branch может быть чем угодно - это просто произвольное имя)

    eg. git remote add repo-A-branch /c/Working/GIT/myprojects
    
  4. Pull из этой ветви (содержащей только каталог, который вы хотите переместить) в репозиторий Б.

    git pull repo-A-branch master --allow-unrelated-histories
    

    The pull копирует как файлы, так и историю. Примечание: Вы можете использовать слияние вместо вытягивания, но вытягивание работает лучше.

  5. наконец, вы, вероятно, хотите немного очистить, удалив пульт подключение к хранилищу

    git remote rm repo-A-branch
    
  6. нажимаем и все готово.

    git push
    

нашел этой очень полезно. Это очень простой подход, когда вы создаете патчи, которые применяются к новому РЕПО. См. связанную страницу для получения дополнительной информации.

Он содержит только три шага (скопировано из блога):

# Setup a directory to hold the patches
mkdir <patch-directory>

# Create the patches
git format-patch -o <patch-directory> --root /path/to/copy

# Apply the patches in the new repo using a 3 way merge in case of conflicts
# (merges from the other repo are not turned into patches). 
# The 3way can be omitted.
git am --3way <patch-directory>/*.patch

единственная проблема у меня было, что я не мог применить все патчи сразу, используя

git am --3way <patch-directory>/*.patch

под Windows я получил ошибку InvalidArgument. Поэтому мне пришлось накладывать все пластыри один за другим.


СОХРАНЕНИЕ ИМЕНИ КАТАЛОГА

подкаталог-фильтр (или более короткая команда git subtree) работает хорошо, но не работает для меня, так как они удаляют имя каталога из информации фиксации. В моем сценарии я просто хочу объединить части одного репозитория в другой и сохранить историю с полным именем пути.

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

# 1. clone the source
git clone ssh://<user>@<source-repo url>
cd <source-repo>
# 2. remove the stuff we want to exclude
git filter-branch --tree-filter "rm -rf <files to exclude>" --prune-empty HEAD
# 3. move to target repo and create a merge branch (for safety)
cd <path to target-repo>
git checkout -b <merge branch>
# 4. Add the source-repo as remote 
git remote add source-repo <path to source-repo>
# 5. fetch it
git pull source-repo master
# 6. check that you got it right (better safe than sorry, right?)
gitk

этот ответ предоставляет интересные команды, основанные на git am и представлены с использованием примеров, шаг за шагом.

цель

  • вы хотите переместить некоторые или все файлы из одного хранилища в другое.
  • вы хотите сохранить свою историю.
  • но вы не заботитесь о сохранении бирки и филиалов.
  • вы принимаете ограниченную историю для переименованных файлов (и файлов в переименованных справочники.)

процедура

  1. извлечение истории в формате электронной почты с помощью
    git log --pretty=email -p --reverse --full-index --binary
  2. реорганизовать дерево файлов и обновить изменение имени файла в истории [необязательно]
  3. применить новую историю с помощью git am

1. Извлечение истории в формате электронной почты

пример: извлечь историю file3, file4 и file5

my_repo
├── dirA
│   ├── file1
│   └── file2
├── dirB            ^
│   ├── subdir      | To be moved
│   │   ├── file3   | with history
│   │   └── file4   | 
│   └── file5       v
└── dirC
    ├── file6
    └── file7

очистить временный каталог назначения

export historydir=/tmp/mail/dir  # Absolute path
rm -rf "$historydir"             # Caution when cleaning

очистите ваше РЕПО источник

git commit ...           # Commit your working files
rm .gitignore            # Disable gitignore
git clean -n             # Simulate removal
git clean -f             # Remove untracked file
git checkout .gitignore  # Restore gitignore

извлечь историю каждого файла в формате электронной почты

cd my_repo/dirB
find -name .git -prune -o -type d -o -exec bash -c 'mkdir -p "$historydir/${0%/*}" && git log --pretty=email -p --stat --reverse --full-index --binary -- "" > "$historydir/"' {} ';'

к сожалению, вариант --follow или --find-copies-harder нельзя комбинировать с --reverse. Вот почему история вырезается при переименовании файла (или при переименовании родительского каталога).

после: временная история в формате электронной почты

/tmp/mail/dir
    ├── subdir
    │   ├── file3
    │   └── file4
    └── file5

2. Реорганизовать дерево файлов и обновить изменение имени файла в истории [необязательно]

Предположим, вы хотите переместить эти три файла в другое РЕПО (может быть то же самое РЕПО).

my_other_repo
├── dirF
│   ├── file55
│   └── file56
├── dirB              # New tree
│   ├── dirB1         # was subdir
│   │   ├── file33    # was file3
│   │   └── file44    # was file4
│   └── dirB2         # new dir
│        └── file5    # = file5
└── dirH
    └── file77

поэтому реорганизуйте свои файлы:

cd /tmp/mail/dir
mkdir     dirB
mv subdir dirB/dirB1
mv dirB/dirB1/file3 dirB/dirB1/file33
mv dirB/dirB1/file4 dirB/dirB1/file44
mkdir    dirB/dirB2
mv file5 dirB/dirB2

ваша временная история теперь:

/tmp/mail/dir
    └── dirB
        ├── dirB1
        │   ├── file33
        │   └── file44
        └── dirB2
             └── file5

изменить также имена в истории:

cd "$historydir"
find * -type f -exec bash -c 'sed "/^diff --git a\|^--- a\|^+++ b/s:\( [ab]\)/[^ ]*:/:g" -i ""' {} ';'

Примечание: эта переписывает историю, чтобы отразить изменение пути и имени файла.
      (т. е. изменение нового местоположения / имени в новом РЕПО)


3. Применить новую историю

другие РЕПО:

my_other_repo
├── dirF
│   ├── file55
│   └── file56
└── dirH
    └── file77

применить фиксации из временных файлов истории:

cd my_other_repo
find "$historydir" -type f -exec cat {} + | git am 

ваш другой РЕПО теперь:

my_other_repo
├── dirF
│   ├── file55
│   └── file56
├── dirB            ^
│   ├── dirB1       | New files
│   │   ├── file33  | with
│   │   └── file44  | history
│   └── dirB2       | kept
│        └── file5  v
└── dirH
    └── file77

использовать git status чтобы увидеть количество коммитов, готовых к нажатию : -)

Примечание: как история была переписана, чтобы отразить путь и имя файла изменить:
      (т. е. по сравнению с местоположением / именем в предыдущем РЕПО)

  • не нужно git mv чтобы изменить местоположение / имя файла.
  • не нужно git log --follow для доступа к полной истории.

дополнительный трюк: обнаружение переименованных / перемещенных файлов в вашем РЕПО

чтобы перечислить переименованные файлы:

find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow {} ';' | grep '=>'

дополнительные настройки: вы можете выполнить команду git log используя опции --find-copies-harder или --reverse. Вы также можете удалить первые два столбца, используя cut -f3- и grepping полный шаблон' {.* => .*}'.

find -name .git -prune -o -exec git log --pretty=tformat:'' --numstat --follow --find-copies-harder --reverse {} ';' | cut -f3- | grep '{.* => .*}'

имея подобный зуд, чтобы поцарапать (altough только для некоторых файлов данного репозитория), этот скрипт оказался действительно полезным:git-import

короткая версия заключается в том, что он создает файлы исправлений данного файла или каталога ($object) из существующего репозитория:

cd old_repo
git format-patch --thread -o "$temp" --root -- "$object"

которые затем применяются к новому репозиторию:

cd new_repo
git am "$temp"/*.patch 

для подробной информации, пожалуйста, посмотрите:


Я всегда здесь http://blog.neutrino.es/2012/git-copy-a-file-or-directory-from-another-repository-preserving-history/ . Просто и быстро.

для соответствия стандартам stackoverflow, вот процедура:

mkdir /tmp/mergepatchs
cd ~/repo/org
export reposrc=myfile.c #or mydir
git format-patch -o /tmp/mergepatchs $(git log $reposrc|grep ^commit|tail -1|awk '{print }')^..HEAD $reposrc
cd ~/repo/dest
git am /tmp/mergepatchs/*.patch

используя воодушевленность от http://blog.neutrino.es/2012/git-copy-a-file-or-directory-from-another-repository-preserving-history/, я создал эту функцию Powershell для того же самого, которая отлично работала для меня до сих пор:

# Migrates the git history of a file or directory from one Git repo to another.
# Start in the root directory of the source repo.
# Also, before running this, I recommended that $destRepoDir be on a new branch that the history will be migrated to.
# Inspired by: http://blog.neutrino.es/2012/git-copy-a-file-or-directory-from-another-repository-preserving-history/
function Migrate-GitHistory
{
    # The file or directory within the current Git repo to migrate.
    param([string] $fileOrDir)
    # Path to the destination repo
    param([string] $destRepoDir)
    # A temp directory to use for storing the patch file (optional)
    param([string] $tempDir = "\temp\migrateGit")

    mkdir $tempDir

    # git log $fileOrDir -- to list commits that will be migrated
    Write-Host "Generating patch files for the history of $fileOrDir ..." -ForegroundColor Cyan
    git format-patch -o $tempDir --root -- $fileOrDir

    cd $destRepoDir
    Write-Host "Applying patch files to restore the history of $fileOrDir ..." -ForegroundColor Cyan
    ls $tempDir -Filter *.patch  `
        | foreach { git am $_.FullName }
}

использование для этого примера:

git clone project2
git clone project1
cd project1
# Create a new branch to migrate to
git checkout -b migrate-from-project2
cd ..\project2
Migrate-GitHistory "deeply\buried\java\source\directory\A" "..\project1"

после того как вы сделали это, вы можете повторно организовать файлы migrate-from-project2 ветвь перед слиянием.


Я хотел что-то надежное и многоразовое (функция one-command-and-go + undo), поэтому я написал следующий сценарий bash. Работал на меня несколько раз, поэтому я решил поделиться им здесь.

он может перемещать произвольную папку /path/to/foo С repo1 на /some/other/folder/bar to repo2 (пути к папкам могут быть одинаковыми или различными, расстояние от корневой папки может отличаться).

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

поскольку это создает осиротевшую ветвь со всей историей старого РЕПО, а затем объединяет ее с главой, она даже будет работать в случае столкновения имен файлов (тогда вам придется разрешить слияние в конце, конечно).

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

недостатком является то, что он, скорее всего, не будет следовать переименованиям файлов (за пределами REWRITE_FROM папка) в исходном repo-pull запросы приветствуются на GitHub для размещения для этого.

ссылка GitHub:git-move-folder-between-repos-keep-history

#!/bin/bash

# Copy a folder from one git repo to another git repo,
# preserving full history of the folder.

SRC_GIT_REPO='/d/git-experimental/your-old-webapp'
DST_GIT_REPO='/d/git-experimental/your-new-webapp'
SRC_BRANCH_NAME='master'
DST_BRANCH_NAME='import-stuff-from-old-webapp'
# Most likely you want the REWRITE_FROM and REWRITE_TO to have a trailing slash!
REWRITE_FROM='app/src/main/static/'
REWRITE_TO='app/src/main/static/'

verifyPreconditions() {
    #echo 'Checking if SRC_GIT_REPO is a git repo...' &&
      { test -d "${SRC_GIT_REPO}/.git" || { echo "Fatal: SRC_GIT_REPO is not a git repo"; exit; } } &&
    #echo 'Checking if DST_GIT_REPO is a git repo...' &&
      { test -d "${DST_GIT_REPO}/.git" || { echo "Fatal: DST_GIT_REPO is not a git repo"; exit; } } &&
    #echo 'Checking if REWRITE_FROM is not empty...' &&
      { test -n "${REWRITE_FROM}" || { echo "Fatal: REWRITE_FROM is empty"; exit; } } &&
    #echo 'Checking if REWRITE_TO is not empty...' &&
      { test -n "${REWRITE_TO}" || { echo "Fatal: REWRITE_TO is empty"; exit; } } &&
    #echo 'Checking if REWRITE_FROM folder exists in SRC_GIT_REPO' &&
      { test -d "${SRC_GIT_REPO}/${REWRITE_FROM}" || { echo "Fatal: REWRITE_FROM does not exist inside SRC_GIT_REPO"; exit; } } &&
    #echo 'Checking if SRC_GIT_REPO has a branch SRC_BRANCH_NAME' &&
      { cd "${SRC_GIT_REPO}"; git rev-parse --verify "${SRC_BRANCH_NAME}" || { echo "Fatal: SRC_BRANCH_NAME does not exist inside SRC_GIT_REPO"; exit; } } &&
    #echo 'Checking if DST_GIT_REPO has a branch DST_BRANCH_NAME' &&
      { cd "${DST_GIT_REPO}"; git rev-parse --verify "${DST_BRANCH_NAME}" || { echo "Fatal: DST_BRANCH_NAME does not exist inside DST_GIT_REPO"; exit; } } &&
    echo '[OK] All preconditions met'
}

# Import folder from one git repo to another git repo, including full history.
#
# Internally, it rewrites the history of the src repo (by creating
# a temporary orphaned branch; isolating all the files from REWRITE_FROM path
# to the root of the repo, commit by commit; and rewriting them again
# to the original path).
#
# Then it creates another temporary branch in the dest repo,
# fetches the commits from the rewritten src repo, and does a merge.
#
# Before any work is done, all the preconditions are verified: all folders
# and branches must exist (except REWRITE_TO folder in dest repo, which
# can exist, but does not have to).
#
# The code should work reasonably on repos with reasonable git history.
# I did not test pathological cases, like folder being created, deleted,
# created again etc. but probably it will work fine in that case too.
#
# In case you realize something went wrong, you should be able to reverse
# the changes by calling `undoImportFolderFromAnotherGitRepo` function.
# However, to be safe, please back up your repos just in case, before running
# the script. `git filter-branch` is a powerful but dangerous command.
importFolderFromAnotherGitRepo(){
    SED_COMMAND='s-\t\"*-\t'${REWRITE_TO}'-'

    verifyPreconditions &&
    cd "${SRC_GIT_REPO}" &&
      echo "Current working directory: ${SRC_GIT_REPO}" &&
      git checkout "${SRC_BRANCH_NAME}" &&
      echo 'Backing up current branch as FILTER_BRANCH_BACKUP' &&
      git branch -f FILTER_BRANCH_BACKUP &&
      SRC_BRANCH_NAME_EXPORTED="${SRC_BRANCH_NAME}-exported" &&
      echo "Creating temporary branch '${SRC_BRANCH_NAME_EXPORTED}'..." &&
      git checkout -b "${SRC_BRANCH_NAME_EXPORTED}" &&
      echo 'Rewriting history, step 1/2...' &&
      git filter-branch -f --prune-empty --subdirectory-filter ${REWRITE_FROM} &&
      echo 'Rewriting history, step 2/2...' &&
      git filter-branch -f --index-filter \
       "git ls-files -s | sed \"$SED_COMMAND\" |
        GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info &&
        mv $GIT_INDEX_FILE.new $GIT_INDEX_FILE" HEAD &&
    cd - &&
    cd "${DST_GIT_REPO}" &&
      echo "Current working directory: ${DST_GIT_REPO}" &&
      echo "Adding git remote pointing to SRC_GIT_REPO..." &&
      git remote add old-repo ${SRC_GIT_REPO} &&
      echo "Fetching from SRC_GIT_REPO..." &&
      git fetch old-repo "${SRC_BRANCH_NAME_EXPORTED}" &&
      echo "Checking out DST_BRANCH_NAME..." &&
      git checkout "${DST_BRANCH_NAME}" &&
      echo "Merging SRC_GIT_REPO/" &&
      git merge "old-repo/${SRC_BRANCH_NAME}-exported" --no-commit &&
    cd -
}

# If something didn't work as you'd expect, you can undo, tune the params, and try again
undoImportFolderFromAnotherGitRepo(){
  cd "${SRC_GIT_REPO}" &&
    SRC_BRANCH_NAME_EXPORTED="${SRC_BRANCH_NAME}-exported" &&
    git checkout "${SRC_BRANCH_NAME}" &&
    git branch -D "${SRC_BRANCH_NAME_EXPORTED}" &&
  cd - &&
  cd "${DST_GIT_REPO}" &&
    git remote rm old-repo &&
    git merge --abort
  cd -
}

importFolderFromAnotherGitRepo
#undoImportFolderFromAnotherGitRepo

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

#Source directory
git remote rm origin
#Target directory
git remote add branch-name-from-old-repo ../source_directory

на этих двух шагах я смог заставить ветку другого РЕПО появиться в том же РЕПО.

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

git br --set-upstream-to=origin/mainline

теперь он вел себя так, как-если бы это было просто еще одна ветка, которую я толкнул против того же РЕПО.


если пути для файлов, о которых идет речь, одинаковы в двух репозиториях, и вы хотите принести только один файл или небольшой набор связанных файлов, один простой способ сделать это-использовать git cherry-pick.

первым шагом является приведение коммитов из другого РЕПО в ваше собственное локальное РЕПО с помощью git fetch <remote-url>. Это уйдет FETCH_HEAD указывая на фиксацию head из другого РЕПО; если вы хотите сохранить ссылку на эту фиксацию после того, как вы сделали другие выборки, вы можете захотеть пометьте его git tag other-head FETCH_HEAD.

затем вам нужно будет создать начальную фиксацию для этого файла (если он не существует) или фиксацию, чтобы привести файл в состояние, которое может быть исправлено с первой фиксацией из другого РЕПО, которое вы хотите ввести. Вы сможете сделать это с помощью git cherry-pick <commit-0> если commit-0 представил файлы, которые вы хотите, или вам может потребоваться создать фиксацию "вручную". Добавить -n к параметрам cherry-pick, если вам нужно изменить начальную фиксацию, например, удалить файлы из этого обещай, что не захочешь вмешиваться.

после этого вы можете продолжать git cherry-pick последующие коммиты, снова используя -n где это необходимо. В простейшем случае (все коммиты-это именно то, что вы хотите и применяете чисто) вы можете дать полный список коммитов в командной строке cherry-pick: git cherry-pick <commit-1> <commit-2> <commit-3> ....