رفتن به محتوای اصلی
Koohestun
توضیح کوهستانسازوکار درونی گیتمقاله آموزشی· 8 دقیقه مطالعه· در فناوری

گیت چگونه کامیت‌ها، درخت‌ها و بلاب‌ها را به عنوان اشیای محتوا-محور در یک گراف جهت‌دار غیرمدور ذخیره می‌کند

گیت در پسِ ظاهر سیستم کنترل نسخه خود، در واقع به عنوان یک پایگاه داده تغییرناپذیر کلید-مقدار عمل می‌کند؛ جایی که فایل‌ها و پوشه‌ها به شکل هش‌های رمزنگاری‌شده در یک گراف جهت‌دار غیرمدور (DAG) ذخیره و نگاشت می‌شوند.

به قلم نیما موسوی

کاربران سطح بالا 60%مهندسان زیربنایی 25%نظریه‌پردازان گراف 15%
کاربران سطح بالا
بر رابط کاربری سطح بالای کنترل نسخه و تفاوت‌های زمانی (diffs) تمرکز دارد.
مهندسان زیربنایی
بر ذخیره‌سازی محتوا-محور و پایگاه داده اشیای تغییرناپذیر تمرکز دارد.
نظریه‌پردازان گراف
بر ویژگی‌های ریاضی گراف جهت‌دار غیرمدور و اجماع توزیع‌شده تمرکز دارد.

دیدگاه‌هایی که این گزارش پوشش نداده

  • معماران سیستم‌های کنترل نسخه جایگزین
  • نگهدارندگان سیستم ذخیره‌سازی فایل‌های حجیم (LFS)

چرا مهم است

درک سازوکار درونی گیت، ابهامات عملیات پیچیده‌ای مثل ری‌بیس (rebasing) و تداخل‌های ادغام (merge conflicts) را از بین می‌برد و به توسعه‌دهندگان اجازه می‌دهد به جای حفظ کردن طوطی‌وار دستورات، تاریخچه مخزن کد را با اطمینان کامل مدیریت کنند.

در یک سیستم فایل سنتی مانند NTFS ویندوز یا APFS مک‌اواس، یک سند با درخواست یک مسیر فایل مشخص از سیستم‌عامل، مانند C:\project\main.py، بازیابی می‌شود. اما گیت مفهوم مکان را به کلی کنار می‌گذارد. در هسته خود، فراگیرترین سیستم کنترل نسخه جهان در واقع اصلاً یک سیستم کنترل نسخه نیست، بلکه یک پایگاه داده کاملاً محتوا-محور است. وقتی یک توسعه‌دهنده فایلی را از گیت می‌خواهد، در واقع آدرس یک مکان را طلب نمی‌کند؛ بلکه یک هش رمزنگاری‌شده ۴۰ کاراکتری را می‌خواهد که نمایانگر محتوای دقیق آن فایل در یک میلی‌ثانیه خاص از زمان است.[1][4]

در حالی که پلتفرم‌های توسعه‌دهنده مدرن با وعده‌های پر زرق و برقِ همکاری بی‌وقفه و نسخه‌بندی هوشمند برای خود بازاریابی می‌کنند، قابلیت واقعی که صنعت جهانی نرم‌افزار را به پیش می‌راند، به طرز شگفت‌آوری ساده و بی‌تکلف است. بیشتر توسعه‌دهندگان از طریق دستورات سطح بالا (porcelain) مانند git diff یا git log با گیت تعامل دارند؛ چیزی که این توهم را ایجاد می‌کند که نرم‌افزار در حال ذخیره یک توالی زمانی از تغییرات است — یعنی یک فایل پایه که به دنبال آن لیستی از حذفیات و اضافات آمده است. این دقیقاً همان روشی است که سیستم‌های قدیمی‌تر دهه ۱۹۹۰ مانند Subversion و CVS کار می‌کردند.[2][5]

اما واقعیت معماری گیت کاملاً متفاوت است. گیت اسنپ‌شات‌ها را ذخیره می‌کند، نه تفاوت‌ها (diffs) را. هر بار که کاربری تغییری را کامیت می‌کند، گیت یک تصویر کامل از وضعیت تمام فایل‌ها در همان لحظه دقیق ثبت می‌کند. اگر فایلی تغییر نکرده باشد، گیت یک کپی اضافی از آن ذخیره نمی‌کند؛ بلکه به سادگی به همان فایل یکسان قبلی که از پیش در پایگاه داده‌اش وجود دارد لینک می‌دهد. این رویکرد، بار پردازشی تولید لحظه‌ای تفاوت‌ها را فدای یکپارچگی مطلق داده‌ها و بازیابی آنی اسنپ‌شات‌ها می‌کند.[4][5]

