Bài trước ta thấy merge tạo một commit mới nối hai nhánh, giữ nguyên lịch sử rẽ nhánh. git rebase giải cùng bài toán — đưa công việc của nhánh này lên trên nhánh kia — nhưng theo cách hoàn toàn khác: nó viết lại lịch sử. Câu này nghe đáng sợ, và nhiều người dùng rebase mà không thật sự hiểu nó làm gì, để rồi hoảng khi thấy commit "biến mất" hay SHA đổi hết. Bài này chứng minh chính xác điều gì xảy ra, bằng SHA thật chạy trong Git 2.39.
Rebase làm gì: đổi điểm neo của một chuỗi commit
Hình dung bạn tách nhánh tinh-nang từ main tại một điểm nào đó, làm hai commit, rồi trong lúc đó main cũng tiến thêm. Giờ lịch sử rẽ đôi. Có hai cách gộp lại:
- merge: tạo một commit merge có hai parent, giữ nguyên vết rẽ nhánh (bài trước).
- rebase: lấy các commit của
tinh-nang, phát lại chúng lần lượt lên trên đỉnhmain— như thể bạn vừa tách nhánh từ chỗmainđang đứng bây giờ.
Điểm mấu chốt mà đa số hiểu sai: rebase không di chuyển các commit cũ sang chỗ mới. Một commit trong Git là bất biến — SHA của nó băm từ toàn bộ nội dung bao gồm cả con trỏ parent. Đổi parent nghĩa là đổi nội dung băm, nghĩa là ra một SHA khác, nghĩa là một commit mới. Rebase thực chất tạo một chuỗi commit mới sao chép nội dung (diff) của chuỗi cũ, rồi dời con trỏ nhánh sang chuỗi mới. Chuỗi cũ bị bỏ lại, không còn nhánh nào trỏ tới.
# Tách nhánh tinh-nang từ main, làm 2 commit
git switch -c tinh-nang
echo "dòng A" >> app.txt && git commit -am "feat 1"
echo "dòng B" >> app.txt && git commit -am "feat 2"
# Trong lúc đó main tiến thêm
git switch main
echo "hotfix" >> other.txt && git commit -am "main tien them"
# Phát lại tinh-nang lên đỉnh main
git switch tinh-nang
git rebase main

Hình 1: Tách tinh-nang làm hai commit, main tiến thêm, rồi git rebase main phát lại hai commit lên đỉnh main. Vì SHA băm cả con trỏ parent, đổi parent tạo ra commit mới chứ không dời commit cũ.
Bằng chứng: cùng nội dung, cùng message, SHA khác hẳn
Đây là phần thuyết phục nhất. Trước rebase, hai commit của tinh-nang là 8aac0ef (feat 1) và 9fc4d6c (feat 2). Sau git rebase main, chạy lại git log thấy hai commit mang đúng message "feat 1"/"feat 2", đúng nội dung — nhưng SHA đã là f45bc00 và efd241e. Chúng là commit mới toanh.

