ابدأ من سؤال: أين الخطأ؟
أوامر التراجع في Git كثيرة لأن "التراجع" يعني أشياء مختلفة حسب المكان الذي وصل إليه الخطأ. حدّد موقفك من الجدول، ثم انتقل لقسمه:
| الموقف | الأمر |
|---|---|
| عدّلت ملفًا ولم أجهّزه، وأريد إلغاء التعديل | git restore file |
جهّزته بـ add بالخطأ | git restore --staged file |
| آخر commit: رسالته خاطئة أو نسيت فيه ملفًا | git commit --amend |
| أريد إلغاء commits لم أرفعها | git reset |
| أريد إلغاء commit رفعته للفريق | git revert |
| أريد نسخة قديمة من ملف واحد | git restore --source=<commit> file |
مسحت شيئًا بـ reset --hard | git reflog |
تعديلات لم تُحفظ: restore
git restore app.js # أعد الملف كما في آخر commit
git restore . # كل الملفات المعدّلة
⚠️ هذا الأمر يمحو التعديلات نهائيًّا: لم تُحفظ في commit ولا في stash، فلا شيء في Git يستطيع إعادتها. إن كنت مترددًا،
git stashبدلًا منه (درس stash).
إلغاء التجهيز
git add .
git restore --staged .env # أخرجه من منطقة التجهيز
--staged لا تلمس الملف نفسه، بل تلغي git add فقط: التعديل باقٍ في مجلّد العمل.
ملفات جديدة غير متتبَّعة
restore لا يعرف الملفات التي لم يتتبّعها Git أصلًا. لحذف الملفات الجديدة (مخرجات بناء، ملفات تجريبية):
git clean -n # اعرض ما سيُحذف فقط
git clean -f # احذف فعلًا
ابدأ بـ -n دائمًا: clean -f يحذف من القرص ملفات لا نسخة لها في أي مكان.
آخر commit: amend
git add forgotten.txt
git commit --amend --no-edit # أضف الملف لآخر commit دون تغيير الرسالة
git commit --amend -m "Fix login redirect" # غيّر الرسالة
--amend لا "يعدّل" الـ commit؛ يستبدله بـ commit جديد برقم جديد. لذلك ينطبق عليه ما ينطبق على reset وrebase: آمن قبل الرفع، ويحتاج --force-with-lease بعده، وممنوع على تاريخ مشترك.
إلغاء commits محلية: reset
reset يعيد مؤشّر الفرع إلى commit سابق، فتختفي الـ commits بعده من الفرع. الفرق بين أنماطه الثلاثة هو ماذا يحدث لتغييرات تلك الـ commits:
git reset --soft HEAD~1 # ألغِ آخر commit، والتغييرات تبقى مجهّزة
git reset HEAD~1 # (--mixed الافتراضي) التغييرات تبقى غير مجهّزة
git reset --hard HEAD~1 # ألغِ الـ commit وتغييراته كليًّا
| النمط | مؤشّر الفرع | منطقة التجهيز | ملفاتك | الاستخدام |
|---|---|---|---|---|
--soft | يرجع | كما هي (مجهّزة) | كما هي | أعد كتابة الـ commit برسالة أو تقسيم أفضل |
--mixed | يرجع | تُفرَّغ | كما هي | أعد التفكير فيما يدخل كل commit |
--hard | يرجع | تُفرَّغ | تعود للقديم | تخلَّ عن العمل تمامًا |
HEAD~1 تعني "الـ commit الذي قبل الحالي"، وHEAD~3 ثلاثة للخلف. ويمكنك كتابة رقم commit مباشرة: git reset --hard a8b6e0e.
--soft و--mixed آمنان على عملك: لا يضيع شيء، فقط يُعاد تنظيمه. --hard هو الوحيد الذي يمحو، ويمحو معه أي تعديلات غير محفوظة كانت في مجلّد العمل، وتلك لا تُستعاد أبدًا.
إلغاء commit مرفوع: revert
إن كان الـ commit في main على GitHub وسحبه زملاؤك، فحذفه بـ reset يعني أن تاريخك صار مختلفًا عن تاريخهم، ويتطلّب رفعًا بالقوّة يكسر نسخهم. revert لا يحذف شيئًا، بل يضيف commit جديدًا يعكس التغيير:
git revert 76dcdc3
[main 5b33972] Revert "Break app"
5b33972 Revert "Break app" ← جديد: يعيد السطور كما كانت
76dcdc3 Break app ← ما زال في التاريخ
de4c8f2 Update app
التاريخ يقول الحقيقة: "أضفنا هذا، ثم تراجعنا عنه"، ويُرفع بـ git push عادي. بلا --no-edit، يفتح Git المحرّر لتكتب لماذا تراجعت، وهي معلومة تستحق التسجيل.
التراجع عن commit دمج
git revert <merge-hash>
error: commit 4541c92... is a merge but no -m option was given.
commit الدمج له أبوان، وGit يسأل: إلى أي خطّ أعود؟ -m 1 تعني "الأب الأول"، أي الفرع الذي دُمج فيه (main غالبًا)، فيُلغى ما جاء من الفرع المدموج:
git revert -m 1 <merge-hash>
نسخة قديمة من ملف واحد
git restore --source=HEAD~2 app.js # app.js كما كان قبل commitين
git restore --source=v1.0.0 config.json # كما كان في إصدار
يغيّر الملف في مجلّد العمل فقط، فتراجعه ثم تعمل commit إن أعجبك. بقية المشروع لا يتأثّر.
الخلاص الأخير: reflog
نفّذت reset --hard وأدركت أنك تحتاج الـ commit؟ ما دام قد حُفظ في commit يومًا، فهو غالبًا ما زال موجودًا:
git reflog
de4c8f2 HEAD@{0}: reset: moving to HEAD~1
76dcdc3 HEAD@{1}: commit: Break app
git reset --hard HEAD@{1} # عُد إلى ما قبل reset
reflog سجلّ محلي لكل مكان وقف فيه HEAD. التفاصيل ومدّة الاحتفاظ في درس reflog.
reset أم revert؟
- لم ترفع بعد:
reset(أو--amendلآخر commit). التاريخ ما زال ملكك وحدك. - رفعت إلى فرع يستخدمه غيرك:
revertدائمًا. - رفعت إلى فرع PR شخصي لا يبني عليه أحد:
resetثمpush --force-with-leaseمقبول.
أخطاء شائعة
reset --hardلإلغاء commit مع وجود تعديلات غير محفوظة: تضيع التعديلات معه. افحصgit statusأولًا، أو استخدم--soft.resetعلى commit مرفوع ثمpush --forceعلىmain: يكسر نسخ الفريق. استخدمrevert.git clean -fبلا-nقبلها.--amendبعد الرفع وانتظار أن يعملgit pushالعادي: يُرفض، لأن الـ commit استُبدل.git checkout fileالقديمة في السكربتات: تعمل، لكنها تمحو التعديلات بلا تحذير مثلrestore.restoreأوضح في النيّة.
🎯 التالي: انتقاء الـ commit (Cherry-pick).