← العودة إلى Git و GitHub
NTF Hub / Git & GitHub

سيناريو عملي: فريق برمجي موزع حول العالم

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

Team Workflow Branches Merge Conflict Remote Stash Cherry-pick

الفكرة العامة

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

تخيل أن المشروع موقع حجوزات سياحية باسم NTF Travel. المدير في السعودية، مطور الواجهات الخلفية للصفحات في أمريكا، ومصممة الواجهة في إيطاليا. لا يمكن أن يرسل كل شخص ملفًا مضغوطًا للآخرين كل يوم؛ هذه طريقة فوضوية ستنتهي بملفات مثل:

project-final.zip project-final-2.zip project-new-final.zip project-last-version-real.zip

بدل ذلك سيستخدم الفريق Git لتسجيل التاريخ، وGitHub لمشاركة المشروع، والفروع حتى يعمل كل شخص دون أن يكسر عمل الآخرين.

أعضاء الفريق

👨‍💼

عبدالله

الخرج، السعودية

يقود المشروع من مكتبه في الخرج. بجانبه قهوة عربية وجهازه مفتوح على GitHub وNetlify. عبدالله مسؤول عن إنشاء المستودع، مراجعة التعديلات، ودمج الفروع المهمة داخل main.

👨‍💻

Ethan Walker

Seattle, USA

يعمل من مساحة عمل قريبة من وسط المدينة. مهمته بناء صفحة الرحلات وصفحة الحجز، ويركز على HTML وJavaScript وتنظيم بيانات الرحلات.

👩‍💻

Giulia Rossi

Florence, Italy

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

بداية المشروع على جهاز عبدالله

يبدأ عبدالله بإنشاء مجلد المشروع. في هذه اللحظة لا يوجد Git ولا يوجد تاريخ للتغييرات. مجرد مجلد عادي.

mkdir ntf-travel cd ntf-travel

بعد ذلك يحوّل المجلد إلى مستودع Git:

git init

الآن أنشأ Git مجلدًا مخفيًا باسم .git. هذا المجلد هو عقل المستودع؛ فيه يحفظ Git معلومات الفروع، تاريخ الـ commits، وموقع HEAD.

ينشئ عبدالله الملفات الأولى:

ntf-travel/ ├── index.html ├── style.css └── script.js

ثم يسأل Git عن حالة المشروع:

git status

سيخبره Git أن الملفات غير متتبعة. معنى ذلك أنها موجودة في Working Directory لكن لم تدخل بعد في منطقة التجهيز.

إدخال الملفات إلى Staging Area

git add .

الأمر git add . لا ينشئ Commit. هو فقط يقول: “هذه الملفات جاهزة للدخول في اللقطة القادمة”.

أول Commit

git commit -m "Initial project structure"

الآن أصبح لدى المشروع نقطة حفظ واضحة. يمكن للفريق الرجوع إليها أو مقارنة التعديلات اللاحقة بها.

A Initial project structure ↑ main / HEAD
الفكرة المهمة: Working Directory هو مكان العمل الحالي، Staging Area هي قائمة ما سيُحفظ، وCommit هو اللقطة المحفوظة فعلًا.

رفع المشروع إلى GitHub

إلى الآن المشروع موجود فقط على جهاز عبدالله. لو تعطل جهازه أو أراد Ethan وGiulia المشاركة، فلن يستطيعوا الوصول للمشروع. لذلك ينشئ عبدالله مستودعًا بعيدًا على GitHub.

يربط المستودع المحلي بالمستودع البعيد:

git remote add origin https://github.com/ntf-travel/project.git

يتأكد من الرابط:

git remote -v

ثم يرفع الفرع الرئيسي لأول مرة:

git push -u origin main

الخيار -u يربط الفرع المحلي main بالفرع البعيد origin/main، وبعدها يستطيع عبدالله استخدام git push مباشرة في المرات القادمة.

جهاز عبدالله Local main │ │ git push -u origin main ▼ GitHub origin/main

انضمام Ethan وGiulia إلى المشروع

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

git clone https://github.com/ntf-travel/project.git

الأمر git clone ينزل الملفات وتاريخ Git والرابط البعيد معًا. لذلك يصبح لدى كل عضو نسخة محلية كاملة، وليست مجرد نسخة من الملفات الحالية.

لماذا هذا مهم؟ لأن Ethan يستطيع العمل في أمريكا دون اتصال دائم بعبدالله، وGiulia تستطيع إنشاء commits على جهازها ثم رفعها لاحقًا.

الفروع والعمل المتوازي

طلب عبدالله من Ethan إنشاء صفحة الرحلات. الخطأ الشائع أن يعمل Ethan مباشرة على main. الأفضل أن ينشئ فرعًا مستقلًا للميزة الجديدة.

git switch -c flights-page

