كن مطوّر واجهات أمامية محترفا
12 دقيقة قراءة

Git وGitHub: التحكّم في الإصدارات من الصفر إلى الاحتراف

دليل عملي كامل لـ Git وGitHub — المناطق الثلاث، والتهيئة والالتزام، وفحص التاريخ، والفروع والدمج مقابل إعادة الأساس، والتعارضات، والتراجع عن أي شيء (amend/restore/reset/revert)، والتخبئة، وإعادة الأساس التفاعلية، وcherry-pick، والوسوم، والاسترداد بـ reflog وbisect، والريموت والدفع القسري الآمن، واستراتيجيات التفرّع، وسير عمل GitHub الكامل (المراجعات، مسودّات PR، الفروع المحميّة، Actions) — مع تمارين عملية وحلولها.

Git هو الأداة التي يستخدمها كل مطوّر كل يوم، والتي يتعلّم معظم الناس ما يكفي منها للتدبّر — حتى يأتي يوم سيّئ بتغيير ضائع أو فرع متشابك. فهم النموذج تحت الأوامر يحوّل Git من مجموعة تعويذات مخيفة إلى شيء تستطيع التفكير فيه، والأوامر "المتقدّمة" (rebase، reset، reflog، bisect) تتوقّف عن كونها مرهبة حالما يرسخ النموذج. هذه هي العُدّة كاملة، من أول التزام إلى استرداد عمل ظننته ضاع. (يقترن طبيعيًّا بـ وحدات ES والأدوات، لأن package.json ومستودعك يعيشان معًا.)

نموذج Git كلّه لقطات عبر الزمن. الالتزام لقطة غير قابلة للتغيير لمشروعك مع مؤشّر إلى أبيه؛ والفرع مجرّد مؤشّر متحرّك إلى إحدى تلك اللقطات، وHEAD يشير إلى مكانك الآن. كل أمر تقريبًا — merge، rebase، reset، revert، cherry-pick — هو تحريك أو نسخ لتلك المؤشّرات واللقطات. وحالما ترى ذلك، لا شيء في Git سحر.

ما هو التحكّم في الإصدارات

التحكّم في الإصدارات يسجّل تاريخ مشروعك — كل تغيير، ومن أجراه، ولماذا — كي تراجعه، وتتراجع عن الأخطاء، وتتعاون دون أن يطمس أحدكم عمل الآخر. Git هو نظام التحكّم في الإصدارات (يعمل محليًّا، دون اتصال بالكامل)؛ وGitHub خدمة تستضيف مستودعات Git على الإنترنت للمشاركة والتعاون. مترابطان لكنهما متمايزان: Git هو المحرّك، وGitHub مكان واحد لركن نسخة.

المناطق الثلاث

هذا النموذج الذهني الذي يجعل Git مفهومًا. يتحرّك الملفّ عبر ثلاثة أماكن:

  • دليل العمل (working directory) — ملفّاتك الفعلية، حيث تحرّر.
  • منطقة التهيئة (staging/index) — منطقة احتجاز للتغييرات التي تريدها في التزامك التالي.
  • المستودع (repository) — التاريخ الملتزَم، اللقطات الدائمة.
git init              # ابدأ تتبّع مشروع (ينشئ المستودع)
# ...حرّر الملفّات في دليل العمل...
git add file.js       # هيّئ تغييرًا (دليل العمل → التهيئة)
git commit -m "msg"   # سجّل التغييرات المهيّأة (التهيئة → المستودع)

خطوتا add ثم commit مقصودتان: التهيئة تتيح صياغة التزام من التغييرات التي تختارها بالضبط. وتستطيع حتى تهيئة جزء من ملفّ:

git add -p            # راجع كل قطعة تغيير وهيّئها y/n — لصياغة التزامات نظيفة

التهيئة والالتزام

الحلقة اليومية. افحص ما تغيّر، هيّئه، التزمه برسالة واضحة:

git status            # ما تغيّر، ما هُيّئ
git diff              # تغييرات الأسطر غير المهيّأة
git diff --staged     # ما هُيّئ (على وشك الالتزام)
git add .             # هيّئ كل ما تغيّر
git commit -m "Add login validation"

