> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# Git في العمل اليومي: الدمج والتراجع دون أن تخسر عملك
- URL: https://arabroot.io/articles/git-merge-و-rebase-و-cherry-pick/
- Published: 2026-10-05T13:30:00.000Z
- Updated: 2026-10-09T09:42:18.000Z
- Description: متى تستخدم Merge ومتى تستخدم Rebase، وكيف تنقل إصلاحاً إلى فرع الإصدار أو تسترجع عملاً ظننت أنه ضاع.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, DevOps والتشغيل, DevOps وCI/CD, الاستضافة الذاتية

بعد أن تتعلم أن تنشئ Commit وفرعاً Branch وتدفع إلى السيرفر، يبدأ العمل الحقيقي مع الفريق، فتجد نفسك أمام أسئلة لا يجيب عنها الدليل الأول: زميلك دفع قبلك فرفض السيرفر الدفع، وفرعك تأخر عن main أسبوعاً، ومنصة Gitea تسألك عند الدمج هل تريد Merge أم Rebase أم Squash، والعميل يريد إصلاحاً واحداً في الإصدار الذي يعمل عنده الآن دون الميزات التي لم تكتمل، وأحدهم نفذ `git reset --hard` فاختفى عمل يوم كامل، وآخر حذف ملف .env بعد أن دفعه وظن أن المشكلة انتهت. وكل هذه المواقف لها أوامر في Git صممت لها بالتحديد، ولكن كل أمر منها له قاعدة أمان إذا تجاوزتها خسرت عملك أو عمل زملائك.

