Komit Git diduplikasi di cabang yang sama setelah melakukan rebase

131

Saya memahami skenario yang disajikan dalam Pro Git tentang The Perils of Rebasing . Penulis pada dasarnya memberi tahu Anda bagaimana menghindari komit yang digandakan:

Jangan rebase komitmen yang telah Anda dorong ke repositori publik.

Saya akan memberi tahu Anda situasi khusus saya karena menurut saya itu tidak benar-benar sesuai dengan skenario Pro Git dan saya masih berakhir dengan komit duplikat.

Katakanlah saya memiliki dua cabang jarak jauh dengan rekan lokalnya:

origin/master    origin/dev
|                |
master           dev

Keempat cabang berisi komit yang sama dan saya akan memulai pengembangan di dev:

origin/master : C1 C2 C3 C4
master        : C1 C2 C3 C4

origin/dev    : C1 C2 C3 C4
dev           : C1 C2 C3 C4

Setelah beberapa komit, saya mendorong perubahan ke origin/dev:

origin/master : C1 C2 C3 C4
master        : C1 C2 C3 C4

origin/dev    : C1 C2 C3 C4 C5 C6  # (2) git push
dev           : C1 C2 C3 C4 C5 C6  # (1) git checkout dev, git commit

Saya harus kembali ke masteruntuk membuat perbaikan cepat:

origin/master : C1 C2 C3 C4 C7  # (2) git push
master        : C1 C2 C3 C4 C7  # (1) git checkout master, git commit

origin/dev    : C1 C2 C3 C4 C5 C6
dev           : C1 C2 C3 C4 C5 C6

Dan kembali ke devsaya rebase perubahan untuk memasukkan perbaikan cepat dalam pengembangan saya yang sebenarnya:

origin/master : C1 C2 C3 C4 C7
master        : C1 C2 C3 C4 C7

origin/dev    : C1 C2 C3 C4 C5 C6
dev           : C1 C2 C3 C4 C7 C5' C6'  # git checkout dev, git rebase master

Jika saya menampilkan riwayat komit dengan GitX / gitk, saya perhatikan bahwa origin/devsekarang berisi dua komit yang identik C5'dan C6'yang berbeda dengan Git. Sekarang jika saya mendorong perubahan origin/devini adalah hasilnya:

origin/master : C1 C2 C3 C4 C7
master        : C1 C2 C3 C4 C7

origin/dev    : C1 C2 C3 C4 C5 C6 C7 C5' C6'  # git push
dev           : C1 C2 C3 C4 C7 C5' C6'

Mungkin saya kurang memahami penjelasan di Pro Git, jadi saya ingin mengetahui dua hal:

  1. Mengapa Git menduplikasi komit ini saat melakukan rebasing? Apakah ada alasan khusus untuk melakukan itu daripada hanya melamar C5dan C6setelahnya C7?
  2. Bagaimana saya bisa menghindarinya? Apakah bijaksana untuk melakukannya?
elitalon.dll
sumber

Jawaban:

87

Anda seharusnya tidak menggunakan rebase di sini, penggabungan sederhana sudah cukup. Buku Pro Git yang Anda tautkan pada dasarnya menjelaskan situasi yang tepat ini. Cara kerja bagian dalam mungkin sedikit berbeda, tetapi inilah cara saya memvisualisasikannya:

  • C5dan C6ditarik keluar untuk sementaradev
  • C7 diterapkan ke dev
  • C5dan C6dimainkan kembali di atas C7, menciptakan diff baru dan oleh karena itu komit baru

Jadi, di devcabang Anda , C5dan C6secara efektif sudah tidak ada lagi: mereka sekarang C5'dan C6'. Saat Anda mendorong ke origin/dev, git melihat C5'dan C6'sebagai komitmen baru dan memakainya ke akhir sejarah. Memang, jika Anda melihat perbedaan antara C5dan C5'dalam origin/dev, Anda akan melihat bahwa meskipun isinya sama, nomor barisnya mungkin berbeda - yang membuat hash dari komit berbeda.

