guide

أولوية قواعد gitignore وأنماط النفي (!)

لماذا تفوز القاعدة الأخيرة، والأولوية بين .gitignore وinfo/exclude والإعداد العام، ولماذا لا يستطيع ! إعادة تضمين ملف إذا استُبعد مجلده الأب.

عندما لا تعمل قاعدة gitignore كما هو متوقع، يكون السبب غالبًا في الترتيب والمجلد الأب. يلخّص هذا المقال القاعدة التي يتبعها git حين تنطبق عدة قواعد.

داخل الملف الواحد: آخر قاعدة مطابقة هي التي تفوز

داخل ملف .gitignore واحد يقرأ git من الأعلى إلى الأسفل، وآخر نمط مطابق هو الذي يحدد النتيجة. إن بدأ ذلك النمط بـ ! فلا يُتجاهل الملف، وإلا فإنه يُتجاهل.

*.log
!important.log

يطابق important.log السطرين معًا، لكن السطر الأخير نفي، لذا يبقى مُتتبَّعًا. اعكس الترتيب تنعكس النتيجة أيضًا.

!important.log
*.log

الآن آخر مطابقة لـ important.log هي *.log، فيُتجاهل. ولهذا السبب عند دمج عدة قوالب في المولّد يصبح ترتيب الاختيار هو ترتيب القواعد. فمثلًا عند استخدام *.jar من قالب Java مع !gradle-wrapper.jar من قالب Gradle، يجب أن يأتي Gradle بعده.

الأولوية بين الملفات

يقرأ git القواعد من أربعة مواضع. كلما كان الموضع أعلى في القائمة كانت أولويته أعلى.

  1. الأنماط المُمرَّرة في سطر الأوامر (بعض الأوامر مثل git ls-files --exclude)
  2. ملف .gitignore في مجلد المسار نفسه أو في مجلداته الأب — الملفات الأعمق تتغلب على الملفات الأعلى
  3. $GIT_DIR/info/exclude (عادةً .git/info/exclude)
  4. الملف الذي يشير إليه الإعداد core.excludesFile (ملف gitignore العام)

مثلًا، إذا احتوى .gitignore في الجذر على *.tmp واحتوى sub/.gitignore على !*.tmp، فإن sub/y.tmp يبقى مُتتبَّعًا بفضل قاعدة النفي في الملف الأعمق. وفي المقابل لا تفيد كتابة !x في .git/info/exclude، لأن x في .gitignore له الأولوية فيبقى x مُتجاهَلًا.

$ git check-ignore -v x y.tmp sub/y.tmp
.gitignore:1:x	x
.gitignore:2:*.tmp	y.tmp
sub/.gitignore:1:!*.tmp	sub/y.tmp

أهم استثناء: إذا استُبعد المجلد الأب فلا يمكن إعادة التضمين

تنص وثائق git على أنه لا يمكن إعادة تضمين ملف إذا كان أحد مجلداته الأب مستبعدًا. لأسباب تتعلق بالأداء لا يدخل git المجلد المستبعد أصلًا، فلا تُقرأ قواعد ! الخاصة بالملفات التي بداخله.

logs/
!logs/keep.txt

مع هذه القواعد يبقى logs/keep.txt مُتجاهَلًا، ويعرض git check-ignore -v القاعدة logs/ بوصفها القاعدة الحاسمة.

الحل هو استبعاد محتوى المجلد لا المجلد نفسه.

logs/*
!logs/keep.txt

لا يطابق logs/* المجلد logs نفسه، لذا يدخل git إليه ويطبّق النفي على keep.txt. ويعتمد /log/* + !/log/.keep في قالب Rails و.vscode/* + !.vscode/settings.json في قالب VS Code على المبدأ نفسه.

لإعادة تضمين مسار أعمق يجب إعادة تضمين كل مجلد وسيط أيضًا.

config/*
!config/app/
config/app/*
!config/app/defaults.yml

نمط قائمة السماح (allowlist)

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

# ignore everything at the root
/*
# re-include only what is needed
!/.gitignore
!/src/
!/README.md

يطابق /* كل عنصر في الجذر من دون أن يستبعد الجذر نفسه، لذا يعمل !/src/. وفي هذا الأسلوب يقع ملف .gitignore نفسه تحت /*، فلا تنسَ !/.gitignore. وبالفعل، من دون هذا السطر يظهر .gitignore ضمن قائمة المتجاهَلات في git status --ignored.

القواعد المكررة والترتيب

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

*.log
!debug.log
*.log

هنا يؤدي حذف *.log الأخير إلى أن يصبح debug.log مُتتبَّعًا. يكتشف مولّد هذا الموقع هذه الحالة، فلا يحذف التكرار الواقع في الأسفل إلا إذا لم توجد قاعدة نفي بينهما، ويُبقي ما عدا ذلك.

المراجع

← السابقالدليل الكامل لصياغة أنماط .gitignore