Menulis ulang riwayat repositori kode, atau mengapa Anda terkadang bisa git push -f





Salah satu peringatan pertama yang diterima Padawan muda dengan akses ke repositori git adalah: "jangan pernah makan salju kuning, lakukanlah git push -f." Karena ini adalah salah satu dari ratusan maksim yang perlu dipelajari oleh seorang insinyur perangkat lunak pemula, tidak ada yang meluangkan waktu untuk menjelaskan mengapa hal ini tidak boleh dilakukan. Ini seperti bayi dan api: โ€œkorek api bukanlah mainan untuk anak-anakโ€ dan hanya itu. Tapi kita tumbuh dan berkembang sebagai manusia dan sebagai profesional, dan suatu hari pertanyaan "mengapa sebenarnya?" meningkat dalam pertumbuhan penuh. Artikel ini ditulis berdasarkan pertemuan internal kami, dengan topik: "Kapan dan haruskah Anda menulis ulang histori commit".







Saya pernah mendengar bahwa kemampuan untuk menjawab pertanyaan ini dalam wawancara di beberapa perusahaan merupakan kriteria untuk wawancara untuk posisi senior. Tetapi untuk lebih memahami jawabannya, Anda perlu memahami mengapa menulis ulang sejarah itu buruk?



Untuk melakukan ini, pada gilirannya, kita memerlukan perjalanan singkat ke dalam struktur fisik dari repositori git. Jika Anda yakin bahwa Anda mengetahui segalanya tentang perangkat repo, Anda dapat melewati bagian ini, tetapi bahkan dalam proses mencari tahu, saya belajar banyak hal baru untuk diri saya sendiri, dan sesuatu yang lama ternyata tidak terlalu relevan.



Pada level terendah, git repo adalah kumpulan objek dan penunjuk ke sana. Setiap objek memiliki hash unik 40 digit (20 byte heksadesimal), yang dihitung berdasarkan konten objek.







Ilustrasi yang diambil dari The Git Community Book



Jenis objek utama adalah blob (hanya isi file), tree (kumpulan pointer ke blob dan pohon lain), dan commit. Objek bertipe komit hanya penunjuk ke pohon, ke komit sebelumnya, dan informasi layanan: tanggal / waktu, penulis dan komentar.



Di mana cabang dan tag yang kami gunakan untuk beroperasi? Dan mereka bukan objek, mereka hanya pointer: sebuah cabang menunjuk ke komit terakhir di dalamnya, sebuah tag menunjuk ke komit sewenang-wenang di repo. Artinya, ketika kita melihat cabang yang digambar dengan indah dengan lingkaran komit pada mereka di IDE atau klien GUI, mereka dibangun dengan cepat, berjalan di sepanjang rantai komit dari ujung cabang sampai ke "root". Komit pertama dalam repo tidak memiliki yang sebelumnya, alih-alih penunjuk, ada null.



Poin penting untuk dipahami: komit yang sama dapat muncul di beberapa cabang pada waktu yang sama. Komit tidak disalin ketika cabang baru dibuat, itu hanya mulai "berkembang" dari tempat HEAD saat perintah dikeluarkan git checkout -b <branch-name>.



Jadi mengapa menulis ulang riwayat repositori berbahaya?







Pertama, dan ini jelas, saat Anda mengunggah cerita baru ke repositori yang sedang dikerjakan oleh tim teknik, orang lain mungkin akan kehilangan perubahan mereka. Perintah git push -f menghapus dari cabang di server semua komit yang tidak ada dalam versi lokal, dan menulis yang baru.



Untuk beberapa alasan, hanya sedikit orang yang tahu bahwa untuk waktu yang lama tim git pushmemiliki kunci "aman"--force-with-leaseyang menyebabkan perintah gagal jika ada komit yang ditambahkan oleh pengguna lain ke repositori jarak jauh. Saya selalu merekomendasikan untuk menggunakannya -f/--force.



