Git

معمارية تطبيقات المصادر المفتوحة (المجلد الثاني)Git

معمارية تطبيقات المصادر المفتوحة (المجلد الثاني) Git

Susan Potter

إذا استمتعت بهذه الكتب، فقد تستمتع أيضًا بـ التصميم البرمجي بالأمثلة في Python والتصميم البرمجي بالأمثلة في JavaScript.

6.1. Git في لمحة

يتيح Git صيانةَ مجموعة عمل رقمية (غالبًا، لكن ليس حصرًا، الشيفرة البرمجية) من قِبل عددٍ كبير من المتعاونين عبر شبكة من المستودعات تعمل بنمط نظير-إلى-نظير (peer-to-peer). وهو يدعم سير العمل الموزّع، بما يسمح لمجموعة العمل بأن تتقارب في نهاية المطاف أو أن تتباعد مؤقتًا.

سيبيّن هذا الفصل كيف تعمل جوانب مختلفة من Git تحت الغطاء لتمكين ذلك، وكيف يختلف عن أنظمة التحكّم بالإصدارات (VCS) الأخرى.

6.2. نشأة Git

لفهم فلسفة تصميم Git على نحو أفضل، من المفيد أولًا فهم الظروف التي انطلق فيها مشروع Git داخل مجتمع نواة Linux.

كانت نواة Linux، قياسًا بمعظم مشاريع البرمجيات التجارية في ذلك الحين، استثنائيةً بسبب العدد الكبير من الملتزمين بإصدار التغييرات (committers) وبسبب التباين الكبير في مشاركة المساهمين ومعرفتهم بقاعدة الشيفرة القائمة. فقد صُيّنت النواة عبر ملفات tar والرقع (patches) لسنواتٍ طويلة، وكان مجتمع التطوير الأساسي يجد صعوبة في إيجاد نظام تحكّم بالإصدارات يحقّق معظم احتياجاتهم.

Git مشروع مفتوح المصدر وُلد من تلك الاحتياجات ومن تلك الإحباطات عام 2005. وفي ذلك الوقت كانت قاعدة شيفرة نواة Linux مُدارة عبر نظامَي تحكّم بالإصدارات، هما BitKeeper وCVS، على يد مطوّرين أساسيين مختلفين. وكان BitKeeper يقدّم تصوّرًا مختلفًا لنَسَب تاريخ أنظمة التحكّم بالإصدارات عمّا كان تقدّمه أنظمة التحكّم بالإصدارات المفتوحة المصدر الشائعة آنذاك.

وبعد أيام قليلة من إعلان BitMover، الشركة المصنّعة لـ BitKeeper، عن نيّتها سحب تراخيص بعض المطوّرين الأساسيين في نواة Linux، بدأ Linus Torvalds، في عجلة، بتطوير ما سيصير Git. وقد بدأ بكتابة مجموعة من السكربتات تساعده على إدارة رقع البريد الإلكتروني (email patches) وتطبيقها واحدًا تلو الآخر. وكان الهدف من هذه المجموعة الأولية من السكربتات هو تمكينه من إيقاف عمليات الدمج (merges) سريعًا، بحيث يتمكّن المشرف من تعديل قاعدة الشيفرة في منتصف تسلسل الرقع لينفّذ الدمج يدويًا، ثمّ يستأنف دمج الرقع التالية.

منذ البداية، كان لدى Torvalds هدف فلسفي واحد لـ Git—أن يكون ضد CVS—إضافةً إلى ثلاثة أهداف تصميمية تخصّ سهولة الاستخدام:

  • دعم سير عمل موزّع مشابه لما يتيحه BitKeeper
  • توفير ضمانات تقي من تلف المحتوى
  • توفير أداء عالٍ

وقد تحقّقت هذه الأهداف التصميمية وصُنت إلى حدٍّ ما، كما سأحاول أن أبيّن، عبر تشريح استخدام Git للرسوم البيانية الموجَّهة الخالية من الدورات (DAGs) في تخزين المحتوى، ومؤشّرات المراجع (reference pointers) للرؤوس، وتمثيل نموذج الكائنات (object model)، وبروتوكول الاتصال عن بُعد؛ وأخيرًا كيف يتتبّع Git عمليات دمج الأشجار.

ورغم تأثير BitKeeper في التصميم الأصلي لـ Git، فإنّ هذا التصميم مُنفَّذ بطرق مختلفة جوهريًا، وهو يتيح سير عمل أكثر توزيعًا، فضلًا عن سير عمل محلّي حصري (local-only)، وهي أمور لم تكن ممكنة مع BitKeeper. ومن المرجّح أنّ Monotone، وهو نظام تحكّم بالإصدارات موزّع مفتوح المصدر بدأ عام 2003، كان مصدر إلهام آخر في المراحل المبكرة من تطوير Git.

