Як ви об'єднаєте два сховища Git?


1620

Розглянемо наступний сценарій:

Я розробив невеликий експериментальний проект A у власному Git repo. Зараз він дозрів, і я хотів би, щоб A став частиною більшого проекту B, який має власне велике сховище. Тепер я хотів би додати A як підкаталог Б.

Як я злити A в B, не втрачаючи історії ні з одного боку?


8
Якщо ви просто намагаєтеся об'єднати два сховища в один, без необхідності тримати обидва репозиторіїв, поглянути на це питання: stackoverflow.com/questions/13040958 / ...
Флімм

Для злиття Git репо в призначеній для користувача директорії зі збереженням всіх comits використовувати stackoverflow.com/a/43340714/1772410
Андрій Izman

Відповіді:


436

Одну гілку іншого сховища можна легко розмістити під підкаталогом, що зберігає свою історію. Наприклад:

git subtree add --prefix=rails git://github.com/rails/rails.git master

Це з'явиться як єдиний коміт, де всі файли головного відділення Rails додаються в каталог "rails". Однак назва заголовка містить посилання на старе дерево історії:

Додайте "рейли /" від фіксації <rev>

Де <rev>знаходиться хеш SHA-1. Ви все ще можете бачити історію, звинувачувати деякі зміни.

git log <rev>
git blame <rev> -- README.md

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

# finishes with all files added at once commit
git log rails/README.md

# then continue from original tree
git log <rev> -- README.md

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

Команда git-subtree є частиною офіційного git-contrib, деякі менеджери пакетів встановлюють її за замовчуванням (OS X Homebrew). Але вам, можливо, доведеться встановити його на додаток до git.


2
Нижче наведені інструкції по встановленню Git SUBTREE (станом на червень 2013 року ): stackoverflow.com/a/11613541/694469 (і я замінив git co v1.7.11.3 з ... v1.8.3).
KajMagnus

1
Дякую за голову щодо нижченаведеної відповіді. Станом на git 1.8.4 'subtree' все ще не включено (принаймні, не на Ubuntu 12.04 git ppa (ppa: git-core / ppa))
Matt Klein

1
Я можу підтвердити, що після цього git log rails/somefileне відображатиметься історія файлів файлів, за винятком об'єднання об'єднань. Як @artfulrobot запропонував, перевірте відповідь Грега Хьюгілла . І вам може знадобитися використовувати git filter-branchрепо, яке ви хочете включити.
Jifeng Zhang

6
Або прочитайте " Емір
Jifeng Zhang

4
Як казали інші, git subtreeможливо, не робити те, що ви думаєте! Дивіться тут для більш повного рішення.
Пол Дрейпер

1906

Якщо ви хочете об'єднатись project-aу project-b:

cd path/to/project-b
git remote add project-a path/to/project-a
git fetch project-a --tags
git merge --allow-unrelated-histories project-a/master # or whichever branch you want to merge
git remote remove project-a

Взято з: git spajanje різних сховищ?

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

У випадку, якщо ви хочете ввести project-aпідкаталог, ви можете скористатися git-filter-repo( filter-branchне рекомендується ). Виконайте такі команди перед командами вище:

cd path/to/project-a
git filter-repo --to-subdirectory-filter project-a

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

Примітка:--allow-unrelated-histories параметр існує тільки з Git> = 2.9. Див. Git - git merge Documentation / - дозволити неспоріднені історії

Оновлення : додано, --tagsяк запропонував @jstadler, щоб зберегти теги.


8
Це зробило для мене бізнес. Перший раз працював як шарм, маючи лише один конфлікт у файлі .gitignore! Це чудово зберегло історію комітетів. Великий плюс у порівнянні з іншими підходами - окрім простоти - полягає в тому, що при цьому не повинно бути постійних посилань на об'єднане репо. Однак, на що слід стежити - якщо ви розробник iOS, як я, - це бути дуже обережним, щоб потрапити в проектний файл цільового репо в робочу область.
Макс Маклеод

30
Дякую. Працювали для мене. Мені потрібно було перемістити об'єднаний каталог у підпапку, тому після виконання вищезазначених кроків я просто використавgit mv source-dir/ dest/new-source-dir
Sid

13
git mergeКрок зазнає невдачі тут з fatal: refusing to merge unrelated histories; --allow-unrelated-historiesвиправляє, як пояснено в документах .
ssc

19
--allow-unrelated-historiesбуло введено в git 2.9 . У попередніх версіях це було поведінку за замовчуванням.
Дуглас Ройдс

11
Коротше: git fetch /path/to/project-a master; git merge --allow-unrelated-histories FETCH_HEAD.
jthill

614

Ось два можливі рішення:

Підмодулі

Або скопіюйте сховище A в окремий каталог більшого проекту B, або (можливо, краще) клонуйте сховище A у підкаталог проекту B. Потім використовуйте підмодуль git, щоб зробити це сховище підмодулем сховища B.

Це гарне рішення для слабо пов'язаних сховищ, де розвиток в сховище А триває, і основна частина розвитку є окремою автономною розробки в області А. Дивіться також SubmoduleSupport і GitSubmoduleTutorial сторінок на Git Wiki.

