Saya ditunjukkan bukti: output dari dua perintah. Yang pertama adalah
git show deadbeef
- menunjukkan perubahan pada file, sebut saja Page.php. Metode canBeEdited dan penggunaannya telah ditambahkan ke dalamnya.
Dan dalam output dari perintah kedua -
git log -p Page.php
- tidak ada komit deadbeef. Dan dalam versi file Page.php saat ini tidak ada metode canBeEdited.
Tidak menemukan solusi dengan cepat, kami membuat tambalan lain untuk master, mengatur perubahan - dan saya memutuskan bahwa saya akan kembali ke masalah dengan pikiran yang segar.
"Keluar topik"
, Git. , , .
Apakah itu dilakukan dengan sengaja? File telah diganti namanya?
Saya mulai mencari masalah dengan meminta bantuan dalam obrolan tim insinyur rilis. Mereka bertanggung jawab untuk menghosting repositori dan mengotomatiskan proses terkait Git, antara lain. Sejujurnya, mereka mungkin bisa menghapus tambalan itu, tapi mereka akan melakukannya tanpa jejak.
Salah satu insinyur rilis menyarankan untuk menjalankan git log dengan opsi --follow. Mungkin file telah diganti namanya dan oleh karena itu Git tidak menunjukkan beberapa perubahan.
--follow
Lanjutkan daftar riwayat file di luar ganti nama (hanya berfungsi untuk satu file).
(Tampilkan riwayat file setelah mengganti namanya (hanya berfungsi untuk file tunggal))
Ada
git log --follow Page.php
deadbeef dalam output , tetapi tidak ada file yang dihapus atau diganti namanya. Namun tidak terlihat bahwa metode canBeEdited telah dihapus di suatu tempat. Opsi ikuti tampaknya berperan dalam cerita ini, tetapi kemana perginya perubahan masih belum jelas.
Sayangnya, repositori yang dimaksud adalah salah satu yang terbesar yang kami miliki. Dari saat patch pertama diperkenalkan hingga hilang, ada 21.000 komit. Beruntung juga bahwa file yang diperlukan hanya diedit di sepuluh dari mereka. Saya mempelajari semuanya dan tidak menemukan sesuatu yang menarik.
Kami mencari saksi! Kami membutuhkan livebear
Berhenti! Kami hanya mencari bangkai? Mari berpikir logis: harus ada komit, sebut saja livebear, setelah itu deadbeef tidak lagi ditampilkan di riwayat file. Mungkin ini tidak akan memberi kita apa-apa, tetapi itu akan memberi kita beberapa pemikiran.
Ada perintah git bisect untuk menelusuri riwayat Git. Menurut dokumentasi , ini memungkinkan Anda untuk menemukan komit di mana bug pertama kali muncul. Dalam praktiknya, ini dapat digunakan untuk menemukan momen apa pun dalam sejarah jika Anda tahu cara menentukan apakah momen itu telah tiba. Bug kami adalah kurangnya perubahan pada kode. Saya dapat memeriksa ini dengan perintah lain - git grep. Bagaimanapun, cukup bagi saya untuk mengetahui apakah ada metode canBeEdited di Page.php. Sedikit debugging dan membaca dokumentasi:
livebear [build]: Gabungkan cabang asal / XXX ke build_web_yyyy.mm.dd.hh
Ini terlihat seperti komit gabungan normal dari cabang tugas dengan cabang rilis. Tetapi dengan komit ini saya berhasil mereproduksi masalah:
$ git checkout -b test livebear^1 2>/dev/null $ grep -c canBeEdited Page.php 2 $ git merge β-no-edit -βno-stat livebear^2 Removing β¦ β¦ Removing β¦ Merge made by the βrecursiveβ strategy. $ grep -c canBeEdited Page.php 0 $ git log -p Page.php | grep -c canBeEdited 0
Benar, saya tidak menemukan sesuatu yang menarik dalam livebear, dan hubungannya dengan masalah kami tetap tidak jelas. Setelah berpikir sedikit, saya mengirimkan hasil pencarian saya ke pengembang: kami setuju bahwa, bahkan jika kami mendapatkan kebenaran, skema reproduksi akan terlalu rumit dan kami tidak akan dapat mengasuransikan hal seperti ini di masa depan. Oleh karena itu, kami secara resmi memutuskan untuk berhenti mencari.
Namun, rasa ingin tahu saya tetap tidak terpuaskan.
Kegigihan bukanlah sifat buruk, tapi sangat menjijikkan
Beberapa kali saya kembali ke masalah, menjalankan git bisect dan menemukan semakin banyak komitmen. Semua mencurigakan, semuanya merger, tapi itu tidak memberi saya apa-apa. Tampak bagi saya bahwa satu komitmen kemudian lebih sering datang kepada saya daripada yang lain, tetapi saya tidak yakin bahwa dialah yang pada akhirnya menjadi pelakunya.
Tentu saja saya juga mencoba metode pencarian lain. Misalnya, beberapa kali saya melewati 21.000 komitmen yang dibuat pada saat masalah. Itu tidak terlalu menarik, tapi saya menemukan pola yang menarik. Saya menjalankan perintah yang sama:
git grep -c canBeEdited {commit} -- Page.php
Ternyata komit yang "buruk", yang tidak memiliki kode yang diperlukan, berada di cabang yang sama! Dan pencarian di utas ini dengan cepat membawa saya ke sebuah petunjuk:
changekiller Gabungkan cabang 'master' ke dalam TICKET-XXX_description
Ini juga merupakan gabungan dari dua cabang. Dan ketika mencoba mengulangnya secara lokal, ada konflik di file yang dibutuhkan - Page.php. Dilihat dari keadaan repositori, pengembang meninggalkan versinya file, membuang perubahan dari master (yaitu, perubahan tersebut hilang). Waktu yang lama berlalu, dan pengembang tidak ingat apa yang sebenarnya terjadi, tetapi dalam praktiknya situasinya direproduksi dalam urutan sederhana:
git checkout -b test changekiller^1 git merge -s ours changekiller^2
Masih harus dilihat bagaimana urutan tindakan yang sah dapat mengarah pada hasil seperti itu. Tidak menemukan apa pun tentangnya di dokumentasi, saya masuk ke kode sumber.
Apakah si pembunuh Git?
Dokumentasi mengatakan bahwa git log menerima banyak komit sebagai masukan dan harus menunjukkan kepada pengguna komit orang tua mereka, tidak termasuk orang tua dari komit yang dikirimkan dengan ^ di depannya. Ternyata git log A ^ B harus menunjukkan komit yang merupakan induk dari A dan bukan induk dari B.
Kode perintah ternyata cukup rumit. Ada banyak pengoptimalan yang berbeda untuk bekerja dengan memori, dan secara umum, membaca kode C bagi saya bukan pengalaman yang menyenangkan. Logika dasar dapat direpresentasikan dengan pseudocode berikut:
// , commit commit; rev_info revs; revs = setup_revisions(revisions_range); while (commit = get_revision(revs)) { log_tree_commit(commit); }
Di sini fungsi get_revision menerima revs, satu set flag kontrol, sebagai input. Setiap panggilannya harus memberikan komit berikutnya untuk diproses dalam urutan yang benar (atau kekosongan, ketika kita sampai di bagian akhir). Ada juga fungsi setup_revisi yang mengisi struktur revs dan log_tree_commit, yang menampilkan informasi di layar.
Saya memiliki perasaan bahwa saya menemukan di mana mencari masalah. Saya memberikan file tertentu (Page.php) ke perintah, karena saya hanya tertarik pada perubahannya. Ini berarti bahwa git log harus memiliki semacam logika untuk memfilter komit "ekstra". Fungsi setup_revision dan get_revision telah digunakan di banyak tempat - hampir tidak menjadi masalah dengan mereka. Itu meninggalkan log_tree_commit.
Saya sangat senang, dalam fungsi ini benar-benar ada kode yang menghitung perubahan apa yang dibuat dalam komit tertentu. Saya pikir logika umum akan terlihat seperti ini:
void log_tree_commit(commit) { if (tree_has_changed(commit, commit->parents)) { log_tree_commit_1(commit); } }
Tetapi semakin lama saya melihat kode yang sebenarnya, semakin saya menyadari bahwa saya salah. Fungsi ini hanya mencetak pesan. Jadi percayalah perasaan Anda setelah itu!
Saya kembali ke fungsi setup_revision dan get_revision. Logika pekerjaan mereka sulit untuk dipahami - "kabut" fungsi tambahan ikut campur, beberapa di antaranya diperlukan untuk bekerja dengan benar dengan pointer dan memori. Semuanya tampak seolah-olah logika utama adalah traversal luas pertama yang sederhana dari pohon komit, yaitu algoritme yang cukup standar:
rev_info setup_revisions(revisions_range, ...) { rev_info rev; commit commit; // β for (commit = get_commit_from_range(revisions_range)) { revs->commits = commit_list_append(commit, revs->commits) } } commit get_revision(rev_info revs) { commit c; commit l; c = get_revision_1(revs); for (l = c->parents; l; l = l->next) { commit_list_insert(l, &revs->commits); } return c; } commit get_revision_1(rev_info revs) { return pop_commit(revs->commits); }
Sebuah daftar dibuat (revs-> komit), elemen pertama (paling atas) dari pohon komit ditempatkan di sana. Kemudian, komit dari awal secara bertahap diambil dari daftar ini, dan orang tua mereka ditambahkan di akhir.
Membaca kodenya, saya menemukan bahwa di antara fungsi pembantu "kabut", ada logika kompleks untuk memfilter komit, yang telah lama saya cari. Ini terjadi di fungsi get_revision_1:
commit get_revision_1(rev_info revs) { commit commit; commit = pop_commit(revs->commits); try_to_sipmlify_commit(commit); return commit; } void try_to_simplify_commit(commit commit) { for (parent = commit->parents; parent; parent = parent->next) { if (rev_compare_tree(revs, parent, commit) == REV_TREE_SAME) { parent->next = NULL; commit->parents = parent; } } }
Dalam kasus ketika beberapa cabang digabungkan, jika status file tetap sama seperti di salah satunya, tidak masuk akal untuk mempertimbangkan cabang lain. Jika status file tidak berubah di mana pun, kami hanya akan meninggalkan cabang pertama.
Contoh. Mari kita tunjukkan dengan nol komit di mana file tidak berubah, dengan satu - yang file telah berubah, dan X - gabungan cabang.
Dalam situasi ini, kode tidak akan mempertimbangkan cabang fitur - tidak ada perubahan di dalamnya. Jika file diubah di sana, maka di X perubahan itu "dibuang", yang berarti riwayatnya tidak terlalu relevan: kode ini sudah tidak ada lagi.
Hal serupa terjadi pada kami. Dua pengembang membuat perubahan dalam file yang sama - Page.php, satu di cabang master, di komit deadbeef, dan yang kedua di cabang tugas mereka.
Ketika pengembang kedua menggabungkan perubahan dari cabang master ke cabang tugas, konflik terjadi, selama penyelesaiannya dia hanya membuang perubahan dari master. Waktu berlalu, dia selesai mengerjakan tugas, dan cabang tugas diunggah ke master, sehingga menghapus perubahan dari komit deadbeef.
Komitmen itu sendiri tetap ada. Tetapi jika Anda menjalankan git log dengan parameter Page.php, Anda tidak akan melihat komit deadbeef di keluaran.
Optimasi adalah pekerjaan tanpa pamrih
Saya terburu-buru mempelajari aturan untuk mengirimkan perubahan dan bug ke Git itu sendiri. Lagi pula, saya pikir saya telah menemukan masalah yang sangat serius: pikirkan saja, beberapa komit hilang begitu saja dari output - dan ini adalah perilaku default! Untungnya, aturannya ternyata banyak, waktunya sudah larut, dan keesokan paginya sekring saya hilang.
Saya menyadari bahwa pengoptimalan ini sangat mempercepat kinerja Git pada repositori besar seperti milik kami. Ada juga dokumentasinya di man git-rev-list , dan perilaku ini bisa dimatikan dengan sangat mudah.
Ngomong-ngomong, bagaimana --follow terlibat dalam cerita ini?
Sebenarnya, ada banyak cara untuk mempengaruhi cara kerja logika ini. Secara khusus, tentang bendera ikuti di kode Git, sebuah komentar ditemukan 13 tahun yang lalu:
Tidak dapat memangkas komitmen dengan mengganti nama berikut: jalur berubah.
(Terjemahan: Tidak dapat membuang komit saat penggantian nama sedang berlangsung: jalur dapat berubah)
PS
I sendiri telah menjadi bagian dari tim teknik rilis Badoo selama beberapa tahun sekarang, dan banyak di perusahaan percaya bahwa kami memahami Git.
(Terjemahan. Asli: xkcd.com/1597 )
Dalam hal ini, kami harus menangani masalah yang muncul dalam sistem ini, dan beberapa di antaranya menurut saya cukup membuat penasaran - seperti, misalnya, dijelaskan dalam artikel ini. Sangat sering masalah diselesaikan dengan cepat: kami telah menjumpai banyak hal, sesuatu dijelaskan dengan baik dalam dokumentasi. Kasus ini merupakan pengecualian.
Sebenarnya, dokumentasinya memang memiliki bagian Penyederhanaan Sejarah, tetapi itu hanya untuk perintah git rev-list dan saya tidak berpikir untuk mencarinya di sana. Enam bulan lalu, bagian ini disertakan dalam manual perintah git log, tetapi kasus kami terjadi sedikit lebih awal - saya sama sekali tidak punya waktu untuk menyelesaikan artikel ini. (*)
Dan akhirnya, saya memiliki bonus kecil bagi mereka yang telah membaca sampai akhir. Saya memiliki repositori yang sangat kecil tempat masalahnya muncul kembali:
$ git clone https://github.com/Md-Cake/lost-changes.git Cloning into 'lost-changes'... β¦ $ git log --oneline test.php edfd6a4 master: print 3 between 1 and 2 096d4cf init $ git log --oneline --full-history test.php afea493 (HEAD -> master, origin/master, origin/HEAD) Merge branch 'changekiller' 57041b8 (origin/changekiller) print 4 between 1 and 2 edfd6a4 master: print 3 between 1 and 2 096d4cf init
Terima kasih atas perhatiannya!
(*) UPD: Ternyata bagian Penyederhanaan Riwayat telah ada dalam dokumentasi perintah git log lebih dari enam bulan, dan saya hanya melewatinya. Terima kasih kamu kerenyang menarik perhatian ini!