Saya akan menyatakan kembali aturan Pro Git: jangan pernah melakukan rebase yang pernah ada di mana pun kecuali repositori lokal Anda . Gunakan penggabungan sebagai gantinya.

Justin ᚅᚔᚈᚄᚒᚔ
sumber
Saya memiliki masalah yang sama, bagaimana saya bisa memperbaiki riwayat cabang jarak jauh saya sekarang, apakah ada pilihan lain selain menghapus cabang dan membuatnya kembali dengan memetik ceri ??
Wazery
1
@xdsy: Coba lihat ini dan ini .
Justin ᚅᚔᚈᚄᚒᚔ
2
Anda mengatakan "C5 dan C6 untuk sementara ditarik dari dev ... C7 diterapkan ke dev". Jika ini masalahnya, lalu mengapa C5 dan C6 muncul sebelum C7 dalam urutan commit pada origin / dev?
KJ50
@ KJ50: Karena C5 dan C6 sudah didorong ke origin/dev. Ketika di dev-rebased, sejarahnya diubah (C5 / C6 sementara dihapus dan diterapkan kembali setelah C7). Mengubah riwayat repo yang didorong umumnya adalah Ide Benar-benar Buruk ™ kecuali Anda tahu apa yang Anda lakukan. Dalam kasus sederhana ini, masalah dapat diselesaikan dengan melakukan dorongan paksa dari devke origin/devsetelah rebase dan memberi tahu orang lain yang bekerja origin/devbahwa mereka mungkin akan mengalami hari yang buruk. Jawaban yang lebih baik, sekali lagi, adalah "jangan lakukan itu ... gunakan penggabungan"
Justin ᚅᚔᚈᚄᚒᚔ
3
Satu hal yang perlu diperhatikan: Hash dari C5 dan C5 'pasti berbeda, tetapi bukan karena nomor barisnya berbeda, tetapi untuk dua fakta berikut ini yang mana saja sudah cukup untuk perbedaannya: 1) hash yang kita bicarakan adalah hash dari seluruh pohon sumber setelah komit, bukan hash perbedaan delta, dan oleh karena itu C5 'berisi apa pun yang berasal dari C7, sedangkan C5 tidak, dan 2) Induk C5' berbeda dari C5, dan informasi ini juga termasuk dalam simpul akar dari pohon komit yang mempengaruhi hasil hash.
Ozgur Murat
113

Jawaban singkat

Anda menghilangkan fakta bahwa Anda berlari git push, mendapatkan kesalahan berikut, dan kemudian melanjutkan untuk menjalankan git pull:

To [email protected]:username/test1.git
 ! [rejected]        dev -> dev (non-fast-forward)
error: failed to push some refs to '[email protected]:username/test1.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Meskipun Git berusaha membantu, saran 'git pull' yang dimilikinya kemungkinan besar bukanlah yang ingin Anda lakukan .

Jika Anda:

  • Bekerja pada "cabang fitur" atau "cabang pengembang" saja , lalu Anda dapat menjalankan git push --forceuntuk memperbarui remote dengan komitmen pasca-rebase Anda ( sesuai jawaban pengguna4405677 ).
  • Bekerja pada cabang dengan banyak pengembang pada saat yang sama, maka Anda mungkin tidak boleh menggunakannyagit rebase sejak awal. Untuk memperbarui devdengan perubahan dari master, Anda harus, alih-alih menjalankan git rebase master dev, jalankan git merge mastersaat di dev( sesuai jawaban Justin ).

Penjelasan yang sedikit lebih panjang

Setiap hash komit di Git didasarkan pada sejumlah faktor, salah satunya adalah hash komit yang ada sebelumnya.

Jika Anda menyusun ulang komit, Anda akan mengubah hash komit; rebasing (ketika melakukan sesuatu) akan mengubah hash komit. Dengan itu, hasil dari menjalankan git rebase master dev, di mana devtidak sinkron dengan master, akan membuat komit baru (dan dengan demikian hash) dengan konten yang sama seperti yang ada devtetapi dengan komit pada yang masterdisisipkan sebelumnya.

