الباب الثالث: التفريع في جت

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

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

الفروع بإيجاز

لنفهم حقًا طريقة التفريع في جت، علينا أن نتراجع خطوة إلى الخلف ونتدبر طريقته في تخزين البيانات.

كما قد تتذكر من ما هو جت؟، لا يخزن جت بياناته في صورة فروقات، بل في صورة لقطات.

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

حتى نستطيع تصور هذا، لنفترض أن لديك مجلدًا به ثلاثة ملفات، وأنك أهّلتها جميعها ثم أودعتها. يحسب تأهيل الملفات بصمة كل ملف (بصمة SHA-1 التي ذكرناها في ما هو جت؟)، ويخزن نسخة الملف هذه في المستودع (وهي ما يسميها جت «ك‍تلة رقمية» (‪“blob”‬)، ونسميها «ك‍تلة» اختصارا)، ويضيف تلك البصمة إلى منطقة التأهيل:

$ git add README test.rb LICENSE
$ git commit -m 'Initial commit'

عندما تودع بأمر git commit، فإن جت يحسب أيضا بصمات كل مجلد ومجلد فرعي (في هذه الحالة، مجلد جذر المشروع فقط)، ويخزنها في صورة كائنات أشجار في المستودع. ثم ينشئ جت كائنَ إيداعٍ فيه بيانات وصفية وإشارة إلى شجرة جذر المشروع، حتى يستطيع إعادة إنشاء تلك اللقطة عند الحاجة.

صار في مستودعك الآن خمسة كائنات: ثلاث ك‍تل (كلٌ منها يمثل محتويات ملف من الثلاثة)، وشجرة واحدة (تسرد محتويات المجلد وما الك‍تل التي تشير إليها أسماء الملفات)، وإيداع واحد (فيه إشارة إلى شجرة الجذر تلك وكذلك البيانات الوصفية للإيداع).

إيداعٌ وشجرته
شكل ٩. إيداعٌ وشجرته

إذا أجريت تعديلات وأودعتها، فإن إيداعك التالي سيخزن إشارةً إلى الإيداع السابق له مباشرةً.

إيداعات وآباؤها
شكل ١٠. إيداعات وآباؤها

فإنما الفرع في جت هو إشارة متحركة تشير إلى أحد هذه الإيداعات. والفرع المبدئي في جت يُسمى master. فعندما تشرع في صنع الإيداعات، فإن جت يعطيك فرعا رئيسا يسمى master ويشير إلى آخر إيداع صنعته. ويتقدم فرع master تلقائيًا مع كل إيداعٍ تودِعه.

فرع master في جت ليس مميزًا. فهو تمامًا مثل أي فرع آخر. والسبب الوحيد لوجوده في أغلب المستودعات أن أمر git init ينشئه بهذا الاسم المبدئي وأكثر الناس لا يبالون بتغييره.

فرع وتاريخ إيداعاته
شكل ١١. فرع وتاريخ إيداعاته

إنشاء فرع جديد

ماذا يحدث عندما تنشئ فرعًا جديدًا؟ الإجابة: ينشئ جت إشارة جديدة لك لتحركها كما تشاء. لنقُل إنك أردت إنشاء فرع جديد اسمه testing. تفعل هذا بأمر التفريع، git branch:

$ git branch testing

ينشئ هذا إشارةً إلى الإيداع الذي تقف عنده الآن.

فرعان يشيران إلى سلسلة الإيداعات نفسها
شكل ١٢. فرعان يشيران إلى سلسلة الإيداعات نفسها

كيف يعرف جت في أيّ فرع أنت الآن؟ إنه يحتفظ بإشارة مخصوصة تسمى «إشارة الرأس» (HEAD). لاحظ أن هذه مختلفة كثيرًا عن مفهوم HEAD في الأنظمة الأخرى مثل Subversion و CVS. في جت، هذه إشارة إلى الفرع المحلي الذي تقف فيه الآن. في حالتنا هذه، ما زلتَ واقفًا في فرع master. فما على أمر git branch إلا إنشاء فرع جديد؛ ليس عليه الانتقال إليه.

إشارة الرأس `HEAD` تشير إلى فرع
شكل ١٣. إشارة الرأس HEAD تشير إلى فرع

يمكنك رؤية هذا بسهولة بأمر السجل، والذي يُظهر لك ما تشير إليه إشارات الفروع، وذلك بالخيار --decorate.

$ git log --oneline --decorate
f30ab (HEAD -> master, testing) Add feature #32 - ability to add new formats to the central interface
34ac2 Fix bug #1328 - stack overflow under certain conditions
98ca9 Initial commit

يمكنك رؤية فرعَي master و testing عند إيداع f30ab.

الانتقال بين الفروع

للانتقال إلى فرع موجود، استخدم أمر السحب git checkout. هيا بنا نتنقل إلى فرعنا الجديد testing:

$ git checkout testing

يحرك هذا الأمر إشارة الرأس لتشير إلى فرع testing.

إشارة الرأس تشير إلى الفرع الحالي
شكل ١٤. إشارة الرأس تشير إلى الفرع الحالي

ما دلالة هذا؟ لنصنع إيداعًا آخر إذًا.

$ vim test.rb
$ git commit -a -m 'Make a change'
فرع الرأس يتقدم عند صنع إيداع
شكل ١٥. فرع الرأس يتقدم عند صنع إيداع

هذا يدعو للتفكر، لأن الآن فرع testing قد تقدم، بينما قعد فرع master في مكانه مشيرًا إلى الإيداع القديم نفسه عندما انتقلنا إلى الفرع الجديد بأمر السحب. لنعد إلى فرع master:

$ git checkout master
لا يُظهر أمر السجل جميع الفروع طوال الوقت

إذا نفذت git log الآن، فستتساءل أين ذهب فرع testing الذي أنشأته، لأنه لن يظهر في ناتجه.

لم يتبخر الفرع، ولكن جت لا يعلم أنك مهتمٌ به الآن، ولا يُظهر لك جت إلا ما يظن أنك مهتم به. بلفظ آخر، لا يُظهر لك أمر السجل بطبيعته إلا تاريخ الفرع الذي تقف فيه حاليًا.

لإظهار تاريخ فرع آخر، عليك طلب ذلك صراحةً، مثل git log testing. ولإظهار جميع الفروع، اطلب ذلك من git log بالخيار --all.

تتحرك إشارة الرأس عندما تنتقل إلى فرع آخر
شكل ١٦. تتحرك إشارة الرأس عندما تنتقل إلى فرع آخر

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

الانتقال بين الفروع يغيّر الملفات التي في مجلد عملك

مهمٌ ملاحظة أنك عندما تنتقل إلى فرع آخر في جت، فإن الملفات التي في مجلد عملك ستتغير. فإذا انتقلت إلى فرع قديم، سيعود مجلد عملك إلى ما كان عليه عند آخر إيداع في هذا الفرع. وإن لم يستطع جت تغيير الملفات تغييرا نظيفا، فلن يسمح لك بالتبديل أصلا.

لنُجري بعض التعديلات ونودع مجددًا:

$ vim test.rb
$ git commit -a -m 'Make other changes'

الآن افترق تاريخ مشروعك (انظر تاريخ مفترِق). فلقد أنشأتَ فرعًا وانتقلت إليه وعملت فيه قليلا، ثم عدت إلى الفرع الرئيس وعملت فيه عملا آخر. كلا هذين التغييرين منعزلان في فرعين منفصلين: يمكنك التنقل بينهما ذهابًا وإيابًا ثم دمجهما معًا عندما تكون مستعدًا. وكل هذا فعلتَه بسَهولة بأوامر التفريع branch والسحب checkout والإيداع commit.

تاريخ مفترِق
شكل ١٧. تاريخ مفترِق

يمكنك أيضا رؤية هذا بسهولة بأمر السجل، فإذا نفّذت git log --oneline --decorate --graph --all فسيُظهر لك تاريخ إيداعاتك ومواضع إشارات فروعك وكيف افترق تاريخك.

$ git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) Make other changes
| * 87ab2 (testing) Make a change
|/
* f30ab Add feature #32 - ability to add new formats to the central interface
* 34ac2 Fix bug #1328 - stack overflow under certain conditions
* 98ca9 Initial commit of my project

