Chapters ▾ 2nd Edition

10.2 (Git Internals) - اشیا گیت (Git Objects)

اشیا گیت (Git Objects)

Git یک content-addressable filesystem است. خیلی خوب. این یعنی چی؟ یعنی در هسته‌ی Git یک key-value data store ساده قرار دارد. به این معنا که شما می‌توانید هر نوع محتوایی را داخل یک Git repository وارد کنید و Git یک کلید یکتا به شما برمی‌گرداند که بعداً می‌توانید با آن کلید، محتوای خود را بازیابی کنید.

به‌عنوان یک نمایش عملی، بیایید به دستور plumbing به نام git hash-object نگاه کنیم؛ این دستور داده‌ای را دریافت می‌کند، آن را داخل دایرکتوری .git/objects (یعنی object database) ذخیره می‌کند، و کلید یکتایی به شما برمی‌گرداند که به آن object اشاره دارد.

ابتدا یک Git repository جدید initialize کنید و مطمئن شوید که (قابل پیش‌بینی است) چیزی در دایرکتوری objects وجود ندارد:

$ git init test
Initialized empty Git repository in /tmp/test/.git/
$ cd test
$ find .git/objects
.git/objects
.git/objects/info
.git/objects/pack
$ find .git/objects -type f

Git دایرکتوری objects را ساخته و زیرشاخه‌های pack و info را ایجاد کرده، اما هیچ فایل عادی وجود ندارد. حالا بیایید با git hash-object یک data object جدید بسازیم و آن را به‌صورت دستی در Git database ذخیره کنیم:

$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4

در ساده‌ترین حالت، git hash-object محتوایی که به آن می‌دهید را می‌گیرد و صرفاً کلید یکتایی برمی‌گرداند که می‌تواند برای ذخیره‌ی آن محتوا در Git database استفاده شود. گزینه -w به دستور می‌گوید که فقط کلید را برنگرداند، بلکه object را واقعاً در database بنویسد. گزینه --stdin هم به git hash-object می‌گوید که محتوا را از stdin دریافت کند؛ در غیر این صورت، دستور انتظار دارد نام فایلی در انتهای دستور داده شود که شامل محتوای مورد استفاده است.

خروجی دستور بالا یک hash ۴۰ کاراکتری است. این همان SHA-1 hash است – یک checksum از محتوایی که ذخیره کرده‌اید به‌علاوه‌ی یک header (که کمی بعد یاد می‌گیرید). حالا می‌توانید ببینید Git چطور داده‌های شما را ذخیره کرده است:

$ find .git/objects -type f
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

اگر دوباره دایرکتوری objects را بررسی کنید، می‌بینید که حالا یک فایل برای آن محتوای جدید وجود دارد. Git محتوا را در ابتدا به این شکل ذخیره می‌کند – یک فایل به‌ازای هر قطعه‌ی محتوا، با نامی که از SHA-1 آن محتوا و header گرفته شده است. زیرشاخه با دو کاراکتر اول SHA-1 نام‌گذاری می‌شود و اسم فایل، ۳۸ کاراکتر باقی‌مانده است.

وقتی محتوایی در object database دارید، می‌توانید با دستور git cat-file آن محتوا را بررسی کنید. این دستور مثل یک چاقوی سوئیسی برای مشاهده‌ی Git objectهاست. اگر به آن -p بدهید، اول نوع object را تشخیص می‌دهد و سپس آن را به‌درستی نمایش می‌دهد:

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

حالا می‌توانید محتوایی به Git اضافه کنید و دوباره آن را بیرون بکشید. همچنین می‌توانید همین کار را با فایل‌ها انجام دهید. برای مثال، می‌توانید روی یک فایل ساده، نسخه‌سازی انجام دهید. ابتدا یک فایل جدید بسازید و محتوای آن را در database ذخیره کنید:

$ echo 'version 1' > test.txt
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30

سپس محتوای جدیدی در فایل بنویسید و دوباره آن را ذخیره کنید:

$ echo 'version 2' > test.txt
$ git hash-object -w test.txt
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a

Database شما حالا هر دو نسخه‌ی این فایل جدید (به‌علاوه‌ی اولین محتوایی که ذخیره کرده‌اید) را دارد:

$ find .git/objects -type f
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

در این مرحله، می‌توانید نسخه‌ی محلی فایل test.txt را پاک کنید، سپس با Git آن را از database بازیابی کنید؛ یا نسخه‌ی اولی را:

$ git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt
$ cat test.txt
version 1

یا نسخه‌ی دومی را:

$ git cat-file -p 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a > test.txt
$ cat test.txt
version 2

اما به خاطر سپردن کلید SHA-1 هر نسخه از فایل عملی نیست؛ به‌علاوه، شما نام فایل را در سیستم ذخیره نمی‌کنید – فقط محتوا ذخیره می‌شود. این نوع object را یک blob می‌نامند. می‌توانید با دستور git cat-file -t و داشتن کلید SHA-1، نوع هر object را در Git ببینید:

$ git cat-file -t 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
blob

درخت اشیاء (Tree Objects)

نوع بعدی Git object که بررسی می‌کنیم tree است؛ این مشکل را حل می‌کند که نام فایل هم ذخیره شود و علاوه بر آن امکان ذخیره‌ی گروهی از فایل‌ها را هم فراهم می‌کند. Git داده‌ها را به شکلی شبیه به فایل‌سیستم UNIX ذخیره می‌کند، اما کمی ساده‌تر. همه‌ی محتوا به‌صورت tree و blob ذخیره می‌شود؛ treeها مشابه ورودی‌های دایرکتوری در UNIX هستند و blobها تقریباً متناظر با inodeها یا محتوای فایل‌ها. یک tree object شامل یک یا چند ورودی است؛ هرکدام یک SHA-1 hash از یک blob یا subtree همراه با mode، نوع (type) و نام فایل هستند. مثلاً فرض کنید پروژه‌ای دارید که آخرین tree آن چیزی شبیه این است:

$ git cat-file -p master^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859      README
100644 blob 8f94139338f9404f26296befa88755fc2598c289      Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0      lib

سینتکس master^{tree} مشخص می‌کند که کدام tree object توسط آخرین commit روی شاخه‌ی master اشاره می‌شود. دقت کنید که زیرشاخه‌ی lib یک blob نیست، بلکه یک اشاره‌گر به یک tree دیگر است:

$ git cat-file -p 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0
100644 blob 47c6340d6459e05787f644c2447d2595f5d3a54b      simplegit.rb
یادداشت

بسته به اینکه از چه shellی استفاده می‌کنید، ممکن است هنگام استفاده از سینتکس master^{tree} به خطا بخورید.

  • در CMD روی Windows، کاراکتر ^ برای escaping استفاده می‌شود، پس باید آن را دوبل کنید: git cat-file -p master^^{tree}

  • در PowerShell، پارامترهایی که از {} استفاده می‌کنند باید کوتیشن شوند: git cat-file -p 'master^{tree}'

  • در ZSH، کاراکتر ^ برای globbing استفاده می‌شود، پس باید کل عبارت را در کوتیشن بگذارید: git cat-file -p "master^{tree}"

از نظر مفهومی، داده‌ای که Git ذخیره می‌کند چیزی شبیه این است:

Simple version of the Git data model
نمودار 173. Simple version of the Git data model

شما می‌توانید به‌راحتی tree خودتان را بسازید. معمولاً Git با گرفتن وضعیت staging area یا index شما و نوشتن مجموعه‌ای از tree objectها از آن، یک tree می‌سازد. پس برای ایجاد یک tree object، ابتدا باید یک index با staging بعضی فایل‌ها بسازید. برای ساخت یک index با یک ورودی – اولین نسخه‌ی فایل test.txt – می‌توانید از دستور plumbing به نام git update-index استفاده کنید. این دستور را برای اضافه کردن نسخه‌ی قبلی test.txt به یک staging area جدید استفاده می‌کنید. باید گزینه‌ی --add را بدهید چون فایل هنوز در staging area وجود ندارد (حتی staging area هم هنوز ساخته نشده) و --cacheinfo چون فایلی که اضافه می‌کنید روی دایرکتوری نیست بلکه در database است. سپس mode، SHA-1 و نام فایل را مشخص می‌کنید:

$ git update-index --add --cacheinfo 100644 \
  83baae61804e65cc73a7201a7252750c76066a30 test.txt

در اینجا، شما یک mode با مقدار 100644 مشخص می‌کنید که به معنی یک فایل عادی است. گزینه‌های دیگر عبارتند از: 100755 برای فایل اجرایی (executable) و 120000 برای لینک نمادین (symbolic link). Mode از حالت‌های استاندارد UNIX گرفته شده اما بسیار ساده‌تر است – این سه حالت تنها مقادیر معتبر برای فایل‌ها (blobها) در Git هستند (البته modeهای دیگری برای دایرکتوری‌ها و submoduleها استفاده می‌شوند).

حالا می‌توانید با دستور git write-tree محتوای staging area را به یک tree object بنویسید. نیازی به گزینه -w نیست – اجرای این دستور به‌طور خودکار یک tree object از وضعیت index ایجاد می‌کند اگر هنوز وجود نداشته باشد:

$ git write-tree
d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
100644 blob 83baae61804e65cc73a7201a7252750c76066a30      test.txt

می‌توانید با همان دستور git cat-file که قبلاً دیدید، بررسی کنید که این یک tree object است:

$ git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
tree

حالا یک tree جدید با نسخه دوم فایل test.txt و همچنین یک فایل جدید ایجاد می‌کنید:

$ echo 'new file' > new.txt
$ git update-index --cacheinfo 100644 \
  1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
$ git update-index --add new.txt

staging area شما حالا شامل نسخه جدید test.txt و همچنین فایل new.txt است. tree جدید را بنویسید (ثبت وضعیت staging area یا index به‌عنوان یک tree object) و ببینید چه شکلی است:

$ git write-tree
0155eb4229851634a0f03eb265b69f5a2d56f341
$ git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

دقت کنید که این tree هم فایل‌های مختلف دارد و هم اینکه SHA-1 مربوط به test.txt همان SHA-1 نسخه دوم از قبل (1f7a7a) است. برای سرگرمی، شما tree اول را به‌عنوان یک زیرشاخه به این tree اضافه می‌کنید. می‌توانید treeها را با دستور git read-tree به staging area بخوانید. در اینجا، با استفاده از گزینه‌ی --prefix یک tree موجود را به‌عنوان subtree وارد staging area می‌کنید:

$ git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
$ git write-tree
3c4e9cd789d88d8d89c1073707c3585e41b0e614
$ git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614
040000 tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579      bak
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a      test.txt

اگر از tree جدیدی که نوشتید یک working directory بسازید، دو فایل در سطح اصلی working directory خواهید داشت و یک زیرشاخه به نام bak که شامل نسخه اول فایل test.txt است. می‌توانید داده‌ای که Git برای این ساختارها ذخیره می‌کند را این‌گونه تصور کنید:

The content structure of your current Git data
نمودار 174. The content structure of your current Git data

کامیت اشیاء (Commit Objects)

اگر همه‌ی مراحل بالا را انجام داده باشید، اکنون سه tree دارید که snapshotهای مختلف پروژه شما را نمایش می‌دهند؛ اما مشکل قبلی باقی‌ست: باید هر سه SHA-1 را به خاطر بسپارید تا بتوانید snapshotها را بازیابی کنید. همچنین هیچ اطلاعاتی درباره اینکه چه کسی snapshotها را ذخیره کرده، چه زمانی و چرا ذخیره شده‌اند، ندارید. این همان اطلاعات پایه‌ای است که یک commit object برای شما ذخیره می‌کند.

