Pengaturan pointer orangtua git ke orangtua yang berbeda

220

Jika saya memiliki komit di masa lalu yang menunjuk ke satu orangtua, tapi saya ingin mengubah orangtua yang ditunjuknya, bagaimana saya bisa melakukan itu?

dougvk
sumber

Jawaban:

404

Menggunakan git rebase. Ini adalah generik "take commit (s) dan plop / mereka di atas perintah parent (base)" yang berbeda di Git.

Beberapa hal yang perlu diketahui:

  1. Karena SHA komit melibatkan orang tua mereka, ketika Anda mengubah induk dari komit yang diberikan, SHA-nya akan berubah - seperti halnya SHA dari semua komit yang datang setelahnya (lebih baru daripada itu) di jalur pengembangan.

  2. Jika Anda bekerja dengan orang lain, dan Anda telah mendorong komit yang dipertanyakan ke tempat mereka menariknya, memodifikasi komit mungkin adalah Ide Buruk ™. Ini karena # 1, dan dengan demikian kebingungan yang dihasilkan repositori pengguna lain akan menghadapi ketika mencoba untuk mencari tahu apa yang terjadi karena SHA Anda tidak lagi cocok dengan mereka untuk komitmen "sama". (Lihat bagian "MEMULIHKAN DARI REBASE UPSTREAM" dari halaman manual yang terhubung untuk detail.)

Yang mengatakan, jika Anda saat ini di cabang dengan beberapa komitmen yang ingin Anda pindahkan ke orangtua baru, itu akan terlihat seperti ini:

git rebase --onto <new-parent> <old-parent>

Itu akan memindahkan segalanya setelah <old-parent> di cabang saat ini untuk duduk di atas <new-parent>sebagai gantinya.

Amber
sumber
Saya menggunakan git rebase --untuk mengubah orang tua tetapi sepertinya saya hanya menghasilkan satu komit. Saya mengajukan pertanyaan di atasnya: stackoverflow.com/questions/19181665/…
Eduardo Mello
7
Aha, jadi ITULAH --ontodigunakan untuk apa! Dokumentasinya selalu sama sekali tidak jelas bagi saya, bahkan setelah membaca jawaban ini!
Daniel C. Sobral
6
Baris ini: git rebase --onto <new-parent> <old-parent>Memberi tahu saya segala sesuatu tentang bagaimana menggunakan rebase --ontosetiap pertanyaan dan dokumen yang saya baca sejauh ini gagal.
jpmc26
3
Bagi saya perintah ini harus membaca:, rebase this commit on that oneatau git rebase --on <new-parent> <commit>. Menggunakan orangtua lama di sini tidak masuk akal bagi saya.
Samir Aguiar
1
@kopranb Orang tua yang lama penting ketika Anda ingin membuang beberapa jalur dari kepala Anda ke kepala jarak jauh. Kasing yang Anda uraikan ditangani oleh git rebase <new-parent>.
clacke
35

Jika ternyata Anda perlu menghindari rebasing komit berikutnya (misalnya karena penulisan ulang sejarah tidak akan bisa dipertahankan), maka Anda dapat menggunakan ganti git (tersedia di Git 1.6.5 dan yang lebih baru).

# …---o---A---o---o---…
#
# …---o---B---b---b---…
#
# We want to transplant B to be "on top of" A.
# The tree of descendants from B (and A) can be arbitrarily complex.

replace_first_parent() {
    old_parent=$(git rev-parse --verify "${1}^1") || return 1
    new_parent=$(git rev-parse --verify "${2}^0") || return 2
    new_commit=$(
      git cat-file commit "$1" |
      sed -e '1,/^$/s/^parent '"$old_parent"'$/parent '"$new_parent"'/' |
      git hash-object -t commit -w --stdin
    ) || return 3
    git replace "$1" "$new_commit"
}
replace_first_parent B A

# …---o---A---o---o---…
#          \
#           C---b---b---…
#
# C is the replacement for B.

Dengan penggantian di atas ditetapkan, setiap permintaan untuk objek B benar-benar akan mengembalikan objek C. Isi C persis sama dengan isi B kecuali untuk orang tua pertama (orang tua yang sama (kecuali untuk yang pertama), pohon yang sama, pesan komit yang sama).

