Tugas
Diberikan: sebuah proyek berdasarkan OpenWRT (dan itu didasarkan pada BuildRoot) dengan satu repositori tambahan yang terhubung sebagai umpan. Tugas: menggabungkan repositori tambahan dengan yang utama.
Latar Belakang
Kami membuat router dan, suatu hari, kami ingin memberi pelanggan kemampuan untuk memasukkan aplikasi mereka ke dalam firmware. Agar tidak menderita dengan alokasi SDK, toolchain, dan kesulitan yang menyertainya, kami memutuskan untuk meletakkan seluruh proyek di github dalam repositori pribadi. Struktur repositori:
/target //
/toolchain // gcc, musl
/feeds //
/package //
...
Diputuskan untuk mentransfer beberapa aplikasi dari pengembangan kita sendiri dari repositori utama ke yang tambahan, sehingga tidak ada yang memata-matai. Kami melakukan semuanya, meletakkannya di github dan itu menjadi bagus.
Banyak air mengalir di bawah jembatan sejak saat itu…
Klien sudah lama tidak ada, repositori telah dihapus dari github, dan ide untuk memberi klien akses ke repositori itu busuk. Namun, dua repositori tetap ada dalam proyek tersebut. Dan semua skrip / aplikasi, dengan satu atau lain cara yang terkait dengan git, dipaksa untuk menjadi rumit untuk bekerja dengan struktur seperti itu. Sederhananya, ini adalah hutang teknis. Misalnya, untuk memastikan reproduktifitas rilis, Anda perlu mengkomit ke repositori primer sebuah file, secondary.version, dengan hash dari repositori kedua. Tentu saja, skrip melakukannya, dan itu tidak terlalu sulit untuk itu. Tapi, ada selusin skrip seperti itu, dan semuanya lebih rumit daripada yang seharusnya. Secara umum, saya membuat keputusan yang disengaja untuk menggabungkan repositori sekunder kembali ke yang utama. Pada saat yang sama, kondisi utama ditetapkan - untuk menjaga reproduktifitas rilis.
Setelah kondisi seperti itu ditetapkan, maka metode penggabungan yang sepele, seperti melakukan semuanya dari sekunder secara terpisah dan kemudian, dari atas, membuat komit penggabungan dari dua pohon independen, tidak akan berfungsi. Anda harus membuka kap mesin dan mengotori tangan Anda.
Struktur data Git
Pertama, seperti apa repositori git itu? Ini adalah database objek. Objek terdiri dari tiga jenis: blob, pohon, dan komit. Semua objek ditangani oleh hash sha1 dari kontennya. Bodohnya, blob adalah data tanpa atribut tambahan. Pohon adalah daftar yang diurutkan dari tautan ke pohon dan gumpalan dalam bentuk "<right> <type> <hash> <name>" (di mana <type> adalah gumpalan atau pohon). Jadi, pohon adalah seperti direktori dalam sistem file, dan gumpalan seperti file. Komit berisi nama penulis dan pembuat, tanggal pembuatan dan penambahan, komentar, hash pohon, dan nomor acak (biasanya satu atau dua) tautan ke komit induk. Tautan ini ke komitmen induk mengubah basis objek menjadi digraf asiklik (di antara orang asing, dikenal sebagai DAG).Baca secara detaildi sini :
Jadi, tugas kita telah diubah menjadi tugas membuat digraf baru, mengulangi struktur yang lama. Tetapi dengan penggantian komit dari file secondary.version dengan komit dari repositori tambahan,
proses pengembangan jauh dari gitflow klasik. Kami melakukan segalanya untuk master, berusaha untuk tidak merusaknya pada saat yang bersamaan. Kami membuat bangunan dari sana. Jika perlu, kami membuat cabang penstabil, yang kemudian kami gabungkan kembali menjadi master. Karenanya, grafik repositori tampak seperti batang telanjang sequoia yang dikepang dengan tanaman merambat.
Analisis
Tugas ini secara alami dibagi menjadi dua tahap: analisis dan sintesis. Karena untuk sintesis jelas perlu dijalankan dari saat penempatan repositori sekunder ke semua tag dan cabang, memasukkan komit dari repositori kedua, maka pada tahap analisis Anda perlu menemukan tempat untuk memasukkan komit sekunder dan komit ini sendiri. Jadi, Anda perlu membuat grafik yang diperkecil, di mana node akan menjadi komit dari grafik utama yang mengubah file secondary.version (komit kunci). Selain itu, jika node git ini merujuk ke orang tua, maka dalam grafik baru diperlukan referensi ke turunan. Saya membuat tupel bernama:
node = namedtuple(‘Node’, [‘primary_commit’, ‘secondary_commit’, ‘children’])
reservasi yang diperlukan
, . , .
Saya memasukkannya ke dalam kamus:
master_tip = repo.commit(‘master’)
commit_map = {master_tip : node(master_tip, get_sec_commit(master_tip), [])}
Saya meletakkan semua komit yang mengubah versi sekunder.versi di sana:
for c in repo.iter_commits(all=True, path=’secondary.verion’) :
commit_map[c] = node(c, get_sec_commit(c), [])
Dan saya membuat algoritme rekursif sederhana:
def build_dag(commit, commit_map, node):
for p in commit.parents :
if p in commit_map :
if node not in commit_map[p].children :
commit_map[p].children.append(node)
build_dag(p, commit_map, commit_map[p])
else :
build_dag(p, commit_map, node)
Artinya, seolah-olah, saya merentangkan simpul kunci ke masa lalu dan menghubungkannya dengan orang tua baru.
Saya menjalankannya dan ... kedalaman rekursi maksimum RuntimeError melebihi
Bagaimana hal itu bisa terjadi? Apakah ada terlalu banyak komitmen? git log dan wc tahu jawabannya. Total komit sejak pemisahan sekitar 20.000, dan yang memengaruhi secondary.version - hampir 700. Resepnya diketahui, kita memerlukan versi non-rekursif.
def build_dag(master_tip, commit_map, master_node):
to_process = [(master_tip, master_node)]
while len(to_process) > 0:
c, node = to_process.pop()
for p in c.parents :
if p in commit_map :
if node not in commit_map[p].children :
commit_map[p].children.append(node)
to_process.append(p, commit_map[p])
else :
to_process.append(p, node)
(Dan Anda mengatakan bahwa semua algoritme ini diperlukan hanya untuk lulus wawancara!) Saya
meluncurkannya, dan ... berhasil. Satu menit, lima, dua puluh ... Tidak, Anda tidak bisa selama itu. Saya berhenti. Rupanya, setiap komit dan setiap jalur diproses berkali-kali. Ada berapa cabang di pohon itu? Ternyata ada 40 cabang di pohon itu dan karenanya,jalur yang berbeda hanya dari master. Dan ada banyak jalur yang mengarah ke bagian signifikan dari komitmen utama. Karena saya tidak memiliki ribuan tahun di toko, saya perlu mengubah algoritme sehingga setiap komit diproses tepat satu kali. Untuk melakukan ini, saya menambahkan satu set, di mana saya menandai setiap komit yang diproses. Tetapi ada masalah kecil: dengan pendekatan ini, beberapa tautan akan hilang, karena jalur yang berbeda dengan komit kunci yang berbeda dapat melalui komit yang sama, dan hanya yang pertama yang melangkah lebih jauh. Untuk mengatasi masalah ini, saya mengganti set dengan kamus, di mana kuncinya adalah komit, dan nilainya adalah daftar komitmen kunci yang dapat dijangkau:
def build_dag(master_tip, commit_map, master_node):
processed_commits = {}
to_process = [(master_tip, master_node, [])]
while len(to_process) > 0:
c, node, path = to_process.pop()
p_node = commit_map.get(c)
if p_node :
commit_map[p].children.append(p_node)
for path_c in path :
if all(p_node.trunk_commit != nc.trunk_commit for nc
in processed_cmmts[path_c]) :
processed_cmmts[path_c].append(p_node)
path = []
node = p_node
processed_cmmts[c] = []
for p in c.parents :
if p != root_commit and and p not in processed_cmmts :
newpath = path.copy()
newpath.append(c)
to_process.append((p, node, newpath,))
else :
p_node = commit_map.get(p)
if p_node is None :
p_nodes = processed_cmmts.get(p, [])
else :
p_nodes = [p_node]
for pn in p_nodes :
node.children.append(pn)
if all(pn.trunk_commit != nc.trunk_commit for nc
in processed_cmmts[c]) :
processed_cmmts[c].append(pn)
for path_c in path :
if all(pn.trunk_commit != nc.trunk_commit
for nc in processed_cmmts[path_c]) :
processed_cmmts[path_c].append(pn)
Sebagai hasil dari pertukaran memori tanpa seni ini untuk suatu waktu, grafik dibuat dalam 30 detik.
Perpaduan
Sekarang saya memiliki commit_map dengan node kunci yang ditautkan ke grafik melalui tautan anak. Untuk kenyamanan, saya akan mengubahnya menjadi urutan pasangan (leluhur, keturunan) . Urutan harus dijamin bahwa semua pasangan di mana node terjadi sebagai anak ditempatkan lebih awal dari pasangan mana pun di mana node muncul sebagai leluhur. Kemudian Anda hanya perlu melihat daftar ini dan melakukan komit pertama dari repositori utama, lalu dari repositori tambahan. Di sini Anda perlu mengingat bahwa komit berisi tautan ke pohon, yang merupakan status sistem file. Karena repositori tambahan berisi subdirektori tambahan dalam paket / direktori, maka perlu membuat pohon baru untuk semua komitmen. Di versi pertama, saya hanya menulis blob ke file dan meminta git untuk membuat indeks di direktori kerja. Namun, metode ini tidak terlalu produktif. Masih ada 20.000 komitmen, dan masing-masing perlu dilakukan lagi. Jadi kinerja sangat penting. Sedikit penelitian ke internal GitPython membawa saya ke kelas gitdb.LooseObjectDB , yang mengekspos objek repositori git secara langsung. Dengannya, blob dan pohon (dan objek lain juga) dari satu repositori dapat ditulis langsung ke yang lain. Properti hebat dari database objek git adalah bahwa alamat objek apa pun adalah hash datanya. Oleh karena itu, blob yang sama akan memiliki alamat yang sama, bahkan di repositori yang berbeda.
secondary_paths = set()
ldb = gitdb.LooseObjectDB(os.path.join(repo.git_dir, 'objects'))
while len(pc_pairs) > 0:
parent, child = pc_pairs.pop()
for c in all_but_last(repo.iter_commits('{}..{}'.format(
parent.trunk_commit, child.trunk_commit), reverse = True)) :
newparents = [new_commits.get(p, p) for p in c.parents]
new_commits[c] = commit_primary(repo, newparents, c, secondary_paths)
newparents = [new_commits.get(p, p) for p in child.trunk_commit.parents]
c = secrepo.commit(child.src_commit)
sc_message = 'secondary commits {}..{} <devonly>'.format(
parent.src_commit, child.src_commit)
scm_details = '\n'.join(
'{}: {}'.format(i.hexsha[:8], textwrap.shorten(i.message, width = 70))
for i in secrepo.iter_commits(
'{}..{}'.format(parent.src_commit, child.src_commit), reverse = True))
sc_message = '\n\n'.join((sc_message, scm_details))
new_commits[child.trunk_commit] = commit_secondary(
repo, newparents, c, secondary_paths, ldb, sc_message)
Fungsi komit itu sendiri:
def commit_primary(repo, parents, c, secondary_paths) :
head_tree = parents[0].tree
repo.index.reset(parents[0])
repo.git.read_tree(c.tree)
for p in secondary_paths :
# primary commits don't change secondary paths, so we'll just read secondary
# paths into index
tree = head_tree.join(p)
repo.git.read_tree('--prefix', p, tree)
return repo.index.commit(c.message, author=c.author, committer=c.committer
, parent_commits = parents
, author_date=git_author_date(c)
, commit_date=git_commit_date(c))
def commit_secondary(repo, parents, sec_commit, sec_paths, ldb, message):
repo.index.reset(parents[0])
if len(sec_paths) > 0 :
repo.index.remove(sec_paths, r=True, force = True, ignore_unmatch = True)
for o in sec_commit.tree.traverse() :
if not ldb.has_object(o.binsha) :
ldb.store(gitdb.IStream(o.type, o.size, o.data_stream))
if o.path.find(os.sep) < 0 and o.type == 'tree': # a package root
repo.git.read_tree('--prefix', path, tree)
sec_paths.add(p)
return repo.index.commit(message, author=sec_commit.author
, committer=sec_commit.committer
, parent_commits=parents
, author_date=git_author_date(sec_commit)
, commit_date=git_commit_date(sec_commit))
Seperti yang Anda lihat, komit dari repositori sekunder ditambahkan secara massal. Pada awalnya, saya memastikan untuk menambahkan komit individu, tetapi (tiba-tiba!) Ternyata terkadang komit kunci yang lebih baru berisi versi sebelumnya dari repositori sekunder (dengan kata lain, versinya digulung kembali). Dalam situasi seperti itu, metode iter_commit meneruskan dan mengembalikan daftar kosong. Akibatnya, repositori salah. Oleh karena itu, saya hanya harus menggunakan versi saat ini.
Sejarah kemunculan generator all_but_last menarik. Saya menghilangkan deskripsinya, tapi itu persis seperti yang Anda harapkan. Awalnya hanya ada tantangan
repo.iter_commits('{}..{}^'.format(parent.trunk_commit, child.trunk_commit), reverse = True)... Namun, dengan cepat menjadi jelas bahwa notasi " x..y ^ " tidak berarti "semua komit dari x ke y , tidak termasuk x dan y sendiri " sama sekali, tetapi "semua komit dari x ke induk pertama y , termasuk induk ini". Dalam kebanyakan kasus, mereka adalah hal yang sama. Tetapi tidak jika Anda memiliki beberapa orang tua ...
Secara umum, semuanya berakhir dengan baik. Seluruh skrip masuk ke dalam 300 baris dan berjalan dalam waktu sekitar 6 jam. Moral: GitPython nyaman untuk melakukan segala macam hal keren dengan repositori, tetapi lebih baik menangani hutang teknis tepat waktu