Hướng dẫn

git rebase: cách hoạt động, chế độ tương tác và gộp commit

Cập nhật: · Skillaria.Top

git rebase chuyển các commit của nhánh hiện tại sang một nền mới: Git tạm lấy các commit của bạn ra, đặt nhánh lên cuối nhánh đích rồi áp dụng lại từng commit thành commit mới. Nhờ đó có lịch sử tuyến tính không có commit gộp; còn git rebase -i cho phép gộp (squash), đổi tên hoặc xóa commit. Chỉ nên viết lại những commit chưa được công bố cho người khác.

Git rebase làm gì

git rebase «chuyển chỗ» nhánh của bạn sang một điểm khác trong lịch sử. Giả sử bạn tách feature từ main một tuần trước, còn main đã tiến lên từ đó. Sau rebase, nhánh của bạn trông như thể bạn vừa bắt đầu công việc — từ commit mới nhất của main.

git switch feature
git fetch origin
git rebase origin/main

Bên dưới có ba bước. Git tìm tổ tiên chung của hai nhánh, lần lượt lưu lại thay đổi của từng commit của bạn sau điểm đó, đưa nhánh lên origin/main, rồi áp dụng lại các thay đổi đã lưu, từng cái một. Kết quả là các commit mới với nội dung và thông điệp như cũ, nhưng khác hash.

Trước:   A---B---C---F---G   main
                  \
                   D---E     feature

Sau:     A---B---C---F---G   main
                          \
                           D'---E'   feature

Dấu nháy ở D' và E' không phải là trang trí. Đó là các đối tượng khác với hash khác, có cha khác. Từ đó suy ra quy tắc quan trọng nhất của rebase, sẽ nói ở phần dưới.

Quy tắc vàng: không rebase những gì đã công bố

Rebase viết lại lịch sử. Khi commit chỉ có ở bạn thì điều đó an toàn. Nhưng nếu đồng nghiệp đã tải nhánh về, sau rebase họ vẫn giữ D và E cũ, còn bạn có D' và E' mới; ở lần pull tiếp theo lịch sử sẽ bị nhân đôi, và xung đột phải giải quyết lại từ đầu.

Quy tắc thực tế: rebase áp dụng cho nhánh cục bộ và nhánh tính năng cá nhân mà chỉ một người làm việc. Các nhánh chung — main, develop, nhánh phát hành — không bao giờ được rebase.

Gửi nhánh sau rebase

Nếu nhánh đã có trên máy chủ, git push thông thường sẽ bị từ chối: lịch sử cục bộ không còn tiếp nối lịch sử từ xa. Cần force push, nhưng dùng phiên bản an toàn:

git push --force-with-lease

Khác với --force, cờ --force-with-lease sẽ từ chối ghi đè nhánh nếu trên máy chủ đã xuất hiện commit mà bạn chưa từng thấy. Nhờ vậy bạn không vô tình xóa công việc của người khác.

Xung đột khi rebase

Vì các commit được áp dụng từng cái một, xung đột có thể xuất hiện ở bất kỳ commit nào — thậm chí nhiều lần liên tiếp. Git dừng lại và cho biết commit nào không áp dụng được.

$ git rebase origin/main
Auto-merging src/api.js
CONFLICT (content): Merge conflict in src/api.js
error: could not apply 4e1f9a2... Thêm bộ nhớ đệm cho yêu cầu
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".

Các bước xử lý:

  1. Mở tệp xung đột, chọn phương án đúng và xóa các dấu <<<<<<<, =======, >>>>>>>.
  2. Đánh dấu tệp đã giải quyết: git add src/api.js. Không cần commit.
  3. Tiếp tục: git rebase --continue. Git sẽ áp dụng commit tiếp theo.

Các lệnh điều khiển khác:

LệnhTác dụng
git rebase --continueTiếp tục sau khi đã giải quyết xung đột
git rebase --skipBỏ qua hoàn toàn commit hiện tại (thay đổi của nó sẽ mất)
git rebase --abortHủy rebase và đưa nhánh về trạng thái ban đầu

Lưu ý sự nhầm lẫn giữa phía «của chúng ta» và «của họ». Trong lúc rebase, HEAD là nền mà bạn đang chuyển nhánh tới, còn thay đổi «đến» là các commit của chính bạn. Vì vậy git checkout --ours khi rebase lấy phiên bản từ main, còn --theirs là phiên bản của bạn. Khi merge thì ngược lại.

Xung đột lặp lại: rerere