Penggantian aktif secara default, tetapi dapat diaktifkan dengan menggunakan --no-replace-objectsopsi untuk git (sebelum nama perintah) atau dengan mengatur GIT_NO_REPLACE_OBJECTSvariabel lingkungan. Penggantian dapat dibagi dengan mendorong refs/replace/*(selain yang normal refs/heads/*).

Jika Anda tidak menyukai komit-munging (dilakukan dengan sed di atas), maka Anda dapat membuat komit pengganti Anda menggunakan perintah tingkat yang lebih tinggi:

git checkout B~0
git reset --soft A
git commit -C B
git replace B HEAD
git checkout -

Perbedaan besar adalah bahwa urutan ini tidak menyebarkan orang tua tambahan jika B adalah gabungan komit.

Chris Johnsen
sumber
Dan kemudian, bagaimana Anda mendorong perubahan ke repositori yang ada? Tampaknya bekerja secara lokal, tetapi git push tidak mendorong apa pun setelah itu
Baptiste Wicht
2
@BaptisteWicht: Penggantian disimpan dalam set referensi yang terpisah. Anda perlu mengatur untuk mendorong (dan mengambil) refs/replace/hierarki referensi. Jika Anda hanya perlu melakukannya sekali, maka Anda dapat melakukannya git push yourremote 'refs/replace/*'di repositori sumber, dan git fetch yourremote 'refs/replace/*:refs/replace/*'di repositori tujuan. Jika Anda perlu melakukannya berkali-kali, Anda bisa menambahkan refspec tersebut ke remote.yourremote.pushvariabel config (dalam repositori sumber) dan remote.yourremote.fetchvariabel config (dalam repositori tujuan).
Chris Johnsen
3
Cara yang lebih sederhana untuk melakukan ini adalah menggunakan git replace --grafts. Tidak yakin kapan ini ditambahkan, tetapi seluruh tujuannya adalah untuk membuat pengganti yang sama dengan komit B tetapi dengan orang tua yang ditentukan.
Paul Wagland
32

Perhatikan bahwa mengubah komit di Git mengharuskan semua komit yang mengikutinya juga harus diubah. Ini tidak disarankan jika Anda telah menerbitkan bagian sejarah ini, dan seseorang mungkin telah membangun karya mereka berdasarkan sejarah sebelum perubahan.

Solusi alternatif untuk git rebasedisebutkan dalam respons Amber adalah dengan menggunakan mekanisme cangkok (lihat definisi Git cangkok di Git Glosarium dan dokumentasi .git/info/graftsfile dalam dokumentasi Tata Letak Repositori Git ) untuk mengubah induk dari komit, periksa apakah ia melakukan hal yang benar dengan beberapa penampil riwayat ( gitk, git log --graph, dll.) dan kemudian menggunakan git filter-branch(seperti yang dijelaskan dalam bagian "Contoh" dari halaman manualnya) untuk membuatnya permanen (dan kemudian menghapus graft, dan secara opsional menghapus referensi asli yang didukung oleh git filter-branch, atau memilih repositori):

gema "$ commit-id $ graft-id" >>. git / info / grafts
git filter-branch $ graft-id..HEAD

CATATAN !!! Solusi ini berbeda dari solusi rebase dalam hal itu git rebaseakan rebase / transplantasi perubahan , sedangkan solusi berbasis cangkok hanya akan reparent berkomitmen seperti apa adanya , tidak memperhitungkan perbedaan akun antara orangtua lama dan orangtua baru!

Jakub Narębski
sumber
3
Dari apa yang saya mengerti (dari git.wiki.kernel.org/index.php/GraftPoint ), git replacetelah menggantikan git cangkokan (dengan asumsi Anda memiliki git 1.6.5 atau lebih baru).
Alexander Bird
4
@ Thr4wn: Sebagian besar benar, tetapi jika Anda ingin menulis ulang sejarah maka cangkok + git-filter-cabang (atau rebase interaktif, atau ekspor cepat + misalnya reposurgeon) adalah cara untuk melakukannya. Jika Anda ingin / perlu melestarikan sejarah , maka git-replace jauh lebih unggul daripada cangkok.
Jakub Narębski
@ JakubNarębski, bisakah Anda menjelaskan apa yang Anda maksud dengan menulis ulang vs melestarikan sejarah? Mengapa saya tidak bisa menggunakan git-replace+ git-filter-branch? Dalam pengujian saya, filter-branchtampaknya menghormati penggantian, rebasing seluruh pohon.
cdunn2001
1
@ cdunn2001: git filter-branchmenulis ulang sejarah, menghormati cangkok dan penggantian (tetapi dalam sejarah penulisan ulang penggantian akan diperdebatkan); push akan menghasilkan perubahan non fasf-forward. git replacemenciptakan pengganti yang dapat ditransfer di tempat; push akan maju cepat, tetapi Anda harus mendorong refs/replaceuntuk mentransfer pengganti agar riwayatnya terkoreksi. HTH
Jakub Narębski
23

Untuk memperjelas jawaban di atas dan pasang tanpa malu skrip saya sendiri:

Itu tergantung pada apakah Anda ingin "rebase" atau "reparent". Sebuah rebase , seperti yang disarankan oleh Amber , bergerak di sekitar diffs . Seorang reparen , seperti yang disarankan oleh Jakub dan Chris , bergerak di sekitar foto seluruh pohon. Jika Anda ingin reparen, saya sarankan menggunakan git reparentdaripada melakukan pekerjaan secara manual.

Perbandingan

Misalkan Anda memiliki gambar di sebelah kiri dan Anda ingin terlihat seperti gambar di sebelah kanan:

                   C'
                  /
A---B---C        A---B---C

Baik rebasing dan reparenting akan menghasilkan gambaran yang sama, tetapi definisi C'berbeda. Dengan git rebase --onto A B, C'tidak akan berisi perubahan apa pun yang diperkenalkan oleh B. Dengan git reparent -p A, C'akan identik dengan C(kecuali yang Btidak akan ada dalam sejarah).

Mark Lodato
sumber
1
+1 Saya telah bereksperimen dengan reparentskrip; itu bekerja dengan indah; sangat dianjurkan . Saya juga merekomendasikan membuat branchlabel baru , dan ke checkoutcabang itu sebelum memanggil (karena label cabang akan pindah).
Rhubbarb
Perlu juga dicatat bahwa: (1) simpul asli tetap utuh, dan (2) simpul baru tidak mewarisi anak-anak simpul asli.
Rhubbarb
0

Jawaban @ Jakub pasti membantu saya beberapa jam yang lalu ketika saya mencoba hal yang sama persis seperti OP.
Namun, git replace --graftsekarang solusi yang lebih mudah mengenai cangkok. Juga, masalah utama dengan solusi itu adalah bahwa cabang-filter membuat saya kehilangan setiap cabang yang tidak digabungkan ke dalam cabang KEPALA. Kemudian, git filter-repolakukan pekerjaan itu dengan sempurna dan tanpa cacat.

$ git replace --graft <commit> <new parent commit>
$ git filter-repo --force

Untuk info lebih lanjut: checkout bagian "Menyambung ulang riwayat" di dokumen

Teodoro
sumber