مسئلهای که Git حل میکند
The problem Git solves
بیشتر آدمها Git را با حفظ کردن پنج دستور یاد میگیرند و بعد هر بار که اتفاق
غیرمنتظرهای میافتد، پوشه را پاک میکنند و دوباره clone میگیرند.
این فصل آن پنج دستور را کنار میگذارد و از یک جای دیگر شروع میکند: از اینکه
Git وقتی commit میزنی، دقیقاً چه چیزی روی دیسک مینویسد.
Most people learn Git by memorising five commands, then delete the folder and
re-clone whenever something unexpected happens. This chapter sets those five
commands aside and starts somewhere else: with what Git actually writes to disk
when you make a commit.
دردی که Git درمان میکندThe pain Git treats
قبل از اینکه یک دستور بزنیم، بیایید ببینیم بدون Git چه میکردیم. چون هر قابلیت Git جواب یک درد مشخص است، و اگر آن درد را حس نکرده باشی، آن قابلیت برایت فقط یک دستور عجیب دیگر است.
Before typing a command, let us look at what we did without Git. Every Git feature answers a specific pain, and if you have not felt the pain, the feature is just another strange command.
این پوشه را احتمالاً دیدهای — شاید روی دسکتاپ خودت:
You have probably seen this folder — perhaps on your own desktop:
project/
project-backup/
project-backup2/
project-final/
project-final-REAL/
project-final-REAL-fixed/
project_ارسالی_مشتری/
project_ارسالی_مشتری_ویرایش۳/
این یک شوخی نیست؛ یک سیستم کنترل نسخه است. فقط سیستم بدی است. بیایید دقیق بگوییم چرا بد است، چون هر کدام از این ایرادها بعداً به یک قابلیت Git تبدیل میشود.
This is not a joke; it is a version control system. It is simply a bad one. Let us say precisely why it is bad, because each of these flaws later becomes a Git feature.
-
نمیدانی چه چیزی عوض شده
You cannot see what changed
بین
project-finalوproject-final-REALچه فرقی هست؟ باید فایلبهفایل باز کنی و چشمی مقایسه کنی. اگر پروژه هزار فایل داشته باشد، عملاً غیرممکن است. Git جواب این راdiffمینامد.What differs between
project-finalandproject-final-REAL? You must open them file by file and compare by eye. With a thousand files it is effectively impossible. Git calls the answer to thisdiff. -
نمیدانی چرا عوض شده
You cannot see why it changed
فرض کن فهمیدی خط ۴۲ عوض شده. چرا؟ سه ماه پیش خودت عوضش کردی و دلیلش را فراموش کردهای. نام پوشه جای توضیح نیست. Git جواب این را پیام
commitمینامد.Say you work out that line 42 changed. Why? You changed it three months ago and have forgotten the reason. A folder name is not a place for an explanation. Git calls the answer to this the
commitmessage. -
نمیتوانی مطمئن باشی چیزی گم نشده
You cannot be sure nothing was lost
کپیکردن پوشه ممکن است نصفه بماند، یک فایل جا بیفتد، یا فایلی خراب کپی شود و هیچکس تا شش ماه بعد نفهمد. Git جواب این را آدرسدهی بر اساس محتوا مینامد — که کل بخش سوم همین فصل است.
Copying a folder can stop halfway, miss a file, or corrupt one, and nobody notices for six months. Git calls the answer to this content addressing — the subject of section three.
-
دو نفر نمیتوانند همزمان کار کنند
Two people cannot work at once
اگر تو و همکارت هر دو روی یک فایل کار کنید، یکیتان کار دیگری را پاک میکند. راهحل سنتی این بود که فایل را «قفل» کنی تا کسی دیگر بازش نکند — یعنی همکارت باید منتظر بماند. Git جواب این را
mergeمینامد.If you and a colleague both edit one file, one of you erases the other's work. The traditional fix was to “lock” the file so nobody else could open it — meaning your colleague waits. Git calls the answer to this
merge.
هر بار در این مسیر با دستوری روبهرو شدی که منطقش عجیب به نظر میرسد، به این چهار درد برگرد. تقریباً همیشه جواب یکی از اینهاست. Git از روی سلیقه طراحی نشده؛ از روی مسئله طراحی شده.
Whenever a command in this track seems arbitrary, come back to these four pains. It is almost always answering one of them. Git was not designed from taste; it was designed from a problem.
Git از کجا آمدWhere Git came from
این بخش تاریخ برای حفظ کردن نیست. Git چند تصمیم عجیب دارد که فقط وقتی معنا میدهند که بدانی در چه شرایطی و برای چه کسی ساخته شد.
This section is not history to memorise. Git makes a few odd decisions that only make sense once you know the circumstances it was built under, and for whom.
کنترل نسخه از دههٔ ۱۹۷۰ وجود داشت. نسل اول — مثل RCS — فقط یک فایل
را روی یک ماشین دنبال میکرد. نسل دوم — CVS و بعد
Subversion — یک سرور مرکزی داشت که همهٔ تاریخچه آنجا بود و هر کسی
برای هر کاری باید به آن وصل میشد. این یعنی بدون شبکه نمیتوانستی
commit بزنی، و اگر سرور میمرد، تاریخچهٔ همه با آن میمرد.
Version control existed from the 1970s. The first generation — RCS — tracked one file
on one machine. The second — CVS, then Subversion — had a central server
holding all history, which everyone had to reach for everything. That meant no committing without
a network, and if the server died, everyone's history died with it.
در سال ۲۰۰۵، هستهٔ لینوکس از یک ابزار تجاری به نام BitKeeper استفاده
میکرد و دسترسی رایگانش قطع شد. مسئله این بود: هزاران توسعهدهنده در سراسر دنیا،
بدون هماهنگی مرکزی، روی پروژهای با میلیونها خط کد. لینوس توروالدز در چند هفته
ابزار تازهای نوشت با سه الزام صریح:
In 2005 the Linux kernel was using a commercial tool called BitKeeper, and its free
access was withdrawn. The problem was this: thousands of developers worldwide, with no central
coordination, on a project of millions of lines. Linus Torvalds wrote a new tool in a few weeks
with three explicit requirements:
کاملاً توزیعشده
Fully distributed
هر کپی باید کل تاریخچه را داشته باشد. برای همین است که
clone کند است و بعدش همهچیز سریع — تو داری یک پایگاهدادهٔ
کامل را میگیری، نه یک پوشه.
Every copy holds the whole history. That is why clone is slow and
everything afterwards is fast — you are fetching a complete database, not a folder.
شاخهبندی ارزان
Cheap branching
در Subversion ساختن شاخه یعنی کپیکردن کل پروژه. در Git
یعنی نوشتن یک فایل ۴۱ بایتی. فصل ۵ نشان میدهد چطور.
In Subversion a branch meant copying the project. In Git it means writing
a 41-byte file. Chapter 5 shows how.
تشخیص خرابی
Corruption detection
وقتی هزاران نفر ناشناس به تو کد میفرستند، باید بتوانی ثابت کنی چیزی دستکاری نشده. این الزام مستقیماً به مدل دادهای Git شکل داد — بخش بعد.
When thousands of strangers send you code, you must be able to prove nothing was tampered with. This requirement directly shaped Git's data model — the next section.
آن الزام سوم مهمترین است، چون چیزی که برای امنیت طراحی شد، بهطور اتفاقی کل مدل ذهنی Git را ساخت. بخش بعد دقیقاً همین است.
The third requirement matters most, because something designed for security accidentally produced Git's entire mental model. That is exactly what the next section is about.
Git واقعاً چه چیزی ذخیره میکندWhat Git actually stores
این مهمترین بخش کل مسیر است. اگر فقط یک بخش از این فصل را بخوانی، همین باشد.
This is the most important section in the whole track. If you read only one section, read this one.
تصور رایج این است که Git «تغییرات» را ذخیره میکند — یعنی برای هر
commit مینویسد «در خط ۴۲ این کلمه به آن کلمه تبدیل شد».
این تصور غلط است، و تقریباً همهٔ سردرگمیهای بعدی از همین میآید.
The common assumption is that Git stores “changes” — that each commit records “on
line 42, this word became that word”. That assumption is wrong, and nearly all
later confusion follows from it.
Git یک پایگاهدادهٔ کلید-مقدار است. تو محتوایی به آن میدهی، و آن یک کلید به تو پس میدهد. کلید، خلاصهٔ رمزنگاریشدهٔ خودِ محتوا است. بیایید ثابتش کنیم — این را روی سیستم خودت اجرا کن:
Git is a key-value store. You hand it content and it hands you back a key. That key is a cryptographic digest of the content itself. Let us prove it — run this on your own machine:
# محتوای «hello» را به Git بده و بپرس کلیدش چه میشود. # --stdin یعنی محتوا را از ورودی میگیرم، نه از یک فایل. printf 'hello\n' | git hash-object --stdin ce013625030ba8dba906f756967f9e9ca394464a
برخلاف بقیهٔ خروجیهای این مرجع که روی سیستم تو فرق میکنند، این یکی نباید فرق کند.
روی هر ماشینی، هر سیستمعاملی، هر سالی — محتوای hello همیشه همین کلید
را میدهد. اگر عدد دیگری گرفتی، یعنی محتوایی که فرستادی دقیقاً hello و
یک خط جدید نبوده.
Unlike every other output in this reference, which varies on your machine, this one must not.
On any machine, any operating system, any year — the content hello always yields
this key. If you got a different number, what you sent was not exactly hello plus
a newline.
این خاصیت اسم دارد: آدرسدهی بر اساس محتوا
(content-addressable storage). و سه نتیجهٔ فوری دارد که ارزش دارد
رویشان مکث کنی:
This property has a name: content-addressable storage. It has three immediate consequences worth pausing on:
- محتوای یکسان، کلید یکسان. اگر همان فایل در ده پوشه تکرار شده باشد، Git فقط یک بار ذخیرهاش میکند.
- Identical content, identical key. If the same file appears in ten folders, Git stores it once.
- خرابی قابل تشخیص است. اگر یک بایت از یک فایل ذخیرهشده خراب شود، کلیدش دیگر با محتوایش نمیخواند و Git فوراً میفهمد.
- Corruption is detectable. If one byte of a stored file rots, its key no longer matches its content and Git knows immediately.
- تاریخچه غیرقابل جعل است. چون کلید هر
commitشامل کلیدcommitقبلی است، عوض کردن یکcommitقدیمی یعنی عوض شدن کلید همهٔcommitهای بعدی. این دقیقاً همان چیزی است که در فصل ۷ به آن میگوییم «قانون طلاییrebase». - History cannot be forged. Because each
commit's key includes the previouscommit's key, altering an old commit changes the key of every commit after it. That is precisely what chapter 7 calls “the golden rule ofrebase”.
چهار نوع شیء
The four object types
داخل این پایگاهداده فقط چهار نوع چیز وجود دارد. همهچیز — کل Git — از همین چهارتا ساخته شده.
Inside this database there are only four kinds of thing. Everything — all of Git — is built from them.
| شیءObject | چه چیزی را نگه میداردWhat it holds | معادل روزمرهEveryday analogue |
|---|---|---|
blob |
محتوای یک فایل. فقط بایتها — نه نام، نه تاریخ، نه مجوز. A file's content. Just bytes — no name, no date, no permissions. | محتوای یک برگهThe text on a page |
tree |
فهرستی از نامها و اینکه هر نام به کدام blob یا tree اشاره میکند.
A list of names and which blob or tree each points to. |
یک پوشهA folder |
commit |
یک tree ریشه + نویسنده + زمان + پیام + اشاره به commit قبلی.
A root tree + author + time + message + a pointer to the previous commit. |
یک عکس فوری، با یادداشت پشتشA snapshot with a note on the back |
tag |
یک نام ماندگار برای یک commit مشخص، با امضا و پیام.
A permanent name for one commit, with a signature and message. |
برچسب «نسخهٔ ۱٫۰»A “v1.0” label |
commit نام فایل را نمیداند؛ فقط یک tree را میشناسد. نام فایلها داخل tree است، نه داخل blob.
A commit knows no filenames; it knows one tree. The names live in the tree, not in the blob.
یک نتیجهٔ مهم از این نمودار: هر commit یک عکس کامل از کل
پروژه است، نه فهرستی از تغییرات. پس چرا مخزنها اینقدر کوچکاند؟
چون اگر فایلی بین دو commit عوض نشده باشد، محتوایش یکسان است، پس
کلیدش یکسان است، پس هر دو tree به همان یک blob اشاره
میکنند. تکراری ذخیره نمیشود. صرفهجویی خودبهخود از مدل داده میآید، نه از یک
الگوریتم فشردهسازی هوشمند.
An important consequence of that diagram: every commit is a complete snapshot of the whole project, not a list of changes. So why are repositories so small? Because if a file did not change between two commits, its content is identical, so its key is identical, so both trees point at the same single blob. Nothing is stored twice. The saving falls out of the data model rather than from a clever compression algorithm.
و diff؟ Git آن را در لحظه محاسبه میکند با مقایسهٔ دو عکس.
ذخیرهاش نمیکند. این دقیقاً برعکس چیزی است که بیشتر آدمها فکر میکنند.
And the diff? Git computes it on the spot by comparing two snapshots. It never stores it. This is precisely the opposite of what most people assume.
سه ناحیهThe three areas
حالا که میدانیم Git چه چیزی ذخیره میکند، سؤال بعدی این است: کِی ذخیرهاش میکند. جوابش سه ناحیه است — و ناحیهٔ وسط همان چیزی است که تازهکارها را گیج میکند.
Now that we know what Git stores, the next question is when it stores it. The answer is three areas — and the middle one is what confuses newcomers.
چرا ناحیهٔ وسط اصلاً وجود دارد؟
Why does the middle area exist at all?
این سؤالی است که تقریباً همه میپرسند: چرا نمیشود مستقیم commit
زد؟ چرا باید اول add کنم؟
Nearly everyone asks this: why can I not commit directly? Why must I add first?
جوابش این است: ناحیهٔ آمادهسازی به تو اجازه میدهد commit
را طراحی کنی، نه فقط ثبتش کنی. یک موقعیت واقعی: داری روی یک قابلیت کار
میکنی و وسط کار متوجه یک غلط املایی در فایلی بیربط میشوی و درستش میکنی. حالا
پوشهٔ کاریات دو تغییر نامرتبط دارد. اگر همه را در یک commit بگذاری،
تاریخچهات دروغ میگوید و بعداً اگر بخواهی فقط یکی را برگردانی، نمیتوانی.
The answer: the staging area lets you design a commit, not merely record one. A real situation: you are working on a feature, notice a typo in an unrelated file, and fix it. Your working tree now holds two unrelated changes. Put them in one commit and your history lies — and later, if you want to revert only one of them, you cannot.
با ناحیهٔ آمادهسازی، اول غلط املایی را add و commit
میکنی، بعد قابلیت را. دو commit تمیز، هرکدام با یک دلیل روشن. این
همان چیزی است که در فصل ۱۴ به آن میگوییم «commit اتمی» و در
code review تفاوتش را حس میکنی.
With staging you add and commit the typo first, then the feature. Two clean commits, each with a single clear reason. That is what chapter 14 calls an “atomic commit”, and you feel the difference during code review.
اولین مخزنYour first repository
حالا همهٔ آنچه گفتیم را با دست میسازیم و با چشم میبینیم. هیچکدام از این دستورها به چیزی روی سیستمت آسیب نمیزند.
Now we build everything we described by hand and look at it. None of these commands can harm anything on your system.
اول به Git بگو تو که هستی. این کار فقط یکبار لازم است، و دلیلش این است که هر
commit نام و ایمیل نویسنده را داخل خودش ذخیره میکند — یادت
هست که کلید commit از محتوایش ساخته میشود؟ پس نام تو هم بخشی از آن کلید است.
First tell Git who you are. This is needed only once, and the reason is that every commit stores the author's name and email inside itself — remember that a commit's key is derived from its content? So your name is part of that key.
# --global یعنی برای همهٔ مخزنهای این کاربر، نه فقط این پروژه. git config --global user.name "Your Name" git config --global user.email "you@example.com" # نام شاخهٔ پیشفرض. اگر این را نگذاری، Git هشدار میدهد. git config --global init.defaultBranch main # ببین چه چیزی تنظیم شده و از کدام فایل آمده — سوئیچ --show-origin طلاست # وقتی میخواهی بفهمی یک تنظیم عجیب از کجا آمده. git config --list --show-origin
حالا یک مخزن بساز:
Now create a repository:
mkdir git-lab cd git-lab git init Initialized empty Git repository in /home/you/git-lab/.git/
git init فقط یک کار کرد: یک پوشهٔ پنهان به نام .git
ساخت. تمام مخزن تو همان پوشه است — نه بیشتر. اگر آن را پاک کنی، پروژهات
میماند ولی تاریخچهاش کاملاً میرود. اگر کپیاش کنی، کل تاریخچه را کپی کردهای.
git init did exactly one thing: it created a hidden folder called .git.
Your entire repository is that folder — nothing more. Delete it and your project remains but its
history is gone entirely. Copy it and you have copied the whole history.
ls -1 .git HEAD config description hooks/ info/ objects/ refs/ # objects خالی است — هنوز هیچ محتوایی ذخیره نکردهایم. ls .git/objects info pack
اولین فایل، مرحلهبهمرحله
The first file, step by step
printf 'hello\n' > a.txt git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
a.txt
nothing added to commit but untracked files present (use "git add" to track)
کلمهٔ کلیدی اینجا Untracked است: Git این فایل را میبیند ولی
دنبالش نمیکند. Git هیچوقت خودسرانه چیزی را ردیابی نمیکند — تو باید صریح
بگویی. این تصمیم عمدی است، چون پوشهٔ پروژه پر از چیزهایی است که نباید
در تاریخچه باشند: خروجی بیلد، فایل موقت، رمز.
The key word is Untracked: Git can see the file but is not following it. Git never
tracks anything on its own initiative — you must say so explicitly. That is deliberate, because a
project folder is full of things that must not be in history: build output, temporary
files, secrets.
git add a.txt # حالا نگاه کن به .git/objects — چیزی اضافه شده است! find .git/objects -type f .git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
این دقیقاً همان کلیدی است که در بخش سوم با hash-object گرفتیم.
Git محتوای a.txt را به یک blob تبدیل کرد و با کلید
خودش ذخیرهاش کرد — دو حرف اول کلید نام پوشه است و بقیه نام فایل. این تقسیم
فقط برای این است که یک پوشه میلیونها فایل نداشته باشد.
This is exactly the key we got with hash-object in section three. Git turned the
content of a.txt into a blob and stored it under its own key — the
first two characters are the directory name and the rest is the filename. That split exists only
to stop one directory from holding millions of files.
توجه کن که هنوز commit نزدهایم، اما محتوا همین
الان در پایگاهداده است. git add فقط «علامتگذاری» نیست؛ واقعاً
محتوا را ذخیره میکند. این را نگه دار — در فصل ۱۰ میبینی که چرا همین باعث
میشود کارِ addشده تقریباً هیچوقت گم نشود.
Note that we have not committed yet, and the content is already in the database.
git add is not merely “marking”; it genuinely stores content. Hold on to this — in
chapter 10 you will see why it means staged work is almost never truly lost.
# نوع این شیء چیست؟ git cat-file -t ce013625030ba8dba906f756967f9e9ca394464a blob # محتوایش چیست؟ سوئیچ -p یعنی «چاپ خوانا» git cat-file -p ce013625030ba8dba906f756967f9e9ca394464a hello
حالا commit بزنیم و ببینیم چه چیز دیگری ساخته میشود:
Now let us commit and see what else gets created:
# -m یعنی پیام را همینجا میدهم، وگرنه ویرایشگر باز میشود. git commit -m "افزودن اولین فایل" [main (root-commit) 663808a] افزودن اولین فایل 1 file changed, 1 insertion(+) create mode 100644 a.txt # حالا سه شیء داریم، نه یکی find .git/objects -type f | wc -l 3
سه شیء: blobی که داشتیم، بهاضافهٔ یک tree (که
میگوید نام a.txt به آن blob اشاره میکند) و یک
commit. بیایید زنجیره را از بالا دنبال کنیم:
Three objects: the blob we had, plus a tree (recording that the name a.txt points at
that blob) and a commit. Let us follow the chain from the top:
# HEAD یعنی «آخرین commit شاخهٔ فعلی» git cat-file -p HEAD tree 2e81171448eb9f2ee3821e3d447aa6b2fe3ddba1 author Your Name <you@example.com> 1757376000 +0330 committer Your Name <you@example.com> 1757376000 +0330 افزودن اولین فایل # نحو ^{tree} یعنی «آن tree که این commit به آن اشاره میکند» git cat-file -p HEAD^{tree} 100644 blob ce013625030ba8dba906f756967f9e9ca394464a a.txt
این خط آخر، کل مدل داده را در یک خط نشان میدهد. از راست به چپ بخوانش:
نام a.txt، که یک blob است، با کلید
ce0136… — همان کلیدی که ده دقیقه پیش، قبل از ساختن هیچ مخزنی،
با hash-object حسابش کردیم. عدد 100644 هم مجوز فایل
است (فایل معمولی، غیراجرایی).
That last line shows the entire data model in one line. Read it: the name a.txt,
which is a blob, with key ce0136… — the same key we computed ten minutes
ago with hash-object, before any repository existed. The 100644 is the
file mode (an ordinary, non-executable file).
خواندن تاریخچهReading history
تاریخچهای که نتوانی بخوانی، فایدهای ندارد. سه دستور برای نود درصد کارها کافی است.
A history you cannot read is worthless. Three commands cover ninety percent of the work.
اول یک commit دوم بسازیم تا چیزی برای مقایسه داشته باشیم:
First let us make a second commit so we have something to compare:
printf 'سلام\n' > b.txt git add b.txt git commit -m "افزودن نسخهٔ فارسی"
# --oneline: یک خط برای هر commit. برای مرور سریع بهترین است. # --graph: شکل شاخهها را با خط میکشد — از فصل ۵ به بعد حیاتی میشود. git log --oneline --graph * 4f1c2d8 افزودن نسخهٔ فارسی * 663808a افزودن اولین فایل # قالب دلخواه: چه کسی، کِی، چه چیزی. %h کلید کوتاه، %an نویسنده، # %ar زمان نسبی («۳ روز پیش») و %s خط اول پیام است. git log --pretty=format:'%h %an %ar %s' 4f1c2d8 Your Name 2 minutes ago افزودن نسخهٔ فارسی 663808a Your Name 5 minutes ago افزودن اولین فایل
و برای دیدن اینکه دقیقاً چه چیزی عوض شد:
And to see exactly what changed:
# تفاوت پوشهٔ کاری با ناحیهٔ آمادهسازی — یعنی «چه چیزی add نکردهام؟» git diff # تفاوت ناحیهٔ آمادهسازی با آخرین commit — یعنی «چه چیزی commit خواهد شد؟» # این را همیشه قبل از commit بزن. عادت خوبی است که جلوی خیلی اشتباهها را میگیرد. git diff --staged # یک commit مشخص چه کرد؟ git show 663808a
git diff خالی است ولی git status تغییر نشان میدهد؟
Why is git diff empty while git status shows changes?
چون git diff بدون سوئیچ، فقط پوشهٔ کاری را با ناحیهٔ آمادهسازی
مقایسه میکند. اگر همهچیز را add کردهای، این دو یکساناند و
خروجی خالی است. تغییرات تو در git diff --staged هستند. این
یکی از اولین چیزهایی است که همه را گیج میکند، و حالا میدانی چرا: چون سه
ناحیه داریم، پس سه مقایسهٔ ممکن داریم.
Because bare git diff compares only the working tree with the staging area. If you
have staged everything, those two are identical and the output is empty. Your changes are in
git diff --staged. This confuses everyone early on, and now you know why: three
areas means three possible comparisons.
چیزهایی که نباید ردیابی شوندWhat should never be tracked
یک قاعدهٔ ساده: اگر چیزی را میشود دوباره ساخت، یا اگر نباید کسی ببیندش، در Git نمیرود.
One simple rule: if it can be regenerated, or if nobody should see it, it does not go into Git.
# وابستگیها — با یک دستور دوباره نصب میشوند، پس ذخیرهشان اتلاف است node_modules/ venv/ # خروجی بیلد — از روی کد ساخته میشود، پس منبعش کافی است dist/ build/ *.o # رمز و کلید — هرگز. حتی یک بار. بخش «دامها» را ببین. .env *.pem # فایلهای سیستمعامل و ویرایشگر .DS_Store Thumbs.db .vscode/ # استثنا: با ! میشود یک فایل را از قاعدهٔ بالا مستثنی کرد # اینجا: کل .vscode نادیده گرفته شود جز فایل تنظیمات مشترک تیم !.vscode/settings.json
نکتهٔ مهمی که خیلیها دیر میفهمند: .gitignore فقط روی
فایلهای ردیابینشده اثر دارد. اگر فایلی را یکبار
commit کرده باشی، اضافه کردنش به .gitignore هیچ کاری
نمیکند — Git همچنان دنبالش میکند. دلیلش منطقی است: Git فرض میکند اگر تو
عمداً چیزی را ردیابی کردهای، نمیخواهی بیسروصدا رهایش کند.
A point many people learn late: .gitignore only affects untracked
files. If you have already committed a file, adding it to .gitignore does
nothing — Git keeps following it. The reasoning is sound: Git assumes that if you deliberately
tracked something, you do not want it silently abandoned.
# رها کردن یک فایل از ردیابی، بدون حذفش از دیسک. # --cached یعنی «فقط از index حذف کن، به پوشهٔ کاری دست نزن». # بدون --cached فایل واقعاً پاک میشود. git rm --cached .env git commit -m "حذف .env از ردیابی" # چرا این فایل نادیده گرفته شد؟ کدام قاعده باعثش شد؟ # وقتی .gitignore بزرگ میشود، این دستور وقت زیادی از تو میخرد. git check-ignore -v dist/app.js .gitignore:6:dist/ dist/app.js
کجا خراب میشودWhere it breaks
شش خطایی که تقریباً همه در هفتهٔ اول میبینند. پیام خطا را دقیق آوردهایم تا وقتی همان را دیدی، بشناسیاش.
Six errors nearly everyone meets in their first week. The exact messages are here so you recognise them when they appear.
Author identity unknown
*** Please tell me who you are.
fatal: unable to auto-detect email address (got 'you@laptop.(none)')
- علت
- Cause
- نام و ایمیل تنظیم نشده. Git نمیتواند
commitبسازد، چون نویسنده بخشی از محتوایcommitاست — و بدون محتوای کامل، کلیدی هم وجود ندارد. - Name and email are not configured. Git cannot build a commit, because the author is part of the commit's content — and without complete content there is no key.
- درمان
- Fix
- دو دستور
git config --globalبخش پنجم. اگر برای یک پروژهٔ خاص ایمیل دیگری میخواهی، همانها را بدون--globalداخل آن مخزن بزن. - The two
git config --globalcommands from section five. For a different email on one project, run them without--globalinside that repository.
warning: in the working copy of 'a.txt', LF will be replaced by CRLF the next time Git touches it
- علت
- Cause
- ویندوز پایان خط را
CRLFمینویسد و لینوکس و مکLF. Git میخواهد کمک کند و خودکار تبدیل میکند. مشکل وقتی جدی میشود که نصف تیم ویندوز باشند: هر بار کل فایل «تغییرکرده» به نظر میرسد وdiffها بیمعنی میشوند. - Windows writes line endings as
CRLF, Linux and macOS asLF. Git tries to help by converting automatically. It becomes serious when half the team is on Windows: whole files appear “changed” every time and diffs turn to noise. - درمان
- Fix
- یک فایل
.gitattributesبا خط* text=auto eol=lfبساز و در مخزنcommitکن. این تصمیم را از تنظیم شخصی هر نفر میگیرد و داخل خود پروژه میگذارد — یعنی برای همه یکسان میشود. - Create a
.gitattributescontaining* text=auto eol=lfand commit it. That moves the decision out of each person's personal config and into the project, so it is the same for everyone.
fatal: not a git repository (or any of the parent directories): .git
- علت
- Cause
- در پوشهای هستی که مخزن نیست. Git از پوشهٔ فعلی تا ریشهٔ دیسک بالا
میرود و دنبال
.gitمیگردد؛ پیدا نکرده. معمولاً یعنیcdرا فراموش کردهای، یاgit initرا در جای اشتباه زدهای. - You are in a folder that is not a repository. Git walks up from the current
directory to the filesystem root looking for
.gitand found none. Usually you forgot tocd, or rangit initin the wrong place. - درمان
- Fix
git rev-parse --show-toplevelریشهٔ مخزن فعلی را میگوید. اگر خطا داد، واقعاً بیرون از مخزنی.git rev-parse --show-toplevelprints the current repository's root. If that errors, you really are outside one.
nothing to commit, working tree clean
- علت
- Cause
- این خطا نیست، ولی وقتی انتظارش را نداری گیجکننده است. یعنی هیچ
تفاوتی بین سه ناحیه نیست. اگر مطمئنی فایلی را عوض کردهای، احتمالاً در پوشهٔ
اشتباهی ذخیرهاش کردهای، یا فایل در
.gitignoreاست. - Not an error, but confusing when unexpected. It means there is no difference
between the three areas. If you are sure you edited something, you probably saved it in the
wrong folder, or the file is in
.gitignore. - درمان
- Fix
git status --ignoredفایلهای نادیدهگرفتهشده را هم نشان میدهد. اگر فایلت آنجاست، جوابت را گرفتی.git status --ignoredalso lists ignored files. If yours is there, you have your answer.
hint: Waiting for your editor to close the file...
- علت
- Cause
git commitرا بدون-mزدهای، پس Git ویرایشگر را باز کرده تا پیام بنویسی. اگر ویرایشگر پیشفرضvimباشد و بلد نباشی، عملاً گیر افتادهای.- You ran
git commitwithout-m, so Git opened an editor for the message. If the default editor isvimand you do not know it, you are stuck. - درمان
- Fix
- برای خروج از
vim: Esc بعد:wqو Enter. برای اینکه دیگر تکرار نشود، ویرایشگر دلخواهت را تنظیم کن:git config --global core.editor "code --wait" - To leave
vim: Esc, then:wq, then Enter. To stop it recurring, set your editor:git config --global core.editor "code --wait"
git add . → و حالا .env در تاریخچه است
- علت
- Cause
- پرخطرترین اشتباه این فصل.
git add .همهچیز را اضافه میکند، از جمله فایل رمز که هنوز در.gitignoreنگذاشته بودی. و چون تاریخچه بر اساس کلیدهای زنجیرهای است، فایل با یکcommitبعدی پاک نمیشود — فقط دیگر در آخرین نسخه نیست، ولی در تاریخچه هست و هر کسی میتواند بیرونش بکشد. - The most dangerous mistake in this chapter.
git add .adds everything, including the secrets file you had not yet put in.gitignore. And because history is a chain of keys, a later commit does not erase it — it is merely absent from the newest version while remaining in history for anyone to pull out. - درمان
- Fix
- اگر هنوز
pushنکردهای، فصل ۹ راه برگرداندن را میگوید. اگرpushکردهای: آن رمز را سوخته فرض کن و همین حالا عوضش کن. پاک کردن از تاریخچه ممکن است ولی سخت است و تضمینی نیست کسی کپی نگرفته باشد. عادت درست: اول.gitignoreبساز، بعدgit add. و بهجایgit add .ازgit add -pاستفاده کن که تکهبهتکه نشانت میدهد. - If you have not pushed, chapter 9 shows how to undo it. If you have pushed:
treat that secret as compromised and rotate it now. Removing it from history
is possible but hard, and never proves nobody took a copy. The right habit: write
.gitignorefirst, then add. And prefergit add -p, which shows you each hunk, overgit add .
مرجع سریع این فصلQuick reference
git config --global user.name "…" |
نامی که داخل هر commit ثبت میشود
The name recorded inside every commit |
git config --list --show-origin |
همهٔ تنظیمات، و اینکه هرکدام از کدام فایل آمده Every setting, and which file it came from |
git init |
ساختن پوشهٔ .git — یعنی ساختن مخزن
Create the .git folder — that is the repository |
git status |
تفاوت سه ناحیه. قبل از هر کاری این را بزن. The differences between the three areas. Run it before anything. |
git status --ignored |
بهعلاوهٔ فایلهایی که نادیده گرفته شدهاند Plus the files being ignored |
git add FILE |
محتوا را ذخیره کن و در پیشنویس commit بعدی بگذار
Store the content and put it in the next commit's draft |
git add -p |
تکهبهتکه بپرس چه چیزی اضافه شود — جایگزین امن add .
Ask hunk by hunk what to add — the safe alternative to add . |
git commit -m "…" |
ساختن یک commit از پیشنویس فعلی
Build a commit from the current draft |
git diff |
پوشهٔ کاری در برابر پیشنویس — «چه چیزی add نکردهام؟»
Working tree versus draft — “what have I not staged?” |
git diff --staged |
پیشنویس در برابر آخرین commit — «چه چیزی ثبت میشود؟»
Draft versus last commit — “what is about to be recorded?” |
git log --oneline --graph |
تاریخچهٔ فشرده، با شکل شاخهها Compact history, with branch shape |
git show SHA |
یک commit مشخص چه کرد
What one specific commit did |
git hash-object --stdin |
کلید یک محتوا را حساب کن، بدون ذخیرهکردنش Compute a content's key without storing it |
git cat-file -t SHA |
نوع یک شیء: blob، tree، commit
An object's type: blob, tree or commit |
git cat-file -p SHA |
محتوای یک شیء، خوانا An object's content, human-readable |
git rm --cached FILE |
رها کردن از ردیابی، بدون حذف از دیسک Stop tracking without deleting from disk |
git check-ignore -v FILE |
کدام قاعده باعث نادیدهگرفتن این فایل شد Which rule caused this file to be ignored |
git rev-parse --show-toplevel |
ریشهٔ مخزن فعلی کجاست Where the current repository's root is |
تمرینهاExercises
دوازده تمرین، از ساده به سخت. همه پاسخ کامل دارند — اما اول خودت انجامش بده. برای همهشان یک پوشهٔ خالی کافی است؛ هیچکدام به اینترنت نیاز ندارند.
Twelve exercises, easy to hard. All have full solutions — but do them yourself first. An empty folder is all you need; none of them require the internet.
Git را تنظیم کن و ثابت کن که تنظیماتت اعمال شده. بعد بگو تنظیمات در کدام فایل ذخیره شد.
Configure Git and prove your settings took effect. Then say which file they were written to.
معیار موفقیت: git config user.name نام تو را چاپ کند.
Success criterion: git config user.name prints your name.
راهنماییHint
سوئیچ --show-origin کنار --list مسیر فایل هر تنظیم را میگوید.
--show-origin alongside --list reveals each setting's file.
پاسخ و توضیحSolution
git config --global user.name "Your Name" git config --global user.email "you@example.com" git config user.name Your Name git config --list --show-origin | grep user.name file:/home/you/.gitconfig user.name=Your Name
چرا اینطور: --global در فایل ~/.gitconfig
مینویسد، یعنی برای همهٔ مخزنهای این کاربر. بدون --global در
.git/config همان مخزن مینویسد. سومی هم هست: --system برای کل ماشین.
Git این سه را به همین ترتیب میخواند و نزدیکترین برنده است — یعنی تنظیم مخزن
بر تنظیم کاربر میچربد. برای همین میتوانی برای پروژهٔ کاری ایمیل شرکتی و برای بقیه ایمیل شخصی بگذاری.
Why this way: --global writes to ~/.gitconfig,
applying to all your repositories. Without it, Git writes to that repository's
.git/config. There is a third, --system, for the whole machine. Git reads
all three in that order and the nearest one wins — repository beats user. That is
how you use a work email on one project and a personal one everywhere else.
یک مخزن بساز و ثابت کن که خالی است — یعنی نشان بده هیچ شیئی در پایگاهدادهاش نیست.
Create a repository and prove it is empty — show that its object database holds nothing.
معیار موفقیت: شمارش فایلهای .git/objects عدد صفر بدهد.
Success criterion: counting files under .git/objects gives zero.
راهنماییHint
find با -type f فقط فایلها را میشمارد، نه پوشهها.
دو پوشهٔ info و pack از اول هستند ولی خالیاند.
find with -type f counts only files, not directories. The
info and pack folders exist from the start but are empty.
پاسخ و توضیحSolution
mkdir ex-1-2 && cd ex-1-2 git init find .git/objects -type f | wc -l 0
چرا اینطور: git init فقط اسکلت را میسازد؛ هیچ محتوایی
ذخیره نمیکند چون هنوز چیزی به آن ندادهای. این تأیید میکند که مخزن یک پایگاهداده است
که تو پُرش میکنی، نه چیزی که خودکار پُر شود. توجه کن که ls ساده جواب نمیدهد،
چون دو پوشهٔ خالی میبینی و ممکن است فکر کنی چیزی هست.
Why this way: git init only creates the skeleton; it stores
no content because you have given it none. This confirms the repository is a database you
fill, not something that fills itself. Note that a plain ls misleads here, since you
see two empty folders and might think something is there.
یک فایل بساز و آن را از Untracked تا commitشده ببر. در هر
مرحله git status بزن و بگو چه چیزی عوض شد.
Create a file and take it from Untracked to committed. Run
git status at each step and say what changed.
معیار موفقیت: در پایان git log --oneline یک commit نشان دهد.
Success criterion: at the end, git log --oneline shows one commit.
راهنماییHint
سه حالت ممکن است: Untracked، Changes to be committed،
و working tree clean. هر git add یا git commit یکی جلو میبردت.
Three states: Untracked, Changes to be committed, and
working tree clean. Each add or commit moves you one step.
پاسخ و توضیحSolution
printf 'first line\n' > notes.txt git status --short ?? notes.txt ← ?? یعنی Untracked git add notes.txt git status --short A notes.txt ← A یعنی Added، در ستون اول = ناحیهٔ آمادهسازی git commit -m "افزودن یادداشت" git status --short ← خالی یعنی هر سه ناحیه یکساناند git log --oneline a3f9c21 افزودن یادداشت
چرا --short: خروجی کوتاه دو ستون دارد. ستون اول وضعیت
در ناحیهٔ آمادهسازی است و ستون دوم وضعیت در پوشهٔ کاری. وقتی این را بفهمی،
با یک نگاه میفهمی هر فایل کجای آن سهتایی است — و همین است که تمرین ۱٫۶ رویش سوار میشود.
Why --short: the short output has two columns. The first is
the file's state in the staging area, the second in the working tree. Once that
clicks, one glance tells you where each file sits among the three areas — which is exactly what
exercise 1.6 builds on.
بدون ساختن هیچ مخزنی، کلید محتوای hello را حساب کن. بعد همین کار را
در پوشهای دیگر تکرار کن و نتیجه را مقایسه کن.
Without creating any repository, compute the key for the content hello.
Then repeat it elsewhere and compare.
معیار موفقیت: هر دو بار دقیقاً ce013625… بگیری.
Success criterion: both times you get exactly ce013625….
راهنماییHint
مراقب پایان خط باش. echo در بعضی شلها رفتار متفاوتی دارد؛
printf قابل پیشبینیتر است.
Mind the trailing newline. echo behaves differently across shells;
printf is predictable.
پاسخ و توضیحSolution
printf 'hello\n' | git hash-object --stdin ce013625030ba8dba906f756967f9e9ca394464a # بدون خط جدید، کلید کاملاً فرق میکند: printf 'hello' | git hash-object --stdin b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0
چرا این مهم است: این نشان میدهد کلید فقط به محتوا بستگی دارد — نه به نام فایل، نه به زمان، نه به اینکه در کدام مخزنی. یک بایت تفاوت (همان خط جدید) کل کلید را عوض میکند، و این خاصیت عمدی است: اگر تغییر کوچک کلید را کمی عوض میکرد، تشخیص دستکاری ممکن نبود.
همین خاصیت است که باعث میشود دو نفر در دو نقطهٔ دنیا، بدون هیچ هماهنگی، برای یک فایل یکسان کلید یکسان بگیرند — و Git بفهمد که این دو در واقع یک چیزند.
Why this matters: it shows the key depends on content alone — not the filename, not the time, not which repository. One byte of difference (that newline) changes the whole key, and that is deliberate: if a small change produced a slightly different key, tampering would be undetectable.
This is also what lets two people on opposite sides of the world, with no coordination, derive the same key for the same file — and lets Git recognise that they are the same thing.
ثابت کن که git add واقعاً محتوا را ذخیره میکند و فقط علامتگذاری نیست.
یعنی نشان بده قبل از commit، شیء در پایگاهداده هست.
Prove that git add genuinely stores content rather than merely marking it —
show the object exists in the database before any commit.
معیار موفقیت: بعد از add و قبل از commit،
git cat-file -p محتوای فایل را چاپ کند.
Success criterion: after add and before commit, git cat-file -p
prints the file's content.
راهنماییHint
کلید را از نام فایل داخل .git/objects بساز: دو حرف پوشه + بقیه.
یا سادهتر، دوباره hash-object بزن.
Rebuild the key from the path inside .git/objects: two-character folder plus
the rest. Or more simply, run hash-object again.
پاسخ و توضیحSolution
mkdir ex-1-5 && cd ex-1-5 && git init -q printf 'hello\n' > a.txt find .git/objects -type f | wc -l 0 ← هنوز هیچ چیزی ذخیره نشده git add a.txt find .git/objects -type f .git/objects/ce/013625030ba8dba906f756967f9e9ca394464a # بدون هیچ commit ای، محتوا قابل بازیابی است: git cat-file -p ce013625030ba8dba906f756967f9e9ca394464a hello git log fatal: your current branch 'main' does not have any commits yet
چه دیدیم: محتوا در پایگاهداده هست، ولی هیچ commitی
وجود ندارد. یعنی add و commit دو کار کاملاً جدا هستند:
add محتوا را ذخیره میکند، commit فقط یک نام و زمان و پیام
رویش میگذارد و به تاریخچه وصلش میکند.
نتیجهٔ عملی: اگر کاری را add کرده باشی و بعد اشتباهی
از دستش بدهی، محتوا هنوز روی دیسک است و قابل بازیابی است — حتی اگر Git راه مستقیمی نشانت ندهد.
فصل ۱۰ (reflog و اشیای سرگردان) دقیقاً همین را باز میکند.
What we saw: the content is in the database while no commit exists. So add and commit are entirely separate acts: add stores content; commit merely attaches a name, time and message to it and links it into history.
The practical upshot: if you staged work and then lost it by accident, the content is still on disk and recoverable — even when Git offers no obvious route to it. Chapter 10 (reflog and dangling objects) opens exactly this up.
یک فایل را add کن، بعد دوباره ویرایشش کن. حالا
git status همان فایل را در دو حالت مختلف نشان میدهد. توضیح بده چرا، و بگو
اگر همین حالا commit بزنی کدام نسخه ثبت میشود.
Stage a file, then edit it again. Now git status shows the same file
in two different states. Explain why, and say which version gets recorded if you commit now.
معیار موفقیت: بتوانی قبل از اجرا پیشبینی کنی که
git show HEAD:f.txt کدام متن را نشان میدهد.
Success criterion: you can predict, before running it, which text
git show HEAD:f.txt will contain.
راهنماییHint
به دو ستون git status --short نگاه کن. و یادت باشد
commit از ناحیهٔ آمادهسازی ساخته میشود، نه از پوشهٔ کاری.
Look at the two columns of git status --short. And remember a commit is
built from the staging area, not the working tree.
پاسخ و توضیحSolution
printf 'version A\n' > f.txt git add f.txt printf 'version B\n' > f.txt git status --short AM f.txt ↑↑ │└─ ستون دوم: M یعنی در پوشهٔ کاری تغییر کرده (version B) └── ستون اول: A یعنی در ناحیهٔ آمادهسازی اضافه شده (version A) git diff --staged +version A ← این چیزی است که commit میشود git diff -version A +version B ← این چیزی است که جا میماند git commit -m "test" git show HEAD:f.txt version A
چرا اینطور: در لحظهٔ git add، Git یک عکس از محتوای
همان لحظه گرفت و ذخیره کرد. ویرایش بعدی روی فایل روی دیسک اثر گذاشت، نه روی آن عکس.
commit از عکس ساخته میشود، پس version A ثبت میشود و
version B هنوز فقط در پوشهٔ کاری است.
چرا این مهم است: این پرتکرارترین سوءتفاهم Git است. آدمها فایل را
add میکنند، بعد یک اصلاح کوچک میزنند، commit میکنند، و بعد
تعجب میکنند که چرا اصلاحشان نیست. قاعدهٔ ساده: بعد از هر ویرایش، دوباره
add کن — یا از git commit -a استفاده کن که فایلهای
ردیابیشده را خودکار اضافه میکند (ولی فایلهای تازه را نه).
Why this way: at the moment of git add, Git took a snapshot
of the content at that instant and stored it. The later edit changed the file on disk, not
that snapshot. The commit is built from the snapshot, so version A is recorded while
version B remains only in the working tree.
Why it matters: this is Git's single most common misunderstanding. People
stage a file, make one more small fix, commit, then wonder where the fix went. Simple rule:
re-add after every edit — or use git commit -a, which stages tracked
files automatically (but not new ones).
دو فایل با محتوای کاملاً یکسان ولی نام متفاوت بساز و هر دو را
commit کن. حالا بشمار چند blob ساخته شد و توضیح بده چرا.
Create two files with identical content but different names and commit both. Count how many blobs were created, and explain why.
معیار موفقیت: بتوانی قبل از شمردن، عدد را پیشبینی کنی.
Success criterion: you can predict the number before counting.
راهنماییHint
کلید از محتوا ساخته میشود. نام فایل کجا ذخیره میشود — در blob یا در tree؟
The key comes from content. Where is the filename stored — in the blob or in the tree?
پاسخ و توضیحSolution
mkdir ex-1-7 && cd ex-1-7 && git init -q printf 'same\n' > one.txt printf 'same\n' > two.txt git add . && git commit -q -m "دو فایل، یک محتوا" # همهٔ اشیا را با نوعشان فهرست کن git cat-file --batch-check --batch-all-objects 1275430f… blob 5 8a9d2e4… tree 74 c5e1f30… commit 178
جواب: یک blob، نه دو. چون کلید از محتوا میآید و محتوا
یکسان است، هر دو فایل به همان یک شیء اشاره میکنند. نامها در tree ذخیره شدهاند:
git cat-file -p HEAD^{tree} 100644 blob 1275430f… one.txt 100644 blob 1275430f… two.txt ↑ همان کلید، دو بار
چرا این مهم است: این همان مکانیزمی است که مخزنها را کوچک نگه میدارد.
در پروژهای که صد commit دارد و در هرکدام فقط یک فایل عوض شده، Git صد نسخه از بقیهٔ
فایلها ذخیره نمیکند — همه به همان blobهای قبلی اشاره میکنند. صرفهجویی از
مدل داده میآید، نه از یک الگوریتم فشردهسازی.
The answer: one blob, not two. Since the key derives from content and the content is identical, both files point at the same object. The names live in the tree.
Why it matters: this is what keeps repositories small. In a project with a hundred commits where each changed one file, Git does not store a hundred copies of everything else — they all point at the same earlier blobs. The saving comes from the data model, not from a compression algorithm.
یک .gitignore بنویس که secret.env را نادیده بگیرد. بعد
ثابت کن که کار میکند، و بگو کدام خط از .gitignore مسئولش بود.
Write a .gitignore that ignores secret.env. Prove it works, and
identify which line of the file was responsible.
معیار موفقیت: git status فایل را نشان ندهد، و
git check-ignore -v شمارهٔ خط قاعده را بگوید.
Success criterion: git status does not list it, and
git check-ignore -v reports the rule's line number.
راهنماییHint
چند قاعدهٔ مختلف بنویس تا مشخص شود کدامیک برنده میشود. قاعدهٔ آخر که تطبیق کند، برنده است.
Write several rules so it is clear which wins. The last matching rule wins.
پاسخ و توضیحSolution
mkdir ex-1-8 && cd ex-1-8 && git init -q printf 'KEY=123\n' > secret.env printf '*.log\n*.env\n!example.env\n' > .gitignore git status --short ?? .gitignore ← secret.env نیست، یعنی نادیده گرفته شد git check-ignore -v secret.env .gitignore:2:*.env secret.env ↑ خط ۲، قاعدهٔ *.env
چرا !example.env اثری نداشت: چون فایل ما
secret.env است نه example.env. علامت ! یک استثنا میسازد —
«این را نادیده نگیر» — و برای وقتی است که یک قاعدهٔ کلی نوشتهای ولی میخواهی یک فایل خاص
همچنان ردیابی شود، مثل یک نمونهٔ خالی برای بقیهٔ تیم.
چرا check-ignore ارزش دارد: در پروژهٔ واقعی
.gitignore ممکن است چند فایل باشد (ریشه، زیرپوشه، و ~/.gitignore_global).
وقتی فایلی بیدلیل ناپدید میشود، این دستور در یک ثانیه میگوید کدام قاعده در کدام فایل مقصر است —
بهجای اینکه دستی همه را بگردی.
Why !example.env had no effect: our file is
secret.env, not example.env. The ! creates an exception —
“do not ignore this one” — for when a broad rule is right but one specific file should still be
tracked, such as a blank template for the rest of the team.
Why check-ignore earns its place: in a real project
.gitignore may be several files (root, subdirectory, and
~/.gitignore_global). When a file vanishes inexplicably, this command names the guilty
rule and file in one second instead of a manual hunt.
فایلی را که قبلاً commit کردهای به .gitignore اضافه کن و
ببین اتفاقی نمیافتد. بعد درستش کن — بدون اینکه فایل از دیسک پاک شود.
Add an already-committed file to .gitignore and observe that nothing happens.
Then fix it — without deleting the file from disk.
معیار موفقیت: فایل روی دیسک باقی بماند ولی Git دیگر ردیابیاش نکند.
Success criterion: the file stays on disk but Git no longer tracks it.
راهنماییHint
.gitignore فقط روی فایلهای ردیابینشده اثر دارد. باید اول فایل را از
index بیرون بیاوری. سوئیچی لازم داری که به پوشهٔ کاری دست نزند.
.gitignore affects only untracked files. You must first remove it from the
index — with a flag that leaves the working tree alone.
پاسخ و توضیحSolution
mkdir ex-1-9 && cd ex-1-9 && git init -q printf 'KEY=123\n' > config.env git add config.env && git commit -q -m "اشتباه: افزودن config" # حالا سعی میکنیم نادیدهاش بگیریم printf '*.env\n' > .gitignore printf 'KEY=456\n' > config.env git status --short M config.env ← هنوز ردیابی میشود! .gitignore بیاثر بود # درمان: از index بیرونش بیاور، ولی روی دیسک نگهش دار git rm --cached config.env rm 'config.env' git commit -q -m "حذف config.env از ردیابی" git status --short ← تمیز است، چون حالا .gitignore کار میکند ls config.env config.env ← فایل هنوز روی دیسک است
چرا .gitignore بیاثر بود: این فایل به Git میگوید
«فایلهای ردیابینشده را نادیده بگیر». فایلی که یکبار commit شده، ردیابیشده است،
پس اصلاً مشمول قاعده نمیشود. این طراحی عمدی است: اگر .gitignore میتوانست
فایلهای ردیابیشده را بیسروصدا رها کند، یک نفر میتوانست با یک تغییر کوچک، فایلی مهم را
از تاریخچهٔ همه حذف کند بدون اینکه کسی متوجه شود.
هشدار مهم: این کار فایل را از آینده حذف میکند، نه از گذشته.
KEY=123 هنوز در آن commit اول هست و هر کسی میتواند با
git show ببیندش. اگر رمز واقعی بوده، باید عوضش کنی.
Why .gitignore did nothing: it tells Git to ignore
untracked files. A file that has been committed is tracked, so the rule never applies. This
is deliberate: if .gitignore could silently drop tracked files, one person could remove
an important file from everyone's history with a small edit and nobody would notice.
An important warning: this removes the file from the future, not
the past. KEY=123 is still in that first commit and anyone can see it with
git show. If it was a real secret, rotate it.
کل زنجیرهٔ commit ← tree ← blob را با دست
دنبال کن. از HEAD شروع کن و تا محتوای یک فایل برو، بدون اینکه اسم فایل را
مستقیم به Git بدهی.
Walk the whole commit → tree → blob chain by hand. Start at HEAD and reach a
file's content without giving Git the filename directly.
معیار موفقیت: محتوای فایل را فقط با cat-file و کلیدها چاپ کنی.
Success criterion: print the file's content using only
cat-file and keys.
راهنماییHint
هر خروجی cat-file -p کلید مرحلهٔ بعد را در خودش دارد. سه قدم لازم است.
Each cat-file -p output contains the key for the next step. Three hops.
پاسخ و توضیحSolution
mkdir ex-1-10 && cd ex-1-10 && git init -q mkdir src printf 'hello\n' > src/app.txt git add . && git commit -q -m "شروع" # قدم ۱ — commit به ما کلید tree ریشه را میدهد git cat-file -p HEAD tree 5a1b8c9… author … # قدم ۲ — tree ریشه: یک زیرپوشه دارد git cat-file -p 5a1b8c9… 040000 tree 7d2e4f1… src ↑ نوع tree یعنی این یک پوشه است، نه فایل # قدم ۳ — tree زیرپوشه: بالاخره یک blob git cat-file -p 7d2e4f1… 100644 blob ce013625… app.txt # قدم ۴ — محتوا git cat-file -p ce013625030ba8dba906f756967f9e9ca394464a hello
چه یاد گرفتیم: پوشهها در Git treeهای تودرتو هستند.
عدد 040000 یعنی «این یک پوشه است» و 100644 یعنی «فایل معمولی».
(اگر فایل اجرایی باشد، 100755 میشود — و همین است که مجوز اجرا را در Git حفظ میکند.)
چرا این تمرین ارزش دارد: از این به بعد وقتی میشنوی
«HEAD به یک commit اشاره میکند» یا «branch فقط یک
اشارهگر است»، این جملهها انتزاعی نیستند — تو زنجیره را با دست دنبال کردهای و میدانی هر
اشاره واقعاً یعنی چه. فصلهای ۵ تا ۱۰ همه روی همین یک تصویر ساخته میشوند.
What we learned: folders in Git are nested trees. The 040000
mode means “this is a directory” and 100644 means “ordinary file”. (An executable is
100755 — which is how Git preserves the execute bit.)
Why this exercise earns its time: from here on, when you hear “HEAD points at a commit” or “a branch is just a pointer”, those are not abstractions — you have walked the chain by hand and know what each pointer really is. Chapters 5 through 10 all build on this one picture.
ثابت کن که تاریخچهٔ Git قابل جعل نیست: نشان بده عوض کردن یک commit
قدیمی، کلید همهٔ commitهای بعدی را هم عوض میکند.
Prove Git history cannot be forged: show that altering an old commit also changes the key of every commit after it.
معیار موفقیت: دو فهرست کلید داشته باشی که هیچ
کلید مشترکی ندارند، با آنکه دو commit آخر محتوای یکسانی دارند.
Success criterion: two key lists sharing no keys, even though the last two commits have identical content.
راهنماییHint
زمان بخشی از محتوای commit است، پس اگر آن را ثابت نکنی هر بار
کلیدها فرق میکنند و آزمایش بیمعنا میشود. متغیرهای
GIT_AUTHOR_DATE و GIT_COMMITTER_DATE این را کنترل میکنند.
Time is part of a commit's content, so unless you fix it the keys differ every run and the
experiment proves nothing. GIT_AUTHOR_DATE and GIT_COMMITTER_DATE control it.
پاسخ و توضیحSolution
# زمان را ثابت میکنیم. این خودش نکته است: زمان بخشی از محتوای commit است، # پس بدون ثابت کردنش نمیشود دو تاریخچه را منصفانه مقایسه کرد. export GIT_AUTHOR_DATE="2020-01-01T00:00:00" export GIT_COMMITTER_DATE="2020-01-01T00:00:00" # تاریخچهٔ اصلی mkdir orig && cd orig && git init -q printf 'A\n' > f.txt && git add . && git commit -q -m "c1" printf 'B\n' > g.txt && git add . && git commit -q -m "c2" printf 'C\n' > h.txt && git add . && git commit -q -m "c3" git log --format=%H > ../before.txt cd ..
# همان تاریخچه، ولی فقط محتوای اولین commit عوض شده mkdir tampered && cd tampered && git init -q printf 'A-tampered\n' > f.txt && git add . && git commit -q -m "c1" printf 'B\n' > g.txt && git add . && git commit -q -m "c2" printf 'C\n' > h.txt && git add . && git commit -q -m "c3" git log --format=%H > ../after.txt cd .. # چند کلید مشترک است؟ comm -12 <(sort before.txt) <(sort after.txt) | wc -l 0 ← هیچکدام. حتی c2 و c3 که دستنخورده بودند.
چرا حتی c2 و c3 عوض شدند: چون هر
commit کلید والدش را داخل محتوای خودش دارد. وقتی کلید c1
عوض شد، محتوای c2 عوض شد (چون به کلید جدید اشاره میکند)، پس کلید
c2 هم عوض شد، و همینطور تا آخر. به این ساختار میگویند
زنجیرهٔ درهمسازی (hash chain) و پایهٔ همان چیزی است که
بعدها در بلاکچین هم استفاده شد.
نتیجهٔ عملی برای فصلهای بعد: این توضیح میدهد چرا
rebase روی شاخهای که دیگران هم دارند خطرناک است — تو داری کلیدها را عوض میکنی
و از دید آنها کل تاریخچه ناگهان بیگانه میشود. قانون طلایی فصل ۷ دقیقاً از همینجا میآید.
Why even c2 and c3 changed: each commit holds its parent's key inside its own content. When c1's key changed, c2's content changed (it references the new key), so c2's key changed, and so on to the end. This structure is a hash chain, and it is the same foundation later used in blockchains.
The practical consequence for later chapters: this is why rebasing a branch others also have is dangerous — you are changing keys, and from their side the entire history suddenly becomes unrecognisable. Chapter 7's golden rule comes directly from here.
یک commit بساز بدون استفاده از git add و
git commit — فقط با دستورهای سطحپایین. هدف این است که ببینی آن دو دستور
پرکاربرد، در واقع میانبُری روی چه چیزی هستند.
Build a commit without git add or git commit — using only
plumbing commands. The point is to see what those two everyday commands are a shortcut for.
معیار موفقیت: git log یک commit نشان دهد که
تو بدون دستورهای معمول ساختهای.
Success criterion: git log shows a commit you built without the
usual commands.
راهنماییHint
چهار دستور لازم داری: hash-object -w برای ساخت blob،
update-index برای گذاشتنش در index، write-tree برای
ساخت tree، و commit-tree برای ساخت commit.
آخرش هم باید HEAD را به آن اشاره بدهی.
Four commands: hash-object -w to create the blob, update-index to
place it in the index, write-tree to build the tree, and commit-tree to
build the commit. Finally, point HEAD at it.
پاسخ و توضیحSolution
mkdir ex-1-12 && cd ex-1-12 && git init -q # ۱) محتوا را به یک blob تبدیل کن. سوئیچ -w یعنی «بنویسش»، نه فقط حساب کن. printf 'hello\n' | git hash-object -w --stdin ce013625030ba8dba906f756967f9e9ca394464a # ۲) به index بگو این blob اسمش a.txt است. اینجاست که نام به محتوا وصل میشود. # 100644 مجوز فایل معمولی است. git update-index --add --cacheinfo 100644 \ ce013625030ba8dba906f756967f9e9ca394464a a.txt # ۳) از index یک شیء tree بساز git write-tree 2e81171448eb9f2ee3821e3d447aa6b2fe3ddba1 # ۴) از tree یک commit بساز. بدون -p یعنی این commit والد ندارد. echo "اولین commit دستی" | \ git commit-tree 2e81171448eb9f2ee3821e3d447aa6b2fe3ddba1 7b1e4a2… # ۵) شاخهٔ فعلی را به این commit اشاره بده. # HEAD مینویسیم نه refs/heads/main، چون نام شاخهٔ پیشفرض ممکن است # روی سیستم تو master باشد. این شکل، هرچه باشد درست کار میکند. git update-ref HEAD 7b1e4a2… git log --oneline 7b1e4a2 اولین commit دستی git status --short D a.txt ← حذفشده؟! این درست است — پایین توضیح داده شده # فایل را از خودِ commit بیرون بکش تا پوشهٔ کاری هم بخواند git checkout . git status --short ← حالا تمیز است cat a.txt hello
چرا git status گفت فایل حذف شده: ما هیچوقت
a.txt را روی دیسک نساختیم — محتوا را مستقیم از ورودی به پایگاهداده دادیم.
پس commit میگوید «فایلی به نام a.txt باید باشد» ولی پوشهٔ کاری
خالی است، و Git این اختلاف را بهصورت D (حذفشده) گزارش میکند.
git checkout . فایل را از همان blob بیرون میکشد و روی دیسک مینویسد.
این خودش نشان میدهد سه ناحیه واقعاً از هم مستقلاند: ما دو ناحیه را پر کردیم و سومی را دستنخورده گذاشتیم.
Why git status reported a deletion: we never created
a.txt on disk — we piped the content straight into the database. So the commit says
“there should be a file called a.txt” while the working tree is empty, and Git reports
that gap as D. git checkout . pulls the file out of that same blob and
writes it to disk. This itself demonstrates that the three areas really are independent: we filled
two of them and left the third untouched.
چه چیزی را نشان داد: git add در واقع دو کار است —
hash-object -w و update-index. و git commit هم دو کار است —
write-tree و commit-tree، بهاضافهٔ جابهجا کردن اشارهگر شاخه.
هیچ جادویی در کار نیست.
چرا ارزش وقت گذاشتن داشت: از این به بعد وقتی چیزی در Git عجیب رفتار کرد،
میدانی زیرش چه لایههایی هست و میتوانی هرکدام را جدا بررسی کنی. تقریباً همهٔ دستورهای «ترسناک»
Git — reset، rebase، cherry-pick — فقط ترکیبهای
متفاوتی از همین چهار عملاند.
What it revealed: git add is really two operations —
hash-object -w and update-index. And git commit is two more —
write-tree and commit-tree — plus moving a branch pointer. There is no magic.
Why it was worth the time: from now on, when Git behaves strangely, you know
which layers sit underneath and can inspect each one. Almost every “frightening” Git command —
reset, rebase, cherry-pick — is just a different combination of
these four operations.
پروژهٔ پایان فصلEnd-of-chapter project
دفترچهٔ یادداشت نسخهدار
A versioned notebook
هدف: یک پوشهٔ یادداشت واقعی بساز که تاریخچهاش را خودت بفهمی. در پایان باید بتوانی
به هر کسی نشان بدهی که در هر commit چه چیزی و چرا عوض شد.
The goal: build a real notes folder whose history you understand. By the end you should be able to show anyone what changed in each commit and why.
مراحل
Steps
-
پایه را بگذارLay the foundation
پوشهٔ
notebookبساز،git initبزن، و قبل از هرaddیک.gitignoreبنویس که*.tmpوdrafts/را نادیده بگیرد. ترتیب مهم است — دلیلش را در دام ششم خواندی.Create a
notebookfolder, rungit init, and before any add write a.gitignorefor*.tmpanddrafts/. The order matters — you read why in the sixth pitfall. -
سه
commitبا معناThree meaningful commitsسه فایل یادداشت بساز و هرکدام را در
commitجدا ثبت کن. پیامها باید بگویند چه چیزی اضافه شد، نه «update» و «fix».Create three note files and commit each separately. The messages must say what was added, not “update” or “fix”.
-
ناحیهٔ آمادهسازی را واقعاً استفاده کنActually use the staging area
حالا دو تغییر نامرتبط بساز: یک یادداشت جدید، و اصلاح یک غلط املایی در یادداشت قدیمی. هر دو را در پوشهٔ کاری داشته باش، ولی دو
commitجدا بساز.Now make two unrelated changes: a new note, and a typo fix in an old one. Have both in the working tree, but produce two separate commits.
-
فایل نادیده را امتحان کنTest the ignore rule
یک
scratch.tmpو یکdrafts/idea.mdبساز. ثابت کن که Git هیچکدام را نمیبیند، و بگو کدام قاعده مسئول هرکدام بود.Create
scratch.tmpanddrafts/idea.md. Prove Git sees neither, and name the rule responsible for each. -
تاریخچه را گزارش کنReport the history
فایل
HISTORY.mdبساز و در آن برای هرcommitیک خط بنویس: کلید کوتاه، پیام، و اینکه چند فایل عوض شد. این را از خروجیgit logدربیاور، نه از حافظه.Create
HISTORY.mdwith one line per commit: short key, message, and how many files changed. Derive it fromgit log, not memory.
پاسخ مرجع — بعد از انجام دادن خودت باز کن Reference answer — open after you have done it
# ۱) پایه — .gitignore اول mkdir notebook && cd notebook && git init -q printf '*.tmp\ndrafts/\n' > .gitignore git add .gitignore git commit -q -m "افزودن قواعد نادیدهگرفتن" # ۲) سه commit با معنا printf '# خرید\n- نان\n' > shopping.md git add shopping.md && git commit -q -m "افزودن فهرست خرید" printf '# کتابها\n- سووشون\n' > books.md git add books.md && git commit -q -m "افزودن فهرست کتاب" printf '# ایدهها\n- یادگیری Git\n' > ideas.md git add ideas.md && git commit -q -m "افزودن فهرست ایدهها"
# ۳) دو تغییر نامرتبط، دو commit جدا printf '# سفر\n- شیراز\n' > travel.md # تغییر اول printf '# خرید\n- نان سنگک\n' > shopping.md # تغییر دوم # فقط اصلاح غلط را stage کن — نه فایل جدید را git add shopping.md git diff --staged --stat shopping.md | 2 +- ← فقط همین یکی، درست است git commit -q -m "اصلاح: نان سنگک بهجای نان" # حالا فایل جدید git add travel.md git commit -q -m "افزودن فهرست سفر"
# ۴) آزمون نادیدهگرفتن printf 'موقت\n' > scratch.tmp mkdir drafts && printf 'پیشنویس\n' > drafts/idea.md git status --short ← خالی: هیچکدام دیده نمیشوند git check-ignore -v scratch.tmp drafts/idea.md .gitignore:1:*.tmp scratch.tmp .gitignore:2:drafts/ drafts/idea.md # ۵) گزارش تاریخچه — %h کلید کوتاه، %s پیام git log --pretty=format:'- `%h` %s' --shortstat > HISTORY.md git add HISTORY.md && git commit -q -m "افزودن گزارش تاریخچه" git log --oneline e2c4a91 افزودن گزارش تاریخچه 7d1b3f8 افزودن فهرست سفر 4a9e2c1 اصلاح: نان سنگک بهجای نان b3f7d20 افزودن فهرست ایدهها 9c1a4e6 افزودن فهرست کتاب 2f8d5b3 افزودن فهرست خرید 1a4c7e9 افزودن قواعد نادیدهگرفتن
نکتهٔ اصلی مرحلهٔ ۳: اگر git add . میزدی، هر دو تغییر با هم
در یک commit میرفتند و تاریخچهات دروغ میگفت — «افزودن فهرست سفر» که
همزمان یک غلط املایی را هم درست کرده. حالا اگر شش ماه بعد بخواهی فقط آن اصلاح را برگردانی،
میتوانی؛ در حالت اول نمیتوانستی. این تفاوت، تنها دلیل وجود ناحیهٔ آمادهسازی است.
The key point in step 3: had you run git add ., both changes would have
landed in one commit and your history would lie — “add travel list” that also silently fixes a typo.
Now, if six months later you want to revert only that fix, you can; in the other case you could not.
That difference is the entire reason the staging area exists.
- بگویی Git وقتی
commitمیزنی دقیقاً چه چیزی روی دیسک مینویسد. - Say exactly what Git writes to disk when you commit.
- تفاوت سه ناحیه را توضیح بدهی و بگویی هر دستور بین کدام دو تا حرکت میکند.
- Explain the three areas and say which two each command moves between.
- بگویی چرا کلید یک محتوا روی هر ماشینی یکسان است، و چرا این مهم است.
- Say why a content's key is identical on every machine, and why that matters.
- یک
.gitignoreبنویسی و وقتی کار نکرد، بفهمی چرا. - Write a
.gitignoreand diagnose it when it does not work. - دو تغییر نامرتبط را در دو
commitجدا ثبت کنی. - Record two unrelated changes as two separate commits.
- توضیح بدهی چرا عوض کردن تاریخچهٔ قدیمی، کلید همهٔ
commitهای بعدی را عوض میکند. - Explain why altering old history changes the key of every later commit.