برای ایجاد یک commit object، دستور commit-tree را فراخوانی کرده و یک tree SHA-1 و commitهای قبلی (در صورت وجود) را مشخص می‌کنید. با اولین tree که نوشتید شروع کنید:

$ echo 'First commit' | git commit-tree d8329f
fdf4fc3344e67ab068f836878b6c4951e3b15f3d
یادداشت

شما یک hash متفاوت دریافت خواهید کرد، چون زمان ایجاد و داده‌های نویسنده متفاوت است. در اصل، هر commit object با داشتن آن داده‌ها می‌تواند به‌طور دقیق بازتولید شود، اما جزئیات تاریخیِ تهیه این کتاب باعث می‌شود commit hashهای چاپ‌شده الزاماً با commitهای داده‌شده یکی نباشند. در ادامه، commit و tag hashها را با checksumهای خودتان جایگزین کنید.

حالا می‌توانید commit object جدید خود را با دستور git cat-file بررسی کنید:

$ git cat-file -p fdf4fc3
tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Scott Chacon <schacon@gmail.com> 1243040974 -0700
committer Scott Chacon <schacon@gmail.com> 1243040974 -0700

First commit

فرمت یک commit object ساده است: مشخص می‌کند که tree سطح بالا برای snapshot پروژه در آن نقطه چیست؛ commitهای والد در صورت وجود (commit بالا هیچ والدی ندارد)؛ اطلاعات author/committer (که از تنظیمات user.name و user.email شما و یک timestamp استفاده می‌کند)؛ یک خط خالی و سپس پیام commit.

سپس، دو commit object دیگر را می‌نویسید که هرکدام به commit قبلی خود اشاره دارند:

$ echo 'Second commit' | git commit-tree 0155eb -p fdf4fc3
cac0cab538b970a37ea1e769cbbde608743bc96d
$ echo 'Third commit'  | git commit-tree 3c4e9c -p cac0cab
1a410efbd13591db07496601ebc7a059dd55cfe9

هر سه commit object به یکی از سه snapshot treeای که ساخته‌اید اشاره می‌کنند. جالب است که حالا یک Git history واقعی دارید که می‌توانید با اجرای git log روی آخرین commit SHA-1 آن را ببینید:

$ git log --stat 1a410e
commit 1a410efbd13591db07496601ebc7a059dd55cfe9
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:15:24 2009 -0700

	Third commit

 bak/test.txt | 1 +
 1 file changed, 1 insertion(+)

commit cac0cab538b970a37ea1e769cbbde608743bc96d
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:14:29 2009 -0700

	Second commit

 new.txt  | 1 +
 test.txt | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

commit fdf4fc3344e67ab068f836878b6c4951e3b15f3d
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri May 22 18:09:34 2009 -0700

    First commit

 test.txt | 1 +
 1 file changed, 1 insertion(+)

شگفت‌انگیز است. شما به‌تازگی عملیات سطح پایین برای ساختن یک Git history را بدون استفاده از دستورات front-end انجام دادید. این دقیقاً همان کاری است که Git هنگام اجرای git add و git commit انجام می‌دهد – blobهایی برای فایل‌های تغییر یافته ذخیره می‌کند، index را به‌روزرسانی می‌کند، treeها را می‌نویسد و commit objectهایی که به treeهای سطح بالا و commitهای قبلی اشاره دارند ایجاد می‌کند. این سه Git object اصلی – blob، tree و commit – در ابتدا به‌عنوان فایل‌های جداگانه در دایرکتوری .git/objects ذخیره می‌شوند. در اینجا همه objectهای موجود در دایرکتوری مثال را می‌بینید که هرکدام با توضیح آنچه ذخیره می‌کنند، مشخص شده‌اند:

$ find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1

اگر تمام اشاره‌گرهای داخلی را دنبال کنید، یک object graph شبیه این خواهید داشت:

All the reachable objects in your Git directory
نمودار 175. All the reachable objects in your Git directory

ذخیره سازی اشیاء (Object Storage)

