توی این درس یاد میگیری Git زیر کاپوت چه میکند. میفهمی پوشهی .git شامل چیست، و کل Git روی چهار نوع شیء ساخته شده: blob (محتوای یک فایل)، tree (یک پوشه)، commit و tag؛ هر شیء با یک شناسهی SHA که از خود محتوایش حساب میشود پیدا میشود. میبینی شاخه فقط یک فایل ۴۱ بایتی است (یک اشارهگر)، با git cat-file -p از یک commit تا فایلهایش پایین میروی، و حتی یک commit را فقط با دستورهای سطح پایین میسازی. تمرین: یک commit را با cat-file تا فایلهایش دنبال کن.
مسئله: جعبهی سیاه، نه
Section titled “مسئله: جعبهی سیاه، نه”تا حالا Git را با دستورهایش (add، commit، merge) استفاده کردی. وقتی چیزی غیرمنتظره رخ میدهد (detached HEAD، یک commit «گمشده»، چیزی که انگار پاک شده)، اگر نمیدانی چه چیزی زیرش است حدس میزنی. خبر خوب: مدل داخلی Git ساده و کوچک است و تقریباً هر رفتار عجیبش از همان چند ایده نتیجه میشود. در این درس همهی آن را مستقیم میبینی، با خود فایلها و دستورهای سطح پایین.
تشبیه: انبار با اثر انگشت
Section titled “تشبیه: انبار با اثر انگشت”Git یک انبار بزرگ است که هر بسته را با اثر انگشت محتوایش برچسب میزند. اگر دو بستهی هممحتوا بیاید، یک بسته میماند (اثر انگشتشان یکی است). برای پیدا کردن یک بسته فقط اثر انگشتش را لازم داری. «commit» یک بستهی ویژه است که فهرست محتوای یک لحظه (tree) را با اثر انگشتها نگه میدارد و اثر انگشت بستهی قبلی را. شاخه فقط یک یادداشت چسبان است: «آخرین بستهی این خط، این اثر انگشت».
چهار نوع شیء
Section titled “چهار نوع شیء”| شیء | چه چیزی را نگه میدارد |
|---|---|
| blob | محتوای یک فایل (بدون اسم و بدون دسترسی) |
| tree | فهرست یک پوشه: برای هر ورودی «دسترسی، نوع، شناسه، اسم»؛ ورودی میتواند blob یا tree دیگر باشد |
| commit | اشاره به یک tree (عکس کامل پروژه)، اشاره به commit(های) قبلی (parent)، نویسنده، زمان و پیام |
| tag (annotated) | اشاره به یک شیء (معمولاً commit) با نام، سازنده و پیام |
مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: داخل .git
Section titled “مثال ۱: داخل .git”یک مخزن تازه میسازم و پوشهی .git را قبل و بعد از اولین commit میبینم:
mkdir -p ~/gitlab/inside && cd ~/gitlab/inside && git init -qecho "--- .git یک مخزن خالی:"ls -A .gitecho "--- تعداد فایلهای داخل objects:"find .git/objects -type f | wc -lmkdir src && echo "# Inside" > README.md && echo "print('hi')" > src/app.pygit add . && git commit -qm "First commit"echo "--- بعد از اولین commit:"ls -A .gitecho "--- شیءها:"find .git/objects -type f | sort | head--- .git یک مخزن خالی:HEADbranchesconfigdescriptionhooksinfoobjectsrefs--- تعداد فایلهای داخل objects:0--- بعد از اولین commit:COMMIT_EDITMSGHEADbranchesconfigdescriptionhooksindexinfologsobjectsrefs--- شیءها:.git/objects/75/5d89e1c086583a7bef11c39dbdd6859858a3f6.git/objects/7c/11fbe2228a1c4d2187bedf0e960022995d7977.git/objects/9f/1b437537a2acdadafd3174f6f0af9c1a04f5e4.git/objects/c7/8376de751455c850d91200056a979841b60023.git/objects/e3/be94dbf1b118bf31bf5d612a6bd5429c28699aدر .git |
کار |
|---|---|
objects/ |
همهی داده: blob ها، tree ها، commit ها (و بعد از gc: بستههای pack) |
refs/ |
برچسبها: heads/ (شاخهها)، tags/، remotes/ |
HEAD |
اشارهگر به شاخهی فعلی (یا یک commit در حالت detached) |
index |
ناحیهی staging (فایل باینری) |
config |
تنظیمات همین مخزن |
logs/ |
reflog |
hooks/ |
اسکریپتهای hook |
COMMIT_EDITMSG، ORIG_HEAD، FETCH_HEAD |
فایلهای موقتی عملیاتهای اخیر |
بعد از اولین commit، دو blob (برای README.md و src/app.py)، دو tree (ریشه و src) و یک commit ساخته شد: جمعاً ۵ شیء، هر کدام یک فایل در objects/ که اسمش از شناسهی خودش است (دو حرف اول اسم پوشه و بقیه اسم فایل).
مثال ۲: شناسه از محتوا ساخته میشود
Section titled “مثال ۲: شناسه از محتوا ساخته میشود”هر شیء با SHA-1 محتوایش (با یک سرآیند) نامگذاری میشود. این را مستقیم ببین: git hash-object شناسهی یک محتوا را حساب میکند و برای اینکه جادو نباشد، با sha1sum همان را دستی بازسازی میکنم:
echo "--- شناسهای که Git برای متن «hello» (با یک خط جدید) حساب میکند:"echo "hello" | git hash-object --stdinecho "--- همان را دستی بساز: SHA-1 از «blob ‹اندازه›\0‹محتوا›»:"printf 'blob 6\0hello\n' | sha1sumecho "--- محتوای متفاوت، شناسهی کاملاً متفاوت (فقط یک نویسه فرق دارد):"echo "Hello" | git hash-object --stdin--- شناسهای که Git برای متن «hello» (با یک خط جدید) حساب میکند:ce013625030ba8dba906f756967f9e9ca394464a--- همان را دستی بساز: SHA-1 از «blob ‹اندازه›\0‹محتوا›»:ce013625030ba8dba906f756967f9e9ca394464a ---- محتوای متفاوت، شناسهی کاملاً متفاوت (فقط یک نویسه فرق دارد):e965047ad7c57865823c7d992b1d046ea66edf78دو شناسهی اول دقیقاً یکی هستند: Git واقعاً blob، اندازه، یک بایت صفر و محتوا را به SHA-1 میدهد. و عوضشدن یک حرف شناسه را کاملاً عوض کرد. نتیجهها: همان محتوا همیشه همان شناسه را دارد (روی هر کامپیوتر، همیشه)، و شناسه اثر انگشت محتوا است. (Git از یک نسخهی SHA-1 مقاوم در برابر برخورد (collision) استفاده میکند و یک حالت SHA-256 هم دارد که شناسهها ۶۴ حرفی میشوند؛ مخزنهای SHA-256 هنوز کمتر استفاده میشوند:)
git init -q --object-format=sha256 /tmp/lx-sha256cd /tmp/lx-sha256 && echo "hello" | git hash-object --stdinrm -rf /tmp/lx-sha2562cf8d83d9ee29543b34a87727421fdecb7e3f3a183d337639025de576db9ebb4مثال ۳: یک شیء روی دیسک
Section titled “مثال ۳: یک شیء روی دیسک”-w شیء را در objects/ مینویسد. فایل را پیدا و محتوای واقعیاش را بخوان؛ شیءها با zlib فشردهاند، پس با cat نمیخوانی، ولی با python3 باز میکنی:
mkdir -p ~/gitlab/obj && cd ~/gitlab/obj && git init -qblob=$(echo "hello" | git hash-object -w --stdin)echo "شناسه: $blob"echo "--- فایلش در objects (دو حرف اول = پوشه):"find .git/objects -type fecho "--- Git چه میگوید؟ نوع، اندازه، محتوا:"git cat-file -t $blobgit cat-file -s $blobgit cat-file -p $blobecho "--- و محتوای واقعی فایل بعد از باز کردن zlib:"python3 -c "import zlib,glob; f=glob.glob('.git/objects/ce/*')[0]; print(zlib.decompress(open(f,'rb').read()))"شناسه: ce013625030ba8dba906f756967f9e9ca394464a--- فایلش در objects (دو حرف اول = پوشه):.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a--- Git چه میگوید؟ نوع، اندازه، محتوا:blob6hello--- و محتوای واقعی فایل بعد از باز کردن zlib:b'blob 6\x00hello\n'محتوای واقعی فایل: blob 6\0hello\n: همان سرآیند و محتوا که SHA-1 رویش حساب شد. همهی دیتابیس Git همین است: فایلهایی با اسم SHA و محتوای فشردهی «نوع اندازه\0محتوا».
مثال ۴: ساختن یک commit فقط با دستورهای سطح پایین
Section titled “مثال ۴: ساختن یک commit فقط با دستورهای سطح پایین”دستورهایی که تا حالا زدی (add، commit) «porcelain» (چینیآلات: رابط تمیز برای آدم) هستند. زیرشان دستورهای «plumbing» (لولهکشی: ابزار سطح پایین) هستند. با آنها یک commit را از صفر و بدون git add و git commit میسازم؛ همین نشان میدهد commit چیست:
mkdir -p ~/gitlab/manual && cd ~/gitlab/manual && git init -qecho "--- ۱) محتوا را در یک blob بنویس:"blob=$(echo "hello" | git hash-object -w --stdin); echo "blob: $blob"echo "--- ۲) آن را به ناحیهی staging با اسم hello.txt معرفی کن (دسترسی 100644 = فایل عادی):"git update-index --add --cacheinfo 100644 $blob hello.txtecho "--- ۳) از staging یک tree بساز:"tree=$(git write-tree); echo "tree: $tree"git cat-file -p $treeecho "--- ۴) یک commit بساز که به آن tree اشاره میکند:"commit=$(echo "اولین commit دستی" | git commit-tree $tree); echo "commit: $commit"git cat-file -p $commitecho "--- ۵) برچسب شاخه را روی آن بگذار:"git update-ref refs/heads/main $commitgit log --onelineecho "--- فایل را هم از تاریخچه بیرون بکش:"git restore hello.txt && cat hello.txtgit status -s; echo "(پوشه تمیز)"--- ۱) محتوا را در یک blob بنویس:blob: ce013625030ba8dba906f756967f9e9ca394464a--- ۲) آن را به ناحیهی staging با اسم hello.txt معرفی کن (دسترسی 100644 = فایل عادی):--- ۳) از staging یک tree بساز:tree: aaa96ced2d9a1c8e72c56b253a0e2fe78393feb7100644 blob ce013625030ba8dba906f756967f9e9ca394464a hello.txt--- ۴) یک commit بساز که به آن tree اشاره میکند:commit: 26760237eef20b775456a0df931d06368bfb78d6tree aaa96ced2d9a1c8e72c56b253a0e2fe78393feb7author Ali <ali@example.com> 1791105171 +0000committer Ali <ali@example.com> 1791105171 +0000
اولین commit دستی--- ۵) برچسب شاخه را روی آن بگذار:2676023 اولین commit دستی--- فایل را هم از تاریخچه بیرون بکش:hello(پوشه تمیز)پنج قدم: blob ← index ← tree ← commit ← ref. git commit معمولی دقیقاً همین زنجیره را میرود. چیزی که به commit میگوید «اینجا شاخهی main است» فقط update-ref بود. و دقت کن چیزی به اسم «تغییر» یا «diff» ذخیره نشد؛ فقط عکسها.
مثال ۵: دنبال کردن یک commit تا فایلهایش (تمرین اصلی)
Section titled “مثال ۵: دنبال کردن یک commit تا فایلهایش (تمرین اصلی)”حالا روی یک پروژهی واقعیتر (مثال ۱)، از HEAD شروع میکنم و فقط با cat-file تا محتوای فایلها پایین میروم:
cd ~/gitlab/insideecho "=== HEAD به کدام commit اشاره میکند؟"cat .git/HEADgit rev-parse HEADecho "=== خود شیء commit:"git cat-file -p HEADecho "=== tree ریشه (از روی خط tree در commit):"git cat-file -p 'HEAD^{tree}'echo "=== tree ِ زیرپوشهی src:"git cat-file -p 'HEAD:src'echo "=== و محتوای blob ِ src/app.py:"git cat-file -p 'HEAD:src/app.py'=== HEAD به کدام commit اشاره میکند؟ref: refs/heads/maine3be94dbf1b118bf31bf5d612a6bd5429c28699a=== خود شیء commit:tree c78376de751455c850d91200056a979841b60023author Ali <ali@example.com> 1791105171 +0000committer Ali <ali@example.com> 1791105171 +0000
First commit=== tree ریشه (از روی خط tree در commit):100644 blob 7c11fbe2228a1c4d2187bedf0e960022995d7977 README.md040000 tree 755d89e1c086583a7bef11c39dbdd6859858a3f6 src=== tree ِ زیرپوشهی src:100644 blob 9f1b437537a2acdadafd3174f6f0af9c1a04f5e4 app.py=== و محتوای blob ِ src/app.py:print('hi')زنجیره: commit (خط tree) ← tree ریشه (ورودیهایش: README.md یک blob و src یک tree) ← tree ِ src ← blob ِ app.py. میانبُرها: HEAD^{tree} یعنی «tree ِ commit فعلی»، و HEAD:مسیر (مثل HEAD:src/app.py) یعنی «شیء در این مسیر درون commit». هر بار شناسهی tree یا blob را دستی کپی نمیکنی. دو چیز دیگر که در خروجی دیدی: 100644 دسترسی یک فایل عادی است، 100755 اجرایی و 040000 پوشه.
مثال ۶: محتوای یکسان، یک شیء
Section titled “مثال ۶: محتوای یکسان، یک شیء”چون شناسه از محتوا ساخته میشود، دو فایل هممحتوا یک blob میشوند و فایلی که تغییر نکرده در commit بعدی دوباره ذخیره نمیشود:
mkdir -p ~/gitlab/dedup && cd ~/gitlab/dedup && git init -qecho "same content" > a.txt && cp a.txt b.txt && cp a.txt c.txtgit add . && git commit -qm "Three files, one content"echo "--- شناسهی blob هر سه فایل (در tree):"git ls-tree HEADecho "--- تعداد شیءها: ۱ blob + ۱ tree + ۱ commit:"git count-objects -v | grep '^count'echo "--- commit دوم: فقط a.txt عوض میشود:"echo "changed" > a.txt && git commit -qam "Change a only"git ls-tree HEADecho "--- b.txt و c.txt هنوز به همان blob قبلی اشاره میکنند؛ جمع شیءها:"git count-objects -v | grep '^count'--- شناسهی blob هر سه فایل (در tree):100644 blob 01e6138faef088714355b81759a88101ce07a1a3 a.txt100644 blob 01e6138faef088714355b81759a88101ce07a1a3 b.txt100644 blob 01e6138faef088714355b81759a88101ce07a1a3 c.txt--- تعداد شیءها: ۱ blob + ۱ tree + ۱ commit:count: 3--- commit دوم: فقط a.txt عوض میشود:100644 blob 5ea2ed416fbd4a4cbe227b75fe255dd7fa6bd4d6 a.txt100644 blob 01e6138faef088714355b81759a88101ce07a1a3 b.txt100644 blob 01e6138faef088714355b81759a88101ce07a1a3 c.txt--- b.txt و c.txt هنوز به همان blob قبلی اشاره میکنند؛ جمع شیءها:count: 6سه فایل هممحتوا یک blob دارند (شناسهی یکسان در ls-tree). بعد از commit دوم فقط ۳ شیء جدید اضافه شد: blob ِ جدید برای a.txt، tree ِ جدید و commit؛ ولی b.txt و c.txt همان blob اول را دارند. اشارهگر به محتوا، نه کپی محتوا، همان دلیلی است که شاخهسازی و commit در Git ارزان است. (و دلیل اینکه جابهجا کردن یا تغییر نام یک فایل هم blob جدیدی نمیسازد.)
مثال ۷: شاخه، HEAD و tag فقط اشارهگرند
Section titled “مثال ۷: شاخه، HEAD و tag فقط اشارهگرند”اینها همه فایلهای کوچک در .git/refs هستند که هش یک شیء را دارند. git for-each-ref همه را فهرست میکند:
cd ~/gitlab/insidegit switch -qc feature && git tag -a v1.0 -m "First" main && git switch -q mainecho "--- شاخه، tag و HEAD؛ هر کدام یک فایل:"cat .git/HEADcat .git/refs/heads/mainls .git/refs/heads .git/refs/tagsecho "--- حجم فایل شاخه (۴۰ نویسهی هش + خط جدید = ۴۱ بایت):"wc -c < .git/refs/heads/mainecho "--- git for-each-ref:"git for-each-ref --format='%(objecttype) %(refname)'echo "--- HEAD یک اشارهگر به اشارهگر است:"git symbolic-ref HEAD--- شاخه، tag و HEAD؛ هر کدام یک فایل:ref: refs/heads/maine3be94dbf1b118bf31bf5d612a6bd5429c28699a.git/refs/heads:featuremain
.git/refs/tags:v1.0--- حجم فایل شاخه (۴۰ نویسهی هش + خط جدید = ۴۱ بایت):41--- git for-each-ref:commit refs/heads/featurecommit refs/heads/maintag refs/tags/v1.0--- HEAD یک اشارهگر به اشارهگر است:refs/heads/mainHEAD معمولاً یک اشارهگر نمادین به یک شاخه است (ref: refs/heads/main) و شاخه به یک commit. در حالت detached HEAD فایل HEAD مستقیم یک هش دارد. tag سبک مستقیماً به commit و tag annotated به یک شیء tag اشاره میکند (objecttype: tag، درس tag). برای همین ساختن و پاککردن شاخه اینقدر ارزان است: یک فایل ۴۱ بایتی.
مثال ۸: شناسهی commit را دستی بازسازی کن
Section titled “مثال ۸: شناسهی commit را دستی بازسازی کن”شناسهی هر commit هم SHA-1 محتوای خود commit است (tree، parent، نویسنده، زمان، پیام) با سرآیند «commit اندازه\0». این هم دستی قابلبازسازی است، و همین دلیل ایمنبودن تاریخچه است: هر تغییر در هر چیزِ آن، شناسه را عوض میکند، و چون شناسهی commit بعدی (فرزند) شامل شناسهی این commit (parent) است، تغییر یکی همهی نسلهای بعدی را هم عوض میکند:
cd ~/gitlab/insideecho "--- شناسهی commit طبق Git:"git rev-parse HEADecho "--- همان را دستی از محتوای commit:"size=$(git cat-file commit HEAD | wc -c)( printf "commit %s\0" "$size"; git cat-file commit HEAD ) | sha1sum--- شناسهی commit طبق Git:e3be94dbf1b118bf31bf5d612a6bd5429c28699a--- همان را دستی از محتوای commit:e3be94dbf1b118bf31bf5d612a6bd5429c28699a -دو شناسه یکی است. به همین دلیل «بازنویسی تاریخچه» (amend، rebase، reset) در واقع ساختن commit های جدید با شناسههای جدید است، نه ویرایش commit قبلی: commit قدیمی تغییرناپذیر است و فقط ممکن است دیگر هیچ برچسبی به آن اشاره نکند (درس نجات).
مثال ۹: بستهبندی (pack) و gc
Section titled “مثال ۹: بستهبندی (pack) و gc”اگر هر نسخهی کامل هر فایل را جدا ذخیره کنی، تاریخچهی بزرگ حجم زیادی میگیرد. Git شیءها را اول «لَخت» (loose) (هر کدام یک فایل) مینویسد، و بعد با git gc (یا خودکار، یا هنگام push) آنها را در یک فایل pack جمع میکند که نسخههای مشابه را delta (تفاوت نسبت به یک نسخهی دیگر) ذخیره میکند. یعنی مدل منطقی عکس کامل است ولی ذخیرهی فیزیکی کوچک میشود:
mkdir -p ~/gitlab/pack && cd ~/gitlab/pack && git init -qseq 1 2000 > big.txt && git add big.txt && git commit -qm "c0"for i in $(seq 1 15); do echo "line $i" >> big.txt; git commit -qam "c$i"; doneecho "--- قبل از gc (شیءهای لَخت):"git count-objects -vH | grep -E '^(count|size):'git gc -qecho "--- بعد از gc:"git count-objects -vH | grep -E '^(count|in-pack|packs|size-pack)'ls .git/objects/packecho "--- داخل pack (delta ها):"git verify-pack -v .git/objects/pack/*.idx | tail -3--- قبل از gc (شیءهای لَخت):count: 48size: 192.00 KiB--- بعد از gc:count: 0in-pack: 48packs: 1size-pack: 9.66 KiBpack-a61f6493f334ba9fafad514e3817be9e56a55196.idxpack-a61f6493f334ba9fafad514e3817be9e56a55196.packpack-a61f6493f334ba9fafad514e3817be9e56a55196.rev--- داخل pack (delta ها):non delta: 33 objectschain length = 1: 15 objects.git/objects/pack/pack-a61f6493f334ba9fafad514e3817be9e56a55196.pack: ok۱۶ نسخهی یک فایل ۱۰ کیلوبایتی (که هر بار فقط یک خط عوض میشود) قبل از gc حدود ۱۹۰ کیلوبایت و بعد از آن چند کیلوبایت است: chain length = 1: ۱۵ objects یعنی ۱۵ نسخه بهصورت delta روی نسخهی کامل ذخیره شدهاند. با فایلهای .pack (داده) و .idx (فهرست جستوجوی سریع). این یعنی «Git تفاوت ذخیره نمیکند» فقط در مدل درست است؛ خود بستهبندی از delta استفاده میکند، ولی هیچوقت به آن وابسته نیستی.
مثال ۱۰: Git خرابی را میفهمد
Section titled “مثال ۱۰: Git خرابی را میفهمد”چون هر شیء با اثر انگشت محتوایش نام دارد، هر دستکاری قابلتشخیص است. روی یک مخزن آزمایشی یک شیء را عمداً خراب میکنم و git fsck (File System ChecK) را میزنم:
mkdir -p ~/gitlab/corrupt && cd ~/gitlab/corrupt && git init -qecho "important" > f && git add f && git commit -qm "One"obj=$(git rev-parse HEAD:f)file=.git/objects/${obj:0:2}/${obj:2}chmod u+w $file && echo "garbage" > $fileecho "--- git fsck:"git fsck 2>&1 | head -4echo "--- خواندن همان شیء:"git cat-file -p $obj 2>&1 | head -2--- git fsck:error: inflate: data stream error (incorrect header check)error: unable to unpack header of .git/objects/ea/5b3d7ee315eb4bb4ba741e629f7a048b77c9c1error: ea5b3d7ee315eb4bb4ba741e629f7a048b77c9c1: object corrupt or missing: .git/objects/ea/5b3d7ee315eb4bb4ba741e629f7a048b77c9c1missing blob ea5b3d7ee315eb4bb4ba741e629f7a048b77c9c1--- خواندن همان شیء:error: inflate: data stream error (incorrect header check)error: unable to unpack ea5b3d7ee315eb4bb4ba741e629f7a048b77c9c1 headerGit هم موقع باز کردن (inflate: data stream error) و هم در fsck (object corrupt or missing) خرابی را فوراً میگیرد. این همان ویژگیای است که تضمین میکند تاریخچه بدون هیچکسشدنی بیخبر دستکاری نمیشود. (راه ترمیم: همان شیء را از یک کلون یا remote سالم میگیری؛ برای همین «هر کلون یک بکاپ است».)
پشت پردهی ناحیهی staging
Section titled “پشت پردهی ناحیهی staging”خود ناحیهی staging هم فقط یک فایل است: .git/index، فهرستی از «مسیر ← شناسهی blob» برای commit بعدی. git add blob را مینویسد و index را بهروز میکند؛ git write-tree از index یک tree میسازد. ببین:
cd ~/gitlab/insideecho "--- index چه دارد؟ (دسترسی، blob، شمارهی stage، مسیر):"git ls-files --stageecho "--- یک فایل جدید را add میکنم؛ blob همان لحظه ساخته میشود:"echo "new" > new.txt && git add new.txtgit ls-files --stage new.txtgit cat-file -p $(git ls-files --stage new.txt | awk '{print $2}')git restore --staged new.txt && rm new.txt--- index چه دارد؟ (دسترسی، blob، شمارهی stage، مسیر):100644 7c11fbe2228a1c4d2187bedf0e960022995d7977 0 README.md100644 9f1b437537a2acdadafd3174f6f0af9c1a04f5e4 0 src/app.py--- یک فایل جدید را add میکنم؛ blob همان لحظه ساخته میشود:100644 3e757656cf36eca53338e520d134963a44f793f8 0 new.txtnewجدولهای مرجع
Section titled “جدولهای مرجع”| دستور plumbing | کار |
|---|---|
git hash-object [-w] [--stdin] فایل |
شناسهی یک محتوا (و نوشتنش در objects) |
git cat-file -t / -s / -p شیء |
نوع / اندازه / محتوای زیبا |
git ls-tree [-r] commit |
محتوای یک tree |
git ls-files --stage |
محتوای index |
git update-index --add --cacheinfo حالت blob مسیر |
افزودن به index بدون فایل |
git write-tree |
از index یک tree بساز |
git commit-tree tree [-p parent] |
یک commit از یک tree بساز |
git update-ref ref commit |
برچسب را روی یک commit بگذار |
git rev-parse نام |
شناسهی کامل یک اسم |
git symbolic-ref HEAD |
شاخهی فعلی |
git for-each-ref |
فهرست همهی ref ها |
git count-objects -v / git verify-pack -v |
آمار شیءها / محتوای pack |
git fsck / git gc |
بررسی سلامت / بستهبندی و پاکسازی |
| آدرس | معنی |
|---|---|
HEAD^{tree} |
tree ِ commit فعلی |
HEAD:مسیر |
شیء (blob یا tree) در آن مسیر داخل commit |
HEAD~2 / HEAD^ |
دو commit قبل / والد اول |
main^{commit} |
commit ای که این شاخه یا tag به آن اشاره میکند |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) فکر اینکه Git تفاوت ذخیره میکند
Section titled “۱) فکر اینکه Git تفاوت ذخیره میکند”مدل Git «عکس» است؛ delta فقط یک بهینهسازی فیزیکی در pack است (مثال ۹). اگر بدانی commit یک عکس است، رفتار checkout، diff و … روشن میشود (دیف هر بار حساب میشود).
۲) ویرایش دستی فایلهای داخل .git
Section titled “۲) ویرایش دستی فایلهای داخل .git”شیءها فشردهاند و با SHA بررسی میشوند (مثال ۱۰)؛ دستکاری دستی آنها مخزن را خراب میکند. فایلهای ref و config را هم فقط با ابزار (git branch، git config) عوض کن. استثنا: خواندن آنها برای یادگیری (مثل این درس) بیخطر است.
۳) پاککردن .git/index
Section titled “۳) پاککردن .git/index”index فقط ناحیهی staging است؛ با git reset از آخرین commit بازسازی میشود، ولی هر چه staged بود (و هنوز commit نشده) از دست میرود.
۴) کپی کردن فقط پوشهی کاری
Section titled “۴) کپی کردن فقط پوشهی کاری”اگر فقط فایلهای کاری را کپی کنی و .git را نه، تاریخچه را نداری؛ و اگر فقط .git را کپی کنی، با git restore . فایلها برمیگردند. تاریخچه همهاش در .git است.
۵) نگرانی از برخورد SHA-1 و فراموشی SHA-256
Section titled “۵) نگرانی از برخورد SHA-1 و فراموشی SHA-256”یک «برخورد» (دو محتوای متفاوت با یک شناسه) در SHA-1 سادهی قدیمی نشان داده شده؛ ولی Git سالهاست از یک نسخهی SHA-1 همراه تشخیص حمله استفاده میکند و مسیر مهاجرت به SHA-256 هم دارد (که دیدی). در کار روزمره دغدغهی تو نیست؛ فقط بدان که شناسهها اثر انگشتاند، نه رمزنگاری سنگین.
برای commit فعلی یک مخزن، نوع و اندازهی شیء HEAD و شیء HEAD^{tree} را با git cat-file بخوان.
دیدن جواب
mkdir -p ~/gitlab/ex1 && cd ~/gitlab/ex1 && git init -qecho hi > a.txt && git add a.txt && git commit -qm "Add a"echo "HEAD:" $(git cat-file -t HEAD) $(git cat-file -s HEAD) "بایت"echo "tree:" $(git cat-file -t 'HEAD^{tree}') $(git cat-file -s 'HEAD^{tree}') "بایت"echo "blob:" $(git cat-file -t HEAD:a.txt) $(git cat-file -s HEAD:a.txt) "بایت"HEAD: commit 148 بایتtree: tree 33 بایتblob: blob 3 بایتتمرین اصلی: یک commit را با cat-file تا فایلهایش دنبال کن. مخزنی با یک پوشهی docs و دو فایل بساز؛ فقط با git cat-file -p (و هشی که از خروجی قبلی میگیری، بدون میانبر HEAD:مسیر) از HEAD شروع کن، tree ریشه، tree ِ docs و در پایان محتوای یک فایل را چاپ کن.
دیدن جواب
mkdir -p ~/gitlab/ex2/docs && cd ~/gitlab/ex2 && git init -qecho "# Project" > README.md && echo "Install: run it" > docs/install.txtgit add . && git commit -qm "Add docs"echo "== commit:"; git cat-file -p HEADtree=$(git cat-file -p HEAD | awk '/^tree/ {print $2}')echo "== tree ریشه ($tree):"; git cat-file -p $treedocs=$(git cat-file -p $tree | awk '$4=="docs" {print $3}')echo "== tree ِ docs ($docs):"; git cat-file -p $docsblob=$(git cat-file -p $docs | awk '$4=="install.txt" {print $3}')echo "== blob ($blob):"; git cat-file -p $blob== commit:tree 189a88d3eafd0da617a35948592fc04252a5e180author Ali <ali@example.com> 1791105171 +0000committer Ali <ali@example.com> 1791105171 +0000
Add docs== tree ریشه (189a88d3eafd0da617a35948592fc04252a5e180):100644 blob dab306f45e6a154ab0fe50d67298f165cfc75392 README.md040000 tree 574d081526803382180e7b0770039c3c94289cab docs== tree ِ docs (574d081526803382180e7b0770039c3c94289cab):100644 blob b552914cb640fa98ce4cee507ac0c18d112f9cf4 install.txt== blob (b552914cb640fa98ce4cee507ac0c18d112f9cf4):Install: run itیک مخزن با دو commit را فقط با دستورهای plumbing بساز (hash-object، update-index، write-tree، commit-tree، update-ref): commit اول با a.txt، commit دوم (با -p بهعنوان parent) که b.txt هم دارد. ثابت کن git log دو commit نشان میدهد، git status تمیز است، و git fsck مشکلی ندارد.
دیدن جواب
mkdir -p ~/gitlab/ex3 && cd ~/gitlab/ex3 && git init -qa=$(echo "file a" | git hash-object -w --stdin)git update-index --add --cacheinfo 100644 $a a.txtt1=$(git write-tree)c1=$(echo "first (manual)" | git commit-tree $t1)b=$(echo "file b" | git hash-object -w --stdin)git update-index --add --cacheinfo 100644 $b b.txtt2=$(git write-tree)c2=$(echo "second (manual)" | git commit-tree $t2 -p $c1)git update-ref refs/heads/main $c2git restore a.txt b.txt 2>/dev/null; git checkout -q -- . 2>/dev/nullgit log --onelinegit status -s; echo "(پوشه تمیز)"git fsck && echo "fsck: مشکلی نیست"git ls-tree HEAD7a0321d second (manual)7f8cc1e first (manual)(پوشه تمیز)fsck: مشکلی نیست100644 blob 4ef30bbfe26431a69c3820d3a683df54d688f2ec a.txt100644 blob 4f2e6529203aa6d44b5af6e3292c837ceda003f9 b.txtآزمونک
Section titled “آزمونک”چرا دو فایل هممحتوا در Git فضای ذخیرهی اضافه مصرف نمیکنند؟
blob فقط محتوا را دارد، نه اسم. اسم و دسترسی در tree است. محتوای یکسان = شناسهی یکسان = یک شیء.
چهار نوع شیء Git؟
blob = محتوا، tree = پوشه، commit = عکس + parent + پیام، tag = برچسب با پیام.
شاخه در Git چیست؟
برای همین ساخت و جابهجایی شاخه ارزان است.
شناسهی یک blob از چه ساخته میشود؟
همین محتوا همیشه همین شناسه را دارد.
چرا «بازنویسی تاریخچه» در واقع commit جدید میسازد؟
تغییر یک commit شناسهی همهی فرزندانش را هم عوض میکند.
git gc و packfile چه میکنند؟
مدل منطقی «عکس» است؛ delta فقط ذخیرهسازی فیزیکی است.
جمعبندی
Section titled “جمعبندی”.gitیک پایگاه دادهی کلید-مقدار است:objects/(داده)،refs/(برچسبها)،HEAD،index،config،logs/.- چهار شیء: blob (محتوای فایل)، tree (پوشه: دسترسی، نوع، شناسه، اسم)، commit (tree + parent + نویسنده + پیام)، tag. شناسه = SHA از محتوا (
blob اندازه\0محتوا): محتوای یکسان ⇒ شیء یکسان؛ هر تغییر ⇒ شناسهی جدید. - commit یک عکس کامل است نه diff؛ فایلهای تغییرنکرده به همان blob اشاره میکنند. شاخه، HEAD و tag فایلهای کوچک اشارهگراند (۴۱ بایت).
- زنجیرهی
git commit: blob ← index ← tree ← commit ← ref؛ میشود با plumbing (hash-object،update-index،write-tree،commit-tree،update-ref) دستی ساخت. git cat-file -p،HEAD^{tree}،HEAD:مسیر،git ls-tree،git ls-files --stageبرای کاوش.gcشیءها را در pack با delta فشرده میکند.git fsckهر خرابی را میگیرد.- «بازنویسی تاریخچه» یعنی commit های جدید با شناسههای جدید؛ commit قدیمی تغییرناپذیر است.
| دستور | کاری که میکند |
|---|---|
git cat-file -t / -s / -p شیء | نوع / اندازه / محتوا |
git cat-file -p 'HEAD^{tree}' | tree ریشهی commit فعلی |
git cat-file -p HEAD:مسیر | محتوای فایل در commit |
git ls-tree [-r] HEAD | فهرست محتوای tree |
git ls-files --stage | محتوای index |
echo متن | git hash-object [-w] --stdin | شناسه (و نوشتن) یک blob |
git write-tree / git commit-tree | ساخت tree و commit |
git update-ref refs/heads/main commit | برچسب شاخه |
git for-each-ref | همهی ref ها |
git count-objects -vH / git gc | آمار شیءها / بستهبندی |
git fsck | بررسی سلامت |