Bagaimana Anda menggabungkan dua repositori Git?


1622

Pertimbangkan skenario berikut:

Saya telah mengembangkan proyek eksperimental kecil A di repo Git sendiri. Sekarang telah matang, dan saya ingin A menjadi bagian dari proyek B yang lebih besar, yang memiliki repositori besar sendiri. Sekarang saya ingin menambahkan A sebagai subdirektori dari B.

Bagaimana cara menggabungkan A menjadi B, tanpa kehilangan riwayat di pihak mana pun?


8
Jika Anda hanya ingin menggabungkan dua repositori menjadi satu, tanpa perlu mempertahankan kedua repositori, lihat pertanyaan ini: stackoverflow.com/questions/13040958/…
Flimm

Untuk menggabungkan git repo di dir kustom dengan menyimpan semua komit, gunakan stackoverflow.com/a/43340714/1772410
Andrey Izman

Jawaban:


437

Cabang tunggal dari repositori lain dapat dengan mudah ditempatkan di bawah subdirektori yang menyimpan sejarahnya. Sebagai contoh:

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

Ini akan muncul sebagai komit tunggal di mana semua file cabang master Rails ditambahkan ke direktori "rails". Namun judul komit berisi referensi ke pohon sejarah lama:

Tambahkan 'rails /' dari commit <rev>

Di mana <rev>hash komit SHA-1. Anda masih bisa melihat sejarah, menyalahkan beberapa perubahan.

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

Perhatikan bahwa Anda tidak dapat melihat awalan direktori dari sini karena ini adalah cabang lama aktual yang tersisa. Anda harus memperlakukan ini seperti komit pemindahan file: Anda perlu lompatan ekstra saat mencapainya.

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

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

Ada solusi yang lebih kompleks seperti melakukan ini secara manual atau menulis ulang sejarah seperti yang dijelaskan dalam jawaban lain.

Perintah git-subtree adalah bagian dari git-contrib resmi, beberapa manajer paket menginstalnya secara default (OS X Homebrew). Tetapi Anda mungkin harus menginstalnya sendiri selain git.


2
Berikut adalah petunjuk tentang cara menginstal Git SubTree (per Juni 2013): stackoverflow.com/a/11613541/694469 (dan saya ganti git co v1.7.11.3 dengan ... v1.8.3).
KajMagnus

1
Terima kasih atas jawabannya di bawah. Sampai git 1.8.4 'subtree' masih belum termasuk (setidaknya tidak di Ubuntu 12,04 git ppa (ppa: git-core / ppa))
Matt Klein

1
Saya dapat mengonfirmasi bahwa setelah ini, git log rails/somefiletidak akan menampilkan file yang melakukan riwayat kecuali komit gabungan. Seperti yang disarankan @artfulrobot, periksa jawaban Greg Hewgill . Dan Anda mungkin perlu menggunakan git filter-branchrepo yang ingin Anda sertakan.
Jifeng Zhang

6
Atau baca Eric Lee "Menggabungkan Dua Repositori Git Menjadi Satu Repositori Tanpa Kehilangan Riwayat File" saintgimp.org/2013/01/22/…
Jifeng Zhang

4
Seperti yang orang lain katakan, git subtreemungkin tidak melakukan apa yang Anda pikirkan! Lihat di sini untuk solusi yang lebih lengkap.
Paul Draper

1909

Jika Anda ingin bergabung project-ake 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

Diambil dari: git menggabungkan repositori yang berbeda?

Metode ini bekerja cukup baik untuk saya, lebih pendek dan menurut saya lebih bersih.

Jika Anda ingin dimasukkan project-ake dalam subdirektori, Anda dapat menggunakan git-filter-repo( filter-branchtidak disarankan ). Jalankan perintah berikut sebelum perintah di atas:

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

Contoh menggabungkan 2 repositori besar, menempatkan salah satunya ke dalam subdirektori: https://gist.github.com/x-yuri/9890ab1079cf4357d6f269d073fd9731