تقدّم أنظمة التحكّم بالإصدارات الموزّعة مرونةً كبيرة في سير العمل، وإن كان ذلك غالبًا على حساب البساطة. وتتشمل الفوائد المحدّدة للنموذج الموزّع ما يلي:

  • إتاحة القدرة للمتعاونين على العمل دون اتصال (offline) وعلى إصدار تغييراتهم على دفعات متتالية.
  • السماح للمتعاون بتحديد الوقت المناسب لمشاركة عمله.
  • تتيح للمتعاون الوصول إلى تاريخ المستودع أثناء العمل دون اتصال.
  • السماح بنشر العمل المُدار إلى عدة مستودعات، مع إمكان أن تظهر فيها فروع مختلفة أو درجات متفاوتة من تقصيل التغييرات.

في الفترة التي انطلق فيها مشروع Git تقريبًا، بُدئت ثلاثة مشاريع أخرى مفتوحة المصدر لأنظمة التحكّم بالإصدارات الموزّعة. (ومن بينها Mercurial، الذي يتناوله المجلد الأول من معمارية تطبيقات المصادر المفتوحة.) وتوفّر كلّ هذه الأدوات طرائق مختلفة قليلًا لتهيئة سير عمل شديد المرونة، وهو ما لم تكن أنظمة التحكّم بالإصدارات المركزية التي سبقتها قادرة على تعامله مباشرةً. لِّلعلم: تمتلك Subversion امتدادًا اسمه SVK يتولّى صيانته مطوّرون مختلفون لدعم المزامنة من خادم إلى خادم.

ومن المشاريع المفتوحة المصدر الشائعة التي تُصان بنشاط لأنظمة التحكّم بالإصدارات الموزّعة اليوم: Bazaar وDarcs وFossil وGit وMercurial وVeracity.

6.3. تصميم أنظمة التحكّم بالإصدارات

إنّ هذا وقت مناسب للتراجع خطوةً إلى الوراء والنظر في بدائل أنظمة التحكّم بالإصدارات المتاحة لـ Git. وفهم اختلافاتها سيمكّننا من استكشاف الخيارات المعمارية التي واجهها مطوّرو Git أثناء تطويره.

عادةً ما يملك نظام التحكّم بالإصدارات ثلاثة متطلّبات وظيفية جوهرية، هي:

  • تخزين المحتوى
  • تتبّع التغييرات في المحتوى (التاريخ بما فيه البيانات الوصفية الخاصة بعمليات الدمج)
  • توزيع المحتوى والتاريخ على المتعاونين

لِّلعلم: المتطلّب الثالث أعلاه ليس متطلّبًا وظيفيًا في جميع أنظمة التحكّم بالإصدارات.

تخزين المحتوى

أكثر خيارات التصميم شيوعًا لتخزين المحتوى في عالم أنظمة التحكّم بالإصدارات هو استعمال مجموعة تغييرات قائمة على الفروق (delta-based changeset)، أو تمثيل المحتوى برسم بياني موجَّه خالٍ من الدورات (DAG).

تغلّف مجموعات التغييرات القائمة على الفروق الفروقَ بين نسختين من المحتوى المُسطَّح، إضافةً إلى بعض البيانات الوصفية (metadata). أمّا تمثيل المحتوىكرسم بياني موجَّه خالٍ من الدورات فيتضمّن كائنات (objects) تشكّل تسلسلًا هرميًا يحاكي شجرة نظام الملفات الخاصة بالمحتوى بوصفها لقطةً للإصدار (commit)، مع إعادة استخدام الكائنات غير المتغيّرة داخل الشجرة كلّما أمكن. أمّا Git فيخزّن المحتوىكرسم بياني موجَّه خالٍ من الدورات مستخدمًا أنواعًا مختلفة من الكائنات. ويشرح قسم «قاعدة بيانات الكائنات» لاحقًا في هذا الفصل الأنواع المختلفة من الكائنات التي يمكن أن تشكّل رسومًا بيانية موجَّهة خالية من الدورات داخل مستودع Git.

تواريخ الإصدارات وعمليات الدمج

في جانب التاريخ وتتبّع التغييرات، تستخدم معظم برمجيات التحكّم بالإصدارات أحد النهجَين التاليَين:

  • تاريخ خطّي
  • رسم بياني موجَّه خالٍ من الدورات للتاريخ

ويستخدم Git أيضًا رسمًا بيانيًا موجَّهًا خاليًا من الدورات، غير أنّ ذلك هذه المرة لتخزين تاريخه. ويتضمّن كل إصدار بيانات وصفية عن أسلافه؛ ويمكن أن يكون لأحد الإصدارات في Git عددٌ من إصداراته الأبيّة (parent commits) يساوي صفرًا أو كثيرًا (نظريًّا بلا حدّ). فعلى سبيل المثال، يكون للإصدار الأول في مستودع Git عدد أباء يساوي صفرًا، في حين تكون لنتائج الدمج الثلاثي (three-way merge) ثلاثة أباء.

وهناك فرق أساسي آخر بين Git وبين Subversion وأسلافه ذوي التاريخ الخطّي، وهو قدرته على دعم التفريع (branching) مباشرةً، بحيث يسجّل معظم حالات تاريخ الدمج.

الشكل 6.1: مثال على تمثيل برسم بياني موجَّه خالٍ من الدورات في Git