ولأن الفرع في جت ليس إلا ملفًا هيّنًا فيه ٤٠ حرفًا تمثّل بصمة الإيداع الذي يشير إليه الفرع، فإن إنشاء الفروع وإزالتها عمليتان رخيصتان سريعتان. فعملية إنشاء فرع جديد تماثل في سرعتها ويسرها عملية ك‍تابة ٤١ بايتًا إلى ملف (وهم ٤٠ محرفًا للبصمة ثم محرف نهاية السطر).

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

لنرَ لماذا عليك فعل هذا.

إنشاء فرع جديد والانتقال إليه في خطوة واحدة

من المعتاد أن ترغب في الانتقال إلى فرع جديد فور إنشائه — يمكنك إنشاء فرع والانتقال إليه بأمر واحد: git checkout -b <اسم‌الفرع‌الجديد>.

ابتداءً من النسخة 2.23 من جت يمكنك استعمال أمر التبديل بدلًا من أمر السحب من أجل:

  • الانتقال إلى فرع موجود: git switch testing-branch.

  • إنشاء فرع جديد والانتقال إليه: git switch -c new-branch. الخيار -c للإنشاء، ويمكنك استخدام الخيار الكامل: --create.

  • العودة إلى الفرع المسحوب سابقًا: git switch -.

أسس التفريع والدمج

لننظر مثالا سهلا عن التفريع والدمج بأسلوب سير عمل قد تستعمله في الحقيقة. ستتبع هذه الخطوات:

  1. تقوم ببعض الأعمال على موقع وب.

  2. تنشئ فرعا لـ«قصة المستخدم» الجديدة التي تعمل عليها.

  3. تقوم ببعض الأعمال في هذا الفرع.

وبينما أنت هنا، تأتيك مكالمة بأن علة أخرى خطيرة تحتاج منك إصلاحا عاجلا. فستفعل الآتي:

  1. تنتقل إلى فرعك الإنتاجي ("production").

  2. تنشئ فرعا لإضافة الإصلاح العاجل.

  3. وبعد اختباره، تدمج فرع الإصلاح العاجل، وتدفعه إلى فرع الإنتاج.

  4. تعود إلى قصة المستخدم الأصلية وتكمل عملك عليها.

أسس التفريع

أولا، لنقُل أنك تعمل على مشروعك، ولديك بضعة إيداعات بالفعل في فرع master.

تاريخ إيداعات بسيط
شكل ١٨. تاريخ إيداعات بسيط

ثم قررت أنك ستعمل على المسألة رقم ٥٣ في نظام متابعة المسائل الذي تستخدمه شركتك. فتنفّذ أمر git checkout مع الخيار -b، لإنشاء فرعٍ جديدٍ والانتقال إليه في الوقت نفسه:

$ git checkout -b iss53
Switched to a new branch "iss53"

وهذا اختصار للأمرين:

$ git branch iss53
$ git checkout iss53
إنشاء إشارة إلى فرع جديد
شكل ١٩. إنشاء إشارة إلى فرع جديد

يمكنك العمل على موقعك وصنع بعض الإيداعات. فعل هذا يحرك فرع iss53 إلى الأمام، لأنه الفرع المسحوب (أي أنه الفرع الذي تشير إليه إشارة الرأس HEAD لديك):

$ vim index.html
$ git commit -a -m 'Create new footer [issue 53]'
تقدَّم فرع `iss53` بعملك عليه
شكل ٢٠. تقدَّم فرع iss53 بعملك عليه

ثم تأتيك مكالمة الآن بأن الموقع به علة، وعليك حلها فورا. لا تحتاج مع جت أن تنشر إصلاحك لهذه العلة مع تعديلات iss53 التي صنعتها، ولا تحتاج أيضا إلى بذل المجهود للتراجع عن هذه التعديلات حتى تستطيع العمل على إصلاح علة الموقع. ليس عليك إلا أن تنتقل عائدا إلى فرعك الرئيس master.

ولكن، قبل هذا، عليك ملاحظة أن إن كان مجلد عملك أو منطقة تأهيلك فيهما تعديلات غير مودعة وتختلف عما في الفرع الذي تريد الانتقال إليه، فلن يسمح لك جت بالانتقال. من الأفضل دوما أن تجعل حالة العمل نظيفة قبل الانتقال بين الفروع. يمكن التحايل على هذا بطريقة أو بأخرى (تحديدا، التخبئة وتصحيح الإيداعات) والتي سنتطرق إليها فيما بعد في Stashing and Cleaning. ولكن لنفترض الآن أنك أودعت كل تعديلاتك، حتى يتسنى لك الانتقال عائدا إلى فرعك الرئيس:

$ git checkout master
Switched to branch 'master'

ستجد الآن أن مجلد عملك مطابق تماما لما كان عليه قبل أن تبدأ العمل على المسألة رقم ٥٣، ويمكنك الآن التركيز على إصلاح العلة الجديدة. هذه النقطة مهمة ويجب تذكرها: عندما تنتقل من فرع إلى آخر، يعيد جت مجلد عملك إلى ما كان عليه آخر مرة أودعت فيها في هذا الفرع، فيضيف ويحذف ويعدّل المِلفات آليا، حتى يجعل نسخة العمل مشابهة تماما لما كان عليه الفرع عند آخر إيداع تم فيه.

عليك الآن العمل على الإصلاح العاجل. لننشئ فرع hotfix لتعمل عليه حتى تنتهي:

$ git checkout -b hotfix
Switched to a new branch 'hotfix'
$ vim index.html
$ git commit -a -m 'Fix broken email address'
[hotfix 1fb7853] Fix broken email address
 1 file changed, 2 insertions(+)
فرع الإصلاح العاجل (`hotfix`) مبني على الفرع الرئيس (`master`)
شكل ٢١. فرع الإصلاح العاجل (hotfix) مبني على الفرع الرئيس (master)

يمكنك الآن إجراء الاختبارات والتأكد من أن الإصلاح الذي صنعته هو المراد. ثم دمج فرع الإصلاح العاجل في الفرع الرئيس حتى تدفعه إلى الإنتاج. يمكنك فعل هذا بأمر الدمج git merge:

$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
 index.html | 2 ++
 1 file changed, 2 insertions(+)

ستلاحظ عبارة ‪“fast-forward”‬ («تسريع») في ناتج الدمج. هذا لأن الإيداع C4 الذي يشير إليه فرع الإصلاح العاجل كان مباشرةً أمام الإيداع C2 الذي تقف فيه، فلم يفعل جت سوى تحريك الإشارة إلى الأمام. بلفظ آخر: عندما تريد دمج إيداع في إيداع آخر يمكن الوصول إليه بتتبع تاريخه، فإن جت لا يعقد الأمور بل يحرك الإشارة إلى الأمام، فلا أعمال مفترقة ليحاول دمجها — يسمى هذا «تسريعًا» (‪“fast-forward”‬).

تعديلاتك الآن موجودة في لقطة الإيداع التي يشير إليها الفرع الرئيس، فيمكنك الآن نشرها.

تسريع `master` إلى `hotfix`
شكل ٢٢. تسريع master إلى hotfix

بعد نشر إصلاحك شديد الأهمية، تكون مستعدا للعودة إلى عملك الذي كنت تفعله قبل هذه المقاطعة. ولكن عليك أولا حذف فرع hotfix لأنك لم تعد تحتاج إليه؛ فالفرع الرئيس يشير إلى الشيء نفسه. يمكنك حذفه بالخيار -d مع أمر التفريع git branch:

$ git branch -d hotfix
Deleted branch hotfix (3a0874c).

يمكنك الآن العودة إلى فرع العمل الحالي الخاص بالمسألة رقم ٥٣، وإكمال العمل عليها.

$ git checkout iss53
Switched to branch "iss53"
$ vim index.html
$ git commit -a -m 'Finish the new footer [issue 53]'
[iss53 ad82d7a] Finish the new footer [issue 53]
1 file changed, 1 insertion(+)
استكمال العمل على `iss53`
شكل ٢٣. استكمال العمل على iss53

من الواجب ملاحظة أن مِلفات فرع iss53 لا تحتوى على عملك في فرع hotfix. فإذا احتجت إلى جذبه إليها، ادمج فرع master في فرع iss53 بالأمر git merge master، أو أجّله حتى تقرر جذب فرع iss53 إلى master فيما بعد.

أسس الدمج

