مهمة
معطى: مشروع يعتمد على OpenWRT (وهو قائم على BuildRoot) مع مستودع إضافي واحد متصل كموجز. المهمة: دمج مستودع إضافي مع المستودع الرئيسي.
خلفية
نصنع أجهزة التوجيه ، وفي يوم من الأيام أردنا أن نمنح العملاء القدرة على تضمين تطبيقاتهم في البرامج الثابتة. لكي لا نعاني من تخصيص SDK و toolchain والصعوبات المصاحبة لذلك ، قررنا وضع المشروع بأكمله على github في مستودع خاص. هيكل المستودع:
/target //
/toolchain // gcc, musl
/feeds //
/package //
...
تقرر نقل بعض تطبيقات التطوير الخاص بنا من المستودع الرئيسي إلى المستودع الإضافي ، بحيث لا يتجسس أحد. لقد فعلنا كل شيء ، ووضعناه على جيثب وأصبح جيدًا.
تدفق الكثير من المياه تحت الجسر منذ ذلك الوقت ...
لقد ذهب العميل لفترة طويلة ، وتمت إزالة المستودع من github ، وفكرة منح العملاء حق الوصول إلى المستودع هي فكرة فاسدة. ومع ذلك ، بقي مستودعين في المشروع. وجميع البرامج النصية / التطبيقات ، بطريقة أو بأخرى تتعلق بـ git ، مضطرة إلى التعقيد للعمل مع مثل هذه البنية. ببساطة ، إنه دين تقني. على سبيل المثال ، لضمان إمكانية إعادة إنتاج الإصدارات ، تحتاج إلى الالتزام بالمستودع الأساسي ، الملف الثانوي ، الإصدار ، مع التجزئة من المستودع الثاني. بالطبع ، السيناريو يفعل ذلك ، وليس صعبًا جدًا عليه. لكن ، هناك العشرات من هذه النصوص ، وكلها أكثر تعقيدًا مما يمكن أن تكون. بشكل عام ، اتخذت قرارًا إراديًا بدمج المستودع الثانوي مرة أخرى في المستوى الأساسي. في الوقت نفسه ، تم تعيين الشرط الرئيسي - للحفاظ على استنساخ الإصدارات.
بمجرد تعيين مثل هذا الشرط ، لن تنجح طرق الدمج التافهة ، مثل تنفيذ كل شيء بدءًا من المرحلة الثانوية بشكل منفصل ، ومن ثم ، من الأعلى ، إنشاء التزام دمج لشجرتين مستقلتين. عليك أن تفتح الغطاء وتتسخ يديك.
هيكل البيانات Git
أولاً ، ما هو شكل مستودع git؟ هذه قاعدة بيانات للأشياء. الكائنات من ثلاثة أنواع: النقط والأشجار والالتزامات. تتم معالجة جميع الكائنات بواسطة تجزئة sha1 لمحتواها. النقطة هي ، بغباء ، بيانات بدون أي سمات إضافية. الشجرة عبارة عن قائمة مرتبة من الروابط إلى الأشجار والنقاط ذات الشكل "<right> <type> <hash> <name>” (حيث يكون <type> إما blob أو tree). وبالتالي ، فإن الشجرة تشبه دليلًا في نظام الملفات ، بينما تشبه blob الملف. يحتوي الالتزام على اسم المؤلف والمتعهد وتاريخ الإنشاء والإضافة وتعليق وتجزئة الشجرة ورقم تعسفي (عادة واحد أو اثنين) من الروابط إلى الوالدين. هذه الروابط إلى الالتزامات الأصلية تحول قاعدة الكائن إلى رسم بياني لا دوري (بين الأجانب ، المعروف باسم DAG).اقرأ بالتفصيلهنا :
وهكذا ، تم تحويل مهمتنا إلى مهمة بناء ديغراف جديد ، وتكرار هيكل القديم. ولكن مع استبدال
ارتباطات ملف الإصدار الثانوي بطلبات من المستودع الإضافي ، فإن عملية التطوير بعيدة كل البعد عن gitflow الكلاسيكي. نلزم كل شيء بالسيد ، ونحاول عدم كسره في نفس الوقت. نحن نبني من هناك. إذا لزم الأمر ، فإننا نصنع فروعًا ثابتة ، ثم ندمجها مرة أخرى في السيد. وفقًا لذلك ، يبدو الرسم البياني للمستودع وكأنه جذع مكشوف من السكوية مضفر بالكروم.
تحليل
تنقسم المهمة بطبيعة الحال إلى مرحلتين: التحليل والتوليف. نظرًا لأنه من الواضح أنه من الضروري التشغيل من أجل التوليف من لحظة تخصيص المستودع الثانوي لجميع العلامات والفروع ، وإدراج الالتزامات من المستودع الثاني ، ثم في مرحلة التحليل ، تحتاج إلى العثور على أماكن لإدراج الالتزامات الثانوية وهذه الالتزامات نفسها. لذلك ، تحتاج إلى إنشاء رسم بياني مختزل ، حيث ستكون العقد بمثابة التزامات الرسم البياني الرئيسي الذي يغير ملف الإصدار الثانوي (التزامات المفتاح). علاوة على ذلك ، إذا كانت عُقد هذه البوابة تشير إلى الوالدين ، ففي الرسم البياني الجديد ، يلزم الإشارة إلى الأحفاد. أقوم بإنشاء مجموعة مسماة:
node = namedtuple(‘Node’, [‘primary_commit’, ‘secondary_commit’, ‘children’])
الحجز اللازم
, . , .
أضعها في القاموس:
master_tip = repo.commit(‘master’)
commit_map = {master_tip : node(master_tip, get_sec_commit(master_tip), [])}
أضع كل الالتزامات التي تغير الإصدار الثانوي هناك:
for c in repo.iter_commits(all=True, path=’secondary.verion’) :
commit_map[c] = node(c, get_sec_commit(c), [])
وأقوم ببناء خوارزمية عودية بسيطة:
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)
هذا هو ، كما كان ، أقوم بتمديد العقد الرئيسية في الماضي وربطها بآباء جدد.
أقوم بتشغيله و ... تجاوز أقصى عمق للخطأ في وقت التشغيل
كيف حدث ذلك؟ هل هناك الكثير من الالتزامات؟ بوابة الدخول و مرحاض أعرف الجواب. إجمالي الالتزامات منذ الانقسام هو حوالي 20000 ، وتلك التي تؤثر على الإصدار الثانوي - ما يقرب من 700. الوصفة معروفة ، هناك حاجة إلى إصدار غير متكرر.
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)
(وقلتم إن كل هذه الخوارزميات مطلوبة فقط حتى تمر المقابلة!)
أطلقتها ، و ... إنها تعمل. دقيقة ، خمسة ، عشرين ... لا ، لا يمكنك أن تأخذ كل هذا الوقت. أتوقف. على ما يبدو ، تتم معالجة كل التزام وكل مسار عدة مرات. كم عدد الفروع الموجودة في الشجرة؟ اتضح أن هناك 40 فرعًا في الشجرة ، وبالتاليمسارات مختلفة فقط من السيد. وهناك العديد من المسارات التي تؤدي إلى جزء كبير من الالتزامات الرئيسية. نظرًا لعدم وجود آلاف السنين في المتجر ، فأنا بحاجة إلى تغيير الخوارزمية بحيث تتم معالجة كل التزام مرة واحدة بالضبط. للقيام بذلك ، أقوم بإضافة مجموعة ، حيث أقوم بتمييز كل التزام تمت معالجته. ولكن هناك مشكلة صغيرة: باستخدام هذا النهج ، ستفقد بعض الروابط ، نظرًا لأن المسارات المختلفة ذات الالتزامات الرئيسية المختلفة يمكن أن تمر عبر نفس الالتزامات ، ولن يذهب إلى أبعد من ذلك إلا الأول. للتغلب على هذه المشكلة ، أقوم باستبدال المجموعة بقاموس ، حيث تكون المفاتيح هي الأوامر ، والقيم عبارة عن قوائم من ارتباطات المفاتيح التي يمكن الوصول إليها:
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)
نتيجة لهذا التبادل غير الفني للذاكرة لبعض الوقت ، تم إنشاء الرسم البياني في 30 ثانية.
نتيجة الجمع بين الطريحة والنقيضة
لدي الآن خريطة الالتزام بالعقد الرئيسية المرتبطة بالرسم البياني عبر الروابط الفرعية. للراحة ، أقوم بتحويلها إلى سلسلة من الأزواج (سلف ، سليل) . يجب ضمان التسلسل بأن جميع الأزواج التي تحدث فيها العقدة كطفل تقع قبل أي زوج حيث تحدث العقدة كأصل. ثم تحتاج فقط إلى الاطلاع على هذه القائمة والالتزام بالالتزامات الأولى من المستودع الرئيسي ، ثم من المستودع الإضافي. هنا يجب أن نتذكر أن الالتزام يحتوي على ارتباط إلى الشجرة ، وهي حالة نظام الملفات. نظرًا لأن المستودع الإضافي يحتوي على أدلة فرعية إضافية في الحزمة / الدليل، ثم سيكون من الضروري إنشاء أشجار جديدة لجميع الالتزامات. في الإصدار الأول ، قمت فقط بكتابة blobs إلى الملفات وطلبت من git إنشاء فهرس في دليل العمل. ومع ذلك ، لم تكن هذه الطريقة مثمرة للغاية. لا يزال هناك 20000 التزام ، وكل واحد يحتاج إلى الالتزام مرة أخرى. لذا فإن الأداء مهم للغاية. قادني القليل من البحث في الأجزاء الداخلية لـ GitPython إلى فئة gitdb.LooseObjectDB ، والتي تعرض كائنات مستودع git مباشرة. باستخدامه ، يمكن كتابة النقط والأشجار (وأي كائنات أخرى أيضًا) من أحد المستودعات مباشرةً إلى مستودع آخر. من الخصائص الرائعة لقاعدة بيانات كائن git أن عنوان أي كائن هو تجزئة لبياناته. لذلك ، فإن نفس النقطة سيكون لها نفس العنوان ، حتى في مستودعات مختلفة.
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)
وظائف الالتزام نفسها:
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))
كما ترى ، تتم إضافة الالتزامات من المستودع الثانوي بشكل مجمّع. في البداية ، تأكدت من إضافة الالتزامات الفردية ، ولكن (فجأة!) اتضح أنه في بعض الأحيان يحتوي التزام مفتاح جديد على إصدار سابق من المستودع الثانوي (بمعنى آخر ، يتم التراجع عن الإصدار). في مثل هذه الحالة ، يمر التابع iter_commit ويعيد قائمة فارغة. نتيجة لذلك ، المستودع غير صحيح. لذلك ، كان علي فقط الالتزام بالإصدار الحالي.
إن تاريخ ظهور منشئ all_but_last مثير للاهتمام. لقد حذفت الوصف ، لكنه يفعل بالضبط ما تتوقعه. في البداية كان هناك تحدٍ فقط
repo.iter_commits('{}..{}^'.format(parent.trunk_commit, child.trunk_commit), reverse = True)... ومع ذلك ، سرعان ما أصبح واضحًا أن الترميز " x..y ^ " لا يعني "جميع الالتزامات من x إلى y ، باستثناء x و y نفسيهما " على الإطلاق ، ولكن "جميع الالتزامات من x إلى الأب الأول لـ y ، بما في ذلك هذا الأصل". في معظم الحالات ، هم نفس الشيء. لكن ليس عندما يكون لديك العديد من الآباء ...
بشكل عام ، انتهى كل شيء بشكل جيد. يتلاءم النص بأكمله مع 300 سطر ويستغرق حوالي 6 ساعات. أخلاقي: GitPython مناسب للقيام بكل أنواع الأشياء الرائعة مع المستودعات ، لكن من الأفضل معالجة الدين الفني في الوقت المناسب