Anda bisa berakhir dalam situasi seperti ini dengan berbagai cara. Dua cara yang bisa saya pikirkan:

  • Anda dapat memiliki komitmen masteryang Anda inginkan sebagai dasar devpekerjaan Anda
  • Anda bisa saja komit devyang telah didorong ke remote, yang kemudian Anda ubah (reword komit pesan, susun ulang komit, komit squash, dll.)

Mari kita lebih memahami apa yang terjadi — berikut ini contohnya:

Anda memiliki repositori:

2a2e220 (HEAD, master) C5
ab1bda4 C4
3cb46a9 C3
85f59ab C2
4516164 C1
0e783a3 C0

Set awal komit linier dalam repositori

Anda kemudian melanjutkan untuk mengubah komit.

git rebase --interactive HEAD~3 # Three commits before where HEAD is pointing

(Di sinilah Anda harus mengambil kata-kata saya: ada sejumlah cara untuk mengubah komit di Git. Dalam contoh ini saya mengubah waktunya C3, tetapi Anda memasukkan komit baru, mengubah pesan komit, menyusun ulang komit, meremas komitmen bersama, dll.)

ba7688a (HEAD, master) C5
44085d5 C4
961390d C3
85f59ab C2
4516164 C1
0e783a3 C0

Komitmen yang sama dengan hash baru

Di sinilah penting untuk diperhatikan bahwa hash komit berbeda. Ini adalah perilaku yang diharapkan karena Anda telah mengubah sesuatu (apa pun) tentang mereka. Ini tidak masalah, TAPI:

Log grafik yang menunjukkan bahwa master tidak sinkron dengan remote

Mencoba mendorong akan menunjukkan kesalahan (dan petunjuk bahwa Anda harus lari git pull).

$ git push origin master
To [email protected]:username/test1.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to '[email protected]:username/test1.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Jika kita jalankan git pull, kita melihat log ini:

7df65f2 (HEAD, master) Merge branch 'master' of bitbucket.org:username/test1
ba7688a C5
44085d5 C4
961390d C3
2a2e220 (origin/master) C5
85f59ab C2
ab1bda4 C4
4516164 C1
3cb46a9 C3
0e783a3 C0

Atau, ditunjukkan dengan cara lain:

Log grafik yang menunjukkan komit gabungan

Dan sekarang kami memiliki komitmen ganda secara lokal. Jika kami menjalankannya, git pushkami akan mengirimnya ke server.

Untuk menghindari sampai ke tahap ini, kita bisa saja lari git push --force(di mana kita malah lari git pull). Ini akan mengirimkan komit kami dengan hash baru ke server tanpa masalah. Untuk memperbaiki masalah pada tahap ini, kami dapat mengatur ulang kembali ke sebelum kami menjalankan git pull:

Lihatlah reflog ( git reflog) untuk melihat apa hash komit itu sebelum kita berlari git pull.

070e71d HEAD@{1}: pull: Merge made by the 'recursive' strategy.
ba7688a HEAD@{2}: rebase -i (finish): returning to refs/heads/master
ba7688a HEAD@{3}: rebase -i (pick): C5
44085d5 HEAD@{4}: rebase -i (pick): C4
961390d HEAD@{5}: commit (amend): C3
3cb46a9 HEAD@{6}: cherry-pick: fast-forward
85f59ab HEAD@{7}: rebase -i (start): checkout HEAD~~~
2a2e220 HEAD@{8}: rebase -i (finish): returning to refs/heads/master
2a2e220 HEAD@{9}: rebase -i (start): checkout refs/remotes/origin/master
2a2e220 HEAD@{10}: commit: C5
ab1bda4 HEAD@{11}: commit: C4
3cb46a9 HEAD@{12}: commit: C3
85f59ab HEAD@{13}: commit: C2
4516164 HEAD@{14}: commit: C1
0e783a3 HEAD@{15}: commit (initial): C0