Nếu bạn thường xuyên rebase một nhánh dài và phải giải cùng một xung đột nhiều lần, bật git config --global rerere.enabled true. Git sẽ ghi nhớ cách bạn đã giải quyết xung đột và tự áp dụng lần sau.

Rebase tương tác: chỉnh lịch sử trước khi công bố

Giá trị lớn nhất của rebase nằm ở chế độ tương tác. Nó cho phép dọn dẹp các commit của bạn trước khi đưa lên review: gộp «fix», «sửa chính tả», «thêm fix» thành một commit có ý nghĩa, viết lại thông điệp, đổi thứ tự hoặc bỏ những thứ không cần.

git rebase -i HEAD~4        # 4 commit cuối cùng
git rebase -i origin/main   # mọi commit của nhánh, chưa có trong main

Git mở trình soạn thảo với danh sách commit — từ cũ đến mới (khác với git log):

pick 1a2b3c4 Thêm form đăng nhập
pick 5d6e7f8 fix
pick 9a0b1c2 Kiểm tra e-mail
pick 3d4e5f6 sửa chính tả trong kiểm tra

# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash" but keep only the previous commit's log message
# d, drop = remove commit

Bạn sửa chữ ở đầu dòng, lưu và đóng trình soạn thảo — Git thực hiện kế hoạch từ trên xuống.

LệnhTác dụng
pickGiữ commit nguyên trạng
rewordGiữ thay đổi, nhưng viết lại thông điệp
editDừng ở commit để sửa nội dung (git commit --amend) hoặc tách nó thành nhiều commit
squashGộp vào commit trước, ghép thông điệp và cho phép chỉnh sửa
fixupGộp vào commit trước, bỏ thông điệp của commit này
dropXóa commit cùng các thay đổi của nó

Đổi thứ tự các dòng sẽ đổi thứ tự commit, còn xóa một dòng tương đương với drop.

Git squash: gộp commit thành một

Trong Git không có lệnh riêng git squash. Có thể «squash» commit theo ba cách, trong đó rebase tương tác là linh hoạt nhất.

Cách 1. Rebase tương tác

Với ví dụ trên, biến bốn commit thành hai commit gọn gàng:

pick 1a2b3c4 Thêm form đăng nhập
fixup 5d6e7f8 fix
pick 9a0b1c2 Kiểm tra e-mail
fixup 3d4e5f6 sửa chính tả trong kiểm tra

Commit đầu tiên của mỗi nhóm phải giữ là pick: squash và fixup gộp commit vào commit đứng trước, mà commit đầu tiên thì không có commit nào đứng trước.

Cách 2. Commit fixup và autosquash

Nếu biết trước mình đang sửa commit nào, tạo luôn commit sửa lỗi với đánh dấu:

git commit --fixup=1a2b3c4
git rebase -i --autosquash origin/main

Git tự đặt commit fixup ngay dưới commit đích và đánh dấu nó là fixup — chỉ còn việc lưu kế hoạch. Để bật hành vi này mặc định: git config --global rebase.autoSquash true.

Cách 3. Nén cả nhánh thành một commit

git reset --soft $(git merge-base HEAD origin/main)
git commit -m "Đăng nhập bằng e-mail kèm kiểm tra"

reset --soft đưa con trỏ nhánh về điểm rẽ, nhưng giữ mọi thay đổi trong index — chỉ còn tạo một commit. Ngoài ra có git merge --squash khi gộp vào nhánh chính, hoặc nút «Squash and merge» trong pull request.

Các biến thể hữu ích của rebase

git pull --rebase

Pull thông thường gộp nhánh từ xa vào nhánh của bạn và tạo commit gộp nếu bạn commit song song với đồng nghiệp. git pull --rebase thay vào đó đặt các commit cục bộ của bạn lên trên nhánh từ xa mới. Để đặt làm hành vi mặc định: git config --global pull.rebase true. Đây là vùng rebase an toàn — các commit cục bộ của bạn chưa ai nhìn thấy.

--onto: chuyển một phần nhánh

Bạn tách feature-b từ feature-a, nhưng bây giờ muốn feature-b đi thẳng từ main, không có các commit của feature-a:

git rebase --onto main feature-a feature-b

Đọc như sau: lấy các commit của feature-b không có trong feature-a và đặt chúng lên main.

--exec: kiểm tra từng commit

git rebase -i --exec "npm test" origin/main