پیش‌تر اشاره کردیم که همراه هر object که در Git database ذخیره می‌کنید، یک header هم وجود دارد. بیایید ببینیم Git چطور objectها را ذخیره می‌کند. شما می‌بینید که چگونه می‌توان یک blob object – در این مثال رشته‌ی “what is up, doc?” – را به‌صورت تعاملی در زبان Ruby ذخیره کرد.

می‌توانید حالت تعاملی Ruby را با دستور irb اجرا کنید:

$ irb
>> content = "what is up, doc?"
=> "what is up, doc?"

Git ابتدا یک header می‌سازد که نوع object (در اینجا blob) را مشخص می‌کند. به این بخش اول header، یک فاصله و سپس اندازه محتوای برحسب بایت اضافه می‌شود و در انتها یک null byte قرار می‌گیرد:

>> header = "blob #{content.bytesize}\0"
=> "blob 16\u0000"

سپس Git این header و محتوای اصلی را به هم متصل کرده و checksum آن را با الگوریتم SHA-1 محاسبه می‌کند. در Ruby می‌توانید مقدار SHA-1 یک رشته را با include کردن کتابخانه‌ی SHA1 digest با دستور require و سپس فراخوانی Digest::SHA1.hexdigest() محاسبه کنید:

>> store = header + content
=> "blob 16\u0000what is up, doc?"
>> require 'digest/sha1'
=> true
>> sha1 = Digest::SHA1.hexdigest(store)
=> "bd9dbf5aae1a3862dd1526723246b20206e5fc37"

بیایید این را با خروجی دستور git hash-object مقایسه کنیم. اینجا از echo -n استفاده می‌کنیم تا یک newline به ورودی اضافه نشود:

$ echo -n "what is up, doc?" | git hash-object --stdin
bd9dbf5aae1a3862dd1526723246b20206e5fc37

Git محتوای جدید را با zlib فشرده می‌کند؛ در Ruby هم می‌توانید با کتابخانه‌ی zlib این کار را انجام دهید. ابتدا باید کتابخانه را require کنید و سپس Zlib::Deflate.deflate() را روی محتوا اجرا کنید:

>> require 'zlib'
=> true
>> zlib_content = Zlib::Deflate.deflate(store)
=> "x\x9CK\xCA\xC9OR04c(\xCFH,Q\xC8,V(-\xD0QH\xC9O\xB6\a\x00_\x1C\a\x9D"

در نهایت، محتوای فشرده‌شده با zlib را روی دیسک به‌عنوان یک object می‌نویسید. مسیر object را بر اساس SHA-1 تعیین می‌کنید (دو کاراکتر اول به‌عنوان نام زیرشاخه و ۳۸ کاراکتر باقی‌مانده به‌عنوان نام فایل درون آن دایرکتوری). در Ruby می‌توانید از تابع FileUtils.mkdir_p() برای ایجاد زیرشاخه در صورت عدم وجود آن استفاده کنید. سپس فایل را با File.open() باز کرده و محتوای فشرده‌شده را با یک فراخوانی write() روی file handle ایجادشده بنویسید:

>> path = '.git/objects/' + sha1[0,2] + '/' + sha1[2,38]
=> ".git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37"
>> require 'fileutils'
=> true
>> FileUtils.mkdir_p(File.dirname(path))
=> ".git/objects/bd"
>> File.open(path, 'w') { |f| f.write zlib_content }
=> 32

حالا محتوای object را با دستور git cat-file بررسی کنید:

---
$ git cat-file -p bd9dbf5aae1a3862dd1526723246b20206e5fc37
what is up, doc?
---

همین! شما یک Git blob object معتبر ساخته‌اید.

تمام Git objectها به همین شیوه ذخیره می‌شوند، فقط نوع آن‌ها متفاوت است – به‌جای رشته‌ی blob، header با commit یا tree شروع می‌شود. همچنین، در حالی که محتوای blob می‌تواند تقریباً هر چیزی باشد، محتوای commit و tree ساختار بسیار مشخصی دارند.

scroll-to-top