04

طبقة Git

فحص المستودع، وقائمة الـ commits، والفروقات، وقراءة الـ blobs، ودمج هويات المطورين

5 دقائق قراءة7 أقسام

كل الوصول إلى Git موجود في src/git/. كل أمر يُشغَّل عبر execFile("git", args, { cwd: root }). لا يوجد shell في المنتصف، لذلك المسارات التي فيها مسافات أو رموز خاصة آمنة. وعلى Windows يحوّل execFile الاسم git إلى git.exe.

الأوامر التي تشغّلها CleanLens

الأمرالمكانالغرض
git rev-parse --is-inside-work-treegitService.tsهل المجلد مستودع؟
git rev-parse HEADgitService.tsهل فيه commit واحد على الأقل؟
git log --no-merges --format=%aN|%aE|%aIgitService.tsاسم الكاتب وبريده وتاريخ كل commit ← قائمة المطورين
git log --no-merges --format=%H␟%P␟%aN␟%aE␟%aI [--since=…] [-n N]commitHistory.tsالـ commits التي سيُعاد تشغيلها
git diff --no-color --unified=0 -w -M -C --diff-filter=ACDMRT <parent> <commit>diffService.tsالملفات المتغيرة، ومدى الأسطر، ومعرّفات الـ blobs — استدعاء واحد لكل commit
git cat-file --batchdiffService.tsمحتوى كل blob يحتاجه الـ commit — استدعاء واحد لكل commit
git show <sha>diffService.tsبديل لقراءة blob واحد إذا فشلت القراءة المجمّعة

الرمزان %aN و%aE يطبّقان ملف .mailmap، لذلك وجود ملف .mailmap في المستودع هو أبسط طريقة لتصحيح الهويات على مستوى Git.

gitService.ts

في GitService ثلاث دوال:

  • isRepository() ← تعيد false عند أي خطأ.
  • hasCommits() ← تعيد false إذا لم يكن HEAD موجوداً (مستودع فارغ).
  • listCommits() ← تعيد RawCommit[] ‏(authorName وauthorEmail وauthorDate) لـكل الـ commits غير الدمجية. تُستخدم فقط لبناء قائمة المطورين وإحصاءات النشاط. وهي لا تتقيد بـ maxCommits.

commitHistory.ts — أي commits يُعاد تشغيلها

listCommitsDetailed(cwd, { maxCommits, since }):

  • دائماً مع --no-merges. commits الدمج لا تضيف كوداً جديداً خاصاً بها؛ محتواها مغطى بالـ commits التي دُمجت.
  • since ← --since=<value> (أي قيمة يقبلها Git: 2026-01-01، "6 months ago"، …).
  • maxCommits > 0 ← -n <maxCommits> (أحدث N). القيمة 0 أو عدم التحديد مع عدم وجود since ← السجل كاملاً.
  • تُعكس النتيجة لتبدأ من الأقدم، حتى يمشي المحرّك إلى الأمام في الزمن.
  • لكل commit قائمة parents. الـ commit الجذري ليس له أب، ويُقارن مع الشجرة الفارغة في Git ‏(4b825dc6…).

النافذة الافتراضية هي أحدث 500 commit ‏(DEFAULT_MAX_COMMITS في analyzeRepository.ts).

diffService.ts — ما الذي تغيّر في الـ commit

commitChanges(parent, commit)

يشغّل git diff واحداً ويحوّله إلى CommitFileChange[]:

الحقلالمعنى
statusadded وmodified وdeleted وrenamed وcopied
path / oldPathالمسار بعد / قبل (عند إعادة التسمية والنسخ)
blobBefore / blobAfterمعرّفات الـ blobs، وتكون null إذا لم يكن الملف موجوداً في ذلك الجانب
addedمدى الأسطر (في ملف ما بعد) التي أضافها الـ commit أو غيّرها
removedمدى الأسطر (في ملف ما قبل) التي حذفها الـ commit أو غيّرها
binaryملف ثنائي — يُتجاهل

الخيارات مختارة من أجل العدالة:

  • --unified=0 — بدون أسطر سياق، فتعطي ترويسات الأجزاء المدى الدقيق للأسطر المتغيرة.
  • -w — تجاهل المسافات. إعادة التنسيق (المسافة البادئة، المسافات في نهاية السطر) لا تُعدّ تغييراً في السطر، فلا تنقل المسؤولية.
  • -M -C — كشف إعادة التسمية والنسخ. نقل ملف لا يجعل من نقله كاتباً لمحتواه.
  • --diff-filter=ACDMRT — مضاف، منسوخ، محذوف، معدّل، مُعاد تسميته، تغيّر نوعه.

