توی این درس یاد میگیری با git rebase تاریخچهای تمیز و خطی بسازی: کارهای شاخهات را روی آخرین نسخهی main دوباره بچینی (git rebase main)، conflict وسط rebase را با --continue و --abort مدیریت کنی، و با rebase تعاملی (git rebase -i HEAD~3) commit های شلوغ را یکی (squash)، اصلاح (reword) یا حذف (drop) کنی. مهمتر از همه قانون طلایی را میفهمی: هرگز commit هایی را که دیگران دارند rebase نکن. تمرین: سه commit کوچک را با rebase تعاملی یکی کن.
مسئله: تاریخچهی شلوغ
Section titled “مسئله: تاریخچهی شلوغ”دو دردسر معمول: (۱) وقتی روی ویژگی کار میکردی main جلو رفته و با merge هر بار یک commit ادغام اضافه میشود؛ تاریخچه پر از شاخههای درهم میشود. (۲) وسط کار commit هایی مثل wip، fix typo، oops زدهای که برای هیچکس معنی ندارد و نمیخواهی در تاریخچهی نهایی باشند. rebase هر دو را حل میکند: شاخهات را روی آخرین main بازپخش میکند و اجازه میدهد commit های خودت را قبل از اشتراکگذاری مرتب کنی.
تشبیه: بازپخش، نه کپیکردن
Section titled “تشبیه: بازپخش، نه کپیکردن”merge مثل چسباندن دو دفتر است: هر دو دفتر میماند و یک برگ ادغام بینشان میاندازی. rebase مثل این است که کارهای خودت را از دفتر قدیمی برداری و روی صفحهی آخرِ دفتر جدید دوباره بنویسی؛ نتیجهی کار همان است، ولی انگار از اول از آخرین صفحه شروع کردهای. (و چون «دوباره نوشتهای»، صفحههایت شناسهی جدید میگیرند، یعنی commit های جدید ساخته میشوند.)
مثالهای عملی
Section titled “مثالهای عملی”یک «وبلاگ» با دو commit روی main. بعد یک شاخهی ویژگی با سه commit نیمهکاره، و main هم جلو میرود:
mkdir -p ~/gitlab/blog && cd ~/gitlab/blog && git init -qecho "# 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/postsecho "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 mainecho "<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مثال ۱: git rebase main
Section titled “مثال ۱: git rebase main”برو روی شاخهی خودت و بگو «روی main بازپخش شو». دقت کن هش commit ها را قبل و بعد:
cd ~/gitlab/bloggit switch -q feature/postsecho "--- قبل (هش commit های شاخه):"git log --oneline main..feature/postsgit rebase mainecho "--- بعد (همان تغییرها، هش جدید):"git log --oneline main..feature/postsecho "--- گراف:"git log --oneline --graph --all--- قبل (هش commit های شاخه):8774bab fix typo0a140c8 wip posts 22b8a7bc wip postsSuccessfully rebased and updated refs/heads/feature/posts.--- بعد (همان تغییرها، هش جدید):e5dc83d fix typo2c5e616 wip posts 23ecee62 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 ادغام میشود:
cd ~/gitlab/bloggit switch -q maingit merge feature/postsgit log --oneline --graphUpdating d0e7f22..e5dc83dFast-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مثال ۲: conflict وسط rebase
Section titled “مثال ۲: conflict وسط rebase”rebase هر commit را یکییکی روی پایهی جدید میگذارد؛ پس اگر commit ای با تغییر main تضاد داشته باشد، همانجا متوقف میشود. (حلکردن همان مهارت درس قبل است.)
mkdir -p ~/gitlab/rb2 && cd ~/gitlab/rb2 && git init -qprintf 'title: Blog\ncolor: blue\n' > site.cfg && git add site.cfg && git commit -qm "Add config"git switch -qc feature/themesed -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 mainsed -i 's/color: blue/color: red/' site.cfg && git commit -qam "Use red theme"git switch -q feature/themeecho "=== rebase:"git rebase main 2>&1echo "=== وضعیت وسط rebase:"git status -s=== rebase:Auto-merging site.cfgCONFLICT (content): Merge conflict in site.cfgerror: could not apply 8f32d1e... Use green themehint: Resolve all conflicts manually, mark them as resolved withhint: "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.cfgGit پیام را کامل میگوید: could not apply ... Use green theme و راهحلها: حل کن، git add، git rebase --continue؛ یا --skip (این commit را رها کن)، یا --abort (همه را لغو کن). حل میکنم (قرمز را نگه میدارم و سبز را کنار میگذارم) و ادامه میدهم:
cd ~/gitlab/rb2printf 'title: Blog\ncolor: green-red\n' > site.cfggit add site.cfgGIT_EDITOR=true git rebase --continue 2>&1 | tail -2echo "=== نتیجه:"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 برمیگرداند:
mkdir -p ~/gitlab/rb3 && cd ~/gitlab/rb3 && git init -qecho 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 xgit rebase main 2>&1 | tail -1git rebase --abortgit status | head -2git log --onelineCould not apply 6fe5420... X changeOn branch xnothing to commit, working tree clean6fe5420 X change14fb844 Baseمثال ۳: rebase تعاملی، فهرست کارها
Section titled “مثال ۳: rebase تعاملی، فهرست کارها”git rebase -i (interactive) یک فهرست از commit هایی که قرار است بازپخش شوند را در ویرایشگر نشان میدهد؛ جلوی هر کدام یک دستور است و تو میتوانی آن را عوض کنی. ببین فهرست واقعی چه شکلی است (با ترمینال مجازی و nano):
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 nanocd ~/gitlab/bloggit switch -qc interactivefor i in 1 2 3; do echo "line $i" >> notes.txt; git add notes.txt; git commit -qm "wip $i"; doneTH=24; run_in_tmux "bash --norc"tmux send-keys -t t "cd ~/gitlab/blog && git rebase -i HEAD~3" Enter; sleep 2screen | head -22tmux send-keys -t t C-x; sleep 1tmux -u kill-servergit switch -q main; git branch -qD interactive GNU nano 7.2 /home/ali/gitlab/blog/.git/rebase-merge/git-rebase-todopick e90d6cb wip 1pick 10e2a1b wip 2pick 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/blogcat > ~/set-msg.sh <<'EOF'#!/bin/shprintf '%s\n' "$MSG" > "$1"EOFchmod +x ~/set-msg.shgit switch -qc squash-demofor i in 1 2 3; do echo "line $i" >> notes.txt; git add notes.txt; git commit -qm "wip $i"; doneecho "=== قبل:"git log --oneline -4GIT_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 -1echo "=== بعد: سه commit یکی شد:"git log --oneline -4git show --stat --format='%h %s' HEAD=== قبل:23b26ab wip 38e248d5 wip 2733a05d wip 1e5dc83d fix typoSuccessfully rebased and updated refs/heads/squash-demo.=== بعد: سه commit یکی شد:e199963 Add notes pagee5dc83d fix typo2c5e616 wip posts 23ecee62 wip postse199963 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):
cd ~/gitlab/bloggit switch -q main && git branch -qD squash-demogit switch -qc multiecho "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 -3GIT_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 -1echo "=== بعد:"git log --oneline --reverse -3ls=== قبل (از قدیم به جدید، آخر لیست جدیدترین):e52235f add a (typo mesage)a78286c debug junkf490c16 add bSuccessfully rebased and updated refs/heads/multi.=== بعد:e5dc83d fix typo28077a2 Add ac6b1296 add bREADME.mda.txtb.txtfooter.htmlheader.htmlposts.htmldebug.txt اصلاً وجود ندارد (commit اش حذف شد) و پیام اولی درست شد. برای عوضکردن ترتیب فقط جای خطها را عوض میکنی (مثلاً با sed یا در ویرایشگر خط را ببری و بچسبانی).
مثال ۶: روش مدرن، --fixup و --autosquash
Section titled “مثال ۶: روش مدرن، --fixup و --autosquash”بجای اینکه بعداً دنبال commit ها بگردی، همان لحظه که اشتباه را میبینی، یک commit اصلاحی با اشاره به commit مربوطه میزنی؛ بعد یک دستور همه را به جای درست میچسباند:
cd ~/gitlab/bloggit switch -q main && git branch -qD multigit switch -qc autosqecho "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.htmlgit commit -aq --fixup HEAD~1echo "=== تاریخچه (commit اصلاحی با پیام fixup! ساخته شد):"git log --oneline -3GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash HEAD~3 2>&1 | tail -1echo "=== بعد از autosquash (اصلاح داخل commit اصلی رفت):"git log --oneline -3cat login.html=== تاریخچه (commit اصلاحی با پیام fixup! ساخته شد):ea6d9a9 fixup! Add login formfc20d2f Add other feature8c28d45 Add login formSuccessfully rebased and updated refs/heads/autosq.=== بعد از autosquash (اصلاح داخل commit اصلی رفت):da1c18c Add other featuree3caea9 Add login forme5dc83d fix typologin form with labelgit 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 کنی:
cd ~/gitlabgit init -q --bare blog-server.gitcd bloggit remote add origin ~/gitlab/blog-server.gitgit switch -q maingit switch -qc feature/mineecho "mine" > mine.txt && git add mine.txt && git commit -qm "Add mine"git push -q -u origin feature/mine 2>&1 | tail -1echo "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 mainecho "=== push معمولی:"git push 2>&1 | head -3echo "=== push با --force-with-lease:"git push --force-with-lease 2>&1 | tail -2main جلو میرود و من 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 را کامل یاد نگرفتهای فقط الگو را بدان؛ درس «مخزن راه دور».)
مثال ۸: تور نجات، ORIG_HEAD
Section titled “مثال ۸: تور نجات، ORIG_HEAD”rebase تاریخچه را بازنویسی میکند، ولی commit های قدیمی بلافاصله پاک نمیشوند. Git قبل از rebase جای قبلی را در ORIG_HEAD و در reflog نگه میدارد، پس اشتباه rebase را میشود برگرداند:
cd ~/gitlab/bloggit switch -q main && git switch -qc undo-demoecho "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-demobefore=$(git rev-parse --short HEAD)git rebase -q mainecho "--- بعد از rebase، commit «Z work» هش جدید دارد:"git log --oneline -2echo "--- ORIG_HEAD همان جایی است که قبل از rebase بودیم (قبلاً: $before):"git rev-parse --short ORIG_HEADecho "--- برگشت از rebase با reset --hard ORIG_HEAD:"git reset -q --hard ORIG_HEADgit log --oneline --graph main undo-demogit switch -q main && git branch -qD undo-demo--- بعد از rebase، commit «Z work» هش جدید دارد:47b1e86 Z work98be5a2 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 bloggit reset --hard ORIG_HEAD شاخه را به قبل از rebase برمیگرداند (تا وقتی عملیات دیگری ORIG_HEAD را عوض نکرده)؛ و در هر حال git reflog همهی جابهجاییها را دارد (درس «نجات»).
پشت پرده: rebase دقیقاً چه میکند؟
Section titled “پشت پرده: rebase دقیقاً چه میکند؟”rebase این چهار کار را میکند: ۱) commit های شاخهی تو را که روی main نیستند فهرست میکند؛ ۲) HEAD را روی پایهی جدید (آخر main) میگذارد؛ ۳) هر commit را یکییکی بازپخش میکند (همان تغییر را دوباره روی پایهی جدید اعمال و commit میکند)؛ ۴) در پایان برچسب شاخه را به آخرین commit جدید میبرد. اینها در reflog ثبت میشود:
mkdir -p ~/gitlab/rbinside && cd ~/gitlab/rbinside && git init -qecho 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 topicgit rebase main > /dev/null 2>&1echo "--- reflog از قدیم به جدید (فقط مراحل rebase):"git reflog | grep rebase | tacecho "--- commit ها شناسهی جدید گرفتند ولی تغییرشان همان است:"git log --oneline main..topic--- reflog از قدیم به جدید (فقط مراحل rebase):c0a3727 HEAD@{3}: rebase (start): checkout main816755e HEAD@{2}: rebase (pick): Af8019ea HEAD@{1}: rebase (pick): Bf8019ea HEAD@{0}: rebase (finish): returning to refs/heads/topic--- commit ها شناسهی جدید گرفتند ولی تغییرشان همان است:f8019ea B816755e Arebase (start)، یک rebase (pick) برای هر commit و rebase (finish). commit های قدیمی (با هشهای قبلی) هنوز در .git/objects هستند تا garbage collection پاکشان کند؛ ولی هیچ برچسبی به آنها اشاره نمیکند. پس rebase یعنی بازنویسی تاریخچه: درست مثل amend و reset، فقط روی commit های خصوصی امن است.
قانون طلایی rebase
Section titled “قانون طلایی rebase”جدولهای مرجع
Section titled “جدولهای مرجع”| دستور | کار |
|---|---|
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) |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) rebase روی شاخهی مشترک
Section titled “۱) rebase روی شاخهی مشترک”قانون طلایی. راهحل: برای شاخهی مشترک merge؛ rebase فقط برای کار خصوصی.
۲) rebase با تغییر commit نشده
Section titled “۲) rebase با تغییر commit نشده”mkdir -p ~/gitlab/dirty && cd ~/gitlab/dirty && git init -qecho 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 techo "کار نیمهتمام" >> fgit rebase main 2>&1 | head -3error: cannot rebase: You have unstaged changes.error: Please commit or stash them.cannot rebase: You have unstaged changes: rebase پوشهی کاری را چند بار عوض میکند، پس تغییر ثبتنشده را نمیپذیرد. راهحل: commit کن یا git stash (درس بعد).
۳) squash روی اولین commit
Section titled “۳) squash روی اولین commit”cd ~/gitlab/dirtygit restore fgit 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 -3git status | head -2git rebase --abort 2>/dev/null; git status | head -1error: cannot 'squash' without a previous commitYou 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 7d8c7d5No commands done.On branch sqsquash یعنی «با commit قبلی یکی شو»؛ برای اولین خط قبلی وجود ندارد. راهحل: همیشه اولین خط pick بماند و بقیه squash/fixup شوند.
۴) فراموش کردن --continue
Section titled “۴) فراموش کردن --continue”بعد از حل conflict و git add، rebase تمام نشده؛ تا git rebase --continue نزنی وسط کار میمانی (در git status مینویسد rebase in progress). راهحل: --continue، یا --abort اگر پشیمان شدی.
۵) حل دوبارهی یک conflict
Section titled “۵) حل دوبارهی یک conflict”rebase چند commit را روی هم بازپخش میکند و ممکن است همان conflict را چند بار نشان دهد. راهحل: git config --global rerere.enabled true (درس قبل) تا حلهایت بهخاطر سپرده شود؛ یا اول با squash commit ها را کم کن و بعد rebase.
یک شاخهی ویژگی با دو commit بساز، در main یک commit جدید بزن، شاخهی ویژگی را روی main rebase کن و ثابت کن تاریخچه خطی است (git log --oneline --graph).
دیدن جواب
mkdir -p ~/gitlab/ex1 && cd ~/gitlab/ex1 && git init -qecho base > base.txt && git add base.txt && git commit -qm "Base"git switch -qc featureecho 1 > f1 && git add f1 && git commit -qm "Feature 1"echo 2 > f2 && git add f2 && git commit -qm "Feature 2"git switch -q mainecho m > m.txt && git add m.txt && git commit -qm "Main moves"git switch -q featuregit rebase maingit log --oneline --graph --allSuccessfully 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» تبدیل کن و ثابت کن تاریخچه تمیز شد.
دیدن جواب
mkdir -p ~/gitlab/ex2 && cd ~/gitlab/ex2 && git init -qecho 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 --onelinecat > ~/set-msg.sh <<'EOF'#!/bin/shprintf '%s\n' "$MSG" > "$1"EOFchmod +x ~/set-msg.shGIT_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 -1echo "=== بعد:"; git log --onelinecat profile.txt=== قبل:5d4ee6d fixa6a94c2 wip 2552590c wipea0e279 BaseSuccessfully rebased and updated refs/heads/main.=== بعد:1887e41 Add user profile pageea0e279 Basename, email, avatarبا --fixup و --autosquash: دو commit بزن («Add parser» و «Add tests»)، بعد متوجه یک غلط در commit اول شو و با یک commit اصلاحی و autosquash آن را داخل خود commit اول بچسبان؛ ثابت کن همچنان دو commit دارد و تغییر داخل commit اول رفته (git show).
دیدن جواب
mkdir -p ~/gitlab/ex3 && cd ~/gitlab/ex3 && git init -qecho 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.pygit commit -aq --fixup HEAD~1git log --onelineGIT_SEQUENCE_EDITOR=true git rebase -i --autosquash HEAD~3 2>&1 | tail -1echo "=== بعد:"; git log --onelineecho "--- تغییر داخل commit اول رفته:"git show --format='%s' HEAD~1 -- parser.py | tail -3acd96e1 fixup! Add parser3181ddb Add tests2d3530b Add parserea0e279 BaseSuccessfully rebased and updated refs/heads/main.=== بعد:ee9cd4e Add testsfc99d64 Add parserea0e279 Base--- تغییر داخل commit اول رفته:+++ b/parser.py@@ -0,0 +1 @@+def parse(x): return x.strip().split()آزمونک
Section titled “آزمونک”چرا rebase کردن commit هایی که قبلاً push و دیگران pull کردهاند خطرناک است؟
rebase تاریخچه را بازنویسی میکند. روی commit های مشترک، دیگران نسخهی قدیمی را دارند و با نسخهی جدید سازگار نیست؛ برای شاخهی مشترک از merge استفاده کن.
git rebase main (روی شاخهی feature) چه میکند؟
تغییرها همان است، پایه عوض میشود.
در فهرست rebase تعاملی، تفاوت squash و fixup؟
drop برای حذف کامل است.
وسط rebase conflict داشتی و حلش کردی. چه باید کرد؟
--abort لغو میکند، --skip آن commit را رها میکند.
بعد از rebase یک شاخهی خصوصی که قبلاً push شده، کدام push مناسب است؟
--force-with-lease از --force امنتر است؛ اگر کسی چیز جدیدی push کرده باشد رد میکند.
اولین خط فهرست rebase تعاملی را squash کردی. چه میشود؟
squash یعنی «با قبلی یکی شو».
جمعبندی
Section titled “جمعبندی”git rebase maincommit های شاخهات را روی آخرین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~3 | rebase تعاملی ۳ commit آخر |
(pick / reword / squash / fixup / drop) | دستورهای فهرست تعاملی |
git rebase --continue | بعد از حل conflict ادامه |
git rebase --abort | لغو و برگشت |
git rebase --skip | این commit را رها کن |
git commit --fixup commit | commit اصلاحی |
git rebase -i --autosquash HEAD~5 | fixup ها را خودکار جابهجا کن |
git push --force-with-lease | push بعد از rebase (شاخهی شخصی) |
git reset --hard ORIG_HEAD | برگشت از rebase اشتباه |