Злиття піддерева

Ви можете об'єднати сховище A у підкаталог проекту B, використовуючи стратегію злиття піддерева . Це описано в « Підрядне об’єднання» та «Ви» від Маркуса Принца.

git remote add -f Bproject /path/to/B
git merge -s ours --allow-unrelated-histories --no-commit Bproject/master
git read-tree --prefix=dir-B/ -u Bproject/master
git commit -m "Merge B project as our subdirectory"
git pull -s subtree Bproject master

(Опція --allow-unrelated-historiesпотрібна для Git> = 2.9.0.)

Або ви можете скористатись інструментом Git subtree ( сховище на GitHub ) від apenwarr (Avery Pennarun), оприлюдненого, наприклад, у своєму дописі в блозі Нова альтернатива підмодулям Git: git subtree .


Я думаю, що у вашому випадку (A має бути частиною більшого проекту B), правильним рішенням було б використовувати об'єднання піддібних дерев .


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

55
це неповно . Так, ви отримуєте набір комісій, але вони більше не посилаються на правильні шляхи. git log dir-B/somefileне покаже нічого, крім одного злиття. Дивіться відповідь Грега Х'югілла на цю важливу проблему.
artfulrobot

2
ВАЖЛИВО: підтяжка git --no-rebase -s Bproject master Якщо ви цього не зробите, і ви встановили функцію автоматичного відновлення, ви отримаєте "Не вдалося розібрати об'єкт". Дивіться osdir.com/ml/git/2009-07/msg01576.html
Ерік Боуман - реферат -

4
Ця відповідь може бути заплутаною, оскільки вона містить B як об'єднане піддерево, коли у питанні це було A. Результат копіювання та вставки?
vfclists

11
Якщо ви намагаєтеся просто склеїти два сховища разом, підмодулі та злиття піддерева є неправильним інструментом для використання, оскільки вони не зберігають всю історію файлів (як зазначили інші коментатори). Дивіться stackoverflow.com/questions/13040958/… .
Ерік Лі

194

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

Перше, що потрібно використовувати, git filter-branchщоб переписати назви всього, що знаходиться у другому сховищі, щоб бути у підкаталозі, де ви хотіли б, щоб вони опинилися. Тому замість того foo.c, bar.htmlви б projb/foo.cі projb/bar.html.

Тоді ви маєте змогу зробити щось на зразок наступного:

git remote add projb [wherever]
git pull projb

За git pullзаповітом буде git fetchвиконаний a git merge. Не повинно виникати конфліктів, якщо сховище, до якого ви перебуваєте, ще не має projb/каталогу.

Далі пошук показує , що щось подібне було зроблено для злиття gitkв git. Хуніо С Хамано пише про це тут: http://www.mail-archive.com/git@vger.kernel.org/msg03395.html


4
Злиття підкремлів було б кращим рішенням, і не потрібно переписувати історію включеного проекту
Jakub Narębski

8
Мені хотілося б знати, як git filter-branchце досягти. На сторінці "man" йдеться про навпаки: зробити subdir / стати корінцем, але не навпаки.
artfulrobot

31
ця відповідь була б чудовою, якби вона пояснила, як використовувати фільтр-гілку для досягнення бажаного результату
Anentropic

14
Тут я дізнався, як використовувати гілку фільтрів: stackoverflow.com/questions/4042816/…
Девід Манор

3
Дивіться цю відповідь для реалізації контуру Грега.
Пол Дрейпер

75

git-subtree це приємно, але це, мабуть, не те, що ти хочеш.

Наприклад, якщо projectAкаталог створено в B, після git subtree,

git log projectA

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

Відповідь Грега Хьюгілла найближча, хоча насправді це не говорить про те, як переписати шляхи.


Рішення напрочуд просте.

(1) В А,

PREFIX=projectA #adjust this

git filter-branch --index-filter '
    git ls-files -s |
    sed "s,\t,&'"$PREFIX"'/," |
    GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info &&
    mv $GIT_INDEX_FILE.new $GIT_INDEX_FILE
' HEAD

Примітка. Це переписує історію, тому, якщо ви збираєтесь продовжувати використовувати це репо A, ви можете спершу клонувати (копіювати) копіювальну копію.

Примітка Bene: Вам слід змінити сценарій заміщення всередині команди sed у випадку, якщо ви використовуєте символи, що не містять ascii (або білі символи) у назвах файлів або шляху. У такому випадку розташування файлу всередині запису, створеного "ls-files -s", починається з лапки.

(2) Тоді в B біжіть

git pull path/to/A

Вуаля! У вас є projectAкаталог у B. Якщо ви запустите git log projectA, ви побачите всі комірки від A.


У моєму випадку я хотів два підкаталоги projectAта projectB. У цьому випадку я також зробив крок (1) до B.


1
Схоже, ви скопіювали свою відповідь з stackoverflow.com/a/618113/586086 ?
Ендрю Мао

