Git وتدفقات العمل التعاونية

المستودع الموحد مقابل المستودعات المنفصلة

18 دقيقة الدرس 9 من 28

المستودع الموحد مقابل المستودعات المنفصلة

طريقة تنظيم الكود عبر المستودعات تشكّل ثقافة الهندسة بأكملها — مدى سرعة شحن الفرق للمنتج، ومدى تباعد الاعتماديات، ومدى صعوبة التغييرات التي تمسّ مكونات متعددة في آنٍ واحد. يحلّل هذا الدرس قرار المستودع الموحد مقابل المستودعات المنفصلة على مستوى الإنتاج الفعلي، ويفحص الأدوات الحقيقية التي تجعل كل نهج قابلاً للتطبيق، ويشرح ما تفعله Google وMeta وMicrosoft وAirbnb فعلياً والسبب وراء ذلك.

التعريفات الأساسية

المستودع الموحد (Monorepo) يخزّن مشاريع متعددة — خدمات، ومكتبات، وكود بنية تحتية، وأدوات — في مستودع Git واحد تحت رسم بياني موحّد للإصدارات. أما المستودعات المنفصلة (Polyrepo) فتمنح كل مشروع مستودعه الخاص بفروعه وخطوط CI وإيقاع إصداراته المستقل. يوجد نمط ثالث هجين يُعرف بـMeta-repo يستخدم مستودعًا رئيسيًا رفيعًا يجمع المستودعات المنفصلة عبر Git submodules أو أدوات كـmeta — لكنه يرث أسوأ ما في النهجين ونادرًا ما يكون الخيار الأمثل.

لماذا اختارت الشركات العملاقة المستودع الموحد

تدير Google أكبر مستودع موحد في العالم: مستودع داخلي واحد يُعرف بـ"google3" يحتوي على أكثر من 86 تيرابايت من البيانات و2 مليار سطر من الكود، ويخدم عشرات الآلاف من المهندسين. مستودع Meta المُوحّد "fbsource" بحجم مماثل. بنت كلتا الشركتين أدوات مخصصة (Bazel وBuck على التوالي) لأن أي نظام VCS جاهز لم يكن قادرًا على التوسع بهذا الحجم. مبرراتهم الأساسية:

  • التغييرات العرضية الذرّية — أعد تسمية واجهة برمجية، وحدّث كل من يستخدمها في إيداع واحد. لا حاجة لطبقات توافق للإصدارات ولا تنسيق "تغيير كاسر" بين المستودعات.
  • مصدر حقيقة واحد لإصدارات الاعتماديات — لا مشكلة الاعتمادية الماسية (diamond dependency) بين الفرق.
  • استثمار الأدوات المشتركة موزّع على الجميع — إعداد lint واحد، قالب CI واحد، سياسة ماسح أمني واحدة.
  • إعادة استخدام الكود واكتشافه أسهل — يمكن للمهندسين قراءة أي مكتبة والمساهمة فيها دون التبديل بين المستودعات.
فكرة محورية: المستودع الموحد لا يعني النشر المتكامل. مستودع Google يحتوي على مئات الخدمات القابلة للنشر باستقلالية. المستودع موحّد؛ البيئة التشغيلية ليست كذلك.

أين تتفوق المستودعات المنفصلة

ليست كل مؤسسة بحجم Google. المستودعات المنفصلة هي الخيار الافتراضي الصحيح عندما:

  • استقلالية الفريق أهم من التنسيق — قطارات إصدار مستقلة، مكدسات تقنية مختلفة، مناوبات استجابة منفصلة.
  • المساهمون الخارجيون أو المصدر المفتوح — منح مورّد وصولاً لمستودع خدمة واحدة دون كشف كل شيء آخر.
  • حدود الامتثال التنظيمي — كود نطاق PCI، وخدمات معالجة بيانات HIPAA، أو بنية تحتية تحتوي على أسرار يمكن عزلها بضوابط وصول أضيق.
  • الحجم في المراحل المبكرة — قبل أن يكون لديك استثمار أدواتي يجعل المستودع الموحد سريعًا، تتميز المستودعات المنفصلة بأقل احتكاك تشغيلي.
