رفتن به محتوا
LoopX

برگرداندن تغییرات

توی این درس یاد می‌گیری اشتباه‌ها را امن برگردانی. ابزار درست به این بستگی دارد که اشتباه کجاست: در پوشه‌ی کاری (git restore)، در staging (git restore --staged)، در آخرین commit (git commit --amend)، یا در یک commit قدیمی‌تر (git revert و git reset با سه حالت --soft، --mixed، --hard). مهم‌تر از همه می‌فهمی کدام برای تاریخچه‌ی مشترک امن است (revert) و کدام فقط برای کار خصوصی خودت (reset، amend). تمرین: سه نوع اشتباه بساز و هر کدام را با روش درست برگردان.

مسئله: اشتباه کجا اتفاق افتاده؟

Section titled “مسئله: اشتباه کجا اتفاق افتاده؟”

هر کسی اشتباه می‌کند: فایلی را خراب ویرایش کردی، چیزی را به‌اشتباه add کردی، پیام commit را غلط نوشتی، یا یک commit کامل را نباید می‌زدی. Git برای هر کدام ابزار جدا دارد، و ابزار اشتباه می‌تواند کار را برای همیشه پاک کند. پس قبل از دست‌بردن به دستور، دو سؤال بپرس: اشتباه کجاست؟ (پوشه‌ی کاری، staging، یا commit) و آیا کسی دیگر آن را دیده؟ (آیا push شده؟)

اشتباه ابزار امن؟
فایل را خراب ویرایش کردم (commit نشده) git restore فایل ⚠ تغییرها برای همیشه از بین می‌روند
چیزی را بی‌جا add کردم git restore --staged فایل ✓ تغییرها در پوشه‌ی کاری می‌مانند
پیام یا محتوای آخرین commit غلط است (push نشده) git commit --amend ✓ فقط اگر push نشده
چند commit خصوصی را می‌خواهم پس بگیرم git reset ⚠ تاریخچه را بازنویسی می‌کند
commit ای که به اشتراک گذاشته شده باید خنثی شود git revert ✓ همیشه امن (commit جدید می‌سازد)

یک حسابدار حرفه‌ای هیچ‌وقت صفحه‌ی ثبت‌شده‌ی دفتر را خط نمی‌زند؛ یک سند اصلاحی می‌نویسد (آن revert). ولی پیش‌نویس‌های روی میز خودش را که هنوز وارد دفتر نشده، مجاز است پاره کند (restore و reset) یا آخرین سند را قبل از امضای نهایی ویرایش کند (amend). قاعده: هر چه در دفتر مشترک است، فقط با سند اصلاحی. هر چه خصوصی است، آزادانه.

reset تاریخچه را عقب می‌برد (commit کنار گذاشته‌شده دیگر در شاخه نیست)؛ revert هیچ‌چیز را پاک نمی‌کند و یک commit جدید می‌سازد که اثر قبلی را خنثی می‌کند. برای کار مشترک فقط revert امن است.

یک پروژه‌ی کوچک «یادداشت» با سه commit می‌سازم و از آن آزمایش می‌کنم:

Terminal window
mkdir -p ~/gitlab/notes && cd ~/gitlab/notes && git init -q
echo "خرید نان" > todo.txt
git add todo.txt && git commit -qm "Add todo list"
echo "خرید شیر" >> todo.txt
git commit -qam "Add milk to the list"
echo "نوشتن گزارش" >> todo.txt
git commit -qam "Add report task"
git log --oneline
cat todo.txt
خروجی
2231b5f Add report task
78dc659 Add milk to the list
3c6ad06 Add todo list
خرید نان
خرید شیر
نوشتن گزارش

مثال ۱: پوشه‌ی کاری را برگردان، git restore

Section titled “مثال ۱: پوشه‌ی کاری را برگردان، git restore”

فایل را اشتباهاً خراب کردی و می‌خواهی به آخرین commit برگردد:

Terminal window
cd ~/gitlab/notes
echo "اشتباه! همه‌چیز را پاک کردم" > todo.txt
echo "--- وضعیت:"
git status -s
echo "--- فایل الآن:"
cat todo.txt
git restore todo.txt
echo "--- بعد از git restore todo.txt:"
cat todo.txt
git status -s
echo "(خالی = پوشه تمیز)"
خروجی
--- وضعیت:
M todo.txt
--- فایل الآن:
اشتباه! همه‌چیز را پاک کردم
--- بعد از git restore todo.txt:
خرید نان
خرید شیر
نوشتن گزارش
(خالی = پوشه تمیز)

