Як імпортувати наявне сховище Git в інше?


476

У мене є сховище Git у папці під назвою XXX , і у мене є друге сховище Git під назвою YYY .

Я хочу імпортувати сховище XXX у сховище YYY як підкаталог з назвою ZZZ і додати всю історію змін XXX до YYY .

Структура папки перед:

├── XXX
│   ├── .git
│   └── (project files)
└── YYY
    ├── .git
    └── (project files)

Структура папки після:

YYY
├── .git  <-- This now contains the change history from XXX
├──  ZZZ  <-- This was originally XXX
│    └── (project files)
└──  (project files)

Чи можна це зробити чи потрібно вдатися до використання підмодулів?


2
У Github тепер це можна зробити з веб-інтерфейсу, коли ви створюєте нове репо
bgcode

Відповіді:


430

Напевно, найпростішим способом було б витягнути XXX речі у гілку YYY, а потім об'єднати їх у головний:

У РРР :

git remote add other /path/to/XXX
git fetch other
git checkout -b ZZZ other/master
mkdir ZZZ
git mv stuff ZZZ/stuff                      # repeat as necessary for each file/dir
git commit -m "Moved stuff to ZZZ"
git checkout master                
git merge ZZZ --allow-unrelated-histories   # should add ZZZ/ to master
git commit
git remote rm other
git branch -d ZZZ                           # to get rid of the extra branch before pushing
git push                                    # if you have a remote, that is

Я насправді просто спробував це з парою своїх репостів, і це працює. На відміну від відповіді Йорга, це не дозволить вам продовжувати використовувати інше репо, але я не думаю, що ви все одно це вказали.

Примітка. Оскільки це було спочатку написано у 2009 році, git додав злиття піддерев'я, згадане у відповіді нижче. Я, мабуть, використовую цей метод сьогодні, хоча, звичайно, цей метод все ще працює.


1
Дякую. Я використовував трохи модифіковану версію вашої техніки: я створив гілку "постановки" на XXX, де створив папку ZZZ, і перемістив у неї "речі". Тоді я об'єднав XXX у РРРР.
Vijay Patel

1
Це спрацювало для мене чудово. Єдині зміни, які я вніс, були: 1) "git гілка -d ZZZ" перед натиском, тому що я не хотів, щоб ця тимчаста гілка звисала. 2) "git push" давав мені помилку: "Немає загальних запитів і жодних не вказано; нічого не роблячи. Можливо, вам слід вказати галузь типу" master "." (Походження, на яке я підштовхував, було порожнім оголеним сховищем.) Але "git push - всі" працювали як шампіньон.
CrazyPyro

1
Я хотів закінчити лише папку ZZZ плюс історію в репортажі YYY: я хотів видалити оригінал XXX РЕПО і гілку ZZZ в репо YYY. Я виявив видалення гілки ZZZ, оскільки @CrazyPyro запропонував видалити історію - щоб зберегти її, я об'єднав гілку ZZZ у головну перед видаленням.
Олі Студольме

4
@SebastianBlask Я просто заплутався з цим двома моїми репортажами і зрозумів, що є пропущений крок, якого ніхто, здавалося, ніколи не помічав, незважаючи на те, що роками я звертався до цього питання. :-) Я згадав про об'єднання його в master, але насправді цього не показав. Редагування зараз ...
ebneter

2
Ви можете додати щось подібне під час переміщення файлів у свою папку: git mv $(ls|grep -v <your foldername>) <your foldername>/ Це скопіює всі файли та папки у вашу нову папку
serup

366

Якщо ви хочете зберегти точну історію фіксації другого сховища і, отже, також зберегти можливість легко зливати зміни вгору за потоком в майбутньому, то ось вам потрібний метод. Це призводить до того, що немодифікована історія імпорту піддерева у ваше репортаж плюс одна фіксація злиття для переміщення об'єднаного сховища у підкаталог.

