FlowingDev

شرح الـ Diff: الخلطة السحرية وراء Git وكل مراجعات الكود

تعلم كيف تجد خوارزميات المقارنة (diff) الإضافات والحذف والتغييرات بدقة بين النصوص، المحرك الأساسي وراء أنظمة التحكم في الإصدارات والتعاون على الكود.

جرّب الأداة: مدقق الفروقات

في جملة واحدة

الـ 'diff' هو ملخص محسوب للفروقات الدقيقة بين ملفين أو كتلتين من النص، يوضح بالضبط ما تم إضافته أو حذفه أو تغييره للانتقال من الحالة "قبل" إلى "بعد".

المشكلة التي يحلها

تخيل عالم الحوسبة في أوائل السبعينيات. مساحة التخزين كانت مكلفة بشكل خيالي، والاتصال بكمبيوتر بعيد يتم عبر مودم أبطأ من سلحفاة نعسانة. أنت مطور في معامل بيل (Bell Labs)، وتحتاج إلى تحديث ملف كود مصدري على سيرفر في الطرف الآخر من الحرم الجامعي. الملف يتكون من بضعة آلاف من الأسطر، لكنك غيرت ثلاثة منها فقط.

هل سترسل الملف بأكمله مرة أخرى عبر هذا الاتصال البطيء كالعسل؟ طبعاً لا. هذا مضيعة للوقت والموارد. ما تريده حقًا هو إرسال التغييرات فقط.

كانت هذه هي المشكلة بالضبط التي دفعت دوغلاس ماكلروي إلى إنشاء أمر diff الأصلي لنظام التشغيل يونكس (Unix) في عام 1974. كان هدفه إنشاء أداة يمكنها برمجيًا إيجاد أصغر مجموعة من التغييرات، سطرًا بسطر، اللازمة لتحويل ملف إلى آخر. ناتج أمر diff هذا، وهو ملف "باتش" (patch)، كان صغيرًا جدًا ويمكن إرساله بسرعة. بعد ذلك، يمكن للمستقبل استخدام برنامج مصاحب، patch، لتطبيق هذه التغييرات على نسخته من الملف الأصلي، ليصبح محدثًا.

هذه الفكرة البسيطة والقوية — عزل التغيير نفسه كجزء من البيانات — كانت ثورية. إنها اللبنة الأساسية لجميع أنظمة التحكم في الإصدارات الحديثة مثل Git و Subversion و Mercurial. وهي المحرك وراء مراجعات الكود، وأدوات التعاون على المستندات (مثل وضع "اقتراح" في مستندات Google)، وأنظمة إدارة الإعدادات. إنها تحل المشكلة الجوهرية المتمثلة في تتبع وتوصيل التطور في أي نص رقمي.

كيف يعمل من الداخل

للوهلة الأولى، تبدو عملية المقارنة (diffing) بسيطة: مجرد مسح نصين وتحديد ما هو مختلف. ولكن لتنفيذ ذلك بكفاءة وإنتاج أصغر وأوضح مجموعة من الفروقات هو تحدٍ كلاسيكي في علوم الحاسوب. السر لا يكمن في البحث عما هو مختلف، بل عما هو متطابق.

أطول تتابع جزئي مشترك (Longest Common Subsequence - LCS)

تعتمد معظم خوارزميات الـ diff، بما في ذلك خوارزمية Hunt–McIlwain الشهيرة التي شغّلت diff الأصلي، على حل مشكلة "أطول تتابع جزئي مشترك" (LCS).

التتابع الجزئي هو سلسلة من العناصر التي تظهر بنفس الترتيب كما في السلسلة الأصلية، ولكن ليس بالضرورة بشكل متتالي. الـ LCS هو أطول تتابع من هذا النوع يكون مشتركًا بين سلسلتين.

لنستخدم مثالًا بسيطًا غير برمجي.

  • الأصلي: The quick red fox
  • الجديد: The slow red cat

الخوارزمية، التي تعمل سطرًا بسطر (أو في هذه الحالة، كلمة بكلمة)، تجد أن أطول تتابع جزئي مشترك هو: The red.