git restore فایل محتوای فایل را از آخرین commit (یا از staging اگر در آن باشد) برمی‌گرداند. هشدار جدی: این تغییرها commit نشده بودند، پس بعد از restore دیگر هیچ راهی برای برگرداندنشان نیست (مثال ۹ را ببین). git restore . همه‌ی فایل‌های پوشه را برمی‌گرداند.

مثال ۲: چیزی را از staging بیرون بیاور، git restore --staged

Section titled “مثال ۲: چیزی را از staging بیرون بیاور، git restore --staged”

فایل اشتباهی را add کردی ولی نمی‌خواهی در commit بعدی باشد:

Terminal window
cd ~/gitlab/notes
echo "یادداشت شخصی" > secret.txt
echo "تغییر مفید" >> todo.txt
git add .
echo "--- اشتباهی هر دو add شدند:"
git status -s
git restore --staged secret.txt
echo "--- secret.txt را از staging درآوردم (فایل سر جایش مانده):"
git status -s
ls secret.txt
خروجی
--- اشتباهی هر دو add شدند:
A secret.txt
M todo.txt
--- secret.txt را از staging درآوردم (فایل سر جایش مانده):
M todo.txt
?? secret.txt
secret.txt

--staged فقط ناحیه‌ی staging را عوض می‌کند و فایل را در پوشه‌ی کاری دست‌نخورده می‌گذارد؛ پس کاملاً بی‌خطر است. (secret.txt از A به ?? برگشت: untracked.) ترکیب git restore --staged --worktree فایل هر دو را برمی‌گرداند. حالا todo.txt را با پیام درست commit می‌کنم:

Terminal window
cd ~/gitlab/notes
git commit -qm "Add a useful note"
rm secret.txt
git log --oneline | head -2
خروجی
4795f1c Add a useful note
2231b5f Add report task

مثال ۳: برگرداندن فایل حذف‌شده و فایل از commit قدیمی

Section titled “مثال ۳: برگرداندن فایل حذف‌شده و فایل از commit قدیمی”

restore فقط «آخرین حالت» را برنمی‌گرداند؛ با --source از هر commit می‌توانی فایل را برگردانی:

Terminal window
cd ~/gitlab/notes
rm todo.txt
echo "--- فایل را حذف کردم:"
git status -s
git restore todo.txt
echo "--- برگشت:"
cat todo.txt
echo "--- نسخه‌ی دو commit قبل از همین فایل را به پوشه‌ی کاری بیاور:"
git restore --source=HEAD~2 todo.txt
cat todo.txt
git restore todo.txt
خروجی
--- فایل را حذف کردم:
D todo.txt
--- برگشت:
خرید نان
خرید شیر
نوشتن گزارش
تغییر مفید
--- نسخه‌ی دو commit قبل از همین فایل را به پوشه‌ی کاری بیاور:
خرید نان
خرید شیر

--source=HEAD~2 یعنی «به‌جای آخرین commit، از دو commit قبل بخوان». این برای «فقط این یک فایل را به نسخه‌ی دیروز برگردان» عالی است، بدون اینکه بقیه‌ی پروژه به عقب برود. (آخرین دستور نسخه‌ی کنونی را برمی‌گرداند.)

مثال ۴: آخرین commit را اصلاح کن، git commit --amend

Section titled “مثال ۴: آخرین commit را اصلاح کن، git commit --amend”

دو اشتباه رایج: پیام غلط، و فایلی که یادت رفت. هر دو را با --amend درست می‌کنی (بدون ساختن commit اضافه):

Terminal window
cd ~/gitlab/notes
echo "--- شناسه‌ی آخرین commit قبل از amend:"
git log --oneline -1
echo "--- ۱) پیام را درست می‌کنم:"
git commit --amend -m "Add report task to the todo list"
git log --oneline -1
echo "--- ۲) فایلی که یادم رفته بود را به همان commit اضافه می‌کنم:"
echo "# یادداشت‌ها" > README.md
git add README.md
git commit --amend --no-edit
git show --stat --format='%h %s' HEAD
خروجی
--- شناسه‌ی آخرین commit قبل از amend:
4795f1c Add a useful note
--- ۱) پیام را درست می‌کنم:
[main ae5e6cf] Add report task to the todo list
Date: Sun Oct 4 08:15:32 2026 +0000
1 file changed, 1 insertion(+)
ae5e6cf Add report task to the todo list
--- ۲) فایلی که یادم رفته بود را به همان commit اضافه می‌کنم:
[main adc2b52] Add report task to the todo list
Date: Sun Oct 4 08:15:32 2026 +0000
2 files changed, 2 insertions(+)
create mode 100644 README.md
adc2b52 Add report task to the todo list
README.md | 1 +
todo.txt | 1 +
2 files changed, 2 insertions(+)