Catatan: The --allow-unrelated-historiesparameter hanya ada sejak git> = 2,9. Lihat Git - git merge Documentation / --allow-unrelated-histories

Pembaruan : Ditambahkan --tagsseperti yang disarankan oleh @jstadler untuk menjaga tag.


8
Ini berhasil bagi saya. Bekerja seperti jimat pertama kali dengan hanya satu konflik di file .gitignore! Itu dengan sempurna menyimpan sejarah commit. Nilai tambah besar dari pendekatan lain - selain kesederhanaan - adalah bahwa dengan ini tidak perlu ada referensi berkelanjutan untuk repo gabungan. Satu hal yang harus diperhatikan - jika Anda seorang pengembang iOS seperti saya - adalah sangat berhati-hati untuk memasukkan file proyek repo target ke dalam ruang kerja.
Max MacLeod

30
Terima kasih. Bekerja untukku. Saya perlu memindahkan direktori gabungan ke dalam sub-folder jadi setelah mengikuti langkah-langkah di atas saya hanya menggunakangit mv source-dir/ dest/new-source-dir
Sid

13
The git mergeLangkah gagal di sini dengan fatal: refusing to merge unrelated histories; --allow-unrelated-historiesperbaikan itu seperti yang dijelaskan dalam dokumen .
ssc

19
--allow-unrelated-historiesdiperkenalkan di git 2.9 . Dalam versi sebelumnya itu adalah perilaku default.
Douglas Royds

11
Shorter: git fetch /path/to/project-a master; git merge --allow-unrelated-histories FETCH_HEAD.
jthill

614

Berikut adalah dua solusi yang mungkin:

Submodules

Salin repositori A ke direktori terpisah di proyek B yang lebih besar, atau (mungkin lebih baik) klon repositori A ke subdirektori dalam proyek B. Lalu gunakan git submodule untuk menjadikan repositori ini sebagai submodule dari repositori B.

Ini adalah solusi yang bagus untuk repositori yang digabungkan secara longgar, di mana pengembangan dalam repositori A berlanjut, dan bagian utama dari pengembangan adalah pengembangan terpisah yang terpisah di A. Lihat juga SubmoduleSupport dan GitSubmoduleTutorial page di Git Wiki.

Subtree menggabungkan

Anda bisa menggabungkan repositori A ke subdirektori proyek B menggunakan strategi gabungan subtree . Ini dijelaskan dalam Penggabungan Subtree dan Anda oleh Markus Prinz.

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

(Opsi --allow-unrelated-historiesdiperlukan untuk Git> = 2.9.0.)

Atau Anda dapat menggunakan alat git subtree ( repositori di GitHub ) oleh apenwarr (Avery Pennarun), diumumkan misalnya dalam posting blognya. Sebuah alternatif baru untuk submitran Git: git subtree .


Saya pikir dalam kasus Anda (A adalah menjadi bagian dari proyek yang lebih besar B) solusi yang tepat adalah dengan menggunakan penggabungan subtree .


1
Ini berfungsi dan tampaknya melestarikan sejarah, tetapi tidak sedemikian rupa sehingga Anda bisa menggunakannya untuk file yang berbeda atau membagi dua melalui penggabungan. Apakah saya melewatkan satu langkah?
jettero

56
ini tidak lengkap . Ya, Anda mendapatkan banyak komitmen, tetapi mereka tidak lagi merujuk ke jalan yang benar. git log dir-B/somefiletidak akan menampilkan apa pun kecuali yang digabung. Lihat jawaban Greg Hewgill merujuk masalah penting ini.
artfulrobot

2
PENTING: git pull --no-rebase -s subtree Bproject master Jika Anda tidak melakukannya, dan Anda telah menarik set untuk rebase secara otomatis, Anda akan berakhir dengan "Tidak dapat menguraikan objek". Lihat osdir.com/ml/git/2009-07/msg01576.html
Eric Bowman - abstracto -