إذا رأيت أن عملك على المسألة رقم ٥٣ قد اكتمل وصار جاهزا لدمجه في الفرع الرئيس، فستدمج فرع iss53 في فرع master، تماما مثلما دمجت فرع hotfix سابقا: ليس عليك سوى سحب الفرع الذي تريد الدمج فيه ثم تنفيذ أمر الدمج:

$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index.html |    1 +
1 file changed, 1 insertion(+)

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

اللقطات الثلاثة المستعملة في دمج نموذجي معتاد
شكل ٢٤. اللقطات الثلاثة المستعملة في دمج نموذجي معتاد

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

إيداع دمج
شكل ٢٥. إيداع دمج

الآن قد دُمِج عملك، ولم تعد في حاجة إلى فرع iss53. فيمكنك غلق هذه المسألة في نظام متابعة المسائل، وحذف الفرع:

$ git branch -d iss53

أسس نزاعات الدمج

لا تسير هذه العملية بسلاسة في بعض الأحيان. فإذا عدّلت الجزء نفسه في الملف نفسه تعديلا مختلفا في الفرعين اللذين تنوي دمجهما، فلن يستطيع جت أن يدمجمهما دمجا نظيفا. فإن كان إصلاحك للمسألة رقم ٥٣ عدّل الجزء نفسه من الملف الذي عدّلته في فرع الإصلاح العاجل، فستواجه نزاع دمج يشبه هذا:

$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

لم ينشئ جت آليًّا إيداعَ دمجٍ جديدًا، بل أوقف العملية حتى تحل النزاع. فإذا أردت رؤية الملفات غير المدموجة في أي وقت بعد نزاع الدمج، نفّذ أمر الحالة git status:

$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")

Unmerged paths:
  (use "git add <file>..." to mark resolution)

    both modified:      index.html

no changes added to commit (use "git add" and/or "git commit -a")

ستظهر الملفات المتنازع عليها ولم تُدمج بعد أنها غير مدموجة ‪“unmerged”‬. ويضيف جت علامات معيارية لحل النزاعات إلى الملفات المتنازع عليها، حتى تتمكن من تحريرها يدويا وحل تلك النزاعات. فستجد أن في ملفك جزءًا يشبه هذا:

<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
 please contact us at support@github.com
</div>
>>>>>>> iss53:index.html

تجد نسخة HEAD (فرع master، لأنه الفرع الذي سحبته قبل أمر الدمج) في النصف الأعلى من هذه الك‍تلة (كل ما هو فوق سطر =======)، ونسخة iss53 في النصف الأسفل منها. وحتى تحل هذا النزاع، عليك اختيار أحد الجزأين أو دمج محتواهما بنفسك. فمثلا قد تحله بتغيير الك‍تلة كلها إلى:

<div id="footer">
please contact us at email.support@github.com
</div>

يحمل هذا الحل شيئا من كلا الجزأين. أما الأسطر <<<<<<< و ======= و >>>>>>> فقد أزلناها بالكامل. وبعد حل كل نزاع مثل هذا في كل ملف متنازع عليه، نفّذ أمر الإضافة git add على كل ملف لإعلام جت أنه قد حُلّ. فتأهيل الملف في جت يعلن نزاعه محلولا.

وإذا أردت استعمال أداة رسومية لحل هذه المشاكل، فيمكنك تنفيذ git mergetool، والذي يشغّل أداة دمج رسومية مناسبة ويسير معك خلال النزاعات:

$ git mergetool

This message is displayed because 'merge.tool' is not configured.
See 'git mergetool --tool-help' or 'git help config' for more details.
'git mergetool' will now attempt to use one of the following tools:
opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse diffmerge ecmerge p4merge araxis bc3 codecompare vimdiff emerge
Merging:
index.html

Normal merge conflict for 'index.html':
  {local}: modified file
  {remote}: modified file
Hit return to start merge resolution tool (opendiff):

إذا أردت استعمال أداة دمج أخرى غير الأداة المبدئية (اختار جت في هذه الحالة أداة opendiff لأننا نفّذاه على نظام ماك)، فيمكنك رؤية قائمة بجميع الأدوات المدعومة في الأعلى بعد جملة ‪“one of the following tools”‬. ليس عليك سوى ك‍تابة اسم الأداة التي تريدها.

إذا احتجت أدوات متقدمة أكثر لحل النزاعات العويصة، فسنتحدث أكثر عن الدمج في Advanced Merging.

بعد إغلاق أداة الدمج، فسيسألك جت عما إذا كان الدمج ناجحا. إذا أجبت بنعم، فسيؤهل الملف لك لإعلان أنه قد حُل. ويمكنك عندئذٍ استعراض الحالة مجددا للتحقق أن جميع النزاعات قد حُلت:

$ git status
On branch master
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:

    modified:   index.html

فإذا كنت راضيا عن هذا، وتأكدت من أن كل شيء كان عليه نزاع قد أُهِّل، نفّذ git commit لاختتام إيداع الدمج. ورسالة الإيداع المبدئية تشبه هذا:

Merge branch 'iss53'

Conflicts:
    index.html
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
#	.git/MERGE_HEAD
# and try again.


# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# All conflicts fixed but you are still merging.
#
# Changes to be committed:
#	modified:   index.html
#

فيها يمكنك شرح حلك للنزاع وتعليل تعديلاتك إن لم تكن واضحة، إذا ظننت أن هذا يفيد من يرى هذا الإيداع فيما بعد.

إدارة الفروع

الآن وقد أنشأت فروعا ودمجتها وحذفتها، لنرَ أدوات إدارة فروع ستفيدك عندما تشرع في استعمال الفروع طوال الوقت.

ليس أمر التفريع git branch لإنشاء فروع وحذفها فحسب. فإذا نفّذته بلا مُعامِلات، سيسرد لك فروعك الحالية:

$ git branch
  iss53
* master
  testing

لاحظ مِحرف النجمة * أمام فرع master؛ إنه يعني أن هذا الفرع هو الفرع المسحوب حاليا (أي أنه الفرع الذي تشير إليه إشارة الرأس HEAD). يعني هذا أنك إذا أودعت الآن، فإن فرع master سيتقدم إلى الأمام بعملك الجديد. لرؤية آخر إيداع في كل فرع، نفّذ git branch -v:

$ git branch -v
  iss53   93b412c Fix javascript issue
* master  7a98805 Merge branch 'iss53'
  testing 782fd34 Add scott to the author list in the readme

والخياران المفيدان --merged («مدموج») و --no-merged («غير مدموج») يصفّيان هذه القائمة فلا ترى إلا الفروع التي دمجتها أو التي لم تدمجها في الفرع الذي تقف فيه. فللفروع المدموجة في الفرع الحالي، نفّذ git branch --merged:

$ git branch --merged
  iss53
* master

ترى iss53 في القائمة لأنك دمجته سابقًا. والفروع التي في هذه القائمة وليس أمامها نجمة (*)، يمكنك في العموم حذفها بأمان بالأمر git branch -d؛ لن تفقد شيئًا بحذفها لأنك بالفعل ضممت ما فيها من عمل إلى فرع آخر.

لرؤية جميع الفروع التي بها عمل غير مدموج بعد، نفّذ git branch --no-merged:

$ git branch --no-merged
  testing

يُظهر لك هذا الأمر فرعك الآخر. ستفشل محاولة حذفه بالأمر git branch -d لأن به عملًا غير مدمج بعد:

$ git branch -d testing
error: The branch 'testing' is not fully merged.
If you are sure you want to delete it, run 'git branch -D testing'.

إن رغبت حقًا ويقينًا في حذف الفرع وفقد ما فيه من عمل، فأجبر جت على حذفه بالخيار -D، كما تخبرك الرسالة.

إذا لم تعطِ إيداعًا أو اسمَ فرعٍ إلى الخيارين --merged و --no-merged، فإنهما، على الترتيب، سيسردان ما الذي دُمج أو لم يُدمج في الفرع الحالي.

يمكنك دائما إعطاؤهما اسم فرع للسؤال عن حالة دمجه من غير أن تحتاج إلى الانتقال أولا إلى هذا الفرع بأمر السحب، مثلا: ما الذي لم يُدمج في فرع master؟

$ git checkout testing
$ git branch --no-merged master
  topicA
  featureB

تغيير اسم فرع

لا تغيّر اسم فرع ما زال الآخرون يستعمله. ولا تغيّر اسم فرع مثل master أو main أو mainline قبل أن تقرأ فصل تغيير اسم الفرع الرئيس.