دقت کن هر بار شناسه‌ی commit عوض شد. amend در واقع commit قبلی را اصلاح نمی‌کند؛ یک commit جدید می‌سازد و جای قبلی را می‌گیرد (قبلی دور ریخته می‌شود). برای همین فقط قبل از push امن است (مثال ۸). --no-edit یعنی «پیام قبلی را همان‌طور نگه دار».

reset اشاره‌گر شاخه را به commit دیگری می‌برد. سه حالت فرق دارند در اینکه با staging و پوشه‌ی کاری چه کند. این سه آزمایش را روی سه commit آخر انجام می‌دهم (بعد از هر کدام، همه‌چیز را به حالت اول برمی‌گردانم):

Terminal window
cd ~/gitlab/notes
orig=$(git rev-parse HEAD)
git log --oneline | head -3
echo "=== --soft HEAD~1: commit را بردار، تغییرها staged بمانند:"
git reset --soft HEAD~1
git status -s; git log --oneline | head -1
git reset -q --hard $orig
echo "=== --mixed HEAD~1 (پیش‌فرض): commit را بردار، تغییرها فقط در پوشه‌ی کاری:"
git reset --mixed HEAD~1
git status -s; git log --oneline | head -1
git reset -q --hard $orig
echo "=== --hard HEAD~1: commit و تغییرها، هر دو دور ریخته می‌شود:"
git reset --hard HEAD~1
git status -s; git log --oneline | head -1
cat todo.txt
git reset -q --hard $orig
خروجی
adc2b52 Add report task to the todo list
2231b5f Add report task
78dc659 Add milk to the list
=== --soft HEAD~1: commit را بردار، تغییرها staged بمانند:
A README.md
M todo.txt
2231b5f Add report task
=== --mixed HEAD~1 (پیش‌فرض): commit را بردار، تغییرها فقط در پوشه‌ی کاری:
Unstaged changes after reset:
M todo.txt
M todo.txt
?? README.md
2231b5f Add report task
=== --hard HEAD~1: commit و تغییرها، هر دو دور ریخته می‌شود:
HEAD is now at 2231b5f Add report task
2231b5f Add report task
خرید نان
خرید شیر
نوشتن گزارش
حالت شاخه staging پوشه‌ی کاری کاربرد
--soft به commit مقصد می‌رود دست‌نخورده (تغییرها staged) دست‌نخورده «commit را بردار ولی همین را دوباره commit می‌کنم» (مثلاً چند commit را یکی کنی)
--mixed (پیش‌فرض) می‌رود خالی می‌شود دست‌نخورده (تغییرها modified) «commit را بردار، می‌خواهم دوباره تقسیمش کنم»
--hard می‌رود خالی برمی‌گردد (تغییرها از بین می‌رود) «همه را دور بریز» ⚠

سه ناحیه‌ی درس قبل را به یاد بیاور: --soft فقط شاخه را می‌برد؛ --mixed شاخه و staging؛ --hard هر سه. از --hard با احتیاط استفاده کن، چون تغییرهای commit نشده را هم می‌برد.

reset --hard HEAD~1: اشاره‌گر main از C3 به C2 می‌رود و C3 (خط‌چین) دیگر روی شاخه نیست. هنوز در .git هست (مثال ۷ نشان می‌دهد چطور برمی‌گردد)، ولی هیچ برچسبی به آن اشاره نمی‌کند.

مثال ۶: git revert، خنثی‌کردن بدون بازنویسی

Section titled “مثال ۶: git revert، خنثی‌کردن بدون بازنویسی”

برای commit ای که دیگران ممکن است داشته باشند، revert تاریخچه را دست نمی‌زند و یک commit جدید می‌سازد:

Terminal window
cd ~/gitlab/notes
echo "--- تاریخچه قبل:"
git log --oneline
git revert --no-edit HEAD
echo "--- بعد از revert کردن آخرین commit:"
git log --oneline
echo "--- فایل‌ها (تغییر آن commit معکوس شد، ولی خود commit هنوز در تاریخچه است):"
cat todo.txt; ls
git show --stat --format='%h %s' HEAD
echo "--- (برای ادامه‌ی مثال‌ها، این revert را در کار خصوصی خودم برمی‌گردانم)"
git reset -q --hard HEAD~1
خروجی
--- تاریخچه قبل:
adc2b52 Add report task to the todo list
2231b5f Add report task
78dc659 Add milk to the list
3c6ad06 Add todo list
[main 81f24aa] Revert "Add report task to the todo list"
Date: Sun Oct 4 08:15:32 2026 +0000
2 files changed, 2 deletions(-)
delete mode 100644 README.md
--- بعد از revert کردن آخرین commit:
81f24aa Revert "Add report task to the todo list"
adc2b52 Add report task to the todo list
2231b5f Add report task
78dc659 Add milk to the list
3c6ad06 Add todo list
--- فایل‌ها (تغییر آن commit معکوس شد، ولی خود commit هنوز در تاریخچه است):
خرید نان
خرید شیر
نوشتن گزارش
todo.txt
81f24aa Revert "Add report task to the todo list"
README.md | 1 -
todo.txt | 1 -
2 files changed, 2 deletions(-)
--- (برای ادامه‌ی مثال‌ها، این revert را در کار خصوصی خودم برمی‌گردانم)

git revert commit اثر آن commit را معکوس می‌کند و آن را به‌صورت یک commit تازه ثبت می‌کند (پیامش Revert "..." است). هیچ commit ای پاک نشد؛ تاریخچه فقط یک خط بلندتر شد: هر کس که تاریخچه‌ی قبلی را دارد با pull ساده آن را می‌گیرد. می‌شود هر commit قدیمی را هم revert کرد، ولی اگر commit های بعدی روی همان خط‌ها تغییر داده باشند، conflict می‌شود (اشتباهات رایج ۴).

حتی بعد از reset --hard، commit های ثبت‌شده گم نمی‌شوند: Git یک دفترچه‌ی داخلی (reflog) از هر جابه‌جایی HEAD نگه می‌دارد:

Terminal window
cd ~/gitlab/notes
git log --oneline | head -2
git reset -q --hard HEAD~2
echo "--- بعد از reset --hard HEAD~2 دو commit «گم» شدند:"
git log --oneline
echo "--- ولی reflog همه‌ی جابه‌جایی‌ها را یادش است:"
git reflog | head -4
echo "--- برمی‌گردم به قبل از reset (HEAD@{1}):"
git reset -q --hard HEAD@{1}
git log --oneline | head -3
خروجی
adc2b52 Add report task to the todo list
2231b5f Add report task
--- بعد از reset --hard HEAD~2 دو commit «گم» شدند:
78dc659 Add milk to the list
3c6ad06 Add todo list
--- ولی reflog همه‌ی جابه‌جایی‌ها را یادش است:
78dc659 HEAD@{0}: reset: moving to HEAD~2
adc2b52 HEAD@{1}: reset: moving to HEAD~1
81f24aa HEAD@{2}: revert: Revert "Add report task to the todo list"
adc2b52 HEAD@{3}: reset: moving to adc2b52195e37e523f72d93642360ed80165f202
--- برمی‌گردم به قبل از reset (HEAD@{1}):
adc2b52 Add report task to the todo list
2231b5f Add report task
78dc659 Add milk to the list

HEAD@{1} یعنی «جایی که HEAD یک جابه‌جایی پیش بود». تا چند ماه (به‌صورت پیش‌فرض ۹۰ روز) commit های «گم‌شده» در .git می‌مانند. ولی reflog فقط commit ها را نگه می‌دارد، نه تغییرهای commit نشده (مثال ۹). درس «نجات و ابزارهای پیشرفته» reflog را کامل توضیح می‌دهد.

مثال ۸: چرا بعد از push نباید amend یا reset کرد؟

Section titled “مثال ۸: چرا بعد از push نباید amend یا reset کرد؟”

یک مخزن «سرور» (bare) می‌سازم، commit را push می‌کنم، بعد آن را amend می‌کنم و دوباره push می‌کنم تا ببینی Git چه می‌گوید (remote و push در درس‌های بعد کامل می‌آید؛ این‌جا فقط نتیجه مهم است):

