Git والفروع وطلبات السحب (Pull Requests) — كيف تعمل الفرق البرمجية
من git init إلى push وpull، واستبعاد .venv و.env عبر .gitignore، وإنشاء فروع الميزات الصغيرة، ورسائل الالتزام الواضحة وطلبات السحب — مع أداة بايثون لفحص سجل git log.
كيف تريد أن تتعلم؟
المشكلة التي نقوم بحلها
قضى مهندس ذكاء اصطناعي أسبوعاً كاملاً يعمل على مشروع لتصنيف تذاكر الدعم الفني (ticket triage): يعدل الصياغة في المطالبة (prompt)، ويوسع قائمة الكلمات المفتاحية، ويعيد كتابة إحدى الدوال. وعند حلول يوم الجمعة، أصبحت النتائج أسوأ مما كانت عليه يوم الإثنين! أي تعديل بالتحديد هو المتسبب في هذا التراجع؟ مجلد المشروع يحتوي على triage.py وtriage_v2.py وtriage_final.py وtriage_final_REAL.py — ولا أحد في الفريق يدري ما الذي تغير في كل ملف منها.
في الوقت نفسه، قام زميل له كان يعدل نفس الملف على مجلد مشترك بمسح كل ما كتبه عن غير قصد. وقبل شهر من الآن، قام شخص ما بلصق مفتاح واجهة برمجية (API key) سري مباشرة داخل الكود لتجربة سريعة، وعبر ذلك المجلد المشترك تسرب المفتاح لعشرة أشخاص آخرين.
كل هذه المشكلات الثلاث لها حل وحيد معتمد لدى كل الفرق البرمجية المحترفة تقريباً حول العالم: توثيق تاريخ المشروع باستخدام نظام Git، وإنجاز كل مهمة في فرع (branch) مستقل خاص بها، وقبل دمج أي سطر في الكود الرئيسي، يراجعه زميل آخر عبر طلب سحب (Pull Request). يعلمك هذا الفصل دورة العمل هذه بالكامل — ويختتم بكتابة أداة بايثون عملية لأتمتة إحدى مهام المراجع الفني.
بنهاية هذا الفصل ستكون قادراً على
- حفظ ومشاركة تاريخ المشروع باستخدام أوامر
git initوaddوcommitوpushوpull - إبقاء بيئات
.venvوالأسرار في.envخارج المستودع عبر ملف.gitignore، ومعرفة كيفية التصرف إذا تم ارتكاب خطأ بإضافتها - العمل على فروع ميزات (feature branches) صغيرة وقصيرة الأجل وفتح طلب سحب (Pull Request) منظم ونظيف
- كتابة رسائل الالتزام (commit messages) بالطريقة الاحترافية التي يتوقعها المراجعون
- بناء أداة بايثون تقرأ سجل
git logعبر مكتبةsubprocessوتفحص معايير رسائل الالتزام آلياً
المتطلبات المسبقة: مسار مهندس الذكاء الاصطناعي وبيئات التطوير باستخدام uv — وتحديداً أمري uv init وuv run.
الإعداد لأول مرة
يسجل Git اسم وبريد صاحب كل التزام (commit). لذلك، ستقوم بالتعريف عن نفسك لمرة واحدة، ومن الأفضل أيضاً إخبار Git بتسمية الفرع الرئيسي في المستودعات الجديدة باسم main، لأن هذا هو المعيار المعتمد لدى أغلب الفرق البرمجية اليوم:
git config --global user.name "Nadia Karim"
git config --global user.email "[email protected]"
git config --global init.defaultBranch mainاستخدم اسمك الحقيقي وبريدك الإلكتروني المسجل به في منصة استضافة الأكواد. بدون السطر الأخير، قد تطلق إصدارات Git القديمة اسم master على الفرع الرئيسي — ورغم أنه سيعمل، إلا أنه لن يتطابق مع معايير الفريق.
الأماكن الثلاثة: مجلد العمل، ومنطقة التجهيز، وتاريخ المشروع
في الفصل السابق، قام أمر uv init بإنشاء مستودع Git تلقائياً. يبدأ مشروع هذا الفصل بالطريقة ذاتها:
uv init ticket-triage --python 3.12
cd ticket-triageيتعامل Git مع ثلاثة أماكن رئيسية، وكل أمر ينقل الملفات من مكان إلى آخر:
- شجرة العمل (The working tree) — مجلد المشروع على جهازك، حيث تعدل الملفات وتكتب الكود
- منطقة التجهيز (The staging area) — قائمة التعديلات المجهزة للالتزام القادم؛ يضع أمر
git addالملفات هنا - تاريخ المشروع (History) — سلسلة الالتزامات الدائمة؛ يحول أمر
git commitما في منطقة التجهيز إلى التزام محفوظ ومؤرخ
لماذا توجد خطوة وسيطة للتجهيز (Staging)؟ لأنك في منتصف العمل قد تعدل خمسة ملفات: اثنان منها لإصلاح خلل برمجي، وثلاثة لإضافة ميزة جديدة. تتيح لك منطقة التجهيز فصلها إلى التزامين مستقلين وذوي مغزى واضح — وهو أمر بالغ الأهمية لمراجع الكود.
الأمر الأساسي لمعرفة حالة الملفات هو git status، أو بصيغته المختصرة git status --short.
ملف .gitignore: ما لا يدخل مستودع الكود أبداً
تحتوي مشاريع الذكاء الاصطناعي دائماً تقريباً على ملف .env لحفظ مفاتيح الواجهات البرمجية (API keys). لنفترض أن المشروع يحتوي على ملف بهذا الشكل (الرمز sk-... هنا مجرد مثال توضيحي وليس مفتاحاً حقيقياً):
LLM_API_KEY=sk-...ملف .gitignore الذي أنشأه uv init يتجاهل مجلد .venv لكنه لا يتجاهل .env. لذا فإن أول مهمة هي إضافته في نهاية الملف:
# Secrets
.envالآن نفّذ الأمر uv run main.py لمرة واحدة (حتى يتم إنشاء .venv وملف uv.lock) ثم تحقق من الحالة:
git status --short
?? .gitignore
?? .python-version
?? README.md
?? main.py
?? pyproject.toml
?? uv.lockتعني علامة ?? أن Git يرى الملف لكنه لا يتتبعه بعد. والخبر الأهم هنا هو ما لا يظهر في هذه القائمة: لا وجود لـ .venv ولا لـ .env. كلاهما موجود في المجلد، لكن Git يتجاهلهما تماماً. (في الواقع يتمتع مجلد .venv بطبقتين من الحماية: فعندما تنشئه أداة uv، تضع بداخله ملف .gitignore مصغراً يحتوي فقط على *). وتلاحظ وجود uv.lock في القائمة — ويجب حفظه في المستودع بالفعل، لأنه الضمان لحصول زملائك على البيئة نفسها تماماً.
ولكن كيف سيعرف الزملاء ما هي المفاتيح التي يحتاجونها؟ الطريقة المتعارف عليها هي حفظ ملف باسم .env.example يحتوي على أسماء المتغيرات فقط دون قيمها. ينسخه العضو الجديد إلى .env ويضع مفاتيحه الخاصة.
السر الذي تم رفعه للالتزام لا يُحذف بمجرد مسحه في الالتزام التالي. وظيفة Git الجوهرية هي تذكر كل حالة سابقة للمشروع، وبالتالي يظل المفتاح محفوظاً في الالتزام القديم إلى الأبد — ويمكن لأي شخص استخراجه بأمرgit show. وبمجرد رفع المفتاح إلى أي خادم بعيد، فإن الحل الآمن الوحيد هو التوجه لمزود الخدمة، وإلغاء المفتاح (revoke) فوراً، وتوليد مفتاح جديد. لهذا السبب يجب إضافة.envإلى.gitignoreقبل إنشاء الملف أصلاً.
الالتزام الأول ورسائل الالتزام الجيدة
git add .
git commit -m "chore: scaffold project with uv"
[main (root-commit) 0f78e59] chore: scaffold project with uv
6 files changed, 35 insertions(+)
create mode 100644 .gitignore
create mode 100644 .python-version
create mode 100644 README.md
create mode 100644 main.py
create mode 100644 pyproject.toml
create mode 100644 uv.lockالرمز 0f78e59 هو المعرف الفريد للالتزام (بداية الـ hash الخاص به) — وسيختلف بالتأكيد على جهازك لأن الوقت وهوية الكاتب تدخل في حسابه.
تُكتب رسالة الالتزام من أجل القارئ المستقبلي — أنت بعد ستة أشهر، ومراجع الكود اليوم. تتبع معظم الفرق نمطاً موحداً يُدعى الالتزامات التقليدية (Conventional Commits):
type(scope): short summary in the imperative- type يوضح طبيعة التعديل:
feat(ميزة جديدة)،fix(إصلاح خطأ)،docs(توثيق)،refactor(إعادة هيكلة دون تغيير السلوك)،test(اختبارات)،chore(مهام صيانة) - scope اختياري ويوضع بين قوسين، لتحديد الجزء المعني في المشروع:
feat(triage): ... - summary بصيغة الأمر المباشر (مثل "add", "fix", "remove" — وليس "added")، ولا يتجاوز 72 حرفاً، وبدون نقطة في نهايته
بالمقارنة المباشرة يتضح الفارق جلياً:
wip
fixed stuff
Fixed the bug where priority returned normal for tickets that say OUTAGE in capitals
fix(triage): lowercase ticket text before keyword matchingأول ثلاثة أمثلة لا تولد أي أخطاء — فـ Git يقبل أي نص. لكن في سجل git log، أول رسالتين لا تقدمان أي معلومة مفيدة، والثالثة طويلة جداً لدرجة أنها تُبتر عند عرض السجل المختصر في سطر واحد. أما الأخيرة، فيعرف المراجع فور قراءتها ماذا تغير وأين. وإذا كان سبب التغيير ("لماذا") يحتاج لشرح أوسع، فيوضع في جسم الرسالة بعد سطر فارغ.
الفروع: إنجاز كل مهمة في مساحة مستقلة
الفرع الرئيسي main هو أثمن ما يملكه الفريق: كل ما فيه يعمل بنجاح ويمكن نشره للإنتاج في أي لحظة. لذلك لا يعمل أحد مباشرة على main. كل مهمة تحصل على فرع مستقل:
git switch -c feat/priority-score
Switched to a new branch 'feat/priority-score'هنا نكتب الكود في ملف triage.py:
def priority(text: str) -> str:
urgent_words = ("outage", "down", "breach")
if any(word in text for word in urgent_words):
return "high"
return "normal"git add triage.py
git commit -m "feat(triage): add keyword-based priority"
[feat/priority-score ded4456] feat(triage): add keyword-based priority
1 file changed, 5 insertions(+)
create mode 100644 triage.pyتتبع أسماء الفروع نفس الأسلوب: feat/...، fix/...، docs/... — لتعرف بمجرد قراءة القائمة طبيعة كل فرع.
ملاحظة حول استراتيجيات الفروع (Branching Strategy). في السابق كانت بعض الفرق تعتمد على فروع طويلة الأمد — مثل develop وrelease وhotfix — وتتبع قواعد معقدة لنقل الأكواد بينها. أما اليوم، فتعتمد أغلب الفرق على مسار أبسط: فرع رئيسي واحد main، وتتفرع منه فروع ميزات صغيرة وقصيرة الأجل تتم مراجعتها ودمجها خلال يوم أو يومين. فكلما طالت فترة بقاء الفرع منفصلاً، ابتعد عنه main، وتضخمت تعارضات الدمج (conflicts). تتبع هذه الدورة هذا المسار المبسط الحديث.
المستودعات البعيدة: الرفع (push) والسحب (pull)
حتى الآن، كل شيء موجود على جهازك المحلي فقط. لمشاركة العمل مع الفريق تحتاج إلى مستودع بعيد (remote) — عادة على GitHub أو منصة مشابهة. عند إنشاء مستودع هناك ستحصل على رابطه، وتكتب في الطرفية:
git remote add origin https://github.com/<your-account>/ticket-triage.gitتم التحقق من أوامر هذا الفصل دون الحاجة لخدمات خارجية: تم إنشاء مستودع "خام" (bare repository) في مجلد مجاور واستخدامه كمستودع بعيد. بالنسبة لـ Git، الأمران متطابقان تماماً، ويمكنك التدرب على جهازك بهذه الطريقة أيضاً:
git init --bare ../remote.git
git remote add origin ../remote.gitيحتاج أمر الرفع الأول إلى معامل -u لربط فرعك المحلي بالفرع البعيد:
git push -u origin main
branch 'main' set up to track 'origin/main'.
To ../remote.git
* [new branch] main -> mainويتم رفع فرع الميزة بنفس الطريقة:
git push -u origin feat/priority-score
branch 'feat/priority-score' set up to track 'origin/feat/priority-score'.
To ../remote.git
* [new branch] feat/priority-score -> feat/priority-scoreالآن يقوم زميلك باستنساخ المستودع:
git clone remote.git teammate
Cloning into 'teammate'...
done.يحتوي مجلده على README.md وmain.py وpyproject.toml وuv.lock (بالإضافة للملفات المخفية) — لكن دون .venv ودون .env. وهذا هو المطلوب تماماً: سينفذ uv sync لبناء بيئته الخاصة، وينشئ ملف .env الخاص به انطلاقاً من .env.example.
بعد دمج الميزة في main (يحدث ذلك في بيئة العمل الحقيقية عند قبول طلب السحب — كما سنرى أدناه)، يسحب الزميل التحديثات الجديدة:
git pull
Updating 0f78e59..5f802b4
Fast-forward
triage.py | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 triage.py(تبدأ المخرجات الحقيقية بسطر From ... يوضح مسار المستودع الكامل؛ وتم حذفه هنا للاختصار). تشير عبارة Fast-forward إلى أن الزميل لم تكن لديه التزامات جديدة خاصة به، لذا قام Git ببساطة بتقديم فرع main لديه للأمام.
التذاكر وطلبات السحب (Issues and Pull Requests)
في الشركات، يبدأ العمل عادةً بـ تذكرة (issue) — وهو وصف مرقم للمشكلة يُكتب على منصة استضافة الأكواد. على سبيل المثال: #12 — عدم التعرف على الكلمات المكتوبة بأحرف كبيرة (التذاكر التي تحتوي على كلمة "OUTAGE" تصنف كعادية).
تسير دورة العمل بعد ذلك وفق الخطوات التالية:
- إنشاء فرع انطلاقاً من
main:git switch -c fix/uppercase-keywords - تسجيل التزامات صغيرة وواضحة المعنى
- رفع الفرع إلى المستودع البعيد:
git push - من خلال موقع المنصة، أو عبر سطر الأوامر، يتم فتح طلب سحب (Pull Request - PR) — وهو طلب يقول للفريق: "راجعوا تعديلات هذا الفرع واعتمدوا دمجها في
main" - يكتب المراجع ملاحظاته، وترد عليها بإضافة التزامات جديدة على نفس الفرع
- بعد الحصول على الموافقة واجتياز الفحوصات الآلية (CI)، يُدمج طلب السحب ويُحذف الفرع
باستخدام أداة GitHub لسطر الأوامر gh، تبدو الخطوة الرابعة بهذا الشكل (تعتمد المخرجات على حسابك ومستودعك، لذا لم نعرضها هنا):
gh pr create --base main --title "fix(triage): detect uppercase keywords" --body-file PR.mdالوصف المنظم لطلب السحب (PR.md) يجيب عن أسئلة المراجع الثلاثة الأساسية — ماذا، ولماذا، وكيف تم الاختبار:
## What
Lowercase ticket text before matching urgent keywords.
## Why
Tickets written in capitals ("OUTAGE") were scored as normal.
Closes #12
## How I tested
priority("Service DOWN") returned "normal" on main, "high" on this branch
uv run check_commits.py -> 0 of 2 commit messages need workعبارة Closes #12 هي اختصار ذكي: عندما يُدمج طلب السحب في الفرع الافتراضي، تقوم المنصة بإغلاق التذكرة رقم 12 تلقائياً.
كيف يراجع المشرفون الأكواد؟
تتم مراجعة كل تمرين عملي في هذه الدورة بعين مراجع محترف، وفي الشركات هذه هي الأسئلة التي يطرحها المراجع دائماً:
- هل طلب السحب صغير وله غرض محدد واحد؟ ميزة واحدة، أو إصلاح خطأ واحد، أو إعادة هيكلة واحدة — وليس كل ذلك معاً. عندما يتجاوز التعديل بضع مئات من الأسطر، تنخفض جودة المراجعة لأن الإنسان يصعب عليه التركيز في فروقات (diff) طويلة ومعقدة.
- هل تحكي رسائل الالتزام قصة واضحة لتاريخ المشروع؟ السجل المليء بكلمات مثل
wipوfixلا يفيد أحداً عند الحاجة لتتبع المشكلات. - هل تسربت ملفات غير مرغوب فيها؟
.venv،.env،__pycache__، أو ملفات بيانات ضخمة — إذا ظهر أي منها في الفروقات، يُرفض طلب السحب فوراً. - هل كل ما هو ضروري موجود؟ إذا أُضيفت حزمة جديدة، يجب أن تظهر التعديلات المقابلة لها في
pyproject.tomlوuv.lock. - هل يوضح الوصف بوضوح كيف تم اختبار الكود؟
إذا كانت رسالة الالتزام الأخير غير دقيقة، يمكنك تعديلها قبل الرفع:
git commit --amend -m "docs: describe the project in README"
git log --oneline -1
a18cc1a docs: describe the project in READMEتعديل الالتزام بعد الرفع يعني إعادة كتابة التاريخ المشترك، وهو ما يربك الزملاء؛ وفي تلك الحالة يُفضل إضافة التزام جديد. تدمج العديد من الفرق طلبات السحب بأسلوب "squash" — حيث تُدمج جميع التزامات الفرع في التزام واحد، ويصبح عنوان طلب السحب هو رسالة هذا الالتزام، ولهذا يجب أن يتبع العنوان نفس القواعد الدقيقة أيضاً.
مثال عملي كامل
يمكن إسناد إحدى مهام المراجع إلى أداة برمجية آلية: فحص التزام كل رسالة على الفرع بالمعايير المطلوبة. تشغل الفرق مثل هذه الفحوصات في أنظمة التكامل المستمر (CI)، بحيث تظهر النتيجة بمجرد فتح أي طلب سحب. سكريبت check_commits.py التالي يؤدي هذه الوظيفة بدقة — فهو ينفذ أمر git log ويقرأ التزامات الفرع الحالي فقط، متجاهلاً ما هو موجود مسبقاً على main:
import re
import subprocess
import sys
ALLOWED_TYPES = {"feat", "fix", "docs", "refactor", "test", "chore"}
SUBJECT = re.compile(r"^(?P<type>[a-z]+)(\([a-z0-9-]+\))?: (?P<summary>.+)$")
MAX_LENGTH = 72
VAGUE = {"wip", "fix", "update", "changes", "stuff", "misc"}
def problems_in(subject: str) -> list[str]:
found = []
if subject.strip().lower() in VAGUE:
found.append("too vague for a reviewer")
if len(subject) > MAX_LENGTH:
found.append(f"longer than {MAX_LENGTH} characters ({len(subject)})")
match = SUBJECT.match(subject)
if match is None:
found.append("missing 'type: summary' prefix")
elif match["type"] not in ALLOWED_TYPES:
found.append(f"unknown type '{match['type']}'")
if subject.endswith("."):
found.append("ends with a period")
return found
def branch_subjects(base: str) -> list[str]:
result = subprocess.run(
["git", "log", "--reverse", "--format=%s", f"{base}..HEAD"],
capture_output=True,
encoding="utf-8",
)
if result.returncode != 0:
raise SystemExit(f"git log failed: {result.stderr.strip()}")
return result.stdout.splitlines()
def main() -> int:
subjects = branch_subjects("main")
failures = 0
for subject in subjects:
problems = problems_in(subject)
print(("ok " if not problems else "FAIL") + " " + subject)
for problem in problems:
print(f" - {problem}")
failures += 1 if problems else 0
print(f"{failures} of {len(subjects)} commit messages need work")
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main())لاختباره، أنشأنا فرعاً متفرعاً من main يحتوي على خمسة التزامات — بعضها يحمل رسائل خاطئة عمداً. (المعامل --allow-empty يُنشئ التزاماً دون تعديل أي ملف، وهو كافٍ لاختبار الرسائل؛ بينما في العمل الفعلي يحمل كل التزام تغييراً في الكود):
git switch -c fix/uppercase-keywords
git commit --allow-empty -m "wip"
git commit --allow-empty -m "Fixed the bug where priority returned normal for tickets that say OUTAGE in capitals"
git commit --allow-empty -m "fix(triage): lowercase text before matching."
git commit --allow-empty -m "feature: add tests"
git commit --allow-empty -m "test(triage): cover uppercase keywords"ثم من داخل المستودع، نشغل الأداة:
uv run check_commits.pyFAIL wip
- too vague for a reviewer
- missing 'type: summary' prefix
FAIL Fixed the bug where priority returned normal for tickets that say OUTAGE in capitals
- longer than 72 characters (84)
- missing 'type: summary' prefix
FAIL fix(triage): lowercase text before matching.
- ends with a period
FAIL feature: add tests
- unknown type 'feature'
ok test(triage): cover uppercase keywords
4 of 5 commit messages need workرمز الخروج (exit code) للبرنامج هو 1، لأن رسالة واحدة على الأقل فشلت في الفحص. وإذا كانت كل الرسائل سليمة، فإن رمز الخروج يكون 0.
القرارات البرمجية في هذا السكريبت:
نطاق main..HEAD — النقطتان تعنيان "الالتزامات الموجودة في HEAD ولكنها غير موجودة في main". بعبارة أخرى، التعديلات الخاصة بهذا الفرع فقط. ففحص التاريخ بأكمله قد يفشل طلب السحب بسبب التزامات قديمة كتبها أشخاص آخرون قبل عام. والمعامل --reverse يرتبها من الأقدم للأحدث، بنفس ترتيب تنفيذ العمل.
معامل --format=%s — الرمز %s يعني استخراج السطر الأول فقط من الالتزام (عنوان الرسالة). لا تظهر قيم الـ hash أو التواريخ أو أسماء المؤلفين — وبالتالي لا حاجة لمعالجة نصوص معقدة؛ كل سطر هو رسالة التزام واحدة.
encoding="utf-8" — عند استخدام text=True، يستخدم بايثون الترميز الافتراضي لنظام التشغيل، وهو في ويندوز غالباً ما يكون غير UTF-8. فإذا كتب أحدهم رسالة بأحرف عربية أو برمز تعبيري (emoji)، قد يتعطل البرنامج. تحديد الترميز صراحة يقضي على هذه المشكلة.
عرض رسالة خاصة بدلاً من check=True — عند فشل أمر git (كأن يُشغل خارج مستودع، أو عند عدم وجود فرع باسم main)، يعرض السكريبت رسالة خطأ git نفسها بدلاً من تتبع خطأ طويل وغير مفهوم من بايثون. تمرير نص إلى SystemExit يجعل بايثون يطبعه في قناة الأخطاء القياسية (stderr) ويتوقف مع كود الخروج 1.
كود الخروج (Exit Code) — أنظمة CI لا تقرأ النصوص المطبوعة، بل تعتمد حصراً على ما إذا كان البرنامج انتهى بكود الخروج 0 أم لا. لذلك تعيد دالة main() رقماً، وتمرره sys.exit إلى نظام التشغيل.
عند حدوث مشاكل شائعة
Author identity unknown متبوعة بـ fatal: unable to auto-detect email address لا يعلم Git بعد اسم من يضعه على الالتزام. نفّذ أَمري git config --global user.name ... وuser.email ... المذكورين في بداية الفصل لمرة واحدة.
error: src refspec main does not match any لا توجد أي التزامات في المستودع بعد، لذا فإن فرع main غير موجود فعلياً — ولا يوجد ما يمكن رفعه. قم بتنفيذ git add ثم git commit أولاً، ثم ارفع التغييرات. قد تظهر الرسالة ذاتها إذا كان اسم الفرع master؛ تحقق من اسم فرعك عبر git branch.
fatal: The current branch feat/x has no upstream branch. أنت تحاول رفع فرع جديد للمرة الأولى، ولا يعلم Git أي فرع بعيد يجب إرسال التغييرات إليه. الرسالة نفسها توضح الحل: git push --set-upstream origin feat/x (أو بصيغتها المختصرة git push -u origin feat/x). بعد ذلك، يكفيك كتابة git push فقط.
! [rejected] main -> main (fetch first) قام شخص آخر برفع تعديلات على نفس الفرع منذ آخر مرة قمت فيها بجلب التحديثات. ادمج تعديلاته أولاً باستخدام git pull، ثم أعد المحاولة. إياك واستخدام "الحل" العنيف git push --force — لأنه سيمسح عمل زملائك تماماً.
warning: in the working copy of 'main.py', LF will be replaced by CRLF the next time Git touches it هذا تحذير طبيعي على ويندوز وليس خطأ. ينهي ويندوز الأسطر بمحارف مختلفة عن macOS ولينكس، ويخبرك Git بأنه سيوائم بينها تلقائياً. تم تسجيل الالتزام بنجاح.
تم ارتكاب خطأ وحفظ ملف .env في الالتزام نفّذ الأمر git rm --cached .env — سيبقي الملف على القرص ويزيله فقط من تتبع Git (ستكون المخرجات rm '.env') — ثم أضف .env إلى .gitignore وسجل التزاماً جديداً. لكن تذكر أن المفتاح لا يزال محفوظاً في الالتزام القديم؛ وإذا تم رفعه لمستودع خارجي، فلا حل آمن سوى إبطال المفتاح فوراً وتوليد غيره.
الخطوة ٤ من ٦ — التوقع
تحقّق من فهمك
Three messages are checked against the chapter's SUBJECT pattern. What prints?
import re
SUBJECT = re.compile(r"^(?P<type>[a-z]+)(\([a-z0-9-]+\))?: (?P<summary>.+)$")
for subject in ["fix: handle empty input", "Fix: handle empty input", "fix:handle empty input"]:
print(SUBJECT.match(subject) is not None)- A`True`, `False`, `True`
- B`True`, `True`, `True`
- C`True`, `True`, `False`
- D`True`, `False`, `False`
A colleague "fixed" this function in check_commits.py. Now CI is green even on a branch with wip and feature: add tests commits, and the output is 0 of 0 commit messages need work. Where is the bug?
import subprocess
def branch_subjects(base: str) -> list[str]:
result = subprocess.run(
["git", "log", "--reverse", "--format=%s", f"HEAD..{base}"],
capture_output=True,
encoding="utf-8",
)
if result.returncode != 0:
raise SystemExit(f"git log failed: {result.stderr.strip()}")
return result.stdout.splitlines()- AThe range is reversed: `HEAD..main` means "in `main` but not on this branch", so none of the branch's own commits come back; it should be `main..HEAD`
- B`--reverse` drops commits; removing it will show them
- CThe `returncode` check is discarding every commit; it needs `check=True`
- DWith `encoding="utf-8"`, `git` produces no output; it needs `text=True`
A PR description contains Closes #12. What does it do?
- AWhen the PR is merged into the default branch, the code-hosting service closes issue #12 by itself and keeps the two linked
- BIssue #12 is closed as soon as the PR is opened
- CNothing — it is just a note for the reviewer; no machine reads it
- DGit deletes commit number 12 from history
اللعب جزء من Pro
+30 XP بانتظارك
العب هذا الفصل مع Pro
القراءة مجانية، والفصل كله متاح. أول 5 فصول من كل دورة مجانية متاحة للعب مجانًا. ابتداءً من الفصل 6، الإجابة عن الأسئلة وكسب نقاط XP وتنمية سلسلتك جزء من Pro.
افتح اللعب مع Proدورك الآن
أنشئ مشروعاً على جهازك وطبق الدورة الكاملة لمرة واحدة:
- أنشئ المشروع بالأمر
uv init my-triage --python 3.12، وأضف.envإلى.gitignore، واحتفظ بملف.env.example(يحتوي فقط علىLLM_API_KEY=)، وتأكد من أنgit status --shortلا يُظهر.envولا.venv، ثم سجل الالتزام الأول - أنشئ مستودعاً خاماً ليكون المستودع البعيد (
git init --bare ../my-triage-remote.git) ثم ارفع فرعmain - أنشئ فرعاً باسم
feat/priority-scoreوأضف ملفيtriage.pyوcheck_commits.pyالمشروحين في هذا الفصل في ثلاثة التزامات متتالية بالضبط، مع التزام كل رسالة بالقواعد؛ ثم ارفع الفرع - اكتب ملف
PR.md— ماذا، ولماذا، وكيف اختبرت الكود — وإذا كان لديك حساب على منصة استضافة أكواد، افتح طلب سحب حقيقياً هناك مستخدماً هذا الوصف
على الفرع الجديد، يجب أن ينتهي تشغيل uv run check_commits.py بهذا السطر تحديداً، مسبوقاً بثلاثة أسطر ok:
0 of 3 commit messages need workبعد ذلك، تعمّد كتابة رسالة التزام خاطئة (git commit --allow-empty -m "wip")، وشغّل الأداة مجدداً لترى المشكلتين اللتين ستكتشفهما، ثم صحح الأمر باستخدام git commit --amend -m "...". أو جرب هذا: استبدل main..HEAD بـ HEAD..main داخل السكريبت، وشاهد ما يطبعه، واستنتج لماذا يُعد هذا أخطر أنواع الأخطاء في فحوصات CI الآلية.
الخطوة ٦ من ٦
التحدي — اختبار الفصل
عشرة أسئلة متدرجة من السهل إلى الصعب. الأسئلة الأخيرة صعبة عن قصد.
العب مع Pro