این سازوکار کاملاً بر ذخیره‌سازی محتوا-محور استوار است. مستندات رسمی Git SCM توضیح می‌دهد: «گیت یک سیستم فایل محتوا-محور است. این بدان معناست که در هسته گیت یک ذخیره‌ساز ساده داده از نوع کلید-مقدار قرار دارد.» در این مدل، نام یک قطعه داده مستقیماً از خود آن داده و با استفاده از یک تابع هش رمزنگاری‌شده، به طور خاص الگوریتم ۱۶۰ بیتی SHA-1، استخراج می‌شود.[1][2]

در یک سیستم فایل محتوا-محور، داده‌ها به جای مسیر فایل، توسط هش رمزنگاری‌شده محتوایشان شناسایی می‌شوند.

وقتی فایلی به یک مخزن گیت اضافه می‌شود، سیستم یک رشته هگزادسیمال ۴۰ کاراکتری را بر اساس محتوای فایل و یک هدر کوتاه محاسبه می‌کند. این هش به هویت دائمی فایل تبدیل می‌شود. اگر حتی یک ویرگول در یک پایگاه کد ۱۰,۰۰۰ خطی تغییر کند، هش کاملاً تغییر می‌کند و یکپارچگی ۱۰۰ درصدی داده‌ها را تضمین می‌نماید. از آنجا که هش از محتوا به دست می‌آید، دو فایل کاملاً یکسان در پوشه‌های متفاوت، دقیقاً یک هش یکسان تولید کرده و تنها یک بار ذخیره می‌شوند.[2][3]

این موضوع ما را به چهار نوع شیء بنیادی می‌رساند که پوشه پنهان .git/objects گیت را پر می‌کنند. پایه‌ای‌ترینِ آن‌ها «بلاب» (blob) است که مخفف Binary Large Object (شیء بزرگ باینری) است. یک بلاب هیچ‌چیز جز داده‌های خام فایل را ذخیره نمی‌کند. این شیء نام فایل، تاریخ ایجاد یا مجوزهای خود را نمی‌داند. بلاب صرفاً بایت‌های خام محتواست که با استفاده از zlib فشرده شده و با هش SHA-1 خود نام‌گذاری شده است. از آنجا که بلاب‌ها فاقد متادیتا هستند، دو فایل با نام‌های متفاوت اما محتوای یکسان، دقیقاً به یک بلاب واحد هش می‌شوند که این امر باعث صرفه‌جویی قابل‌توجهی در فضای ذخیره‌سازی می‌گردد.[1][5]

از آنجا که بلاب‌ها فاقد بستر ساختاری هستند، گیت برای بازسازی سلسله‌مراتب پوشه‌های یک پروژه به نوع دوم از اشیاء نیاز دارد: «درخت» (tree). یک شیء درخت عملکردی بسیار شبیه به یک پوشه در یک سیستم‌عامل استاندارد دارد و بلاب‌ها و زیردرخت‌ها را سازماندهی می‌کند. یک درخت حاوی لیستی از اشاره‌گرهاست. هر ورودی در یک شیء درخت شامل حالت فایل (file mode)، نوع شیء (یک درخت دیگر یا یک بلاب)، هش SHA-1 آن شیء و نام قابل‌خواندن فایل برای انسان است. با پیوند دادن درخت‌ها به بلاب‌ها و سایر درخت‌های تو در تو، گیت کل ساختار پوشه‌های یک پروژه را در یک نقطه زمانی مشخص ترسیم می‌کند.[1][3]

از آنجا که بلاب‌ها فاقد بستر ساختاری هستند، گیت برای بازسازی سلسله‌مراتب پوشه‌های یک پروژه به نوع دوم از اشیاء نیاز دارد: «درخت» (tree).

با این حال، درخت‌ها و بلاب‌ها به تنهایی فقط یک وضعیت ایستا و منجمد در زمان را توصیف می‌کنند. برای ردیابی تاریخچه و انتساب تغییرات به نویسندگان خاص، گیت سومین و آشناترین نوع شیء را معرفی می‌کند: «کامیت» (commit). یک شیء کامیت به طرز شگفت‌آوری سبک است و صرفاً به عنوان یک پوشش (wrapper) پیرامون یک وضعیت خاص عمل می‌کند. این شیء حاوی یک اشاره‌گر به شیء درخت ریشه در بالاترین سطح است که کل ساختار پوشه پروژه را نشان می‌دهد؛ به همراه متادیتاهایی مانند نام و ایمیل نویسنده، برچسب زمانی (timestamp) و یک پیام کامیت که زمینه تغییر را توضیح می‌دهد.[2][4]

