ما هي الخطافات؟
الخطاف (Hook) سكربت يشغّله Git تلقائيًّا عند حدث معيّن: قبل الـ commit، بعد كتابة رسالته، قبل الرفع، بعد الدمج. الفكرة: الأشياء التي "يجب أن تتذكّر فعلها" (تشغيل المدقّق، تنسيق الرسالة، عدم الرفع لـ main) تحدث وحدها، فلا تعتمد على ذاكرة أحد.
الخطافات ملفات في .git/hooks/ تحمل اسم الحدث بالضبط، بلا امتداد:
| الخطاف | متى | فشله (exit غير 0) |
|---|---|---|
pre-commit | قبل إنشاء الـ commit | يلغي الـ commit |
commit-msg | بعد كتابة الرسالة، ويستلم مسار ملفها | يلغي الـ commit |
post-commit | بعد الـ commit | لا يؤثّر: للإشعارات فقط |
pre-push | قبل الرفع | يلغي الرفع |
post-merge | بعد دمج ناجح (ومنه git pull) | لا يؤثّر |
pre-receive | على الخادم قبل قبول رفع | يرفض الرفع |
القاعدة الأهم: الخطاف الذي ينتهي بـ exit 1 يوقف العملية. هذا ما يجعل pre-* أداة منع لا تذكير فقط.
حين تُنشئ مستودعًا، تجد في .git/hooks/ أمثلة بامتداد .sample لا تعمل. لتفعيل أحدها، أزل الامتداد.
pre-commit: افحص قبل الحفظ
#!/bin/sh
# .git/hooks/pre-commit
branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$branch" = "main" ]; then
echo "⛔ لا commit مباشر على main. أنشئ فرعًا: git switch -c feature/..."
exit 1
fi
# ملفات أكبر من 5MB بين المجهَّز
for f in $(git diff --cached --name-only --diff-filter=AM); do
size=$(git cat-file -s ":$f")
if [ "$size" -gt 5000000 ]; then
echo "⛔ $f حجمه $size بايت (الحد 5MB)"
exit 1
fi
done
git diff --cached --name-onlyتعطي الملفات المجهّزة فقط، أي ما سيدخل الـ commit فعلًا، لا كل ما في المجلّد. و--diff-filter=AMتستبعد المحذوف.git cat-file -s ":$f"حجم النسخة المجهّزة (النقطتان تعنيان منطقة التجهيز)، لا النسخة على القرص التي قد تكون عدّلتها بعدadd.
الملف يحتاج صلاحية تنفيذ، وإلا تجاهله Git مع تنبيه:
chmod +x .git/hooks/pre-commit
hint: The '.git/hooks/pre-commit' hook was ignored because it's not set as executable.
commit-msg: صيغة موحّدة للرسائل
#!/bin/sh
# .git/hooks/commit-msg
pattern='^((feat|fix|docs|refactor|test|ci|chore)(\(.+\))?: .+|Merge .+|Revert .+)'
if ! head -n1 "$1" | grep -qE "$pattern"; then
echo "⛔ رسالة الـ commit لا تتبع الصيغة: type(scope): وصف"
echo " مثال: feat(auth): add Google sign-in"
exit 1
fi
يستلم الخطاف مسار ملف الرسالة في $1، ويفحص سطرها الأول. هذه صيغة Conventional Commits. لاحظ استثناء رسائل Merge وRevert التي يكتبها Git نفسه؛ بدونه يفشل كل git merge وgit revert.
post-merge: تذكير بعد السحب
#!/bin/sh
# .git/hooks/post-merge
if git diff --name-only ORIG_HEAD HEAD | grep -q "package-lock.json"; then
echo "📦 package-lock.json تغيّر: شغّل npm install"
fi
ORIG_HEAD هو موقعك قبل الدمج. سحبت تحديثات فيها مكتبة جديدة؟ الخطاف ينبّهك قبل أن تقضي عشر دقائق في خطأ "module not found".
المشكلة: .git/hooks لا يُرفع
كل ما في .git/ محلي. الخطافات التي كتبتها لا تصل لزملائك مع git push، ولا تأتي مع git clone. حلّان:
1. مجلّد متتبَّع + core.hooksPath
mkdir .githooks
# ضع الخطافات فيه، وأعطها صلاحية التنفيذ
chmod +x .githooks/*
git add .githooks && git commit -m "chore: add shared hooks"
# كل عضو في الفريق، مرة واحدة بعد clone:
git config core.hooksPath .githooks
بلا أي اعتماديات، ويعمل في أي مشروع بأي لغة (منذ Git 2.9). عيبه الخطوة اليدوية الأخيرة لكل شخص.
2. Husky في مشاريع JavaScript
npm install --save-dev husky
npx husky init
husky init يفعل ثلاثة أشياء: ينشئ مجلّد .husky/، ويضع فيه pre-commit يشغّل npm test، ويضيف لـ package.json السكربت "prepare": "husky". سكربت prepare يُشغَّل تلقائيًّا مع npm install، فيضبط Husky الخطافات لكل من يثبّت المشروع، بلا خطوة يدوية.
لتعديل خطاف أو إضافة آخر، اكتب ملفه مباشرة (الأمر القديم husky add لم يعد مدعومًا في الإصدار 9):
echo "npm run lint" > .husky/pre-commit
echo "npm test" > .husky/pre-push
git add .husky && git commit -m "chore: lint before commit, test before push"
ومع lint-staged، يفحص pre-commit الملفات المجهّزة فقط بدل المشروع كله، فيبقى سريعًا في المستودعات الكبيرة.
الخطافات ليست حماية
git commit --no-verify -m "..."
--no-verify (أو -n) تتجاوز pre-commit وcommit-msg، وتعمل مع git push أيضًا. وأي شخص يستطيع حذف الخطاف من جهازه. الخطافات المحلية راحة تكتشف الأخطاء مبكرًا وتوفّر رحلة إلى CI، لا قاعدة لا يمكن كسرها.
ما يجب أن يكون مضمونًا يُفرض على الخادم: فحوصات CI على كل Pull Request، وحماية الفرع التي تمنع الدمج في main بلا موافقة واختبارات ناجحة (درس طلبات السحب). الخطاف المحلي يقول لك "ستفشل في CI" قبل أن ترفع، وCI هو الحكم.
خطاف جيد
- سريع:
pre-commitيعمل عشرات المرات يوميًّا. خطاف يأخذ دقيقتين سيتجاوزه الجميع بـ--no-verify. ضع الاختبارات الطويلة فيpre-pushأو CI. - رسالة واضحة: ماذا فشل، ولماذا، وكيف يُصلح.
- يفحص المجهَّز لا مجلّد العمل.
أخطاء شائعة
- خطاف بلا صلاحية تنفيذ: يُتجاهل بتنبيه.
chmod +x. - امتداد في الاسم (
pre-commit.sh): Git يبحث عن الاسم بالضبط. - فحص كل الملفات بدل المجهّزة: بطيء، ويرفض commits بسبب ملفات لن تدخلها.
commit-msgيرفض رسائل Git التلقائية (Merge ...): استثنِها.- الاعتماد على الخطافات للأمان: تُتجاوز بـ
--no-verify. افرض القواعد في CI وحماية الفروع. npx husky addمن شرح قديم: لم يعد مدعومًا في Husky 9. اكتب الملف مباشرة.
🎯 التالي: آلية Git الداخلية (Objects, Refs, Packfiles).