git remote add XXX_remote <path-or-url-to-XXX-repo>
git fetch XXX_remote
git merge -s ours --no-commit --allow-unrelated-histories XXX_remote/master
git read-tree --prefix=ZZZ/ -u XXX_remote/master
git commit -m "Imported XXX as a subtree."

Ви можете відстежувати зміни вгору за потоком:

git pull -s subtree XXX_remote master

Git з'ясовує самостійно, де є корені перед тим, як здійснити злиття, тому вам не потрібно вказувати префікс на наступних злиттях.

Недоліком є те , що в об'єднаному історії ці файли без префікса (не в підкаталозі). Як результат, git log ZZZ/aви покажете всі зміни (якщо такі є), крім тих, які є в об'єднаній історії. Ви можете зробити:

git log --follow -- a

але це не відображатиме інших змін, ніж у об'єднаній історії.

Іншими словами, якщо ви не змінюєте ZZZфайли файлів у сховищі XXX, вам потрібно вказати --followі нефіксований шлях. Якщо ви змінюєте їх в обох сховищах, то у вас є 2 команди, жодна з яких не відображає всіх змін.

Версії Git до 2.9 : --allow-unrelated-historiesопцію не потрібно передавати git merge.

Метод в іншій відповіді, який використовує read-treeта пропускає merge -s oursкрок, фактично не відрізняється від копіювання файлів на cp та фіксації результату.

Оригінальне джерело було із статті довідки github "Злиття підкреслень" . І ще одне корисне посилання .


9
це, здається, не зберегло історію ... якщо я роблю git logбудь-який із файлів, які я втягнув, я просто бачу єдине об'єднання об'єднань і нічого з попереднього життя в іншому репо? Git 1.8.0
Anentropic

8
ага! якщо я використовую старий шлях імпортованого файлу, тобто пропускаю субдір, в який він був імпортований, то git log дасть мені історію фіксації, наприклад git log -- myfileзамістьgit log -- rack/myfile
Anentropic

2
@FrancescoFrassinelli, хіба це не бажано? Особливістю цього методу є внесення історії в історію .
patrickvacek

4
@FrancescoFrassinelli, якщо ви не хочете історії, чому б просто не зробити звичайну копію? Я намагаюся розібратися, що б привернуло вас до цього методу, якби не історія - це єдина причина, що я застосував цей метод!
patrickvacek

7
Оскільки Git 2.9, вам потрібна опція --allow-unrelated-historiesпід час злиття.
stuXnet

112

git-subtree- це сценарій, призначений саме для цього випадку використання об'єднання декількох сховищ в одне, зберігаючи історію (та / або розділяючи історію підрядів, хоча це питання не має значення для цього питання). Він розповсюджується як частина дерева git з моменту випуску 1.7.11 .

Щоб об'єднати сховище <repo>при редакції <rev>як підкаталог <prefix>, використовуйте git subtree addнаступне:

git subtree add -P <prefix> <repo> <rev>

git-subtree реалізує стратегію злиття піддерева більш зручним для користувачів способом.

Для вашого випадку, всередині сховища YYY, ви запускаєте:

git subtree add -P ZZZ /path/to/XXX.git master

Недоліком є те , що в об'єднаному історії ці файли без префікса (не в підкаталозі). Як результат, git log ZZZ/aви покажете всі зміни (якщо такі є), крім тих, які є в об'єднаній історії. Ви можете зробити:

git log --follow -- a

але це не відображатиме інших змін, ніж у об'єднаній історії.

Іншими словами, якщо ви не змінюєте ZZZфайли файлів у сховищі XXX, вам потрібно вказати --followі нефіксований шлях. Якщо ви змінюєте їх в обох сховищах, то у вас є 2 команди, жодна з яких не відображає всіх змін.

Детальніше про це тут .


4
Якщо у вас є каталог для об'єднання замість голого сховища або віддаленого сховища,git subtree add -P name-of-desired-prefix ~/location/of/git/repo-without-.git branch-name
Татш,