بمجرد العثور على الـ LCS، يصبح المنطق بسيطًا:

  • أي عنصر في النص الأصلي ليس ضمن الـ LCS لا بد أنه تم حذفه. (quick, fox)
  • أي عنصر في النص الجديد ليس ضمن الـ LCS لا بد أنه تم إضافته. (slow, cat)

من خلال العثور على أطول أساس من المحتوى المشترك، يمكن للخوارزمية تحديد جزر التغيير المحيطة به بوضوح وإيجاز. تنتج هذه الطريقة مجموعة دنيا من الفروقات، وهو ما نريده للحصول على diff نظيف ومفهوم.

من LCS إلى Diff قابل للقراءة

إيجاد التغييرات هو نصف المعركة فقط. النصف الآخر هو تقديمها بتنسيق موحد وقابل للقراءة. ربما رأيت هذا إذا نظرت يومًا إلى pull request على GitHub. التنسيق الأكثر شيوعًا هو "تنسيق diff الموحد" (unified diff format).

لنطبقه على مثال مختلف قليلاً:

  • الملف أ (القديم):
    An apple a day.
    Keeps the doctor away.
    Or so they say.
    
  • الملف ب (الجديد):
    An apple a day,
    Keeps the doctor away.
    For what it's worth.
    

ستقوم أداة diff بإنشاء شيء كهذا:

--- a/file_a.txt
+++ b/file_b.txt
@@ -1,3 +1,3 @@
-An apple a day.
+An apple a day,
 Keeps the doctor away.
-Or so they say.
+For what it's worth.

دعنا نحلل ذلك:

  • --- a/file_a.txt: الملف "المصدر" (from). تشير علامة - إلى مصدر الحذف.
  • +++ b/file_b.txt: الملف "الهدف" (to). تشير علامة + إلى مصدر الإضافات.
  • @@ -1,3 +1,3 @@: هذا هو "رأس المقطع" (hunk header). يبدو غامضًا بعض الشيء، لكنه يعطيك السياق. -1,3 تعني "يبدأ هذا المقطع من السطر 1 وطوله 3 أسطر في الملف الأصلي." +1,3 تعني "يبدأ هذا المقطع من السطر 1 وطوله 3 أسطر في الملف الجديد."
  • الأسطر التي تبدأ بمسافة ( ) هي أسطر سياق. هي متطابقة في كلا الملفين وتُعرض لمساعدتك على فهم مكان حدوث التغيير.
  • الأسطر التي تبدأ بـ - هي عمليات حذف. توجد فقط في النص "قبل".
  • الأسطر التي تبدأ بـ + هي عمليات إضافة. توجد فقط في النص "بعد".

تُظهر الأداة التغيير من An apple a day. إلى An apple a day, ليس كتعديل لسطر واحد، بل كحذف للسطر القديم وإضافة للسطر الجديد. هذا النهج القائم على الأسطر هو سمة أساسية لمعظم أدوات diff التقليدية.

ما بعد النص العادي: الـ Diffs الدلالية (Semantic Diffs)

يعتبر الـ diff القياسي القائم على الأسطر رائعًا للنصوص النثرية أو البرمجية، ولكنه يفشل مع البيانات المهيكلة مثل JSON أو XML أو YAML.

خذ هذا الـ JSON بعين الاعتبار:

// Original
{
  "name": "Alex",
  "role": "Developer"
}

وهذا:

// New
{
  "role": "Developer",
  "name": "Alex"
}

الـ diff النصي سيرى هذا كمسح كامل وإعادة كتابة:

-  "name": "Alex",
-  "role": "Developer"
+  "role": "Developer",
+  "name": "Alex"

هذا صحيح تقنيًا ولكنه عديم الفائدة دلاليًا. ترتيب المفاتيح في كائن JSON لا يهم بشكل عام. أداة semantic diff أكثر ذكاءً. فهي تحلل النص أولاً إلى بنية بيانات، ثم تقارن البنى. ستحدد بشكل صحيح أن هذين الكائنين من JSON متطابقان، مما يؤدي إلى عدم وجود فروقات. هذا أمر حاسم لمقارنة ملفات الإعدادات، أو استجابات الـ API، أو أي بيانات مهيكلة أخرى تهتم فيها بالمعنى وليس فقط بتنسيق النص.

قصص من الواقع

بق الحرف الواحد الذي عطّل عملية الدفع

كان مطور مبتدئ يدمج مزود دفع جديدًا. نسخ مثال طلب الـ API من التوثيق، وضع مفاتيحه، وقام بتشغيله. فشل. حاول مرة أخرى. فشل. أمضى ساعات يحدق في كوده وفي التوثيق، مقتنعًا بأنهما متطابقان. في حالة من الإحباط، قام بلصق مثال التوثيق "العامل" في جانب واحد من مدقق diff وكوده الخاص في الجانب الآخر.

في البداية، بدا كل شيء متطابقًا. لكنه لاحظ بعد ذلك تمييزًا خفيًا في نهاية سطر مفتاح الـ API الخاص به. مسافة زائدة واحدة غير مرئية في آخر السطر. عملية النسخ واللصق من صفحة الويب قد أدرجتها، وكان كوده يرسلها بأمانة، مما أبطل صلاحية المفتاح. أداة الـ diff، التي رأت المسافة كأي حرف آخر، كانت "العين" الوحيدة التي يمكنها اكتشافها.

الدرس: الـ diff هو مجهرُك النهائي. ليس لديه أي افتراضات وسيُظهر لك بالضبط ما هو موجود، بما في ذلك الحروف غير المرئية التي يمكن أن تدمر نظامًا بأكمله.

ورطة انحراف الإعدادات

بدأ موقع ويب ذو حركة مرور عالية يواجه أخطاء غريبة ومتقطعة. المهندسة المناوبة، مايا، كانت في حيرة. آخر تحديث تم نشره كان قبل أسبوع وكان مستقرًا. لا شيء في السجلات يشير إلى سبب واضح. حدسها أخبرها أن شيئًا ما على الخادم قد تم تغييره يدويًا.

سحبت ملف إعدادات Nginx الرسمي من مستودع Git الخاص بهم، ثم دخلت عبر SSH إلى خادم الإنتاج ونسخت الإعدادات الفعلية قيد التشغيل. لصقت كليهما في أداة diff. bingo! كانت هناك ثلاثة أسطر مختلفة. أضاف أحدهم قاعدة إعادة توجيه "مؤقتة" مباشرة على الخادم لإصلاح مشكلة بسيطة الأسبوع الماضي ونسي أمرها تمامًا. هذا "الإصلاح" كان الآن يتعارض مع أنماط حركة المرور الجديدة. أزالت مايا الأسطر الدخيلة، واختفت الأخطاء. على الفور، طبق الفريق سياسة لمراجعة إعدادات الخادم مقابل Git يوميًا.

الدرس: نظام التحكم في الإصدارات الخاص بك هو مصدر الحقيقة. مقارنة الواقع بهذا المصدر باستخدام الـ diff هي أفضل طريقة لاكتشاف "انحراف الإعدادات" (configuration drift) والعثور على التغييرات غير المصرح بها أو المنسية.

مراجعة الكود التي على طريقة "يبدو جيدًا بالنسبة لي"

تلقى مطور خبير، بن، طلب سحب (pull request) من موظف جديد. كان العنوان "تحديثات". كان الـ diff بحرًا من الأحمر والأخضر عبر 20 ملفًا وأكثر من 3000 سطر. كان يحتوي على ميزة جديدة، وإصلاح لـ bug لا علاقة له بالموضوع، وإعادة تنسيق ضخمة للكود للتبديل من tabs إلى spaces، وترقية مكتبة برمجية. كان من المستحيل مراجعته. هل كان إصلاح الـ bug صحيحًا؟ هل أدخلت الميزة الجديدة ثغرة أمنية؟ كان كل شيء مخفيًا في عاصفة ثلجية من تغييرات المسافات البيضاء.

رفض بن الـ PR مع ملاحظة لطيفة: "أهلاً بك! الـ diff الخاص بالـ PR يروي قصة. هذا الـ diff يحاول أن يروي أربع قصص مختلفة في وقت واحد. هل يمكنك من فضلك تقسيم هذا إلى أربعة PRs منفصلة؟" فعل الموظف الجديد ذلك. تمت الموافقة على PR إعادة التنسيق على الفور. كان من السهل التحقق من إصلاح الـ bug. كانت ترقية المكتبة واضحة ومباشرة. وأخيرًا، يمكن مراجعة الميزة الجديدة بمفردها.

الدرس: قيمة الـ diff تتناسب عكسيًا مع حجمه وتعقيده. الـ diffs الصغيرة والمركزة التي تمثل تغييرًا منطقيًا واحدًا يسهل مراجعتها وفهمها وتصحيحها لاحقًا.

الأخطاء والفخاخ الشائعة

  • تجاهل المسافات البيضاء (whitespace). التغيير من tabs إلى spaces أو إضافة سطر جديد في نهاية الملف يمكن أن يبدو كتغيير هائل على مستوى الملف بأكمله لأداة diff. في حين أن هذا يكون مقصودًا أحيانًا، إلا أنه غالبًا ما يخلق ضوضاء تخفي التغييرات الحقيقية وذات المغزى. قم بتهيئة أدواتك لتجاهل أو إبراز تغييرات المسافات البيضاء بشكل مناسب.
  • الـ diff "الأعمى دلاليًا". كما ذكرنا سابقًا، استخدام diff نصي عادي على بيانات مهيكلة مثل JSON أو XML يمكن أن يكون مضللاً للغاية. إعادة ترتيب السمات أو المفاتيح يمكن أن تبدو كتغيير كبير بينما، من الناحية الوظيفية، لم يتغير شيء. استخدم دائمًا أداة diff مدركة للدلالات (semantic-aware) لهذه التنسيقات.
  • نسيان السياق. يوضح لك الـ diff ماذا تغير، لكنه لا يخبرك أبدًا لماذا. هذه هي وظيفة رسالة الـ commit أو وصف الـ pull request. الـ diff بدون سياق يشبه إجابة بدون سؤال؛ من الصعب الحكم على ما إذا كانت صحيحة أم خاطئة.
  • إنشاء diffs "فرانكشتاين". تجميع تغييرات غير ذات صلة في commit واحد (إصلاح bug، وميزة، وتصحيح خطأ إملائي) يجعل الـ diff كابوسًا للقراءة. ويجعل من المستحيل التراجع عن أحد هذه التغييرات لاحقًا دون التأثير على الآخرين. يجب أن يكون كل commit تغييرًا منطقيًا وذريًا واحدًا.

لماذا يجب أن يكون على رادارك

فهم الـ diffing ليس اختياريًا للمطور الحديث؛ إنه أساسي مثل معرفة كيفية استخدام لوحة المفاتيح. ستواجه الـ diffs عدة مرات في اليوم، كل يوم:

  • عندما تشغل git status أو git diff لرؤية عملك الذي لم يتم عمل commit له.
  • عندما تنشئ pull request ليراجعه زملاؤك.
  • عندما تراجع pull request لشخص آخر.
  • عندما تستخدم git blame لمعرفة من كتب سطرًا معينًا من الكود ولماذا.
  • عندما تقوم بتصحيح مشكلة عن طريق مقارنة إعدادات تعمل مع إعدادات معطلة.

حتى بالنسبة لغير المطورين، المفهوم قوي. إنه "تتبع التغييرات" في مستند Word الخاص بك. إنه سجل الإصدارات لمقالة ويكيبيديا الخاصة بك. إنها القدرة على رؤية كيف تطور عقد بين المسودات. فهم الـ diffing هو فهم كيفية إدارتنا وتواصلنا بشأن التغيير في العالم الرقمي. إنه السجل القابل للتدقيق والتحقق للتقدم.

تعمق أكثر

انتهينا من النظرية. حان وقت التطبيق — 100% في متصفحك.

جرّب الأداة: مدقق الفروقات