يتيح Git قدرةً كاملة على التفريع باستخدام الرسوم البيانية الموجَّهة الخالية من الدورات لتخزين المحتوى. ويرتبط تاريخ الملف صعودًا عبر بنيته الدائرية (عبر عقدٍ تمثّل الأدلّة) حتى دليل الجذر، الذي يُربط بدوره بعقدة إصدار. ويمكن بدورها لعقدة الإصدار هذه أن يكون لها واحد أو أكثر من الآباء. وهذا يمنح Git خاصيتين تتيحان لنا استنتاج التاريخ والمحتوى على نحو أكثر تحديدًا مما تفعله عائلة أنظمة التحكّم بالإصدارات المشتقّة من RCS، وهي:

  • عندما تمتلك عقدة محتوى (أي ملف أو دليل) في الرسم البياني هوية المرجع نفسها (تجزئة SHA في Git) التي تمتلكها في إصدار آخر، فإنّ العقدتين مضمونَتان على احتوائهما المحتوى نفسه، مما يتيح لـ Git اختصار مقارنة المحتوى بكفاءة.
  • عند دمج فرعين فإنّنا ندمج محتوى عقدتين داخل رسم بياني موجَّه خالٍ من الدورات. ويتيح هذا الرسم لـ Git تحديد الأبناء المشتركين «بكفاءة» (مقارنةً بعائلة أنظمة التحكّم بالإصدارات RCS).

التوزيع

تعاملت حلول أنظمة التحكّم بالإصدارات مع توزيع محتوى نسخة العمل على المتعاونين في أحد ثلاثة أشكال:

  • محلّي فقط (Local-only): للحلول التي لا تتوفّر على المتطلّب الوظيفي الثالث أعلاه.
  • خادم مركزيّ (Central server): حيث يجب أن تمرّ جميع التغييرات على المستودع عبر مستودع واحد محدّد كي تُسجَّل في التاريخ أصلًا.
  • نموذج موزّع (Distributed model): حيث تتوفّر غالبًا مستودعات متاحة للعامة يمكن للمتعاونين أن «يدفعوا» إليها، مع إمكان إصدار التغييرات محلّيًا ثمّ دفعها لاحقًا إلى هذه العُقد العامة، ما يسمح بالعمل دون اتصال.

ولبيان فوائد وحدود كل خيار تصميم رئيسي، سننظر في مستودع Subversion ومستودع Git (على خادم) ذاتَي محتوى متكافئ (أي أنّ HEAD للفرع الافتراضي في مستودع Git يحمل المحتوى نفسه الذي تحمله أحدث مراجعة في trunk الخاص بمستودع Subversion). ولدى مطوّر اسمه Alex نسخة مُستخرَجة محلّيًا من مستودع Subversion ونسخة مُستنسَخة محلّيًا (clone) من مستودع Git.

ولنفترض أنّ Alex يُجري تغييرًا على ملف حجمه 1 ميغابايت في النسخة المحلّية المُستخرَجة من Subversion، ثمّ يُصدر هذا التغيير. محلّيًا، تتبع النسخة المُستخرَجة من الملف التغيير الأخير وتُحدَّث البيانات الوصفية المحلّية. وأثناء إصدار Alex للتغيير في مستودع Subversion المركزي، يُولَّد فرق (diff) بين لقطة الملفات السابقة والتغييرات الجديدة، ويُخزَّن هذا الفرق في المستودع.

قارن ذلك بطريقة عمل Git. عندما يُجري Alex التعديل نفسه على الملف المكافئ في النسخة المحلّية المستنسخة من Git، يُسجَّل التغيير محلّيًا أولًا، ثمّ يستطيع Alex أن «يدفع» (push) الإصدارات المعلّقة محلّيًا إلى مستودع عام كي تتشارك العمل مع بقية المتعاونين في المشروع. ويُخزَّن تغيير المحتوى بالطريقة نفسها في كل مستودع Git موجود فيه الإصدار. وعند الإصدار المحلّي (الحالة الأبسط)، ينشئ مستودع Git المحلّي كائنًا جديدًا يمثّل ملفًا للملف المتغيّر (مع كامل محتواه). ولكل دليل يقع أعلى الملف المتغيّر (إضافةً إلى دليل جذر المستودع)، يُنشأ كائن شجرة (tree) جديد بمُعرِّف جديد. ويُنشأ رسم بياني موجَّه خالٍ من الدورات انطلاقًا من كائن شجرة الجذر المُنشأ حديثًا، الذي يشير إلى الكائنات الثنائية (blobs) (مع إعادة استخدام مراجع الكائنات الثنائية الموجودة حيث لم يتغيّر محتوى الملفات في هذا الإصدار) ويشير إلى الكائن الثنائي المُنشأ حديثًا بدلًا من كائن الملف الثنائي السابق ضمن التسلسل الهرمي للأشجار السابق. (ويمثّل blob، أي الكائن الثنائي، ملفًا مخزَّنًا في المستودع.)

في هذه اللحظة يبقى الإصدار محلّيًا في النسخة المحلّية الحالية على جهاز Alex. وعندما «يدفع» Alex هذا الإصدار إلى مستودع Git متاح للعامة، يُرسَل الإصدار إلى ذلك المستودع. وبعد أن يتحقّق المستودع العام من إمكان تطبيق الإصدار على الفرع، تُخزَّن في المستودع العام الكائنات نفسها التي أُنشئت أصلًا في مستودع Git المحلّي.