4
Jawaban ini mungkin membingungkan karena memiliki B sebagai subtree gabungan ketika dalam pertanyaan itu adalah A. Hasil salinan dan tempel?
vfclists

11
Jika Anda mencoba merekatkan dua repositori bersama-sama, submodul dan gabungan subtree adalah alat yang salah untuk digunakan karena mereka tidak mempertahankan semua riwayat file (seperti dicatat oleh komentator lain). Lihat stackoverflow.com/questions/13040958/… .
Eric Lee

194

Pendekatan submodule baik jika Anda ingin mempertahankan proyek secara terpisah. Namun, jika Anda benar-benar ingin menggabungkan kedua proyek ke dalam repositori yang sama, maka Anda memiliki sedikit pekerjaan yang harus dilakukan.

Hal pertama yang akan digunakan git filter-branch untuk menulis ulang nama-nama segala sesuatu di repositori kedua menjadi di subdirektori di mana Anda ingin mereka berakhir. Jadi, bukannya foo.c, bar.html, Anda akan memiliki projb/foo.cdanprojb/bar.html .

Maka, Anda harus dapat melakukan sesuatu seperti berikut:

git remote add projb [wherever]
git pull projb

The git pullakan melakukangit fetch diikuti oleh git merge. Seharusnya tidak ada konflik, jika repositori yang Anda tarik belum memiliki projb/direktori.

Pencarian lebih lanjut menunjukkan bahwa sesuatu yang mirip dilakukan untuk penggabungan gitkke dalam git. Junio ​​C Hamano menulisnya di sini: http://www.mail-archive.com/git@vger.kernel.org/msg03395.html


4
subtree merge akan menjadi solusi yang lebih baik, dan tidak memerlukan menulis ulang sejarah proyek termasuk
Jakub Narębski

8
Saya ingin tahu cara menggunakannya git filter-branchuntuk mencapai ini. Di halaman manual tertulis tentang sebaliknya: menjadikan subdir / menjadi root, tetapi bukan sebaliknya.
artfulrobot

31
jawaban ini akan sangat bagus jika menjelaskan bagaimana menggunakan filter-branch untuk mencapai hasil yang diinginkan
Anentropic

14
Saya menemukan cara menggunakan filter-branch di sini: stackoverflow.com/questions/4042816/…
David Minor

3
Lihat jawaban ini untuk implementasi garis besar Greg.
Paul Draper

75

git-subtree itu bagus, tetapi mungkin itu bukan yang Anda inginkan.

Misalnya, jika projectAdirektori dibuat dalam B, setelah git subtree,

git log projectA

daftar hanya satu komit: gabungan. Komit dari proyek gabungan adalah untuk jalur yang berbeda, sehingga tidak muncul.

Jawaban Greg Hewgill datang paling dekat, meskipun tidak benar-benar mengatakan bagaimana menulis ulang jalan.


Solusinya sangat sederhana.

(1) Dalam A,

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

Catatan: Riwayat penulisan ulang ini, jadi jika Anda ingin terus menggunakan repo A ini, Anda mungkin ingin mengkloning (menyalin) salinan yang dibuang terlebih dahulu.

Catatan Bene: Anda harus memodifikasi skrip pengganti di dalam perintah sed jika Anda menggunakan karakter non-ascii (atau karakter putih) dalam nama file atau path. Dalam hal ini lokasi file di dalam catatan yang diproduksi oleh "ls-file -s" dimulai dengan tanda kutip.

(2) Lalu di B, jalankan

git pull path/to/A

Voila! Anda memiliki projectAdirektori di B. Jika dijalankan git log projectA, Anda akan melihat semua komit dari A.


Dalam kasus saya, saya ingin dua subdirektori, projectAdan projectB. Dalam hal itu, saya melakukan langkah (1) ke B juga.


1
Sepertinya Anda menyalin jawaban Anda dari stackoverflow.com/a/618113/586086 ?
Andrew Mao

