Rebase، Stash و تاریخچه تمیز
دو ابزار قدرتمند برای حرفهایها: rebase برای تاریخچه تمیز و خطی، و stash برای ذخیره موقت کارهای ناتمام. در این فصل interactive rebase، squash، fixup و تفاوت merge با rebase را میبینیم.
دو ابزار قدرتمند برای حرفهایها: rebase برای تاریخچه تمیز و خطی، و stash برای ذخیره موقت کارهای ناتمام. در این فصل interactive rebase، squash، fixup و تفاوت merge با rebase را میبینیم.
Rebase در مقابل Merge
هر دو همان هدف را دارند: ادغام تغییرات. اما روش متفاوت:
قبل:
main: A ── B ── C ←── main
D ── E ←── feature
بعد از merge feature به main:
main: A ── B ── C ── M ←── main
╱
D ── E
بعد از rebase feature روی main:
main: A ── B ── C ── D'─ E' ←── feature (حالا)
(بعد main:= feature → fast-forward)
| ویژگی | Merge | Rebase |
|---|---|---|
| تاریخچه | غیرخطی، با merge commit | خطی |
| commit جدید | merge commit ساخته میشود | commitها بازنویسی میشوند |
| تاریخچه محفوظ | بله | خیر (hash جدید) |
| مناسب | branchهای public/shared | feature branch محلی |
| conflict | یکبار در merge | ممکن است در هر commit |
Rebase ساده
# روی feature branch هستید، میخواهید با main همگام شوید
git switch feature
git fetch origin
git rebase origin/main
# Git هر commit شما را یکی یکی روی main جدید پخش میکند
# اگر conflict شد:
# 1. حل کنید
nano file.py
git add file.py
# 2. ادامه
git rebase --continue
# لغو
git rebase --abort
# skip یک commit
git rebase --skip
چرا rebase؟
قبل از rebase:
main: A ── B ── C ── F ── G ←── main
D ── E ←── feature
بعد از rebase:
main: A ── B ── C ── F ── G ←── main
D'── E' ←── feature
# حالا feature روی آخرین main است
# میتوانید fast-forward merge کنید
git switch main
git merge feature # fast-forward، بدون merge commit
Interactive Rebase
قدرتمندترین قابلیت Git: ویرایش تاریخچه با جزئیات.
# 5 commit آخر را ویرایش کن
git rebase -i HEAD~5
# یا از یک commit خاص
git rebase -i a3f4b2c
# editor باز میشود:
pick a1b2c3d Add login form
pick d4e5f6g Fix typo
pick h7i8j9k Add validation
pick l0m1n2o WIP
pick p3q4r5s Add tests
# Commands:
# p, pick = استفاده از این commit همانطور که هست
# r, reword = تغییر پیام
# e, edit = توقف برای ویرایش
# s, squash = ادغام در commit قبلی + ویرایش پیام
# f, fixup = squash بدون ویرایش پیام (پیام قبلی نگهداشته میشود)
# d, drop = حذف کامل commit
# x, exec = اجرای command shell
مثال ۱: Squash چند commit
قبل از:
pick a1b2c3d Add login form
pick d4e5f6g Fix typo
pick h7i8j9k Add validation
تغییر دهید به:
pick a1b2c3d Add login form
fixup d4e5f6g Fix typo # ادغام در قبلی
fixup h7i8j9k Add validation # ادغام در قبلی
# ذخیره و خروج
# تاریخچه نهایی: یک commit
# "Add login form" (شامل همه تغییرات)
مثال ۲: تغییر پیام
pick a1b2c3d Add login form
reword d4e5f6g Fix typo ← این
pick h7i8j9k Add validation
# پنجره دوم باز میشود برای پیام جدید
مثال ۳: حذف commit
pick a1b2c3d Add login form
drop d4e5f6g Add debug logs ← حذف
pick h7i8j9k Add validation
مثال ۴: تغییر ترتیب
کافی است خطوط را جابهجا کنید:
pick h7i8j9k Add validation # اول
pick a1b2c3d Add login form # دوم
pick d4e5f6g Fix typo # سوم
مثال ۵: ویرایش یک commit
pick a1b2c3d Add login form
edit d4e5f6g Fix typo ← این
pick h7i8j9k Add validation
# Git در آن commit متوقف میشود
# تغییرات بدهید
nano file.py
git add file.py
git commit --amend
git rebase --continue
–autosquash
روش هوشمند برای squash هنگام commit:
# commit اصلی
git commit -m "Add login form"
# a1b2c3d
# بعداً متوجه باگ میشوید
git commit --fixup a1b2c3d
# پیام خودکار: "fixup! Add login form"
# یا برای squash با ویرایش پیام
git commit --squash a1b2c3d
# بعد از همه fixupها، rebase خودکار:
git rebase -i --autosquash HEAD~5
# Git خودکار fixupها را زیر commit اصلی میچیند
تنظیم همیشگی
git config --global rebase.autosquash true
–onto
برای انتقال یک رشته از commitها به مکان دیگر:
قبل:
main: A ── B ── C
D ── E ──── F ── G ←── feature
# میخواهیم F و G را روی main برداریم (D و E را نه)
git rebase --onto main E feature
بعد:
main: A ── B ── C
F'── G' ←── feature
git stash – ذخیره موقت
سناریو: در میانه کار هستید، ناگهان باید روی main bug fix بزنید. اما کارتان آماده commit نیست.
دستورات اصلی stash
# ذخیره موقت همه تغییرات
git stash
# با پیام
git stash push -m "WIP: working on user profile"
# لیست stashها
git stash list
# stash@{0}: WIP on feature: a3f4b2c WIP working on user profile
# stash@{1}: WIP on main: b2c3d4e Quick fix attempt
# بازگردانی آخرین stash (و حذف از لیست)
git stash pop
# بازگردانی بدون حذف
git stash apply
# بازگردانی stash خاص
git stash apply stash@{2}
git stash pop stash@{2}
# نمایش محتوای stash
git stash show
git stash show -p stash@{0} # با diff کامل
# حذف stash
git stash drop stash@{1}
# پاک کردن همه stashها
git stash clear
چه چیزهایی stash میشود؟
# پیشفرض: tracked فایلهای modified و staged
git stash
# همراه untracked
git stash -u
git stash --include-untracked
# همراه ignored هم
git stash -a
git stash --all
# فقط فایلهای خاص (Git 2.13+)
git stash push file1.py file2.py
# فقط staged (Git 2.35+)
git stash push --staged
# با interactive (مثل git add -p)
git stash push -p
سناریو کامل
# در حال کار روی feature
# فایلهای modified زیادی دارید
# bug fix فوری در main لازم است
git stash push -m "WIP: feature work"
# کار روی main
git switch main
git pull
# bug fix...
git commit -m "fix: critical bug"
git push
# بازگشت
git switch feature
git stash pop
# تغییرات شما برگشتند
ساخت branch از stash
# سناریو: stash قدیمی دارید که میخواهید روی branch جدید کار کنید
git stash branch new-feature stash@{2}
# branch جدید از زمان stash ساخته میشود + stash apply میشود
amend vs stash vs rebase
| کار | دستور |
|---|---|
| تغییر آخرین commit | git commit --amend |
| تغییر چند commit | git rebase -i HEAD~N |
| ذخیره موقت بدون commit | git stash |
| ادغام چند commit به یک | rebase + squash/fixup |
| همگام با main بدون merge commit | git rebase main |
جریان کاری حرفهای
# 1. شروع feature
git switch -c feature/payment
# 2. commitهای زیاد در حال کار
git commit -m "WIP: payment form structure"
git commit -m "fix typo"
git commit -m "add validation"
git commit -m "fix validation bug"
git commit -m "WIP: backend integration"
git commit -m "add tests"
git commit -m "fix test"
# 3. آماده برای PR، تاریخچه را تمیز کن
git rebase -i HEAD~7
# در editor:
pick a1 WIP: payment form structure
fixup a2 fix typo
fixup a3 add validation
fixup a4 fix validation bug
pick a5 WIP: backend integration
fixup a6 add tests
fixup a7 fix test
# 4. تغییر پیامها
reword a1 feat: add payment form with validation
reword a5 feat: integrate payment backend with tests
# 5. push
git push -u origin feature/payment
# 6. اگر main تغییر کرد قبل از merge:
git fetch origin
git rebase origin/main
git push --force-with-lease # چون rebase تاریخچه را عوض کرد
سیاست تیمی
هر تیم سیاست خودش را دارد. سه گزینه رایج:
۱. همیشه Merge (تاریخچه کامل)
- تاریخچه کامل اما شلوغ
- هیچوقت rebase نکنید
- مناسب: تیمهایی که میخواهند تاریخچه دقیق
۲. Rebase + Merge (پیشنهاد رایج)
- محلی rebase کنید
- هنگام merge به main، با merge commit
- تاریخچه تمیز اما با groupبندی PRها
۳. Rebase + Fast-Forward (تاریخچه کاملاً خطی)
- قبل از merge همیشه rebase
- merge فقط fast-forward
- تاریخچه مستقیم بدون شاخه
- یا squash merge در GitHub
git pull –rebase
بهجای merge هنگام pull، رفتار rebase:
# pull معمولی → merge commit میسازد اگر divergent باشد
git pull
# pull با rebase → تاریخچه خطی
git pull --rebase
# تنظیم default
git config --global pull.rebase true
# برای یک repo خاص
git config pull.rebase true
reset vs rebase vs revert
| ابزار | کار | مناسب برای |
|---|---|---|
reset |
عقب بردن HEAD | local commits |
rebase |
بازنویسی commitها | local feature branch |
revert |
commit جدید برای undo | commitهای push شده |
نکات کاربردی
۱. تنظیم editor برای rebase
git config --global core.editor "code --wait"
# حالا rebase -i در VS Code باز میشود
�2. abort هر rebase که گیر کرده
git rebase --abort
# اگر کاملاً گیر کرده:
git reflog
git reset --hard HEAD@{N} # قبل از rebase
۳. حفاظت از branch
# هنگام rebase دیگر developerها مشکل پیدا میکنند
# main را protected کنید (در فصل ۵)
# تنها از طریق PR + squash merge
۴. stash با دیگر دستورات
# تعویض branch با stash خودکار (Git 2.5+)
git switch --autostash other-branch
# pull با autostash
git pull --autostash
# تنظیم همیشه
git config --global rebase.autostash true
مثال کامل: PR تمیز
# شروع
git switch main
git pull
git switch -c feature/dark-mode
# توسعه با commitهای زیاد
echo "..." > dark.css
git commit -am "start dark mode"
git commit -am "fix"
git commit -am "more"
git commit -am "WIP"
git commit -am "tests"
# تاریخچه شلوغ - تمیز کن
git rebase -i HEAD~5
# همه را به دو commit معنادار تبدیل کن:
# - "feat: add dark mode CSS and toggle"
# - "test: add dark mode tests"
# همگام با main
git fetch origin
git rebase origin/main
# اگر هیچ تغییر برخوردی نبود، حالا feature branch روی آخرین main است
# push
git push -u origin feature/dark-mode
# اگر قبلاً push کرده بودید:
git push --force-with-lease
# PR باز کنید
# بعد از merge، در GitHub باید "squash and merge" زده شده باشد
# یا اگر تاریخچه تمیز کردهاید، "rebase and merge"
بهترین شیوهها
- هرگز branchهای public را rebase نکنید
- قبل از PR، تاریخچه را با interactive rebase تمیز کنید
- fixup + autosquash برای جریان روزمره
- stash برای کارهای موقت، نه کارهای طولانیمدت
- force-push با –force-with-lease (نه –force)
autostashبرای راحتتر کردن pull/rebase- سیاست تیمی واضح در CONTRIBUTING.md
- commitهای semantic (Conventional Commits) قبل از rebase
جمعبندی
- Rebase = بازنویسی تاریخچه برای خطی شدن
- Interactive rebase: pick، squash، fixup، reword، edit، drop
- autosquash + commit –fixup برای حرفهایها
- Stash برای ذخیره موقت کار
- قانون طلایی: rebase فقط commitهای local
- force-push با –force-with-lease
- سیاست تیمی روشن مهمتر از انتخاب صحیح است
در فصل بعد، Tagها و Releaseها در GitHub را یاد میگیریم.