git rebase

Xem thử

Vấn đề cần giải quyết

Nhánh feature của bạn tách từ main cách đây một tuần. Từ đó main đã có thêm commit mới. Nếu merge, lịch sử có thêm một commit hợp nhất và các đường rẽ nhánh. Nhiều đội muốn một lịch sử thẳng, như thể bạn bắt đầu làm việc từ commit mới nhất của main. git rebase làm được điều đó, đổi lại bạn phải hiểu nó tạo commit mới.

Sau bài này, bạn có thể

  • Giải thích rebase tạo bản sao mới của commit trên nền mới và vì sao hash đổi.
  • So sánh kết quả của merge và rebase.
  • Dùng git rebase -i để gộp, đổi tên và bỏ commit, và xử lý xung đột khi rebase.

Tiên quyết: bài Tổng quan viết lại lịch sử và khái niệm nhánh, merge.

Merge và rebase cho hai hình dạng khác nhau

Cách đọc hình. Cả hai panel xuất phát từ cùng một tình huống: feature tách từ B, còn main đã đi tiếp tới C. Panel trên là merge: Git thêm commit hợp nhất M có hai cha là C và E, lịch sử giữ hình dạng rẽ nhánh và D, E giữ nguyên hash. Panel dưới là rebase: Git tạo D' và E' là bản sao mới của D và E nằm sau C, nên lịch sử là một đường thẳng, nhưng D' và E' có hash khác D và E.

Thử và quan sát: rebase từng bước

Hãy dự đoán: sau git rebase main trên feature, commit D và E có giữ nguyên hash không? Chạy mô phỏng và quan sát từng commit được áp lại, rồi thử biến thể gặp xung đột.

Đang tải Animation…

Bảng bước (bản đọc tĩnh của mô phỏng)

BướcLệnhĐiều xảy ra
1git rebase mainGit áp lại D lên đỉnh main thành D' với hash mới. HEAD tạm tách ra trong lúc rebase.
2(tiếp tục áp lại)Tiếp theo E được áp lại thành E' trên D'.
3(hoàn tất)Nhánh feature chuyển sang E'. D và E cũ không còn nhánh nào trỏ tới (nét đứt). main không đổi.
4git switch main && git merge featureVì lịch sử đã thẳng, merge chỉ là fast-forward: main trượt tới E, không có commit hợp nhất.

Biến thể: Rebase gặp xung đột rồi hủy

BướcLệnhĐiều xảy ra
1git rebase mainD sửa cùng vùng với C trên main, nên Git không áp tự động được và dừng lại giữa chừng.
2git statusGit cho biết đang ở giữa rebase và gợi ý ba lối đi: sửa rồi tiếp tục, bỏ commit này, hoặc hủy cả rebase.
3git rebase --abortHủy toàn bộ: feature trở về đúng trạng thái trước khi rebase, không có gì bị mất.

Giải đáp. Không. Rebase không di chuyển commit: nó áp lại thay đổi của D và E lên đỉnh main để tạo D' và E' với hash mới, rồi cho feature trỏ tới E'. D và E cũ không còn nhánh nào trỏ tới. Vì lịch sử đã thẳng, merge feature vào main chỉ là fast-forward.

bash
git switch feature
git rebase main

Khi gặp xung đột

Nếu một commit không áp sạch, rebase dừng giữa chừng. Bạn có ba lối đi.

Đang tải sơ đồ…

Cách đọc hình. Rebase bắt đầu ở trạng thái Rebasing (lệnh git rebase main) và áp từng commit. Nếu một commit không áp sạch, Git dừng ở Conflict. Từ đó bạn sửa tệp, git add rồi git rebase --continue để quay lại Rebasing, hoặc git rebase --abort để trở về trạng thái ban đầu (Original). Khi mọi commit áp xong, rebase kết thúc ở Done.

bash
git add <tệp>
git rebase --continue
git rebase --abort

--abort luôn là lối thoát an toàn khi bạn bối rối.

Rebase tương tác

bash
git rebase -i HEAD~4

Trình soạn thảo mở ra bốn commit gần nhất, mỗi dòng một lệnh:

LệnhTác dụng
pickGiữ nguyên commit
rewordGiữ nội dung, đổi thông điệp
squashGộp vào commit đứng trước, hợp nhất thông điệp
fixupGộp vào commit đứng trước, bỏ thông điệp
editDừng lại để bạn sửa commit
dropBỏ hẳn commit

Hãy dự đoán: bạn có bốn commit, trong đó một commit sửa chính tả và một commit thử nghiệm. Làm sao gộp, đổi thông điệp và bỏ chúng bằng một lần rebase?

Đang tải Animation…

Bảng bước (bản đọc tĩnh của mô phỏng)

BướcLệnhĐiều xảy ra
1git rebase -i HEAD~4Git mở trình soạn thảo với bốn commit gần nhất, mỗi dòng mặc định là pick (giữ nguyên).
2(sửa dòng 2: squash)squash gộp commit này vào commit đứng trước nó, nên b4c5 sẽ nhập vào a1b2.
3(sửa dòng 3: reword)reword giữ nguyên nội dung nhưng cho bạn đổi thông điệp.
4(sửa dòng 4: drop)drop bỏ hẳn commit thử nghiệm.
5(lưu và đóng trình soạn thảo)Git áp lại theo danh sách: a1b2 và b4c5 thành A', c7d8 thành C' với thông điệp mới, d0e1 bị bỏ. Bốn commit thành hai, tất cả có hash mới.

Biến thể: Dùng fixup tự động

BướcLệnhĐiều xảy ra
1git commit --fixup=a1b2Bạn nhận ra lỗi nhỏ thuộc về commit a1b2 và tạo một commit fixup trỏ tới nó.
2git rebase -i --autosquash HEAD~5Cờ --autosquash tự sắp xếp commit fixup đứng ngay sau commit mục tiêu và đặt sẵn lệnh fixup.
3(lưu và đóng trình soạn thảo)fixup gộp sửa lỗi vào a1b2 mà không giữ thông điệp riêng. Kết quả sạch hơn squash vì không phải viết lại thông điệp.

Giải đáp. Đổi dòng thứ hai thành squash, dòng thứ ba thành reword, dòng thứ tư thành drop, rồi lưu. Bốn commit thành hai, và cả hai đều là commit mới với hash mới. Biến thể cho thấy --fixup cùng --autosquash để sửa lỗi thuộc về một commit cũ mà không phải viết lại thông điệp.

Quy tắc vàng của rebase

Đừng bao giờ rebase một nhánh đã được chia sẻ công khai. Rebase viết lại hash và làm lệch bản sao của người khác. Chỉ rebase các commit chưa rời khỏi máy bạn.

Hiểu lầm thường gặp

  • "Rebase di chuyển commit." Rebase tạo commit mới; commit cũ chỉ còn trong reflog.
  • "Rebase luôn tốt hơn merge." Rebase cho lịch sử sạch nhưng viết lại hash; merge giữ nguyên lịch sử thật. Mỗi đội có quy ước riêng.
  • "Gặp xung đột khi rebase là hỏng rồi." Bạn sửa xung đột rồi --continue, hoặc --abort để quay lại, không mất gì.

Tự kiểm tra

  1. Vì sao hash của D và D' khác nhau dù nội dung thay đổi giống nhau?
  2. Lệnh nào hủy một rebase đang dở?
  3. Đồng đội đã lấy nhánh feature của bạn. Có nên rebase nó không, vì sao?

Đáp án gợi ý. (1) D' có cha khác (đỉnh main mới), và hash tính từ cả cha nên khác. (2) git rebase --abort. (3) Không, vì rebase đổi hash và bản của đồng đội sẽ lệch với bản của bạn.

Điều cần nhớ

  • Rebase tạo bản sao mới trên nền mới, cho lịch sử thẳng.
  • rebase -i dùng pick, reword, squash, fixup, edit, drop.
  • Xung đột: sửa, add, --continue; hoặc --abort.
  • Không rebase nhánh đã chia sẻ.
  • Bài sau: git reflog, lưới an toàn khi viết lại lịch sử.

Nguồn tham khảo

Pro Git, chương 3.6 "Rebasing" và 7.6 "Rewriting History" (git-scm.com/book); tài liệu git-rebase (git-scm.com/docs/git-rebase).