آموزش

تفاوت git reset و git revert (و اینکه کِی سراغ کدام برویم)

یک کامیت اشتباه زده‌اید و دنبال راه برگشت هستید. قبل از اینکه دستور بعدی را کپی کنید، فقط یک چیز مهم است: آن تغییر push شده یا نه؟

علی پورملک
نویسنده
۶ دقیقه مطالعه۱۵ بازدید
اشتراک گذاری۱۵

کامیتی زده‌اید که نباید می‌زدید. یا فایلی را پاک کرده‌اید، یا نصف کار نیمه‌کاره را با هم فرستاده‌اید. جست‌وجو می‌کنید و اولین چیزی که پیدا می‌شود git reset --hard است — و همین دستور است که بیشترین کدِ از‌دست‌رفته را در دنیا روی وجدانش دارد.

قبل از هر دستوری، فقط یک سؤال مهم است: آن تغییر push شده یا نه؟ اگر نه، git reset کار شماست؛ اگر بله، فقط git revert. تفاوت این دو در همین یک چیز است: اولی تاریخچه را بازنویسی می‌کند، دومی رویش می‌نویسد.

اول ببینید تغییر کجاست

گیت سه جای متفاوت دارد که کار شما می‌تواند در آن باشد: فایل‌های روی دیسک (working tree)، ناحیه‌ی staging، و کامیت. git status هر سه را نشان می‌دهد و لازم است قبل از هر کاری یک بار بخوانیدش. ابزارِ برگرداندن، بر اساس همین موقعیت انتخاب می‌شود، نه بر اساس اینکه کدام دستور را بلدید.

هنوز کامیت نکرده‌اید: restore

bash# فایل را به آخرین نسخه‌ی کامیت‌شده برگردان
# هرچه ذخیره نشده از بین می‌رود و برنمی‌گردد
git restore src/app.py

# فقط از staging بیرونش بیاور؛ خود تغییرها دست‌نخورده می‌مانند
git restore --staged src/app.py

اگر جایی git checkout -- file دیده‌اید، همین کار را می‌کند؛ restore از نسخه‌ی ۲.۲۳ گیت اضافه شد تا این کار با «جابه‌جا شدن بین شاخه‌ها» قاطی نشود. سراغ فرم قدیمی نروید.

اگر نمی‌خواهید تغییرها را از دست بدهید ولی الان مزاحمتان هستند، git stash دقیقاً برای همین است.

کامیت کرده‌اید ولی push نکرده‌اید: reset

اینجا تاریخچه فقط مال خودتان است، پس بازنویسی‌اش هیچ ایرادی ندارد:

bash# کامیت آخر را باز کن، تغییرها را staged نگه دار
# برای وقتی که می‌خواهید همان کار را دوباره و درست کامیت کنید
git reset --soft HEAD~1

# کامیت را باز کن، تغییرها را در فایل‌ها نگه دار (حالت پیش‌فرض)
git reset HEAD~1

# کامیت و تغییرهایش را با هم پاک کن
git reset --hard HEAD~1

تفاوت این سه فقط در این است که کارتان تا کجا عقب می‌آید: --soft تا staging، پیش‌فرض تا فایل‌ها، --hard تا هیچ‌جا.

و خیلی وقت‌ها به هیچ‌کدام نیاز ندارید. اگر مشکل فقط متن پیام کامیت یا یک فایل جاافتاده است:

bashgit add فایل_جاافتاده.py
git commit --amend

push کرده‌اید: فقط revert

revert تاریخچه را دست نمی‌زند؛ یک کامیت جدید می‌سازد که اثر کامیت قبلی را برمی‌گرداند:

bashgit revert 4f2a9c1

# چند کامیت پشت سر هم، همه در یک کامیت برگشت
# دقت کنید به ^ — بدون آن، خودِ 4f2a9c1 برنمی‌گردد
git revert --no-commit 4f2a9c1^..HEAD
git commit -m "برگرداندن تغییرهای ناقص فرم ثبت‌نام"

آن ^ اشتباه رایجی است که مردم را گیج می‌کند: در گیت، بازه‌ی A..B یعنی «هرچه در B هست و در A نیست» — یعنی خودِ A بیرون از بازه می‌ماند. اگر می‌خواهید همان کامیت هم برگردد، A^ را شروع بازه بگذارید.

روی شاخه‌ی مشترک، reset --hard و بعد push --force نزنید. تاریخچه‌ای که بازنویسی می‌کنید مال شما نیست — همکارتان که همان کامیت‌ها را دارد، در git pull بعدی با تاریخچه‌ای مواجه می‌شود که دیگر وجود ندارد، و کار خودش را روی آن دوباره می‌سازد. اگر روزی واقعاً مجبور شدید تاریخچه‌ی مشترک را عوض کنید، حداقل --force-with-lease بزنید تا اگر کسی چیزی push کرده باشد، دستور شما رد شود.

اگر با reset --hard چیزی از دست دادید: reflog

هر جابه‌جایی HEAD در reflog ثبت می‌شود، حتی وقتی کامیت از تاریخچه بیرون افتاده:

bashgit reflog
# a3c1f8e HEAD@{0}: reset: moving to HEAD~1
# 4f2a9c1 HEAD@{1}: commit: افزودن اعتبارسنجی فرم

git reset --hard 4f2a9c1   # برگشت به همان نقطه

یک نکته‌ی صادقانه: reflog فقط چیزی را نجات می‌دهد که کامیت شده باشد.

ولی یک شانس دوم هم هست. هر چیزی که حتی یک بار git add شده، به‌عنوان یک شیء در دیتابیس گیت نوشته شده و با --hard هم پاک نمی‌شود:

bashgit fsck --lost-found
# dangling blob fd783bc2146f42436a411d0d2bf274234a7697d3

git cat-file -p fd783bc   # محتوای همان فایل، دست‌نخورده

اگر تغییری هیچ‌وقت add نشده باشد، این هم کمکی نمی‌کند. تفاوتِ «قابل بازیابی» و «رفت» دقیقاً همان یک بار git add است.

کدام دستور، در کدام وضعیت

وضعیت دستور
تغییر ذخیره‌نشده در فایل git restore <file>
فقط از staging دربیاید git restore --staged <file>
پیام یا محتوای کامیت آخر (push‌نشده) git commit --amend
کامیت push‌نشده git reset --soft HEAD~1
کامیت push‌شده git revert <hash>
اشتباهاً پاکش کردم git reflog

و یک عادت که ارزشش را دارد: قبل از هر آزمایش خطرناکی، یک شاخه‌ی پشتیبان بسازید. یک اشاره‌گر ساده است، هیچ فایلی کپی نمی‌شود و تفاوتش با یک شب کارِ از‌دست‌رفته همین یک خط است:

bashgit branch backup-before-reset

اگر می‌خواهید گیت را از پایه و با تمرین عملی یاد بگیرید — شاخه، merge، حل تعارض و کار تیمی روی گیت‌هاب — دوره‌ی گیت و گیت‌هاب همین مسیر را قدم‌به‌قدم می‌رود.

علی پورملک
نویسنده

نظرها

اولین نفری باشید که نظر می‌دهد
برای ثبت نظر وارد شوید

سؤالتان را بپرسید یا تجربه‌تان را بنویسید — نویسنده‌ی مطلب پاسخ می‌دهد.

مطالب مرتبط

از همین دسته
آموزش

چرا Rust به‌جای segfault، خطای کامپایل می‌دهد

در C یا ++C یک اشاره‌گر نامعتبر معمولاً ماه‌ها بعد، در پروداکشن، خودش را نشان می‌دهد. در Rust همان اشتباه، همان لحظه‌ی کامپایل متوقفت می‌کند.

تحریریه‌ی دینا کد۵ دقیقه