1
@AndrewMao, saya kira begitu ... Saya sebenarnya tidak ingat. Saya sudah menggunakan script ini sedikit.
Paul Draper

6
Saya akan menambahkan bahwa \ t tidak berfungsi pada OS X dan Anda harus memasukkan <tab>
Muneeb Ali

2
"$GIT_INDEX_FILE"harus dikutip (dua kali), jika tidak, metode Anda akan gagal jika jalur berisi spasi.
Rob W

4
Jika Anda ingin tahu, untuk memasukkan <tab> di osx, Anda perluCtrl-V <tab>
casey

48

Jika kedua repositori memiliki jenis file yang sama (seperti dua repositori Rails untuk proyek yang berbeda), Anda dapat mengambil data dari repositori sekunder ke repositori Anda saat ini:

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

dan kemudian menggabungkannya ke repositori saat ini:

git merge --allow-unrelated-histories branch_name

Jika versi Git Anda lebih kecil dari 2,9, hapus --allow-unrelated-histories.

Setelah ini, konflik dapat terjadi. Anda dapat menyelesaikannya misalnya dengan git mergetool. kdiff3dapat digunakan hanya dengan keyboard, jadi 5 file konflik diperlukan saat membaca kode hanya beberapa menit.

Ingatlah untuk menyelesaikan penggabungan:

git commit

25

Saya terus kehilangan sejarah ketika menggunakan penggabungan, jadi saya akhirnya menggunakan rebase karena dalam kasus saya kedua repositori cukup berbeda untuk tidak berakhir pada penggabungan di setiap komit:

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

=> menyelesaikan konflik, lalu melanjutkan, sebanyak yang diperlukan ...

git rebase --continue

Melakukan ini mengarah ke satu proyek yang memiliki semua komitmen dari projA diikuti oleh komitmen dari projB


25

Dalam kasus saya, saya memiliki my-pluginrepositori dan main-projectrepositori, dan saya ingin berpura-pura yang my-pluginselalu dikembangkan di pluginssubdirektori darimain-project .

Pada dasarnya, saya menulis ulang sejarah my-pluginrepositori sehingga tampaknya semua pengembangan terjadi di plugins/my-pluginsubdirektori. Kemudian, saya menambahkan sejarah perkembangan my-pluginke dalam main-projectsejarah, dan menggabungkan kedua pohon itu bersama-sama. Karena tidak ada plugins/my-plugindirektori yang ada dimain-project repositori, ini adalah penggabungan tanpa konflik sepele. Repositori yang dihasilkan berisi semua sejarah dari kedua proyek asli, dan memiliki dua akar.

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

Versi panjang

Pertama, buat salinan my-pluginrepositori, karena kita akan menulis ulang sejarah repositori ini.

Sekarang, navigasikan ke root my-pluginrepositori, periksa cabang utama Anda (mungkin master), dan jalankan perintah berikut. Tentu saja, Anda harus mengganti my-plugindan pluginsapa pun nama Anda yang sebenarnya.

$ 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

Sekarang untuk penjelasan. git filter-branch --tree-filter (...) HEADmenjalankan (...)perintah di setiap komit yang dapat dijangkau HEAD. Perhatikan bahwa ini beroperasi langsung pada data yang disimpan untuk setiap komit, jadi kami tidak perlu khawatir tentang gagasan "direktori kerja", "indeks", "staging", dan sebagainya.

Jika Anda menjalankan filter-branchperintah yang gagal, itu akan meninggalkan beberapa file di .gitdirektori dan pada saat Anda mencobanya filter-branchakan mengeluh tentang ini, kecuali jika Anda memberikan -fopsi untukfilter-branch .

Adapun perintah yang sebenarnya, saya tidak punya banyak keberuntungan bashuntuk melakukan apa yang saya inginkan, jadi alih-alih saya gunakan zsh -cuntuk membuat zshmenjalankan perintah. Pertama saya mengatur extended_globopsi, yang memungkinkan ^(...)sintaks dalam mvperintah, serta glob_dotsopsi, yang memungkinkan saya untuk memilih dotfile (seperti .gitignore) dengan glob ( ^(...)).

