رفتن به محتوا
LoopX

rebase

توی این درس یاد می‌گیری با git rebase تاریخچه‌ای تمیز و خطی بسازی: کارهای شاخه‌ات را روی آخرین نسخه‌ی main دوباره بچینی (git rebase main)، conflict وسط rebase را با --continue و --abort مدیریت کنی، و با rebase تعاملی (git rebase -i HEAD~3) commit های شلوغ را یکی (squash)، اصلاح (reword) یا حذف (drop) کنی. مهم‌تر از همه قانون طلایی را می‌فهمی: هرگز commit هایی را که دیگران دارند rebase نکن. تمرین: سه commit کوچک را با rebase تعاملی یکی کن.

دو دردسر معمول: (۱) وقتی روی ویژگی کار می‌کردی main جلو رفته و با merge هر بار یک commit ادغام اضافه می‌شود؛ تاریخچه پر از شاخه‌های درهم می‌شود. (۲) وسط کار commit هایی مثل wip، fix typo، oops زده‌ای که برای هیچ‌کس معنی ندارد و نمی‌خواهی در تاریخچه‌ی نهایی باشند. rebase هر دو را حل می‌کند: شاخه‌ات را روی آخرین main بازپخش می‌کند و اجازه می‌دهد commit های خودت را قبل از اشتراک‌گذاری مرتب کنی.

تشبیه: بازپخش، نه کپی‌کردن

Section titled “تشبیه: بازپخش، نه کپی‌کردن”

merge مثل چسباندن دو دفتر است: هر دو دفتر می‌ماند و یک برگ ادغام بینشان می‌اندازی. rebase مثل این است که کارهای خودت را از دفتر قدیمی برداری و روی صفحه‌ی آخرِ دفتر جدید دوباره بنویسی؛ نتیجه‌ی کار همان است، ولی انگار از اول از آخرین صفحه شروع کرده‌ای. (و چون «دوباره نوشته‌ای»، صفحه‌هایت شناسه‌ی جدید می‌گیرند، یعنی commit های جدید ساخته می‌شوند.)

قبل: feature از C2 جدا شده ولی main تا C3 جلو رفته. بعد از rebase: دو commit شاخه (خط‌چین‌ها، قدیمی) روی C3 بازپخش شده‌اند و commit های جدید F1' و F2' ساخته شده‌اند؛ شاخه یک خط صاف بعد از main است.
merge تاریخچه را همان‌طور که اتفاق افتاد نگه می‌دارد (با commit ادغام)؛ rebase آن را خطی و تمیز بازنویسی می‌کند ولی commit ها را عوض می‌کند.

یک «وبلاگ» با دو commit روی main. بعد یک شاخه‌ی ویژگی با سه commit نیمه‌کاره، و main هم جلو می‌رود:

Terminal window
mkdir -p ~/gitlab/blog && cd ~/gitlab/blog && git init -q
echo "# Blog" > README.md && git add README.md && git commit -qm "Init blog"
echo "<header>" > header.html && git add header.html && git commit -qm "Add header"
git switch -qc feature/posts
echo "post list" > posts.html && git add posts.html && git commit -qm "wip posts"
echo "post list v2" > posts.html && git commit -qam "wip posts 2"
echo "post list v2 fixed" > posts.html && git commit -qam "fix typo"
git switch -q main
echo "<footer>" > footer.html && git add footer.html && git commit -qm "Add footer"
git log --oneline --graph --all
خروجی
* 8774bab fix typo
* 0a140c8 wip posts 2
* 2b8a7bc wip posts
| * d0e7f22 Add footer
|/
* 67b22ea Add header
* ce3c757 Init blog

برو روی شاخه‌ی خودت و بگو «روی main بازپخش شو». دقت کن هش commit ها را قبل و بعد:

Terminal window
cd ~/gitlab/blog
git switch -q feature/posts
echo "--- قبل (هش commit های شاخه):"
git log --oneline main..feature/posts
git rebase main
echo "--- بعد (همان تغییرها، هش جدید):"
git log --oneline main..feature/posts
echo "--- گراف:"
git log --oneline --graph --all
خروجی
--- قبل (هش commit های شاخه):
8774bab fix typo
0a140c8 wip posts 2
2b8a7bc wip posts
Successfully rebased and updated refs/heads/feature/posts.
--- بعد (همان تغییرها، هش جدید):
e5dc83d fix typo
2c5e616 wip posts 2
3ecee62 wip posts
--- گراف:
* e5dc83d fix typo
* 2c5e616 wip posts 2
* 3ecee62 wip posts
* d0e7f22 Add footer
* 67b22ea Add header
* ce3c757 Init blog

