لطالما كانت أنظمة التحكم في الإصدار أداة يومية للمطورين. في المستودعات الاحتكارية الكبيرة ، تكون متطلباتها محددة للغاية. لهذا السبب ، إما أن تقوم الشركات بتكييف الحلول الحالية ، كما يفعل Facebook مع Mercurial و Microsoft مع Git ، أو تطوير أنظمتها الخاصة: Piper و CitC على Google و Arc VCS على Yandex.
في التقرير ، يخبر المطور Vladimir Kikhtenko
kikht سبب احتياج Yandex لنظام التحكم في الإصدار الخاص به وكيف يعمل. فكر في الأمر من جانب مطور عادي: كيفية الوصول إلى الكود المصدري ، وتخصيص فرع للتطوير ، ودمج التغييرات في قاعدة الكود الشائعة. ننظر تحت الغطاء - نتعرف على التمثيل الداخلي للبيانات وعرضها في نظام ملفات افتراضي مع نسخة عاملة. سنناقش الصعوبات في تنفيذ وظائف VCS في نظام الملفات الظاهري وعند تحميل البيانات بتكاسل. دعونا نتحدث عن كيفية ضمان موثوقية البنية التحتية للخادم في المستودع.
في النهاية ، يمكنك مشاهدة سجل غير رسمي للتقرير.
- مساء الخير جميعا ، اسمي فلاديمير. سمعت جميعًا خطبًا حول عدم كتابة الدراجات. سيكون تقريري على الجانب الآخر من الحاجز.
في الواقع ، تمتلك Yandex مستودعًا احترافيًا به الكثير من التعليمات البرمجية. وتوصلنا إلى استنتاج مفاده أننا نعمل على تطوير نظام التحكم في الإصدار الخاص بنا.

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

ما هو حجم مشكلتنا؟ فيما يلي بعض الأرقام: 6 ملايين عملية ارتكاب ، وحوالي 2 مليون ملف فردي. الحجم الكلي مع السجل الكامل للمستودع هو 2 تيرابايت. لتوضيح معنى هذه الأرقام مقارنة بالمستودعات النموذجية الأخرى ، إليك رسم بياني. GitHub median هو متوسط حجم مستودع التخزين على GitHub ، 1 ميغابايت. المئوية التسعين على جيثب هو ما أسماه زملائي "مستودع ابن صديقة أمي". وكل شيء آخر هو المستودعات الكبيرة الشهيرة.

بقدر ما أعرف ، فإن أكبر مستودع في العالم هو مع جوجل. ويرد تقدير لحجمها من مقال في عام 2015 - ربما منذ ذلك الحين نمت. كما ترون ، المقياس لوغاريتمي. يمكن أن نرى أننا أيضًا كبيرون جدًا.
كيف تعمل أنظمة التحكم في الإصدار المختلفة عند محاولة تنزيل هذا المستودع بالكامل؟ بطبيعة الحال ، لم نبدأ على الفور في تطوير نظام التحكم في الإصدار الخاص بنا. لقد حاولنا تحويل مستودعنا إلى أنظمة مختلفة. تم إجراء أخطر محاولة مع Mercurial. ونتائج وقت العمليات النموذجية لا تزال لا تناسبنا.

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

كيف حققنا هذا؟ الميزة الرئيسية: نسخة العمل التي ننشئها ليست ملفًا حقيقيًا على القرص. هذا هو نظام الملفات الظاهري. على نظامي Linux و Mac ، يتم ذلك باستخدام الصمامات ، على نظام Windows مع ProjFS. إننا نحمل جميع البيانات بتكاسل ، بحيث يتم استخدام مساحة كبيرة على القرص كما نحتاج إليها حقًا ، ونحن لا نحاول تحميل كل شيء مقدمًا. وننفذ جميع أنواع العمليات الثقيلة إلى الخادم. على وجه الخصوص - تشغيل السجل وبعض أكثر.

تقوم واجهة نظام التحكم في الإصدار الخاص بنا ، بشكل عام ، بتكرار Git ، لذلك لن أعرض كيف يبدو سير العمل النموذجي. تخيل جيت. كل شيء هو نفسه: الخروج للحصول على المراجعة المطلوبة ، وفرع لإنشاء فروع ، والالتزام لالتزامات ، ويدعم أيضا في stash بنفس الطريقة. ماذا يعطي هذا النهج؟ نحن نخفف بدرجة كبيرة من عتبة الدخول. يمكن لمعظم المطورين داخل وخارج ياندكس العمل مع جيت. ليس لديهم لتعلم أي شيء جديد.
من ناحية أخرى ، ليس لدينا هدف في إحداث انخفاض في استبدال Git. سأتحدث عن هذا لاحقًا بمزيد من التفاصيل. لدعم كل مجموعة متنوعة من فرق git يبدو مجنون ، نحن بالكاد بحاجة لهم جميعا.

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

كيف تبدو البنية؟ هناك كائنات ارتكاب ، كل التزام له أسلاف منفصلة أو عدة. وبهذه الطريقة يقومون ببناء بعض القصص DAG (الرسم البياني acyclic).

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