هذا الأمر ينشئ فرعًا جديدًا باسم flights-page وينتقل إليه مباشرة.

A main \ B flights-page

في الوقت نفسه تعمل Giulia على صفحة تسجيل الدخول:

git switch -c login-page
A main ├── B flights-page └── C login-page

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

العمل داخل فرع Ethan

أضاف Ethan الملفات التالية:

flights.html booking.html assets/trips.json
git status git add . git commit -m "Add flights and booking pages"

ثم يرفع الفرع إلى GitHub:

git push origin flights-page

المراجعة والدمج

يفتح عبدالله GitHub ويراجع التغييرات. بعد التأكد أن صفحة الرحلات لا تكسر المشروع، يدمج العمل داخل الفرع الرئيسي. يمكن الدمج من GitHub، أو محليًا بهذا الشكل:

git switch main git pull git merge flights-page

قبل الدمج نفذ عبدالله git pull حتى يتأكد أن نسخة main عنده محدثة.

قبل الدمج: A──B main \ C──D flights-page بعد الدمج: A──B────M main \ / C──D flights-page

إذا كان main لم يتحرك منذ إنشاء الفرع، قد يحدث Fast-forward merge، أي أن Git يقدّم مؤشر main للأمام فقط. أما إذا كان هناك عمل آخر على main، فقد ينشئ Git Merge Commit.

سيناريو التعارض: من تسبب فيه؟

في اليوم التالي عدّل عبدالله زر الحجز في style.css ليكون ذهبيًا، لأن هوية NTF Hub تعتمد على اللون الذهبي.