هناك مزيدٌ كثير من القِطع المتحرّكة في سيناريو Git، سواء تحت الغطاء أو من منظور المستخدم، إذ يتطلّب ذلك منه التعبير صراحةً عن نيّته مشاركة التغييرات مع المستودع البعيد، بمعزل عن تتبّع التغيير بوصفه إصدارًا محلّيًا. غير أنّ مستويي التعقيد المضافين كلاهما يمنحان الفريق مرونةً أكبر فيما يخصّ سير العمل وقدراته على النشر، كما هو موضَّح في قسم «نشأة Git» أعلاه.

في سيناريو Subversion، لم يكن على المتعاون أن يتذكّر الدفع إلى المستودع البعيد العام حين يصبح جاهزًا ليرى الآخرون التغييرات التي أُجريت. وعندما يُرسَل تعديل صغير على ملف كبير إلى مستودع Subversion المركزي، يكون الفرق (delta) المخزَّن أكثر كفاءةً بكثير من تخزين كامل محتويات الملف لكل نسخة. غير أنّنا، كما سنرى لاحقًا، ثمّة حلّ وسط لهذا يستفيد منه Git في بعض السيناريوهات.

6.4. مجموعة الأدوات

يضمّ النظام البيئي لـ Git اليوم أدوات سطر أوامر وواجهات مستخدم كثيرة على عددٍ من أنظمة التشغيل (بما فيها Windows، التي كانت في الأصل مدعومة بالكاد). ومعظم هذه الأدوات مبنيّة إلى حدٍّ كبير فوق مجموعة أدوات Git الأساسية.

ولأنّ Git كُتب في الأصل بأسلوب Linus، وانطلق داخل مجتمع Linux، فقد كُتب وفق فلسفة تصميم أدوات (toolkit) تجسّد إلى حدٍّ بعيد تقاليد Unix في أدوات سطر الأوامر.

تنقسم مجموعة أدوات Git إلى جزأين: plumbing وporcelain. ويتكوّن plumbing من أوامر منخفضة المستوى تتيح تتبّع المحتوى الأساسي والتعامل مع الرسوم البيانية الموجَّهة الخالية من الدورات (DAG). أمّا porcelain فهو المجموعة الأصغر من أوامر git التي يُرجَّح أن يحتاج إليها معظم المستخدمين النهائيين لـ Git لصيانة المستودعات والتواصل بين المستودعات بغرض التعاون.

على أنّ تصميم مجموعة الأدوات وفّر أوامر كافية تتيح وصولًا دقيقّ المستوى إلى الوظائف لكثيرٍ من كاتبي السكربتات، فقد اشتكى مطوّرو التطبيقات من غياب مكتبة قابلة للربط بـ Git. وبما أنّ الملف التنفيذي لـ Git يستدعي die()، فإنّه غير قابل لإعادة الدخول (reentrant)، وستضطرّ واجهات المستخدم الرسومية أو واجهات الويب أو الخدمات طويلة التشغيل إلى الاستدعاء عبر fork/exec للملف التنفيذي الخاص بـ Git، وهو أمر قد يكون بطيئًا.

ويجري العمل على تحسين الوضع بالنسبة إلى مطوّري التطبيقات؛ ولمزيد من المعلومات، انظر قسم «الأعمال الحالية والمستقبلية».

6.5. المستودع والفهرس ومناطق العمل

لنُدهن أيدينا ونغوص في استخدام Git محلّيًا، وإن يكن ذلك لفهم بضعة مفاهيم أساسية فحسب.

أولًا، لإنشاء مستودع Git جديد مُهيَّأ على نظام ملفاتنا المحلّي (باستخدام نظام تشغيل مستوحى من Unix) نفعل:

  $ mkdir testgit
  $ cd testgit
  $ git init

الآن لدينا مستودع Git فارغ لكنّه مُهيَّأ في مجلّد testgit. يمكننا إنشاء الفروع (branch)، وإصدار التغييرات (commit)، ووضع الوسوم (tag)، بل والتواصل مع مستودعات Git أخرى محلّية وبعيدة. وحتى التواصل مع أنواع أخرى من مستودعات أنظمة التحكّم بالإصدارات ممكن باستخدام حفنة من أوامر git فحسب.

ينشئ الأمر git init مجلّدًا فرعيًا باسم .git داخل testgit. فلنُطّلع على محتوياته:

tree .git/
.git/
|-- HEAD
|-- config
|-- description
|-- hooks
|   |-- applypatch-msg.sample
|   |-- commit-msg.sample
|   |-- post-commit.sample
|   |-- post-receive.sample
|   |-- post-update.sample
|   |-- pre-applypatch.sample
|   |-- pre-commit.sample
|   |-- pre-rebase.sample
|   |-- prepare-commit-msg.sample
|   |-- update.sample
|-- info
|   |-- exclude
|-- objects
|   |-- info
|   |-- pack
|-- refs
    |-- heads
    |-- tags

