طبقة Git
فحص المستودع، وقائمة الـ commits، والفروقات، وقراءة الـ blobs، ودمج هويات المطورين
كل الوصول إلى Git موجود في src/git/. كل أمر يُشغَّل عبر execFile("git", args, { cwd: root }). لا يوجد shell في المنتصف، لذلك المسارات التي فيها مسافات أو رموز خاصة آمنة. وعلى Windows يحوّل execFile الاسم git إلى git.exe.
الأوامر التي تشغّلها CleanLens
| الأمر | المكان | الغرض |
|---|---|---|
git rev-parse --is-inside-work-tree | gitService.ts | هل المجلد مستودع؟ |
git rev-parse HEAD | gitService.ts | هل فيه commit واحد على الأقل؟ |
git log --no-merges --format=%aN|%aE|%aI | gitService.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 --batch | diffService.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[]:
| الحقل | المعنى |
|---|---|
status | added و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.