جواب خیلی کوتاه

گیت فایل‌ها رو بر اساس محتواشون ذخیره میکنه.

جواب کوتاه نسبتاً بلند

اول کار که شما دستور git init رو اجرا می‌کنید، گیت داخل پوشه‌ی جاری یه فولدر .git می‌سازه که محتویاتش اینه:

 1[meysampg@freedom git]$ git init
 2Initialized empty Git repository in /srv/http/test/git/.git/
 3[meysampg@freedom git]$ ls -lah .git
 4total 40K
 5drwxr-xr-x 7 meysampg users 4.0K Mar 10 11:07 .
 6drwxr-xr-x 3 meysampg users 4.0K Mar 10 11:07 ..
 7drwxr-xr-x 2 meysampg users 4.0K Mar 10 11:07 branches
 8-rw-r--r-- 1 meysampg users   92 Mar 10 11:07 config
 9-rw-r--r-- 1 meysampg users   73 Mar 10 11:07 description
10-rw-r--r-- 1 meysampg users   23 Mar 10 11:07 HEAD
11drwxr-xr-x 2 meysampg users 4.0K Mar 10 11:07 hooks
12drwxr-xr-x 2 meysampg users 4.0K Mar 10 11:07 info
13drwxr-xr-x 4 meysampg users 4.0K Mar 10 11:07 objects
14drwxr-xr-x 4 meysampg users 4.0K Mar 10 11:07 refs

چیزی که جواب سوال بالاست در پوشه‌ی objects نهفته‌ست.

گیت یک سیستم محتوا-آدرسی‌ه، بدین معنی که گیت بدون توجه به نام فایل و بر اساس محتوا یک کلید منحصربفرد برای هر فایل می‌سازه (که همون هش SHA1ی هست که موقع کامیت کردن نشون میده) و اون رو داخل پوشه‌ی objects بر اساس قاعده‌ی زیر ذخیره می‌کنه:

  1. تو یه رشته بنویس blob و بعدش فاصله و بعدش طول محتوای فایل و بعدش کاراکتر نال رو بذار (مثلا blob 4\0).
  2. به رشته‌ی بالا محتوای فایل رو بچسبون (مثلا blob 4\0pgpg).
  3. از رشته‌ی مرحله‌ی ۲ یه هش SHA1 بگیر و از این به بعد این میشه شناسه‌ی این فایل (مثلا برای رشته‌ی بالا میشه c8f50ec947636ea1e848da84bc6e844f593426a1).
  4. دو کاراکتر اول رشته‌ی حاصل از مرحله‌ی ۳ رو بردار و داخل پوشه‌ی objects یه پوشه بساز. با ۳۸ کاراکتر باقی‌مانده یه فایل داخل پوشه‌ای که ساختی بساز (مثلا میشه objects/c8/f50ec947636ea1e848da84bc6e844f593426a1).
  5. با ZLib محتوای مرحله‌ی ۲ رو فشرده کن و اونو داخل فایلی که در مرحله‌ی ۴ ساختی ذخیره کن.

یعنی اگر بخوایم با پایتون این پنج مرحله رو کد بزنیم به اینطور چیزی می‌رسیم:

 1import hashlib
 2import os
 3import zlib
 4
 5
 6def get_file_content(path) -> str:
 7    with open(path) as f:
 8        return f.read()
 9
10# step 1 & 2
11def generate_blob_text(content: str) -> str:
12    return 'blob {}\0{}'.format(len(content), content)
13
14
15# step 3
16def generate_hash(content: str) -> str:
17    return hashlib.sha1(content.encode('utf-8')).hexdigest()
18
19
20# step 5 :)
21def generate_zlib_content(content: str) -> bytes:
22    return zlib.compress(content.encode('utf-8'))
23
24# step 4
25def create_file(hash: str, content: str) -> int:
26    folder = hash[:2]
27    filename = hash[2:]
28    path = os.path.join('./', folder, filename)
29
30    os.makedirs(folder, exist_ok=True)
31
32    with open(path, 'w') as f:
33        return f.write(content)
34
35
36# all together
37def store_like_git(path: str):
38    content = get_file_content(path)
39    blob = generate_blob_text(content)
40    blob_hash = generate_hash(blob)
41    compressed_blob = generate_zlib_content(blob).hex()
42
43
44    return create_file(blob_hash, compressed_blob)
45
46
47store_like_git('./file.txt')

پس اسم فایل‌ها چی میشن؟

همه چی آبجکته