Terminal window
cd ~/gitlab
git init -q --bare server.git
cd notes
git remote add origin ~/gitlab/server.git
git push -q -u origin main 2>&1 | tail -1
echo "--- commit را (که الان روی «سرور» است) بازنویسی می‌کنم:"
git commit --amend -qm "Reword: add report task"
echo "--- دوباره push:"
git push 2>&1
خروجی
--- commit را (که الان روی «سرور» است) بازنویسی می‌کنم:
--- دوباره push:
To /home/ali/gitlab/server.git
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to '/home/ali/gitlab/server.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Git رد کرد: ! [rejected] main -> main (non-fast-forward). تاریخچه‌ی محلی (با commit بازنویسی‌شده) از تاریخچه‌ی سرور جدا شده و Git نمی‌گذارد با یک push ساده سرور را عقب ببرد. راه زورکی git push --force است که تاریخچه‌ی سرور را بازنویسی می‌کند و کار هر کسی که از commit قدیمی ادامه داده را خراب می‌کند. (نسخه‌ی مؤدب‌تر --force-with-lease است که فقط اگر کسی دیگر چیزی push نکرده اجازه می‌دهد.) قاعده: بعد از push، به‌جای amend و reset از revert استفاده کن. حالا درستش می‌کنم، یعنی تاریخچه‌ی محلی را با سرور هماهنگ می‌کنم:

Terminal window
cd ~/gitlab/notes
git reset -q --hard origin/main
git log --oneline | head -3
خروجی
adc2b52 Add report task to the todo list
2231b5f Add report task
78dc659 Add milk to the list

(origin/main همان وضعیت سرور است. این‌جا چون فقط من آن را داشتم و می‌خواستم تغییرِ amend را دور بریزم، reset --hard امن بود.)

مثال ۹: git clean و تغییرهای از دست رفته

Section titled “مثال ۹: git clean و تغییرهای از دست رفته”

restore فقط فایل‌های دنبال‌شده را برمی‌گرداند. فایل‌های جدید و untracked را git clean پاک می‌کند، و آن هم برگشت‌ناپذیر است. همیشه اول با -n (dry run) ببین چه چیزی پاک می‌شود:

Terminal window
cd ~/gitlab/notes
echo "موقت" > tmp1.log; echo "موقت" > tmp2.log; mkdir build && echo x > build/out.o
echo "--- فایل‌های untracked:"
git status -s
echo "--- git clean -n (فقط نشان بده، پاک نکن):"
git clean -n
echo "--- با -d پوشه‌ها هم:"
git clean -nd
git clean -fdq
echo "--- بعد از git clean -fd:"
git status -s
echo "(خالی)"
خروجی
--- فایل‌های untracked:
?? build/
?? tmp1.log
?? tmp2.log
--- git clean -n (فقط نشان بده، پاک نکن):
Would remove tmp1.log
Would remove tmp2.log
--- با -d پوشه‌ها هم:
Would remove build/
Would remove tmp1.log
Would remove tmp2.log
--- بعد از git clean -fd:
(خالی)

-f (force) لازم است (Git برای ایمنی بدون آن کار نمی‌کند)، -d پوشه‌ها را هم می‌گیرد. و -x حتی فایل‌های .gitignore شده را پاک می‌کند، که بسیار خطرناک است (ممکن است .env تو را ببرد). همیشه اول -n.

پشت پرده: reset یک اشاره‌گر را جابه‌جا می‌کند

Section titled “پشت پرده: reset یک اشاره‌گر را جابه‌جا می‌کند”

شاخه فقط یک فایل کوچک است که هش یک commit را نگه می‌دارد. git reset همان فایل را عوض می‌کند. ببین:

Terminal window
cd ~/gitlab/notes
echo "--- شاخه‌ی main فقط یک هش است:"
cat .git/refs/heads/main
git rev-parse HEAD
git reset -q --soft HEAD~1
echo "--- بعد از reset --soft HEAD~1، همان فایل هش دیگری دارد:"
cat .git/refs/heads/main
git reset -q --soft HEAD@{1}
echo "--- و بعد از برگشتن:"
cat .git/refs/heads/main
خروجی
--- شاخه‌ی main فقط یک هش است:
adc2b52195e37e523f72d93642360ed80165f202
adc2b52195e37e523f72d93642360ed80165f202
--- بعد از reset --soft HEAD~1، همان فایل هش دیگری دارد:
2231b5f0f6b3c8ff57906e03e0c56ddf03f0634e
--- و بعد از برگشتن:
adc2b52195e37e523f72d93642360ed80165f202