المشكلة مع B ، والتي لا لزوم لها. يبدو أننا لم نتمكن من الدخول فيه ، لكننا نظرنا إليه. وكلما زاد الفرق بين الفرع والجذع باستخدام مثال ، كلما زاد عدد الالتزامات الإضافية التي سنجدها. في حالة وجود مستودع أحادي ، عندما يكون معدل الالتزام بجذع مرتفعًا بدرجة كافية ، يمكن أن تكون هذه المسافة كبيرة جدًا. وسوف يكون هناك عشرات الآلاف من هذه الالتزامات الإضافية.


في حالة وجود أرقام جيل ، يمكننا استخدام قائمة انتظار الأولوية عند الزحف ، وسيظهر الزحف مثل هذا: مرة واحدة - والعثور على ما تحتاجه على الفور.

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

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

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

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

كيف يتم دمج نظامنا مع SVN؟ يستمر مستودع SVN في العيش. علاوة على ذلك ، فإن نظام التحكم في الإصدار الخاص بنا ليس مكتفياً ذاتياً. كيف تعمل في هذا الجزء؟ في البداية ، هناك بعض مكونات المحول التي تراقب حالة مستودع SVN وتحول التزامات SVN إلى تطبيقات commits - نظامنا للتحكم في الإصدار.
بعد ذلك ، هناك عميل يحمّل نسخة عمل وينتقل إلى الخادم للبيانات. عندما يرتكب أحد المطورين شيئًا ما ، يتم إرساله أولاً إلى خادم Arc ، ولكن حتى يتم الانتقال إلى هذه التغييرات ، وهي فرعنا الرئيسي ، يجب عليهم الانتقال إلى نظام طلب التجمع ونظام مراجعة الكود. هنا تأتي خدمة أخرى تراقب فروع Arc ، وإذا تم تحديثها ، ترسل طلب تجمع إلى مراجعة كود النظام لدينا. التالي هو نظام مراجعة الكود ، عندما تقرر ضرورة دمج هذا التصحيح ، يلزمه بـ SVN. ليس بهذه البساطة: إنه يضيف قدرًا معينًا من البيانات التعريفية إلى أن هذا الالتزام هو في الواقع دمج لمثل هذا الفرع وكذا من Arc. ومن ثم يرى هذا الالتزام بالفعل المحول ، ويعثر على بيانات التعريف هذه ويخلق التزامًا في خادم Arc. هذه هي دورة ارتكابها. لذلك ، بينما لا يمكننا العيش بدون SVN ، لأننا لدينا صندوق في SVN.
تتم مزامنة الفرع الرئيسي باستمرار مع خادمنا ، لكننا لا نسمح بالالتزام به مباشرة.

حول موثوقية الخلفية. بالطبع ، نحن نخطط أن يستخدم كل مطوري Yandex هذا الشيء ، لذلك من المهم بالنسبة لنا ألا ينهار. هذا معيار داخل الفهرس: خدماتنا يجب أن تصمد أمام فشل أي مركز بيانات. نظام التحكم في الإصدار ليس استثناء. هنا ، نحن ننقذ إلى حد كبير حقيقة أن YDB يدعم هذا. وخلفياتنا عديمة الجنسية ، وهناك أجزاء مختلفة يتم تنفيذها بطرق مختلفة قليلاً. الخوادم التي تعمل على كائنات Arc تعمل على الفروع ، فهي عديمة الحالة ، متماثلة. يتم نسخ المحولات التي يتم تحويلها باستمرار من SVN وفقًا للمخطط النشط. هناك العديد من المحولات التي تعمل في وقت واحد ، ويقومون بالتحويل في نفس الوقت ، وفي اللحظة التي يحاولون فيها تحديث فرع Arc ، يقومون بحل التعارضات. واحد نجح ، والآخر فشل. إنه يحاول تحويل شيء آخر.
يتم نسخ خدمة طلب البلياردو بواسطة السيد والعبد. هناك عمل واحد رئيسي. إذا فشلت ، يتم اختيار واحدة جديدة من خلال YDB. هناك شيء رائع مثل الإشارات ، التي لديها ضمانات جدية للوصول ، والموثوقية. يتم الوصول إلى تسلسل الإشارات بالكامل. نحن نستخدم الإشارات لكل من خدمة اكتشاف طلب التجميع واختيار القادة.
قليلا عن كيفية عمل العميل. هذا هو الجزء الأكثر صعوبة في نظام التحكم في الإصدار ، نظرًا لوجود نظام ملفات افتراضي. في الواقع ، نحن مضطرون لتنفيذ جميع العمليات على الملفات لوحدنا. سأجري بعض العمليات الأساسية ، أصف تقريبًا على الأصابع ما يحدث في الداخل عندما نقوم بها.