Di atas kami melihat bahwa ba7688akami berkomitmen sebelum berlari git pull. Dengan hash komit di tangan kita dapat mengatur ulang kembali ke itu ( git reset --hard ba7688a) dan kemudian menjalankan git push --force.

Dan kami selesai.

Tapi tunggu, saya terus mendasarkan pekerjaan dari komit yang digandakan

Jika Anda entah bagaimana tidak menyadari bahwa komit diduplikasi dan terus bekerja di atas komit duplikat, Anda benar-benar telah membuat kekacauan untuk diri Anda sendiri. Ukuran kekacauan sebanding dengan jumlah komitmen yang Anda miliki di atas duplikat.

Seperti apa ini:

3b959b4 (HEAD, master) C10
8f84379 C9
0110e93 C8
6c4a525 C7
630e7b4 C6
070e71d (origin/master) Merge branch 'master' of bitbucket.org:username/test1
ba7688a C5
44085d5 C4
961390d C3
2a2e220 C5
85f59ab C2
ab1bda4 C4
4516164 C1
3cb46a9 C3
0e783a3 C0

Git log yang menampilkan komit linier di atas komit yang digandakan

Atau, ditunjukkan dengan cara lain:

Grafik log yang menampilkan komit linier di atas komit yang digandakan

Dalam skenario ini kami ingin menghapus komit duplikat, tetapi tetap mempertahankan komitmen yang telah kami lakukan berdasarkan komitmen tersebut — kami ingin mempertahankan C6 hingga C10. Seperti kebanyakan hal, ada beberapa cara untuk melakukannya:

Antara:

  • Buat cabang baru di komit 1 duplikat terakhir , cherry-pickmasing-masing komit (termasuk C6 hingga C10) ke cabang baru itu, dan perlakukan cabang baru itu sebagai kanonik.
  • Jalankan git rebase --interactive $commit, di mana $commitkomit sebelum kedua komit yang digandakan 2 . Di sini kita bisa langsung menghapus garis untuk duplikat.

1 Tidak masalah mana yang Anda pilih, salah satu ba7688aatau 2a2e220berfungsi dengan baik.

2 Dalam contoh itu akan 85f59ab.

TL; DR

Setel advice.pushNonFastForwardke false:

git config --global advice.pushNonFastForward false
Whymarrh
sumber
1
Tidak apa-apa untuk mengikuti saran "git pull ..." selama seseorang menyadari elipsis menyembunyikan opsi "--rebase" (alias "-r"). ;-)
G. Sylvie Davies
4
Saya akan merekomendasikan menggunakan git pushs --force-with-leasesaat ini karena ini adalah default yang lebih baik
Whymarrh
4
Bisa jawaban ini atau mesin waktu. Terima kasih!
ZeMoon
Penjelasan yang sangat rapi ... Saya menemukan masalah serupa yang menggandakan kode saya 5-6 kali setelah saya mencoba melakukan rebase berulang kali ... hanya untuk memastikan kode tersebut mutakhir dengan master ... tetapi setiap kali didorong komit baru ke cabang saya, menggandakan kode saya juga. Bisakah Anda memberi tahu saya jika memaksa push (dengan opsi sewa) aman dilakukan di sini jika saya adalah satu-satunya pengembang yang bekerja di cabang saya? Atau menggabungkan master menjadi milik saya sebagai gantinya rebasing adalah cara yang lebih baik?
Dhruv Singhal
12

Saya pikir Anda melewatkan detail penting saat menjelaskan langkah Anda. Lebih khusus lagi, langkah terakhir Anda, git pushpada dev, sebenarnya akan memberi Anda kesalahan, karena Anda biasanya tidak dapat mendorong perubahan non-fastforward.

Jadi yang Anda lakukan git pullsebelum dorongan terakhir, yang menghasilkan komit gabungan dengan C6 dan C6 'sebagai induk, itulah sebabnya keduanya akan tetap terdaftar di log. Format log yang lebih cantik mungkin membuatnya lebih jelas bahwa mereka adalah cabang gabungan dari komit yang digandakan.