Sau mỗi commit được áp dụng, test sẽ chạy; nếu test lỗi, rebase dừng ở commit có vấn đề. Tiện khi cần đảm bảo mọi commit trong lịch sử đều build được.

--update-refs: chuỗi nhánh phụ thuộc

Nếu bạn giữ một chuỗi nhánh phụ thuộc lẫn nhau, cờ --update-refs (Git 2.38+) khi rebase nhánh trên cùng sẽ đồng thời di chuyển các nhánh trung gian.

Cách hoàn tác rebase

Trong khi rebase vẫn đang chạy — git rebase --abort. Nếu rebase đã xong và kết quả không hợp ý, các commit cũ vẫn còn: chúng được ghi trong nhật ký tham chiếu.

$ git reflog
e5f6a7b HEAD@{0}: rebase (finish): returning to refs/heads/feature
e5f6a7b HEAD@{1}: rebase (pick): Kiểm tra e-mail
...
c3d4e5f HEAD@{5}: commit: sửa chính tả trong kiểm tra

$ git reset --hard HEAD@{5}

Tìm trong git reflog mục trước khi rebase bắt đầu và đặt nhánh về đó. Ngay sau khi rebase xong, có đường tắt: git reset --hard ORIG_HEAD. Reflog lưu cục bộ và mặc định giữ các mục khoảng 90 ngày, nên không cần hoảng sợ sau một lần rebase thất bại.

Lỗi thường gặp

  • Đã rebase nhánh chung và force push — lịch sử của đồng nghiệp bị rối. Cách sửa: thống nhất với nhau, khôi phục nhánh từ reflog của người đang giữ bản đúng, và cấm force push vào nhánh được bảo vệ trên máy chủ.
  • Dùng --force thay vì --force-with-lease và ghi đè commit mà đồng nghiệp vừa push lên cùng nhánh.
  • Squash ở dòng đầu tiên của kế hoạch tương tác — Git báo lỗi «cannot 'squash' without a previous commit».
  • Bắt đầu rebase khi thư mục làm việc chưa sạch. Git sẽ từ chối chạy. Commit các chỉnh sửa, cất vào git stash, hoặc dùng git rebase --autostash.
  • Giải quyết xung đột rồi chạy git commit thay vì git rebase --continue. Khi đó sinh ra một commit thừa; có thể gỡ nó bằng một rebase tương tác khác.

Tóm lại

git rebase chuyển các commit của bạn sang nền mới và tạo lại chúng, nên lịch sử thẳng hàng và gọn gàng. Chế độ tương tác biến các commit nháp thành lịch sử tử tế: squash và fixup gộp các chỉnh sửa, reword sửa thông điệp, drop bỏ những thứ thừa. Quy tắc quan trọng nhất là chỉ rebase những gì chưa công bố cho người khác, và sau khi rebase nhánh riêng trên máy chủ, hãy gửi bằng --force-with-lease. Rebase và làm sạch lịch sử commit được thực hành trong khóa học Git.

Câu hỏi thường gặp

Có làm thay đổi nội dung mã không khi dùng git rebase?

Nếu không có xung đột, nội dung cuối cùng của tệp giống với khi gộp, nhưng các commit được tạo lại với hash và cha mới. Nếu khi xử lý xung đột bạn chọn phương án khác, các commit trung gian sẽ khác với bản gốc.

Làm sao rebase nhánh lên commit đầu tiên của kho?

Dùng git rebase -i --root: kế hoạch sẽ bao gồm mọi commit, kể cả commit gốc. Việc này hiếm khi cần, ví dụ để sửa thông điệp của commit đầu tiên.

Rebase có giữ tác giả và ngày commit không?

Tác giả và ngày tác giả được giữ nguyên, còn người commit và ngày commit được cập nhật thành hiện tại. Cờ --committer-date-is-author-date giữ ngày commit bằng ngày tác giả.

Có thể rebase các commit gộp (merge commit) không?

Mặc định rebase làm phẳng lịch sử và loại bỏ các commit gộp. Để giữ cấu trúc gộp, dùng git rebase --rebase-merges.

Làm sao biết rebase đang diễn ra?

git status sẽ hiển thị dòng interactive rebase in progress hoặc You are currently rebasing và gợi ý các lệnh có thể dùng. Nhiều shell và IDE cũng hiển thị điều này ngay trên dòng nhắc lệnh.

Học chủ đề này qua video

Chủ đề này được giảng chi tiết kèm thực hành trong khóa học “Git с нуля до уверенного разработчика”.

Đến khóa học