سه چیز: ۱) تاریخچه حالا یک خط صاف است: feature/posts درست بعد از آخرین commit main ایستاده. ۲) هر سه commit هش جدید گرفتند: Git آن‌ها را دوباره ساخت (با همان تغییر و پیام، پایه‌ی جدید). ۳) main..feature/posts یعنی «commit هایی که در feature هست و در main نیست». چون خطی است، ادغام بعدی fast-forward و بدون commit ادغام می‌شود:

Terminal window
cd ~/gitlab/blog
git switch -q main
git merge feature/posts
git log --oneline --graph
خروجی
Updating d0e7f22..e5dc83d
Fast-forward
posts.html | 1 +
1 file changed, 1 insertion(+)
create mode 100644 posts.html
* e5dc83d fix typo
* 2c5e616 wip posts 2
* 3ecee62 wip posts
* d0e7f22 Add footer
* 67b22ea Add header
* ce3c757 Init blog

rebase هر commit را یکی‌یکی روی پایه‌ی جدید می‌گذارد؛ پس اگر commit ای با تغییر main تضاد داشته باشد، همان‌جا متوقف می‌شود. (حل‌کردن همان مهارت درس قبل است.)

Terminal window
mkdir -p ~/gitlab/rb2 && cd ~/gitlab/rb2 && git init -q
printf 'title: Blog\ncolor: blue\n' > site.cfg && git add site.cfg && git commit -qm "Add config"
git switch -qc feature/theme
sed -i 's/color: blue/color: green/' site.cfg && git commit -qam "Use green theme"
echo "extra" > extra.txt && git add extra.txt && git commit -qm "Add extra file"
git switch -q main
sed -i 's/color: blue/color: red/' site.cfg && git commit -qam "Use red theme"
git switch -q feature/theme
echo "=== rebase:"
git rebase main 2>&1
echo "=== وضعیت وسط rebase:"
git status -s
خروجی
=== rebase:
Auto-merging site.cfg
CONFLICT (content): Merge conflict in site.cfg
error: could not apply 8f32d1e... Use green theme
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 8f32d1e... Use green theme
=== وضعیت وسط rebase:
UU site.cfg

Git پیام را کامل می‌گوید: could not apply ... Use green theme و راه‌حل‌ها: حل کن، git add، git rebase --continue؛ یا --skip (این commit را رها کن)، یا --abort (همه را لغو کن). حل می‌کنم (قرمز را نگه می‌دارم و سبز را کنار می‌گذارم) و ادامه می‌دهم:

Terminal window
cd ~/gitlab/rb2
printf 'title: Blog\ncolor: green-red\n' > site.cfg
git add site.cfg
GIT_EDITOR=true git rebase --continue 2>&1 | tail -2
echo "=== نتیجه:"
git log --oneline --graph --all
خروجی
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/feature/theme.
=== نتیجه:
* 371ed25 Add extra file
* 312f832 Use green theme
* e48eeac Use red theme
* 275516a Add config

(GIT_EDITOR=true فقط برای این است که ویرایشگر پیام باز نشود؛ در ترمینال واقعی خودش باز می‌شود و پیام را تأیید می‌کنی.) بعد از --continue Git commit بعدی را هم بازپخش کرد و تمام شد. اگر وسط کار پشیمان شوی: git rebase --abort دقیقاً به حالت قبل از rebase برمی‌گرداند:

Terminal window
mkdir -p ~/gitlab/rb3 && cd ~/gitlab/rb3 && git init -q
echo a > f && git add f && git commit -qm "Base"
git switch -qc x && echo b > f && git commit -qam "X change"
git switch -q main && echo c > f && git commit -qam "Main change"
git switch -q x
git rebase main 2>&1 | tail -1
git rebase --abort
git status | head -2
git log --oneline
خروجی
Could not apply 6fe5420... X change
On branch x
nothing to commit, working tree clean
6fe5420 X change
14fb844 Base

مثال ۳: rebase تعاملی، فهرست کارها

Section titled “مثال ۳: rebase تعاملی، فهرست کارها”

git rebase -i (interactive) یک فهرست از commit هایی که قرار است بازپخش شوند را در ویرایشگر نشان می‌دهد؛ جلوی هر کدام یک دستور است و تو می‌توانی آن را عوض کنی. ببین فهرست واقعی چه شکلی است (با ترمینال مجازی و nano):

Terminal window
run_in_tmux() { tmux -u kill-server 2>/dev/null; tmux -u new-session -d -s t -x 90 -y "${TH:-24}" "$@"; sleep 1; }
screen() { tmux capture-pane -t t -p | sed -e :a -e '/^[[:space:]]*$/{$d;N;ba' -e '}'; }
git config --global core.editor nano
cd ~/gitlab/blog
git switch -qc interactive
for i in 1 2 3; do echo "line $i" >> notes.txt; git add notes.txt; git commit -qm "wip $i"; done
TH=24; run_in_tmux "bash --norc"
tmux send-keys -t t "cd ~/gitlab/blog && git rebase -i HEAD~3" Enter; sleep 2
screen | head -22
tmux send-keys -t t C-x; sleep 1
tmux -u kill-server
git switch -q main; git branch -qD interactive
خروجی
GNU nano 7.2 /home/ali/gitlab/blog/.git/rebase-merge/git-rebase-todo
pick e90d6cb wip 1
pick 10e2a1b wip 2
pick 282ba73 wip 3
# Rebase e5dc83d..282ba73 onto e5dc83d (3 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
[ Read 34 lines ]

بالا سه commit با ترتیب قدیمی ← جدید (برعکس git log) و دستور pick جلوی هر کدام است. زیرش فهرست همه‌ی دستورها آمده. با عوض‌کردن کلمه‌ی اول هر خط، تعیین می‌کنی با آن commit چه شود:

دستور کار
pick (p) همان commit را نگه دار
reword (r) commit را نگه دار ولی پیامش را عوض کن
edit (e) بایست تا commit را (محتوا یا پیام) تغییر بدهم، بعد --continue
squash (s) commit را با قبلی یکی کن و پیام‌ها را ترکیب کن
fixup (f) مثل squash ولی پیام این commit را دور بریز
drop (d) این commit را حذف کن (یا خط را پاک کن)
(ترتیب خط‌ها) عوض‌کردن جای خط‌ها = عوض‌کردن ترتیب commit ها

(من اینجا فقط فهرست را نشان دادم و با Ctrl+X بدون تغییر بستم، پس هیچ commit ای عوض نشد. در مثال‌های بعدی تغییر را اعمال می‌کنم. برای قابل‌اجرا بودن، به‌جای ویرایش دستی فهرست، کلمه‌ها را با متغیر GIT_SEQUENCE_EDITOR و sed عوض می‌کنم؛ هم‌ارز همان کاری است که تو با دست می‌کنی.)

مثال ۴: یکی‌کردن commit ها، squash (تمرین اصلی)

Section titled “مثال ۴: یکی‌کردن commit ها، squash (تمرین اصلی)”

سه commit «wip» را به یک commit با پیام معنادار تبدیل می‌کنم. اول یک اسکریپت کوچک برای نوشتن پیام می‌سازم (به‌جای تایپ در ویرایشگر)، و فهرست را طوری عوض می‌کنم که commit اول pick و دو تای بعدی squash باشند:

cd ~/gitlab/blog
cat > ~/set-msg.sh <<'EOF'
#!/bin/sh
printf '%s\n' "$MSG" > "$1"
EOF
chmod +x ~/set-msg.sh
git switch -qc squash-demo
for i in 1 2 3; do echo "line $i" >> notes.txt; git add notes.txt; git commit -qm "wip $i"; done
echo "=== قبل:"
git log --oneline -4
GIT_SEQUENCE_EDITOR="sed -i '2s/^pick/squash/;3s/^pick/squash/'" MSG="Add notes page" GIT_EDITOR=~/set-msg.sh git rebase -i HEAD~3 2>&1 | tail -1
echo "=== بعد: سه commit یکی شد:"
git log --oneline -4
git show --stat --format='%h %s' HEAD
خروجی
=== قبل:
23b26ab wip 3
8e248d5 wip 2
733a05d wip 1
e5dc83d fix typo
Successfully rebased and updated refs/heads/squash-demo.
=== بعد: سه commit یکی شد:
e199963 Add notes page
e5dc83d fix typo
2c5e616 wip posts 2
3ecee62 wip posts
e199963 Add notes page
notes.txt | 3 +++
1 file changed, 3 insertions(+)