Berikutnya, saya menggunakan mkdir -pperintah untuk membuat kedua pluginsdan plugins/my-pluginpada saat yang sama.

Akhirnya, saya menggunakan fitur zsh"gumpalan negatif" ^(.git|plugins)untuk mencocokkan semua file di direktori root repositori kecuali untuk .gitdan my-pluginfolder yang baru dibuat . (Mengecualikan .gitmungkin tidak diperlukan di sini, tetapi mencoba memindahkan direktori ke dalam dirinya sendiri adalah kesalahan.)

Dalam repositori saya, komit awal tidak termasuk file apa pun, jadi mvperintah mengembalikan kesalahan pada komit awal (karena tidak ada yang tersedia untuk dipindahkan). Karena itu, saya menambahkan || trueagargit filter-branch tidak dibatalkan.

The --allpilihan memberitahu filter-branchuntuk menulis ulang sejarah untuk semua cabang di repositori, dan ekstra --diperlukan untuk memberitahu gituntuk menafsirkannya sebagai bagian dari daftar pilihan untuk cabang untuk menulis ulang, bukan sebagai pilihan untukfilter-branch dirinya sendiri.

Sekarang, navigasikan ke main-projectrepositori Anda dan periksa cabang apa pun yang ingin Anda gabungkan. Tambahkan salinan lokal my-pluginrepositori Anda (dengan riwayatnya diubah) sebagai remote dari main-projectdengan:

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

Anda sekarang akan memiliki dua pohon yang tidak terkait dalam riwayat komit Anda, yang dapat Anda visualisasikan dengan baik menggunakan:

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

Untuk menggabungkannya, gunakan:

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

Perhatikan bahwa pada Pra-2.9.0 Git, --allow-unrelated-historiesopsi tidak ada. Jika Anda menggunakan salah satu versi ini, cukup hapus opsi: pesan kesalahan yang --allow-unrelated-historiesmencegah juga ditambahkan di 2.9.0.

Anda seharusnya tidak memiliki konflik gabungan. Jika Anda melakukannya, itu mungkin berarti bahwa salah satu filter-branchperintah tidak berfungsi dengan benar atau sudah ada plugins/my-plugindirektori di main-project.

Pastikan untuk memasukkan pesan komit penjelas untuk kontributor masa depan yang bertanya-tanya apa peretasan yang terjadi untuk membuat repositori dengan dua root.

Anda dapat memvisualisasikan grafik komit baru, yang seharusnya memiliki dua komit root, menggunakan git logperintah di atas . Perhatikan bahwa hanya mastercabang yang akan digabung . Ini berarti bahwa jika Anda memiliki pekerjaan penting pada my-plugincabang lain yang ingin Anda gabungkan ke dalam main-projectpohon, Anda harus menahan diri dari menghapus my-pluginremote sampai Anda telah melakukan penggabungan ini. Jika tidak, maka komit dari cabang-cabang itu akan tetap berada di main-projectrepositori, tetapi beberapa tidak akan terjangkau dan rentan terhadap pengumpulan sampah akhirnya. (Selain itu, Anda harus merujuknya oleh SHA, karena menghapus remote menghapus cabang-cabang pelacakan jarak jauhnya.)

Secara opsional, setelah Anda menggabungkan semua yang ingin Anda simpan my-plugin, Anda dapat menghapus my-pluginremote menggunakan:

$ git remote remove my-plugin

Anda sekarang dapat dengan aman menghapus salinan my-pluginrepositori yang riwayatnya Anda ubah. Dalam kasus saya, saya juga menambahkan pemberitahuan penghentian ke my-pluginrepositori nyata setelah penggabungan selesai dan didorong.


Diuji pada Mac OS X El Capitan dengan git --version 2.9.0dan zsh --version 5.2. Jarak tempuh Anda mungkin beragam.