الالتزام الجيّد صغير ومركّز، برسالة تقول لماذا، بصيغة الأمر ("Fix overflow bug"، لا "fixed stuff"). التزِم كثيرًا — كل التزام نقطة حفظ تستطيع العودة إليها.

فحص التاريخ

قراءة التاريخ جيّدًا نصف استخدام Git. أوامر السجلّ والفحص:

git log --oneline --graph --all   # رسم فروع مدمج ومرئيّ
git log -p file.js                # فرق كامل لكل تغيير على ملفّ
git log --author="Ada" --since=2.weeks
git show <commit>                 # ماذا غيّر التزام محدّد
git diff main..feature            # الفرق بين فرعين
git blame file.js                 # من غيّر كل سطر آخر مرّة (وفي أي التزام)

git blame + git show هما تحقيق "لماذا هذا السطر هنا؟" المعياريّ: blame يجد الالتزام، وshow يكشف التغيير ورسالته.

الفروع

الفرع (branch) مؤشّر متحرّك إلى خطّ من الالتزامات — يتيح العمل على ميزة دون إزعاج الشيفرة الرئيسية:

git branch                    # اسرد الفروع
git switch -c feature/login   # أنشئ وانتقل إلى فرع جديد
# ...التزِم العمل على الفرع...
git switch main               # عُد إلى main
git branch -d feature/login   # احذف فرعًا مدموجًا

التفرّع رخيص وفوريّ في Git (الفرع مجرّد مؤشّر 40 بايت)، ولهذا سير العمل المعتاد فرع لكل ميزة أو إصلاح — يبقى main مستقرًّا، ويحدث العمل على الفروع.

الدمج مقابل إعادة الأساس

طريقتان لجمع عمل فرع مع آخر. تُنتجان نفس الملفّات لكن تاريخًا مختلفًا:

# الدمج (MERGE) — يصل التاريخين بالتزام دمج؛ يحفظ شكل الفرع
git switch main
git merge feature/login

# إعادة الأساس (REBASE) — يعيد تشغيل التزامات فرعك فوق main؛ تاريخ خطّيّ، بلا التزام دمج
git switch feature/login
git rebase main               # الآن يجلس feature بنظافة على أحدث main

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

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

حين يغيّر فرعان الأسطر نفسها، لا يستطيع Git القرار تلقائيًّا ويبلّغ عن تعارض، معلّمًا الموضع في الملفّ:

<<<<<<< HEAD
const timeout = 3000;
=======
const timeout = 5000;
>>>>>>> feature/login

حرّر الملفّ إلى النسخة التي تريدها (مزيلًا علامات <<</===/>>>)، ثم تابِع:

git add file.js       # علّمه محلولًا
git commit            # يُكمِل الدمج
# (أثناء rebase، استخدم `git rebase --continue` بدلًا منه؛ و`git rebase --abort` للتراجع)

التعارضات تبدو مخيفة لكنها روتينية — تعني فقط أن شخصين لمسا الأسطر نفسها، وGit يطلب منك الاختيار. وgit merge --abort يتراجع بنظافة إن فضّلت البدء من جديد.

التراجع عن أي شيء تقريبًا

من هنا تأتي الثقة — معرفة أنك تستطيع عكس أي خطوة:

# أصلِح الالتزام الأخير (الرسالة أو ملفّ منسيّ) — قبل الدفع
git commit --amend

# تجاهل تغييرات ملفّ في دليل العمل
git restore file.js

# ألغِ تهيئة ملفّ (أبقِ التعديلات، أزله من التهيئة فقط)
git restore --staged file.js

# حرّك مؤشّر الفرع للخلف، مُبقيًا التغييرات...
git reset --soft HEAD~1     # ...مهيّأة
git reset HEAD~1            # ...غير مهيّأة (الافتراضي: --mixed)
git reset --hard HEAD~1     # ...مُلغاة كليًّا (مدمّر)

# تراجع عن التزام مدفوع بأمان — يصنع التزامًا جديدًا يعكسه
git revert <commit>

التمييز المفتاحي: reset يعيد كتابة التاريخ (استخدمه على التزامات محلّية غير مدفوعة)، بينما revert يضيف تاريخًا (استخدمه على الالتزامات المشتركة/المدفوعة — لا يكسر نسخة أحد). وreset --hard يرمي العمل، فالجأ إليه عمدًا.