commit های دور‌ریخته‌شده هنوز در .git/objects هستند (برای همین reflog می‌تواند برشان گرداند)؛ فقط هیچ برچسبی به آن‌ها اشاره نمی‌کند، و Git بعد از مدتی (garbage collection) پاکشان می‌کند. revert برعکس: اشاره‌گر را فقط جلو می‌برد، به یک commit تازه.

دستور کار commit ها داده‌ی از دست‌رفتنی؟
git restore فایل پوشه‌ی کاری ← از staging/HEAD تغییر نمی‌کند تغییر commit نشده ⚠
git restore --staged فایل از staging بیرون بیاور تغییر نمی‌کند نه
git restore --source=commit فایل فایل از commit دلخواه تغییر نمی‌کند تغییر commit نشده ⚠
git commit --amend جایگزینی آخرین commit بازنویسی نه (قبلی در reflog)
git reset --soft commit شاخه عقب؛ staging و کاری سالم بازنویسی نه
git reset commit (mixed) شاخه عقب؛ staging خالی بازنویسی نه
git reset --hard commit همه عقب بازنویسی تغییر commit نشده ⚠
git revert commit commit جدید که معکوس می‌کند فقط اضافه نه
git clean -n / -fd فایل‌های untracked — بله ⚠
git reflog جابه‌جایی‌های HEAD (نجات) — —
می‌خواهم… بزن
تغییر نکرده‌ی فایل را دور بریزم git restore فایل
add را لغو کنم git restore --staged فایل
پیام آخرین commit را عوض کنم git commit --amend -m "..."
فایل فراموش‌شده را به آخرین commit بیفزایم git add فایل && git commit --amend --no-edit
۳ commit خصوصی را یکی کنم git reset --soft HEAD~3 و git commit
commit مشترک را خنثی کنم git revert commit
همه‌ی تغییرها را دور بریزم و مثل commit آخر شوم git reset --hard HEAD ⚠

۱) reset --hard یا restore روی کار commit نشده

Section titled “۱) reset --hard یا restore روی کار commit نشده”
Terminal window
mkdir -p ~/gitlab/danger && cd ~/gitlab/danger && git init -q
echo "نسخه‌ی ثبت‌شده" > f.txt && git add f.txt && git commit -qm "Commit f"
echo "یک ساعت کار مهم، هنوز commit نشده" > f.txt
git reset --hard HEAD
echo "--- محتوای فایل بعد از reset --hard:"
cat f.txt
echo "--- آیا reflog چیزی از آن کار دارد؟"
git reflog
خروجی
HEAD is now at edfab30 Commit f
--- محتوای فایل بعد از reset --hard:
نسخه‌ی ثبت‌شده
--- آیا reflog چیزی از آن کار دارد؟
edfab30 HEAD@{0}: reset: moving to HEAD
edfab30 HEAD@{1}: commit (initial): Commit f

آن «یک ساعت کار» رفت و هیچ راه برگشتی ندارد: reflog فقط commit ها را به‌خاطر می‌سپارد، نه تغییرهای ثبت‌نشده. راه‌حل: قبل از هر restore/reset --hard، git status و git diff را بخوان؛ و اگر شک داری اول یک commit موقت بزن (git commit -am wip)، که بعداً با reset یا amend تمیزش می‌کنی.

مثال ۸: non-fast-forward. راه‌حل: git revert. و اگر عمداً تاریخچه‌ی خودت را بازنویسی کرده‌ای (روی شاخه‌ی خصوصی)، git push --force-with-lease (نه --force).

۳) revert روی یک commit ادغام (merge)

Section titled “۳) revert روی یک commit ادغام (merge)”
Terminal window
mkdir -p ~/gitlab/mergerev && cd ~/gitlab/mergerev && git init -q
echo 1 > f && git add f && git commit -qm "Start"
git switch -qc feature && echo a > g && git add g && git commit -qm "Feature work"
git switch -q main && echo 2 >> f && git commit -qam "Main work"
git merge -q --no-edit feature
git revert --no-edit HEAD 2>&1 | head -3
خروجی
error: commit 9dd3913470fa0705555dc54547c9caa1a1869614 is a merge but no -m option was given.
fatal: revert failed

