Git: Phân biệt ^ và ~ để không bao giờ Reset nhầm nhánh

Git tutorial - IT technology blog
Git tutorial - IT technology blog

Chuyện gì xảy ra khi bạn gõ nhầm HEAD~1 thành HEAD^1?

Hồi mới vào nghề, mình từng “toát mồ hôi hột” khi dùng git reset để gỡ một bản merge lỗi. Lúc đó mình cứ đinh ninh ~ (Tilde) và ^ (Caret) là một, thích dùng cái nào cũng được. Kết quả? Mình nhảy nhầm vào một nhánh feature cũ rích thay vì quay lại commit chính trên master. May mà có git reflog cứu cánh, nếu không công sức cả buổi sáng coi như bỏ đi.

Trong team 8 người của mình, ngay cả các bạn senior đôi khi vẫn khựng lại vài giây khi gặp các merge commit phức tạp. Hiểu rõ hai ký hiệu này giúp bạn làm chủ git rebase -i. Bạn sẽ tự tin hơn khi cần lục lại đống code cũ mà không sợ làm loạn lịch sử commit.

Mẹo nhớ nhanh trong 30 giây

Nếu bạn đang vội, hãy nhớ quy tắc đơn giản này:

  • Dấu ~ (Tilde): Đi ngược về quá khứ theo đường thẳng. HEAD~3 là lùi đúng 3 bước về phía trước.
  • Dấu ^ (Caret): Chọn cha nào trong một vụ merge. Ký hiệu này chỉ thực sự quan trọng khi một commit có từ 2 cha trở lên.
# Lùi lại 1 commit (cả hai đều cho kết quả như nhau)
git reset --hard HEAD~1
git reset --hard HEAD^

# Lùi đúng 3 commit theo trục dọc
git reset --hard HEAD~3

# Chọn commit cha thứ 2 của một merge commit
git show HEAD^2

Bây giờ, hãy cùng mình soi kỹ bản chất để bạn không bao giờ bị Git “lừa” nữa.

1. Tilde (~): Nhảy ngược theo trục dọc

Hãy coi lịch sử Git là một sợi dây thừng. Dấu ~ hoạt động như số bước chân bạn lùi lại trên sợi dây đó.

Khi viết HEAD~n, Git hiểu là: “Từ đây, hãy lùi lại n bước. Ở mỗi bước, nếu gặp ngã rẽ, hãy luôn chọn commit cha đầu tiên (first parent)”.

  • HEAD~1: Cha trực tiếp.
  • HEAD~2: Ông nội.
  • HEAD~3: Cụ.

Viết HEAD~~~ cũng ra kết quả tương tự HEAD~3. Nhưng mình khuyên bạn nên dùng số cho gọn, vừa đỡ mỏi tay vừa tránh đếm nhầm dấu.

2. Caret (^): Chọn hướng tại ngã rẽ

Sự khác biệt thực sự xuất hiện tại các Merge Commit. Một commit bình thường chỉ có 1 cha, nhưng khi bạn gộp nhánh Feature vào Main, commit đó sẽ có 2 cha.

  • Cha thứ nhất (Parent 1): Là commit thuộc về nhánh bạn đang đứng (ví dụ main) trước khi merge.
  • Cha thứ hai (Parent 2): Là commit cuối cùng của nhánh bạn vừa kéo vào.

Lúc này, HEAD^1 đưa bạn về lại mạch chính. Trong khi đó, HEAD^2 sẽ dẫn bạn sang nhánh phụ đã merge. Nếu dùng HEAD^3, Git sẽ tìm cha thứ 3. Trường hợp này cực hiếm, thường chỉ thấy khi dùng Octopus Merge để gộp 4-5 nhánh cùng lúc.

Điểm cần lưu ý: Với commit đơn (không phải merge), HEAD^1HEAD~1 trỏ về cùng một chỗ. Đây chính là lý do nhiều người lầm tưởng chúng là anh em sinh đôi.

3. Hình ảnh hóa sơ đồ commit

Để dễ hình dung, hãy nhìn vào sơ đồ dưới đây:

G (HEAD)
| 
F (Merge commit: gộp E vào D)
/ \
D   E
|   |
C   |
|  /
B /
|
A

Tại vị trí G (HEAD), kết quả sẽ là:

  • HEAD~1 trỏ về F.
  • HEAD~2 trỏ về D (Git đi G -> F -> D theo nhánh chính).
  • HEAD^1 trỏ về F.
  • F^1 trỏ về D (Nhánh chính trước merge).
  • F^2 trỏ về E (Nhánh feature vừa gộp vào).

Vậy nếu muốn đi từ G đến E? Bạn dùng: HEAD~1^2. Nghĩa là: lùi 1 bước đến F, sau đó rẽ sang cha thứ 2.

4. Kỹ thuật kết hợp khi Rebase

Khi dọn dẹp commit bằng git rebase -i, việc kết hợp ~^ rất lợi hại. Bạn không cần mất công copy mã SHA-1 dài dằng dặc nữa. Chỉ cần chỉ định đúng tọa độ là xong.

Ví dụ: HEAD~3^2

  1. HEAD~3: Lùi 3 bước theo đường thẳng.
  2. ^2: Tại điểm đó, rẽ sang nhánh phụ.

Kinh nghiệm của mình: Trước khi chạy lệnh reset --hard, hãy kiểm tra đích đến bằng show:

git show HEAD~2^1 --oneline -s

Nếu tên commit hiện ra đúng ý bạn, lúc đó mới bấm Enter thực hiện lệnh chính.

Lời khuyên thực tế: Hãy tin vào đồ họa

Lý thuyết là vậy, nhưng khi đối mặt với dự án có 10.000 commit, việc đếm ~ bằng mắt là tự sát. Trong team mình, mình luôn yêu cầu mọi người bật sơ đồ trước khi gõ lệnh:

git log --oneline --graph --all

Nhìn vào biểu đồ, bạn sẽ thấy rõ các đường nối. Việc xác định HEAD^2 hay HEAD~5 lúc này trực quan hơn nhiều.

Một tình huống hay gặp khác là git cherry-pick. Khi muốn lấy code từ một merge commit, Git sẽ đứng hình vì không biết chọn bên nào. Bạn bắt buộc phải dùng flag -m kèm số thứ tự cha, ví dụ: git cherry-pick -m 1 <commit_hash>. Đây chính là lúc kiến thức về Parent phát huy tác dụng.

Tổng kết

Để dùng Git như dân chuyên, hãy nhớ:

  • Dùng Tilde (~) để đi lùi theo thời gian trên cùng một nhánh.
  • Dùng Caret (^) để rẽ sang nhánh khác tại các điểm Merge.
  • Luôn dùng git log --graph để soi đường trước khi nhảy.

Làm chủ được hai ký hiệu này, bạn sẽ thấy lịch sử code không còn là mê cung nữa. Chúc các bạn quản lý commit thật mượt mà!

Share: