دليل شامل لأمر Git Reset: استعادة التغييرات وإدارة سجل المشروع بكفاءة

دقائق القراءة: 9

هل يبدو هذا مألوفًا لك؟ ‘مساعدة! لقد قمت بعمل commit على الفرع الخاطئ!’، ‘حدث ذلك مرة أخرى… أين commit الخاص بي؟’ حسنًا، لقد مررت بهذه المواقف مرات عديدة. كثيرًا ما يتم استدعائي للمساعدة عندما يحدث خطأ ما في Git. ولم يحدث هذا فقط عندما كنت أقوم بتدريس الطلاب، بل أيضًا أثناء العمل مع مطورين ذوي خبرة. بمرور الوقت، أصبحت نوعًا ما ‘خبير Git‘.

نحن نستخدم Git طوال الوقت، وعادة ما يساعدنا في إنجاز العمل بكفاءة. ولكن في بعض الأحيان، وبشكل أكثر تكرارًا مما نود، تسوء الأمور. ربما قمنا بعمل commit على الفرع الخاطئ، أو ربما فقدنا بعض الأكواد التي كتبناها، أو ربما قمنا بعمل commit لشيء لم نكن نقصده.

صورة كاريكاتورية تظهر مطورًا يواجه مشكلة في Git ويسأل عن كيفية التراجع عن التغييرات، مما يعكس الارتباك الشائع حول Git Reset.

توجد العديد من المصادر المتاحة عبر الإنترنت حول Git، وبعضها يركز بالفعل على ما يحدث في هذه السيناريوهات غير المرغوبة. لكنني دائمًا ما شعرت أن هذه المصادر تفتقر إلى شرح ‘السبب’. عند تزويدنا بمجموعة من الأوامر، ما الذي يفعله كل أمر بالضبط؟ وكيف وصلنا إلى هذه الأوامر في المقام الأول؟

في منشور سابق، قدمت مقدمة مرئية عن آليات Git الداخلية. وبينما يعد فهم آليات Git الداخلية مفيدًا، فإن مجرد الحصول على الجانب النظري لا يكفي أبدًا. كيف نطبق معرفتنا بآليات Git الداخلية ونستخدمها لإصلاح المشكلات التي وقعنا فيها؟

في هذا المقال، أود سد هذه الفجوة والتوسع في شرح أمر git reset. سنتعمق في فهم ما يفعله git reset خلف الكواليس، ثم نطبق هذه المعرفة لحل سيناريوهات مختلفة.

الأسس المشتركة: دليل العمل، منطقة التجهيز، والمستودع

لفهم الآليات الداخلية لأمر git reset، من الضروري استيعاب عملية تسجيل التغييرات داخل Git. وبشكل خاص، أعني هنا دليل العمل (working dir)، ومنطقة التجهيز (index)، والمستودع (repository). إذا كنت واثقًا من فهمك لهذه المصطلحات، فلا تتردد في الانتقال إلى القسم التالي.

عندما نعمل على شيفرتنا المصدرية، فإننا نعمل من دليل عمل (working dir) — وهو أي دليل على نظام ملفاتنا مرتبط بمستودع Git. يحتوي هذا الدليل على مجلدات وملفات مشروعنا، بالإضافة إلى دليل مخفي يسمى .git.

بعد إجراء بعض التغييرات، نرغب في تسجيلها في مستودعنا (repository). المستودع هو مجموعة من الالتزامات (commits)، يمثل كل منها أرشيفًا لشكل شجرة عمل المشروع في تاريخ سابق، سواء على جهازنا أو جهاز شخص آخر.

رسم بياني يوضح العلاقة بين دليل العمل (Working Directory) ومنطقة التجهيز (Staging Area) والمستودع (Repository) في Git.

دعنا نقم بإنشاء ملف في دليل العمل (working dir) وننفذ الأمر git status:

لقطة شاشة لناتج أمر git status بعد إنشاء ملف جديد، تظهر الملف كـ untracked file.

مع ذلك، لا يقوم Git بعمل commit للتغييرات من شجرة العمل (working tree) مباشرة إلى المستودع (repository). بدلاً من ذلك، يتم تسجيل التغييرات أولاً في ما يسمى بمنطقة التجهيز (index)، أو منطقة التدريج (staging area). يشير كلا المصطلحين إلى نفس الشيء، ويتم استخدامهما غالبًا في وثائق Git. سنستخدم هذه المصطلحات بالتبادل في هذا المقال.

عندما نستخدم الأمر git add، فإننا نضيف الملفات (أو التغييرات داخل الملفات) إلى منطقة التجهيز (staging area). دعنا نستخدم هذا الأمر على الملف الذي أنشأناه سابقًا:

لقطة شاشة لناتج أمر git status بعد إضافة ملف جديد إلى منطقة التجهيز باستخدام git add، يظهر الملف كـ Changes to be committed.

كما يكشف الأمر git status، فإن ملفنا أصبح في منطقة التجهيز (staged) (وجاهزًا ‘للالتزام’). ومع ذلك، فهو ليس جزءًا من أي commit بعد. بعبارة أخرى، هو الآن موجود في دليل العمل (working dir)، وكذلك في منطقة التجهيز (index)، لكنه ليس في المستودع (repository).

رسم بياني يوضح أن الملف موجود في دليل العمل ومنطقة التجهيز، لكنه لم يتم إضافته بعد إلى المستودع.

بعد ذلك، عندما نستخدم الأمر git commit، فإننا ننشئ commit بناءً على حالة منطقة التجهيز (index). لذا، فإن الـ commit الجديد (commit 3 في المثال أدناه) سيتضمن الملف الذي أضيف إلى منطقة التجهيز (index) مسبقًا.

رسم بياني يوضح عملية إنشاء commit جديد بناءً على محتوى منطقة التجهيز، وربط الفرع الحالي به.

بمعنى آخر، يصبح لدليل العمل (working dir) نفس الحالة تمامًا مثل منطقة التجهيز (index) والمستودع (repository). كما يجعل الأمر git commit الفرع الحالي (master) يشير إلى كائن الـ commit الذي تم إنشاؤه حديثًا.

رسم بياني يوضح أن دليل العمل ومنطقة التجهيز والمستودع جميعها متطابقة بعد عملية الـ commit، وأن الفرع master يشير إلى أحدث commit.

الآليات الداخلية لأمر Git Reset

أحب أن أفكر في أمر git reset كأمر يعكس العملية الموصوفة أعلاه (إدخال تغيير إلى دليل العمل working dir، إضافته إلى منطقة التجهيز index، ثم عمل commit له إلى المستودع repository).

يمتلك أمر git reset ثلاثة أوضاع تشغيل رئيسية: --soft، --mixed، أو --hard. يمكننا تصورها كمراحل ثلاث متتالية، حيث كل مرحلة تشمل تأثيرات المرحلة السابقة وتضيف عليها:

  • المرحلة 1: تحديث مؤشر HEAD فقط — git reset --soft
  • المرحلة 2: تحديث مؤشر HEAD ومنطقة التجهيز (index) — git reset --mixed
  • المرحلة 3: تحديث مؤشر HEAD ومنطقة التجهيز ودليل العمل (working dir) — git reset --hard

المرحلة الأولى: تحديث مؤشر HEAD فقط (git reset –soft)

أولاً، يقوم أمر git reset بتحديث ما يشير إليه مؤشر HEAD. على سبيل المثال، عند استخدام git reset --soft HEAD~1، سيتم نقل ما يشير إليه HEAD (في مثالنا أعلاه، الفرع master) إلى الـ commit السابق مباشرة (HEAD~1).

إذا تم استخدام العلامة --soft، يتوقف git reset عند هذه النقطة. بالاستمرار في مثالنا، سيشير HEAD الآن إلى commit 2، وبالتالي لن يكون الملف new_file.txt جزءًا من شجرة الـ commit الحالي. ومع ذلك، سيظل جزءًا من منطقة التجهيز (index) ودليل العمل (working dir).

رسم بياني يوضح تأثير git reset --soft، حيث يتم تحريك مؤشر HEAD إلى commit سابق بينما يبقى الملف في منطقة التجهيز ودليل العمل.

بالنظر إلى ناتج أمر git status، يمكننا أن نرى أن الملف بالفعل في منطقة التجهيز (staged) ولكنه لم يتم عمل commit له بعد:

لقطة شاشة لناتج أمر git status بعد استخدام git reset --soft، تظهر الملف كـ Changes to be committed.

بمعنى آخر، لقد عكسنا العملية إلى المرحلة التي استخدمنا فيها الأمر git add، ولكننا لم نستخدم الأمر git commit بعد.

المرحلة الثانية: تحديث مؤشر HEAD ومنطقة التجهيز (git reset –mixed)

إذا استخدمنا الأمر git reset --mixed HEAD~1، فلن يتوقف Git بعد تحديث ما يشير إليه HEAD (الفرع master) إلى HEAD~1. بل سيقوم أيضًا بتحديث منطقة التجهيز (index) لتتطابق مع حالة الـ commit الذي يشير إليه HEAD (والذي تم تحديثه بالفعل). في مثالنا، هذا يعني أن منطقة التجهيز (index) ستكون لها نفس حالة commit 2:

رسم بياني يوضح تأثير git reset --mixed، حيث يتم تحريك مؤشر HEAD ومنطقة التجهيز إلى commit سابق، بينما يبقى الملف في دليل العمل.

وهكذا، لقد عكسنا العملية إلى المرحلة التي سبقت استخدام الأمر git add — الملف الذي تم إنشاؤه حديثًا أصبح الآن جزءًا من دليل العمل (working dir)، ولكن منطقة التجهيز (index) والمستودع (repository) لا يحتويان عليه.

رسم بياني يوضح أن الملف موجود في دليل العمل فقط، بينما منطقة التجهيز والمستودع لا تحتويان عليه بعد استخدام git reset --mixed.

المرحلة الثالثة: تحديث مؤشر HEAD ومنطقة التجهيز ودليل العمل (git reset –hard)

باستخدام الأمر git reset --hard HEAD~1، بعد تحديث ما يشير إليه HEAD (الفرع master) إلى HEAD~1، بالإضافة إلى تحديث منطقة التجهيز (index) لتتطابق مع حالة HEAD (الذي تم تحديثه بالفعل)، سيستمر Git في تحديث دليل العمل (working dir) ليبدو مطابقًا لمنطقة التجهيز (index). في مثالنا، هذا يعني أن دليل العمل (working dir) سيحتوي على نفس حالة منطقة التجهيز (index)، والتي بدورها تحتوي بالفعل على نفس حالة commit 2:

رسم بياني يوضح تأثير git reset --hard، حيث يتم تحريك مؤشر HEAD ومنطقة التجهيز ودليل العمل جميعها إلى commit سابق، مما يؤدي إلى فقدان التغييرات غير الملتزم بها.

في الواقع، لقد عكسنا العملية برمتها إلى ما قبل إنشاء الملف my_file.txt.

تطبيق المعرفة في سيناريوهات واقعية

الآن بعد أن فهمنا كيفية عمل أمر git reset، دعنا نطبق هذه المعرفة لإنقاذ يومنا!

1. خطأ! لقد قمت بعمل commit لشيء بالخطأ.

دعنا نأخذ السيناريو التالي في الاعتبار. لقد أنشأنا ملفًا يحتوي على السلسلة النصية This is very importnt، ثم أضفناه إلى منطقة التجهيز وقمنا بعمل commit له.

لقطة شاشة لناتج الأوامر التي تنشئ ملفًا، تضيفه إلى منطقة التجهيز، وتقوم بعمل commit له.

ثم… يا للهول! أدركنا أن لدينا خطأ إملائيًا. حسنًا، الآن نعرف أنه يمكننا حل ذلك بسهولة. يمكننا التراجع عن آخر commit لنا، وإعادة الملف إلى دليل العمل (working dir) باستخدام الأمر git reset --mixed HEAD~1. الآن، يمكننا تعديل محتوى ملفنا، إضافته إلى منطقة التجهيز، وعمل commit له مرة أخرى.

نصيحة: في هذه الحالة بالذات، كان بإمكاننا أيضًا استخدام الأمر git commit --amend لتعديل الـ commit الأخير مباشرة.

2. خطأ! لقد قمت بعمل commit على فرع خاطئ — وأحتاجه على فرع جديد.

لقد مررنا جميعًا بهذا الموقف. قمنا ببعض العمل، ثم قمنا بعمل commit له…

رسم بياني يوضح أن commit جديد تم إجراؤه على الفرع master بالخطأ.

يا إلهي، لقد قمنا بعمل commit على الفرع master، بينما كان يجب علينا إنشاء فرع جديد ثم إصدار طلب سحب (pull request). في هذه المرحلة، أجد أنه من المفيد تصور الحالة التي نحن فيها، وإلى أين نود الوصول:

رسم بياني يوضح الحالة الحالية (commit على master) والحالة المستهدفة (commit على فرع جديد مع بقاء master على commit سابق).

