به نام خدا

گیت یکی از مهمترین ابزارها در بین توسعه دهندگان همه پلتفرم ها و همچنین از ابزار هایی که توی هرشرکت مربوط به حوزه نرم افزار برید از شما انتظار میره که راجع بهش بدونید.

اما امروز میخوایم ببینم که گیت چطور فایل ها رو ذخیره میکنه؟ و مفهوم برنچ و کامیت در اصل داستان چی هست؟

اولین چیزی که نیاز هست بدونید اینه که دستورات گیت به دو دسته Plumbing و Porcelain و دستوراتی که همه ما بلدیم و مربوط به گیت میشه از دسته دوم یعنی Porcelain هست این دستورات کاری به پشت صحنه ندارند و کار های ما با این دستورات انجام میشه مثل git status و git commit و... اما دستورات دسته اول یعنی Plumbing چی هست؟ این دستورات برای افراد پیشرفته و همچنین برنامه هایی که نیاز دارند با گیت کار کنند استفاده میشه.

درون فولدر .git که پس از اجرای دستور git init ایجاد می شود چند فایل و فولدر مهم وجود دارد.

۱. فولدر objects (تمامی آبجکت هایی که گیت ایجاد میکند در این فولدر قرار میگیرد)

۲. فولدر refs (تمامی رفرنس ها شامل رفرنس های remote, heads, tags) در این فولدر نگهداری می شوند.

۳. فایل Head (این هد همان اشاره گر مکان فعلی ماست که میتواند به هد یک برنچ اشاره کند یا اینکه مستقیم به یک کامیت اشاره کند)

۴. فولدر log (کاربردی برای زمانی که دستوری را اشتباه اجرا میکنید و نیاز دارید که دستورات قبلی که اجرا شده اند و هش کامیت ها را ببینید تا بتوانید برنچی را برگردانید یا....)

حال به دستورات plumbing میپردازیم.

اولین دستور توی این دسته دستور زیر هست:

git hash-object -w 

این دستور فایلی که نام آن را قرار داده اید را در درون فولدر object به شکل BLOB (Binary Large OBject) کپی میکنه و هش فایل رو روی اسم فایل قرار میده البته دو حرف اول هش رو برمیداره و روی نام فولدر میذاره و ادامه ی هش رو به عنوان نام فایل میذاره و اون رو توی اون پوشه ای که اسمش دوحرف ابتدایی هش فایل هست میریزه.

برای اینکه این دستور از استاندارد input بخونه میتونید به شکل زیر ازش استفاده کنید.

echo "salam" | git hash-object -w --stdin

توی این حالت شما متنی رو که خودتون میخواید ذخیره میکنید و هش اون هم به شما برگردونده میشه و شما میتونید با مراجعه به فولدر git/objects/ اون فولدر و فایل داخلش رو پیدا کنید همچنین برای خوندن فایل میتونید از دستور زیر استفاده کنید.

git cat-file -p 

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

ذخیره تغییرات فایل ها

تا اینجای کار فهمیدیم که گیت چطور فایل ها رو ذخیره میکنه؟ اما آیا گیت شیوه ای برای ذخیره تغییرات هم داره؟ به طور کلی خیر، گیت هربار که فایلی رو تغییر میدید و عملا هش اون فایل عوض میشه اون فایل رو مجددا به صورت کامل ذخیره میکنه و عملا برخلاف سایر ورژن کنترل ها که delta-base هستند و فقط اختلاف دوفایل رو ذخیره میکنند گیت همه فایل هایی که تغییر حتی یک خطی هم داشته باشند رو مجددا ذخیره میکنه. شاید بپرسید که این شیوه ذخیره سازی حجم زیادی میگیره، بله اما گیت راهکاری داره که بعد از بزرگ شدن پروژه اون ها رو به شیوه خودش پک میکنه و این پک کردن باعث کاهش چشمگیر حجم مصرفی روی دیسک میشه (دستور این کار git gc است که ممکن است در ادامه به آن بپردازیم).

کامیت چیست؟ و چگونه انجام می شود؟

کامیت نیز مانند سایر فایل هایی که در objects ذخیره می شوند در آنجا ذخیره می شود و شامل چندین مشخصه من جمله نام کامیت کننده، نویسنده، توضیحات کامیت، والد کامیت (هش کامیت قبلی) و مهمتر از همه هش tree است.

هش tree چیست؟ tree همان دایرکتوری شماست که در آن فایل های خود را نگهداری میکنید هر دایرکتوری یک tree است به این شکل که شما وقتی یک دایرکتوری رو باز میکنید فایل ها+ فولدرها (دایرکتوری های) داخل آن را مشاهده میکنید. یک tree نیز دقیقا همینطور است داخل یک tree نام تمامی فایل ها به علاوه هش (که به عنوان آدرس استفاده می شود) وجود دارد و آدرس این tree در کامیت قرار میگیرد. اگر دایرکتوری دیگری در دایرکتوری اصلی شما موجود باشد آن دایرکتوری نیز به صورت مجزا به عنوان یک tree ذخیره می شود و هش آن در tree اصلی وارد می شود.

اگر با دستور git cat-file -p یک کامیت را باز کنید چنین چیزی را خواهید دید.

$ git cat-file -p cf6279770a1ac408408673b9b19106b16223f72a

tree d300ef96c834d093bc127de64f49c3a887a00a5d
parent 465cb617937fcac7612cdc92f96cf796114234d1
author Mohammad Fallah <mohammadfallah840@gmail.com> 1672190301 +0100
committer Mohammad Fallah <mohammadfallah840@gmail.com> 1672190301 +0100

second commit this is desciption commit that you entered by -m flag

حالا میرویم به سراغ tree داخل آن که به ما لیست فایل های موجود در این کامیت + هش آن ها را میدهد.

 git cat-file -p d300ef96c834d093bc127de64f49c3a887a00a5d

100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391	Readme.txt
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391	main.c
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391	second.c
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391	include.c

و با همین دستور میتوانید فایل ها را نیز بخوانید.

 

برنچ چیست؟ چگونه کار می کند؟

برنچ یکی از سبک ترین مفاهیم در گیت است. عملا برنچ ها تنها اشاره گر به کامیت هستند و از آنجایی که هرکامیت آدرس کامیت قبلی خود (هش والد) خود را حفظ میکند پس تنها چیزی که یک برنچ نیاز به ذخیره سازی آن دارد هش آخرین کامیت انجام شده است که به آن head برنچ میگویند و این head را نباید تغییر داد البته تغییر دادن آن آسان نیست مگراینکه شما به گیت دستور بدهید که git reset --hard را روی یک کامیت انجام دهد در اینصورت هد برنچ شما گم می شود و البته شما هیچ چیز را از دست نداده اید و با کمی زکاوت می توانید آن را از داخل لاگ ها پیدا کنید و برگردانید.

هر برنچ ۴۱ بایت است که ۴۰ بایت برای هش SHA-1 مصرف می شود و یک بایت هم new line است. پس برای استفاده از برنچ ها صرفه جویی نکنید.

هد برنچ ها درون git/refs/heads نگهداری می شود. در آنجا فایل های ۴۱ بایتی است که نام آنها نام همان برنچ است و درون هرکدام هش آخرین کامیت همان برنچ قرار دارد. وقتی شما کامیت انجام می دهید تنها کاری که انجام می شود گیت با توجه به فایل git/HEAD مشاهده میکند که شما در کدام برنچ قرار دارید سپس هش والد کامیتی که در حال انجام است را همان هشی قرار میدهد که در فایل برنچ مربوطه وجود دارد و همچنین هش کامیت جدید که انجام شده و آخرین کامیت محسوب میشود را جایگزین هش قبلی درون فایل برنچ مربوطه میکند به همین راحتی یک کامیت در یک برنچ انجام می شود.

اگر نام این فایل ها را تغییر دهید مستقیما نام برنچ تغییر میکنید همچنین اگر فایل جدیدی در این پوشه /refs/heads ایجاد کنید مستقیما درون لیست branch های شما قرار میگرد.

Merge و ترکیب برنچ ها چیست؟

وقتی یک برنچ ایجاد می شود هدف آن است که برنچ پس از چند کامیت به برنچ اصلی مرج شود. در مرج دو حالت وجود دارد.

  • FastForward Merge: این حالت زمانی پیش می آید که وقتی از برنچ فرعی که باید به برنچ اصلی مرج شود به عقب برمیگردیم میتوانیم هد برنچ اصلی را ببینیم یعنی عملا هنوز همه چیز در یک خط است و شاخه ای ایجاد نشده است پس وقتی این نوع مرج را انجام می دهیم تنها به سادگی پوینتر هد برنچ اصلی به پوینتر برنچ فرعی تغییر میکند و تمام کامیت های برنچ فرعی به تاریخچه برنچ اصلی وارد می شوند.

تصویر۱‌) وقتی برنچی میسازیم و در آن یک کامیت انجام می دهیم یک کامیت اتفاق میفتد که اشاره گر به آن را یک برنچ برعهده دارد.

تصویر۲ ) وقتی که برنچ hotfix را در master مرج میکنیم به دلیلی که در بالا توضیح داده شد، فقط head برنچ مستر به هد برنچ hotfix اشاره میکند.

حال فرض کنید که بخواهیم با دستور git branch -d hotfix این برنچ را حذف کنیم. چه اتفاقی می افتد؟ هیچ. تنها اشاره گر به کامیت C4 از دست میرود که اهمیتی ندارد زیرا ما آن کامیت را در مستر نیز داریم پس هیچ اتفاقی نمی افتد و هیچ فایلی حذف نمی شود.

 

  • Diverged History: در این حالت شاخه اصلی که از آن شاخه گرفته بودیم یک یا چند کامیت انجام داده و جلوتر است مثل شکل زیر.

تصویر۳ ) برنچ master پس از آخرین کامیت برنچ testing یک کامیت کرده و شاخه ایجاد شده

در این حالت وقتی که شما برنج testing را با برنچ master مرج میکنید عملا یک کامیت جدید به نام Merge Commit در برنچ master ایجاد میکنید که دو parent دارد و این شاخه در هیستوری باقی میماند.

در مثال که برنچ مستر را با برنچ iss53 مرج کردیم و چون هد مستر از اجداد برنچ iss53 نبود یک کامیت در master به وجود آمد که merge commit نام دارد

آیا میدانید چگونه میتوان از ایجاد این شاخه در تاریخچه جلوگیری کرد؟ درست است با rebase با این شیوه که برنچ را rebase کنیم این کار باعث می شود تمام تغییرات که در C4 اتفاق افتاده است به تمامی کامیت های برنچ iss53 اعمال شود.

مثال پایین را ببینید تا کاملا متوجه شوید.

همانطور که میبینید کامیت C4 که مربوط به برنچ expriment بود پس از rebase شدن برنچ اینبار به هد مستر اشاره میکند و در دایرکتوری آن و tree آن تمام تغییراتی که تفاوت بین C2 و C3 بود روی آن اعمال شده است (حتی اگر چندین کامیت هم بود تمامی کامیت ها این تغییرات را اعمال میکردند). در این حالت باز یک خط میشود و میتوان آن را Fast-Forward Merge کرد.

 

اگر برنچی را قبل از مرج کردن حذف کنیم آیا همه فایل ها و کامیت های آن برنچ حذف می شوند؟

پاسخ خیر است تنها چیزی که حذف می شود همان فایل ۴۱ بایتی موجود در git/refs/heads است. همه فایل ها و کامیت ها در فایل objects موجود خواهد بود و با پیدا کردن هش اخرین کامیت می توانید برنچ از دست رفته را برگردانید تنها کافی است که با دستور git branch name hash یک اشاره گر (برنچ) جدید به آن کامیت ایجاد کنید. اما باید توجه داشت که این فایل ها به دلیل unrechable بودن و نداشتن هیچگونه رفرنس به آن در خطر هستند و با دستوراتی که گیت برای کاهش فضای مصرفی میکنند قابل حذف هستند پس هرچه زودتر به فکر بازیابی باشید.

 