Alasan kedua mengapa perintah git push -fdianggap berbahaya adalah ketika mencoba menggabungkan cabang dengan sejarah yang ditulis ulang dengan cabang di mana itu disimpan (lebih tepatnya, komit yang dihapus dari riwayat yang ditulis ulang dipertahankan), kita akan mendapatkan sejumlah konflik (dengan jumlah melakukan, sebenarnya). Ada jawaban sederhana untuk ini: jika Anda mengikuti Gitflow atau Gitlab Flow dengan cermat , situasi seperti itu kemungkinan besar bahkan tidak akan muncul.



Dan akhirnya, ada sisi yang tidak menyenangkan dari penulisan ulang sejarah: komitmen yang, seolah-olah, dihapus dari cabang, pada kenyataannya, tidak menghilang di mana pun dan hanya tetap selamanya dalam repo. Agak, tapi tidak menyenangkan. Untungnya, pengembang git telah mengatasi masalah ini juga, dengan perintah pengumpulan sampah git gc --prune. Kebanyakan git host, setidaknya GitHub dan GitLab, melakukan ini di latar belakang dari waktu ke waktu.



Jadi, setelah menghilangkan ketakutan untuk mengubah sejarah repositori, kita akhirnya dapat beralih ke pertanyaan utama: mengapa itu diperlukan dan kapan itu dibenarkan?



Faktanya, saya yakin bahwa hampir setiap pengguna git yang lebih atau kurang aktif telah mengubah riwayat setidaknya satu kali, ketika tiba-tiba ternyata ada yang tidak beres pada pengubahan terakhir: kesalahan ketik yang mengganggu merayap ke dalam kode, membuat komit bukan dari itu pengguna (dari email pribadi alih-alih kantor atau sebaliknya), lupa menambahkan file baru (jika Anda, seperti saya, suka menggunakan git commit -a). Bahkan mengubah deskripsi komit mengarah pada kebutuhan untuk menulis ulang, karena hash dihitung dari deskripsi juga!



Tapi ini kasus yang sepele. Mari kita lihat yang lebih menarik.



Katakanlah Anda membuat fitur besar, yang Anda gergaji selama beberapa hari, mengirimkan hasil kerja harian ke repositori di server (4-5 komit), dan mengirim perubahan Anda untuk ditinjau. Dua atau tiga pengulas yang tak kenal lelah menghujani Anda dengan rekomendasi besar dan kecil untuk pengeditan, atau bahkan menemukan tiang tembok (4-5 komit lagi). Kemudian QA menemukan beberapa kasus edge yang juga memerlukan perbaikan (2-3 komit lagi). Dan akhirnya, selama integrasi, ditemukan beberapa ketidakcocokan atau autotest masuk, yang juga perlu diperbaiki.



Jika sekarang Anda menekan tombol Gabung tanpa melihat, maka selusin setengah komit seperti "Fitur saya, hari 1", "Hari 2", "Perbaiki tes", "Perbaiki ulasan" akan ditambahkan ke cabang utama (bagi banyak orang, ini disebut master dengan cara lama) dll. Ini, tentu saja, membantu mode squash, yang sekarang ada di GitHub dan GitLab, tetapi Anda harus berhati-hati dengannya: pertama, ini dapat menggantikan deskripsi komit dengan sesuatu yang tidak dapat diprediksi, dan kedua, mengganti pembuat fitur pada orang yang menekan tombol Gabung (di negara kita, ini umumnya adalah robot yang membantu insinyur rilis membangun penerapan hari ini). Oleh karena itu, hal paling sederhana adalah, sebelum integrasi terakhir ke rilis, menciutkan semua komit cabang menjadi satu menggunakan git rebase.