مجلّد .git أعلاه هو، افتراضيًا، مجلّدًا فرعيًا ضمن مجلّد العمل الجذري، testgit. وهو يحتوي على عدة أنواع مختلفة من الملفات والأدلّة:

  • التهيئة (Configuration): تساعد ملفات .git/config و.git/description و.git/info/exclude عمليًا على تهيئة المستودع المحلّي.
  • الخطّافات (Hooks): يحتوي مجلّد .git/hooks على سكربتات يمكن تشغيلها عند أحداث معيّنة في دورة حياة المستودع.
  • منطقة التهيئة المرحلية (Staging Area): يوفّر الملف .git/index (الذي لم يظهر بعد في قائمة الشجرة أعلاه) منطقة تهيئة مرحلية لمجلّد العمل.
  • قاعدة بيانات الكائنات (Object Database): مجلّد .git/objects هو قاعدة بيانات كائنات Git الافتراضية، وهي تحتوي على كامل المحتوى أو على مؤشّرات إليه. وتصبح جميع الكائنات غير قابلة للتغيير بمجرّد إنشائها.
  • المراجع (References): مجلّد .git/refs هو الموقع الافتراضي لتخزين مؤشّرات المراجع لكلٍّ من الفروع والوسوم والرؤوس، المحلّية والبعيدة على السواء. والمرجع مؤشّر إلى كائن، غالبًا من نوع tag أو commit. وتُدار المراجع خارج قاعدة بيانات الكائنات كي تتغيّر وجهاتها مع تطوّر المستودع. وقد تشير بعض حالات المراجع الخاصة إلى مراجع أخرى، مثل HEAD.

مجلّد .git هو المستودع الفعلي. أمّا المجلّد الذي يحتوي مجموعة ملفات العمل فيسمّى مجلّد العمل (working directory)، وهو عادةً المجلّد الأب لمجلّد .git (أي المستودع (repository)). وإن كنت تُنشئ مستودع Git بعيدًا لا يملك مجلّد عمل، فيمكنك تهيئته باستخدام الأمر git init --bare. وسيؤدي ذلك إلى إنشاء ملفات المستودع المجرّدة في الجذر فحسب، بدلًا من إنشاء المستودع كمجلّد فرعي ضمن شجرة العمل.

ومن الملفات ذات الأهمية الكبيرة فهرس Git (Git index): .git/index. وهو يوفّر منطقة التهيئة المرحلية بين مجلّد العمل المحلّي والمستودع المحلّي. ويُستخدم الفهرس لتهيئة تغييرات محدّدة داخل ملف واحد (أو أكثر) على نحو مرحلي، لتُصدر جميعها معًا. وحتى لو أجريت تغييرات تتعلّق بأنواع مختلفة من الميزات، فيمكن إصدار التغييرات المتشابهة معًا، لتوصفها صورةً أكثر منطقية في رسالة الإصدار. ولتهيئة تغييرات محدّدة بشكل انتقائي في ملف أو مجموعة ملفات، يمكنك استخدام git add -p.

يُخزَّن فهرس Git، افتراضيًا، في ملف واحد داخل مجلّد المستودع. ويمكن تخصيص مسارات هذه المناطق الثلاث باستخدام متغيّرات البيئة.

ومن المفيد فهم التفاعلات التي تحدث بين هذه المناطق الثلاث (المستودع، والفهرس، ومناطق العمل) أثناء تنفيذ بعض أوامر Git الجوهرية:

  • git checkout [branch] ينقل مرجع HEAD في المستودع المحلّي إلى مسار مرجع الفرع (مثل refs/heads/master)، ويملأ الفهرس ببيانات ذلك الرأس، ويحدّث مجلّد العمل ليُمثّل الشجرة عند ذلك الرأس.
  • git add [files] يقابل بين مجاميع التحقّق (checksums) للملفات المحدّدة ومداخلها المقابلة في فهرس Git للتحقّق ممّا إذا كان فهرس الملفات المهيَّأة بحاجة إلى تحديث ليناسب نسخة مجلّد العمل. ولا يتغيّر شيء في مجلّد Git (أي المستودع).

لنستكشف ما يعنيه هذا على نحو أكثر ملموسًا عبر فحص محتوى الملفات الموجودة ضمن مجلّد .git (أي المستودع).

  $ GIT_DIR=$PWD/.git
  $ cat $GIT_DIR/HEAD

  ref: refs/heads/master

  $ MY_CURRENT_BRANCH=$(cat .git/HEAD | sed 's/ref: //g')
  $ cat $GIT_DIR/$MY_CURRENT_BRANCH

  cat: .git/refs/heads/master: No such file or directory

نحصل على خطأ لأنّه، قبل إجراء أي التزامات في مستودع Git على الإطلاق، لا توجد فروع سوى الفرع الافتراضي في Git وهو master، سواء كان موجودًا أم لا.

والآن إذا أجرينا التزامًا جديدًا، يُنشَأ الفرع master افتراضيًا لهذا الالتزام. لنفعل ذلك (مستمّرين في الصدفة نفسها، مع الاحتفاظ بالسياق والتاريخ):

  $ git commit -m "Initial empty commit" --allow-empty
  $ git branch

  * master

  $ cat $GIT_DIR/$MY_CURRENT_BRANCH

  3bce5b130b17b7ce2f98d17b2998e32b1bc29d68

  $ git cat-file -p $(cat $GIT_DIR/$MY_CURRENT_BRANCH)