شماره‌ی خط‌ها در فهرست از ۱ شروع می‌شود (2s/... یعنی «خط دوم»). نتیجه: یک commit با پیام تازه که هر سه تغییر را دارد و در جلوی تاریخچه‌ای که هیچ wip ای ندارد. (تغییر خود فایل‌ها یکی است؛ فقط تعداد commit ها کم شد.)

مثال ۵: reword و drop و عوض‌کردن ترتیب

Section titled “مثال ۵: reword و drop و عوض‌کردن ترتیب”

یک اجرای rebase می‌تواند چند کار را با هم انجام دهد. این‌جا: پیام اولی را اصلاح می‌کنم (reword)، commit میانی را حذف می‌کنم (drop):

Terminal window
cd ~/gitlab/blog
git switch -q main && git branch -qD squash-demo
git switch -qc multi
echo "a" > a.txt && git add a.txt && git commit -qm "add a (typo mesage)"
echo "oops" > debug.txt && git add debug.txt && git commit -qm "debug junk"
echo "b" > b.txt && git add b.txt && git commit -qm "add b"
echo "=== قبل (از قدیم به جدید، آخر لیست جدیدترین):"
git log --oneline --reverse -3
GIT_SEQUENCE_EDITOR="sed -i '1s/^pick/reword/;2s/^pick/drop/'" MSG="Add a" GIT_EDITOR=~/set-msg.sh git rebase -i HEAD~3 2>&1 | tail -1
echo "=== بعد:"
git log --oneline --reverse -3
ls
خروجی
=== قبل (از قدیم به جدید، آخر لیست جدیدترین):
e52235f add a (typo mesage)
a78286c debug junk
f490c16 add b
Successfully rebased and updated refs/heads/multi.
=== بعد:
e5dc83d fix typo
28077a2 Add a
c6b1296 add b
README.md
a.txt
b.txt
footer.html
header.html
posts.html

debug.txt اصلاً وجود ندارد (commit اش حذف شد) و پیام اولی درست شد. برای عوض‌کردن ترتیب فقط جای خط‌ها را عوض می‌کنی (مثلاً با sed یا در ویرایشگر خط را ببری و بچسبانی).

مثال ۶: روش مدرن، --fixup و --autosquash

Section titled “مثال ۶: روش مدرن، --fixup و --autosquash”

بجای اینکه بعداً دنبال commit ها بگردی، همان لحظه که اشتباه را می‌بینی، یک commit اصلاحی با اشاره به commit مربوطه می‌زنی؛ بعد یک دستور همه را به جای درست می‌چسباند:

Terminal window
cd ~/gitlab/blog
git switch -q main && git branch -qD multi
git switch -qc autosq
echo "login form" > login.html && git add login.html && git commit -qm "Add login form"
echo "other work" > other.txt && git add other.txt && git commit -qm "Add other feature"
echo "login form with label" > login.html
git commit -aq --fixup HEAD~1
echo "=== تاریخچه (commit اصلاحی با پیام fixup! ساخته شد):"
git log --oneline -3
GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash HEAD~3 2>&1 | tail -1
echo "=== بعد از autosquash (اصلاح داخل commit اصلی رفت):"
git log --oneline -3
cat login.html
خروجی
=== تاریخچه (commit اصلاحی با پیام fixup! ساخته شد):
ea6d9a9 fixup! Add login form
fc20d2f Add other feature
8c28d45 Add login form
Successfully rebased and updated refs/heads/autosq.
=== بعد از autosquash (اصلاح داخل commit اصلی رفت):
da1c18c Add other feature
e3caea9 Add login form
e5dc83d fix typo
login form with label