هَبْ فرعًا لديك اسمه bad-branch-name وتريد جعله corrected-branch-name مع الإبقاء على تاريخه بالكامل. وتريد أيضا تغيير اسمه على الخادوم البعيد (جت‌هب GitHub أو جت‌لاب GitLab أو غيرهما). كيف تفعل هذا؟

غيّر اسم الفرع محليًّا بالأمر git branch --move :

$ git branch --move bad-branch-name corrected-branch-name

هذا يغيّر bad-branch-name إلى corrected-branch-name، ولكن هذا التغيير محليٌّ فقط حتى الآن. ولجعل الآخرين يرون الفرع الصحيح في المستودع البعيد، عليك دفعه:

$ git push --set-upstream origin corrected-branch-name

لنلقِ نظرةً على حالنا الآن:

$ git branch --all
* corrected-branch-name
  main
  remotes/origin/bad-branch-name
  remotes/origin/corrected-branch-name
  remotes/origin/main

لاحظ أنك في فرع corrected-branch-name وأنه متاح في المستودع البعيد. ولكنّ الفرع ذا الاسم الخاطئ متاح كذلك هناك، ولكن يمكنك حذفه بالأمر التالي:

$ git push origin --delete bad-branch-name

الآن قد حلّ اسم الفرع الصحيح محل اسم الفرع الخاطئ في كل مكان.

تغيير اسم الفرع الرئيس

تغيير اسم فرع مثل master أو main أو mainline أو default سيُعطّل التكاملات والخدمات والأدوات المساعدة وبُريمِجات البناء والإصدار التي يستخدمها مستودعك. لذا عليك التشاور مع زملائك في المشروع قبل الإقدام على هذا الأمر. وعليك كذلك أن تبحث بحثًا وافيًا في مستودعك وتحدّث أيّ إشارة إلى الاسم القديم للفرع في الكود والبُريمِجات.

غيّر اسم فرع master المحلي إلى main بالأمر:

$ git branch --move master main

لم يعد لدينا أي فرع محلي master، لأننا غيّرنا اسمه إلى main.

ولجعل الآخرين يرون فرع main الجديد، عليك دفعه إلى المستودع البعيد. هذا يجعل الفرع الجديد متاحًا هناك:

$ git push --set-upstream origin main

نجد الآن أنفسنا في الحالة التالية:

$ git branch --all
* main
  remotes/origin/HEAD -> origin/master
  remotes/origin/main
  remotes/origin/master

اختفى فرعك المحلي master، وحلّ محله الفرع main. وصار main في المستودع البعيد. ولكن فرع master القديم بقى موجودًا في المستودع البعيد. فسيظل المشاركون الآخرون يتّخِذون فرع master أساسًا لأعمالهم، حتى تتّخذَ إجراءً آخر.

بين يديك الآن عددٌ من المهام لاجتياز تلك المرحلة الانتقالية:

  • على جميع المشروعات المعتمِدة على هذا المشروع تحديث كودها و/أو إعداداتها.

  • عليك تحديث أي مِلفات إعدادات خاصة بالاختبارات.

  • عليك مواءمة بُريمِجات البناء والإصدار.

  • عليك مواءمة إعدادات خادوم مستودعك، مثل الفرع المبدئي وقواعد الدمج والأمور الأخرى التي تعتمد على أسماء الفروع.

  • عليك تحديث الإشارات إلى الفرع القديم في التوثيق.

  • عليك إغلاق أو دمج كل طلبات الجذب الموجهة إلى الفرع القديم.

بعد فعل جميع هذه المهام، والتيقن أن فرع main يقوم بعمله تمامًا مثل فرع master، يمكنك حذف فرع master:

$ git push origin --delete master

أساليب العمل التفريعية

بما أنك الآن تعلم أسس التفريع والدمج، ماذا يمكنك أو يجدر بك فعله بهما؟ سنتناول في هذا الفصل بعض أشهر أساليب العمل التي يجعلها هذا التفريع الخفيف ممكنة، لكي تقرر إذا ما كنت تود أن تجعلها جزءًا من دورة التطوير التي تتبعها.

الفروع طويلة العمر

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

الكثيرون من المطورين مستخدمي جت يعتمدون هذا النهج في أسلوب سير العمل، فيخصصون مثلا الفرع الرئيس للمصدر المستقر تماما وحسب، أو للذي أُصدر فعلا، أو للذي سيُصدر. ويكون لديهم فرعا موازيا اسمه develop أو next مثلا، ليعملوا منه أو ليستعملوه لاختبار الاستقرار، فليس بالضرورة أن يكون مستقرا دوما، ولكن عند استقراره، يمكن دمجه في الفرع الرئيس. ويستعملون هذا الفرع ليجذبوا فيه فروع المواضيع (الفروع قصيرة العمر، مثل فرع iss53 المذكور سابقا) عندما تكون جاهزة، لضمان اجتيازها جميع الاختبارات وأنها لا تُحدِث عللًا.

نحن فعليا نتحدث عن إشارات ترتقي في سلّم ايداعاتك. فالفروع المستقرة في أسفله، أما طليعة التطوير ففي أعلاه.

منظور خطي لتفريع الاستقرار المتزايد
شكل ٢٦. منظور خطي لتفريع الاستقرار المتزايد

لعل الأسهل تصور أنها صومعات عمل منعزلة، فتتخرج دفعة من الإيداعات إلى صومعة أخرى أكثر استقرارا عندما تجتاز جميع الاختبارات.

منظور «صومعي» لتفريع الاستقرار المتزايد
شكل ٢٧. منظور «صومعي» لتفريع الاستقرار المتزايد

يمكنك فعل هذا بعدة مستويات من الاستقرار. فلدى بعض المشروعات الكبيرة فرع proposed أو pu («تحديثات مقترحة») ويدمجوا فيه فروعا قد لا تكون جاهزة لأن تكون في فرع next أو master. فالأمر أن فروعك في مستويات مختلفة من الاستقرار، فعندما يصل أحدها إلى مستوى استقرار أعلى، فإنه يُدمج في الفرع الأعلى. ونكرر: ليس ضروريا استعمال عدد من الفروع طويلة العمر، ولكنه كثيرا ما يفيد، خصوصا عندما تتعامل مع مشروعات معقدة أو كبيرة جدا.

فروع المواضيع

لكن فروع المواضيع تفيد جميع المشروعات بغض النظر عن حجمها. فرع الموضوع هو فرع قصير العمر تنشئه وتستعمله لميزة واحدة أو ما يخصها من عمل. لعلك لم تفعل هذا قط مع نظام إدارة نسخ آخر، لأن التفريع والدمج غالبا ما يكونا بطيئين جدا في الأنظمة الأخرى. ولكن الشائع مع جت هو إنشاء فروع والعمل عليها ودمجها وحذفها عدة مرات في اليوم الواحد.

وقد رأيت هذا في الفصل السابق في فرعَي iss53 و hotfix اللذين أنشأتهما، فقد صنعت بضعة إيداعات فيهما ثم حذفتهما فور دمجهما في فرعك الرئيس. يسمح لك هذا الأسلوب بـ«تبديل السياق» سريعا وبالكامل، لأنك قسّمت عملك إلى صومعات، وكل صومعة (فرع) ليس فيها إلا التعديلات التي تخص موضوعا واحدا،فيسهّل ذلك رؤيتها عند المراجعة (code review) وغير ذلك. ويمكنك إبقاء التعديلات هناك دقائق أو أياما أو شهورا، ثم دمجها عندما تكون جاهزة، بغض النظر عن ترتيب إنشائها أو العمل عليها.

لنقُل مثلا إنك عملت (في master)، ثم تفرّعت لإصلاح علة (iss91)، وعملت عليها قليلا، ثم تفرّعت مجددا (من الفرع الثاني) لتجرب طريقة أخرى لإصلاح العلة نفسها (iss91v2)، ثم عدت إلى فرعك الرئيس (master) وعملت فيه قليلا، ثم تفرّعت منه لتجربة شيء لست واثقا أنه جيد (فرع dumbidea). سيبدو تاريخ إيداعك الآن مثل هذا:

فروع مواضيع متعددة
شكل ٢٨. فروع مواضيع متعددة

لنقُل إنك الآن وجدت إصلاحك الثاني للعلة (iss91v2) أفضل، وأنك أريت زملاءك فرع dumbidea فأخبروك أنه عبقري. فيمكنك إذًا إلقاء فرع iss91 الأصلي (وفقد الإيداعين C5 و C6)، ودمج الفرعين الآخرين في الفرع الرئيس. سيبدو تاريخك الآن كهذا:

التاريخ بعد دمج `dumbidea` و `iss91v2`
شكل ٢٩. التاريخ بعد دمج dumbidea و iss91v2

سنتحدث بتفصيل أكبر عن مختلف أساليب سير العمل الممكنة في مشروعات جت في جت الموزع، فعليك قراءة هذا الفصل قبل أن تقرر أي أسلوب تفريع سيتبعه مشروعك التالي.

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

الفروع البعيدة

الإشارات البعيدة هي تلك الإشارات الموجودة في مستودعاتك البعيدة، كالفروع والوسوم. يمكنك سرد جميع الإشارات البعيدة بالأمر git ls-remote <البعيد>، أو سرد الفروع البعيدة ومعلوماتها بالأمر git remote show <البعيد> (حيث <البعيد> هو الاسم المختصر المستودع البعيد). ولكن الشائع هو الانتفاع بـ«الفروع المتعقِّبة للبعيد».

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

يكون شكل أسماء الفروع المتعقِّبة للبعيد <remote>/<branch> (أيْ اسم المستودع البعيد ثم شرطة مائلة ثم اسم الفرع). فمثلا إذا أردت رؤية كيف بدا فرع master في مستودعك البعيد origin عندما اتصلت به آخر مرة، فانتقل إلى فرع origin/master. وإذا كنت تعمل مع زميل على مسألةٍ ودفَعَ فرع iss53 إلى المستودع البعيد، فقد يكون لديك فرع محلي بالاسم نفسه، ولكن الفرع الذي على الخادوم سيمثله عندك الفرع المتعقِّب للبعيد الذي اسمه origin/iss53.

لعل الكلام غامض، فدعنا ننظر إلى مثال. لنقُل إن لديك خادوم جت على شبكتك عنوانه git.ourcompany.com. إذا استنسخته، فإن أمر الاستنساخ سيسميه origin لك، ويجذب كل ما فيه من بيانات، وينشئ إشارة إلى ما يشير إليه فرع master عليه ويسميه origin/master محليا. وسيعطيك جت أيضا فرع master محلي خاص بك بادئا من المكان نفسه الذي فيه فرع master الخاص بالأصل، حتى يتسنّى لك البدء بالعمل.

الاسم ‪“origin”‬ ليس مميزا

تماما مثلما أن اسم الفرع الرئيس ‪“master”‬ لا يحمل أي معنى خاص في جت، فكذلك اسم المستودع البعيد الأصل ‪“origin”‬. فإن ‪“master”‬ هو الاسم المبدئي لأول فرع ينشئه جت عندما تستخدم git init (وهو السبب الوحيد لشيوعه)، وكذلك ‪“origin”‬ هو الاسم المبدئي للمستودع البعيد عندما تستخدم git clone. فإذا استخدمت git clone -o yalla مثلا، فإنك ستجد أن yalla/master هو اسم الفرع البعيد المبدئي.

المستودعان البعيد والمحلي بعد الاستنساخ
شكل ٣٠. المستودعان البعيد والمحلي بعد الاستنساخ

إذا عملت في فرعك الرئيس المحلي، ودفع أحد إلى الفرع الرئيس في المستودع البعيد، فإن تاريخَي الفرعين سيتقدمان مفترقين. وإن تجنبت الاتصال مع مستودعك البعيد على الخادوم الأصل، فلن تتحرك إشارة origin/master التي لديك.

قد يفترق العمل المحلي والبعيد
شكل ٣١. قد يفترق العمل المحلي والبعيد

لمزامنة عملك مع مستودع بعيد، نفّذ الأمر git fetch <البعيد> (في حالتنا git fetch origin). فهذا الأمر يبحث عن المستودع المسمى ‪“origin”‬ (في حالتنا git.ourcompany.com)، ويستحضر البيانات التي عليه وليست عندك بعد، ويحدّث قاعدة بياناتك المحلية، ويحرك إشارة origin/master الخاص بك لتشير إلى موقعها الجديد المحدَّث.

يحدّث أمر الاستحضار `git fetch` فروعك المتعقِّبة للبعيد
شكل ٣٢. يحدّث أمر الاستحضار git fetch فروعك المتعقِّبة للبعيد

لتمثيل وجود خواديم بعيدة عديدة ولإيضاح منظر الفروع المتعقِّبة لهذه المستودعات البعيدة، لنقُل إن لديك خادوم جت داخلي آخر، والذي لا يستخدمه إلا فريق واحد من أجل التطوير، وإن عنوانه هو git.team1.ourcompany.com. يمكنك إضافته إشارةً بعيدة جديدة في مشروعك، بأمر git remote add كما رأينا في أسس جت، وتسميته teamone، والذي يُعتبر اسما مختصرا لرابطه الكامل.

إضافة إشارة إلى خادوم بعيد آخر
شكل ٣٣. إضافة إشارة إلى خادوم بعيد آخر

والآن، نفّذ أمر git fetch teamone لاستحضار كل ما لدى خادوم teamone البعيد وليس لديك بعد. ولأن ليس لديه من البيانات إلا جزءًا من التي لدى خادومك الأصلي (origin) الآن، فلن يستحضر جت شيئا، ولكنه سيضبط فرعا متعقبا للبعيد يسمى teamone/master ليشير إلى الإيداع الذي يشير إليه فرع master في مستودع teamone.

فرع متعقب للبعيد للفرع `teamone/master`
شكل ٣٤. فرع متعقب للبعيد للفرع teamone/master

الدفع

عندما تريد أن تشارك فرعا مع العالَم، فعليك دفعه إلى مستودع بعيد لديك إذن تحريره. ففروعك المحلية لا تُزامَن آليًّا إلى المستودعات البعيدة، حتى التي دفعت إليها؛ عليك دفع الفروع التي تريد مشاركتها بأمر صريح. يسمح لك هذا أن تستعمل فروعا خصوصية للأعمال التي لا تريد مشاركتها، وألا تدفع إلا فروع المواضيع التي تريد التعاون عليها.

مثلا إذا كان لديك فرعا اسمه serverfix وتريد العمل عليه مع الآخرين، يمكنك دفعه بالطريقة نفسها التي دفعت بها فرعك الأول؛ نفّذ git push <remote> <branch> (أي اسم المستودع البعيد ثم الفرع):

$ git push origin serverfix
Counting objects: 24, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (15/15), done.
Writing objects: 100% (24/24), 1.91 KiB | 0 bytes/s, done.
Total 24 (delta 2), reused 0 (delta 0)
To https://github.com/schacon/simplegit
 * [new branch]      serverfix -> serverfix

هذا اختصار، لأن جت يفك اسم الفرع serverfix إلى refs/heads/serverfix:refs/heads/serverfix، الذي يعني «ادفع فرعي المحلي serverfix إلى المستودع البعيد origin لتحدّث فرع serverfix عليه». سنفصّل شرح جزء refs/heads/ في دواخل جت، ولكن عامةً يمكنك تركه. كذلك يمكنك تنفيذ git push origin serverfix:serverfix الذي يفعل الشيء نفسه؛ إنه يقول: «خذ فرعي المسمى serverfix واجعله فرع serverfix في المستودع البعيد». هذه الصياغة مفيدة لدفع فرع محلي إلى فرع بعيد باسم مختلف. فمثلا إن لم تُرِده أن يسمى serverfix في المستودع البعيد، فنفّذ git push origin serverfix:awesomebranch، فهذا يدفع فرعك المحلي serverfix إلى فرع awesomebranch في المستودع البعيد.

لا تكتب كلمة مرورك كل مرة

إذا كنت تستعمل رابط HTTPS للدفع، فإن خادوم جت سيسألك عن اسم مستخدمك وكلمة مرورك للاستيثاق. المعتاد أن عميل جت سيسألك عن هذا في الطرفية حتى يعرف الخادوم إذا ما كان مسموحا لك بالدفع.

إذا لم تشأ أن تكتب كلمة مرورك في كل مرة تدفع فيها، فعليك إعداد «تذكُّر مؤقت للاستيثاق» (‪“credential cache”‬). أسهل خيار هو جعله في ذاكرة الحاسوب لعدة دقائق، والذي يمكنك إعداده بالأمر git config --global credential.helper cache.