وما نراه هنا في بدايته هو تمثيل المحتوى داخل قاعدة بيانات كائنات Git.

6.6. قاعدة بيانات الكائنات

الشكل 6.2: كائنات Git

يمتلك Git أربعة أنواع أساسية من الكائنات البدائية التي يُبنى حولها كل نوع من المحتوى في المستودع المحلّي. ولكل نوع من الكائنات السمات التالية: النوع والحجم والمحتوى. وأنواع الكائنات البدائية هي:

  • الشجرة (Tree): قد يكون عنصر في الشجرة شجرةً أخرى أو كائنًا ثنائيًا (blob) عند تمثيل دليل محتوى.
  • الكائن الثنائي (Blob): يمثّل الكائن الثنائي ملفًا مخزَّنًا في المستودع.
  • الالتزام (Commit): يشير الالتزام إلى شجرة تمثّل الدليل الأعلى لذلك الالتزام، فضلًا عن التزاماته الأبيّة والسمات القياسية.
  • الوسم (Tag): للوسم اسم، وهو يشير إلى التزام عند النقطة في تاريخ المستودع التي يمثّلها الوسم.

جميع الكائنات البدائية يُشار إليها بتجزئة SHA، أي هوية كائن من أربعين خانة، لها الخصائص التالية:

  • إذا كان كائنان متطابقان فسيكون لهما التجزئة نفسها.
  • وإذا كان كائنان مختلفان فستكون لهما تجزئتان مختلفتان.
  • إذا نُسخ كائن جزئيًا فقط أو حدث شكلٌ آخر من أشكال تلف البيانات، فإنّ إعادة حساب تجزئة SHA للكائن الحالي تكشف هذا التلف.

وخاصيتا SHA الأولتان، المتعلّقتان بهوية الكائنات، هما الأكثر نفعًا في تمكين النموذج الموزّع لـ Git (الهدف الثاني لـ Git). أمّا الخاصية الأخيرة فتتيح بعض ضمانات الحماية من التلف (الهدف الثالث لـ Git).

ورغم النتائج المرغوبة للتخزين القائم على الرسوم البيانية الموجَّهة الخالية من الدورات، سواءً لتخزين المحتوى أو لتواريخ الدمج، فإنّ التخزين بالفروق (delta) سيكون أكثر إحكامًا للمساحة من استخدام كائنات DAG منفصلة (loose) في كثير من المستودعات.

6.7. تقنيات التخزين والضغط

يعالج Git مشكلة مساحة التخزين بحزم الكائنات في صيغة مضغوطة، مستخدمًا ملف فهرس يشير إلى إزاحات (offsets) لتحديد مواضع كائنات بعينها في الملف المحزوم (packed) المقابل.

الشكل 6.3: مخطّط لملف حزمة مع ملف الفهرس المقابل

يمكننا عدّ الكائنات المنفصلة (أي غير المحزومة) في مستودع Git المحلّي باستخدام git count-objects. ويمكننا الآن أن نطلب من Git حزم الكائنات المنفصلة في قاعدة بيانات الكائنات، وحذف الكائنات المنفصلة المحزومة سلفًا، والبحث عن ملفات حزم مكرّرة باستخدام أوامر Git من نوع plumbing إن رغبنا في ذلك.

لقد تطوّرت صيغة ملف الحزمة (pack file) في Git، إذ خزّنت الصيغة الأولية مجاميع التحقّق CRC لملف الحزمة وملف الفهرس داخل ملف الفهرس نفسه. غير أنّ هذا ترك احتمالًا لتلف في البيانات المضغوطة لا يمكن اكتشافه، إذ إنّ مرحلة إعادة الحزم (repacking) لم تتضمّن أي فحوص إضافية. وتُغلب النسخة الثانية من صيغة ملف الحزمة هذه المشكلة بإدراج مجاميع تحقّق CRC لكل كائن مضغوط في ملف فهرس الحزمة. كما تسمح النسخة الثانية بملفات حزم تتجاوز 4 غيغابايت، وهو ما لم تدعمه الصيغة الأولية. وللكشف السريع عن تلف ملف الحزمة، يحتوي طرف ملف الحزمة على تجزئة SHA1 بطول 20 بايت تمثّل مجموع القائمة المرتّبة لجميع تجزئات SHA في ذلك الملف. ويصبّ تركيز صيغة ملف الحزمة الأحدث في تلبية هدف Git الثاني في سهولة الاستخدام، وهو الحماية من تلف البيانات.

أمّا الاتصال البعيد، فإنّ Git يحسب الإصدارات والمحتوى الذي يلزم إرساله عبر الشبكة لمزامنة المستودعات (أو فرع واحد فحسب)، ويولّد صيغة ملف الحزمة أثناء التشغيل (on the fly) ليُرسلها لاحقًا باستخدام البروتوكول الذي يطلبه العميل.

6.8. تواريخ الدمج