git commit --fixup commit یک commit با پیام fixup! ... می‌سازد؛ --autosquash در فهرست rebase آن را خودکار زیر commit مربوط می‌گذارد و fixup می‌کند. (GIT_SEQUENCE_EDITOR=true فقط فهرست پیشنهادی را بدون ویرایش تأیید می‌کند.) تا git config --global rebase.autosquash true را بگذاری، دیگر --autosquash هم لازم نیست.

مثال ۷: بعد از rebase یک شاخه‌ی push‌شده، --force-with-lease

Section titled “مثال ۷: بعد از rebase یک شاخه‌ی push‌شده، --force-with-lease”

rebase commit ها را عوض می‌کند؛ پس اگر شاخه‌ات قبلاً روی سرور رفته، push معمولی رد می‌شود (درس قبل: non-fast-forward). برای شاخه‌ی خودت (که هیچ‌کس دیگر روی آن کار نمی‌کند) این‌جا قانونی است که با --force-with-lease push کنی:

Terminal window
cd ~/gitlab
git init -q --bare blog-server.git
cd blog
git remote add origin ~/gitlab/blog-server.git
git switch -q main
git switch -qc feature/mine
echo "mine" > mine.txt && git add mine.txt && git commit -qm "Add mine"
git push -q -u origin feature/mine 2>&1 | tail -1
echo "main جلو می‌رود و من rebase می‌کنم:"
git switch -q main && echo "new" > new.txt && git add new.txt && git commit -qm "Main moves on"
git switch -q feature/mine && git rebase -q main
echo "=== push معمولی:"
git push 2>&1 | head -3
echo "=== push با --force-with-lease:"
git push --force-with-lease 2>&1 | tail -2
خروجی
main جلو می‌رود و من rebase می‌کنم:
=== push معمولی:
To /home/ali/gitlab/blog-server.git
! [rejected] feature/mine -> feature/mine (non-fast-forward)
error: failed to push some refs to '/home/ali/gitlab/blog-server.git'
=== push با --force-with-lease:
To /home/ali/gitlab/blog-server.git
+ 8adee61...8a0bd27 feature/mine -> feature/mine (forced update)

--force-with-lease از --force امن‌تر است: فقط وقتی می‌نویسد که سرور هنوز همان چیزی را دارد که آخرین بار از آن دیدی؛ اگر کسی دیگر در این فاصله چیزی push کرده باشد، رد می‌کند و کار او را بی‌صدا پاک نمی‌کند. (دقت: تا وقتی remote و push را کامل یاد نگرفته‌ای فقط الگو را بدان؛ درس «مخزن راه دور».)

rebase تاریخچه را بازنویسی می‌کند، ولی commit های قدیمی بلافاصله پاک نمی‌شوند. Git قبل از rebase جای قبلی را در ORIG_HEAD و در reflog نگه می‌دارد، پس اشتباه rebase را می‌شود برگرداند:

Terminal window
cd ~/gitlab/blog
git switch -q main && git switch -qc undo-demo
echo "z" > z.txt && git add z.txt && git commit -qm "Z work"
git switch -q main && echo "q" > q.txt && git add q.txt && git commit -qm "Main moves"
git switch -q undo-demo
before=$(git rev-parse --short HEAD)
git rebase -q main
echo "--- بعد از rebase، commit «Z work» هش جدید دارد:"
git log --oneline -2
echo "--- ORIG_HEAD همان جایی است که قبل از rebase بودیم (قبلاً: $before):"
git rev-parse --short ORIG_HEAD
echo "--- برگشت از rebase با reset --hard ORIG_HEAD:"
git reset -q --hard ORIG_HEAD
git log --oneline --graph main undo-demo
git switch -q main && git branch -qD undo-demo
خروجی
--- بعد از rebase، commit «Z work» هش جدید دارد:
47b1e86 Z work
98be5a2 Main moves
--- ORIG_HEAD همان جایی است که قبل از rebase بودیم (قبلاً: 6e98993):
6e98993
--- برگشت از rebase با reset --hard ORIG_HEAD:
* 98be5a2 Main moves
| * 6e98993 Z work
|/
* 5999f79 Main moves on
* e5dc83d fix typo
* 2c5e616 wip posts 2
* 3ecee62 wip posts
* d0e7f22 Add footer
* 67b22ea Add header
* ce3c757 Init blog