Referensi:


1
Dari mana --allow-unrelated-historiesdatangnya?
xpto

3
@MarceloFilho Periksa man git-merge. Secara default, perintah git merge menolak untuk menggabungkan sejarah yang tidak memiliki nenek moyang yang sama. Opsi ini dapat digunakan untuk mengesampingkan keamanan ini saat menggabungkan sejarah dua proyek yang memulai kehidupan mereka secara mandiri. Karena ini adalah kejadian yang sangat jarang, tidak ada variabel konfigurasi untuk mengaktifkannya secara default dan tidak akan ditambahkan.
Radon Rosborough

Haruskah tersedia di git version 2.7.2.windows.1?
xpto

2
@MarceloFilho Ini ditambahkan di 2.9.0, tetapi dalam versi yang lebih lama Anda tidak harus melewati opsi (itu hanya akan berfungsi). github.com/git/git/blob/…
Radon Rosborough

Ini bekerja dengan baik. Dan saya bisa menggunakan cabang filter untuk menulis ulang nama file ke tempat yang saya inginkan di pohon sebelum penggabungan. Saya kira ada lebih banyak pekerjaan yang terlibat jika Anda perlu memindahkan sejarah selain cabang utama.
codeDr

9

Saya sudah mencoba melakukan hal yang sama selama berhari-hari, saya menggunakan git 2.7.2. Subtree tidak melestarikan sejarah.

Anda dapat menggunakan metode ini jika Anda tidak akan menggunakan proyek yang lama lagi.

Saya menyarankan agar Anda cabang B dulu dan bekerja di cabang.

Berikut langkah-langkahnya tanpa bercabang:

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

Jika sekarang Anda mencatat salah satu file di subdir A Anda akan mendapatkan riwayat lengkap

git log --follow A/<file>

Ini adalah pos yang membantu saya melakukan ini:

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


8

Jika Anda ingin meletakkan file dari cabang di repo B di subtree dari repo A dan juga melestarikan sejarah, teruslah membaca. (Dalam contoh di bawah ini, saya berasumsi bahwa kita ingin cabang master repo B bergabung menjadi cabang master repo A.)

Di repo A, pertama-tama lakukan hal berikut untuk membuat repo B tersedia:

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

Sekarang kami membuat cabang baru (dengan hanya satu komit) di repo A yang kami sebut new_b_root. Komit yang dihasilkan akan memiliki file yang dikomit di komit pertama dari cabang master repo B tetapi dimasukkan ke dalam subdirektori bernama 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))"

Penjelasan: --orphanOpsi untuk perintah checkout memeriksa file dari cabang master A tetapi tidak membuat komit. Kami dapat memilih komit karena selanjutnya kami akan menghapus semua file. Kemudian, tanpa melakukan ( -n), kami memilih-ceri komit pertama dari cabang utama B. (Pilihan ceri mempertahankan pesan komit asli yang tampaknya tidak dilakukan checkout langsung.) Kemudian kita membuat subtree tempat kita ingin meletakkan semua file dari repo B. Kita kemudian harus memindahkan semua file yang diperkenalkan di cherry-pick ke subtree. Pada contoh di atas, hanya ada satu READMEfile untuk dipindahkan. Kemudian kami melakukan komit B-repo kami, dan, pada saat yang sama, kami juga menjaga stempel waktu dari komit asli.

Sekarang, kita akan membuat B/mastercabang baru di atas yang baru dibuat new_b_root. Kami memanggil cabang baru b:

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

Sekarang, kami menggabungkan bcabang kami menjadi A/master:

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

Akhirnya, Anda dapat menghapus cabang Bjarak jauh dan sementara:

git remote remove B
git branch -D new_b_root b

Grafik akhir akan memiliki struktur seperti ini:

masukkan deskripsi gambar di sini


Jawaban yang bagus, terima kasih! Saya benar-benar ketinggalan jawaban lain dengan "git subtree" atau "gabungkan --allow-unrelated-histories" dari Andresch Serj bahwa sub direktori tidak memiliki log.
Ilendir

8

Saya telah mengumpulkan banyak informasi di sini di Stack OverFlow, dll, dan telah berhasil menyatukan skrip yang memecahkan masalah bagi saya.

Peringatannya adalah ia hanya memperhitungkan cabang 'berkembang' dari masing-masing repositori dan menggabungkannya ke direktori terpisah dalam repositori yang sama sekali baru.

Tag dan cabang lainnya diabaikan - ini mungkin bukan yang Anda inginkan.

Script bahkan menangani cabang fitur dan tag - menamai mereka dalam proyek baru sehingga Anda tahu dari mana mereka berasal.

#!/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

Anda juga bisa mendapatkannya dari http://paste.ubuntu.com/11732805

Pertama buat file dengan URL ke setiap repositori, misalnya:

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

Kemudian panggil skrip yang memberi nama proyek dan jalur ke skrip:

./mergeGitRepositories.sh eitchnet_test eitchnet.lst

Script itu sendiri memiliki banyak komentar yang harus menjelaskan apa fungsinya.


Alih-alih mengarahkan pembaca ke jawaban, silakan kirim jawabannya di sini (alias edit apa yang Anda katakan dalam komentar itu ke dalam jawaban ini).
josliber

1
Tentu, hanya berpikir lebih baik tidak mengulangi diri saya sendiri ... =)
eitch

Jika menurut Anda pertanyaan ini identik dengan yang lain, maka Anda dapat menandainya sebagai duplikat menggunakan tautan "bendera" di bawah pertanyaan itu sendiri dan menunjukkan pertanyaan lainnya. Jika itu bukan pertanyaan duplikat tetapi Anda pikir jawaban yang sama persis dapat digunakan untuk menyelesaikan kedua masalah, maka cukup kirimkan jawaban yang sama untuk kedua masalah (seperti yang telah Anda lakukan sekarang). Terima kasih telah berkontribusi!
josliber

Luar biasa! Tidak bekerja di Windows bash prompt, tetapi berjalan dengan sempurna membentuk kotak Vagrant yang menjalankan ubuntu. Penghemat waktu yang luar biasa!
xverges

Senang melayani =)
eitch

7

Saya tahu itu sudah lama setelah kenyataan, tetapi saya tidak senang dengan jawaban lain yang saya temukan di sini, jadi saya menulis ini:

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
Ini persis apa yang saya cari. Terima kasih! Namun, saya harus mengubah jalur 22 ke:if [[ $dirname =~ ^.*\.git$ ]]; then
heyman

2
^. * blarg $ boros RE serakah. Lebih baik mengatakan .blarg $ dan lewati jangkar depan.
jettero

7

Jika Anda mencoba merekatkan dua repositori bersama-sama, submodul dan penggabungan subtree adalah alat yang salah untuk digunakan karena mereka tidak mempertahankan semua riwayat file (seperti yang telah dicatat orang pada jawaban lain). Lihat jawaban ini di sini untuk cara sederhana dan benar untuk melakukan ini.


1
Solusi Anda hanya berfungsi dengan baik untuk repositori baru, tetapi bagaimana cara menggabungkan repo di repositori lain dengan konflik file?
Andrey Izman

6

Saya memiliki tantangan yang sama, tetapi dalam kasus saya, kami telah mengembangkan satu versi basis kode di repo A, lalu mengkloningnya menjadi repo baru, repo B, untuk versi baru produk. Setelah memperbaiki beberapa bug di repo A, kami perlu melakukan FI perubahan menjadi repo B. Akhirnya melakukan hal berikut:

  1. Menambahkan remote ke repo B yang menunjuk ke repo A (git remote add ...)
  2. Menarik cabang saat ini (kami tidak menggunakan master untuk perbaikan bug) (git pull remoteForRepoA bugFixBranch)
  3. Mendorong gabungan ke github

Berhasil memperlakukan :)


5