.btn-book{ background: gold; color: #0b1220; }

في الوقت نفسه، كانت Giulia تعمل على فرع login-page وعدّلت نفس السطر تقريبًا ليكون أزرقًا لأنها كانت تختبر نمطًا جديدًا.

.btn-book{ background: #2563eb; color: white; }

المشكلة ليست أن أحدهما أخطأ. التعارض حدث لأن الاثنين عدلا نفس المكان في الملف نفسه، وGit لا يستطيع أن يقرر وحده أي نية هي الصحيحة.

عندما حاولت Giulia تحديث فرعها ظهر التعارض:

git pull origin main
CONFLICT (content): Merge conflict in style.css Automatic merge failed; fix conflicts and then commit the result.

فتحوا الملف فوجدوا علامات التعارض:

<<<<<<< HEAD .btn-book{ background: #2563eb; color: white; } ======= .btn-book{ background: gold; color: #0b1220; } >>>>>>> origin/main
من تسبب في التعارض؟ ليس بالضرورة شخص واحد. التعارض نتيجة عمل متزامن على نفس الموضع. لكن السبب الإداري هنا أن الفريق لم يتفق مسبقًا على تصميم الزر.

كيف عالجوا المشكلة؟

ناقش عبدالله وGiulia الهوية البصرية، وقرروا استخدام لون ذهبي أهدأ مع ظل مناسب:

.btn-book{ background:#d4af37; color:#0b1220; box-shadow:0 10px 24px rgba(212,175,55,.22); }

بعد إزالة علامات التعارض وحفظ الملف:

git add style.css git commit -m "Resolve booking button style conflict"

الآن صار حل التعارض جزءًا من تاريخ المشروع، ويمكن لأي عضو رؤية متى حدث ولماذا.

استخدام Stash أثناء العمل غير المكتمل

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

الحل هو استخدام Stash:

git stash

هذا الأمر يخفي التعديلات مؤقتًا ويعيد Working Directory إلى حالة نظيفة.

git pull origin main

بعد جلب التحديثات تعيد Giulia تعديلاتها:

git stash pop

لو حدث تعارض بعد stash pop يتم حله مثل أي تعارض عادي.

متى لا تستخدم Stash؟ لا تجعله مكانًا دائمًا لحفظ العمل. إذا كان العمل أصبح واضحًا ومهمًا، الأفضل أن تحفظه في Commit داخل فرع مناسب.

الفرق بين Fetch و Pull داخل القصة

قبل الدمج النهائي، أراد عبدالله أن يعرف ما الذي تغيّر على GitHub دون أن يدمج شيئًا مباشرة في جهازه.

git fetch

هذا الأمر يجلب معلومات التحديثات من GitHub لكنه لا يدمجها في الفرع الحالي.

git diff main origin/main

هنا يقارن عبدالله بين الفرع المحلي والفرع البعيد. إذا كان كل شيء مناسبًا، يمكنه تنفيذ:

git pull

git pull عمليًا تعني: Fetch ثم دمج أو تطبيق التحديثات على الفرع الحالي.

git fetch يجلب التحديثات فقط git pull يجلب التحديثات + يطبقها على الفرع الحالي

التراجع عن الأخطاء: Revert أم Reset؟

رفع Ethan بالخطأ Commit يحتوي ملفًا تجريبيًا باسم test-data.json إلى فرع تمت مشاركته مع الفريق. هنا يجب الانتباه: إذا وصل الـ Commit إلى GitHub وشاهده الآخرون، فالأكثر أمانًا غالبًا هو revert.

git revert COMMIT_ID

هذا لا يمسح التاريخ القديم، بل ينشئ Commit جديدًا يعكس أثر الـ Commit الخاطئ. لذلك هو آمن في العمل الجماعي.

أما reset فيستخدم غالبًا قبل مشاركة التعديل، أو في حالات يعرف الفريق أثرها جيدًا:

git reset --soft HEAD~1

هذا يرجع خطوة واحدة مع إبقاء التعديلات في Staging Area.

git reset --mixed HEAD~1

هذا يرجع خطوة واحدة ويخرج التعديلات من Staging Area لكنها تبقى في الملفات.

git reset --hard HEAD~1

هذا أخطر خيار لأنه يرجع التاريخ ويحذف التعديلات من الملفات. لا تستخدمه على عمل مهم دون فهم كامل.

قاعدة عملية: في الفروع المشتركة استخدم revert غالبًا. في العمل المحلي قبل الرفع يمكن استخدام reset بحذر.

Cherry-pick: أخذ تعديل واحد فقط

أنشأ Ethan فرعًا تجريبيًا اسمه experimental-search. كان الفرع يحتوي أفكارًا كثيرة غير جاهزة، لكن فيه Commit واحد ممتاز يحسن طريقة البحث عن الرحلات.

عبدالله لا يريد دمج الفرع كاملًا. يريد Commit واحدًا فقط.

git log --oneline
a91c2d4 Improve trip search filtering b82aa19 Try experimental layout c77fd10 Add temporary test cards

يأخذ عبدالله الـ Commit المطلوب فقط:

git switch main git cherry-pick a91c2d4

الآن انتقل أثر ذلك الـ Commit إلى main دون نقل بقية التجارب غير الجاهزة.

الفكرة: merge يدمج مسارًا كاملًا من العمل، أما cherry-pick فيلتقط Commit محددًا وينسخه إلى الفرع الحالي.

يوم الإطلاق

قبل الإطلاق، يتأكد عبدالله أن المشروع نظيف:

git status

ثم يعرض التاريخ بشكل مختصر:

git log --oneline --graph --all
* 91a7c20 Release first public version * 7df31b2 Add offers page * 4b18a50 Resolve booking button style conflict * c04a871 Add login page * b71d820 Add flights and booking pages * a10f1e2 Initial project structure

يرفع عبدالله آخر نسخة:

git push origin main

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

Local main ↓ git push GitHub origin/main ↓ Build ↓ Deployment
النتيجة: لم يعد المشروع مجموعة ملفات عشوائية. أصبح له تاريخ واضح، فريق يعمل بتنسيق، وفروع يمكن مراجعتها، وتحديثات تصل للنشر بثقة.

ماذا تعلمنا من السيناريو؟

هذا السيناريو جمع معظم أوامر Git التي يحتاجها الفريق في مشروع حقيقي:

1

إنشاء المشروع

استخدم عبدالله git init ثم add وcommit لحفظ أول نسخة.

2

ربط GitHub

استخدم remote وpush -u لنقل المشروع إلى مستودع بعيد.

3

انضمام الفريق

استخدم Ethan وGiulia أمر clone للحصول على المشروع كاملًا.

4

العمل بالفروع

استخدم الفريق branch وswitch لعزل كل ميزة عن main.

5

الدمج والتعارض

استخدموا merge وحلوا conflict في style.css.

6

الإدارة اليومية

استخدموا fetch وpull وstash وrevert وreset وcherry-pick حسب الحاجة.

أسئلة مراجعة

1. لماذا أنشأ Ethan فرعًا بدل العمل على main مباشرة؟

حتى يطوّر الميزة دون التأثير على النسخة الرئيسية المستقرة.

2. لماذا حدث التعارض في style.css؟

لأن عبدالله وGiulia عدلا السطر نفسه تقريبًا في الملف نفسه.

3. ما الفرق بين fetch وpull؟

fetch يجلب التحديثات فقط، أما pull فيجلبها ويطبقها على الفرع الحالي.

4. متى يكون revert أفضل من reset؟

عندما يكون الـ Commit منشورًا أو مشتركًا مع الفريق.

5. لماذا استخدم عبدالله cherry-pick؟

لأنه احتاج Commit واحدًا من فرع تجريبي دون دمج كل الفرع.

🧪 طبّق السيناريو في محاكي Git

افتح المحاكي وجرّب إنشاء الفروع، الدمج، التعارض، stash، pull، وpush بنفسك.