1
@AndrewMao, я так думаю ... я насправді не пам'ятаю. Цей сценарій я досить мало використовував.
Пол Дрейпер

6
Я додам, що \ OS не працює на OS X, і вам доведеться ввести <tab>
Muneeb Ali

2
"$GIT_INDEX_FILE"повинні бути цитовані (двічі), інакше ваш метод не вдасться, якщо, наприклад, шлях містить пробіли.
Роб Ш

4
Якщо вам цікаво, щоб вставити <tab> в osx, вам потрібноCtrl-V <tab>
casey

48

Якщо обидва сховища мають однаковий тип файлів (наприклад, два сховища Rails для різних проектів), ви можете отримати дані вторинного сховища у ваше поточне сховище:

git fetch git://repository.url/repo.git master:branch_name

а потім об'єднати його у поточний сховище:

git merge --allow-unrelated-histories branch_name

Якщо ваша версія Git менша за 2,9, видаліть --allow-unrelated-histories.

Після цього можуть виникнути конфлікти. Ви можете вирішити їх, наприклад, за допомогою git mergetool. kdiff3може використовуватися виключно з клавіатурою, тому 5 конфліктних файлів займає при читанні коду всього кілька хвилин.

Не забудьте закінчити злиття:

git commit

25

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

git clone git@gitorious/projA.git projA
git clone git@gitorious/projB.git projB

cd projB
git remote add projA ../projA/
git fetch projA 
git rebase projA/master HEAD

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

git rebase --continue

Це призводить до того, що один проект має всі комісії від projA, а потім комісії від projB


25

У моєму випадку у мене було my-pluginсховище та main-projectсховище, і я хотів зробити вигляд, що my-pluginзавжди був розроблений у pluginsпідкаталозі main-project.

В основному я переписав історію my-pluginсховища так, що, здавалося, вся розробка відбулася в plugins/my-pluginпідкаталозі. Потім я додав історію розвитку my-pluginв main-projectісторію і з’єднав два дерева разом. Оскільки plugins/my-pluginв main-projectсховищі вже не було каталогу , це було тривіальним об'єднанням без конфліктів. Отримане сховище містило всю історію з обох оригінальних проектів і мало два корені.

TL; DR

$ cp -R my-plugin my-plugin-dirty
$ cd my-plugin-dirty
$ git filter-branch -f --tree-filter "zsh -c 'setopt extended_glob && setopt glob_dots && mkdir -p plugins/my-plugin && (mv ^(.git|plugins) plugins/my-plugin || true)'" -- --all
$ cd ../main-project
$ git checkout master
$ git remote add --fetch my-plugin ../my-plugin-dirty
$ git merge my-plugin/master --allow-unrelated-histories
$ cd ..
$ rm -rf my-plugin-dirty

Довга версія

По-перше, створіть копію my-pluginсховища, тому що ми будемо переписувати історію цього сховища.

Тепер перейдіть до кореня my-pluginсховища, перевірте свою головну гілку (можливо master) та запустіть наступну команду. Звичайно, ви повинні замінити my-pluginта pluginsякими б не були ваші фактичні імена.

$ git filter-branch -f --tree-filter "zsh -c 'setopt extended_glob && setopt glob_dots && mkdir -p plugins/my-plugin && (mv ^(.git|plugins) plugins/my-plugin || true)'" -- --all

Тепер для пояснення. git filter-branch --tree-filter (...) HEADзапускає (...)команду на кожній комісії, до якої можна дістатися HEAD. Зауважте, що це діє безпосередньо на даних, що зберігаються для кожної комісії, тому нам не потрібно турбуватися про поняття "робочий каталог", "індекс", "інсценізація" тощо.

Якщо запустити filter-branchкоманду, яка не вдасться, вона залишить після себе деякі файли в .gitкаталозі, і наступного разу, коли ви спробуєте, filter-branchвона скаржиться на це, якщо ви не надасте -fможливість filter-branch.

Що стосується фактичної команди, то мені не пощастило bashзробити те, що я хотів, тому замість цього я використовую zsh -cдля zshвиконання команди. Спочатку я встановив extended_globопцію, яка вмикає ^(...)синтаксис у mvкоманді, а також glob_dotsопцію, яка дозволяє мені вибирати точкові файли (наприклад .gitignore) з glob ( ^(...)).

Далі я використовую mkdir -pкоманду для створення обох pluginsі plugins/my-pluginодночасно.

Нарешті, я використовую функцію zsh"негативний глобул", ^(.git|plugins)щоб співставити всі файли в кореневій директорії сховища, крім .gitновоствореної my-pluginпапки. (Виключення тут .gitможе не знадобитися, але спроба перемістити каталог у себе - це помилка.)

У моєму сховищі початкова комісія не включала жодних файлів, тому mvкоманда повертала помилку на початковій фіксації (оскільки для переміщення нічого не було). Тому я додав таке, || trueщоб git filter-branchне переривати.

--allВаріант говорить filter-branchпереписати історію для всіх гілок в сховище, а додатковий --треба сказати , gitщоб інтерпретувати його як частину списку опцій для філій переписувати, а в якості опції до filter-branchсебе.