commit ادغام دو والد دارد و Git نمی‌داند کدام طرف را «نگه دارد». باید با -m 1 بگویی «والد اول (main) را اصلی بدان»: git revert -m 1 HEAD. (این را بعد از درس merge بهتر می‌فهمی؛ فعلاً بدان خطای is a merge but no -m option was given یعنی این.)

۴) revert یک commit قدیمی که بعدی‌ها به آن وابسته‌اند

Section titled “۴) revert یک commit قدیمی که بعدی‌ها به آن وابسته‌اند”
Terminal window
cd ~/gitlab/notes
git log --oneline | head -3
echo "--- خنثی‌کردن commit ای که دو commit قبل است:"
git revert --no-edit HEAD~1 2>&1 | head -3
echo "--- وضعیت (UU = هر دو طرف عوض کرده‌اند):"
git status -s
git revert --abort
echo "--- با --abort به حالت قبل برگشتم:"
git status -s; echo "(خالی)"
خروجی
adc2b52 Add report task to the todo list
2231b5f Add report task
78dc659 Add milk to the list
--- خنثی‌کردن commit ای که دو commit قبل است:
Auto-merging todo.txt
CONFLICT (content): Merge conflict in todo.txt
error: could not revert 2231b5f... Add report task
--- وضعیت (UU = هر دو طرف عوض کرده‌اند):
UU todo.txt
--- با --abort به حالت قبل برگشتم:
(خالی)

CONFLICT (content): commit های بعدی همان بخش فایل را عوض کرده‌اند و Git نمی‌داند معکوس‌کردن را کجا و چطور اعمال کند. راه‌حل: conflict را دستی حل کن (درس بعدی)، یا با git revert --abort منصرف شو.

۵) استفاده از دستور قدیمی git checkout -- فایل

Section titled “۵) استفاده از دستور قدیمی git checkout -- فایل”

قبل از Git ۲.۲۳ برای همه‌ی این کارها git checkout بود (که هم شاخه عوض می‌کرد هم فایل برمی‌گرداند و گیج‌کننده بود). حالا git restore (فایل‌ها) و git switch (شاخه‌ها) جدا شده‌اند. دستور قدیمی هنوز کار می‌کند، ولی در آموزش و کد جدید restore را بنویس.

✎ تمرینآسان

فایلی را ویرایش کن، با git diff تغییر را ببین، و بعد با git restore پوشه‌ی کاری را به آخرین commit برگردان.

دیدن جواب
Terminal window
mkdir -p ~/gitlab/ex1 && cd ~/gitlab/ex1 && git init -q
echo "نسخه‌ی خوب" > a.txt && git add a.txt && git commit -qm "Add a"
echo "نسخه‌ی خراب" > a.txt
git diff
git restore a.txt
cat a.txt
git status -s
خروجی
diff --git a/a.txt b/a.txt
index cecab49..851717b 100644
--- a/a.txt
+++ b/a.txt
@@ -1 +1 @@
-نسخه‌ی خوب
+نسخه‌ی خراب
نسخه‌ی خوب
✎ تمرینمتوسط

تمرین اصلی: سه نوع اشتباه بساز و هر کدام را با روش درست برگردان: (۱) یک فایل را add کن و پشیمان شو (بدون از دست دادن تغییرش)، (۲) پیام آخرین commit (push نشده) غلط است، (۳) یک commit قدیمی‌تر که «به اشتراک گذاشته شده» باید خنثی شود.

دیدن جواب
Terminal window
mkdir -p ~/gitlab/ex2 && cd ~/gitlab/ex2 && git init -q
echo one > f && git add f && git commit -qm "Add f"
echo bug > g && git add g && git commit -qm "Add g (has a bug)"
echo two >> f && git commit -qam "Update f"
echo "=== ۱) add اشتباهی:"
echo draft > draft.txt && git add draft.txt
git restore --staged draft.txt && git status -s && rm draft.txt
echo "=== ۲) پیام غلط:"
git commit --amend -qm "Add two to f"
git log --oneline | head -1
echo "=== ۳) خنثی‌کردن commit دوم («Add g») که به اشتراک گذاشته شده:"
git revert --no-edit HEAD~1 2>&1 | head -2
git log --oneline
ls
خروجی
=== ۱) add اشتباهی:
?? draft.txt
=== ۲) پیام غلط:
fb05b74 Add two to f
=== ۳) خنثی‌کردن commit دوم («Add g») که به اشتراک گذاشته شده:
[main d519f19] Revert "Add g (has a bug)"
Date: Sun Oct 4 08:15:33 2026 +0000
d519f19 Revert "Add g (has a bug)"
fb05b74 Add two to f
b7483db Add g (has a bug)
222a8cc Add f
f