Atau Anda membuat git pull --rebase(atau tanpa eksplisit --rebasejika itu tersirat oleh konfigurasi Anda) sebagai gantinya, yang menarik C5 dan C6 asli kembali ke dev lokal Anda (dan selanjutnya merombak yang berikut ini ke hash baru, C7 'C5' 'C6' ').

Salah satu jalan keluarnya adalah git push -fmemaksa push ketika memberikan kesalahan dan menghapus C5 C6 dari aslinya, tetapi jika orang lain juga menariknya sebelum Anda menghapusnya, Anda akan mendapat lebih banyak masalah .. Pada dasarnya setiap orang yang memiliki C5 C6 tentu perlu melakukan langkah-langkah khusus untuk menghilangkannya. Itulah mengapa mereka mengatakan Anda tidak boleh mendasarkan kembali apa pun yang sudah diterbitkan. Ini masih bisa dilakukan jika mengatakan "penerbitan" ada dalam tim kecil.

pengguna4405677
sumber
1
Penghilangan git pullsangat penting. Rekomendasi Anda git push -f, meskipun berbahaya, mungkin itulah yang dicari pembaca.
Whymarrh
Memang. Dulu ketika saya menulis pertanyaan yang sebenarnya saya lakukan git push --force, hanya untuk melihat apa yang akan dilakukan Git. Saya belajar banyak tentang Git sejak itu dan saat ini rebaseadalah bagian dari alur kerja normal saya. Namun, saya melakukannya git push --force-with-leaseuntuk menghindari menimpa karya orang lain.
elitalon
Menggunakan --force-with-leaseadalah default yang baik, saya akan meninggalkan komentar di bawah jawaban saya juga
Whymarrh
2

Saya menemukan bahwa dalam kasus saya, masalah ini adalah konsekuensi dari masalah konfigurasi Git. (Melibatkan tarik dan penggabungan)

Deskripsi masalah:

Gejala: Komit diduplikasi pada cabang anak setelah rebase, menyiratkan banyak penggabungan selama dan setelah rebase.

Alur Kerja: Berikut adalah langkah-langkah alur kerja yang saya lakukan:

  • Bekerja pada "Features-branch" (anak dari "Develop-branch")
  • Komit dan Dorong perubahan di "Cabang-Fitur"
  • Lihat "Develop-branch" (Cabang utama dari Fitur) dan kerjakan dengannya.
  • Komit dan dorong perubahan pada "Kembangkan-cabang"
  • Lihat "Fitur-cabang" dan tarik perubahan dari repositori (Seandainya orang lain telah melakukan pekerjaan)
  • Rebase "Features-branch" ke "Develop-branch"
  • Kekuatan dorong perubahan pada "Cabang-fitur"

Sebagai konsekuensi dari alur kerja ini, duplikasi dari semua komit "Cabang-fitur" sejak rebase sebelumnya ... :-(

Masalahnya adalah karena tarikan perubahan cabang anak sebelum rebase. Konfigurasi pull default Git adalah "merge". Ini mengubah indeks komit yang dilakukan di cabang anak.

Solusinya: di file konfigurasi Git, konfigurasikan pull untuk bekerja dalam mode rebase:

...
[pull]
    rebase = preserve
...

Semoga bisa membantu JN Grx

JN Gerbaux
sumber
1

Anda mungkin telah menarik dari cabang jauh yang berbeda dari saat ini. Misalnya Anda mungkin telah menarik diri dari Master ketika cabang Anda mengembangkan pelacakan berkembang. Git akan dengan patuh menarik komit duplikat jika ditarik dari cabang yang tidak terlacak.

Jika ini terjadi, Anda dapat melakukan hal berikut:

git reset --hard HEAD~n

dimana n == <number of duplicate commits that shouldn't be there.>

Kemudian pastikan Anda menarik dari cabang yang benar dan kemudian jalankan:

git pull upstream <correct remote branch> --rebase

Menarik dengan --rebaseakan memastikan Anda tidak menambahkan komitmen asing yang dapat memperburuk riwayat komit.

Ini sedikit pegangan tangan untuk git rebase.

ScottyBlades
sumber