2
Досвід Noob: git (версія 2.9.0.Windows.1) відповідає "фатально: неоднозначний аргумент" HEAD ": невідома редакція чи шлях не в робочому дереві", коли я спробував це у свіжому ініціалізованому, локальному, неоголеному сховищі, Але це спрацювало чудово після того, як я дійсно отримав новий сховище, тобто після додавання простого файлу та доручення звичайного шляху.
Штейн

Чудово працював за моїм сценарієм.
Джонні Юта

О, це фантастично.
dwjohnston

Я використовував пропозицію @Tatsh, і це спрацювало на мене
Carmine Tambascia

49

Відомий приклад цього в самому сховищі Git, який у спільноті Git спільно відомий як " найкрутіше злиття коли-небудь " (після тематичного рядка Лінус Торвальдс, який використовується в електронному листі до списку розсилки Git, який описує це злиття). У цьому випадку gitkграфічний інтерфейс Git, який зараз є частиною власне Git, насправді був окремим проектом. Лінусу вдалося об'єднати це сховище у сховище Git таким чином

  • він з’являється у сховищі Git так, ніби він завжди розроблявся як частина Git,
  • всю історію зберігають недоторканою і
  • він все ще може бути розроблений самостійно у своєму старому сховищі, при цьому зміни просто git pullредагуються

Електронна пошта містить кроки, необхідні для відтворення, але це не для слабкого серця: по-перше, Лінус написав Гіта, тож він, напевно, знає про це трохи більше, ніж ти чи я, по-друге, це було майже 5 років тому і Git покращилася значно відтоді, так що, може бути , тепер набагато простіше.

Зокрема, я думаю, що сьогодні в такому конкретному випадку можна використовувати підмодуль gitk.


3
До речі. стратегія, яка використовується для наступних злиття (якщо такі є), називається піддією злиття, і є сторонній git-subtreeінструмент, який може вам допомогти у цьому: github.com/apenwarr/git-subtree
Jakub Narębski

Спасибі, я про це забув. Стратегія subtreeзлиття, особливо у поєднанні з git-subtreeінструментом, є приємною, можливо, навіть чудовою альтернативою субмодулям.
Йорг W Міттаг

12

Простий спосіб зробити це - використовувати git format-patch.

Припустимо, у нас є 2 сховища git foo та bar .

foo містить:

  • foo.txt
  • .git

бар містить:

  • bar.txt
  • .git

і ми хочемо закінчити foo, що містить історію панелі та ці файли:

  • foo.txt
  • .git
  • foobar / bar.txt