دستور فوق العاده fsck و ریکاوری داده ها

شاید شما هم تا اینجای مطلب به این مورد فکر کرده باشید که آیا میتوان فایل های موجود در objects را به تفکیک کامیت، BLOB و tree مشاهده کرد؟ من در این زمینه اطلاعات کافی ندارم اما این دستور می تواند این کار را برای شما انجام دهد.

$ git fsck -v

Checking object directory
Checking commit 1883ffb8d10991aa2b4770fb42f3585a376c16a5
Checking tree 5e4f8cb6452a3ae55402b672dec42fb3f1c4e4cf
Checking tree 667961ddbff143292ca3b0ad8da9a648cfe98836
Checking commit 991324e4f8bbe8acbf92b288dc8b53ad66497310
Checking commit 3a77ae4816df268a205067807bba7cb04cb7fdd9
Checking commit 01e18bf6d0db710a3a52ffb11f21ceb09282396b
Checking commit a363ed2468e7246adbe223637bf523ac4e1b950c
Checking commit cf6279770a1ac408408673b9b19106b16223f72a
Checking commit 465cb617937fcac7612cdc92f96cf796114234d1
Checking commit df191daa31a7ef8ee4135e5ea281679c603a8e35
Checking commit f4d068da5eee4c4e1cda806dc2492876c3c9d3c4
Checking blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
Checking tree d0132667f415c85b07bfbb61e1d8965c8663f20b
Checking tree d300ef96c834d093bc127de64f49c3a887a00a5d
Checking tree ea87881ac6a4e08c105dc36f2c2b98dc2c8438dc
Checking tree d8a968f7ab9f04c20105cd5daa16003348dbf4f4
Checking tree 1f089f23bed75aa9e28b8c4087a44e46efd9a6e6
Checking tree 38905c6fa9df39770815331188423a851b5f3740
Checking tree f5873849abba4c893577f05a5ccd7150fd2ee531

این دستور میتواند به شما کمک کند تا کامیت ها را ببینید البته که دستور fsck وظیفه validate کردن را برعهده دارد اما فلگ verbose آن میتواند در این زمینه به ما کمک کند.

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

$ git fsck --full

فلگ full جستجو را در تمام موارد انجام میدهد.

راه دیگری که میتوانید دستورات قبلی خود را ببینید و تغییرات خود را دنبال کنید خواندن reflog است که میتوانید با دستور زیر آن را بخوانید

git reflog

این دستور با استفاده از فایل موجود در git/log لاگ ها را به شما نشان میدهد که حتی میتوانید مستقیما آن را از درون آدرس ذکر شده بخوانید.

همچنین اگر میخواید کامیت ها را از داخل این لاگ به شکل تمیزی بیرون بیاورید میتوانید از دستور زیر استفاده کنید توجه کنید که دستور زیر نیز دقیقا از همین فایل میخواند.

git log -g

 

آیا هرکامیت فقط فایل های استیج را ذخیره میکند؟ یا تمامی فایل ها مجددا ذخیره می شود؟

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

 

عملا parent در کامیت ها یک مشخصه ی نمایشی است مگرنه در کارکرد هیچ تاثیری ندارد و هیچ ارث بری انجام نمی شود یعنی تمام tree و همه نام فایل ها ذخیره می شود.

توجه کنید که این بدین معنا نیست که در هر کامیت تمامی فایل ها را ذخیره می کند و صد البته که فایل هایی که هش تکراری دارند (یعنی تغییراتی نداشته اند) ذخیره نمی شوند اما تمام tree در آن وجود دارد و اینگونه نیست که فقط فایل های اضافه شده در یک کامیت وجود داشته باشند.