Тепер перейдіть до вашого main-projectсховища та перевірте, у яку галузь ви хочете об'єднатись. Додайте локальну копію my-pluginсховища (із зміненою історією) як віддалену програму main-projectіз:

$ git remote add --fetch my-plugin $PATH_TO_MY_PLUGIN_REPOSITORY

Зараз у вас буде два непов’язаних дерева у вашій історії комедій, які ви можете наочно візуалізувати, використовуючи:

$ git log --color --graph --decorate --all

Щоб об'єднати їх, використовуйте:

$ git merge my-plugin/master --allow-unrelated-histories

Зауважте, що в попередньому 2.9.0 Git --allow-unrelated-historiesопція не існує. Якщо ви використовуєте одну з цих версій, просто пропустіть опцію: повідомлення про помилку, яке --allow-unrelated-historiesперешкоджає, було також додано в 2.9.0.

У вас не повинно виникнути конфліктів злиття. Якщо це зробити, це, ймовірно, означає, що або filter-branchкоманда не працювала правильно, або в plugins/my-pluginкаталозі вже був каталог main-project.

Не забудьте ввести пояснювальне повідомлення про зобов’язання для будь-яких майбутніх учасників, які цікавляться, який хакері збирається зробити сховище з двома коренями.

Ви можете візуалізувати новий графік фіксації, який повинен мати два кореневі коміти, використовуючи вищевказану git logкоманду. Зауважте, що лише masterгалузь буде об’єднана . Це означає, що якщо у вас є важлива робота над іншими my-pluginгілками, які ви хочете об'єднати в main-projectдерево, вам слід утриматися від видалення my-pluginпульта, поки ви не зробите ці злиття. Якщо ви цього не зробите, то комісії з цих гілок залишатимуться у main-projectсховищі, але деякі будуть недоступними та сприйнятливими до можливого вивезення сміття. (Крім того, вам доведеться посилатися на них SHA, оскільки видалення пульта видаляє його гілки віддаленого відстеження.)

За бажанням, після того як ви об’єднали все, що хочете зберегти my-plugin, ви можете видалити my-pluginпульт за допомогою:

$ git remote remove my-plugin

Тепер ви можете безпечно видалити копію my-pluginсховища, історію якого ви змінили. У моєму випадку я також додав повідомлення про анулювання до реального my-pluginсховища після того, як злиття було завершено і натиснуто.


Тестовано на Mac OS X El Capitan з git --version 2.9.0та zsh --version 5.2. Ваш пробіг може відрізнятися.

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


1
Звідки --allow-unrelated-historiesберуться?
xpto

3
@MarceloFilho Check man git-merge. За замовчуванням команда git merge відмовляється об'єднувати історії, які не мають спільного предка. Ця опція може бути використана для подолання цієї безпеки при об'єднанні історії двох проектів, які розпочали своє життя самостійно. Оскільки це дуже рідкісний випадок, не існує жодної змінної конфігурації, яка б це могла включити за замовчуванням і не буде додана.
Радон Росборо

Чи має бути доступний на git version 2.7.2.windows.1?
xpto

2
@MarceloFilho Це було додано в 2.9.0, але у старих версіях вам не доведеться передавати параметр (він просто працюватиме). github.com/git/git/blob/…
Радон Росборо

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

9

Я намагаюся зробити те ж саме днями, я використовую git 2.7.2. Піддерево не зберігає історію.

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

Я б запропонував вам спочатку відділити В і працювати у філії.

Ось кроки без розгалуження:

cd B

# You are going to merge A into B, so first move all of B's files into a sub dir
mkdir B

# Move all files to B, till there is nothing in the dir but .git and B
git mv <files> B

git add .

git commit -m "Moving content of project B in preparation for merge from A"


# Now merge A into B
git remote add -f A <A repo url>

git merge A/<branch>

mkdir A

# move all the files into subdir A, excluding .git
git mv <files> A

git commit -m "Moved A into subdir"