Mirip dengan @Smar tetapi menggunakan jalur sistem file, ditetapkan dalam PRIMARY dan SECONDARY:

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

Kemudian Anda secara manual bergabung.

(diadaptasi dari pos oleh Anar Manafov )


5

Menggabungkan 2 repo

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

Saat Anda ingin menggabungkan tiga proyek atau lebih dalam satu komit, lakukan langkah-langkah seperti yang dijelaskan dalam jawaban lain ( remote add -f, merge). Kemudian, (lunak) setel ulang indeks ke head lama (di mana tidak ada penggabungan yang terjadi). Tambahkan semua file ( git add -A) dan komit (pesan "Menggabungkan proyek A, B, C, dan D menjadi satu proyek). Sekarang ini adalah kom-id master.

Sekarang, buat .git/info/graftsdengan konten berikut:

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

Lari git filter-branch -- head^..head head^2..head head^3..head. Jika Anda memiliki lebih dari tiga cabang, tambahkan saja sebanyak yang head^n..headAnda miliki cabang. Untuk memperbarui tag, tambahkan --tag-name-filter cat. Jangan selalu menambahkan itu, karena ini dapat menyebabkan penulisan ulang beberapa commit. Untuk detailnya lihat halaman manual cabang-filter , cari "cangkokan".

Sekarang, komit terakhir Anda memiliki orang tua yang tepat terkait.


1
Tunggu, mengapa Anda ingin menggabungkan tiga proyek dalam satu komit?
Steve Bennett

Saya mulai dengan repositori, repositori-klien, dan pemodel sebagai proyek git terpisah. Ini sulit bagi rekan kerja, jadi saya bergabung dengan mereka dalam satu proyek git. Agar "root" dari proyek baru tersebut berasal dari tiga proyek lain, saya ingin memiliki komitmen gabungan tunggal .
koppor

4

Untuk menggabungkan A dalam B:

1) Dalam proyek A

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

2) Dalam proyek B

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

Di cabang ini lakukan semua operasi yang perlu Anda lakukan dan komit.

C) Kemudian kembali ke master dan penggabungan klasik antara dua cabang:

git checkout master
git merge projectA

2

Fungsi ini akan mengkloning repo jarak jauh ke dir repo lokal, setelah menggabungkan semua komit akan disimpan, git logakan ditampilkan komit asli dan jalur yang benar:

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

Cara Penggunaan:

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

Jika membuat sedikit perubahan, Anda bahkan dapat memindahkan file / dir repo yang digabungkan ke jalur yang berbeda, misalnya:

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"


Jalur Pemberitahuan menggantikan via sed, jadi pastikan jalur tersebut dipindahkan di jalur yang benar setelah penggabungan.
The --allow-unrelated-historiesparameter hanya ada sejak git> = 2,9.


1

Perintah yang diberikan adalah solusi terbaik yang saya sarankan.

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

1

Saya menggabungkan proyek sedikit secara manual, yang memungkinkan saya untuk menghindari perlunya menangani konflik penggabungan.

pertama, salin file-file dari proyek lain sesuai keinginan Anda.

cp -R myotherproject newdirectory
git add newdirectory

tarikan berikutnya dalam sejarah

git fetch path_or_url_to_other_repo

suruh git untuk bergabung dalam sejarah benda yang terakhir diambil

echo 'FETCH_HEAD' > .git/MERGE_HEAD

sekarang komit namun Anda biasanya komit

git commit

0

Saya ingin memindahkan proyek kecil ke subdirektori yang lebih besar. Karena proyek kecil saya tidak memiliki banyak komitmen, saya menggunakan git format-patch --output-directory /path/to/patch-dir. Kemudian pada proyek yang lebih besar, saya menggunakangit am --directory=dir/in/project /path/to/patch-dir/* .

Ini terasa jauh lebih menakutkan dan jauh lebih bersih daripada cabang-filter. Memang, itu mungkin tidak berlaku untuk semua kasus.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.