التخبئة (Stash)

تحتاج تبديل فرع في منتصف تغيير لكنك لست جاهزًا للالتزام؟ التخبئة تركن عملك الجاري وتستعيد شجرة نظيفة:

git stash              # اركن التغييرات غير الملتزَمة
git switch main        # افعل الأمر العاجل
git stash pop          # أعد تغييراتك
git stash list         # اعرض كل المخابئ

التخبئة جواب "أحتاج فحص شيء بسرعة على فرع آخر لكن عملي ليس جاهزًا للالتزام."

إعادة كتابة التاريخ: إعادة الأساس التفاعلية

قبل مشاركة فرع، تستطيع تنظيفه — دمج التزامات "WIP"، أو إعادة صياغة الرسائل، أو إعادة الترتيب، أو إسقاط التزامات:

git rebase -i HEAD~3   # حرّر آخر 3 التزامات تفاعليًّا

تحصل على قائمة يمكن لكل سطر فيها أن يكون pick أو reword أو squash (طيّ في السابق) أو fixup أو edit أو drop. دمج ثلاثة التزامات فوضوية "fix typo / wip / actually fix it" في التزام نظيف واحد هو الاستخدام الأشيع. ومجدّدًا: أعد كتابة التاريخ غير المدفوع فقط.

Cherry-pick

خذ التزامًا واحدًا من فرع آخر دون دمج الكلّ:

git cherry-pick <commit>   # طبّق ذلك الالتزام فقط على الفرع الحالي

مفيد لنقل إصلاح عاجل إلى فرع إصدار، أو سحب التزام منتهٍ واحد من فرع ليس جاهزًا للدمج.

الوسوم والإصدارات

الوسم (tag) يعلّم التزامًا محدّدًا بشكل دائم — يُستخدَم لإصدارات النسخ:

git tag -a v1.2.0 -m "Release 1.2.0"   # وسم موسوم على الالتزام الحالي
git push origin v1.2.0                  # الوسوم لا تُدفَع افتراضيًّا
git push origin --tags                  # ادفع كل الوسوم

على عكس الفروع، الوسوم لا تتحرّك. وعلى GitHub، يمكن أن يصير الوسم Release بملاحظات وأصول قابلة للتنزيل.

الاسترداد: reflog وbisect

أمران يحوّلان "فقدتُ عملي" و"ما الذي كسر هذا؟" إلى مشاكل محلولة.

git reflog يسجّل كل مكان كان فيه HEAD — حتى الالتزامات التي "فقدتها" بـ reset سيّئ أو حذف فرع ما زالت هناك لفترة:

git reflog                 # اسرد مواضع HEAD الأخيرة بهاشاتها
git reset --hard <hash>    # اقفز عائدًا إلى التزام "ضائع"

هذه شبكة الأمان خلف reset --hard: ما دام التزام قد وُجد، يستطيع reflog عادةً استرجاعه.

git bisect يبحث ثنائيًّا في تاريخك ليجد الالتزام الذي أدخل علّة:

git bisect start
git bisect bad             # الالتزام الحالي معطوب
git bisect good v1.0       # هذه النسخة القديمة عملت
# يسحب Git المنتصف؛ تختبر وتعلّم good/bad؛ كرّر حتى يُعثَر عليه
git bisect reset

bisect يجد المذنب في log₂(n) خطوة — عشرات الاختبارات عبر آلاف الالتزامات.

.gitignore

بعض الملفّات لا ينبغي تتبّعها — التبعيات، ومخرجات البناء، والأسرار، ونفايات نظام التشغيل:

node_modules/
dist/
.env
.DS_Store
*.log

لا تلتزم أبدًا node_modules/ (قابل لإعادة التثبيت من package.json) أو ملفّات .env (تحمل أسرارًا). أضف قواعد التجاهل قبل أول التزام حيثما أمكن — حالما يُتتبَّع ملفّ، لن يُلغي .gitignore تتبّعه (استخدم git rm --cached file لذلك).

الريموت: Push وPull وFetch

الريموت نسخة من مستودعك مستضافة في مكان آخر (عادةً GitHub):