لمعلومات أكثر عن خيارات تذكر الاستيثاق المتاحة، انظر Credential Storage.

وفي المرة التالية التي يستحضر أحد زملائك من الخادوم، سيحصل على إشارة إلى حيث يشير فرع serverfix على الخادوم؛ سيجدها عنده في الفرع البعيد origin/serverfix:

$ git fetch origin
remote: Counting objects: 7, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0)
Unpacking objects: 100% (3/3), done.
From https://github.com/schacon/simplegit
 * [new branch]      serverfix    -> origin/serverfix

من المهم ملاحظة أنك عندما تستحضر، يجلب لك هذا فروعًا جديدة متعقبة للبعيد، أي أنك لا تحصل تلقائيًّا على نسخ محلية منها يمكنك تعديلها. بلفظ آخر، لا تحصل آليًّا على فرع serverfix جديد في هذه الحالة: لم تُعطَ إلا إشارة origin/serverfix التي لا يمكنك التعديل فيها.

لدمج هذا العمل في فرعك الحالي، يمكنك تنفيذ git merge origin/serverfix. وإذا أردت فرع serverfix خاصًا بك تستطيع العمل فيه، يمكنك تفريعه من الفرع المتعقب للبعيد:

$ git checkout -b serverfix origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

يعطيك هذا فرعًا محليًا يمكنك العمل فيه، والذي يبدأ من حيث يقف origin/serverfix.

تعقب الفروع

إن سحب فرع محلي من فرع متعقب للبعيد ينشئ آليًّا ما يسمى «فرع متعقِّب» (والفرع الذي يتعقبه يسمى «الفرع المنبع»). الفروع المتعقبة هي فروع محلية ذات علاقة مباشرة بفرع بعيد. فإذا كنت في فرع متعقب وكتبت git pull، فسيعرف جت تلقائيا أي مستودع بعيد يستحضر منه وأي فرع يدمج فيه.

عندما تستنسخ مستودعًا، ينشئ جت فرعًا باسم الفرع المبدئي فيه (مثل master) ويجعله يتعقب الفرع المبدئي في المستودع الأصل (origin/master). ولكن يمكنك إعداد فروع متعقبة أخرى إذا أردت، لتعقب مستودعات بعيدة أخرى، أو لتعقب فرع غير الرئيس. أيسر حالة مثلما رأيتَ آنفًا، عند اتحاد اسم الفرع المحلي والبعيد، أيْ git checkout -b <branch> <remote>/<branch>. وهذه العملية شائعة بما يكفي أن جت يتيح اختصارها بالخيار --track:

$ git checkout --track origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

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

$ git checkout serverfix
Branch serverfix set up to track remote branch serverfix from origin.
Switched to a new branch 'serverfix'

ولإعداد فرع محلي باسم مختلف عن الفرع البعيد، فسهلٌ استعمال الصيغة الأولى مع اسم فرع محلي مختلف:

$ git checkout -b sf origin/serverfix
Branch sf set up to track remote branch serverfix from origin.
Switched to a new branch 'sf'

الآن، فرعك المحلي sf سيجذب آليًّا من origin/serverfix.

إذا كان لديك بالفعل فرعا محليا وتريد ضبطه ليجذب من فرع بعيد جذبته للتو، أو تريد تغيير الفرع المنبع الذي تتعقبه، استعمل الخيار -u أو --set-upstream-to مع أمر التفريع git branch لضبطه بأمر صريح في أي وقت.

$ git branch -u origin/serverfix
Branch serverfix set up to track remote branch serverfix from origin.
الاسم المختصر للمنبع

عندما يكون لديك فرع متعقب مضبوط، يمكنك الإشارة إلى فرعه المنبع بالاختصار @{upstream} أو @{u}. فإذا كنت في master وكان يتعقب origin/master، يمكنك تنفيذ أمر مثل git merge @{u} بدلا من git merge origin/master إن أردت.

لرؤية الفروع المتعقِّبة التي ضبطتها، استعمل الخيار -vv مع أمر التفريع git branch، ليسرد لك فروعك المحلية مع معلومات مزيدة فيها ما يتعقبه كل فرع وإذا كان فرعك متقدما عنه (ahead) أو متأخرا (behind) أو كليهما.

$ git branch -vv
  iss53     7e424c3 [origin/iss53: ahead 2] Add forgotten brackets
  master    1ae2a45 [origin/master] Deploy index fix
* serverfix f8674d9 [teamone/server-fix-good: ahead 3, behind 1] This should do it
  testing   5ea463a Try something new

فنرى هنا أن فرع iss53 يتعقب origin/iss53 وأنه «متقدم» (ahead) باثنين، أي أن لدينا إيداعين محليين ولم ندفعهما إلى الخادوم بعد. ونرى أيضا أن فرع master يتعقب origin/master وأنه محدَّث. ثم نرى بعدهما أن فرع serverfix يتعقب فرع server-fix-good على خادوم teamone وأنه متقدم عنه بثلاثة ومتأخر بواحد، أي أن لدى الخادوم إيداعا لم ندمجه في فرعنا بعد، وأن لدينا ثلاثة إيداعات محلية لم ندفعها إليه. ثم نرى في النهاية أن فرع testing لا يتعقب أي فرع بعيد.

مهم ملاحظة أن هذه الأعداد ليست إلا منذ آخر استحضار (fetch) من كل خادوم نتعقبه. فلا يحاول هذا الأمر الاتصال بالخواديم؛ إنما يخبرك بما يحفظه على حاسوبك عما فيها. فإذا أردت من أعداد التقدم والتأخر أن تكون أحدث ما يكون، فعليك الاستحضار من جميع خواديمك البعيدة قبل تنفيذ هذا الأمر مباشرةً. ويمكنك فعل ذلك هكذا:

$ git fetch --all; git branch -vv

الجذب

نعلم أن أمر الاستحضار git fetch يستحضر التعديلات التي في المستودع البعيد وليست لديك بعد، ولكنه لا يعدّل مجلد عملك إطلاقا؛ إنما يجلب البيانات لك ويتركك تدمجها بنفسك. ولكن لدى جت أمر يسمى أمر الجذب git pull، وهذا الأمر عمليًّا يكافئ استحضارًا git fetch متبوعًا مباشرةً بدمج git merge، في معظم الحالات. فإذا كان لديك فرع متعقِّب مضبوط كما في الفصل السابق، إما بضبطه صراحةً وإما بأن يضبطه لك أمر الاستنساخ git clone أو أمر السحب git checkout، فإن أمر الجذب git pull سينظر أيّ مستودع وأيّ فرع يتعقبهما فرعك الحالي، ويستحضر ما في المستودع البعيد ويحاول دمجه في فرعك.

الأفضل عمومًا هو الاستخدام الصريح لأمرَي الاستحضار fetch والدمج merge، فالسحر الذي يقوم به أمر الجذب كثيرًا ما يكون مُلغِزًا.

حذف فروع بعيدة

لنقُل إنك قضيت ما تريد من فرع بعيد، مثلا انتهيت أنت وزملاؤك من إضافة ميزة جديدة ودمجتموها في الفرع الرئيس. يمكنك حذف فرع بعيد بالخيار --delete مع أمر الدفع git push. فإذا أردت حذف فرع serverfix من الخادوم، يمكنك تنفيذ الأمر التالي:

$ git push origin --delete serverfix
To https://github.com/schacon/simplegit
 - [deleted]         serverfix

لا يفعل هذا الأمر سوى أنه يحذف الإشارة من على الخادوم. ولكن خواديم جت عمومًا تبقى البيانات موجودة وقتًا، إلى أن يعمل جامع المهملات، فغالبًا ستستطيع استعادته بسَهولة إن حذفته بالخطأ.

إعادة التأسيس

توجد طريقتان في جت لضم التعديلات من فرع إلى آخر: الدمج merge وإعادة التأسيس rebase. سنتعلم في هذا الفصل ما هي إعادة التأسيس، وكيف نفعلها، ولماذا هي أداة مذهلة فعلا، ومتى لن تود استخدامها.

أسس إعادة التأسيس

إذا عدت إلى مثال سابق في أسس الدمج، ستجد أن عملك افترق إلى إيداعات في فرعين مختلفين.

تاريخ بسيط مفترق
شكل ٣٥. تاريخ بسيط مفترق