نکته حیاتی این است که یک کامیت همچنین حاوی اشاره‌گری به کامیت والد خود است. کامیت اولیه در یک مخزن هیچ والدی ندارد. یک کامیت متوالی استاندارد دقیقاً یک والد دارد، در حالی که یک کامیت ادغام (merge commit) دارای دو یا چند والد است. همین پیوند والد-فرزندی است که اسنپ‌شات‌های منزوی را به یک خط زمانی پیوسته و قابل‌پیمایش تبدیل می‌کند. بدون این اشاره‌گرهای والد، یک مخزن کد صرفاً توده‌ای از هم‌گسیخته از وضعیت‌های تاریخی بدون هیچ ترتیب زمانی می‌بود.[4][5]

این کامیت‌ها در کنار هم یک گراف جهت‌دار غیرمدور (DAG) را تشکیل می‌دهند. در نظریه گراف، DAG شبکه‌ای از گره‌هاست که توسط یال‌هایی به هم متصل شده‌اند که در یک جهت جریان دارند و هرگز به روی خود حلقه نمی‌زنند. در DAG گیت، گره‌ها همان اشیای کامیت هستند و یال‌های جهت‌دار، اشاره‌گرهای والد محسوب می‌شوند. از آنجا که هش‌های رمزنگاری‌شده تغییرناپذیرند، تاریخچه فقط می‌تواند رو به جلو حرکت کند. شما نمی‌توانید یک کامیت گذشته را بدون تغییر اساسی هش آن دستکاری کنید؛ کاری که ارتباط آن را با تمام کامیت‌های بعدی قطع کرده و نیازمند بازنویسی کل گرافِ پایین‌دست است.[3][5]

کامیت‌ها به درخت‌ها اشاره می‌کنند و درخت‌ها نیز به نوبه خود به بلاب‌ها اشاره دارند؛ این ساختار اسنپ‌شات‌های تغییرناپذیری را می‌سازد که تاریخچه مخزن را شکل می‌دهند.

این تغییرناپذیری سخت‌گیرانه، بستر اصلی قابلیت اطمینان گیت در میان تیم‌های توزیع‌شده است. وقتی یک توسعه‌دهنده تلاش می‌کند با استفاده از دستوراتی مانند git rebase یا git commit --amend تاریخچه را بازنویسی کند، در واقع کامیت‌های موجود را ویرایش نمی‌کند. در عوض، نرم‌افزار اشیای کامیت کاملاً جدیدی با هش‌های تازه تولید کرده و قبلی‌ها را رها می‌کند. کامیت‌های اصلی در پایگاه داده محلی دست‌نخورده و کاملاً یتیم باقی می‌مانند، تا زمانی که در نهایت توسط روتین‌های داخلی جمع‌آوری زباله (garbage collection) گیت برای همیشه حذف شوند. این امر تضمین می‌کند که عملیات‌های مخرب به ندرت منجر به از دست رفتن فوری داده‌ها می‌شوند.[4][5]

آخرین قطعه از معماری هسته، شیء «تگ» (tag) است که یک برچسب دائمی و قابل‌خواندن برای انسان را به یک هش کامیت خاص اختصاص می‌دهد و اغلب برای نشانه‌گذاری نسخه‌های انتشار یافته مانند v1.0.0 استفاده می‌شود. آنچه توسعه‌دهندگان معمولاً به عنوان «برنچ» (branch) یا شاخه می‌شناسند، موجودیت‌های ساختاری پیچیده‌ای در داخل DAG نیستند. یک برنچ در گیت صرفاً یک فایل متنی ۴۱ بایتی است که حاوی هش ۴۰ کاراکتری SHA-1 یک کامیت، به علاوه یک کاراکتر خط جدید (newline) است. وقتی کامیت جدیدی روی یک برنچ انجام می‌شود، این فایل متنی به سادگی با هش جدید بازنویسی می‌شود.[1][5]

این جداسازی معماری بین پایگاه داده اشیای تغییرناپذیر — شامل بلاب‌ها، درخت‌ها و کامیت‌ها — و اشاره‌گرهای تغییرپذیر مانند برنچ‌ها و تگ‌ها، همان چیزی است که به گیت اجازه می‌دهد عملیات پیچیده را با سرعتی تقریباً آنی انجام دهد. ایجاد یک برنچ جدید، فایل‌ها را کپی نمی‌کند یا نیازی به محاسبات سنگین ندارد؛ بلکه صرفاً یک اشاره‌گر ۴۱ بایتی به یک گره موجود در گراف ایجاد می‌کند. جابجایی بین برنچ‌ها به سادگی دایرکتوری کاری را به‌روز می‌کند تا با درخت ارجاع‌داده‌شده توسط آن اشاره‌گر مطابقت داشته باشد؛ موضوعی که تغییر بستر کاری (context switching) را به جای یک کپی سنگین در سیستم فایل، به یک عملیات پیش‌پاافتاده تبدیل می‌کند.[4][5]

در حالی که صنعت نرم‌افزار اغلب درباره ابزارهای جدید کدنویسی با هوش مصنوعی و پلتفرم‌های همکاری ابری هیاهو به پا می‌کند، مکانیک زیربنایی گیت از زمانی که لینوس توروالدز این سیستم را در سال ۲۰۰۵ طراحی کرد، تا حد زیادی بدون تغییر باقی مانده است. قابلیت واقعی که توسعه مدرن را به پیش می‌راند، نه در الگوریتم‌های پیچیده تفاوت‌سنجی (diffing)، بلکه در سادگی ظریف گراف جهت‌دار غیرمدور نهفته است. گیت با در نظر گرفتن تاریخچه کد به عنوان یک گراف ریاضی از اسنپ‌شات‌های تغییرناپذیر به جای یک توالی شکننده از ویرایش‌ها، مشکل همکاری توزیع‌شده را برای همیشه حل کرد.[2][6]

از آنجا که فایل‌های یکسان هش‌های SHA-1 یکسانی تولید می‌کنند، گیت به طور خودکار فایل‌های بدون تغییر را در میان کامیت‌ها از حالت تکراری خارج (deduplicate) می‌کند.

درک این لایه پنهان و زیربنایی، جادوی ظاهری دستورات سطح بالا (porcelain) که کاربر با آن‌ها سر و کار دارد را از بین می‌برد. وقتی یک تداخل ادغام (merge conflict) رخ می‌دهد یا یک مخزن وارد وضعیت HEAD جداشده (detached HEAD) می‌شود، این یک خرابی فاجعه‌بار سیستم نیست، بلکه صرفاً مسئله پیمایش گراف و تراز کردن اشاره‌گرها در یک ذخیره‌ساز کلید-مقدار محتوا-محور است. توسعه‌دهندگانی که مکانیک گراف جهت‌دار غیرمدور را درک می‌کنند، حفظ کردن سینتکس مبهم دستورات را کنار گذاشته و شروع به دستکاری مستقیم گراف می‌کنند؛ و به این ترتیب، یک سیستم کنترل نسخه گیج‌کننده را به یک پایگاه داده بسیار قابل‌پیش‌بینی و شفاف تبدیل می‌نمایند.[5][6]

نکات کلیدی

  • گیت به عنوان یک سیستم فایل محتوا-محور عمل می‌کند و داده‌ها را به جای مسیر فایل، با یک هش رمزنگاری‌شده ۴۰ کاراکتری شناسایی می‌کند.
  • این سیستم به جای ثبت لیستی زمانی از تفاوت‌ها (diffs) یا تغییرات، اسنپ‌شات‌های کاملی از کل پروژه را ذخیره می‌کند.
  • چهار نوع شیء بنیادی — بلاب‌ها (blobs)، درخت‌ها (trees)، کامیت‌ها (commits) و تگ‌ها (tags) — کل پایگاه داده تغییرناپذیر گیت را تشکیل می‌دهند.
  • کامیت‌ها از طریق اشاره‌گرهای والد به یکدیگر متصل می‌شوند و یک گراف جهت‌دار غیرمدور (DAG) دقیق را می‌سازند که تاریخچه پروژه را ترسیم می‌کند.
  • برنچ‌ها (شاخه‌ها) کپی‌های ساختاری از داده‌ها نیستند، بلکه صرفاً فایل‌های متنی ۴۱ بایتی هستند که به هش یک کامیت خاص اشاره می‌کنند.

اصطلاحات کلیدی