Hình 2: Trước rebase feat 1/feat 2 là 8aac0ef/9fc4d6c; sau rebase là f45bc00/efd241e — SHA đổi hoàn toàn dù nội dung và message y nguyên. Lịch sử thành một đường thẳng, và các commit cũ vẫn nằm trong reflog.
Ba điều rút ra ngay từ số liệu này:
- SHA đổi = commit mới. Không phải Git "sửa" commit cũ; nó tạo commit mới với cùng diff nhưng parent khác. Đây là lý do rebase một nhánh người khác đang dùng lại gây rối: SHA họ đang giữ bỗng không còn nhánh nào trỏ tới.
- Nội dung không mất. Diff của từng commit được phát lại y nguyên (trừ khi có xung đột phải giải). Chỉ có "danh tính" (SHA) thay đổi.
- Lịch sử thành đường thẳng. Sau rebase,
git logcho:base → main tien them → feat 1 → feat 2. Không còn điểm rẽ, không có commit merge. Đây chính là sức hút của rebase — lịch sử sạch, dễ đọc, dễgit bisect.
Commit cũ chưa mất — nó nằm trong reflog
Nghe "viết lại lịch sử" dễ tưởng commit cũ bị xoá vĩnh viễn. Không. Hai SHA cũ 8aac0ef/9fc4d6c giờ mồ côi (orphaned) — không nhánh nào trỏ tới — nhưng object vẫn còn trong kho cho tới lần thu gom rác (git gc) tiếp theo, thường là hàng tuần trở lên. Và Git ghi lại mọi bước dời HEAD trong reflog:
git reflog
# efd241e HEAD@{0}: rebase (finish): returning to refs/heads/tinh-nang
# efd241e HEAD@{1}: rebase (pick): feat 2
# f45bc00 HEAD@{2}: rebase (pick): feat 1
Reflog cho thấy từng bước pick mà rebase thực hiện. Lỡ tay rebase hỏng? git reset --hard HEAD@{n} hoặc git reset --hard 9fc4d6c đưa nhánh về đúng chuỗi commit cũ — miễn là gc chưa dọn. Đây là lưới an toàn khiến rebase ít đáng sợ hơn nhiều so với danh tiếng của nó (bài sau sẽ đào sâu reflog).
Quy tắc vàng: đừng rebase nhánh đã chia sẻ
Vì rebase đổi SHA, nó chỉ an toàn khi không ai khác đang dùng các commit đó. Quy tắc vàng của Pro Git: chỉ rebase những commit còn nằm trong nhánh riêng của bạn, chưa push chung. Nếu bạn rebase một nhánh đã push và người khác đã pull, SHA của họ và của bạn phân kỳ — lần push sau bạn phải --force, và đồng đội của bạn sẽ thấy lịch sử "nhảy" khỏi dưới chân họ, dẫn tới commit trùng lặp khi họ merge lại.
Lựa chọn thực tế thường là kết hợp: rebase nhánh tính-năng riêng của mình cho gọn trước khi mở PR (lịch sử sạch, review dễ), rồi merge --no-ff vào nhánh chung (giữ được ranh giới của tính năng). So sánh nhanh:
| rebase | merge | |
|---|---|---|
| Lịch sử | thẳng, dễ đọc | giữ vết rẽ nhánh, có commit merge |
| SHA | viết lại (commit mới) | giữ nguyên, thêm 1 commit merge |
| An toàn với nhánh chia sẻ | Không — đừng rebase commit đã push chung | Có |
| Hợp khi | dọn nhánh riêng trước khi gộp | gộp nhánh nhiều người dùng |
Ba ý mang về
- Rebase tạo commit mới, không di chuyển commit cũ. SHA băm cả con trỏ parent, nên đổi parent là đổi SHA — đo thật:
8aac0ef/9fc4d6cthànhf45bc00/efd241edù nội dung và message y nguyên. "Viết lại lịch sử" nghĩa đen là vậy. - Commit cũ vẫn cứu được qua reflog. SHA cũ mồ côi nhưng còn trong kho tới lần
gc;git reflogliệt kê từng bướcpick, vàgit reset --hard <SHA-cũ>đưa nhánh về nguyên trạng. - Đừng rebase nhánh đã push chung. Rebase cho lịch sử thẳng đẹp nhưng đổi SHA, nên chỉ dùng cho commit chưa ai khác giữ — quy tắc vàng của Pro Git. Nhánh chia sẻ thì merge; nhánh riêng thì rebase cho gọn rồi mới merge vào chung.
Nguồn
- Pro Git — Git Branching: Rebasing (kèm "The Perils of Rebasing" và quy tắc vàng): https://git-scm.com/book/en/v2/Git-Branching-Rebasing
- Git Documentation — git-rebase: https://git-scm.com/docs/git-rebase
Phần sau ta đi sâu vào chính lưới an toàn vừa nhắc: reflog ghi lại mọi lần HEAD dịch chuyển thế nào, và vì sao gần như mọi thao tác "mất commit" (reset, rebase, xoá nhánh) đều khôi phục được nhờ nó.