git clone <url>                # انسخ مستودعًا بعيدًا محليًّا
git remote add origin <url>    # اربط مستودعًا محليًّا بريموت
git push -u origin main        # ادفع وأعدّ التتبّع (فلاحقًا فقط `git push`)
git fetch                      # نزّل التغييرات البعيدة دون دمج
git pull                       # fetch + merge في خطوة واحدة

origin هو الاسم الاصطلاحي لريموتك الرئيسي. فضّل git fetch ثم المراجعة، على git pull الأعمى، حين تريد رؤية الوارد أولًا. وحين تكون قد أعدت الأساس ويجب تحديث فرع مدفوع، استخدم القوّة الآمنة:

git push --force-with-lease    # يرفض إن دفع شخص آخر منذ آخر جلب لك

--force-with-lease هو الدفع القسري المسؤول — لا يطمس عمل زميل بصمت كما قد يفعل --force المجرّد.

استراتيجيات التفرّع

كيف ينظّم فريق فروعه:

  • التفرّع بالميزات (feature branching) — فرع قصير العمر لكل ميزة/إصلاح، يُدمَج عبر PR. الافتراضي الشائع.
  • التطوير القائم على الجذع (trunk-based) — الجميع يدمج في main بتواتر خلف PRs صغيرة وأعلام ميزات؛ يقلّل التباعد طويل العمر. مفضّل لدى الفرق السريعة وأماكن CI الكثيف.
  • GitFlow — فروع develop وrelease وhotfix طويلة العمر؛ أثقل، يناسب الإصدارات المجدولة والمنتجات المُنسَّخة.

معظم فرق الويب الحديثة تستخدم فروع ميزات قصيرة العمر أو trunk-based — أبقِ الفروع صغيرة وادمج كثيرًا لتجنّب التعارضات المؤلمة.

سير عمل GitHub

هكذا تتعاون الفرق (والمصادر المفتوحة):

  1. تفرّع عن main (git switch -c fix/typo)، التزِم، وادفع بـ -u.
  2. افتح طلب سحب (PR) — اقتراح لدمج فرعك، حيث يُراجَع الفرق.
  3. يعلّق المراجعون، أو يطلبون تغييرات، أو يوافقون؛ وتعمل فحوص CI (GitHub Actions) الاختبارات/التدقيق تلقائيًّا على الـ PR.
  4. حالما يُوافَق ويصير أخضر، ادمج (غالبًا squash-merge لتاريخ main نظيف)؛ ويُحذَف الفرع.