أسهل طريقة لضم الفرعين، كما ناقشنا بالفعل، هي أمر الدمج merge، والذي يقوم بدمج ثلاثي بين آخر لقطتين في الفرعين (C3 و C4) وآخر سلف مشترك لهما (C2)، وينشئ لقطة جديدة (وإيداعًا).

الدمج لضم تاريخ العمل المفترق
شكل ٣٦. الدمج لضم تاريخ العمل المفترق

لكن توجد طريقة أخرى: يمكنك أخذ رُقعة التعديلات (‪“patch”‬) التي صنعتها في هذا الإيداع (C4) وإعادة تطبيقها على الإيداع الآخر (C3). هذه ما نسميها «إعادة التأسيس» (‪“rebasing”‬) في جت. فبأمر إعادة التأسيس rebase يمكنك أخذ جميع التعديلات التي أودعتها في فرعٍ ما، وإعادة صنعها في فرع آخر.

في هذا المثال سنسحب فرع experiment، ثم نعيد تأسيسه على الفرع الرئيس master.

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

تتم هذه العملية بالذهاب إلى السلف المشترك للفرعين (الفرع الحالي الذي تقف فيه وتريد إعادة تأسيسه، والفرع الذي تريد إعادة التأسيس عليه)، وحساب التعديلات التي تمت في كل إيداع في الفرع الحالي وحفظها في ملفات مؤقتة، ثم ضبط الفرع الحالي إلى الإيداع الذي عنده الفرع الذي تريد إعادة التأسيس عليه، وأخيرا تطبيق كل تعديل عليه بالترتيب.

إعادة تأسيس تعديلات `C4` على `C3`
شكل ٣٧. إعادة تأسيس تعديلات C4 على C3

يمكنك الآن العودة إلى الفرع الرئيس وعمل دمج تسريع (‪“fast-forward”‬).

$ git checkout master
$ git merge experiment
تسريع فرع `master`
شكل ٣٨. تسريع فرع master

اللقطة التي يشير إليها إيداع C4' الآن مطابقة تماما لتلك التي كان يشير إليها C5 في مثال الدمج. لا فرق في الناتج النهائي، ولكن تعطينا إعادة التأسيس تاريخًا أنظف. فإذا نظرت إلى سجل فرع مُعاد تأسيسه، ستجده تاريخًا خطيًّا: يبدو أن كل العمل تم على التوالي، ولو أنه في الأصل قد تم على التوازي.

غالبا ستفعل هذا لضمان أن إيداعاتك ستُطبّق بنظافة على فرعٍ بعيد — مثلا في مشروع تريد المشاركة فيه لكنك لست مطورًا فيه. فستعمل في هذه الحالة في فرع، ثم تعيد تأسيسه على origin/master عندما تكون جاهزًا لتسليم رُقعتك إليهم. فهكذا لن يُجهِد المطورين ضمُ عملك، فما الأمر إلا تسريعًا، أو تطبيقًا نظيفًا للرقعة.

لاحظ أن اللقطة التي يشير إليها الإيداع النهائي، سواء كان آخر الإيداعات المعاد تأسيسها أو كان الإيداع النهائي لدمج، هي اللقطة نفسها؛ ليس الاختلاف إلا في التاريخ. فإعادة التأسيس تعيد صنع تعديلات تاريخ عمل في تاريخ عمل آخر بترتيبها نفسه، لكن الدمج يدمج آخر نقطتين معًا.

إعادات تأسيس شيقة أكثر

يمكنك أيضا جعل إعادة التأسيس تطبّق التعديلات على فرع غير «الفرع المستهدف». لنرَ مثلا تاريخا مثل «تاريخ فيه فرع موضوع متفرع من فرع موضوع آخر». لقد أنشأت فرع موضوع (server) لإضافة ميزات في جزء الخادوم في مشروعك، وصنعت إيداعا. بعدئذٍ أنشأت فرعا من هذا الفرع لعمل تعديلات في جزء العميل (client) وصنعت بضعة إيداعات. ثم في آخر الأمر عدت إلى فرع server وصنعت بضعة إيداعات أخرى.

تاريخ فيه فرع موضوع متفرع من فرع موضوع آخر
شكل ٣٩. تاريخ فيه فرع موضوع متفرع من فرع موضوع آخر

لنقُل إنك قررت أنك تريد دمج تعديلاتك الخاصة بجزء العميل في المسار الرئيس لكي تصدرها، ولكنك تريد الإبقاء على تعديلات جزء الخادوم حتى تختبرها أكثر. إن التعديلات التي في client وليست في server (وهي C8 و C9) تستطيع إعادة تطبيقها على فرع master بالخيار --onto مع أمر git rebase:

$ git rebase --onto master server client

إنما يقول هذا: «احسب فروقات فرع client (التعديلات التي سُجّلت فيه) منذ أن افترق عن فرع server، ثم أعد تطبيقها في فرع client كأنه قد تفرّع من فرع master وليس من server.» صعبة قليلا، لكن النتيجة عظيمة.

إعادة تأسيس فرع موضوع على فرع موضوع آخر
شكل ٤٠. إعادة تأسيس فرع موضوع على فرع موضوع آخر

عندئذٍ يمكنك تسريع الفرع الرئيس master (انظر «تسريع الفرع الرئيس لضم تعديلات فرع client»):

$ git checkout master
$ git merge client
تسريع الفرع الرئيس لضم تعديلات فرع `client`
شكل ٤١. تسريع الفرع الرئيس لضم تعديلات فرع client

لنقُل إنك قررت جذب فرع server كذلك. يمكنك إعادة تأسيس فرع server على الفرع الرئيس بلا حاجة إلى سحبه أولا، بالأمر git rebase <basebranch> <topicbranch> (أي الفرع الأساس ثم فرع الموضوع)، وهذا يسحب لك فرع الموضوع (server في حالتنا) ويعيد تطبيق ما فيه من عمل على الفرع الأساس (master):

$ git rebase master server

هذا يُعيد تطبيق العمل الذي في فرع الخادوم server على العمل الذي في الفرع الرئيس master، كما يظهر في «إعادة تأسيس فرع server على الفرع الرئيس».

إعادة تأسيس فرع `server` على الفرع الرئيس
شكل ٤٢. إعادة تأسيس فرع server على الفرع الرئيس

عندئذٍ يمكنك تسريع الفرع الأساس (master):

$ git checkout master
$ git merge server

ثم تستطيع حذف الفرعين client و server لأن كل ما فيهما قد ضُمَّ بالفعل ولم تعد بحاجة إليهما، فيصير تاريخك في نهاية هذه العملية كما في «تاريخ الإيداعات في النهاية»:

$ git branch -d client
$ git branch -d server
تاريخ الإيداعات في النهاية
شكل ٤٣. تاريخ الإيداعات في النهاية

محذورات إعادة التأسيس

ولكن… نعيم إعادة التأسيس ليس بغير عيوب، والتي يمكن اختصارها في سطر واحد:

لا تعد تأسيس إيداعات لها وجود خارج مستودعك فربما قد بنى الناس عليها عملا.

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

فعندما تعيد التأسيس، فإنك تهجر الإيداعات الموجودة وتصنع إيداعات جديدة شبيهة بالقديمة لكن مختلفة عنها. وإذا دفعت هذه الإيداعات إلى مستودعٍ ما وجذبها الآخرون وبنوا عليها أعمالا، ثم جئت فأعدت ك‍تابة هذه الإيداعات بأمر git rebase ثم دفعتها من جديد، فسيضطر زملاؤك إلى إعادة دمج أعمالهم، وستؤول الأمور إلى فوضى عندما تحاول جذب أعمالهم إلى عملك.

لنرَ كيف يمكن لإعادة تأسيس عملٍ منشور أن تسبب مشاكل. لنقُل إنك استنسخت من خادوم مركزي، ثم بنيت عليه عملا. سيبدو تاريخ إيداعك مثل هذا:

استنسخ مستودعا، وابنِ عملا عليه
شكل ٤٤. استنسخ مستودعا، وابنِ عملا عليه

ثم جاء شخصٌ آخر وصنع المزيد من الإيداعات، والتي شملت دمجًا، ثم دفعها إلى الخادوم المركزي. فقمت باستحضار (fetch) الفرع البعيد الجديد ودمجه في عملك، فصار تاريخك مثل الآتي:

استحضر المزيد من الإيداعات، وادمجها في عملك
شكل ٤٥. استحضر المزيد من الإيداعات، وادمجها في عملك