كما ذُكر سابقًا، يختلف Git جوهريًا في نهج تواريخ الدمج عن عائلة أنظمة التحكّم بالإصدارات المشتقّة من RCS. فمثلًا يمثّل Subversion تاريخ الملفات أو الأشجار في تسلسل خطّي؛ فأيّ ما يحمل رقم مراجعة أعلى يحلّ محلّ كلّ ما سبقه. والتفريع (branching) غير مدعوم بشكل مباشر، بل فقط عبر بنية أدلّة غير مفروضة الضوابط داخل المستودع.

الشكل 6.4: مخطّط يُظهر نَسَب تاريخ الدمج

لنستخدم أولًا مثالًا لنبيّن كيف يمكن أن يكون هذا إشكاليًا عند صيانة عدة فروع لعمل ما. ثمّ سننظر في سيناريو لنظهر حدوده.

عند العمل على «فرع» في Subversion عند الموقع المعتاد branches/branch-name، فإنّنا نعمل في شجرة أدلّة فرعية مجاورة لـ trunk (حيث تقيم عادةً الشيفرة الحيّة أو ما يقابلها من master). ولنفترض أنّ هذا الفرع يمثّل تطويرًا متوازيًا لشجرة trunk.

فمثلًا، قد نكون نعيد كتابة قاعدة شيفرة لاستخدام قاعدة بيانات مختلفة. وفي منتصف إعادة الكتابة نرغب في دمج تغييرات واردة (upstream) من شجرة فرع أخرى (ليست trunk). ندمج هذه التغييرات، يدويًا عند الضرورة، ثمّ نُكمل إعادة الكتابة. وفي وقت لاحق من اليوم نفسه ننهي تغييرات شيفرة الترحيل إلى مورّد قاعدة بيانات جديد على فرعنا branches/branch-name وندمج تغييراتنا في trunk. والمشكلة في طريقة تعامل أنظمة التحكّم بالإصدارات ذات التاريخ الخطّي مثل Subversion مع هذا الموقف هي عدم وجود أي وسيلة لمعرفة أنّ مجموعات التغييرات من الفرع الآخر صارت مشمولة الآن داخل trunk.

تعامل أنظمة التحكّم بالإصدارات ذات تواريخ الدمج القائمة على الرسوم البيانية الموجَّهة الخالية من الدورات، مثل Git، مع هذه الحالة معالجةً معقولة. وافتراضًا أنّ الفرع الآخر لا يحتوي التزامات لم تُدمج بعد في فرعنا الخاص بالترحيل إلى مورّد قاعدة البيانات (لنقل db-migration في مستودع Git الخاص بنا)، يمكننا أن نحدّد—من علاقات آباء كائن الالتزام—أنّ التزامًا على فرع db-migration كان يحتوي قمة (tip) الفرع الوارد الآخر، أي HEAD الخاص به. ولاحظ أنّ كائن الالتزام قد يملك عددًا من الآباء يساوي صفرًا أو أكثر (لا يحدّه إلا قدرة منفّذ الدمج). لذا فإنّ التزام الدمج على فرع db-migration يعرف أنّه دمج HEAD الحالي للفرع الحالي مع HEAD للفرع الوارد الآخر، وذلك عبر تجزئات SHA للآباء. وينطبق الأمر نفسه على التزام الدمج في master (وهو ما يقابل trunk في Git).

أمّا السؤال الصعب الإجابة عنه بشكل قاطع باستخدام تواريخ الدمج القائمة على DAG (أو القائمة على التسلسل الخطّي)، فهو: أيّ الالتزامات مشمولة داخل كل فرع؟ فمثلًا، افترضنا في السيناريو أعلاه أنّنا دمجنا في كل فرع جميع تغييرات الفرعين. وقد لا يكون الأمر كذلك.

وفي الحالات الأبسط، يملك Git القدرة على انتقاء التزامات (cherry-pick) من فروع أخرى إلى الفرع الحالي، شريطة أن يكون بالإمكان تطبيق الالتزام نظيفًا على الفرع.

6.9. ماذا بعد؟

كما ذُكر سابقًا، فإنّ Git الأساسي كما نعرفه اليوم يستند إلى فلسفة تصميم أدوات (toolkit) مصدرها عالم Unix، وهي مفيدة جدًا لكتابة السكربتات لكنها أقلّ نفعًا للدمج داخل التطبيقات أو الخدمات طويلة التشغيل أو الربط بها. ورغم أنّ بيئات التطوير المتكاملة (IDEs) الشائعة اليوم توفّر دعمًا لـ Git، فإنّ إضافة هذا الدعم وصيانته كانا أصعب من دمج دعم أنظمة التحكّم بالإصدارات التي توفّر مكتبة سهلة الربط والمشاركة عبر منصّات متعدّدة.

ولمقاومة ذلك، قاد Shawn Pearce (من مكتب برامج المصادر المفتوحة في Google) جهدًا لإنشاء مكتبة Git قابلة للربط بترخيص أكثر تساهلًا لا يعيق استخدام المكتبة. وقد سُمّيت libgit2. ولم تحظَ بترسّخ كبير إلى أن اختارها طالب اسمه Vincent Marti لمشروعه في Google Summer of Code العام الماضي. ومنذ ذلك الحين ظلّ Vincent ومهندسو Github يشاركون في مشروع libgit2، وأنشأوا روابط ربط (bindings) للعدّة من اللغات الشائعة الأخرى مثل Ruby وPython وPHP ولغات ‎.NET وLua وObjective-C.

كما بدأ Shawn Pearce مكتبة Java خالصة بترخيص BSD اسمها JGit تدعم عمليات شائعة كثيرة على مستودعات Git. وهي تُصان اليوم من قِبل مؤسسة Eclipse لاستخدامها في تكامل Git مع بيئة التطوير Eclipse (Eclipse IDE).

ومن المبادرات الأخرى المثيرة للاهتمام والتجريبية في المصادر المفتوحة خارج مشروع Git الأساسي عددٌ من التنفيذات التي تستخدم مخازن بيانات بديلة كواجهات خلفية لقاعدة بيانات كائنات Git، مثل:

  • jgit_cassandra، الذي يوفّر ثبات كائنات Git باستخدام Apache Cassandra، وهو مخزن بيانات هجين يستخدم توزيعًا على نمط Dynamo مع دلالات نموذج بيانات عمود العائلة الخاص بـ BigTable.
  • jgit_hbase، الذي يتيح عمليتي القراءة والكتابة على كائنات Git المخزَّنة في HBase، وهو مخزن بيانات مفاتيح-قيم موزّع.
  • libgit2-backends، الذي نشأ من جهد libgit2 لإنشاء واجهات خلفية لقاعدة بيانات كائنات Git لعدّة مخازن بيانات شائعة مثل Memcached وRedis وSQLite وMySQL.

جميع مشاريع المصادر المفتوحة هذه تُصان بمعزل عن مشروع Git الأساسي.

وكما ترى، هناك اليوم عدد كبير من الطرق لاستخدام صيغة Git. لم يعد وجه Git واجهة سطر الأوامر الخاصة بمجموعة أدوات مشروع Git Core فحسب، بل هو صيغة المستودع والبروتوكول المستخدم للمشاركة بين المستودعات.

وحتى وقت كتابة هذا الفصل، لم يصل معظم هذه المشاريع، بحسب مطوّريها، إلى إصدار مستقرّ، لذا ما زال يتعيّن العمل في هذا المجال، لكن مستقبل Git يبدو واعدًا.

6.10. الدروس المستفادة

في البرمجيات، كلّ قرار تصميم هو في النهاية مقايضة (trade-off). وبصفتي مستخدمًا متقدّمًا لـ Git في التحكّم بالإصدارات، وبصفتي شخصًا طوّر برمجيات حول نموذج قاعدة بيانات كائنات Git، فإنّ لي مودّةً عميقةً لـ Git في صورته الحالية. لذا فإنّ هذه الدروس المستفادة تمثّل بالأحرى تأمّلًا في الشكاوى المتكرّرة والشائعة حول Git، والتي تعود إلى قرارات التصميم وإلى تركيز مطوّري Git الأساسيين.

ومن أكثر الشكاوى شيوعًا بين المطوّرين والمديرين الذين يقيّمون Git، افتقاره إلى تكامل مع بيئات التطوير (IDEs) يضاهي ما توفّره أدوات التحكّم بالإصدارات الأخرى. وقد جعل تصميم مجموعة أدوات Git هذا الأمر أصعب من دمج أدوات التحكّم بالإصدارات الحديثة الأخرى داخل بيئات التطوير والأدوات المرتبطة بها.

وفي المراحل المبكرة من تاريخ Git، نُفِّذ بعض الأوامر في صورة سكربتات صدفة (shell scripts). وقد جعلت هذه السكربتات Git أقلّ قابلية للنقل، ولا سيما إلى Windows. وأنا واثق أنّ مطوّري Git الأساسيين لم يسهروا بسبب هذه المسألة، غير أنّها أثّرت سلبًا في تبنّي Git داخل المؤسسات الأكبر، بسبب مشاكل قابلية النقل التي كانت سائدة في الأيام الأولى من تطوير Git. أمّا اليوم فقد بدأ متطوّعون مشروعًا اسمه Git for Windows لضمان نقل الإصدارات الجديدة من Git إلى Windows في الوقت المناسب.

ومن النتائج غير المباشرة لتصميم Git حول تصميم مجموعة أدوات يحوي عددًا كبيرًا من أوامر plumbing أنّ المستخدمين الجدد يضيعون سريعًا؛ إذ تبدأ الحيرة عند كثرة الأوامر الفرعية المتاحة، وتمتدّ إلى العجز عن فهم رسائل الخطأ بسبب فشل مهمة plumbing منخفضة المستوى، وهناك مواضع كثيرة قد يضلّ فيها المستخدم الجديد طريقه. وقد جعل ذلك تبنّي Git أصعب بالنسبة إلى بعض فرق التطوير.

ورغم هذه الشكاوى حول Git، فإنّني متحمّس للإمكانات التي يوفّرها التطوير المستقبلي لمشروع Git Core، فضلًا عن جميع مشاريع المصادر المفتوحة المرتبطة به التي انطلقت منه.