Monorepo vs Polyrepo structure comparison Monorepo Single Git Repository services/api services/auth libs/ui infra/terraform infra/k8s tooling Shared CI pipeline, lint, versions Atomic cross-cutting commits + إيداع واحد يعدّل كل المستخدمين + لا تباعد إصدارات بين الفرق - Git لا يتوسع دون أدوات إضافية Polyrepo repo: api own CI + releases repo: auth own CI + releases repo: shared-ui semver versioned repo: infra own CI + releases api pins shared-ui@1.4 auth pins shared-ui@1.7 — version drift + عزل قوي بين الفرق + تحكم وصول محدود لكل مستودع - التغييرات العرضية تحتاج N طلبات سحب
المستودع الموحد يوحّد الكود تحت تاريخ واحد؛ المستودعات المنفصلة تعزل الملكية لكنها تخلق تباعدًا في الاعتماديات وتكاليف تنسيق بين المستودعات.

المشكلة الحقيقية للمستودع الموحد: Git لا يتوسع

صُمِّم Git لتطوير نواة Linux — مشروع واحد ومئات المساهمين. على مستوى المستودع الموحد، يستغرق git status دقائق على شجرة بـ10 ملايين ملف، وgit clone أمر غير عملي، وgit log --all يصبح ضوضاء. آليتان تعالجان هذا مباشرة.

الفحص المتفرق في وضع المخروط (Sparse Checkout - Cone Mode)

يتيح الفحص المتفرق للمطوّر سحب المجلدات التي يحتاجها فقط. اعتبارًا من Git 2.25، وضع المخروط (cone mode) هو النهج الموصى به في بيئات الإنتاج — يقيّد المسارات إلى مجموعة من المجلدات باستخدام مطابقة نمط بسيطة أسرع بمراتب من النهج القديم القائم على wildcards.

# استنساخ بدون تجسيد أي ملفات في دليل العمل (مخزن الكائنات فقط) git clone --filter=blob:none --no-checkout https://github.com/org/monorepo.git cd monorepo # تفعيل الفحص المتفرق في وضع المخروط السريع git sparse-checkout init --cone # سحب المجلدات التي يمتلكها فريقك فقط git sparse-checkout set services/payments libs/shared-utils # التحقق مما تم تجسيده محلياً git sparse-checkout list # services/payments # libs/shared-utils git checkout main # لاحقاً: إضافة نطاق جديد دون إعادة الاستنساخ git sparse-checkout add infra/terraform
نصيحة إنتاجية: ادمج --filter=blob:none (الاستنساخ الجزئي — يحذف blobs الملفات حتى الحاجة) مع --depth=1 (الاستنساخ الضحل) للحصول على أسرع عملية استنساخ ممكنة. أفاد مهندسون في Shopify وTwitter بتقليص وقت الاستنساخ من 45 دقيقة إلى أقل من دقيقتين على مستودعات موحدة ضخمة باستخدام هذين الخيارين معاً.

العزل على مستوى نظام البناء: Bazel وNx

المشكلة الأعمق في المستودع الموحد ليست Git — بل CI. إذا كان كل إيداع يؤدي لإعادة بناء كاملة، فإن مستودعاً موحداً بـ500 حزمة يصبح غير قابل للاستخدام. الحل هو نظام بناء يفهم رسم الاعتمادية ويعيد البناء والاختبار للحزم المتأثرة فقط.

Bazel (النسخة مفتوحة المصدر من Blaze الداخلي لـGoogle) يُصنّف كل هدف بناء بشكل صريح. ملف BUILD يعلن المدخلات والمخرجات والاعتماديات. Bazel يُجزّئ المدخلات ويخزّن المخرجات عن بُعد — إذا لم يتغير شيء في المنبع، تُقدَّم نتيجة الاختبار من الكاش في ميلي ثانية.

# مثال: ملف Bazel BUILD لخدمة Go (services/payments/BUILD.bazel) load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_test") go_binary( name = "payments", srcs = glob(["*.go"]), deps = [ "//libs/shared-utils:utils", "@com_github_gin_gonic_gin//:gin", ], visibility = ["//visibility:public"], ) go_test( name = "payments_test", srcs = ["payments_test.go"], embed = [":payments"], ) # يحسب Bazel تلقائياً مجموعة الحزم المتأثرة # bazel build //services/payments:payments # bazel test //... --build_event_protocol=bep.json