تا اینجا گفتیم گیت فایل‌ها رو بر اساس محتوا ذخیره می‌کنه، نه اسم فایل. و خب سوال پیش میاد که چطور میشه رد تغییرات یک فایل مشخص رو گرفت؟ پرمیژن فایل‌ها چی میشن؟ کی کجا رفت و این داستانا. اینجاست که باید یه کم برگردیم عقب‌تر: «اصن اون blob چی بود؟». جواب این سوال مشخص می‌کنه که تروالدز چه مغز خفنی داره. داخل گیت خبری از تایپ‌های مختلف برای ذخیره‌سازی اطلاعات مختلف نیست. «تو گیت همه چی آبجکته». آبجکت یعنی همون چیزی که تو مرحله‌ی قبل ساخته شد. همه چی به همون صورت ساخته میشه ولی برای اینکه هش یه فایل و یه آبجکت از نوع دیگه (که در ادامه می‌بینیم چیا هستن) از شانس ما یکی در نیاد، با گذاشتن blob اول کار انگار یه فضای نام برای هر کدوم از تایپ‌ها اختصاص می‌دیم. در کل گیت چهار نوع آبجکت اصلی داره:

  • blob
  • tree
  • commit
  • tag

درخت

و اینجا بر می‌گردیم به سوال اصلی. تو قسمت قبل متوجه شدیم که محتوای هر فایل به صورت blob ذخیره میشه. برای تکمیل پازل، گیت از آبجکت tree استفاده می‌کنه تا هر چیز مرتبط با ساختار مخزن (repository) رو ذخیره کنه. ساختارش هم خیلی ساده‌ست. یه آبکجت tree شامل یک یا چند خطه که:

  • در خط اول کلمه‌ی tree میاد و بعد یه فاصله و بعد اندازه‌ی خط بعد و در انتها یه کاراکتر نال \0.
  • بعد از خط اول، در هر خط، اول پرمیژن و اجازه‌ی اجرا مشخص میشه:
    • 100644: فایل معمولی
    • 100755: فایل اجرایی
    • 120000: لینک نمادین (sym link)
    • 040000: پوشه (در واقع یه tree دیگه)
  • بعد یه فاصله میاد و اسم اون آبجکت
  • بعد یه کاراکتر نال میاد و بعدش هش به سبک قسمت اول که به آبجکتی که دخیره شده اشاره می‌کنه یعنی اگه بخوایم خودمونی‌تر به ماجرا اشاره کنیم، برای ذخیره‌ی a/b/c/d.txt تا الان با دوتا آبجکت blob و tree که تعریف کردیم، گیت این مسیر رو میره:
1a (tree)
2└─ b (tree)
3   └─ c (tree)
4      └─ d (blob)

یعنی فولدر گیت ما (با آبجکت‌هایی که تا حالا شناختیم) میشه ۳تا درخت و یه blob.

تا اینجا تونستیم یه شکلی از ذخیره‌سازی فایل‌ها رو داشته باشیم و به جواب سوال «گیت چطور فایل‌ها رو ذخیره می‌کنه؟» رسیدیم. گیت فایل رو نه بر اساس اسم و کپی کردن، که بر اساس محتوا هش می‌کنه و فایل‌ها رو هم داخل یه درخت قرار می‌ده. و خب یچی کمه هنوز.

گیت چطور تاریخچه نگه می‌داره؟

اگه صرفا دنبال نگه داشتن فایل بودیم واقعاً این همه دنگ و فنگ نیاز نبود. خود سیستم‌عامل تقریباً همه‌ی این چیزایی که صحبت شد رو داره و داشت کارش رو می‌کرد. کل ماجرا از اونجا شروع میشه که کی چیکار کرد و کِی؟ در جواب این مسئله گیت میاد آبجکت commit رو معرفی می‌کنه. کامیت همون آبجکتیه که به صورت روزمره باهاش سر و کله می‌زنیم با دستور git log عموماً می‌بینیمش:

1meysam@Meysams-Mac git % git log
2commit 46fc19645c43b01d3291f76304a8058112193138
3Author: Meysam P. Ganji <p.g.meysam@gmail.com>
4Date:   Wed Feb 11 02:19:20 2026 +0330
5
6    first commit

وقتی کامیت می‌کنیم

برای اینکه ببینیم، آبجکت کامیت چیه، اول می‌ریم سر وقت زیردستور کامیت. فرض کنیم فایل‌های زیر تغییر پیدا کردن:

1└─ a
2   └─ b
3      └─ c.txt
4   └─ d
5      └─ e.txt
6└─ f
7   └─ g.txt
8h.txt 