# Move B's files back to root    
git mv B/* ./

rm -rf B

git commit -m "Reset B to original state"

git push

Якщо ви зараз ввійдете будь-який з файлів у підкаталог A, ви отримаєте повну історію

git log --follow A/<file>

Це був пост, який допомагає мені це зробити:

http://saintgimp.org/2013/01/22/merging-two-git-repositories-into-one-repository-without-losing-file-history/


8

Якщо ви хочете помістити файли з гілки в repo B в піддерево repo A, а також зберегти історію, продовжуйте читати. (У наведеному нижче прикладі я припускаю, що ми хочемо, щоб головна гілка repo B об'єдналася в головну гілку repo A.)

У repo A спочатку виконайте наступне, щоб зробити доступною для repo B:

git remote add B ../B # Add repo B as a new remote.
git fetch B

Тепер ми створюємо абсолютно нове відділення (лише з одним комітетом) в репо А, який ми називаємо new_b_root. У результаті комісії будуть мати файли, які були зроблені під час першого фіксування головного відділення репо B, але поміщені у підкаталог під назвою path/to/b-files/.

git checkout --orphan new_b_root master
git rm -rf . # Remove all files.
git cherry-pick -n `git rev-list --max-parents=0 B/master`
mkdir -p path/to/b-files
git mv README path/to/b-files/
git commit --date="$(git log --format='%ai' $(git rev-list --max-parents=0 B/master))"

Пояснення: --orphanПараметр команди команд перевірки перевіряє файли з головного відділення A, але не створює жодних комітів. Ми могли обрати будь-яку комісію, тому що далі ми все одно очистимо всі файли. Тоді, не виконуючи ще ( -n), ми вишнево підбираємо перший комітет з головного відділення B. (Вишня вибору зберігає оригінальне повідомлення про фіксацію, яке, схоже, не робить прямий замовлення.) Тоді ми створюємо піддерево, куди ми хочемо помістити всі файли з репо B. Тоді ми повинні перемістити всі файли, що були введені в вишневий до під дерева. У наведеному вище прикладі READMEпотрібно перемістити лише файл. Тоді ми беремо на себе кореневе зобов’язання B-repo, і, в той же час, ми також зберігаємо часову позначку первісної фіксації.

Тепер ми створимо нову B/masterгілку поверх новоствореної new_b_root. Ми називаємо нову філію b:

git checkout -b b B/master
git rebase -s recursive -Xsubtree=path/to/b-files/ new_b_root

Тепер ми об'єднуємо нашу bгалузь у A/master:

git checkout master
git merge --allow-unrelated-histories --no-commit b
git commit -m 'Merge repo B into repo A.'

Нарешті, ви можете видалити Bвіддалені та тимчасові гілки:

git remote remove B
git branch -D new_b_root b

Кінцевий графік матиме таку структуру:

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


Чудова відповідь, дякую! Я дійсно пропустив в інших відповідях з "git subtree" або "merge --allow-nepovezaними-історіями" від Andresch Serj, що в каталозі sub не було журналу.
Ілендір

8

Я зібрав тут багато інформації про Stack OverFlow тощо, і мені вдалося скласти сценарій разом, який вирішує проблему для мене.

Застереження полягає в тому, що він враховує лише гілку "розвивати" кожного сховища і об'єднує його в окремий каталог у абсолютно новому сховищі.

Теги та інші гілки ігноруються - це може бути не те, що ви хочете.

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

#!/bin/bash
#
################################################################################
## Script to merge multiple git repositories into a new repository
## - The new repository will contain a folder for every merged repository
## - The script adds remotes for every project and then merges in every branch
##   and tag. These are renamed to have the origin project name as a prefix
##
## Usage: mergeGitRepositories.sh <new_project> <my_repo_urls.lst>
## - where <new_project> is the name of the new project to create
## - and <my_repo_urls.lst> is a file contaning the URLs to the respositories
##   which are to be merged on separate lines.
##
## Author: Robert von Burg
##            eitch@eitchnet.ch
##
## Version: 0.3.2
## Created: 2018-02-05
##
################################################################################
#

# disallow using undefined variables
shopt -s -o nounset

# Script variables
declare SCRIPT_NAME="${0##*/}"
declare SCRIPT_DIR="$(cd ${0%/*} ; pwd)"
declare ROOT_DIR="$PWD"
IFS=$'\n'

# Detect proper usage
if [ "$#" -ne "2" ] ; then
  echo -e "ERROR: Usage: $0 <new_project> <my_repo_urls.lst>"
  exit 1
fi


## Script variables
PROJECT_NAME="${1}"
PROJECT_PATH="${ROOT_DIR}/${PROJECT_NAME}"
TIMESTAMP="$(date +%s)"
LOG_FILE="${ROOT_DIR}/${PROJECT_NAME}_merge.${TIMESTAMP}.log"
REPO_FILE="${2}"
REPO_URL_FILE="${ROOT_DIR}/${REPO_FILE}"


# Script functions
function failed() {
  echo -e "ERROR: Merging of projects failed:"
  echo -e "ERROR: Merging of projects failed:" >>${LOG_FILE} 2>&1
  echo -e "$1"
  exit 1
}

function commit_merge() {
  current_branch="$(git symbolic-ref HEAD 2>/dev/null)"
  if [[ ! -f ".git/MERGE_HEAD" ]] ; then
    echo -e "INFO:   No commit required."
    echo -e "INFO:   No commit required." >>${LOG_FILE} 2>&1
  else
    echo -e "INFO:   Committing ${sub_project}..."
    echo -e "INFO:   Committing ${sub_project}..." >>${LOG_FILE} 2>&1
    if ! git commit -m "[Project] Merged branch '$1' of ${sub_project}" >>${LOG_FILE} 2>&1 ; then
      failed "Failed to commit merge of branch '$1' of ${sub_project} into ${current_branch}"
    fi
  fi
}


# Make sure the REPO_URL_FILE exists
if [ ! -e "${REPO_URL_FILE}" ] ; then
  echo -e "ERROR: Repo file ${REPO_URL_FILE} does not exist!"
  exit 1