على سبيل المثال ، فتحنا ملف للتسجيل. عندما فتحنا الملف للكتابة ، نجد النقطة المقابلة لطراز كائننا. إذا لزم الأمر ، قم بتحميل شيء من الخادم. إذا أنشأنا ملفًا فعليًا في متجر خاص ، فسيتم تقديم جميع الطلبات الأخرى التي تذهب إلى هذا الملف هناك. وبالتالي ، إلى أن يتم الالتزام بالتغييرات المترجمة (في Git يطلق عليها unstaged) ، فإنها تدخل في التخزين المؤقت. نحن نسمي هذه الملفات تتحقق.

إذا فتحنا الملف للقراءة ، فلن نتمكن من تجسيد أي شيء ، ولكن ببساطة إعطاء البيانات مباشرة من النقطة.

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

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

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

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

لماذا قد لا يعمل هذا؟ كان الإصدار الأول حول هذا الموضوع. تكمن المشكلة في جميع أنواع العمليات الصعبة مثل arc reset –soft. أنها تحول لنا التبديل شجرة ، ولكن لا تجسد الملفات. أنها لا تزال موجودة في مكان ما المقدسة. لدينا أيضًا ملفات لم يتم تعقبها وتجاهلها ، والتي تحتاج أيضًا إلى معالجتها بطريقة خاصة. في هذا المكان ، جمعنا الكثير من المكابس وتوصلنا في النهاية إلى نتيجة مفادها أننا ما زلنا بحاجة إلى أن نأخذ شجرة (نسخة عمل واحدة الآن) أثناء الخروج ، ونأخذ شجرة الالتزام الذي نتحول إليه ، ونأخذ الفهرس ، ونعمل عليه بدقة. انتظر
ولكن من حيث تعقيد الخوارزميات ، لم نفقد أي شيء هنا: كل هذه الأشجار من التغييرات المحلية تتناسب مع التغييرات التي أجريناها. لذلك ، لا ينبغي لنا أن نتجول في المستودع بالكامل بهذه العمليات ، فهي لا تزال تعمل بسرعة كبيرة.
في الوقت نفسه ، نقوم ببعض السحر حتى تكون الطوابع الزمنية التي نعطيها للملفات صحيحة إلى حد ما. إذا قمنا فقط بتخزين الملفات في نظام الملفات ، فسيراقب ذلك ، ويمضي الوقت دائمًا. نحن هنا يجب أن نتذكر بطريقة ما الملف الذي شاهده المستخدم في أي لحظة. وإذا تحول إلى التزام سابق ، فلا تبدأ في إعطائه وقت سابق. نظرًا لأن أنظمة التجميع ، فإن جميع IDEs غير جاهز لذلك ، فإنها تأخذ الكثير من الأشياء.

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

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

ما هي خططنا؟ يستخدم 20٪ من مطوري monospository بالفعل نظامنا للتحكم في الإصدار. لقد خرجنا بالفعل من نوع ما من حالة الطفولة ، وهذا نظام يستخدم على محمل الجد ، من المستحيل ببساطة التخلص من هذا القبيل. الهدف النهائي هو أن تصبح نظام التحكم في الإصدار الرئيسي في ياندكس. يجب أن نقنع بطريقة أو بأخرى 80٪ المتبقية من المطورين بأننا مستقرون إلى حد ما وموثوقة وقابلة للاستخدام. من الواضح أنك بحاجة إلى إصلاح جميع الأخطاء وإنهاء تلك الميزات الموجودة في Git.
وبطبيعة الحال ، في بعض المنظور ، نخطط لأن تصبح مكتفية ذاتيًا ، أو تتخلى عن المحول أو تنشره في الاتجاه المعاكس ، بحيث تنتقل جميع التغييرات أولاً إلى Arc ، ثم إلى SVN للمبرمجين الأكثر ثباتًا.
الآن لدينا تحد كبير - دمج نظام التحكم في الإصدار في التجميع التلقائي لدينا ، في CI وخط أنابيب آخر. التحدي هو أن الناس ضعفاء في الروح ، وأنهم يكتبون الكود ببطء ويلتزمون ببطء. ويقومون بتنزيل الكود لأنفسهم ببطء شديد. والروبوتات محرومة من هذا العيب.
على سبيل المزاح - مؤخرًا لمشروع واحد ، لقد سمح لنا الروبوت الخاص بهم بالانتقال مباشرةً إلى Arc والالتزام قليلاً. وأنشأوا أعباء عمل أكثر مرتين لدينا من جميع مطوري ياندكس مجتمعين. وهذه مجرد قطعة صغيرة. عندما يأتي إلينا تجميع تلقائي كبير ، وهو المسؤول عن الجزء الرئيسي من رمز C ++ في المستودع الأحادي ، نتوقع أن يزيد التحميل مئات المرات. تحتاج إلى الاستعداد بجدية لهذا.اقتراح للمناقشة. طوال الطريق ظللت أكرر الكلمات "مثل في بوابة". وبشكل عام ، تبقى خطتنا كما هي: كرر واجهة Git. ولكن كلما قمنا بذلك ، كلما أدركنا أن هذا لا يعمل جيدًا.. Git . , . - . , checkout reset, . , , . : Git. « , ». Git .
. Git, git begin-wave-stash?
:
— .
— , Git ? — , , , . , . Git . , . .