اصلاح اشتباهات – reset، revert، reflog
همه ما اشتباه میکنیم - فایل اشتباه commit کردیم، پیام را اشتباه نوشتیم، یا یک commit بد ساختیم. خوشبختانه Git ابزارهای قدرتمندی برای اصلاح دارد: reset، revert، reflog، checkout و cherry-pick.
همه ما اشتباه میکنیم – فایل اشتباه commit کردیم، پیام را اشتباه نوشتیم، یا یک commit بد ساختیم. خوشبختانه Git ابزارهای قدرتمندی برای اصلاح دارد: reset، revert، reflog، checkout و cherry-pick.
دستهبندی اصلاحها
| وضعیت تغییر | دستور |
|---|---|
| تغییر uncommitted در Working Directory | git restore |
| تغییر staged ولی هنوز commit نشده | git restore --staged |
| commit شده ولی push نشده | git reset یا git commit --amend |
| commit شده و push شده | git revert (امن) یا git push --force (خطرناک) |
| کلاً پروژه را خراب کردم! | git reflog + git reset |
git restore – برگرداندن فایلها
برگرداندن تغییرات Working Directory
# تغییرات یک فایل را به آخرین commit برگردان
git restore file.py
# همه فایلها
git restore .
# فقط زیرپوشه خاص
git restore src/
# دستور قدیمی (هنوز کار میکند)
git checkout -- file.py
Unstage کردن (خارج کردن از staging)
# فایلی را که add کردهاید، از staging خارج کنید
git restore --staged file.py
# همه را
git restore --staged .
# دستور قدیمی
git reset HEAD file.py
برگرداندن به یک commit خاص
# برگرداندن file.py به یک commit قدیمی
git restore --source=a3f4b2c file.py
# یا
git checkout a3f4b2c -- file.py
git commit –amend – تصحیح آخرین commit
تغییر پیام آخرین commit
# Editor باز میشود
git commit --amend
# یا مستقیم
git commit --amend -m "پیام جدید"
اضافه کردن فایل فراموششده
# اوه، forgotten.py را add نکردم
git add forgotten.py
git commit --amend --no-edit # بدون تغییر پیام
تغییر نویسنده
git commit --amend --author="Real Name <real@email.com>"
git reset – بازنویسی تاریخچه
سه نوع reset وجود دارد:
| نوع | HEAD | Staging | Working Dir | کاربرد |
|---|---|---|---|---|
--soft |
عقب میرود | دستنخورده | دستنخورده | commit دوباره با تغییرات |
--mixed (پیشفرض) |
عقب میرود | reset | دستنخورده | تغییرات میمانند ولی unstaged |
--hard |
عقب میرود | reset | پاک میشود! | پاک کردن کامل (خطرناک) |
git reset –soft
# آخرین commit را undo کن، اما تغییرات را در staging نگه دار
git reset --soft HEAD~1
# مفید: میخواهید commit را دوباره بسازید با پیام/فایلهای متفاوت
git commit -m "پیام جدید"
git reset –mixed (یا فقط reset)
# آخرین commit را undo، تغییرات unstaged میشوند
git reset HEAD~1
git reset --mixed HEAD~1 # معادل
# حالا git status تغییرات را modified نشان میدهد
git add . # دوباره stage
git commit -m "پیام بهتر"
git reset –hard
# خطرناک! همه چیز را به آخرین commit (یا commit مشخص) برگردان
git reset --hard HEAD~1 # یک commit عقب
git reset --hard a3f4b2c # به commit خاص
# تغییرات uncommitted از دست میروند!
# تغییرات commit شده اخیر هم از دست میروند!
--hard فایلهای uncommitted را پاک میکند بدون امکان بازگشت. قبلش حتماً git status بزنید.
سناریو: لغو چند commit
# تاریخچه:
# C1 ── C2 ── C3 ── C4 (HEAD)
# میخواهیم به C2 برگردیم
git reset --hard HEAD~2 # 2 commit عقب
# C1 ── C2 (HEAD)
# C3 و C4 ناپدید شدند (تا زمانی که garbage collected شوند)
git revert – لغو امن
برخلاف reset که تاریخچه را بازنویسی میکند، revert یک commit جدید میسازد که تغییرات یک commit قبلی را لغو میکند.
# commit C3 را undo کن (یعنی commit جدید بساز که برعکس C3 است)
git revert C3-hash
# تاریخچه:
# C1 ── C2 ── C3 ── C4 ── C5_revert_of_C3 (HEAD)
# تاریخچه حفظ میشود، اما تغییرات C3 خنثی شده
این روش امن برای commitهای push شده است:
# ساده
git revert HEAD # آخرین commit را revert کن
git revert HEAD~3 # سه commit قبل
# revert چند commit
git revert HEAD~3..HEAD # سه commit آخر
# بدون open شدن editor (پیشفرض پیام را قبول کن)
git revert --no-edit HEAD
# revert بدون commit (در staging بماند)
git revert -n HEAD
revert vs reset
قبل: A ── B ── C ── D ←── main
reset --hard B:
A ── B ←── main
(C و D حذف شدند)
revert C:
A ── B ── C ── D ── E (revert C) ←── main
(تاریخچه حفظ، تغییر C خنثی)
git reflog – نجاتدهنده!
Git یک log داخلی از همه حرکات HEAD نگه میدارد. حتی اگر reset --hard زده باشید، میتوانید پیدا کنید!
git reflog
# a3f4b2c HEAD@{0}: commit: Add feature
# b2c3d4e HEAD@{1}: reset: moving to HEAD~2
# c4d5e6f HEAD@{2}: commit: Important work !!!
# d5e6f7a HEAD@{3}: commit: Yesterday's work
سناریو: اشتباهاً hard reset زدم
# کارهای امروز را با reset --hard پاک کردم!
git reflog
# میبینم HEAD@{2} commit "Important work !!!" بود
# بازگشت
git reset --hard HEAD@{2}
# یا
git reset --hard c4d5e6f # hash از reflog
reflog یک branch خاص
git reflog show feature-branch
git checkout – بازگشت به نسخه قدیم
دیدن یک فایل در نسخه قدیم
# فقط نمایش، بدون تغییر
git show a3f4b2c:src/main.py
# گرفتن نسخه قدیم به working directory
git checkout a3f4b2c -- src/main.py
# حالا main.py نسخه a3f4b2c است
# سپس commit کنید
git add src/main.py
git commit -m "Restore old version of main.py"
Detached HEAD – رفتن به یک commit قدیم
git checkout a3f4b2c
# You are in 'detached HEAD' state...
# میتوانید نگاه کنید، تست کنید
# اما commitهای جدید به branch تعلق ندارند
# برای بازگشت
git switch main
ساخت branch از یک commit قدیم
# شاید بخواهید از یک commit قدیم branch جدید بسازید
git checkout -b old-version a3f4b2c
# یا با switch
git switch -c old-version a3f4b2c
بازیابی Branch حذفشده
# branch را اشتباهاً حذف کردید
git branch -D feature-x
# پیدا کردن آخرین commit branch در reflog
git reflog
# a3f4b2c HEAD@{5}: checkout: moving from feature-x to main
# ساخت دوباره branch
git branch feature-x a3f4b2c
# یا
git switch -c feature-x a3f4b2c
git cherry-pick – دوباره
یادآوری: cherry-pick یک commit خاص از branch دیگر را به branch فعلی منتقل میکند:
# من یک bug fix در branch feature دارم که الان در main لازم است
git switch main
git cherry-pick a3f4b2c
# Conflict؟
git status # ببین کدام فایل
# حل کن
git add resolved-file.py
git cherry-pick --continue
# لغو
git cherry-pick --abort
cherry-pick بدون commit
# فقط تغییرات را stage کن، خودت commit بساز
git cherry-pick -n a3f4b2c
# حالا میتوانید تغییرات را ویرایش کنید
git add modified.py
git commit -m "Cherry-pick with modifications"
بازیابی فایل پاکشده
# اگر هنوز commit نکردید
git restore deleted-file.py
# اگر commit شده اما هنوز در تاریخچه است
git log -- deleted-file.py # پیدا کردن آخرین commit
git checkout HEAD~1 -- deleted-file.py
# یا
git checkout a3f4b2c -- deleted-file.py
git clean – حذف فایلهای untracked
# Dry run - فقط نشان بده چه چیزی حذف خواهد شد
git clean -n
# حذف فایلهای untracked (نه پوشهها)
git clean -f
# حذف فایلها و پوشهها
git clean -fd
# حذف فایلهای untracked + ignored (مثل node_modules)
git clean -fdx
# با تأیید interactive
git clean -fi
git clean فایلها را برای همیشه پاک میکند. اول -n بزنید.
حذف فایل از کل تاریخچه
سناریو: secret یا فایل بزرگی را اشتباهاً push کردهاید و باید از کل تاریخچه حذف شود.
روش ۱: BFG Repo-Cleaner (پیشنهاد – سریعتر)
# دانلود از https://rtyley.github.io/bfg-repo-cleaner/
# clone با --mirror
git clone --mirror git@github.com:USER/repo.git
cd repo.git
# حذف یک فایل
java -jar bfg.jar --delete-files "secret.env" .
# حذف فایلهای > 100MB
java -jar bfg.jar --strip-blobs-bigger-than 100M .
# پاکسازی
git reflog expire --expire=now --all
git gc --prune=now --aggressive
# push (همه را force میکند)
git push
روش ۲: git filter-branch (داخلی Git، اما کند)
git filter-branch --force --index-filter
"git rm --cached --ignore-unmatch secret.env"
--prune-empty --tag-name-filter cat -- --all
git push origin --force --all
git bisect – یافتن commit معیوب
سناریو: کد در یک نقطه کار میکرد، الان نمیکند. نمیدانیم کدام commit آن را خراب کرده. bisect با binary search کمک میکند:
# شروع
git bisect start
# commit فعلی bad است
git bisect bad
# commit قدیمی (که میدانیم کار میکرد) good است
git bisect good a3f4b2c
# Git نصف میکند و به یک commit میرود
# Bisecting: 50 revisions left to test after this (roughly 6 steps)
# تست کنید (اجرا، چک)
# اگر کار میکند:
git bisect good
# اگر خراب است:
git bisect bad
# Git ادامه میدهد تا commit مقصر را پیدا کند
# پایان
git bisect reset
اتوماسیون bisect
# اگر اسکریپت تست داریم
git bisect start HEAD a3f4b2c
git bisect run npm test
# Git خودکار بین commitها bisect میکند تا جایی که تست fail شود
git fsck – بررسی سلامت
# بررسی integrity
git fsck
# پیدا کردن objectهای "گمشده" (مثلاً بعد از reset --hard)
git fsck --lost-found
# نتیجه: dangling commitها
# اینها commitهایی هستند که هیچ branch به آنها اشاره نمیکند
# میتوانید آنها را با reset بازیابی کنید
سناریوهای عملی
۱. اشتباهاً به branch اشتباه commit کردم
# روی main کار کردم، باید روی feature میکردم
git log --oneline # دیدن commit
git reset HEAD~1 # undo (تغییرات میمانند)
git switch -c feature # یا برو به feature موجود
git add .
git commit -m "..."
۲. پیام آخرین commit اشتباه است
git commit --amend -m "پیام درست"
۳. یک فایل را اشتباهاً پاک کردم
git restore deleted-file.py
۴. فقط تغییرات یک فایل را undo کنم
git restore file.py
# فقط این فایل به آخرین commit برمیگردد
۵. اشتباهاً commit کردم با فایل بزرگ
# اگر هنوز push نکردهام
git reset HEAD~1
# .gitignore را آپدیت کن
echo "huge-file.zip" >> .gitignore
git add .gitignore
git commit -m "..."
# اگر push کردهام، با BFG/filter-branch
۶. branch دیگر merge کردم اشتباه
git reset --hard HEAD~1 # اگر هنوز push نشده
# یا
git revert -m 1 a3f4b2c # اگر push شده، revert با parent 1
۷. همه چیز را خراب کردم!
git reflog # پیدا کن آخرین حالت سالم
git reset --hard HEAD@{N} # برگرد
بهترین شیوهها
- قبل از
reset --hard، حتماًgit statusوgit stash - برای commitهای push شده، همیشه
revertبهجایreset - force-push را با
--force-with-leaseامنتر کنید reflogدوست شماست – قبل از panic، یکgit reflogبزنید- secret که اشتباهاً push شد را rotate کنید (نه فقط حذف از تاریخچه)
- push روزانه = backup روزانه
amendفقط برای commitهای local
جمعبندی
git restore: برگرداندن فایلهاgit commit --amend: تصحیح آخرین commitgit reset: بازنویسی تاریخچه (soft/mixed/hard)git revert: لغو امن با commit جدیدgit reflog: log همه حرکات HEAD – نجاتدهنده!git checkout: نگاه به نسخه قدیمیgit cherry-pick: انتخاب یک commitgit clean: حذف untrackedgit bisect: پیدا کردن commit معیوب با binary search- BFG برای حذف از تاریخچه کامل
در فصل بعد، استراتژیهای branching سازمانی – Git Flow، GitHub Flow، Trunk-Based Development.