چون commit های بعدی به فایل g دست نزده‌اند، revert بدون conflict انجام شد و فایل g رفت، بدون اینکه تاریخچه بازنویسی شود.

✎ تمرینسخت

یک commit بزرگ زده‌ای که دو کار نامرتبط را با هم دارد (a.txt و b.txt). آن را بدون از دست دادن تغییرها به دو commit جدا تقسیم کن. (راهنما: reset --mixed HEAD~1 و بعد دو بار add و commit.)

دیدن جواب
Terminal window
mkdir -p ~/gitlab/ex3 && cd ~/gitlab/ex3 && git init -q
echo base > base.txt && git add base.txt && git commit -qm "Base"
echo a > a.txt && echo b > b.txt
git add . && git commit -qm "Do two things at once"
echo "--- قبل: یک commit بزرگ:"
git show --stat --format='%h %s' HEAD | head -4
git reset -q --mixed HEAD~1
git add a.txt && git commit -qm "Add a"
git add b.txt && git commit -qm "Add b"
echo "--- بعد: دو commit جدا:"
git log --oneline
خروجی
--- قبل: یک commit بزرگ:
014a4ea Do two things at once
a.txt | 1 +
b.txt | 1 +
--- بعد: دو commit جدا:
02a5946 Add b
2f67a88 Add a
8072019 Base
⚡ بررسی سریع

یک commit را push کرده‌ای و دیگران آن را گرفته‌اند. حالا می‌فهمی اشتباه بوده. کدام روش امن است؟

؟ آزمونک
  1. git restore --staged فایل چه می‌کند؟

  2. تفاوت reset --soft و reset --hard؟

  3. commit --amend چه چیزی می‌سازد؟

  4. بعد از «git reset --hard HEAD~2»، commit های کنار گذاشته‌شده را چطور برمی‌گردانی؟

  5. چرا git clean بدون -n خطرناک است؟

  • ابتدا بپرس: اشتباه کجاست (کاری، staging، commit) و آیا push شده؟
  • git restore فایل پوشه‌ی کاری را برمی‌گرداند (⚠ تغییر commit نشده نابود می‌شود)؛ --staged فقط از staging درمی‌آورد (امن)؛ --source=commit از commit دلخواه.
  • git commit --amend آخرین commit را با یک commit جدید جایگزین می‌کند (فقط قبل از push).
  • git reset: --soft (فقط شاخه)، --mixed (شاخه + staging)، --hard (همه ⚠)؛ تاریخچه را بازنویسی می‌کند، فقط برای کار خصوصی.
  • git revert commit یک commit جدید می‌سازد که اثر قبلی را خنثی می‌کند؛ برای تاریخچه‌ی مشترک امن است. برای merge: -m 1.
  • git reflog تور نجات است برای commit های گم‌شده؛ ولی تغییر commit نشده را نه. git clean -n را قبل از -fd بزن.
  • بعد از push: reset/amend + push ← non-fast-forward؛ راه امن revert.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
git restore فایلپوشه‌ی کاری را از آخرین commit برگردان ⚠
git restore --staged فایلاز staging بیرون بیاور
git restore --source=HEAD~2 فایلفایل را از دو commit قبل بیاور
git commit --amend -m "پیام"پیام آخرین commit را عوض کن
git add فایل && git commit --amend --no-editفایل فراموش‌شده را به آخرین commit اضافه کن
git reset --soft HEAD~1commit را بردار، تغییرها staged
git reset HEAD~1commit را بردار، تغییرها در پوشه‌ی کاری
git reset --hard HEAD~1commit و تغییرها را دور بریز ⚠
git revert commitcommit مشترک را خنثی کن (commit جدید)
git reflogجابه‌جایی‌های HEAD؛ نجات commit گم‌شده
git clean -nd / git clean -fdفایل‌های untracked: نمایش / حذف ⚠