Tetapi kebetulan juga Anda telah mendekati peninjauan kode dengan riwayat repo yang mengingatkan pada salad Olivier. Hal ini terjadi jika fitur telah digergaji selama beberapa minggu, karena pembusukan yang buruk, atau, meskipun tim yang layak dipukul dengan tempat lilin untuk ini, persyaratan telah berubah selama proses pengembangan. Misalnya, berikut adalah permintaan penggabungan nyata yang datang kepada saya untuk ditinjau dua minggu lalu:







Tangan saya secara otomatis meraih tombol "Laporkan penyalahgunaan", karena bagaimana lagi Anda dapat mencirikan permintaan 50 komit dengan hampir 2.000 baris yang diubah? Dan bagaimana, orang bertanya-tanya, meninjaunya?



Sejujurnya, saya butuh dua hari untuk memaksa diri saya memulai ulasan ini. Dan ini adalah reaksi normal bagi seorang insinyur; seseorang dalam situasi yang sama, hanya tanpa melihat, menekan Setuju, menyadari bahwa dalam waktu yang wajar mereka masih tidak dapat melakukan tugas untuk meninjau perubahan ini dengan kualitas yang memadai.



Tetapi ada cara untuk membuat hidup lebih mudah bagi seorang teman. Selain pekerjaan pendahuluan tentang penguraian masalah yang lebih baik, setelah menyelesaikan penulisan kode utama, Anda dapat membawa riwayat penulisannya ke dalam bentuk yang lebih logis, memecahnya menjadi komitmen atomik dengan pengujian hijau di masing-masing: "membuat layanan baru dan lapisan transport untuknya", "membuat model dan menulis memeriksa invarian "," menambahkan validasi dan penanganan pengecualian "," tes tulis ".

Masing-masing dari komit ini dapat ditinjau secara terpisah (baik GitHub dan GitLab dapat melakukan ini) dan melakukannya dalam penggerebekan saat beralih antar tugas atau saat istirahat.



Yang sama git rebasedengan kuncinya akan membantu kita melakukan semua ini --interactive. Sebagai parameter, Anda harus meneruskan hash dari komit, yang darinya Anda perlu menulis ulang histori. Jika kita berbicara tentang 50 komit terakhir, seperti pada contoh di gambar, Anda dapat menulis git rebase --interactive HEAD~50(gantikan angka โ€œ50โ€).



Ngomong-ngomong, jika Anda menambahkan cabang master ke diri Anda sendiri dalam proses mengerjakan tugas, maka Anda perlu melakukan rebase cabang ini terlebih dahulu sehingga menggabungkan komit dan komit dari master tidak bingung.



Berbekal pengetahuan tentang internal repositori git, seharusnya mudah untuk memahami cara kerja rebase pada master. Perintah ini mengambil semua komit di cabang kami dan mengubah induk dari yang pertama menjadi komit terakhir di cabang master. Lihat diagram:









Ilustrasi diambil dari buku Pro Git



Jika perubahan pada C4 dan C3 bertentangan, maka setelah menyelesaikan konflik, komit C4 akan mengubah isinya, sehingga dinamai ulang pada diagram kedua menjadi C4 '.



Dengan cara ini, Anda akan mendapatkan cabang yang hanya terdiri dari perubahan Anda dan tumbuh dari atas master. Tentu, master harus up-to-date. Anda bisa menggunakan versi dari server: git pull --rebase origin/master(seperti yang Anda ketahui, git pullsetara git fetch && git merge, dan kuncinya --rebaseakan memaksa git untuk melakukan rebase alih-alih menggabungkan).



Mari akhirnya kembali kegit rebase --interactive... Itu dibuat oleh programmer untuk programmer, dan menyadari stres apa yang akan dialami orang dalam prosesnya, kami mencoba menyelamatkan saraf pengguna sebanyak mungkin dan membebaskannya dari kebutuhan untuk mengejan secara berlebihan. Inilah yang akan Anda lihat di layar:





Ini adalah gudang dari paket Guzzle yang populer. Sepertinya dia bisa menggunakan rebase ...