وقتی ما کامند git add رو می‌زنیم، گیت blob هر کدوم از فایل‌هایی که تغییر کردن رو می‌سازه و بعد از اون وقتیgit commit می‌کنیم، از برگ‌ها شروع می‌کنه و شروع به ساختن آبجکت‌های tree می‌کنه. یعنی در مرحله‌ی اول ۳تا آبجکت درخت ساخته میشه:

1T(b) -> c.txt
2T(d) -> e.txt
3T(f) -> g.txt

در مرحله‌ی بعد، یه لول از برگ‌هایی که بهشون اشاره کرده بالاتر میاد و یه آبجکت درخت برای a می‌سازه:

1T(a) -> T(b), T(d)

و در نهایت آبجکت نهایی رو می‌سازه که اسمش رو می‌ذاریم T:

1T -> T(a), T(f), h.txt

الان ما با داشتن T، می‌تونیم هر زمان به حالتی که این درخت ساخته برگردیم و این همون چیزیه که آبجکت کامیت نگه می‌داره.

آبجکت کامیت

بالطبع، مثل بقیه‌ی آبجکت‌ها، اول فایل commit میاد و طول خطوط بعدی و کاراکتر نال و بعدش:

  • کلمه‌ی tree و هش درختی داره بهش اشاره می‌کنه (در مثال بالا میشه T) و در آخر کاراکتر \n
  • کلمه‌ی parent و هش کامیت قبلی (کامیتـ(هایـ)ـی که موقع ساختن این کامیت فعال بوده/ن) و کاراکتر \n
  • کلمه‌ی author و اسم و ایمیل و زمان ایجاد کامیت توسط سازنده اصلی \n
  • کلمه‌ی committer و اسم و ایمیل و زمان کامیت کردن \n
  • اطلاعات مربوط با ساین اگه باشه
  • مسیج کامیت \n

نشانه‌گذاری حالت‌های خاص

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

تفاوت تگ و برنچ چیه؟

به نظر می‌رسه که برنچ و تگ یکی باشن. هر دو دارن اسم می‌ذارن روی یک حالت خاص. ولی اینطور نیست. برنچ‌ها فقط رفرنس‌هایی هستن به یه کامیت مشخص و هر زمان می‌تونن تغییر کنن. این تیکه کد می‌تونه ماجرا رو واضح‌تر کنه:

 1meysam@Meysams-Mac git % gch -b "fdf"
 2Switched to a new branch 'fdf'
 3meysam@Meysams-Mac git % gch -
 4Switched to branch 'main'
 5meysam@Meysams-Mac refs % cd .git/refs/heads
 6meysam@Meysams-Mac heads % ll
 7total 16
 8-rw-r--r--@ 1 meysam  staff    41B Feb 11 14:09 fdf
 9-rw-r--r--@ 1 meysam  staff    41B Feb 11 02:19 main
10meysam@Meysams-Mac heads % cat fdf
1146fc19645c43b01d3291f76304a8058112193138
12meysam@Meysams-Mac heads % cat main
1346fc19645c43b01d3291f76304a8058112193138

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

نمونه‌ی جمع و جور

این اسنیپت نشون میده که چطور میشه محتویات یه مخزن گیت رو دید و تریس کرد و رفت جلو. دستور cat-file محتویات رو نشون میده با دوتا سوئیچ کاربردی:

  • سوئیچ t: تایپ آبجکت
  • سوئیچ p: چاپ خوشگل
 1meysam@Meysams-Mac git % git init
 2Initialized empty Git repository in /Users/meysam/test/git/.git/
 3
 4meysam@Meysams-Mac git % ga file.txt
 5
 6meysam@Meysams-Mac git % gc "first commit"
 7[main (root-commit) 08ec49b] first commit
 8 1 file changed, 1 insertion(+)
 9 create mode 100644 file.txt
