کدنامهمرجع‌های مهندسی نرم‌افزار، به فارسی
گیت · فصل ۱Git · Chapter 1

مسئله‌ای که 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.

۸بخشsections
۱۲تمرین با پاسخsolved exercises
۴۵دقیقهminزمان مطالعهreading time
۰پیش‌نیازprerequisites

دردی که 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:

~/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.

  1. نمی‌دانی چه چیزی عوض شده You cannot see what changed

    بین project-final و project-final-REAL چه فرقی هست؟ باید فایل‌به‌فایل باز کنی و چشمی مقایسه کنی. اگر پروژه هزار فایل داشته باشد، عملاً غیرممکن است. Git جواب این را diff می‌نامد.

    What differs between project-final and project-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 this diff.

  2. نمی‌دانی چرا عوض شده 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 commit message.

  3. نمی‌توانی مطمئن باشی چیزی گم نشده 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.

  4. دو نفر نمی‌توانند همزمان کار کنند 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.

چرا این فهرست را نوشتیمWhy we wrote that list

هر بار در این مسیر با دستوری روبه‌رو شدی که منطقش عجیب به نظر می‌رسد، به این چهار درد برگرد. تقریباً همیشه جواب یکی از این‌هاست. 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:

bash
# محتوای «hello» را به Git بده و بپرس کلیدش چه می‌شود.
# ‏--stdin یعنی محتوا را از ورودی می‌گیرم، نه از یک فایل.
printf 'hello\n' | git hash-object --stdin
ce013625030ba8dba906f756967f9e9ca394464a
عدد تو باید دقیقاً همین باشد Your number must be exactly this

برخلاف بقیهٔ خروجی‌های این مرجع که روی سیستم تو فرق می‌کنند، این یکی نباید فرق کند. روی هر ماشینی، هر سیستم‌عاملی، هر سالی — محتوای 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 previous commit's key, altering an old commit changes the key of every commit after it. That is precisely what chapter 7 calls “the golden rule of rebase”.

چهار نوع شیء

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 و blob commit a1b2c3 «افزودن سلام» “add greeting” والد: ندارد parent: none commit d4e5f6 «افزودن نام» “add name” parent: a1b2c3 والد parent tree 9f8e7d ‏a.txt ← ce0136 a.txt → ce0136 ‏b.txt ← 0f4b5c b.txt → 0f4b5c blob ce0136 "hello\n" blob 0f4b5c "سلام\n" تاریخچه history ساختار پوشه folder structure محتوای فایل‌ها file contents هر فلش یک کلید ‎SHA‎ است — و کلید هر شیء از محتوای همان شیء ساخته شده every arrow is a SHA key — and each key is derived from that object's own content
‏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.

سه ناحیهٔ Git و دستورهای بین آن‌ها پوشهٔ کاری working tree working directory فایل‌هایی که می‌بینی و ویرایش می‌کنی the files you see and edit ناحیهٔ آماده‌سازی staging area index / staging پیش‌نویس commit بعدی a draft of the next commit مخزن repository .git/objects تاریخچهٔ ماندگار permanent history git add git commit ‏git checkout · git restore — برگرداندن فایل از تاریخچه git checkout · git restore — bringing files back from history ‏git status تفاوت این سه را به تو نشان می‌دهد git status shows you the differences between these three
تقریباً هر دستور Git یعنی «چیزی را از یکی از این جعبه‌ها به دیگری ببر». Almost every Git command means “move something from one of these boxes to another”.

چرا ناحیهٔ وسط اصلاً وجود دارد؟

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.

bash
# ‏--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:

bash
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.

bash
ls -1 .git
HEAD
config
description
hooks/
info/
objects/
refs/

# ‏objects خالی است — هنوز هیچ محتوایی ذخیره نکرده‌ایم.
ls .git/objects
info  pack

اولین فایل، مرحله‌به‌مرحله

The first file, step by step

bash
printf 'hello\n' > a.txt
git status
output
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.

bash
git add a.txt

# حالا نگاه کن به .git/objects — چیزی اضافه شده است!
find .git/objects -type f
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
همان عدد!The same number!

این دقیقاً همان کلیدی است که در بخش سوم با 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.

bash
# نوع این شیء چیست؟
git cat-file -t ce013625030ba8dba906f756967f9e9ca394464a
blob

# محتوایش چیست؟ سوئیچ -p یعنی «چاپ خوانا»
git cat-file -p ce013625030ba8dba906f756967f9e9ca394464a
hello

حالا commit بزنیم و ببینیم چه چیز دیگری ساخته می‌شود:

Now let us commit and see what else gets created:

bash
# ‏-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:

bash
# ‏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:

bash
printf 'سلام\n' > b.txt
git add b.txt
git commit -m "افزودن نسخهٔ فارسی"
bash
# ‏--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:

bash
# تفاوت پوشهٔ کاری با ناحیهٔ آماده‌سازی — یعنی «چه چیزی 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.

.gitignore
# وابستگی‌ها — با یک دستور دوباره نصب می‌شوند، پس ذخیره‌شان اتلاف است
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.

bash
# رها کردن یک فایل از ردیابی، بدون حذفش از دیسک.
# ‏--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 --global commands from section five. For a different email on one project, run them without --global inside 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 as LF. 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 .gitattributes containing * text=auto eol=lf and 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 .git and found none. Usually you forgot to cd, or ran git init in the wrong place.
درمان
Fix
git rev-parse --show-toplevel ریشهٔ مخزن فعلی را می‌گوید. اگر خطا داد، واقعاً بیرون از مخزنی.
git rev-parse --show-toplevel prints 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 --ignored also 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 commit without -m, so Git opened an editor for the message. If the default editor is vim and 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 .gitignore first, then add. And prefer git add -p, which shows you each hunk, over git add .

مرجع سریع این فصلQuick reference

دستورهای فصل ۱Chapter 1 commands
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.

تمرین‌های حل‌شدهSolved ۰ / ۱۲
تمرین ۱٫۱Exercise 1.1 مبتدیBeginner ۵ دقیقه5 min

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
bash
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.

تمرین ۱٫۲Exercise 1.2 مبتدیBeginner ۵ دقیقه5 min

یک مخزن بساز و ثابت کن که خالی است — یعنی نشان بده هیچ شیئی در پایگاه‌داده‌اش نیست.

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
bash
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.

تمرین ۱٫۳Exercise 1.3 مبتدیBeginner ۱۰ دقیقه10 min

یک فایل بساز و آن را از 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
bash
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.

تمرین ۱٫۴Exercise 1.4 مبتدیBeginner ۱۰ دقیقه10 min

بدون ساختن هیچ مخزنی، کلید محتوای 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
bash
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.

تمرین ۱٫۵Exercise 1.5 متوسطIntermediate ۱۵ دقیقه15 min

ثابت کن که 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
bash
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.

تمرین ۱٫۶Exercise 1.6 متوسطIntermediate ۱۵ دقیقه15 min

یک فایل را 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
bash
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).

تمرین ۱٫۷Exercise 1.7 متوسطIntermediate ۱۵ دقیقه15 min

دو فایل با محتوای کاملاً یکسان ولی نام متفاوت بساز و هر دو را 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
bash
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 ذخیره شده‌اند:

bash
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.

تمرین ۱٫۸Exercise 1.8 متوسطIntermediate ۱۵ دقیقه15 min

یک .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
bash
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.

تمرین ۱٫۹Exercise 1.9 متوسطIntermediate ۲۰ دقیقه20 min

فایلی را که قبلاً 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
bash
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.

تمرین ۱٫۱۰Exercise 1.10 پیشرفتهAdvanced ۲۵ دقیقه25 min

کل زنجیرهٔ 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
bash
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.

تمرین ۱٫۱۱Exercise 1.11 پیشرفتهAdvanced ۲۵ دقیقه25 min

ثابت کن که تاریخچهٔ 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
bash
# زمان را ثابت می‌کنیم. این خودش نکته است: زمان بخشی از محتوای 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 ..
bash
# همان تاریخچه، ولی فقط محتوای اولین 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.

تمرین ۱٫۱۲Exercise 1.12 پیشرفتهAdvanced ۳۰ دقیقه30 min

یک 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
bash
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

پروژهٔ کوچک · حدود ۳۰ دقیقهMini project · about 30 minutes

دفترچهٔ یادداشت نسخه‌دار

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

  1. پایه را بگذارLay the foundation

    پوشهٔ notebook بساز، git init بزن، و قبل از هر add یک .gitignore بنویس که *.tmp و drafts/ را نادیده بگیرد. ترتیب مهم است — دلیلش را در دام ششم خواندی.

    Create a notebook folder, run git init, and before any add write a .gitignore for *.tmp and drafts/. The order matters — you read why in the sixth pitfall.

  2. سه 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”.

  3. ناحیهٔ آماده‌سازی را واقعاً استفاده کن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.

  4. فایل نادیده را امتحان کنTest the ignore rule

    یک scratch.tmp و یک drafts/idea.md بساز. ثابت کن که Git هیچ‌کدام را نمی‌بیند، و بگو کدام قاعده مسئول هرکدام بود.

    Create scratch.tmp and drafts/idea.md. Prove Git sees neither, and name the rule responsible for each.

  5. تاریخچه را گزارش کنReport the history

    فایل HISTORY.md بساز و در آن برای هر commit یک خط بنویس: کلید کوتاه، پیام، و اینکه چند فایل عوض شد. این را از خروجی git log دربیاور، نه از حافظه.

    Create HISTORY.md with one line per commit: short key, message, and how many files changed. Derive it from git log, not memory.

پاسخ مرجع — بعد از انجام دادن خودت باز کن Reference answer — open after you have done it
bash
# ۱) پایه — .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 "افزودن فهرست ایده‌ها"
bash
# ۳) دو تغییر نامرتبط، دو 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 "افزودن فهرست سفر"
bash
# ۴) آزمون نادیده‌گرفتن
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.

در پایان این فصل باید بتوانیBy the end of this chapter you should be able to
  • بگویی 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 .gitignore and 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.