ذخیره‌سازی محتوا-محور
سازوکار ذخیره‌سازی که در آن داده‌ها به جای مسیر یا مکان مشخص فایل، بر اساس هش رمزنگاری‌شده محتوایشان بازیابی می‌شوند.
گراف جهت‌دار غیرمدور (DAG)
شبکه‌ای از گره‌ها که توسط یال‌های جهت‌دار به هم متصل شده‌اند و هرگز یک حلقه بسته تشکیل نمی‌دهند؛ گیت از این ساختار برای ترسیم تاریخچه کامیت‌ها استفاده می‌کند.
بلاب (Blob)
مخفف Binary Large Object؛ نوعی شیء در گیت که محتویات خام و فرمت‌نشده یک فایل ردیابی‌شده را ذخیره می‌کند.
درخت (Tree)
شیئی در گیت که نمایانگر ساختار دایرکتوری است و حاوی اشاره‌گرهایی به بلاب‌ها (فایل‌ها) و سایر درخت‌ها (زیرپوشه‌ها) می‌باشد.
SHA-1
یک الگوریتم هش رمزنگاری ۱۶۰ بیتی که گیت برای تولید یک شناسه منحصربه‌فرد ۴۰ کاراکتری برای هر شیء در پایگاه داده خود از آن استفاده می‌کند.
دستورات سطح بالا (Porcelain)
دستورات کاربرپسند و سطح بالای گیت (مانند git add و git commit) که عملیات پیچیده پایگاه داده زیربنایی را از دید کاربر پنهان می‌کنند.

پرسش‌های متداول

آیا گیت در هر کامیت یک کپی جدید از تمام فایل‌ها ذخیره می‌کند؟

خیر. گیت یک اسنپ‌شات کامل از پروژه ذخیره می‌کند، اما داده‌های تکراری را حذف می‌کند. اگر فایلی بین کامیت‌ها تغییر نکرده باشد، شیء درخت جدید به سادگی به همان هش بلاب کامیت قبلی اشاره می‌کند و در فضای ذخیره‌سازی صرفه‌جویی می‌شود.

وقتی یک برنچ را ری‌بیس (rebase) می‌کنم، چه اتفاقی برای کامیت قدیمی می‌افتد؟

ری‌بیس کردن کامیت‌های موجود را تغییر نمی‌دهد، زیرا اشیای گیت تغییرناپذیرند. در عوض، اشیای کامیت کاملاً جدیدی با هش‌های جدید ایجاد می‌کند. کامیت‌های قدیمی در پایگاه داده باقی می‌مانند تا زمانی که سیستم جمع‌آوری زباله گیت آن‌ها را برای همیشه حذف کند.

چرا گیت از SHA-1 استفاده می‌کند در حالی که از نظر رمزنگاری شکسته شده محسوب می‌شود؟

گیت از SHA-1 در درجه اول برای یکپارچگی داده‌ها و شناسایی محتوا استفاده می‌کند، نه برای امنیت در برابر حملات هدفمند تصادم (collision). اگرچه نسخه‌های جدیدتر گیت در حال گذار به SHA-256 هستند، احتمال وقوع یک تصادم تصادفی SHA-1 در یک مخزن کد همچنان بی‌نهایت کوچک است.

تفاوت بین یک درخت و یک بلاب چیست؟

یک بلاب فقط محتویات خام یک فایل را بدون هیچ متادیتایی ذخیره می‌کند. یک درخت مانند یک دایرکتوری عمل می‌کند و لیستی از اشاره‌گرها به بلاب‌ها و سایر درخت‌ها را به همراه نام فایل‌ها و مجوزهای مرتبط با آن‌ها ذخیره می‌نماید.

منابع

پوشش منابع

6 منبع

3 دیدگاه شناسایی‌شده

کاربران سطح بالا 60%مهندسان زیربنایی 25%نظریه‌پردازان گراف 15%
  1. [1]Git SCMمهندسان زیربنایی

    10.2 Git Internals - Git Objects

    مطالعه در Git SCM →
  2. [2]GeeksforGeeksکاربران سطح بالا

    Git Internals

    مطالعه در GeeksforGeeks →
  3. [3]Substackمهندسان زیربنایی

    A Nibble of Git's Object Store

    مطالعه در Substack →
  4. [4]Mediumکاربران سطح بالا

    Understanding the Magic Behind Git: A Deep Dive into Git Internals

    مطالعه در Medium →
  5. [5]Career Dastakنظریه‌پردازان گراف

    Beyond Commit and Push: A Deep Dive into Git Internals (Blobs, Trees, and Refs)

    مطالعه در Career Dastak →
  6. [6]تیم سردبیری کوهستاننظریه‌پردازان گراف

    تحلیل تیم سردبیری کوهستان

    مطالعه در تیم سردبیری کوهستان →

نظرات

همیشه در جریان باشید

هر زاویه. هر روز.

دریافت فناوری اخبار همراه با پوشش کامل منابع و تحلیل دیدگاه‌ها، مستقیم در صندوق ورودی شما.