10 
11meysam@Meysams-Mac git % git cat-file -t 08ec49b
12commit
13
14meysam@Meysams-Mac git % git cat-file -p 08ec49b
15tree 70bd082dc4cdea292e136794fcab576352451191
16author Meysam P. Ganji <p.g.meysam@gmail.com> 1770759888 +0330
17committer Meysam P. Ganji <p.g.meysam@gmail.com> 1770759888 +0330
18gpgsig -----BEGIN PGP SIGNATURE-----
19
20 iQIzBAABCAAdFiEE9KVakJAdWESNuaZ6/uk7It4xDeQFAmmLptAACgkQ/uk7It4x
21 DeQjyA//dSA+N+WEr+qdftbjCj6yOxpcv070FIJNz1TGr8pPBo9oXaMOxmeF8GtD
22 PBJ7LC/AGLiQIsiTMABC2vqX6Mu0nvjZ7xNabXzJsWhg7IiXc5492Crtg6KYr9ld
23 GMqrFHwhim/FVvS/MhFEDMRObSstCs1ThBFcn/vMcw43HyQsy9YEhwMRQB/d0WTI
24 qFbeHVF65gUlMPnVHPGJg4uWwT0cG3Z/oEQUed0xvQ54TV9aX3UFGTOYkvkAHYDz
25 ut85s/hnDbmlfw0bP10nWiSJKISQw7xQdudsZwqOC4+/uxdmMQQ2m0naftZCWN9b
26 2LA+girvaykXuFSJwtVSHW5jHjrRPhoG6cjovr/N0pQOyhip7RbrNXgQEo5Ok5fH
27 2td+v3nP8v31JaqGwN02JQukNmXyP3GyDr/WQVf3VvU8i71KRUUJVae1Nw+8owJM
28 qvZUSRznKHZoP5/bMoJ5rgIzHBueyZiJfN3XbfAQrysKFsg3qjND8CMGkDdpS7ed
29 0umD5ngzfBF7fl06se9NiWIUUZxqKIONawadffXMwuB43dSLCurSZ8gGY+YHWn3p
30 Il8SdQxnnjqxWf28xJZL0c5fWSoV/0KLGxNzlutPxswNyaz20tAnrixg39ZNxyZL
31 uhLxTuPljKJkuGC1FFZIaraIMnU9sam+yEKTGKI4+WoAaDWcGws=
32 =nQAD
33 -----END PGP SIGNATURE-----
34
35first commit
36
37meysam@Meysams-Mac git % git cat-file -p 70bd08
38100644 blob 6fe0c98f9b56645abb217983d4f2180a4fdce66b	file.txt
39
40meysam@Meysams-Mac git % git cat-file -t 6fe0c
41blob
42
43meysam@Meysams-Mac git % git cat-file -p 6fe0c
44pgpg

و برای دیدن محتویات خام، این بش فانکشن کمک می‌کنه:

1function view_object() { 
2	python3 -c 'import sys,zlib;print(zlib.decompress(sys.stdin.buffer.read()))' < .git/objects/"${1:0:2}"/"${1:2}" | cat -v 
3}

خب که چه فایده؟

آبجکت دیدن هر چیزی بر اساس محتوای فایل داخل گیت، چندتا فایده‌ی اساسی داره.

دو فایل با محتوای یکسان = یک blob

برای گیت فرقی نمی‌کنه اگه فایل‌های یکسان، اسم یا مسیرشون فرق کنه. چون محتواشون یکیه blobشون هم یکیه. این دست‌آورد حجم مخزن رو کنترل‌شده نگه می‌داره (جدا از اینکه خود مخزن بعداً فشرده هم میشه و گاربج کالکتور داره) و برای داشتن تاریخچه‌های مختلف نیاز نیست کل کد رو با هر بار تغییر کپی کرد. برای تست کردن میشه از کد اول کار استفاده کرد یا راحت‌تر از زیردستور hash-object بهره برد:

1meysam@Meysams-Mac git % printf "pgpg" > a.txt
2meysam@Meysams-Mac git % printf "pgpg" > b.txt
3meysam@Meysams-Mac git % git hash-object a.txt
46fe0c98f9b56645abb217983d4f2180a4fdce66b
5meysam@Meysams-Mac git % git hash-object b.txt
66fe0c98f9b56645abb217983d4f2180a4fdce66b

تغییر اسم تقریباً رایگانه

گفتیم که ساختار با tree مشخص می‌شه و blobها هم فقط بر اساس محتوای فایل ساخته میشن. این یعنی با تغییر اسم یه فایل یا دایرکتوری، فقط یه آبجکت جدید درخت ساخته می‌شه که به ساختار جدید اشاره می‌کنه. طبعاً نتیجه‌ی مستقیم این مورد، داشتن چیزی مثل اسنپ‌شات و سفر در زمانه.

جهان‌های موازی

با داشتن امکان پریدن بین درخت‌های مختلف (که ساختارهای مختلف رو نشون میدن)، ابزارهایی مثل git blame و git bisect ساخته میشن. شما برای اینکه هر حالت ممکنی از تاریخچه رو ببینی، کافیه هش آبجکت کامیت رو داشته باشی و یک یا چند درخت رو پیمایش کنی تا به وضعیت کد در اون حالت برسی.

پایان

در نهایت باید گیت رو به عنوان یک دیتابیس فایل بر اساس محتوا دید. ما هر بار در حال ساختن blobها و احتمالاً treeهای مربوط به فایلا هستیم و این یعنی رفتن از یک اسنپ‌شات به یه اسنپ‌شات دیگه (مثل time travel در delta fromatها). همین.