وقد شرحنا المستودع Repository وال Commit والفروع ومنطقة التجهيز Staging Area وطريقة الدفع والسحب في دليل [إدارة النسخ وGit للمبتدئين](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D9%88-git-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، فلن نعيدها هنا، وهذا المقال يبدأ من حيث انتهى ذلك الدليل، وفي كل أمر سوف نسير على الترتيب نفسه: المشكلة التي يحلها، ثم كيف يعمل مع رسم لشكل التاريخ قبله وبعده، ثم مخرجات حقيقية من مستودع نفذنا عليه الأمثلة، ثم متى تستخدمه ومتى لا تستخدمه، ثم قاعدة الأمان. وسوف نناقش في هذا المقال ما يلي:

- الدمج Merge بنوعيه: Fast-forward والدمج الثلاثي Three-way، والخيار `--no-ff`.
- إعادة التأسيس Rebase، والقاعدة الذهبية، والأمر `git pull --rebase`، وإعادة التأسيس التفاعلية Interactive Rebase لترتيب التزاماتك قبل طلب الدمج.
- مقارنة صادقة بين Merge وRebase، والسياسات التي تتبعها الفرق، ثم الدمج مع الضغط Squash Merge.
- الانتقاء Cherry-pick لنقل إصلاح إلى فرع إصدار، والتراجع بالأوامر revert وreset وrestore، وشبكة الأمان reflog.
- الحفظ المؤقت Stash، والوسوم Tags وترقيم الإصدارات Semantic Versioning.
- التعارضات Conflicts بعمق أكثر: mergetool وrerere وكيف تتجنبها.
- العمل مع الفريق: طلبات الدمج Pull Requests وحماية الفروع وفحوص CI الإلزامية، والدفع بالقوة الآمن `--force-with-lease`.
- مقارنة قصيرة بين استراتيجيات التفرع Git Flow وGitHub Flow وTrunk-Based.
- البحث عن الالتزام الذي كسر البرنامج بالأمر `git bisect`، ثم الأخطاء الشائعة وجدول مختصر للأوامر.

## المستودع الذي سوف نجرب عليه

كل المخرجات في هذا المقال حقيقية، ونفذناها على Git 2.48.1 في مستودعات صغيرة لمتجر وهمي فيه ملف `app.py` بدوال مثل `price` و`discount`، وكل مثال يبدأ بمستودع جديد حتى لا تختلط الأمثلة، ولذلك تختلف أرقام ال Commits من مثال إلى آخر. ولكي ترى شكل التاريخ في أي لحظة، فالأمر الذي سوف يتكرر كثيراً هو:

```bash
git log --oneline --graph --all
```

حيث `--oneline` يعرض كل Commit في سطر واحد برقمه المختصر ورسالته، و`--graph` يرسم الخطوط بين ال Commits، و`--all` يعرض كل الفروع وليس الفرع الحالي فقط. أما في الرسوم فسوف نستخدم الحروف A وB وC بدلاً من الأرقام الحقيقية حتى يسهل تتبعها، والزمن فيها يجري من اليسار إلى اليمين كما في أغلب أدوات Git الرسومية.

## الدمج Merge: جمع عمل فرعين

المشكلة التي يحلها الدمج بسيطة، فأنت فتحت فرعاً اسمه `feature/discount` وأضفت فيه دالة الخصم وانتهيت منها، والآن تريد أن تصل هذه التعديلات إلى main. والدمج له شكلان، وGit يختار بينهما تلقائياً بحسب ما حدث في main أثناء عملك على الفرع.

### الدمج السريع Fast-forward: عندما لا يتحرك main

إذا لم يضف أحد أي Commit إلى main منذ أن فتحت فرعك، فإن فرعك هو main نفسه مع التزامات إضافية في آخره، ولذلك لا يحتاج Git أن ينشئ شيئاً جديداً، وإنما يحرك المؤشر main إلى الأمام حتى يصل إلى آخر Commit في فرعك، والمثال التالي يبين ذلك:

```bash
git switch main
git merge feature/discount
git log --oneline --graph
```

والمخرج سوف يكون كما يلي:

```bash
Updating 9b34bb8..d5a7086
Fast-forward
 README.md | 2 ++
 app.py    | 3 +++
 2 files changed, 5 insertions(+)

* d5a7086 Document discount
* 3b93256 Add discount
* 9b34bb8 Add README
* 944030e Add price function
```

في المخرج أعلاه لاحظ التالي:

- السطر `Updating 9b34bb8..d5a7086` يعني أن main كان يشير إلى 9b34bb8 وأصبح يشير إلى d5a7086، وهو آخر Commit في الفرع.
- الكلمة `Fast-forward` تعني أنه لم ينشأ أي Commit جديد، والتاريخ خط مستقيم لا يظهر فيه أن هذه التعديلات كانت في فرع منفصل.

### الدمج الثلاثي Three-way Merge: عندما يتقدم الفرعان

أما إذا دفع زميلك إلى main التزاماً جديداً أثناء عملك، وفي مثالنا التزاماً يضيف ملف الترخيص LICENSE، فإن الفرعين تباعدا، ولم يعد تحريك المؤشر ممكناً لأن ذلك سوف يحذف التزام زميلك من main. وهنا يقارن Git ثلاث نسخ، ولهذا سمي الدمج ثلاثياً: النسخة في آخر main، والنسخة في آخر الفرع، والنسخة في النقطة المشتركة Common Ancestor التي انفصل عندها الفرعان، فيأخذ من كل طرف ما غيره، ثم ينشئ التزام دمج Merge Commit له أبوان:

```bash
git log --oneline --graph --all
git merge -m "Merge branch 'feature/discount'" feature/discount
git log --oneline --graph
```

والمخرج سوف يكون كما يلي:

```bash
* bc01bf8 Add license
| * d5a7086 Document discount
| * 3b93256 Add discount
|/
* 9b34bb8 Add README
* 944030e Add price function

Merge made by the 'ort' strategy.
 README.md | 2 ++
 app.py    | 3 +++
 2 files changed, 5 insertions(+)

*   04b6c2a Merge branch 'feature/discount'
|\
| * d5a7086 Document discount
| * 3b93256 Add discount
* | bc01bf8 Add license
|/
* 9b34bb8 Add README
* 944030e Add price function
```

في المخرج أعلاه لاحظ التالي:

- الرسم الأول يبين التباعد: الفرع يبدأ من 9b34bb8، وmain تقدم بعده بالتزام bc01bf8.
- العبارة `the 'ort' strategy` هي اسم خوارزمية الدمج، وهي [الخوارزمية الافتراضية](https://git-scm.com/docs/merge-strategies?ref=arabroot.io) عند دمج فرع واحد منذ Git 2.34، وقد حلت محل recursive.
- الالتزام 04b6c2a هو التزام الدمج، والخطان اللذان يلتقيان عنده يعنيان أن له أبوين، والتاريخ يحفظ أن التزامي الخصم كانا في فرع مستقل ثم اندمجا في هذه النقطة بالتحديد.

### الخيار --no-ff: التزام دمج حتى لو كان الدمج السريع ممكناً

وقد يتساءل البعض: إذا كان الدمج السريع أبسط، فلماذا يطلب أحد التزام دمج لا يحتاجه؟ والإجابة أن الدمج السريع يمحو أثر الفرع، فبعد شهر لن تعرف من التاريخ أن هذين الالتزامين كانا ميزة واحدة دخلت معاً، ولن تستطيع أن تتراجع عن الميزة كلها بأمر واحد. لذلك يجبر الخيار `--no-ff` الأداة على إنشاء التزام دمج دائماً، وهذا هو الأسلوب الذي تتبعه المنصات مثل GitHub في [خيار Create a merge commit](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/about-merge-methods-on-github?ref=arabroot.io)، وقد نفذناه على المثال الأول نفسه قبل أن يتحرك main:

```bash
git merge --no-ff -m "Merge branch 'feature/discount'" feature/discount
git log --oneline --graph
```

والمخرج سوف يكون كما يلي:

```bash
Merge made by the 'ort' strategy.
 README.md | 2 ++
 app.py    | 3 +++
 2 files changed, 5 insertions(+)

*   52b99b8 Merge branch 'feature/discount'
|\
| * d5a7086 Document discount
| * 3b93256 Add discount
|/
* 9b34bb8 Add README
* 944030e Add price function
```

والصورة التالية تبين الفرق بين الشكلين:

![رسمان للتاريخ: في الأول يتقدم المؤشر main من الالتزام B إلى الالتزام D في خط مستقيم دون التزام جديد، وفي الثاني تقدم الفرعان فينشأ التزام دمج M أخضر له أبوان هما E من main وD من الفرع](https://arabroot.io/content/images/2026/10/git-workflows-01-merge.webp)

الدمج السريع يحرك المؤشر فقط، والدمج الثلاثي ينشئ التزام دمج يربط التاريخين

ومتى تستخدم كل شكل؟ الدمج السريع مناسب عندما تحدث فرعك المحلي من السيرفر، أو عندما يكون الفرع التزاماً واحداً صغيراً لا يستحق أثراً في التاريخ، والخيار `--no-ff` مناسب عندما تدمج ميزة كاملة في main وتريد أن تبقى ظاهرة كوحدة واحدة. وإذا أردت العكس، أي أن يرفض Git الدمج إذا لم يكن سريعاً بدلاً من أن ينشئ التزام دمج دون أن تنتبه، فاستخدم `git merge --ff-only`، وهذا مفيد في السكربتات وفي تحديث الفرع المحلي.

وقاعدة الأمان في الدمج أنه لا يغير أي التزام موجود، فهو يضيف فقط، لذلك فالدمج آمن دائماً على الفروع المشتركة، وإذا ندمت عليه قبل الدفع فتستطيع أن تتراجع عنه كما سيأتي في [قسم التراجع](#undo).

## إعادة التأسيس Rebase: نقل عملك فوق آخر نقطة في main

لنرجع إلى حالة التباعد نفسها: أنت في الفرع `feature/discount` ولديك التزامان، وmain تقدم بالتزام الترخيص. والدمج الثلاثي يحل المشكلة، ولكنه يضيف التزام دمج، وإذا كان الفريق يعمل على عشرة فروع في الأسبوع، وكل شخص يدمج main في فرعه كل يوم ليبقى محدثاً، فسوف يمتلئ التاريخ بالتزامات من نوع «Merge branch main into feature» لا تحمل أي معلومة، ويصبح الرسم شبكة من الخطوط يصعب أن تقرأها.

وإعادة التأسيس Rebase حل آخر: بدلاً من أن تربط التاريخين بالتزام دمج، تأخذ التزاماتك وتعيد تطبيقها واحداً بعد الآخر فوق آخر نقطة في main، كأنك فتحت الفرع اليوم وليس قبل أسبوع. والمثال التالي يبين ذلك:

```bash
git switch feature/discount
git log --oneline --graph --all
git rebase main
git log --oneline --graph --all
```

والمخرج سوف يكون كما يلي:

```bash
* 10300ca Add license
| * cfe90b2 Document discount
| * 46e4452 Add discount
|/
* d32f45b Add README
* 8567728 Add price function

Successfully rebased and updated refs/heads/feature/discount.

* 0e06f59 Document discount
* 4438dfe Add discount
* 10300ca Add license
* d32f45b Add README
* 8567728 Add price function
```

في المخرج أعلاه لاحظ التالي:

- بعد إعادة التأسيس أصبح التاريخ خطاً واحداً، والتزاما الخصم يأتيان بعد التزام الترخيص مباشرة.
- الأهم أن أرقام الالتزامين تغيرت: 46e4452 أصبح 4438dfe، وcfe90b2 أصبح 0e06f59\. والسبب أن رقم ال Commit هاش يحسب من محتواه ومن رقم الالتزام الأب، فلما تغير الأب تغير الرقم، أي أن Git لم ينقل الالتزامات القديمة، وإنما أنشأ التزامات جديدة بالتغييرات نفسها، وترك القديمة دون فرع يشير إليها.

والصورة التالية تبين ما حدث خطوة بخطوة:

![رسم قبل وبعد: قبل إعادة التأسيس يخرج الفرع feature/discount من الالتزام B بالتزامين C وD، وبعدها يظهر التزامان جديدان فوق E بأرقام جديدة 4438dfe و0e06f59، والالتزامان القديمان باللون الرمادي المتقطع لا يشير إليهما شيء إلا reflog، وتحت الرسم أربع خطوات مرقمة](https://arabroot.io/content/images/2026/10/git-workflows-02-rebase.webp)

إعادة التأسيس تنسخ التزاماتك فوق main بأرقام جديدة، والقديمة تبقى في reflog فقط

وبعد إعادة التأسيس يصبح دمج الفرع في main دمجاً سريعاً، لأن الفرع أصبح main مع التزامات إضافية في آخره:

```bash
git switch main
git merge feature/discount
```

والمخرج سوف يكون كما يلي:

```bash
Updating 10300ca..0e06f59
Fast-forward
 README.md | 2 ++
 app.py    | 3 +++
 2 files changed, 5 insertions(+)
```

### القاعدة الذهبية: لا تعد تأسيس التزامات سحبها غيرك

بما أن إعادة التأسيس تنشئ التزامات جديدة وتترك القديمة، فماذا يحدث إذا كان زميلك قد سحب الفرع القديم وبنى عليه عمله؟ سوف يجد عنده التزامات لم تعد موجودة على السيرفر، ونسخاً منها بأرقام أخرى، وعند السحب سوف يحاول Git أن يدمج النسختين، فتظهر التغييرات نفسها مرتين، وتظهر تعارضات في كود لم يغيره أحد. ولذلك يضع كتاب Pro Git [القاعدة](https://git-scm.com/book/en/v2/Git-Branching-Rebasing?ref=arabroot.io) بوضوح: لا تقم بإعادة تأسيس التزامات موجودة خارج مستودعك وقد يكون غيرك بنى عليها عمله.

🛑

لا تقم بعمل Rebase لفرع مشترك مثل main أو develop، ولا لفرع دفعته وسحبه زميلك، والسبب أن إعادة التأسيس تغير أرقام الالتزامات، فيصبح تاريخ زميلك مختلفاً عن تاريخ السيرفر. والمسموح هو أن تعيد تأسيس التزاماتك المحلية التي لم تدفعها، أو فرعك الشخصي الذي لا يعمل عليه أحد غيرك.

### الأمر git pull --rebase: سحب دون التزامات دمج

في دليل المبتدئين رأينا [السيرفر يرفض الدفع](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D9%88-git-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#push-rejected) لأن زميلك دفع قبلك، والحل هناك كان `git pull` بالدمج، فينشأ التزام دمج من نوع «Merge branch 'main' of …» في كل مرة يحدث فيها ذلك. وهذه الحالة بالتحديد هي أفضل استخدام لإعادة التأسيس، لأن التزامك المحلي لم يره أحد بعد، والمثال التالي يبين ذلك بمستودعين لشخصين يدفعان إلى سيرفر واحد:

```bash
git push origin main
git pull origin main
git pull --rebase origin main
git log --oneline --graph
git push origin main
```

والمخرج سوف يكون كما يلي:

```bash
To ../remote.git
 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to '../remote.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

From ../remote
 * branch            main       -> FETCH_HEAD
   3c89335..b1eecf7  main       -> origin/main
hint: You have divergent branches and need to specify how to reconcile them.
hint: You can do so by running one of the following commands sometime before
hint: your next pull:
hint:
hint:   git config pull.rebase false  # merge
hint:   git config pull.rebase true   # rebase
hint:   git config pull.ff only       # fast-forward only
hint:
hint: You can replace "git config" with "git config --global" to set a default
hint: preference for all repositories. You can also pass --rebase, --no-rebase,
hint: or --ff-only on the command line to override the configured default per
hint: invocation.
fatal: Need to specify how to reconcile divergent branches.

From ../remote
 * branch            main       -> FETCH_HEAD
Successfully rebased and updated refs/heads/main.

* 1488c28 Add license
* b1eecf7 Add README
* 3c89335 Add price function

To ../remote.git
   b1eecf7..1488c28  main -> main
```

في المخرج أعلاه لاحظ التالي:

- الأمر `git pull` دون خيار توقف ولم يفعل شيئاً، والسبب أن الإصدارات الحديثة من Git تطلب منك أن تختار الطريقة عندما يتباعد الفرعان، والرسالة نفسها تقترح الإعداد الدائم.
- مع `--rebase` جلب Git التزام زميلك b1eecf7، ثم أعاد تطبيق التزامك فوقه، فصار رقمه 1488c28، وقبل السيرفر الدفع لأنه أصبح دمجاً سريعاً من جهته.

وإذا أردت أن يكون هذا هو السلوك الدائم فنفذ `git config --global pull.rebase true` مرة واحدة، كما يذكر [توثيق git pull](https://git-scm.com/docs/git-pull?ref=arabroot.io)، وإذا ظهر تعارض أثناء إعادة التأسيس فإن Git يتوقف عند الالتزام الذي فيه التعارض، فتحله كما شرحنا في [قسم التعارضات](#conflicts) ثم تنفذ `git rebase --continue`، أو تلغي العملية كلها وترجع إلى ما كنت عليه بالأمر `git rebase --abort`.

### إعادة التأسيس التفاعلية: رتب التزاماتك قبل أن يراها أحد

لنكن صادقين، فالتاريخ الحقيقي لأي فرع لا يبدو مرتباً، فأنت تكتب الميزة، ثم التزاماً اسمه «WIP debug» فيه ملف تجربة نسيته، ثم «fix typo» لأنك أخطأت في الالتزام السابق، ثم «docs». وهذه الالتزامات مفيدة لك أثناء العمل، ولكنها لا تفيد زميلك الذي يراجع طلب الدمج، ولا من يقرأ التاريخ بعد سنة. وإعادة التأسيس التفاعلية `git rebase -i` تفتح لك قائمة بالتزامات الفرع، فتقرر لكل التزام ماذا تفعل به قبل أن يعاد تطبيقه. وهذا هو الفرع في مثالنا:

```bash
git log --oneline
```

والمخرج سوف يكون كما يلي:

```bash
2748bd4 Make the discount rate configurable
3247062 docs
f5a3739 fix typo
681d3a6 WIP debug
6174f96 Add discount
1412288 Add price function
```

وعند تنفيذ `git rebase -i main` يفتح Git المحرر بملف يسمى قائمة المهام Todo List، ولكي نعرضه هنا كما هو جعلنا المحرر هو الأمر `cat` عبر المتغير GIT\_SEQUENCE\_EDITOR، فطبع القائمة ثم نفذها دون تغيير:

```bash
GIT_SEQUENCE_EDITOR=cat git rebase -i main
```

والمخرج سوف يكون كما يلي:

```bash
pick 6174f96 Add discount
pick 681d3a6 WIP debug
pick f5a3739 fix typo
pick 3247062 docs
pick 2748bd4 Make the discount rate configurable

# Rebase 1412288..2748bd4 onto 1412288 (5 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
#                    commit's log message, unless -C is used, in which case
#                    keep only this commit's message; -c is same as -C but
#                    opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
#         create a merge commit using the original merge commit's
#         message (or the oneline, if no original merge commit was
#         specified); use -c <commit> to reword the commit message
# u, update-ref <ref> = track a placeholder for the <ref> to be updated
#                       to this position in the new commits. The <ref> is
#                       updated at the end of the rebase
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
Successfully rebased and updated refs/heads/feature/discount.
```

لاحظ أن القائمة مرتبة من الأقدم إلى الأحدث، أي عكس `git log`، وأن كل سطر يبدأ بأمر، والأوامر التي سوف تستخدمها في أغلب الأحيان هي:

- **pick:** أبق الالتزام كما هو.
- **reword:** أبق الالتزام وغير رسالته فقط.
- **squash:** ادمج الالتزام في الالتزام الذي قبله، واجمع الرسالتين في رسالة واحدة تحررها.
- **fixup:** مثل squash، ولكنه يحذف رسالة هذا الالتزام ويبقي رسالة الالتزام السابق، وهو المناسب لالتزامات «fix typo».
- **drop:** احذف الالتزام وتغييراته نهائياً.
- **تغيير الترتيب:** انقل السطر إلى مكان آخر في القائمة، فيعاد تطبيق الالتزامات بالترتيب الجديد.

والآن نكتب القائمة التي نريدها: نبقي «Add discount»، ونلحق به «fix typo» بالأمر fixup لأنه إصلاح له، ثم ننقل «Make the discount rate configurable» إلى مكانه بعدهما وندمجه بالأمر squash، ونحذف التزام التجربة بالأمر drop، ونغير رسالة «docs» إلى رسالة مفهومة بالأمر reword. وفي العمل اليومي سوف تعدل هذه الأسطر في المحرر بنفسك، أما هنا فقد كتبناها بسكربت صغير جعلناه المحرر، ومعه سكربت آخر يكتب الرسائل الجديدة عبر GIT\_EDITOR، وهذه هي القائمة التي نفذها Git:

```bash
pick 6174f96 Add discount
fixup f5a3739 fix typo
squash 2748bd4 Make the discount rate configurable
drop 681d3a6 WIP debug
reword 3247062 docs
```

ثم نفذنا إعادة التأسيس:

```bash
GIT_SEQUENCE_EDITOR="bash ../todo.sh" GIT_EDITOR="bash ../msg.sh" git rebase -i main
git log --oneline
git show --stat HEAD~1
```

والمخرج سوف يكون كما يلي:

```bash
[detached HEAD 543ee79] Add a configurable discount
 Date: Tue Oct 6 01:08:26 2026 +0300
 1 file changed, 3 insertions(+)
[detached HEAD 553542c] Document the discount in README
 Date: Tue Oct 6 01:08:39 2026 +0300
 1 file changed, 3 insertions(+)
 create mode 100644 README.md
Successfully rebased and updated refs/heads/feature/discount.

553542c Document the discount in README
543ee79 Add a configurable discount
1412288 Add price function

commit 543ee79f6710f17cf14f4ecd65ac8ef249fef593
Author: ArabRoot Team <team@example.com>
Date:   Tue Oct 6 01:08:26 2026 +0300

    Add a configurable discount

 app.py | 3 +++
 1 file changed, 3 insertions(+)
```

في المخرج أعلاه لاحظ التالي:

- خمسة التزامات أصبحت التزامين، كل واحد منهما يحمل فكرة كاملة برسالة واضحة، وهذا ما سوف يراه المراجع في طلب الدمج.
- الالتزام 543ee79 يجمع ثلاثة التزامات: الدالة الأولى وإصلاحها والمعامل الجديد، والأمر `git show --stat` يبين أنه يغير `app.py` فقط، أي أن ملف التجربة `debug.py` اختفى مع التزامه.
- عبارة `detached HEAD` طبيعية أثناء إعادة التأسيس، لأن Git يبني الالتزامات الجديدة أولاً ثم ينقل الفرع إليها في النهاية.

💡

إذا كنت تعرف أثناء العمل أن الالتزام القادم إصلاح لالتزام سابق، فأنشئه بالأمر `git commit --fixup=<hash>`، ثم نفذ لاحقاً `git rebase -i --autosquash main`، فيضع Git كل التزام fixup تحت الالتزام الذي يصلحه تلقائياً، كما في [توثيق git rebase](https://git-scm.com/docs/git-rebase?ref=arabroot.io).

وقاعدة الأمان هنا هي القاعدة الذهبية نفسها، فإعادة التأسيس التفاعلية تعيد كتابة الالتزامات، لذلك نفذها على فرعك قبل أن تفتح طلب الدمج، وإذا كنت قد دفعت الفرع من قبل فسوف تحتاج إلى الدفع بالقوة، وهذا مقبول لفرعك الشخصي بشرط أن تستخدم [\--force-with-lease](#force-with-lease) وليس `--force`.

## Merge أم Rebase؟ مقارنة صادقة

هذا السؤال من أكثر الأسئلة جدلاً بين المطورين، والسبب أن الطريقتين تصلان إلى المحتوى نفسه للملفات، والذي يختلف هو شكل التاريخ فقط، والصورة التالية تبين الفرق على المثال نفسه بالمخرجات الحقيقية:

![مقارنة جانبية: على اليمين نتيجة git merge فيها خطان يلتقيان في التزام دمج M مع مخرج git log الذي يرسم الخطين، وعلى اليسار نتيجة git rebase ثم git merge وهي خط واحد مستقيم مع مخرج git log المستقيم، وتحت كل جانب نقاط القوة والضعف](https://arabroot.io/content/images/2026/10/git-workflows-03-merge-vs-rebase.webp)

الدمج يحفظ ما حدث كما حدث، وإعادة التأسيس تعطي خطاً واحداً بأرقام جديدة

|                          | Merge                           | Rebase                                       |
| ------------------------ | ------------------------------- | -------------------------------------------- |
| شكل التاريخ              | فروع تلتقي في التزامات دمج      | خط واحد مستقيم                               |
| هل يغير التزامات موجودة؟ | لا، يضيف فقط                    | نعم، ينشئ نسخاً بأرقام جديدة                 |
| على فرع مشترك            | آمن دائماً                      | ممنوع                                        |
| التعارضات                | تحلها مرة واحدة في التزام الدمج | قد تحلها مرة لكل التزام يعاد تطبيقه          |
| القراءة والبحث           | أصعب كلما كثرت الفروع           | أسهل، و[git bisect](#bisect) يمر على خط واحد |
| التراجع عن ميزة كاملة    | بالتراجع عن التزام الدمج نفسه   | تحتاج أن تعرف أي الالتزامات كانت الميزة      |

**نقاط القوة في Merge:** لا يكذب التاريخ، فهو يبين متى فتح الفرع ومتى اندمج، ولا يغير أي رقم، فلا يكسر عمل أحد. **ونقاط الضعف:** التزامات الدمج من نوع «Merge branch main into feature» تتكاثر وتجعل الرسم صعب القراءة.

**نقاط القوة في Rebase:** تاريخ نظيف كأن كل شيء كتب بالترتيب، وفرصة لترتيب التزاماتك قبل المراجعة. **ونقاط الضعف:** يعيد كتابة التاريخ، فهو خطير على أي فرع مشترك، والتاريخ الناتج لا يبين الترتيب الحقيقي الذي عمل به الناس.

والإجابة العملية أن أغلب الفرق لا تختار واحدة وتترك الأخرى، وإنما تستخدم كل واحدة في مكانها، وأشهر السياسات ثلاث:

1. **Rebase محلياً ثم Merge عبر طلب الدمج:** كل شخص ينفذ `git pull --rebase` ويعيد تأسيس فرعه على main قبل فتح الطلب، ثم يدمج الفرع بالتزام دمج أو بدمج سريع. وهذه أكثر السياسات توازناً.
2. **Squash Merge في كل طلب دمج:** مهما كان عدد الالتزامات في الفرع، يدخل main التزام واحد لكل طلب، وهذه هي الطريقة الأبسط للفرق الصغيرة، وسوف نشرحها في القسم التالي.
3. **Merge دائماً:** لا أحد يعيد كتابة أي شيء، والتاريخ كما حدث تماماً، وهذه تناسب الفرق التي تقدم الأمان على شكل الرسم، أو التي فيها أعضاء جدد لم يتقنوا Rebase بعد.

وفي المنصات تفرض السياسة من إعدادات المستودع، ففي Gitea يحدد المدير طرق الدمج المسموحة لكل مستودع، والقيم التي يعرفها [ملف الإعداد](https://docs.gitea.com/administration/config-cheat-sheet?ref=arabroot.io) للخيار `DEFAULT_MERGE_STYLE` هي `merge` و`rebase` و`rebase-merge` و`squash` و`fast-forward-only`، والافتراضي merge.

## الدمج مع الضغط Squash Merge

المشكلة التي يحلها Squash هي نفسها التي حلتها إعادة التأسيس التفاعلية، أي الالتزامات الصغيرة الكثيرة في الفرع، ولكن بطريقة أبسط لا تحتاج أن يتقن كل شخص في الفريق `rebase -i`: عند الدمج يأخذ Git كل تغييرات الفرع ويضعها في التزام واحد جديد على main. وهذا ما يفعله خيار Squash عند دمج طلب في Gitea، وما يصفه GitHub بأن [التزامات الطلب تضغط في التزام واحد](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/about-merge-methods-on-github?ref=arabroot.io)، والأمر نفسه على جهازك هو:

```bash
git switch main
git merge --squash feature/discount
git status --short
git commit -qm "Add discount (#12)"
git log --oneline --graph --all
git branch -d feature/discount
```

والمخرج سوف يكون كما يلي:

```bash
Updating cd33688..32f861c
Fast-forward
Squash commit -- not updating HEAD
 README.md | 3 +++
 app.py    | 3 +++
 2 files changed, 6 insertions(+)
 create mode 100644 README.md

A  README.md
M  app.py

* 3788cb0 Add discount (#12)
| * 32f861c docs
| * 81a05f8 fix rounding
| * db33bec Add discount
|/
* cd33688 Add price function

error: the branch 'feature/discount' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D feature/discount'
hint: Disable this message with "git config set advice.forceDeleteBranch false"
```

في المخرج أعلاه لاحظ التالي:

- العبارة `not updating HEAD` تعني أن الأمر لم ينشئ أي التزام، وإنما وضع كل التغييرات في منطقة التجهيز، كما يبين `git status`، وأنت من ينشئ الالتزام برسالتك، وفيها رقم طلب الدمج كما تفعل المنصات.
- الالتزام 3788cb0 على main له أب واحد، فلا يوجد أي رابط بينه وبين التزامات الفرع الثلاثة.
- ولهذا السبب رفض Git حذف الفرع بالأمر `-d`، فهو يرى أن التزامات الفرع لم تدخل main، وهو محق من جهة التاريخ، لذلك تحذف المنصات الفرع بعد Squash، وعلى جهازك تحذفه بالأمر `git branch -D` بعد أن تتأكد أن الطلب دمج.

والصورة التالية تلخص ما حدث:

![رسم قبل وبعد: قبل الدمج طلب دمج رقم 12 فيه ثلاثة التزامات برتقالية وزر Squash and merge، وبعده التزام واحد أخضر S برقم 3788cb0 على main يحمل كل التغييرات، والتزامات الفرع الثلاثة باللون الرمادي المتقطع خارج التاريخ](https://arabroot.io/content/images/2026/10/git-workflows-04-squash.webp)

Squash يحول الطلب كله إلى التزام واحد في main، ويترك الفرع خارج التاريخ

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

## الانتقاء Cherry-pick: إصلاح واحد إلى فرع الإصدار

لنفرض أنك أصدرت النسخة v1.0.0 لعميل، وأنشأت لها فرعاً اسمه `release/1.0` يمثل ما يعمل عنده الآن، ثم تابع الفريق العمل على main فأضاف ميزة الخصم التي لم تختبر بعد، ثم اكتشف أحدهم خطأ في حساب مجموع السلة، فأصلحه على main. والعميل يحتاج هذا الإصلاح اليوم، ولكن دمج main في فرع الإصدار سوف يدخل معه ميزة الخصم التي لم تكتمل، وهذا غير مقبول إطلاقاً. والحل أن تنسخ التزام الإصلاح وحده، وهذا ما يفعله `git cherry-pick`:

```bash
git log --oneline --graph --all --decorate
git switch release/1.0
git cherry-pick -x main
git log --oneline --graph --all --decorate
git log -1 --format=%B
```

والمخرج سوف يكون كما يلي:

```bash
* 53b69a9 (HEAD -> main) Ignore negative items in cart total
* a1ff393 Add discount
* 7ba1817 (tag: v1.0.0, release/1.0) Add price and cart total

Switched to branch 'release/1.0'

[release/1.0 bad4175] Ignore negative items in cart total
 Date: Tue Oct 6 01:11:23 2026 +0300
 1 file changed, 1 insertion(+), 1 deletion(-)

* bad4175 (HEAD -> release/1.0) Ignore negative items in cart total
| * 53b69a9 (main) Ignore negative items in cart total
| * a1ff393 Add discount
|/
* 7ba1817 (tag: v1.0.0) Add price and cart total

Ignore negative items in cart total

(cherry picked from commit 53b69a9add594888a4ed6d73a24aa809120202a2)
```

في المخرج أعلاه لاحظ التالي:

- كتبنا `main` لأنه يشير إلى التزام الإصلاح، وفي العمل اليومي سوف تكتب رقم الالتزام نفسه، وتستطيع أن تنتقي عدة التزامات أو مدى منها مرة واحدة.
- ميزة الخصم a1ff393 لم تدخل فرع الإصدار، والذي دخل هو التزام جديد bad4175 بالتغيير نفسه ورقم مختلف، أي أن الإصلاح موجود الآن مرتين في المستودع، وهذا طبيعي في الانتقاء لأنه نسخ وليس نقلاً.
- الخيار `-x` أضاف إلى الرسالة السطر «cherry picked from commit»، وينصح [توثيق cherry-pick](https://git-scm.com/docs/git-cherry-pick?ref=arabroot.io) باستخدامه عندما تنتقي بين فرعين عامين كما في نقل إصلاح إلى فرع إصدار قديم، حتى يعرف من يقرأ التاريخ أصل الالتزام، ولا ينصح به عند الانتقاء من فرعك الخاص لأن الرقم المذكور لن يفيد أحداً.

والصورة التالية تبين الفكرة:

![رسم لفرعين: main عليه الالتزام A بوسم v1.0.0 ثم B لميزة الخصم ثم F للإصلاح، وفرع release/1.0 يخرج من A، وسهم متقطع ينسخ F إلى نسخة جديدة على فرع الإصدار برقم bad4175 ووسم v1.0.1، ثم سهم إلى سيرفر الإنتاج، وعلامة تبين أن B لم تدخل الإصدار](https://arabroot.io/content/images/2026/10/git-workflows-05-cherry-pick.webp)

الانتقاء ينسخ التزام الإصلاح وحده إلى فرع الإصدار برقم جديد، وتبقى الميزة غير المكتملة في main

وقد يتساءل البعض: لماذا لا نصلح الخطأ على فرع الإصدار أولاً ثم ننقله إلى main؟ والإجابة أن الطريقتين موجودتان، فنموذج Git Flow يصلح في فرع hotfix ثم يدمجه في الفرعين، أما موقع [Trunk-Based Development](https://trunkbaseddevelopment.com/branch-for-release/?ref=arabroot.io) فيوصي بأن يصلح الخطأ على الفرع الرئيسي أولاً مع اختبار يثبته، ثم ينتقى إلى فرع الإصدار، والسبب أن الإصلاح الذي يبدأ في فرع الإصدار قد ينسى ولا يصل إلى main، فيعود الخطأ في الإصدار التالي.

### عندما يتعارض الانتقاء

الانتقاء يطبق تغييرات الالتزام على حالة فرع آخر، فإذا كان الإصلاح يعدل أسطراً تختلف في الفرع الهدف فسوف يتوقف، وهذا ما حدث معنا في أول محاولة عندما كان الإصلاح وميزة الخصم في الملف نفسه وفي أسطر متجاورة:

```bash
Auto-merging app.py
CONFLICT (content): Merge conflict in app.py
error: could not apply 7c1e28f... Ignore negative items in total
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run
hint: "git cherry-pick --continue".
hint: You can instead skip this commit with "git cherry-pick --skip".
hint: To abort and get back to the state before "git cherry-pick",
hint: run "git cherry-pick --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
```

والرسالة نفسها تشرح الخيارات: تحل التعارض وتنفذ `git add` ثم `git cherry-pick --continue`، أو تتخطى هذا الالتزام بالخيار `--skip`، أو تلغي العملية كلها بالخيار `--abort`. وإذا تكررت التعارضات عند الانتقاء إلى فرع إصدار، فهذه علامة على أن الفرع ابتعد كثيراً عن main، وعلى أن الإصلاح الذي تريده يعتمد على تغييرات أخرى لم تنتقها.

وقاعدة الأمان في الانتقاء أنه لا يغير التزامات موجودة، فهو آمن على أي فرع، ولكن لا تقم باستخدامه بديلاً عن الدمج لنقل ميزات كاملة بين فرعين يعيشان طويلاً، والسبب أن كل التزام سوف يظهر مرتين برقمين مختلفين، وعند الدمج بينهما لاحقاً قد تظهر تعارضات على تغييرات موجودة في الطرفين أصلاً.

## التراجع: revert وreset وrestore

في دليل المبتدئين رأينا [git restore](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D9%88-git-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#restore) للتعديلات التي لم تجهزها، و[git revert](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D9%88-git-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#revert) للالتزام الذي وصل إلى الفريق، وهنا سوف نضعها مع الأمر الثالث reset في صورة واحدة، لأن السؤال الحقيقي عند كل خطأ هو: هل أريد أن أضيف تاريخاً جديداً، أم أريد أن أعيد كتابة التاريخ؟ وفي مثالنا ثلاثة التزامات، وآخرها خطأ يضاعف كل الأسعار:

```bash
c85efc2 Double all prices
a893a8a Add discount
631b7eb Add price function
```

### الأمر revert: التزام جديد يعكس القديم

```bash
git revert --no-edit HEAD
git log --oneline
```

والمخرج سوف يكون كما يلي:

```bash
[main 23a0f59] Revert "Double all prices"
 Date: Tue Oct 6 01:12:30 2026 +0300
 1 file changed, 1 insertion(+), 1 deletion(-)

23a0f59 Revert "Double all prices"
c85efc2 Double all prices
a893a8a Add discount
631b7eb Add price function
```

الالتزام الخطأ ما زال في التاريخ، وفوقه التزام يعكس تغييره، فالملفات رجعت كما كانت دون أن يتغير أي رقم، ولذلك فهو الطريقة الوحيدة المناسبة لفرع مشترك مثل main، وزملاؤك يسحبون الالتزام الجديد كأي التزام آخر.

### الأمر restore: تعديلات لم تلتزم بها بعد

```bash
echo "oops" >> app.py
git status --short
git restore app.py
git status --short
```

والمخرج سوف يكون كما يلي:

```bash
 M app.py

```

بعد `git restore` لم يطبع `git status` شيئاً، أي أن الملف رجع إلى آخر نسخة ملتزم بها. وهذا الأمر لا يلمس التاريخ إطلاقاً، ولكنه يحذف تعديلاتك غير الملتزم بها نهائياً، ولا توجد طريقة في Git لاسترجاعها لأنها لم تدخل المستودع أصلاً.

### الأمر reset: إرجاع الفرع إلى الخلف

الأمر `git reset` يحرك مؤشر الفرع الحالي إلى التزام سابق، فتخرج الالتزامات التي بعده من تاريخ الفرع، والسؤال هو ماذا يحدث لتغييراتها، وهنا يأتي الفرق بين الأنواع الثلاثة. وقد نفذنا كل نوع على الالتزامات الثلاثة نفسها، وبعد كل تجربة أرجعنا الفرع إلى حاله:

```bash
git reset --soft HEAD~1
git status --short
```

```bash
M  app.py
```

```bash
git reset --mixed HEAD~1
git status --short
```

```bash
Unstaged changes after reset:
M	app.py

 M app.py
```

```bash
git reset --hard HEAD~2
git log --oneline
git status --short
```

```bash
HEAD is now at 631b7eb Add price function

631b7eb Add price function
```

في المخرجات أعلاه لاحظ التالي:

- **\--soft:** الالتزام خرج من التاريخ، ولكن تغييراته بقيت في منطقة التجهيز، والحرف `M` في العمود الأول يعني أنها جاهزة للالتزام، وهذا مفيد عندما تريد أن تعيد الالتزام برسالة أخرى أو تجمع آخر ثلاثة التزامات في التزام واحد.
- **\--mixed:** وهو النوع الافتراضي إذا لم تكتب خياراً، والتغييرات بقيت في الملفات ولكنها خرجت من منطقة التجهيز، والحرف `M` انتقل إلى العمود الثاني.
- **\--hard:** الالتزامان خرجا من التاريخ، وتغييراتهما حذفت من الملفات أيضاً، و`git status` لا يطبع شيئاً، فكأنها لم تكتب.

والصورة التالية تجمع الفرق بين revert وreset، ومعها شبكة الأمان التي سوف نشرحها الآن:

![أربعة أجزاء: revert يضيف التزاماً أخضر R بعد الالتزام الخطأ C ويبقى التاريخ كاملاً مع علامة أمان، وreset يرجع المؤشر main من C إلى B ويصبح C رمادياً خارج السجل مع علامة تحذير، وجدول يبين ماذا يحدث للتغييرات في منطقة التجهيز ومجلد العمل مع soft وmixed وhard، ومخرج git reflog الذي ما زال يذكر الالتزام المفقود](https://arabroot.io/content/images/2026/10/git-workflows-06-revert-reset.webp)

revert يضيف إلى التاريخ ويناسب الفروع المشتركة، وreset يعيد كتابته ويناسب عملك المحلي فقط، وreflog يحفظ الطريق للرجوع

🛑

لا تقم باستخدام `git reset` لإزالة التزامات دفعتها إلى فرع مشترك، والسبب أنه يعيد كتابة التاريخ، فلن يقبل السيرفر الدفع إلا بالقوة، وعندها تختفي الالتزامات من السيرفر وتبقى عند زملائك، فيعيدها أول من يدفع منهم. والطريقة الصحيحة على الفرع المشترك هي `git revert`. وقبل أي `reset --hard` نفذ `git status`، فإذا وجدت تعديلات لم تلتزم بها فاحفظها بالأمر [git stash](#stash)، لأن reflog لا يحفظ إلا ما التزمت به.

### شبكة الأمان reflog: استرجاع التزام «ضاع»

لنفرض أنك نفذت `reset --hard` وأخطأت في العدد، فحذفت التزامين بدلاً من واحد، وأحدهما فيه عمل يوم كامل، فهل ضاع؟ الإجابة لا، والسبب أن Git يسجل كل حركة لمؤشر HEAD في سجل محلي اسمه reflog، والالتزام نفسه يبقى في المستودع حتى لو لم يعد أي فرع يشير إليه. والمثال التالي يبين ذلك على مستودع جديد:

```bash
git log --oneline
git reset --hard HEAD~2
git log --oneline
git reflog
```

والمخرج سوف يكون كما يلي:

```bash
ab699ca Add VAT calculation
c2f8d93 Add discount
c46725a Add price function

HEAD is now at c46725a Add price function

c46725a Add price function

c46725a HEAD@{0}: reset: moving to HEAD~2
ab699ca HEAD@{1}: commit: Add VAT calculation
c2f8d93 HEAD@{2}: commit: Add discount
c46725a HEAD@{3}: commit (initial): Add price function
```

السطر `HEAD@{1}` يعني المكان الذي كان فيه HEAD قبل حركة واحدة، وهو الالتزام ab699ca الذي اختفى من `git log`. والآن ننشئ عليه فرعاً حتى لا يضيع مرة أخرى، ثم نرجع إليه:

```bash
git branch rescue HEAD@{1}
git log --oneline rescue
git reset --hard rescue
git log --oneline
```

والمخرج سوف يكون كما يلي:

```bash
ab699ca Add VAT calculation
c2f8d93 Add discount
c46725a Add price function

HEAD is now at ab699ca Add VAT calculation

ab699ca Add VAT calculation
c2f8d93 Add discount
c46725a Add price function
```

والطريقة نفسها تنقذك بعد إعادة تأسيس فاشلة أو Squash خاطئ، فكل عملية من هذه تكتب سطراً في reflog. ولكن تذكر حدود هذه الشبكة: reflog محلي على جهازك فقط ولا يدفع إلى السيرفر، والأمر `git gc` يحذف منه السطور القديمة، و[القيم الافتراضية](https://git-scm.com/docs/git-gc?ref=arabroot.io) 90 يوماً للسطور عموماً و30 يوماً للسطور التي تشير إلى التزامات لم تعد في الفرع الحالي، فلا تؤجل الاسترجاع.

## الحفظ المؤقت Stash: عمل لم يكتمل وطلب عاجل

أنت في منتصف ميزة الخصم، والملف معدل وفيه TODO، وعندك ملف ملاحظات جديد، ثم يطلب منك إصلاح عاجل على main. والتزام عمل نصف مكتمل باسم «WIP» حل ممكن، ولكنه يلوث الفرع، والانتقال إلى main بالتعديلات كما هي قد ينقلها معك أو يرفضه Git إذا تعارضت. والحل هو الأمر `git stash`، الذي يحفظ تعديلاتك في مكان جانبي ويعيد مجلد العمل نظيفاً:

```bash
git status --short
git stash push -u -m "discount work in progress"
git status --short
git stash list
git switch main
# ... urgent fix on main ...
git switch feature/discount
git stash pop
git status --short
```

والمخرج سوف يكون كما يلي:

```bash
 M app.py
?? notes.txt

Saved working directory and index state On feature/discount: discount work in progress

stash@{0}: On feature/discount: discount work in progress

Switched to branch 'main'

Switched to branch 'feature/discount'

On branch feature/discount
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   app.py

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	notes.txt

no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (12810149eca983a9ee26d781722e7ed442471f53)

 M app.py
?? notes.txt
```

في المخرج أعلاه لاحظ التالي:

- الخيار `-u` ضروري هنا، لأن `git stash` دونه لا يحفظ الملفات الجديدة غير المتتبعة مثل `notes.txt`، والخيار `-m` يعطي الحفظ اسماً تعرفه به في `git stash list` عندما يكثر.
- الأمر `pop` أعاد التعديلات ثم حذف الحفظ من القائمة، أما `git stash apply` فيعيدها ويبقي الحفظ، وهذا أفضل إذا كنت تخشى تعارضاً.

ومتى لا تستخدمه؟ عندما يكون العمل كبيراً أو سوف تتركه أياماً، والسبب أن الحفظ المؤقت محلي ولا يدفع إلى السيرفر، وينسى بسهولة في القائمة، فالأفضل في هذه الحالة التزام حقيقي على فرعك ثم ترتيبه لاحقاً بإعادة التأسيس التفاعلية.

## الوسوم Tags وترقيم الإصدارات

شرحنا في دليل المبتدئين أن [الوسم Tag](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D9%88-git-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#tags) اسم ثابت على التزام واحد، وهنا نوضح الفرق بين نوعيه، فالوسم الخفيف Lightweight Tag مجرد اسم يشير إلى التزام، والوسم المشروح Annotated Tag كائن كامل في المستودع له كاتب وتاريخ ورسالة:

```bash
git tag before-refactor
git tag -a v1.0.0 -m "Release 1.0.0"
git cat-file -t before-refactor
git cat-file -t v1.0.0
git show v1.0.0 --stat
```

والمخرج سوف يكون كما يلي:

```bash
commit

tag

tag v1.0.0
Tagger: ArabRoot Team <team@example.com>
Date:   Tue Oct 6 01:16:14 2026 +0300

Release 1.0.0

commit e25f5272b04701d836139139d65807c7f506335c
Author: ArabRoot Team <team@example.com>
Date:   Tue Oct 6 01:15:50 2026 +0300

    Add price function

 app.py | 2 ++
 1 file changed, 2 insertions(+)
```

الأمر `git cat-file -t` يبين نوع الكائن، فالوسم الخفيف يشير مباشرة إلى commit، أما المشروح فهو كائن من نوع tag يحمل اسم من أنشأه وتاريخه. ولذلك يقول [توثيق git tag](https://git-scm.com/docs/git-tag?ref=arabroot.io) إن الوسوم المشروحة للإصدارات والخفيفة للعلامات الخاصة أو المؤقتة، وإن بعض الأوامر مثل `git describe` تتجاهل الخفيفة افتراضياً، وهذا الأمر مفيد لأنه يصف أي التزام بالنسبة لآخر إصدار. فبعد أن أضفنا التزاماً جديداً بعد الوسم:

```bash
git describe
```

والمخرج سوف يكون كما يلي:

```bash
v1.0.0-1-gbcf6209
```

أي التزام واحد بعد v1.0.0، ورقمه bcf6209، وهي صيغة مناسبة لتضعها في رقم بناء تطبيقك. وتذكر أن `git push` لا يدفع الوسوم، فادفع الوسم باسمه `git push origin v1.0.0`، وأدوات CI مثل [Gitea Actions](https://arabroot.io/articles/gitea-actions-%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-ci-cd-%D8%B9%D9%84%D9%89-%D8%AE%D8%A7%D8%AF%D9%85%D9%83/) تستطيع أن تبني الإصدار وتنشره عندما يصل وسم جديد.

أما الأرقام نفسها فأغلب المشاريع تتبع [ترقيم الإصدارات الدلالي Semantic Versioning 2.0.0](https://semver.org/?ref=arabroot.io) بالصيغة MAJOR.MINOR.PATCH، فتزيد PATCH عند إصلاح لا يكسر التوافق، مثل الإصلاح الذي انتقيناه إلى v1.0.1، وتزيد MINOR عند إضافة ميزة متوافقة مع ما قبلها مثل ميزة الخصم في v1.1.0، وتزيد MAJOR عند تغيير يكسر التوافق في الواجهة العامة Public API فتصبح v2.0.0\. وتنص المواصفة أيضاً على أن الإصدارات 0.y.z مرحلة تطوير أولى يمكن أن يتغير فيها أي شيء، فلا يعتبر فيها الواجهة العامة مستقرة.

⚠️

لا تقم بنقل وسم إصدار دفعته إلى التزام آخر، والسبب أن من سحبه عنده النسخة القديمة ولن يحدثها Git تلقائياً، فيصبح v1.0.0 عندك غير v1.0.0 عند العميل أو عند خط CI. إذا كان في الإصدار خطأ فأصدر v1.0.1.

## التعارضات Conflicts: لماذا تحدث وكيف تقللها

شرحنا في دليل المبتدئين [التعارض الأول](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D9%88-git-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#conflict) وعلاماته وحله، والفكرة باختصار أن Git يدمج تلقائياً كل ما لا يتعارض، ويتوقف فقط عندما يغير الطرفان السطر نفسه بطريقتين، وهذا يحدث في الدمج وفي إعادة التأسيس وفي الانتقاء وفي `stash pop` بالطريقة نفسها. وفي مثالنا فرع يجعل الخصم 20% لعملاء VIP، وmain جعل الخصم يقرب إلى منزلتين، والسطر نفسه تغير في الطرفين:

```bash
git config rerere.enabled true
git merge feature/vip
git status --short
cat app.py
```

والمخرج سوف يكون كما يلي:

```bash
Auto-merging app.py
CONFLICT (content): Merge conflict in app.py
Automatic merge failed; fix conflicts and then commit the result.
Recorded preimage for 'app.py'

UU app.py

def discount(x):
<<<<<<< HEAD
    return round(x * 0.9, 2)
=======
    return x * 0.8
>>>>>>> feature/vip
```

ما بين `<<<<<<< HEAD` و`=======` هو نسختك أي main، وما بعدها حتى اسم الفرع هو النسخة القادمة، والحرفان `UU` يعنيان أن الملف معدل في الطرفين ولم يحل. والحل الصحيح هنا لا يأخذ أحد الطرفين، وإنما يجمع القرارين، أي خصم 20% مع التقريب، فنكتب الملف هكذا:

```bash
def discount(x):
    return round(x * 0.8, 2)
```

ثم نكمل الدمج:

```bash
git add app.py
git commit --no-edit
```

والمخرج سوف يكون كما يلي:

```bash
Recorded resolution for 'app.py'.
[main fccff21] Merge branch 'feature/vip'
```

### الأمر rerere: لا تحل التعارض نفسه مرتين

لاحظ السطرين `Recorded preimage` و`Recorded resolution` في المخرجين أعلاه، فهما من ميزة اسمها rerere، واختصارها Reuse Recorded Resolution، وقد فعلناها بالإعداد `rerere.enabled` في أول المثال. وهذه الميزة [تسجل](https://git-scm.com/docs/git-rerere?ref=arabroot.io) شكل التعارض والحل الذي كتبته، فإذا ظهر التعارض نفسه مرة أخرى طبقت الحل تلقائياً. ولنختبر ذلك، نلغي الدمج ونعيده:

```bash
git reset --hard HEAD~1
git merge feature/vip
cat app.py
git merge --abort
```

والمخرج سوف يكون كما يلي:

```bash
HEAD is now at 86d543f Round discounted prices

Auto-merging app.py
CONFLICT (content): Merge conflict in app.py
Automatic merge failed; fix conflicts and then commit the result.
Resolved 'app.py' using previous resolution.

def discount(x):
    return round(x * 0.8, 2)
```

Git ما زال يعتبر الدمج متوقفاً حتى تراجع الملف وتنفذ `git add`، ولكن الملف نفسه يحمل الحل الذي كتبته سابقاً، وهذا مفيد جداً مع إعادة التأسيس المتكررة لفرع طويل، أو عندما تلغي دمجاً ثم تعيده.

### أدوات حل التعارض

حل التعارض في محرر نصوص يكفي في الملفات الصغيرة، أما في التعارضات الكبيرة فالأمر [git mergetool](https://git-scm.com/docs/git-mergetool?ref=arabroot.io) يفتح لكل ملف متعارض أداة رسومية تعرض نسختك والنسخة القادمة والنسخة المشتركة جنباً إلى جنب، مثل meld وkdiff3 وvimdiff، والأمر `git mergetool --tool-help` يعرض الأدوات المتاحة على جهازك. وأغلب المحررات الحديثة مثل VS Code تعرض التعارض بأزرار تختار بها النسخة دون أي إعداد.

### كيف تقلل التعارضات

- **طلبات دمج صغيرة:** كلما قل عدد الأسطر التي يغيرها الفرع قلت فرصة أن يغيرها شخص آخر، والطلب الصغير يراجع ويدمج في ساعات وليس في أسابيع.
- **اسحب كثيراً:** نفذ `git pull --rebase` على main وأعد تأسيس فرعك عليه كل يوم، فتحل تعارضاً صغيراً اليوم بدلاً من تعارضات كثيرة يوم الدمج.
- **لا تخلط التنسيق بالمنطق:** تغيير المسافات أو إعادة تنسيق ملف كامل في الطلب نفسه مع تعديل منطقي يجعل كل سطر في الملف متعارضاً مع أي شخص آخر، فاجعل التنسيق في طلب منفصل، أو استخدم أداة تنسيق Formatter موحدة يشغلها الجميع.
- **تكلم مع زملائك:** إذا كنتما ستعدلان الملف نفسه في الأسبوع نفسه فاتفقا على الترتيب، وهذه نصيحة لا تحتاج أي أمر.

## العمل مع الفريق: طلبات الدمج وحماية الفروع

في الفرق لا يدفع أحد إلى main مباشرة، وإنما يعمل كل شخص على فرع ويفتح [طلب دمج](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D9%88-git-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#pull-requests)، ويسمى Pull Request في Gitea وGitHub و[Merge Request](https://docs.gitlab.com/user/project/merge%5Frequests/?ref=arabroot.io) في GitLab، والفكرة واحدة في الثلاثة: الزملاء يرون الفروقات ويعلقون على الأسطر، وخط CI يشغل الاختبارات على الفرع، ثم يدمج الطلب بالطريقة التي يسمح بها المستودع. وإذا كنت تريد هذا كله على سيرفرك فقد شرحنا [تثبيت Gitea](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-gitea-%D9%85%D8%B9-docker-compose/)، و[تشغيل الاختبارات والبناء بأداة Gitea Actions](https://arabroot.io/articles/gitea-actions-%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-ci-cd-%D8%B9%D9%84%D9%89-%D8%AE%D8%A7%D8%AF%D9%85%D9%83/).

ولكن الطلب وحده لا يمنع أحداً من أن يدفع إلى main مباشرة، أو أن يدمج طلباً فشلت اختباراته، ولذلك توفر المنصات حماية الفروع Branch Protection، وفي Gitea تجد في إعدادات المستودع [قواعد الحماية](https://docs.gitea.com/usage/access-control/protected-branches?ref=arabroot.io) التالية:

- **منع الدفع المباشر:** لا يتغير الفرع إلا بدمج طلب، أو تسمح بالدفع لقائمة محددة فقط.
- **منع الدفع بالقوة Force Push:** فلا يستطيع أحد أن يعيد كتابة تاريخ main، وهذا وحده يمنع أخطر خطأ في هذا المقال.
- **عدد الموافقات المطلوبة:** لا يدمج الطلب قبل موافقة عدد من المراجعين، مع خيار يلغي الموافقات القديمة إذا أضيفت التزامات جديدة بعدها.
- **فحوص الحالة الإلزامية Status Checks:** لا يدمج الطلب حتى تنجح مهام CI التي تحددها بأسمائها.
- **إلزام المديرين بالقواعد:** حتى لا يتجاوزها أحد في الاستعجال.

وفي GitHub تمنع كل قاعدة حماية الدفع بالقوة وحذف الفرع [افتراضياً](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches?ref=arabroot.io)، وتستطيع أن تشترط الموافقات ونجاح الفحوص بالطريقة نفسها.

### الدفع بالقوة الآمن: --force-with-lease

بعد إعادة تأسيس فرعك أو تعديل آخر التزام فيه، سوف يرفض السيرفر الدفع لأن تاريخك لم يعد امتداداً لتاريخه، وهنا تحتاج إلى الدفع بالقوة. والمشكلة أن `--force` يكتب نسختك فوق ما على السيرفر مهما كان، فإذا كان زميلك قد دفع التزاماً إلى الفرع نفسه بعد آخر سحب لك فإنه يحذف التزامه دون أي تحذير. والخيار `--force-with-lease` يضيف شرطاً: لا تكتب فوق الفرع إلا إذا كان على السيرفر ما زال كما رأيته آخر مرة. وفي مثالنا عدلنا آخر التزام بالأمر `--amend`، وفي الوقت نفسه دفع شخص آخر التزاماً إلى الفرع نفسه:

```bash
git push --force-with-lease origin main
git fetch origin
git push --force-with-lease --force-if-includes origin main
git push --force-with-lease origin main
```

والمخرج سوف يكون كما يلي:

```bash
To ../remote.git
 ! [rejected]        main -> main (stale info)
error: failed to push some refs to '../remote.git'

From ../remote
   1488c28..94eb265  main       -> origin/main

To ../remote.git
 ! [rejected]        main -> main (remote ref updated since checkout)
error: failed to push some refs to '../remote.git'
hint: Updates were rejected because the tip of the remote-tracking branch has
hint: been updated since the last checkout. If you want to integrate the
hint: remote changes, use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

To ../remote.git
 + 94eb265...1bae812 main -> main (forced update)
```

في المخرج أعلاه لاحظ التالي:

- المحاولة الأولى رفضت بالسبب `stale info`، أي أن السيرفر تغير منذ آخر مرة رأيته، وهذا بالضبط ما نريده، فالتزام زميلك محمي.
- ثم نفذنا `git fetch`، فأصبح Git «يعرف» الحالة الجديدة للسيرفر، مع أننا لم ندمج التزام زميلك في فرعنا. وهنا يأتي الخيار `--force-if-includes`، الذي يتحقق أن ما جلبته دخل فعلاً في فرعك، فرفض الدفع.
- والمحاولة الأخيرة دون `--force-if-includes` نجحت، وحذفت التزام زميلك 94eb265 من السيرفر، والسبب أن الجلب حدث الحالة التي يقارن بها `--force-with-lease`، وهذا التحذير مذكور صراحة في [توثيق git push](https://git-scm.com/docs/git-push?ref=arabroot.io)، خصوصاً مع أي أداة تنفذ `git fetch` في الخلفية تلقائياً.

لذلك فالصيغة التي ننصح بها عند الدفع بالقوة إلى فرعك هي `git push --force-with-lease --force-if-includes`، ولا تقم بالدفع بالقوة إلى main أو أي فرع مشترك بأي صيغة، واجعل حماية الفرع تمنع ذلك على السيرفر حتى لا تعتمد على ذاكرة أحد.

## استراتيجيات التفرع: Git Flow وGitHub Flow وTrunk-Based

الأوامر السابقة أدوات، والاستراتيجية هي الاتفاق في الفريق على متى تفتح الفروع وكم تعيش وكيف تصل إلى الإنتاج Production، والصورة التالية تقارن بين أشهر ثلاث استراتيجيات:

![ثلاثة رسوم للتاريخ: Git Flow بفرعين دائمين main وdevelop وفروع feature وrelease وhotfix ووسوم v1.0 وv1.0.1 وv1.1، وGitHub Flow بفرع main واحد وفروع قصيرة تدمج بطلبات دمج مع نشر بعد كل دمج، وTrunk-Based بالتزامات صغيرة متتالية على trunk وفرع release/2.4 يصله إصلاح بالانتقاء مع الوسمين v2.4.0 وv2.4.1](https://arabroot.io/content/images/2026/10/git-workflows-07-strategies.webp)

Git Flow يفصل التطوير عن الإصدار بفروع دائمة، وGitHub Flow يكتفي بفرع رئيسي وطلبات دمج، وTrunk-Based يدمج كل يوم ويقطع فرع إصدار عند الحاجة

**Git Flow:** نموذج نشره Vincent Driessen في [يناير 2010](https://nvie.com/posts/a-successful-git-branching-model/?ref=arabroot.io)، فيه فرعان دائمان هما main للإنتاج وdevelop للتطوير، وفروع feature تخرج من develop وتعود إليه، وفروع release لتجهيز الإصدار، وفروع hotfix للإصلاحات العاجلة تدمج في main وdevelop معاً. وقد أضاف الكاتب نفسه في مارس 2020 ملاحظة في أعلى المقال يقول فيها إن هذا النموذج ليس الأنسب لفرق تطبيقات الويب التي تنشر باستمرار، وينصحها بنموذج أبسط مثل GitHub Flow، ويبقي Git Flow للبرامج التي تصدر بأرقام إصدارات صريحة أو تدعم أكثر من إصدار في الوقت نفسه.

**GitHub Flow:** فرع رئيسي واحد قابل للنشر دائماً، وكل تغيير في فرع قصير يفتح له طلب دمج، ويراجع وتنجح اختباراته، ثم يدمج ويحذف الفرع، كما في [وصف GitHub](https://docs.github.com/en/get-started/using-github/github-flow?ref=arabroot.io). ولا توجد فروع develop أو release، والنشر يتم بعد الدمج.

**Trunk-Based Development:** كل المطورين يدمجون في فرع واحد يسمى trunk، و[يقاومون إنشاء أي فرع آخر يعيش طويلاً](https://trunkbaseddevelopment.com/?ref=arabroot.io)، والفروع إن وجدت تعيش ساعات أو يوماً أو يومين للمراجعة وCI، والميزات غير المكتملة تدمج مخفية خلف Feature Flags، وعند الحاجة يقطع فرع إصدار من trunk قبل الإصدار مباشرة، والإصلاحات تصل إليه بالانتقاء ولا يدمج في trunk أبداً. وقد شرحنا هذه الطريقة بالتفصيل ومثالها العملي وكيف تنقل فريقك إليها من Git Flow في دليل [التطوير على الفرع الرئيسي Trunk-Based](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B7%D9%88%D9%8A%D8%B1-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D9%81%D8%B1%D8%B9-%D8%A7%D9%84%D8%B1%D8%A6%D9%8A%D8%B3%D9%8A-trunk-based/).

وأي واحدة تناسب فريقاً صغيراً؟ في أغلب الحالات GitHub Flow مع حماية main وفحوص CI إلزامية وSquash Merge، وهو في الواقع قريب جداً من Trunk-Based بفروع قصيرة، وإذا احتجت أن تدعم إصداراً قديماً عند عميل فأضف فرع إصدار عند الحاجة فقط وانقل إليه الإصلاحات بالانتقاء كما فعلنا في [قسم Cherry-pick](#cherry-pick). أما Git Flow فلا ننصح به لفريق صغير ينشر تطبيق ويب، والسبب أن فرع develop الدائم يؤخر وصول كل تغيير إلى الإنتاج ويضاعف التعارضات. وأين يختبر فريق ضمان الجودة QA إذا لم يكن هناك فرع develop؟ وكيف تعطي كل طلب دمج بيئة معاينة Preview خاصة به، وتنقل الإصدار نفسه من الاختبار إلى الإنتاج؟ هذا هو موضوع دليل [البيئات ومعاينة الميزات وGitOps للفرق](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D9%8A%D8%A6%D8%A7%D8%AA-%D9%88%D9%85%D8%B9%D8%A7%D9%8A%D9%86%D8%A9-%D8%A7%D9%84%D9%85%D9%8A%D8%B2%D8%A7%D8%AA-%D9%88-gitops-%D9%84%D9%84%D9%81%D8%B1%D9%82/).

## البحث عن الالتزام الذي كسر البرنامج: git bisect

لنفرض أن اختبار الخصم كان ينجح في الإصدار v1.0.0، وبعد تسعة التزامات من زملاء مختلفين أصبح يفشل، ولا أحد يعرف أي التزام هو السبب. والطريقة البسيطة أن ترجع إلى كل التزام وتختبره، وهذا ممكن مع تسعة التزامات، ولكنه غير عملي مع مئات الالتزامات. والأمر `git bisect` يستخدم [البحث الثنائي Binary Search](https://git-scm.com/docs/git-bisect?ref=arabroot.io)، فتعطيه التزاماً تعرف أنه سليم وآخر تعرف أنه معطوب، فيختار الالتزام الذي في المنتصف، وبحسب نتيجته يستبعد نصف الالتزامات، وهكذا حتى يصل إلى الالتزام الأول المعطوب. والأفضل من ذلك أن تعطيه أمراً يختبر به كل التزام بنفسه، وهذا هو الاختبار في مثالنا:

```bash
from app import discount
r = discount(100)
if r != 90:
    print("FAIL: discount(100) =", r)
    raise SystemExit(1)
print("ok")
```

ثم نبدأ البحث:

```bash
python test_app.py
git bisect start
git bisect bad HEAD
git bisect good v1.0.0
git bisect run python test_app.py
git bisect reset
```

والمخرج سوف يكون كما يلي:

```bash
FAIL: discount(100) = 80.0

status: waiting for both good and bad commits

status: waiting for good commit(s), bad commit known

Bisecting: 4 revisions left to test after this (roughly 2 steps)
[1ab5bcd2ac81c5afc02c0d5e34723d97da37354d] Add currency format

running 'python' 'test_app.py'
ok
Bisecting: 2 revisions left to test after this (roughly 1 step)
[d84756a272bb38ee02f8830a7e937e4dc8c36bba] Tune discount for campaign
running 'python' 'test_app.py'
FAIL: discount(100) = 80.0
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[74de293a5c70f1435292de7b381a7509ddaecfd4] Refactor pricing module
running 'python' 'test_app.py'
ok
d84756a272bb38ee02f8830a7e937e4dc8c36bba is the first bad commit
commit d84756a272bb38ee02f8830a7e937e4dc8c36bba
Author: ArabRoot Team <team@example.com>
Date:   Tue Oct 6 01:23:39 2026 +0300

    Tune discount for campaign

 CHANGELOG.md | 1 +
 app.py       | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)
bisect found first bad commit

Previous HEAD position was 74de293 Refactor pricing module
Switched to branch 'main'
```

في المخرج أعلاه لاحظ التالي:

- من تسعة التزامات اختبر Git ثلاثة فقط، وفي كل خطوة يطبع كم بقي، ومع ألف التزام لن يحتاج أكثر من عشر خطوات تقريباً.
- الأمر `git bisect run` يعتمد على رمز الخروج Exit Code للاختبار، فالصفر يعني سليم، والأرقام من 1 إلى 127 ما عدا 125 تعني معطوب، والرقم 125 يعني أن هذا الالتزام لا يمكن اختباره فيتخطاه، مثل التزام لا يبنى أصلاً.
- النتيجة هي «Tune discount for campaign» الذي غير الخصم إلى 20%، ولاحظ أن رسالته لا توحي بأنه يكسر شيئاً، وهذا هو الوضع الطبيعي في الحياة الحقيقية.
- الأمر `git bisect reset` يرجعك إلى الفرع الذي كنت فيه، فلا تنسه.

وهنا تظهر فائدة التاريخ النظيف، فالبحث الثنائي يعمل على أي تاريخ، ولكنه أسهل عندما يكون كل التزام يبنى وتنجح اختباراته، وعندما لا يكون التاريخ مليئاً بالتزامات «WIP» المكسورة التي يجب أن تتخطاها.

## الأخطاء الأكثر شيوعاً

### الدفع بالقوة إلى main

شخص أعاد تأسيس main عنده أو نفذ reset، ثم رفض السيرفر الدفع فأضاف `--force`، فاختفت من السيرفر التزامات زملائه. والحل الوقائي هو حماية الفرع على السيرفر كما شرحنا في [قسم العمل مع الفريق](#pull-requests)، وإذا حدث فلا تقم بأي دفع آخر، والتزامات الزملاء ما زالت في مستودعاتهم المحلية وفي reflog عند من دفع، فيعيدها من يملك آخر نسخة صحيحة.

### إعادة تأسيس فرع مشترك

وهي القاعدة الذهبية التي شرحناها في [قسم Rebase](#golden-rule)، والعلامة التي تبين أنها حدثت أن تظهر لك التزامات مكررة بالرسالة نفسها وأرقام مختلفة بعد السحب. وإذا كان الفرع لك وحدك فلا مشكلة، أما إذا كان مشتركاً فالقاعدة أن يتفق الفريق على أن الفروع المشتركة تدمج فقط.

### التزام سر ثم «حذفه»

شخص التزم ملف .env فيه كلمة مرور قاعدة البيانات، ثم انتبه فحذفه في التزام جديد، فهل انتهت المشكلة؟ المثال التالي يجيب:

```bash
git rm -q .env
git commit -qm "Remove .env"
ls -a
git log --oneline -- .env
git show HEAD~1:.env
```

والمخرج سوف يكون كما يلي:

```bash
.
..
.git
app.py

0c48231 Remove .env
c97e126 Add config

DB_PASSWORD=S3cr3t-Example
```

الملف اختفى من المجلد، ولكن `git log` يجد الالتزام الذي أضافه، و`git show` يطبع كلمة المرور كاملة من الالتزام السابق، وأي شخص لديه نسخة من المستودع يستطيع أن يفعل ذلك. لذلك فالترتيب الصحيح هو:

1. ألغ السر فوراً وأصدر بديلاً له، أي غير كلمة المرور وألغ المفتاح من الخدمة، وهذه هي [الخطوة الأولى التي يوصي بها GitHub](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository?ref=arabroot.io)، لأن السر الملغى لم يعد يفتح شيئاً.
2. إذا كان لا بد من إزالته من التاريخ فاستخدم [git filter-repo](https://github.com/newren/git-filter-repo?ref=arabroot.io)، وهي الأداة التي يوصي بها مشروع Git بدلاً من git filter-branch القديمة، والأمر `git filter-repo --path .env --invert-paths` يحذف الملف من كل التزامات المستودع، ثم تدفع النتيجة بالقوة وتطلب من الجميع أن يستنسخوا المستودع من جديد.
3. تذكر أن إعادة الكتابة لا تصل إلى النسخ والتفرعات Forks التي عند الآخرين، وأن المنصات قد تحتفظ بنسخ في طلبات الدمج والصفحات المخزنة، كما يذكر توثيق GitHub نفسه، ولهذا فالخطوة الأولى هي الإلغاء وليس التنظيف.

وأين تضع الأسرار بدلاً من المستودع؟ شرحنا ذلك في [قائمة تحقق المطور: الإعدادات والأسرار خارج ال Image](https://arabroot.io/articles/%D9%82%D8%A7%D8%A6%D9%85%D8%A9-%D8%AA%D8%AD%D9%82%D9%82-%D8%A7%D9%84%D9%85%D8%B7%D9%88%D8%B1-%D9%82%D8%A8%D9%84-%D9%86%D8%B4%D8%B1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%87/#config-secrets)، وما لا يجب أن يدخل المستودع عموماً في [دليل المبتدئين](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D9%88-git-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#secrets).

### أخطاء أخرى تتكرر

- **reset --hard فوق تعديلات لم تلتزم بها:** reflog لا ينقذها لأنها لم تدخل المستودع، فنفذ `git status` أولاً، واحفظ ما تحتاجه بالأمر `git stash`.
- **فرع يعيش أسابيع:** كل يوم إضافي يزيد التعارضات يوم الدمج، فقسم العمل إلى طلبات صغيرة.
- **Squash ثم الاستمرار على الفرع نفسه:** Git لا يعرف أن التزاماته دخلت main، فافتح فرعاً جديداً من main بعد كل دمج.
- **نسيان git bisect reset:** تبقى في التزام قديم دون فرع، ثم تلتزم عليه وتظن أنك في main.
- **نقل وسم إصدار منشور:** أصدر رقماً جديداً بدلاً من ذلك.

## جدول مختصر: الموقف والأمر

| الموقف                                   | الأمر                                                | يعيد كتابة التاريخ؟         |
| ---------------------------------------- | ---------------------------------------------------- | --------------------------- |
| دمج فرع ميزة مع إبقاء أثره               | git merge --no-ff feature                            | لا                          |
| تحديث فرعك المحلي دون التزام دمج         | git merge --ff-only origin/main                      | لا                          |
| سحب عمل الزملاء فوق التزاماتك المحلية    | git pull --rebase                                    | نعم، لالتزاماتك المحلية فقط |
| نقل فرعك فوق آخر main                    | git rebase main                                      | نعم                         |
| ترتيب التزاماتك قبل طلب الدمج            | git rebase -i main                                   | نعم                         |
| طلب كامل في التزام واحد                  | git merge --squash feature                           | لا                          |
| نسخ إصلاح إلى فرع إصدار                  | git cherry-pick -x <hash>                            | لا                          |
| التراجع عن التزام دفعته                  | git revert <hash>                                    | لا                          |
| إلغاء تعديل لم تلتزم به                  | git restore <file>                                   | لا، ويحذف التعديل نهائياً   |
| إلغاء آخر التزام محلي مع إبقاء التغييرات | git reset --soft HEAD\~1                             | نعم                         |
| حذف آخر التزام محلي وتغييراته            | git reset --hard HEAD\~1                             | نعم                         |
| استرجاع التزام بعد reset أو rebase       | git reflog ثم git branch rescue HEAD@{1}             | لا                          |
| حفظ عمل غير مكتمل مؤقتاً                 | git stash push -u -m "..."                           | لا                          |
| وسم إصدار                                | git tag -a v1.2.0 -m "..." ثم git push origin v1.2.0 | لا                          |
| الدفع بعد إعادة تأسيس فرعك               | git push --force-with-lease --force-if-includes      | نعم، على السيرفر            |
| البحث عن الالتزام الذي كسر اختباراً      | git bisect run <test>                                | لا                          |
| إعادة استخدام حلول التعارضات             | git config --global rerere.enabled true              | لا                          |

## تدريب عملي

1. أنشئ مستودعاً فيه فرع بخمسة التزامات، منها التزام «WIP» والتزام «fix typo»، ثم رتبها بالأمر `git rebase -i` إلى التزامين، ثم نفذ `git reset --hard HEAD~2` واسترجعهما من `git reflog`.
2. على Gitea عندك، فعل حماية main بمنع الدفع المباشر والدفع بالقوة واشترط نجاح مهمة CI، ثم حاول أن تدفع إلى main مباشرة وأن تدفع بالقوة، واقرأ رسالة الرفض.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- الدمج يضيف ولا يغير، فهو آمن على كل الفروع، والدمج السريع يحرك المؤشر فقط، والدمج الثلاثي أو `--no-ff` ينشئ التزام دمج يحفظ أثر الفرع.
- إعادة التأسيس تنشئ التزامات جديدة بأرقام جديدة، فاستخدمها لعملك المحلي ولفرعك الشخصي فقط، ولا تقم بها على فرع سحبه غيرك.
- `git pull --rebase` و`git rebase -i` يعطيانك تاريخاً نظيفاً قبل أن يراه أحد، وSquash Merge يعطيك النتيجة نفسها على main دون أن يتقن كل الفريق Rebase.
- الانتقاء ينسخ التزاماً برقم جديد، وهو الطريقة الصحيحة لنقل إصلاح إلى فرع إصدار، مع `-x` بين الفروع العامة.
- revert للفروع المشتركة، وreset لعملك المحلي، وreflog يحفظ لك الطريق إلى أي التزام لمدة محدودة وعلى جهازك فقط، وما لم تلتزم به لا ينقذه شيء.
- احم main على السيرفر من الدفع المباشر والدفع بالقوة، واشترط المراجعة ونجاح CI، وادفع إلى فرعك بالقوة بالصيغة `--force-with-lease --force-if-includes` فقط.
- لفريق صغير: فرع رئيسي واحد وطلبات دمج صغيرة وSquash وفروع إصدار عند الحاجة فقط.
- السر الذي دخل المستودع يلغى أولاً، ثم ينظف التاريخ إذا لزم بأداة git filter-repo.

[![](https://arabroot.io/content/images/2026/10/git-basics-cover-cover.webp)دليلإدارة النسخ وGit للمبتدئين: لماذا نحتاجها وكيف تعمللماذا تضيع الملفات بأسماء final\_v2 ويمسح الزملاء عمل بعضهم، وكيف يحل Git ذلك على مشروع حقيقي من أول commit.](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%86%D8%B3%D8%AE-%D9%88-git-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/gitea-cover-cover.webp)دليلGitea: خادم Git خاص لفريقك على سيرفرككيف تثبت Gitea على سيرفرك حتى يبقى كود فريقك عندك بتجربة تشبه GitHub، مع النسخ الاحتياطي والتحديث.](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-gitea-%D9%85%D8%B9-docker-compose/)[![](https://arabroot.io/content/images/2026/10/gitea-actions-cover-cover-2.webp)دليلGitea Actions: تشغيل CI/CD وبناء Docker Images على سيرفركاجعل سيرفرك يختبر الكود ويبني Docker Images تلقائياً مع كل push، دون أن يخرج كودك أو أسرارك إلى خدمة CI خارجية.](https://arabroot.io/articles/gitea-actions-%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-ci-cd-%D8%B9%D9%84%D9%89-%D8%AE%D8%A7%D8%AF%D9%85%D9%83/)[![](https://arabroot.io/content/images/2026/10/trunk-based-cover-cover.webp)دليلTrunk-Based Development: فروع قصيرة ودمج يوميلماذا تنفجر الفروع الطويلة يوم الإصدار، وكيف يدمج الفريق عمله في main كل يوم بأمان مع CI وFeature Flags.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B7%D9%88%D9%8A%D8%B1-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D9%81%D8%B1%D8%B9-%D8%A7%D9%84%D8%B1%D8%A6%D9%8A%D8%B3%D9%8A-trunk-based/)[![](https://arabroot.io/content/images/2026/10/environments-gitops-cover-cover.webp)دليلمن الفرع إلى الإنتاج: البيئات وGitOps للفرقكيف يختبر فريقك كل ميزة وحدها ويعرضها على العميل دون أن تصل ميزة غير جاهزة إلى الإنتاج.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D9%8A%D8%A6%D8%A7%D8%AA-%D9%88%D9%85%D8%B9%D8%A7%D9%8A%D9%86%D8%A9-%D8%A7%D9%84%D9%85%D9%8A%D8%B2%D8%A7%D8%AA-%D9%88-gitops-%D9%84%D9%84%D9%81%D8%B1%D9%82/)[![](https://arabroot.io/content/images/2026/10/dev-checklist-cover-cover.webp)دليلقبل أن تنشر تطبيقك على خادمك: قائمة تحقق للمطورالأخطاء التي لا تظهر إلا بعد نشر تطبيقك في Docker خلف Reverse Proxy، وكيف تعالج كل واحد منها قبل أن يصل إلى المستخدمين.](https://arabroot.io/articles/%D9%82%D8%A7%D8%A6%D9%85%D8%A9-%D8%AA%D8%AD%D9%82%D9%82-%D8%A7%D9%84%D9%85%D8%B7%D9%88%D8%B1-%D9%82%D8%A8%D9%84-%D9%86%D8%B4%D8%B1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%87/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ كل الأمثلة على Git 2.48.1 في مستودعات محلية، مع سيرفر Git محلي في مستودع Bare لأمثلة الدفع والسحب.