في الواقع، هناك ثلاثة تغييرات بين الحالة الحالية والحالة المرغوبة. أولاً، يجب أن يشير الفرع الجديد (new branch) إلى الـ commit الذي أضفناه مؤخرًا. ثانيًا، يجب أن يشير الفرع master إلى الـ commit السابق. ثالثًا، يجب أن يشير HEAD إلى الفرع new. يمكننا الوصول إلى الحالة المرغوبة من خلال ثلاث خطوات بسيطة:

  1. أولاً، اجعل الفرع الجديد (new branch) يشير إلى الـ commit الذي أضيف مؤخرًا — يمكن تحقيق ذلك ببساطة باستخدام الأمر git branch new. وهكذا نصل إلى الحالة التالية:
    رسم بياني يوضح إنشاء فرع جديد (new) يشير إلى آخر commit.
  2. ثانيًا، اجعل الفرع master يشير إلى الـ commit السابق (بمعنى آخر، إلى HEAD~1). يمكننا القيام بذلك باستخدام الأمر git reset --hard HEAD~1. وهكذا نصل إلى الحالة التالية:
    رسم بياني يوضح تحريك الفرع master إلى commit سابق باستخدام git reset --hard.
  3. أخيرًا، نود أن نكون على الفرع new، أي أن نجعل HEAD يشير إلى new. يتم تحقيق ذلك بسهولة عن طريق تنفيذ الأمر git checkout new.

إجمالاً، الأوامر هي:

git branch new
git reset --hard HEAD~1
git checkout new

3. خطأ! لقد قمت بعمل commit على فرع خاطئ — وأحتاجه على فرع آخر موجود بالفعل.

في هذه الحالة، مررنا بنفس الخطوات كما في السيناريو السابق — قمنا ببعض العمل، ثم قمنا بعمل commit له…

رسم بياني يوضح أن commit جديد تم إجراؤه على الفرع master بالخطأ، بينما كان يجب أن يكون على فرع موجود مسبقًا.

يا إلهي، لقد قمنا بعمل commit على الفرع master، بينما كان يجب علينا عمل commit على فرع آخر موجود بالفعل. دعنا نعود إلى لوحة الرسم الخاصة بنا:

رسم بياني يوضح الحالة الحالية (commit على master) والحالة المستهدفة (نقل commit إلى فرع موجود).

مرة أخرى، يمكننا أن نرى أن هناك بعض الاختلافات هنا. أولاً، نحتاج إلى أن يكون الـ commit الأحدث على الفرع existing branch. بما أن الفرع master يشير إليه حاليًا، يمكننا ببساطة أن نطلب من Git أخذ الـ commit الأخير من الفرع master وتطبيقه على الفرع existing branch على النحو التالي:

git checkout existing  # التبديل إلى الفرع existing
git cherry-pick master # تطبيق آخر commit من الفرع master على الفرع الحالي (existing)

الآن، وصلنا إلى الحالة التالية:

رسم بياني يوضح تطبيق commit من master إلى الفرع existing باستخدام git cherry-pick.

الآن نحتاج فقط إلى جعل الفرع master يشير إلى الـ commit السابق، بدلاً من الأحدث. لذلك يمكننا:

git checkout master      # تغيير الفرع النشط إلى master مرة أخرى
git reset --hard HEAD~1  # الآن عدنا إلى الفرع الأصلي (قبل الـ commit الخاطئ)

وهكذا نكون قد وصلنا إلى حالتنا المرغوبة:

رسم بياني يوضح الحالة النهائية بعد نقل commit إلى الفرع الصحيح وإعادة master إلى حالته السابقة.

الخلاصة التقنية

في هذا المقال، استعرضنا بعمق كيفية عمل أمر git reset، ووضحنا أوضاع تشغيله الثلاثة: --soft، --mixed، و--hard. لقد رأينا كيف يؤثر كل وضع على مؤشر HEAD، منطقة التجهيز (index)، ودليل العمل (working directory)، مما يمنحنا تحكمًا دقيقًا في سجل مشروعنا.

بعد ذلك، طبقنا هذه المعرفة لحل بعض المشكلات الشائعة والواقعية التي قد تواجه المطورين عند استخدام Git، مثل التراجع عن commit خاطئ أو نقل commit إلى فرع آخر.

إن فهم الآليات الكامنة وراء git reset لا يقتصر على مجرد معرفة الأوامر، بل يمتد إلى القدرة على تحليل المشكلة واختيار الأداة المناسبة لحلها بثقة. هذا الفهم العميق هو ما يمكّن المطورين من التعامل مع جميع أنواع السيناريوهات غير المرغوبة بكفاءة، وتقدير قوة وجمال هذه الأداة التي لا غنى عنها في عالم تطوير البرمجيات. نأمل أن يكون هذا الدليل قد أضاف قيمة حقيقية لمسيرتك مع Git.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *