گیتمتوسط

ریبیس تعاملی: کامیت‌هایتان را قبل از اشتراک‌گذاری مرتب کنید

با git rebase -i یک ردِ شلوغ از کامیت‌های نیمه‌کاره را به تاریخچه‌ای تمیز و قابل بازبینی تبدیل کنید. کامیت‌ها را ادغام، بازنویسی، جابه‌جا، تقسیم و حذف کنید و بگذارید کامیت‌های fixup مرتب‌سازی را برایتان انجام دهند.

پنج کامیت شلوغ که ادغام و مرتب می‌شوند و به دو کامیت تمیز تبدیل می‌شوند
در این صفحه

کارِ واقعی شلوغ است. یک فرمِ نیمه‌کاره را کامیت می‌کنید، بعد «WIP»، بعد «fix typo»، بعد یک print برای دیباگ که یادتان رفته پاکش کنید. تا وقتی مشغول کارید اشکالی ندارد، اما این تاریخچهٔ خوبی برای تحویل به بازبین نیست، و برای پیدا کردنش شش ماه بعد در git log یا git blame از آن هم بدتر است.

ریبیس تعاملی (interactive rebase) به شما اجازه می‌دهد برنچ خودتان را قبل از اشتراک‌گذاری بازنویسی کنید: کامیت‌ها را با هم ادغام کنید، پیامشان را از نو بنویسید، ترتیبشان را عوض کنید، آن‌ها را تقسیم یا حذف کنید. وقتی با برنچ‌ها راحت شدید، این یکی از مفیدترین مهارت‌هایی است که در گیت می‌توانید یاد بگیرید. اگر هنوز راحت نیستید، اول روش کاری عملی با برنچ‌های گیت برای مبتدی‌ها را بخوانید.

برنچِ شلوغ#

این یک برنچ فیچر با پنج کامیت روی main است:

Bash
$ git log --oneline main..
746aec7 debug print
451c410 fix typo
63d1135 Add password validation
0ce6ff7 WIP
4430f61 Add login form

main.. یعنی «کامیت‌هایی از این برنچ که در main نیستند». چیزی که می‌خواهیم به اشتراک بگذاریم فقط دو کامیت است: Add login form (به‌همراه WIP و رفع غلط تایپی که هر دو روی همان فرم بودند) و Add password validation، بدون print دیباگ.

شروع ریبیس تعاملی#

Bash
git rebase -i main

گیت ویرایشگرتان را با یک فهرست کار (todo list) باز می‌کند؛ برای هر کامیت یک خط:

Text
pick 4430f61 Add login form
pick 0ce6ff7 WIP
pick 63d1135 Add password validation
pick 451c410 fix typo
pick 746aec7 debug print

# Rebase 1c893f9..746aec7 onto 1c893f9 (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, ...
# d, drop <commit> = remove commit
# ...

فهرست کار نسبت به git log برعکس است

git log جدیدترین کامیت را اول نشان می‌دهد. فهرست کار قدیمی‌ترین را اول نشان می‌دهد، چون گیت کامیت‌ها را به همین ترتیب دوباره اعمال می‌کند. آن را مثل یک اسکریپت از بالا به پایین بخوانید.

گیت از main شروع می‌کند و خط‌ها را یکی‌یکی اجرا می‌کند. کلمهٔ اول هر خط را عوض کنید تا بلایی که سر آن کامیت می‌آید عوض شود:

دستور چه کاری می‌کند
pick کامیت را همان‌طور که هست نگه می‌دارد
reword تغییرات را نگه می‌دارد، اما می‌ایستد تا پیام را ویرایش کنید
edit کامیت را اعمال می‌کند و بعد مکث می‌کند تا آن را تغییر دهید (amend، تقسیم و…)
squash در کامیتِ بالایی ادغام می‌شود و هر دو پیام را با هم ترکیب می‌کند
fixup در کامیتِ بالایی ادغام می‌شود و پیامِ این کامیت را دور می‌ریزد
drop کامیت را حذف می‌کند (پاک کردنِ خط هم همین کار را می‌کند)
exec در این نقطه یک دستور شل اجرا می‌کند و اگر شکست بخورد می‌ایستد

ادغام، جابه‌جایی و حذف#

فهرست کار را این‌طور ویرایش کنید، بعد ذخیره کنید و ویرایشگر را ببندید:

Text
pick 4430f61 Add login form
fixup 0ce6ff7 WIP
fixup 451c410 fix typo
pick 63d1135 Add password validation
drop 746aec7 debug print

سه چیز را عوض کردیم: دو کامیتِ کوچک به fixup Add login form تبدیل شدند، fix typo را بالا بردیم تا درست زیرِ کامیتی قرار بگیرد که اصلاحش می‌کند، و print دیباگ حذف شد.

Bash
$ git rebase -i main
Successfully rebased and updated refs/heads/login-form.

$ git log --oneline main..
486c6b2 Add password validation
24c6dfe Add login form
⁧فهرست کار، از بالا به پایین⁩ ⁧نتیجه⁩ pickfixupfixuppickdrop 4430f610ce6ff7451c41063d1135746aec7 Add login formWIPfix typoAdd password validationdebug print ⁧حذف شد⁩ 24c6dfeAdd login form486c6b2Add password validation ⁧⁦fixup⁩ یک کامیت را در کامیتِ بالایی‌اش ذوب می‌کند و پیامش را دور می‌ریزد⁩
فهرست کار از بالا به پایین خوانده می‌شود: دو fixup در Add login form ذوب می‌شوند، Add password validation می‌ماند و print دیباگ حذف می‌شود؛ نتیجه دو کامیتِ تمیز است

دقت کنید که هش‌ها عوض شدند، حتی برای Add password validation که به محتوایش دست نزدیم. ریبیس هرگز کامیت‌ها را ویرایش نمی‌کند: کامیت‌های جدیدی با همان تغییرات می‌سازد. این نکته جلوتر، موقع push، مهم می‌شود.

squash یا fixup؟

وقتی پیامِ کامیت دوم چیزی دارد که ارزش نگه‌داشتن دارد از squash استفاده کنید: گیت ویرایشگر را با هر دو پیام باز می‌کند تا آن‌ها را با هم ترکیب کنید. برای کامیت‌های «اوه، یادم رفت» که پیامشان را به هر حال پاک می‌کردید، fixup را به کار ببرید.

کامیت‌های fixup: در حین کار مرتب کنید#

جابه‌جا کردنِ دستیِ خط‌ها خسته‌کننده می‌شود. اگر از قبل می‌دانید یک تغییر مال کدام کامیت است، موقع کامیت کردن به گیت بگویید:

Bash
# Spotted a bug in "Add password validation"? Fix it, then:
git commit -a --fixup=486c6b2

گیت کامیتی به نام fixup! Add password validation می‌سازد. بعداً --autosquash همهٔ این کامیت‌ها را زیرِ کامیتِ هدفشان می‌برد و برایتان fixup علامت می‌زند:

Bash
$ git rebase -i --autosquash main
Text
pick 24c6dfe Add login form
pick 486c6b2 Add password validation
fixup 4eb2adb fixup! Add password validation

فقط ذخیره کنید و ببندید. برای اینکه این رفتار پیش‌فرض شود، git config --global rebase.autoSquash true را اجرا کنید.

تقسیم یک کامیت با edit#

گاهی یک کامیت دو کارِ بی‌ربط انجام می‌دهد. آن را edit علامت بزنید تا گیت درست بعد از اعمالش مکث کند:

Text
edit 9a1c2d3 Add login form and fix footer links
pick 3f7e8b1 Add password validation
Bash
git reset HEAD^                 # undo the commit, keep its changes as unstaged edits
git add login.html
git commit -m "Add login form"
git add footer.html
git commit -m "Fix footer links"
git rebase --continue           # carry on with the rest of the todo list

وقتی هر دو تغییر در یک فایل هستند، git add -p اینجا به کار می‌آید: دربارهٔ هر تکه از تغییرات جداگانه از شما می‌پرسد.

وقتی تداخل پیش می‌آید#

جابه‌جا یا حذف کردن کامیت‌ها ممکن است تداخل (conflict) ایجاد کند، دقیقاً مثل merge. گیت می‌ایستد و می‌گوید کدام فایل‌ها تداخل دارند. سه راه دارید:

Bash
# fix the files, then mark them as resolved and continue
git add login.html
git rebase --continue

git rebase --skip      # drop the commit that conflicts and move on
git rebase --abort     # give up: put the branch back exactly as it was

--abort تور ایمنی شماست. تا وقتی ریبیس تمام نشده، هیچ چیزی واقعاً تغییر نکرده است.

تست‌ها را روی تک‌تکِ کامیت‌ها اجرا کنید

git rebase -i main --exec "npm test" (یا -x) بعد از هر کامیت یک خط exec اضافه می‌کند. اگر تست‌ها هر جایی شکست بخورند، ریبیس روی همان کامیتی می‌ایستد که آن‌ها را خراب کرده. این یک بررسی نهاییِ عالی است که هر کامیت به‌تنهایی کار می‌کند؛ همان چیزی که بعدها git bisect را مفید می‌کند.

ریبیس بدون جابه‌جا کردنِ برنچ#

git rebase -i main برنچ شما را روی آخرین نسخهٔ main هم می‌برد. معمولاً همین را می‌خواهید، اما اگر main خیلی جلو رفته باشد، به تداخل‌هایی می‌خورید که هیچ ربطی به مرتب‌سازیِ شما ندارند. برای مرتب کردن کامیت‌ها سرِ جایشان، با همان نقطهٔ شروع:

Bash
git rebase -i --keep-base main

نقطهٔ شروع را مستقیم هم می‌توانید بدهید: git rebase -i HEAD~3 اجازه می‌دهد سه کامیت آخر را ویرایش کنید.

push کردنِ یک برنچِ بازنویسی‌شده#

چون کامیت‌ها جدیدند، برنچی که قبل از ریبیس push کرده بودید دیگر با ریموت جور نیست و یک git push معمولی رد می‌شود. از این استفاده کنید:

Bash
git push --force-with-lease

برخلاف --force، اگر از آخرین fetch شما کس دیگری روی برنچِ ریموت push کرده باشد، --force-with-lease از بازنویسی آن خودداری می‌کند؛ پس نمی‌توانید تصادفاً کارِ هم‌تیمی‌تان را پاک کنید.

قانون طلایی ریبیس

فقط کامیت‌هایی را بازنویسی کنید که هیچ‌کس روی آن‌ها کار نساخته است. مرتب کردنِ برنچ فیچرِ خودتان قبل از pull request (یا در حین آن) مشکلی ندارد. ریبیس کردنِ main، یا برنچی که هم‌تیمی‌هایتان pull کرده‌اند، برایشان نسخه‌هایی از کامیت‌های قدیمی و یک merge دردناک باقی می‌گذارد.

برگرداندن یک ریبیس#

ریبیس را تمام کرده‌اید و پشیمانید؟ گیت جای قبلیِ برنچ شما را در ORIG_HEAD نگه می‌دارد:

Bash
git reset --hard ORIG_HEAD

این فقط درست بعد از ریبیس کار می‌کند، چون دستورهای دیگر هم ORIG_HEAD را به‌روز می‌کنند. برای عقب‌تر، reflog هر جایی را که برنچتان بوده به خاطر دارد (git reflog). گیت واقعاً چطور کار می‌کند توضیح می‌دهد چرا کامیت‌های قدیمی هنوز آنجا هستند.

جمع‌بندی#

  • git rebase -i main یک فهرست کار از کامیت‌های برنچتان باز می‌کند، با قدیمی‌ترین در ابتدا.
  • pick را به reword، edit، squash، fixup یا drop تغییر دهید و برای عوض کردن ترتیب کامیت‌ها، خط‌ها را جابه‌جا کنید.
  • git commit --fixup=<commit> به‌همراه --autosquash مرتب‌سازی را تقریباً خودکار می‌کند.
  • --abort یک ریبیسِ در جریان را متوقف می‌کند؛ ORIG_HEAD و reflog یک ریبیسِ تمام‌شده را برمی‌گردانند.
  • ریبیس کامیت‌های جدید می‌سازد، پس با --force-with-lease push کنید، و هرگز کامیت‌هایی را که دیگران روی آن‌ها کار ساخته‌اند ریبیس نکنید.