بعدئذ، قرر الذي دفع العمل المدموج أن يتراجع ويعيد تأسيس عمله بدل دمجه، فدفع بالقوة (git push --force) لإعادة ك‍تابة التاريخ على الخادوم. ثم استحضرت (fetch) من هذا الخادوم، جالبًا الإيداعات الجديدة.

شخصٌ يدفع إيداعات معاد تأسيسها، هاجرًا بذلك الإيداعات التي بنيت عليها عملك
شكل ٤٦. شخصٌ يدفع إيداعات معاد تأسيسها، هاجرًا بذلك الإيداعات التي بنيت عليها عملك

كلاكما الآن في مأزق. فإذا جذبت، ستصنع إيداع دمج يضم كلا التاريخين، وسيبدو مستودعك مثل هذا:

عندما تدمج العمل نفسه مجددا في إيداع دمج جديد
شكل ٤٧. عندما تدمج العمل نفسه مجددا في إيداع دمج جديد

فإذا نظرت في السجل git log عندما يصير تاريخك كهذا، فسترى إيداعين متطابقين في اسم المؤلف وتاريخ الإيداع ورسالته، فيسبب اللَبس. وأضف إلى ذلك أنك إذا دفعت هذا التاريخ إلى الخادوم، فستعيد تقديم كل هذه الإيداعات المعاد تأسيسها إلى الخادوم المركزي من جديد، والذي سيسبّب لبسًا لأكثر للناس. يمكننا الافتراض أن المطور الآخر لا يريد الإيداعين C4 و C6 في التاريخ، ولذا أعاد تأسيسهما.

أعد التأسيس عندما تعيد التأسيس

إذا وجدت نفسك في مثل هذا الموقف، فلدى جت وسائل سحرية أخرى قد تساعدك. فلو أن زميلك دفع بالقوة تعديلاتٍ تعيد ك‍تابة عمل بنيتَ عليه، فالتحدي هو أن تعرف ما هو عملك وما الذي أعاد ك‍تابته ذاك الشخص.

الخبر الجميل أن جت لا يحسب للإيداع وحده بصمة SHA-1، ولكن يحسبها أيضا للرُقعة (الفروقات) التي صنعها ذلك الإيداع. وهذه البصمة تسمى «معرِّف الرقعة» (‪“patch-id”‬).

فإذا جذبت عملا معاد ك‍تابته وأعدت تأسيسه على الإيداعات الجديدة من زميلك، فغالبا سينجح جت في تمييز ما هو عملك الفريد ويطبّقه على الفرع الجديد.

فمثلا في الموقف الافتراضي السابق، لو أننا أعدنا التأسيس (بالأمر git rebase teamone/master)، بدل الدمج عندما كنا في خطوة «شخصٌ يدفع إيداعات معاد تأسيسها، هاجرًا بذلك الإيداعات التي بنيت عليها عملك»، فإن جت سوف:

  • يحدد العمل الذي تفرّد به فرعنا (C2،‏ C3،‏ C4،‏ C6،‏ C7)

  • يحدد ما الذي ليس بإيداعات دمج (C2،‏ C3،‏ C4)

  • يحدد ما الذي لم تُعد ك‍تابته في الفرع المستهدف (فقط C2 و C3، لأن C4 له رقعة C4' نفسها)

  • يطبّق هذه الإيداعات على فرع teamone/master

أعد التأسيس على عمل معاد تأسيسه ومدفوع بالقوة
شكل ٤٨. أعد التأسيس على عمل معاد تأسيسه ومدفوع بالقوة

لن ينجح هذا إلا إذا كان الإيداعان C4 و C4' اللذين صنعهما زميلك لهما الرقعة نفسها تقريبا، وإلا فلن يعرف جت عندما يعيد التأسيس أنهما متطابقين وسيضيف رقعة أخرى شبيهة بالإيداع C4 (والتي غالبا ستفشل في أن تُطبَّق بنظافة، لأن تعديلاتها ستكون مطبقة بالفعل ولو جزئيا).

ويمكنك أن تختصر هذا باستعمال خيار إعادة التأسيس --rebase مع أمر الجذب، أي تنفيذ git pull --rebase بدلا من git pull المجرد. أو يمكنك فعل ذلك يدويا بالاستحضار git fetch ثم إعادة التأسيس، أي تنفيذ git rebase teamone/master في هذه الحالة.

وإذا كنت تستخدم git pull وتريد جعل خيار --rebase خيارًا مفترضًا دومًا، يمكنك تفعيل قيمة التهيئة pull.rebase بأمر مثل git config --global pull.rebase true.

إذا كنت أبدا لا تعيد تأسيس إلا الإيداعات التي لم تغادر حاسوبك، فستكون بخير. وإن كنت تعيد تأسيس إيداعات قد دفعتها، ولكن لم يبنِ عليها أحدٌ آخر إيداعاتٍ، فستكون بخير أيضا. أما إن كنت تعيد تأسيس إيداعات قد دفعتها إلى العالَم وربما بنى عليها الناس أعمالا، فقد تجد نفسك في ورطة مُغيظة منهِكة، ثم ازدراء زملائك لك.

إن وجدت أنت أو زميلٌ لك أن ذلك ضروري يومًا ما، تأكد أن الجميع يعرفون استخدام git pull --rebase، لكي تحاول جعل معاناة ما بعد الحادثة أقل سوءًا ولو قليلا.

بين إعادة التأسيس والدمج

الآن وقد رأيت إعادة التأسيس والدمج عمليًّا، قد تتساءل أيهما أفضل. قبل أن نستطيع الإجابة عن هذا السؤال، لنرجع إلى الوراء قليلا ونتحدث عن معنى التاريخ.

إحدى وجهتَي النظر أن تاريخ إيداعات مستودعك هي سجل لما حدث فعلا. أي أنها وثيقة تاريخية، ولها قيمة في ذاتها، ويجب ألا يُعبث بها. فمن هذا المنظور، يكاد يُعدّ تغيير تاريخ الإيداعات استهزاءً ومسبّة، لأنك تكذب بشأن ما حدث فعلا. إذًا ماذا لو كانت لدينا فوضى من إيداعات الدمج؟ هكذا جرت الأمور، ويجب أن يحتفظ بها المستودع من أجل الأجيال القادمة.

وجهة النظر المقابلة هي أن تاريخ الإيداعات هو قصة صناعة مشروعك. ولأنك لا تنشر المسودة الأولى من ك‍تاب، فلم إذًا تنشر عملًا أشعث أغبر؟ ففي أثناء عملك على مشروع، قد تحتاج سجلا لجميع عثراتك وطرقك المسدودة. ولكن عندما يحين وقت إظهار عملك إلى العالم، فقد تود أن تحكي قصةً متماسكة عن كيفية الوصول من «أ» إلى «ب». ولذلك يستخدم أصحاب هذا المذهب أدوات مثل إعادة التأسيس وتصفية الفروع لإعادة ك‍تابة إيداعاتهم قبل دمجها في الفرع الرئيس، يستخدمون rebase و filter-branch ليحكون القصة بالطريقة الأنسب للقراء في المستقبل.

لنعد الآن إلى السؤال عن التفضيل بين الدمج وإعادة التأسيس: لعلك وجدت أنه أعقد من أن تكون له إجابة يسيرة. فإن جت أداةٌ قوية، ويتيح لك فعل الكثير بتاريخ مستودعك، ولكن كل فريق وكل مشروع هو حالة خاصة. الآن وقد علمت الأسلوبين وطريقة عملهما، عليك أن تقرر بنفسك ما الأنسب لحالتك الخاصة.

ويمكنك الجمع بين ميزات كليهما: أعد تأسيس التعديلات المحلية قبل دفعها حتى تنظف عملك، ولكن لا تعد أبدًا تأسيس أي شيء دفعته إلى مستودعٍ ما.

الخلاصة

تناولنا أسس التفريع والدمج في جت. ينبغي أن يَسهُل عليك الآن إنشاء فروع جديدة والانتقال إليها والانتقال بين الفروع ودمج فروع محلية معًا. ستقدر أيضا على مشاركة فروعك بدفعها إلى مستودع مشترك، وعلى العمل مع آخرين على فروع مشتركة، وعلى إعادة تأسيس فروعك قبل مشاركتها. التالي: سنتحدث عما تحتاج لتشغيل خادومك الخاص بك لاستضافة مستودعات جت.