git reset --hard ORIG_HEAD شاخه را به قبل از rebase برمی‌گرداند (تا وقتی عملیات دیگری ORIG_HEAD را عوض نکرده)؛ و در هر حال git reflog همه‌ی جابه‌جایی‌ها را دارد (درس «نجات»).

پشت پرده: rebase دقیقاً چه می‌کند؟

Section titled “پشت پرده: rebase دقیقاً چه می‌کند؟”

rebase این چهار کار را می‌کند: ۱) commit های شاخه‌ی تو را که روی main نیستند فهرست می‌کند؛ ۲) HEAD را روی پایه‌ی جدید (آخر main) می‌گذارد؛ ۳) هر commit را یکی‌یکی بازپخش می‌کند (همان تغییر را دوباره روی پایه‌ی جدید اعمال و commit می‌کند)؛ ۴) در پایان برچسب شاخه را به آخرین commit جدید می‌برد. این‌ها در reflog ثبت می‌شود:

Terminal window
mkdir -p ~/gitlab/rbinside && cd ~/gitlab/rbinside && git init -q
echo 1 > f && git add f && git commit -qm "Base"
git switch -qc topic && echo a > a && git add a && git commit -qm "A" && echo b > b && git add b && git commit -qm "B"
git switch -q main && echo 2 > g && git add g && git commit -qm "Main work"
git switch -q topic
git rebase main > /dev/null 2>&1
echo "--- reflog از قدیم به جدید (فقط مراحل rebase):"
git reflog | grep rebase | tac
echo "--- commit ها شناسه‌ی جدید گرفتند ولی تغییرشان همان است:"
git log --oneline main..topic
خروجی
--- reflog از قدیم به جدید (فقط مراحل rebase):
c0a3727 HEAD@{3}: rebase (start): checkout main
816755e HEAD@{2}: rebase (pick): A
f8019ea HEAD@{1}: rebase (pick): B
f8019ea HEAD@{0}: rebase (finish): returning to refs/heads/topic
--- commit ها شناسه‌ی جدید گرفتند ولی تغییرشان همان است:
f8019ea B
816755e A

rebase (start)، یک rebase (pick) برای هر commit و rebase (finish). commit های قدیمی (با هش‌های قبلی) هنوز در .git/objects هستند تا garbage collection پاکشان کند؛ ولی هیچ برچسبی به آن‌ها اشاره نمی‌کند. پس rebase یعنی بازنویسی تاریخچه: درست مثل amend و reset، فقط روی commit های خصوصی امن است.

دستور کار
git rebase main شاخه‌ی فعلی را روی آخرین main بازپخش کن
git rebase -i HEAD~3 rebase تعاملی روی ۳ commit آخر
git rebase -i main rebase تعاملی روی همه‌ی commit های شاخه
git rebase --continue بعد از حل conflict ادامه بده
git rebase --skip این commit را رها کن
git rebase --abort لغو و برگشت به قبل
git commit --fixup commit commit اصلاحی برای یک commit قدیمی
git rebase -i --autosquash main fixup ها را خودکار جابه‌جا کن
git push --force-with-lease push امن‌تر بعد از rebase
git reset --hard ORIG_HEAD برگشت از یک rebase اشتباه
می‌خواهم… بزن
تاریخچه‌ی خطی قبل از ادغام git rebase main سپس merge (fast-forward)
۳ commit را یکی کنم rebase -i HEAD~3، دو تای آخر squash
پیام یک commit قدیمی را عوض کنم rebase -i، reword
یک commit را حذف کنم rebase -i، drop
ترتیب commit ها را عوض کنم rebase -i، جابه‌جایی خط‌ها
شاخه‌ی مشترک را به‌روز کنم git merge main (نه rebase)

۱) rebase روی شاخه‌ی مشترک

Section titled “۱) rebase روی شاخه‌ی مشترک”

قانون طلایی. راه‌حل: برای شاخه‌ی مشترک merge؛ rebase فقط برای کار خصوصی.

۲) rebase با تغییر commit نشده

Section titled “۲) rebase با تغییر commit نشده”
Terminal window
mkdir -p ~/gitlab/dirty && cd ~/gitlab/dirty && git init -q
echo a > f && git add f && git commit -qm "Base"
git switch -qc t && echo b >> f && git commit -qam "T"
git switch -q main && echo c > g && git add g && git commit -qm "M"
git switch -q t
echo "کار نیمه‌تمام" >> f
git rebase main 2>&1 | head -3
خروجی
error: cannot rebase: You have unstaged changes.
error: Please commit or stash them.

cannot rebase: You have unstaged changes: rebase پوشه‌ی کاری را چند بار عوض می‌کند، پس تغییر ثبت‌نشده را نمی‌پذیرد. راه‌حل: commit کن یا git stash (درس بعد).

Terminal window
cd ~/gitlab/dirty
git restore f
git switch -qc sq && echo 1 > n && git add n && git commit -qm "N1" && echo 2 >> n && git commit -qam "N2"
GIT_SEQUENCE_EDITOR="sed -i '1s/^pick/squash/'" GIT_EDITOR=true git rebase -i HEAD~2 2>&1 | head -3
git status | head -2
git rebase --abort 2>/dev/null; git status | head -1
خروجی
error: cannot 'squash' without a previous commit
You can fix this with 'git rebase --edit-todo' and then run 'git rebase --continue'.
Or you can abort the rebase with 'git rebase --abort'.
interactive rebase in progress; onto 7d8c7d5
No commands done.
On branch sq

squash یعنی «با commit قبلی یکی شو»؛ برای اولین خط قبلی وجود ندارد. راه‌حل: همیشه اولین خط pick بماند و بقیه squash/fixup شوند.

بعد از حل conflict و git add، rebase تمام نشده؛ تا git rebase --continue نزنی وسط کار می‌مانی (در git status می‌نویسد rebase in progress). راه‌حل: --continue، یا --abort اگر پشیمان شدی.

rebase چند commit را روی هم بازپخش می‌کند و ممکن است همان conflict را چند بار نشان دهد. راه‌حل: git config --global rerere.enabled true (درس قبل) تا حل‌هایت به‌خاطر سپرده شود؛ یا اول با squash commit ها را کم کن و بعد rebase.

✎ تمرینآسان

یک شاخه‌ی ویژگی با دو commit بساز، در main یک commit جدید بزن، شاخه‌ی ویژگی را روی main rebase کن و ثابت کن تاریخچه خطی است (git log --oneline --graph).

دیدن جواب
Terminal window
mkdir -p ~/gitlab/ex1 && cd ~/gitlab/ex1 && git init -q
echo base > base.txt && git add base.txt && git commit -qm "Base"
git switch -qc feature
echo 1 > f1 && git add f1 && git commit -qm "Feature 1"
echo 2 > f2 && git add f2 && git commit -qm "Feature 2"
git switch -q main
echo m > m.txt && git add m.txt && git commit -qm "Main moves"
git switch -q feature
git rebase main
git log --oneline --graph --all
خروجی
Successfully rebased and updated refs/heads/feature.
* 5a33c62 Feature 2
* b33c80a Feature 1
* 3e32224 Main moves
* ea0e279 Base
✎ تمرینمتوسط

تمرین اصلی: سه commit کوچک را با rebase تعاملی یکی کن. سه commit با پیام‌های wip، wip 2، fix بزن؛ بعد با rebase -i HEAD~3 آن‌ها را به یک commit با پیام «Add user profile page» تبدیل کن و ثابت کن تاریخچه تمیز شد.

دیدن جواب
Terminal window
mkdir -p ~/gitlab/ex2 && cd ~/gitlab/ex2 && git init -q
echo base > base.txt && git add base.txt && git commit -qm "Base"
echo "name" > profile.txt && git add profile.txt && git commit -qm "wip"
echo "name, email" > profile.txt && git commit -qam "wip 2"
echo "name, email, avatar" > profile.txt && git commit -qam "fix"
echo "=== قبل:"; git log --oneline
cat > ~/set-msg.sh <<'EOF'
#!/bin/sh
printf '%s\n' "$MSG" > "$1"
EOF
chmod +x ~/set-msg.sh
GIT_SEQUENCE_EDITOR="sed -i '2s/^pick/squash/;3s/^pick/squash/'" MSG="Add user profile page" GIT_EDITOR=~/set-msg.sh git rebase -i HEAD~3 2>&1 | tail -1
echo "=== بعد:"; git log --oneline
cat profile.txt
خروجی
=== قبل:
5d4ee6d fix
a6a94c2 wip 2
552590c wip
ea0e279 Base
Successfully rebased and updated refs/heads/main.
=== بعد:
1887e41 Add user profile page
ea0e279 Base
name, email, avatar
✎ تمرینسخت