المحلل يقرأ diff --git وnew file mode وdeleted file mode وrename from/to وcopy from/to وindex <a>..<b> و---/+++ وترويسات الأجزاء @@. ويقسم النص على \r?\n، لذلك مخرجات Git على Windows بنهايات CRLF تُقرأ بشكل صحيح.

blobs(shas)

يقرأ عدة blobs بعملية git cat-file --batch واحدة: يكتب كل المعرّفات إلى stdin ويقرأ سجلات <sha> blob <size>\n<content>\n من البايتات الخام. إذا بدا أي شيء خاطئاً (سجل ناقص أو حجم غير صحيح)، يتجاهل النتيجة ويعود إلى git show <sha> لكل blob على حدة. هذا المسار أبطأ لكنه صحيح دائماً.

دوال مساعدة

  • rangeLineCount(ranges) — مجموع الأسطر في المدى (يُستخدم لحساب الأسطر المحللة).
  • lineInRanges(line, ranges) وspanOverlapsRanges(start, end, ranges) — تُستخدمان لتحديد ثقة النسب.

contributors.ts — من هم المطورون

aggregateContributors(commits, { identities, mergeSameName }) تحوّل الـ commits إلى مطورين.

الخطوة 1 — التجميع حسب البريد

يُجمع كل commit حسب بريده الإلكتروني بأحرف صغيرة. إذا كان البريد موجوداً في قائمة developers في الإعدادات، يُثبَّت في مجموعة ذلك الإدخال بدلاً من ذلك، فتصبح كل العناوين المدرجة شخصاً واحداً.

لكل مجموعة تتابع CleanLens: الأسماء التي ظهرت، والعناوين، وأيام النشاط (أيام تقويمية مختلفة بتوقيت UTC فيها commit)، وتاريخ أول وآخر commit، وعدد الـ commits. الاسم الأساسي هو الاسم الموجود على أقدم commit في المجموعة.

الخطوة 2 — دمج الهويات المكررة المحتملة (مفعّل افتراضياً)

عندما لا تكون mergeSameNameAuthors مساوية لـ false، تُدمج المجموعات التي تشترك في أي مفتاح دمج (بخوارزمية union-find):

المفتاحيُبنى منمثال على ما يلتقطه
exact:الاسم المعروض، بعد حذف المسافات الطرفية وتحويله لأحرف صغيرة وضم المسافاتAyman Khalil على بريدين مختلفين
name:الاسم بعد حذف كل ما ليس حرفاً أو رقماً وحذف الأرقام في آخره؛ فقط إذا كان 5 أحرف أو أكثرMUSAALAHMED4 ↔ MUSAALAHMED ↔ musa alahmed
mail:مُعرّف البريد: النص بعد آخر + في الجزء المحلي، أو الجزء المحلي كله؛ فقط إذا كان 3 أحرف أو أكثر وليس عاماًayman@work.com ↔ ayman@gmail.com؛ 1234+ayman@users.noreply.github.com

حمايات ضد الدمج الخاطئ:

  • الأسماء المضغوطة الأقصر من 5 أحرف تُتجاهل (user1 / user2، وAnas مقابل Anas Daas).
  • المعرّفات العامة تُتجاهل: noreply وno-reply وgit وgithub وgitlab وadmin وdev وme وhello وinfo وcontact وmail وemail وuser وusers وname وtest.

الخطوة 3 — الاسم المعروض

  • displayName من الإعدادات يغلب دائماً.
  • المجموعة ذات الاسم الواحد تستخدمه.
  • المجموعة التي دمجت أسماء مختلفة تعرضها كلها، الأقدم أولاً: "Primary Name - Other Name".

يُرتَّب المطورون حسب عدد الـ commits ويأخذون المعرّفات dev-1 وdev-2، … بهذا الترتيب. هذه المعرّفات ثابتة داخل التقرير الواحد فقط.

blameService.ts (قديم)

BlameService وparsePorcelain() يحللان مخرجات git blame --line-porcelain -w -M. ينتميان إلى محرّك "HEAD blame" السابق ولم يعد خط التحليل يستخدمهما. بقيا لأن الاختبارات تغطيهما ولأنهما مفيدان لميزات مستقبلية.

تخصيص هذه الطبقة

  • دمج الهويات ← developers في الإعدادات، أو ملف .mailmap (11 — دليل الضبط والتخصيص).
  • تغيير نافذة إعادة التشغيل ← analysis.maxCommits وanalysis.since و--full-history.
  • إيقاف الدمج التلقائي ← mergeSameNameAuthors: false.