لمستودعات JavaScript/TypeScript الموحدة، تؤدي Nx وTurborepo الدور ذاته. Nx يبني رسم المشروع من اعتماديات package.json ومسارات tsconfig؛ Turborepo يستخدم pipeline معرّفاً في turbo.json. كلاهما يدعم الكاش عن بُعد عبر خلفيات مستضافة (Nx Cloud، Vercel).

# turbo.json — Turborepo pipeline لمستودع JS موحد { "$schema": "https://turbo.build/schema.json", "pipeline": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**", ".next/**"] }, "test": { "dependsOn": ["build"], "inputs": ["src/**", "tests/**"] }, "lint": { "outputs": [] } }, "remoteCache": { "enabled": true } } # تشغيل الـpipeline الكامل — تُعاد بناء الحزم المتغيرة فقط npx turbo run build test --filter=...[origin/main]

ما تفعله العمالقة فعلياً

  • Google — مستودع موحد (google3)، Bazel، Piper VCS (خاص)، Critique لمراجعة الكود. تقريباً كل الكود في مستودع واحد.
  • Meta — مستودع موحد (fbsource)، Sapling VCS (مفتوح المصدر 2022)، Buck2. اختاروا Sapling بدلاً من Git لأن نموذج كائنات Git لا يتعامل مع هذا الحجم.
  • Microsoft — مستودع Windows يستخدم GVFS (Git Virtual File System) لافتراضية نظام الملفات بحيث يُنزّل Git الملفات عند الوصول فقط. فتحوا المصدر كبديل عملي لـsparse checkout الكامل.
  • Airbnb / Lyft — مستودعات منفصلة مُنظَّمة حسب فريق الـdomain؛ يستثمرون بكثافة في أدوات المنصة للحفاظ على تزامن إصدارات الاعتماديات عبر المستودعات.
  • Shopify — هاجروا من monolith Rails إلى مستودع موحد قائم على المكوّنات باستخدام packwerk لتطبيق الحدود دون تقسيم المستودعات.
فخ إنتاجي: الهجرة من مستودعات منفصلة إلى موحد في منتصف النمو مؤلمة للغاية. تعيد كتابة تاريخ Git (باستخدام git filter-repo أو git subtree) لدمج التاريخيات وإعادة بناء pipelines CI من الصفر. اتخذ القرار المعماري مبكراً، قبل أن يكون لديك 40 مستودعاً و200 مهندس.

إطار اتخاذ القرار

استخدم هذا النموذج الذهني عند الاختيار:

  1. كم مرة تشترك خدماتك في الكود؟ كلما زاد الكود المشترك والتغييرات العرضية، كلما زادت مزايا المستودع الموحد.
  2. هل لديك فريق منصة لبناء أدوات المستودع الموحد؟ بدون Bazel/Nx/Turborepo وكاش عن بُعد، يتحول المستودع الموحد إلى عنق زجاجة CI في غضون أشهر.
  3. هل التحكم في الوصول أو حدود الامتثال متطلب صارم؟ إذا نعم، فالمستودعات المنفصلة هي الطريق الأسهل.
  4. ما حجمك الحالي؟ تحت ~10 خدمات و~20 مهندساً، كلا النهجين يعمل؛ حسّن تجربة المطوّر لا نقاء المعمارية.
وسط عملي: كثير من الشركات متوسطة الحجم تستخدم مستودعاً موحداً محدود النطاق — مستودع موحد واحد لكل نطاق تجاري (مثلاً platform-monorepo، data-monorepo). يجني معظم فائدة الأدوات المشتركة مع إبقاء التحكم في الوصول قابلاً للإدارة وأداء Git معقولاً دون أدوات VCS متخصصة.

أنماط الهجرة العملية

إذا كنت بحاجة لدمج مستودع منفصل موجود في مستودع موحد، يُعدّ git subtree النهج المعياري للحفاظ على التاريخ:

# دمج repo-B في المستودع الموحد تحت مسار services/auth (مع الحفاظ على التاريخ) cd monorepo # إضافة المستودع المصدر كـremote git remote add auth-origin https://github.com/org/auth-service.git git fetch auth-origin # قراءة شجرة auth-service بأكملها في مجلد فرعي git read-tree --prefix=services/auth -u auth-origin/main # إيداع الدمج git commit -m "chore: import auth-service into monorepo (history preserved)" # في الاتجاه العكسي: استخراج services/payments إلى مستودع خاص به git subtree split --prefix=services/payments -b split/payments git push https://github.com/org/payments-service.git split/payments:main

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