با --fixup و --autosquash: دو commit بزن («Add parser» و «Add tests»)، بعد متوجه یک غلط در commit اول شو و با یک commit اصلاحی و autosquash آن را داخل خود commit اول بچسبان؛ ثابت کن همچنان دو commit دارد و تغییر داخل commit اول رفته (git show).

دیدن جواب
Terminal window
mkdir -p ~/gitlab/ex3 && cd ~/gitlab/ex3 && git init -q
echo base > base.txt && git add base.txt && git commit -qm "Base"
echo "def parse(x): return x.split()" > parser.py && git add parser.py && git commit -qm "Add parser"
echo "assert parse('a b') == ['a','b']" > test_parser.py && git add test_parser.py && git commit -qm "Add tests"
echo "def parse(x): return x.strip().split()" > parser.py
git commit -aq --fixup HEAD~1
git log --oneline
GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash HEAD~3 2>&1 | tail -1
echo "=== بعد:"; git log --oneline
echo "--- تغییر داخل commit اول رفته:"
git show --format='%s' HEAD~1 -- parser.py | tail -3
خروجی
acd96e1 fixup! Add parser
3181ddb Add tests
2d3530b Add parser
ea0e279 Base
Successfully rebased and updated refs/heads/main.
=== بعد:
ee9cd4e Add tests
fc99d64 Add parser
ea0e279 Base
--- تغییر داخل commit اول رفته:
+++ b/parser.py
@@ -0,0 +1 @@
+def parse(x): return x.strip().split()
⚡ بررسی سریع

چرا rebase کردن commit هایی که قبلاً push و دیگران pull کرده‌اند خطرناک است؟

؟ آزمونک
  1. git rebase main (روی شاخه‌ی feature) چه می‌کند؟

  2. در فهرست rebase تعاملی، تفاوت squash و fixup؟

  3. وسط rebase conflict داشتی و حلش کردی. چه باید کرد؟

  4. بعد از rebase یک شاخه‌ی خصوصی که قبلاً push شده، کدام push مناسب است؟

  5. اولین خط فهرست rebase تعاملی را squash کردی. چه می‌شود؟

  • git rebase main commit های شاخه‌ات را روی آخرین main بازپخش می‌کند: تاریخچه خطی و ادغام بعدی fast-forward. commit ها شناسه‌ی جدید می‌گیرند (تاریخچه بازنویسی می‌شود).
  • conflict وسط rebase: حل ← git add ← git rebase --continue؛ یا --skip / --abort.
  • rebase تعاملی (git rebase -i HEAD~N): pick، reword، edit، squash، fixup، drop و جابه‌جایی خط‌ها؛ اولین خط pick بماند. --fixup و --autosquash روش مدرن‌اند.
  • بعد از rebase شاخه‌ی push‌شده‌ی شخصی: git push --force-with-lease (نه --force).
  • قانون طلایی: commit های مشترک را rebase نکن. برای شاخه‌ی مشترک merge؛ rebase برای کار خصوصی و مرتب‌کردن قبل از اشتراک.
  • تور نجات: ORIG_HEAD و git reflog. rebase با تغییر commit نشده خطا می‌دهد.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
git rebase mainشاخه‌ی فعلی را روی main بازپخش کن
git rebase -i HEAD~3rebase تعاملی ۳ commit آخر
(pick / reword / squash / fixup / drop)دستورهای فهرست تعاملی
git rebase --continueبعد از حل conflict ادامه
git rebase --abortلغو و برگشت
git rebase --skipاین commit را رها کن
git commit --fixup commitcommit اصلاحی
git rebase -i --autosquash HEAD~5fixup ها را خودکار جابه‌جا کن
git push --force-with-leasepush بعد از rebase (شاخه‌ی شخصی)
git reset --hard ORIG_HEADبرگشت از rebase اشتباه