File yang dihasilkan terbuka di editor teks. Di bawah ini Anda akan menemukan informasi rinci tentang apa yang harus dilakukan di sini. Selanjutnya, dalam mode edit mudah, Anda memutuskan apa yang harus dilakukan dengan komit di cabang Anda. Semuanya sesederhana tongkat: pilih - biarkan apa adanya, reword - ubah deskripsi komit, squash - gabungkan dengan yang sebelumnya (proses bekerja dari bawah ke atas, yaitu, yang sebelumnya adalah baris di bawah), drop - delete semuanya, edit - dan ini yang menarik adalah berhenti dan diam. Setelah git menemukan perintah edit, itu akan mengambil posisi di mana perubahan dalam komit telah ditambahkan ke mode bertahap. Anda dapat mengubah apapun dalam komit ini, menambahkan beberapa lagi di atasnya, dan kemudian perintah git rebase --continueuntuk melanjutkan proses rebase.



Oh, dan omong-omong, Anda bisa menukar komit. Ini dapat menimbulkan konflik, tetapi secara umum, proses rebase jarang sepenuhnya bebas konflik. Seperti yang mereka katakan, setelah melepaskan kepala mereka, mereka tidak menangis untuk rambut mereka.



Jika Anda bingung dan sepertinya semuanya hilang, Anda memiliki tombol ejeksi darurat git rebase --abortyang akan segera mengembalikan semuanya ke sana.



Anda dapat mengulang beberapa kali, hanya menyentuh bagian cerita, dan membiarkan sisanya tidak tersentuh dengan pick, memberikan cerita Anda tampilan yang lebih dan lebih selesai, seperti kendi tembikar. Ini adalah praktik yang baik, seperti yang saya tulis di atas, untuk memastikan bahwa tes di setiap komit akan berwarna hijau (untuk ini, edit membantu dengan sempurna dan pada langkah berikutnya - squash).



Aerobatik lain, berguna jika Anda perlu menguraikan beberapa perubahan dalam file yang sama menjadi komit yang berbeda - git add --patch. Ini dapat berguna dengan sendirinya, tetapi dalam kombinasi dengan edit direktif, ini akan memungkinkan Anda untuk membagi satu komit menjadi beberapa, dan melakukannya pada tingkat baris individu, yang, jika saya tidak salah, tidak ada klien GUI dan tidak ada IDE yang tidak mengizinkan.



Sekali lagi memastikan bahwa segala sesuatu adalah dalam rangka, Anda dapat akhirnya dengan ketenangan pikiran untuk melakukan sesuatu, apa yang mulai tutorial ini: git push --force. Oh, tentu saja --force-with-lease!







Pada awalnya, Anda kemungkinan besar akan menghabiskan satu jam untuk proses ini (termasuk rebase awal pada master), atau bahkan dua jam jika fiturnya benar-benar luas. Tetapi bahkan ini jauh lebih baik daripada menunggu dua hari sampai pengkaji memaksa dirinya untuk akhirnya menerima permintaan Anda, dan beberapa hari lagi sampai dia menyelesaikannya. Nantinya, Anda kemungkinan besar akan fit dalam 30-40 menit. Produk IntelliJ dengan alat resolusi konflik built-in (pengungkapan penuh: FunCorp membayar produk ini kepada karyawannya) sangat membantu dalam hal ini.



Satu kata peringatan terakhir adalah jangan menulis ulang sejarah cabang selama proses tinjauan kode. Ingatlah bahwa peninjau yang teliti dapat menggandakan kode Anda secara lokal agar dapat melihatnya melalui IDE dan menjalankan pengujian.



Terima kasih untuk semua orang yang membaca sampai akhir! Saya berharap artikel ini bermanfaat tidak hanya untuk Anda, tetapi juga untuk rekan kerja yang menerima kode Anda untuk ditinjau. Jika Anda memiliki beberapa peretasan git yang keren - bagikan di komentar!



All Articles