Отже, щоб зробити це:

 1. create a temporary directory eg PATH_YOU_WANT/patch-bar
 2. go in bar directory
 3. git format-patch --root HEAD --no-stat -o PATH_YOU_WANT/patch-bar --src-prefix=a/foobar/ --dst-prefix=b/foobar/
 4. go in foo directory
 5. git am PATH_YOU_WANT/patch-bar/*

І якщо ми хочемо переписати всі повідомлення повідомлення з бар, які ми можемо зробити, наприклад, в Linux:

git filter-branch --msg-filter 'sed "1s/^/\[bar\] /"' COMMIT_SHA1_OF_THE_PARENT_OF_THE_FIRST_BAR_COMMIT..HEAD

Це додасть "[bar]" на початку кожного повідомлення про фіксацію.


Якщо оригінальний сховище містив гілки та злиття, git amшвидше за все , вийде з ладу.
Адам Монсен

1
Незначна готча: git am знімає що-небудь [ ]із повідомлення про фіксацію. Тож вам слід використовувати інший маркер, ніж[bar]
HRJ

Не працювали для мене. Отримав "помилку: foobar / mySubDir / test_host1: не існує в індексі. Копія патча, який не вдалося знайти, знаходиться у: /home/myuser/src/proj/.git/rebase-apply/patch Після вирішення цієї проблеми , запустіть "git am - продовжуйте". Це було після застосування 11 патчів (із 60).
oligofren

1
Цей блог має аналогічну відповідь на дещо інше питання (переміщення лише вибраних файлів).
Джессі Глік

Я бачу один недолік, всі комісії додаються до HEAD цільового сховища.
CSchulz

8

Ця функція буде клонувати віддалене репо в локальний репо-реп, після об'єднання всіх комітетів буде збережено, git logбуде показано оригінальні коміти та правильні шляхи:

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

Якщо внести невеликі зміни, ви навіть можете перемістити файли / режими об'єднаного репо в різні контури, наприклад:

repo="https://github.com/example/example"
path="$(pwd)"

tmp="$(mktemp -d)"
remote="$(echo "$tmp" | sed 's/\///g' | sed 's/\./_/g')"

git clone "$repo" "$tmp"
cd "$tmp"

GIT_ADD_STORED=""

function git-mv-store
{
    from="$(echo "$1" | sed 's/\./\\./')"
    to="$(echo "$2" | sed 's/\./\\./')"

    GIT_ADD_STORED+='s,\t'"$from"',\t'"$to"',;'
}

# NOTICE! This paths used for example! Use yours instead!
git-mv-store 'public/index.php' 'public/admin.php'
git-mv-store 'public/data' 'public/x/_data'
git-mv-store 'public/.htaccess' '.htaccess'
git-mv-store 'core/config' 'config/config'
git-mv-store 'core/defines.php' 'defines/defines.php'
git-mv-store 'README.md' 'doc/README.md'
git-mv-store '.gitignore' 'unneeded/.gitignore'

git filter-branch --index-filter '
    git ls-files -s |
    sed "'"$GIT_ADD_STORED"'" |
    GIT_INDEX_FILE="$GIT_INDEX_FILE.new" git update-index --index-info &&
    mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
' HEAD

GIT_ADD_STORED=""

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"

Повідомлення
Шляхи замінюються через sed, тому переконайтеся, що він перемістився правильними шляхами після об’єднання. Параметр існує тільки з Git> = 2.9.
--allow-unrelated-histories


2
Для ОС X X там встановіть, gnu-sedщоб git-add-repoфункція працювала. Ще раз дякую Андрію!
ptaylor

7

Виходячи з цієї статті , використання піддіаграму - це те, що працювало для мене, і передалася лише застосовна історія. Опублікуйте тут, якщо комусь потрібні кроки (обов'язково замініть заповнювачі на значення, що стосуються вас):

у вашому вихідному сховищі розділити підпапку на нову гілку

git subtree split --prefix=<source-path-to-merge> -b subtree-split-result

у вашому місці репо злиття у відділенні результатів розділення

git remote add merge-source-repo <path-to-your-source-repository>
git fetch merge-source-repo
git merge -s ours --no-commit merge-source-repo/subtree-split-result
git read-tree --prefix=<destination-path-to-merge-into> -u merge-source-repo/subtree-split-result

підтвердити свої зміни та взяти на себе зобов'язання

git status
git commit

Не забувайте

Очистіть, видаливши subtree-split-resultгілку

git branch -D subtree-split-result

Видаліть пульт, який ви додали, щоб отримати дані з джерела репо

git remote rm merge-source-repo


3

Додавання ще однієї відповіді, оскільки я думаю, що це трохи простіше. Витягнення repo_dest робиться в repo_to_import, а потім виконується натискання --set-up-upstream URL: repo_dest master.

Цей метод працював для мене, імпортуючи кілька менших репостів у більші.

Як імпортувати: repo1_to_import в repo_dest

# checkout your repo1_to_import if you don't have it already 
git clone url:repo1_to_import repo1_to_import
cd repo1_to_import

# now. pull all of repo_dest
git pull url:repo_dest
ls 
git status # shows Your branch is ahead of 'origin/master' by xx commits.
# now push to repo_dest
git push --set-upstream url:repo_dest master

# repeat for other repositories you want to import

Перейменуйте або перемістіть файли та файли в потрібне місце в оригінальному репо, перш ніж здійснити імпорт. напр

cd repo1_to_import
mkdir topDir
git add topDir
git mv this that and the other topDir/
git commit -m"move things into topDir in preparation for exporting into new repo"
# now do the pull and push to import

Метод, описаний на наступному посиланні, надихнув цю відповідь. Мені це сподобалось, як здавалося простіше. АЛЕ Остерігайся! Будуть дракони! https://help.github.com/articles/importing-an-external-git-repository git push --mirror url:repo_dest виштовхує локальну історію репортажу та стан до віддаленого (url: repo_dest). АЛЕ воно видаляє стару історію та стан віддаленого. Весело настає! : -E


1

У моєму випадку я хотів імпортувати лише деякі файли з іншого сховища (XXX). Піддерево було для мене занадто складним, і інші рішення не спрацювали. Ось що я зробив:

ALL_COMMITS=$(git log --reverse --pretty=format:%H -- ZZZ | tr '\n' ' ')

Це дає вам розділений пробілом список усіх комітетів, які впливають на файли, які я хотів імпортувати (ZZZ) у зворотному порядку (можливо, вам доведеться додати - нижче, щоб також захопити перейменування). Потім я зайшов у цільовий сховище (YYY), додав інше сховище (XXX) як віддалене, зробив його з нього і нарешті:

git cherry-pick $ALL_COMMITS

який додає всі зобов’язання до вашого відділення, таким чином ви матимете всі файли з їх історією, і ви можете робити з ними все, що завгодно, як ніби вони завжди були в цьому сховищі.


1

Див. Основний приклад у цій статті та розглянемо таке відображення у сховищах:

  • A<-> YYY,
  • B <-> XXX

Після всієї діяльності, описаної в цьому розділі (після злиття), видаліть гілку B-master:

$ git branch -d B-master

Потім натисніть на зміни.

Це працює для мене.


0

Я був у ситуації, коли мене шукали, -s theirsале, звичайно, цієї стратегії не існує. Моя історія полягала в тому, що я подав проект на GitHub, і тепер я чомусь masterне міг бути об'єднаним, upstream/masterхоча я не вніс жодних місцевих змін у цю галузь. (Дійсно, не знаю, що там сталося - мабуть, нагорі за течією, можливо, було зроблено кілька брудних штовхань за лаштунками, можливо?)

Що я в кінцевому підсумку робив

# as per https://help.github.com/articles/syncing-a-fork/
git fetch upstream
git checkout master
git merge upstream/master
....
# Lots of conflicts, ended up just abandonging this approach
git reset --hard   # Ditch failed merge
git checkout upstream/master
# Now in detached state
git branch -d master # !
git checkout -b master   # create new master from upstream/master

Тож тепер моя masterзнову синхронізована з upstream/master(і ви можете повторити вищезазначене для будь-якої іншої гілки, яку ви також хочете синхронізувати аналогічно).


1
А git reset --hard upstream/masterу вашому місцевому masterвідділенні зробить цю роботу. Таким чином, ви не втрачаєте конфлікт локальної гілки - такі речі, як типова верхня частина за течією.
tomekwi

0

Я можу запропонувати інше рішення (альтернатива git-підмодулям ) для вашої проблеми - інструмент gil (git links)

Це дозволяє описувати та керувати складними залежностями git-сховищ.

Крім того, це забезпечує вирішення проблеми залежності рекурсивних субмодулів git .

Розглянемо, що у вас є такі проектні залежності: зразок графіка залежності репозиторію git

Тоді ви можете визначити .gitlinksфайл із описом відношення репозиторіїв:

# Projects
CppBenchmark CppBenchmark https://github.com/chronoxor/CppBenchmark.git master
CppCommon CppCommon https://github.com/chronoxor/CppCommon.git master
CppLogging CppLogging https://github.com/chronoxor/CppLogging.git master

# Modules
Catch2 modules/Catch2 https://github.com/catchorg/Catch2.git master
cpp-optparse modules/cpp-optparse https://github.com/weisslj/cpp-optparse.git master
fmt modules/fmt https://github.com/fmtlib/fmt.git master
HdrHistogram modules/HdrHistogram https://github.com/HdrHistogram/HdrHistogram_c.git master
zlib modules/zlib https://github.com/madler/zlib.git master

# Scripts
build scripts/build https://github.com/chronoxor/CppBuildScripts.git master
cmake scripts/cmake https://github.com/chronoxor/CppCMakeScripts.git master

Кожен рядок описує посилання git у такому форматі:

  1. Унікальна назва сховища
  2. Відносний шлях сховища (починається з шляху файлу .gitlinks)
  3. Репозиторій Git, який буде використовуватися в команді git clone відділення сховища для оформлення замовлення
  4. Порожній рядок або рядок, розпочатий з #, не розбираються (трактуються як коментар).

Нарешті, вам потрібно оновити сховище вашого кореневого зразка:

# Clone and link all git links dependencies from .gitlinks file
gil clone
gil link

# The same result with a single command
gil update

В результаті ви будете клонувати всі необхідні проекти та зв’язувати їх між собою належним чином.

Якщо ви хочете здійснити всі зміни в якомусь сховищі зі всіма змінами в дочірніх сховищах, це можна зробити за допомогою однієї команди:

gil commit -a -m "Some big update"

Команди "pull", "push" працює аналогічно:

gil pull
gil push

Інструмент Gil (git-посилання) підтримує такі команди:

usage: gil command arguments
Supported commands:
    help - show this help
    context - command will show the current git link context of the current directory
    clone - clone all repositories that are missed in the current context
    link - link all repositories that are missed in the current context
    update - clone and link in a single operation
    pull - pull all repositories in the current directory
    push - push all repositories in the current directory
    commit - commit all repositories in the current directory

Детальніше про проблему залежності рекурсивних субмодулів git .


0

Дозвольте мені використовувати імена a(замість XXXі ZZZ) та b(замість YYY), оскільки це робить опис трохи простішим для читання.

Припустимо , ви хочете об'єднати репозиторій aв b(я припускаю , що вони розташовані поруч один з одним):

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

Для цього вам потрібно git-filter-repoвстановити ( filter-branchне рекомендується ).

Приклад об’єднання 2 великих сховищ, розміщення одного з них у підкаталозі: https://gist.github.com/x-yuri/9890ab1079cf4357d6f269d073fd9731

Детальніше про це тут .


-1

Я не знаю простого способу зробити це. Ви можете зробити це:

  1. Для додавання супер-каталогу ZZZ у сховище XXX використовуйте гіт-фільтр git
  2. Перемістіть нову гілку до сховища YYY
  3. З’єднайте висунуту гілку зі стволом YYY.

Я можу редагувати деталі, якщо це звучить привабливо.


-2

Я думаю, що ви можете це зробити, використовуючи 'git mv' та 'git pull'.

Я справедливий git noob - тому будьте обережні з вашим основним сховищем, - але я просто спробував це в темп-режимі і, здається, працює.

Спочатку - перейменуйте структуру XXX, щоб вона відповідала тому, як ви хочете, щоб вона виглядала, коли вона знаходиться в межах РРР:

cd XXX
mkdir tmp
git mv ZZZ tmp/ZZZ
git mv tmp ZZZ

Тепер XXX виглядає так:

XXX
 |- ZZZ
     |- ZZZ

Тепер використовуйте "git pull", щоб отримати зміни в межах:

cd ../YYY
git pull ../XXX

Тепер РРР виглядає так:

YYY
 |- ZZZ
     |- ZZZ
 |- (other folders that already were in YYY)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.