أمور تستحقّ المعرفة فوق ذلك:

  • مسودّات PR — افتحها مبكرًا لمشاركة عمل جارٍ دون طلب مراجعة بعد.
  • Fork + PR — للمستودعات التي لا تستطيع الكتابة فيها: تشعّب نسختك، ادفع هناك، افتح PR من التشعّب. سير مساهمة المصادر المفتوحة.
  • الفروع المحميّة — يمكن أن يتطلّب main طلبات سحب وفحوصًا ناجحة وموافقات قبل الدمج — لا دفع مباشر.
  • القضايا (Issues) — تتبّع العلل والمهام؛ أشِر إليها في الالتزامات/PRs (Fixes #42 يغلق القضية تلقائيًّا عند الدمج).
  • GitHub Actions — CI/CD: شغّل الاختبارات على كل PR، وانشر عند الدمج في main.

الـ PR قلب التعاون — حيث تحدث المراجعة والنقاش والفحوص الآلية قبل وصول الشيفرة إلى main.

الأخطاء الشائعة

  • التزام node_modules/ أو مخرجات البناء أو أسرار .env — أضف .gitignore أولًا.
  • رسائل التزام غامضة ("update"، "fix") لا تقول شيئًا عن لماذا.
  • التزامات ضخمة تخلط تغييرات غير مترابطة — أبقِها صغيرة ومركّزة (استخدم git add -p).
  • العمل مباشرةً على main بدل فرع.
  • إعادة أساس أو amend لالتزامات دُفعت سلفًا وشُورِكت — يعيد كتابة التاريخ ويتباعد عن نسخة الجميع.
  • اللجوء إلى reset --hard بينما تقصد revert (أو العكس) — reset يعيد الكتابة، وrevert يضيف.
  • git push --force المجرّد إلى فرع مشترك — استخدم --force-with-lease.
  • الهلع بعد reset سيّئ أو فرع محذوف — تفقّد git reflog أولًا؛ الالتزام عادةً قابل للاسترداد.
  • الخلط بين Git (الأداة المحلّية) وGitHub (المضيف) — Git يعمل دون اتصال بالكامل.

تمارين

جرّب كلًّا منها قبل فتح الحل.

تمرين 1 — أصلِح الالتزام الأخير

التزمت لكنك نسيت تضمين styles.css وتريد رسالة أفضل — ولم تدفع بعد.

إظهار الحل
git add styles.css
git commit --amend -m "Add login form with styles"

--amend يستبدل الالتزام السابق بآخر جديد يتضمّن الملفّ المهيّأ والرسالة الجديدة. آمن لأنك لم تدفع — تعديل الالتزامات المشتركة يعيد كتابة التاريخ.

تمرين 2 — تراجع عن التزام مدفوع

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

إظهار الحل
git revert <commit>
git push

revert يصنع التزامًا جديدًا يلغي التغييرات، فيبقى تاريخ الجميع متّسقًا. (reset سيعيد كتابة التاريخ ويكسر نسخ المتعاونين — الأداة الخطأ لالتزام مدفوع.)

تمرين 3 — ادمج التزامات فوضوية

فرعك فيه ثلاثة التزامات: "wip" و"fix typo" و"actually works". اجمعها في واحد قبل فتح PR.

إظهار الحل
git rebase -i HEAD~3
# في المحرّر: أبقِ الأول `pick`، غيّر الآخرين إلى `squash` (أو `fixup`)
# ثم اكتب رسالة التزام نظيفة واحدة

إعادة الأساس التفاعلية تطوي الثلاثة في التزام مرتّب واحد. مقبول هنا لأن الفرع لم يُدمَج/يُشارَك بعد.

تمرين 4 — استردّ التزامًا "ضائعًا"

شغّلت git reset --hard HEAD~1 وأدركت أنك كنت تحتاج ذلك الالتزام. أعِده.

إظهار الحل
git reflog                  # جِد هاش الالتزام قبل الـ reset
git reset --hard <hash>     # أو: git cherry-pick <hash> لجلبه وحده

reflog يسرد كل موضع HEAD أخير، شاملًا الذي أعدت ضبطه عنه — الالتزام ليس ضائعًا، فقط غير مُشار إليه.

تمرين 5 — بدّل الفروع في منتصف تغيير

أنت في منتصف التحرير وزميل يحتاج إصلاحًا عاجلًا على main. اركن عملك، أصلِح main، عُد.

إظهار الحل
git stash               # اركن تعديلاتك الجارية
git switch main
# ...اصنع والتزم الإصلاح العاجل...
git switch feature/x
git stash pop           # استعد تعديلاتك

stash ينظّف شجرة عملك دون التزام عمل نصف منجَز، ثم pop يستعيده بالضبط حين تعود.

النموذج الذهني الذي تحتفظ به

Git لقطات عبر الزمن: كل التزام لقطة غير قابلة للتغيير تشير إلى أبيها، والفرع مؤشّر متحرّك، ومعظم الأوامر تحرّك أو تنسخ تلك المؤشّرات. تتدفّق الملفّات دليل العمل → التهيئة (add) → المستودع (commit). ابنِ العادة اليومية — فرع لكل ميزة، التزِم صغيرًا، ادمج أو أعِد الأساس عائدًا — واتّكئ على البقية بالفئة: افحص بـ log/show/blame/bisect؛ تراجَع بـ amend/restore/reset (تعيد كتابة التاريخ — محليّ فقط) مقابل revert (يضيف تاريخًا — آمن للمدفوع)؛ أعِد كتابة الفروع المحلّية بـ rebase التفاعلي؛ استردّ بـ reflog. تعاون عبر الريموت (fetch/pull/push، والقوّة فقط بـ --force-with-lease) وسير عمل PR في GitHub (المراجعة + CI قبل الدمج). القاعدة الواحدة التي تمنع معظم الكوارث: لا تعد كتابة تاريخ دُفع وشُورِك سلفًا. تمسّك بذلك، ويتوقّف Git عن كونه مصدر رهبة ويصير شبكة الأمان التي تتيح لك العمل بلا خوف.