fi


# Make sure the required directories don't exist
if [ -e "${PROJECT_PATH}" ] ; then
  echo -e "ERROR: Project ${PROJECT_NAME} already exists!"
  exit 1
fi


# create the new project
echo -e "INFO: Logging to ${LOG_FILE}"
echo -e "INFO: Creating new git repository ${PROJECT_NAME}..."
echo -e "INFO: Creating new git repository ${PROJECT_NAME}..." >>${LOG_FILE} 2>&1
echo -e "===================================================="
echo -e "====================================================" >>${LOG_FILE} 2>&1
cd ${ROOT_DIR}
mkdir ${PROJECT_NAME}
cd ${PROJECT_NAME}
git init
echo "Initial Commit" > initial_commit
# Since this is a new repository we need to have at least one commit
# thus were we create temporary file, but we delete it again.
# Deleting it guarantees we don't have conflicts later when merging
git add initial_commit
git commit --quiet -m "[Project] Initial Master Repo Commit"
git rm --quiet initial_commit
git commit --quiet -m "[Project] Initial Master Repo Commit"
echo


# Merge all projects into the branches of this project
echo -e "INFO: Merging projects into new repository..."
echo -e "INFO: Merging projects into new repository..." >>${LOG_FILE} 2>&1
echo -e "===================================================="
echo -e "====================================================" >>${LOG_FILE} 2>&1
for url in $(cat ${REPO_URL_FILE}) ; do

  if [[ "${url:0:1}" == '#' ]] ; then
    continue
  fi

  # extract the name of this project
  export sub_project=${url##*/}
  sub_project=${sub_project%*.git}

  echo -e "INFO: Project ${sub_project}"
  echo -e "INFO: Project ${sub_project}" >>${LOG_FILE} 2>&1
  echo -e "----------------------------------------------------"
  echo -e "----------------------------------------------------" >>${LOG_FILE} 2>&1

  # Fetch the project
  echo -e "INFO:   Fetching ${sub_project}..."
  echo -e "INFO:   Fetching ${sub_project}..." >>${LOG_FILE} 2>&1
  git remote add "${sub_project}" "${url}"
  if ! git fetch --tags --quiet ${sub_project} >>${LOG_FILE} 2>&1 ; then
    failed "Failed to fetch project ${sub_project}"
  fi

  # add remote branches
  echo -e "INFO:   Creating local branches for ${sub_project}..."
  echo -e "INFO:   Creating local branches for ${sub_project}..." >>${LOG_FILE} 2>&1
  while read branch ; do
    branch_ref=$(echo $branch | tr " " "\t" | cut -f 1)
    branch_name=$(echo $branch | tr " " "\t" | cut -f 2 | cut -d / -f 3-)

    echo -e "INFO:   Creating branch ${branch_name}..."
    echo -e "INFO:   Creating branch ${branch_name}..." >>${LOG_FILE} 2>&1

    # create and checkout new merge branch off of master
    if ! git checkout -b "${sub_project}/${branch_name}" master >>${LOG_FILE} 2>&1 ; then failed "Failed preparing ${branch_name}" ; fi
    if ! git reset --hard ; then failed "Failed preparing ${branch_name}" >>${LOG_FILE} 2>&1 ; fi
    if ! git clean -d --force ; then failed "Failed preparing ${branch_name}" >>${LOG_FILE} 2>&1 ; fi

    # Merge the project
    echo -e "INFO:   Merging ${sub_project}..."
    echo -e "INFO:   Merging ${sub_project}..." >>${LOG_FILE} 2>&1
    if ! git merge --allow-unrelated-histories --no-commit "remotes/${sub_project}/${branch_name}" >>${LOG_FILE} 2>&1 ; then
      failed "Failed to merge branch 'remotes/${sub_project}/${branch_name}' from ${sub_project}"
    fi

    # And now see if we need to commit (maybe there was a merge)
    commit_merge "${sub_project}/${branch_name}"

    # relocate projects files into own directory
    if [ "$(ls)" == "${sub_project}" ] ; then
      echo -e "WARN:   Not moving files in branch ${branch_name} of ${sub_project} as already only one root level."
      echo -e "WARN:   Not moving files in branch ${branch_name} of ${sub_project} as already only one root level." >>${LOG_FILE} 2>&1
    else
      echo -e "INFO:   Moving files in branch ${branch_name} of ${sub_project} so we have a single directory..."
      echo -e "INFO:   Moving files in branch ${branch_name} of ${sub_project} so we have a single directory..." >>${LOG_FILE} 2>&1
      mkdir ${sub_project}
      for f in $(ls -a) ; do
        if  [[ "$f" == "${sub_project}" ]] ||
            [[ "$f" == "." ]] ||
            [[ "$f" == ".." ]] ; then
          continue
        fi
        git mv -k "$f" "${sub_project}/"
      done

      # commit the moving
      if ! git commit --quiet -m  "[Project] Move ${sub_project} files into sub directory" ; then
        failed "Failed to commit moving of ${sub_project} files into sub directory"
      fi
    fi
    echo
  done < <(git ls-remote --heads ${sub_project})


  # checkout master of sub probject
  if ! git checkout "${sub_project}/master" >>${LOG_FILE} 2>&1 ; then
    failed "sub_project ${sub_project} is missing master branch!"
  fi

  # copy remote tags
  echo -e "INFO:   Copying tags for ${sub_project}..."
  echo -e "INFO:   Copying tags for ${sub_project}..." >>${LOG_FILE} 2>&1
  while read tag ; do
    tag_ref=$(echo $tag | tr " " "\t" | cut -f 1)
    tag_name_unfixed=$(echo $tag | tr " " "\t" | cut -f 2 | cut -d / -f 3)

    # hack for broken tag names where they are like 1.2.0^{} instead of just 1.2.0
    tag_name="${tag_name_unfixed%%^*}"

    tag_new_name="${sub_project}/${tag_name}"
    echo -e "INFO:     Copying tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}..."
    echo -e "INFO:     Copying tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}..." >>${LOG_FILE} 2>&1
    if ! git tag "${tag_new_name}" "${tag_ref}" >>${LOG_FILE} 2>&1 ; then
      echo -e "WARN:     Could not copy tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}"
      echo -e "WARN:     Could not copy tag ${tag_name_unfixed} to ${tag_new_name} for ref ${tag_ref}" >>${LOG_FILE} 2>&1
    fi
  done < <(git ls-remote --tags --refs ${sub_project})

  # Remove the remote to the old project
  echo -e "INFO:   Removing remote ${sub_project}..."
  echo -e "INFO:   Removing remote ${sub_project}..." >>${LOG_FILE} 2>&1
  git remote rm ${sub_project}

  echo
done


# Now merge all project master branches into new master
git checkout --quiet master
echo -e "INFO: Merging projects master branches into new repository..."
echo -e "INFO: Merging projects master branches into new repository..." >>${LOG_FILE} 2>&1
echo -e "===================================================="
echo -e "====================================================" >>${LOG_FILE} 2>&1
for url in $(cat ${REPO_URL_FILE}) ; do

  if [[ ${url:0:1} == '#' ]] ; then
    continue
  fi

  # extract the name of this project
  export sub_project=${url##*/}
  sub_project=${sub_project%*.git}

  echo -e "INFO:   Merging ${sub_project}..."
  echo -e "INFO:   Merging ${sub_project}..." >>${LOG_FILE} 2>&1
  if ! git merge --allow-unrelated-histories --no-commit "${sub_project}/master" >>${LOG_FILE} 2>&1 ; then
    failed "Failed to merge branch ${sub_project}/master into master"
  fi

  # And now see if we need to commit (maybe there was a merge)
  commit_merge "${sub_project}/master"

  echo
done


# Done
cd ${ROOT_DIR}
echo -e "INFO: Done."
echo -e "INFO: Done." >>${LOG_FILE} 2>&1
echo

exit 0

Ви також можете отримати його з http://paste.ubuntu.com/11732805

Спочатку створіть файл із URL-адресою кожного сховища, наприклад:

git@github.com:eitchnet/ch.eitchnet.parent.git
git@github.com:eitchnet/ch.eitchnet.utils.git
git@github.com:eitchnet/ch.eitchnet.privilege.git

Потім зателефонуйте до скрипту із зазначенням назви проекту та шляху до сценарію:

./mergeGitRepositories.sh eitchnet_test eitchnet.lst

Сам сценарій має багато коментарів, які повинні пояснити, що він робить.


Замість того, щоб спрямовувати читачів на відповідь, будь ласка, опублікуйте відповідь тут (він же редагувати те, що ви сказали в цьому коментарі, у цій відповіді).
josliber

1
Звичайно, просто подумав, що краще не повторюватися ... =)
eitch

Якщо ви вважаєте, що це питання є ідентичним іншому, тоді ви можете позначити його як дублікат, використовуючи посилання "flag" під самим питанням та вказуючи інше питання. Якщо це не повторне запитання, але ви вважаєте, що для вирішення обох питань може бути використана однакова відповідь, тоді просто опублікуйте однакову відповідь на обидва питання (як ви це зробили зараз). Дякуємо за ваш внесок!
josliber

Дивовижний! Не працював у Windows bash prompt, але він бездоганно працював і утворював поле Vagrant під управлінням ubuntu. Яка економія часу!
xverges

Щасливий, що можемо вам послужити =)
eitch

7

Я знаю, що це довго після факту, але я не був задоволений іншими відповідями, які я знайшов тут, тому написав це:

me=$(basename $0)

TMP=$(mktemp -d /tmp/$me.XXXXXXXX)
echo 
echo "building new repo in $TMP"
echo
sleep 1

set -e

cd $TMP
mkdir new-repo
cd new-repo
    git init
    cd ..

x=0
while [ -n "$1" ]; do
    repo="$1"; shift
    git clone "$repo"
    dirname=$(basename $repo | sed -e 's/\s/-/g')
    if [[ $dirname =~ ^git:.*\.git$ ]]; then
        dirname=$(echo $dirname | sed s/.git$//)
    fi

    cd $dirname
        git remote rm origin
        git filter-branch --tree-filter \
            "(mkdir -p $dirname; find . -maxdepth 1 ! -name . ! -name .git ! -name $dirname -exec mv {} $dirname/ \;)"
        cd ..

    cd new-repo
        git pull --no-commit ../$dirname
        [ $x -gt 0 ] && git commit -m "merge made by $me"
        cd ..

    x=$(( x + 1 ))
done

2
Це саме те, що я шукав. Дякую! Однак мені довелося змінити рядок 22 на:if [[ $dirname =~ ^.*\.git$ ]]; then
гейман

2
^. * blarg $ є марно жадібним RE. Краще сказати .blarg $ і пропустити передній якір.
jettero

7

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


1
Ваше рішення добре працює лише для нового сховища, але як щодо об’єднання репо в інший із конфліктами файлів?
Андрій Ізман

6

У мене був подібний виклик, але в моєму випадку ми розробили одну версію кодової бази в repo A, потім клонували її до нової репо, репо B, для нової версії продукту. Виправивши деякі помилки в репо A, нам потрібно було перевірити зміни у репо B. Закінчивши це:

  1. Додавання пульта до репо B, яке вказувало на репо A (git remote add ...)
  2. Потягнення поточної гілки (ми не використовували майстер для виправлення помилок) (git pull remoteForRepoA bugFixBranch)
  3. Натискання зливається з github

Відпрацювали частування :)


5

Схожий на @Smar, але використовує шляхи до файлової системи, встановлені в PRIMARY і SECONDARY:

PRIMARY=~/Code/project1
SECONDARY=~/Code/project2
cd $PRIMARY
git remote add test $SECONDARY && git fetch test
git merge test/master

Потім ви вручну зливаєтеся.

(адаптовано з допису Анара Манафова )


5

Об’єднання 2 репостів

git clone ssh://<project-repo> project1
cd project1
git remote add -f project2 project2
git merge --allow-unrelated-histories project2/master
git remote rm project2

delete the ref to avoid errors
git update-ref -d refs/remotes/project2/master

4

Коли ви хочете об'єднати три чи більше проектів в один комітет, виконайте дії, описані в інших відповідях ( remote add -f, merge). Потім (м'який) скиньте індекс до старої голови (там, де не відбулося злиття). Додайте всі файли ( git add -A) і виконайте їх (повідомлення "Об'єднання проектів A, B, C і D в один проект). Це тепер ідентифікатор" master ".

Тепер створіть .git/info/graftsіз наступним вмістом:

<commit-id of master> <list of commit ids of all parents>

Біжи git filter-branch -- head^..head head^2..head head^3..head. Якщо у вас більше трьох гілок, просто додайте стільки, скільки head^n..headу вас є гілок. Щоб оновити теги, додайте --tag-name-filter cat. Не завжди додайте це, оскільки це може спричинити перезапис деяких комітетів. Детальніше див. Сторінку людини фільтр-філія , пошук "трансплантатів".

Тепер у вашому останньому зобов'язанні пов’язані права батьків.


1
Зачекайте, чому ви хочете об'єднати три проекти в один комітет?
Стів Беннетт

Я почав із сховища, клієнта-сховища та моделера як окремих git-проектів. Це було важко для колег, тому я приєднався до них в одному проекті git. Щоб мати можливість, що "корінь" нового проекту походить від трьох інших проектів, я хотів мати єдине об'єднання об'єднань.
коппор

4

Щоб об'єднати A в B:

1) У проекті A

git fast-export --all --date-order > /tmp/ProjectAExport

2) У проекті В

git checkout -b projectA
git fast-import --force < /tmp/ProjectAExport

У цій галузі виконайте всі операції, які вам потрібно зробити, і виконайте їх.

В) Потім поверніться до головного і класичне злиття між двома гілками:

git checkout master
git merge projectA

2

Ця функція буде клонувати віддалений репо в локальний репо-реп, після об'єднання всіх комітетів буде збережено, 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


1

Дана команда - найкраще можливе рішення, яке я пропоную.

git subtree add --prefix=MY_PROJECT git://github.com/project/my_project.git master

1

Я зливаю проекти трохи вручну, що дозволяє мені уникати необхідності вирішувати конфлікти злиття.

по-перше, скопіюйте у файли іншого проекту, проте ви хочете їх.

cp -R myotherproject newdirectory
git add newdirectory

Наступний потяг в історії

git fetch path_or_url_to_other_repo

скажіть git, щоб злитися в історії останньої видобутої речі

echo 'FETCH_HEAD' > .git/MERGE_HEAD

тепер взяти на себе зобов’язання, однак ви зазвичай брали на себе зобов'язання

git commit

0

Я хотів перенести невеликий проект у підкаталог більшого. Оскільки мій маленький проект не мав багато комісій, я використовував git format-patch --output-directory /path/to/patch-dir. Тоді на більшому проекті я використав git am --directory=dir/in/project /path/to/patch-dir/*.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.