jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Git nâng cao thực dụng: switch, patch, worktree, reflog và bisect

Cẩm nang Git nâng cao theo tình huống thực tế: tách commit bằng patch, chuyển thay đổi, làm nhiều branch song song, tìm commit gây lỗi và phục hồi lịch sử an toàn.

Bạn không cần thuộc hàng trăm lệnh Git để làm việc hiệu quả. Phần lớn thời gian, chỉ cần biết thêm một nhóm lệnh đúng chỗ là đủ giải quyết những tình huống vốn rất mất công:

  • một file chứa ba thay đổi nhưng bạn chỉ muốn commit một thay đổi;
  • cần chuyển bản sửa lỗi sang branch khác mà không muốn copy file thủ công;
  • đang làm feature thì phải mở hotfix ngay lập tức;
  • test bỗng hỏng nhưng không biết commit nào gây ra;
  • vừa rebase hoặc reset nhầm và tưởng đã mất commit.

Bài này không đi theo kiểu liệt kê lệnh từ A đến Z. Chúng ta sẽ bắt đầu từ vấn đề, chọn công cụ phù hợp, rồi xem Git thực sự thay đổi vùng dữ liệu nào.

Nếu chưa quen với status, diff, staging area và commit, hãy đọc Git Commands Daily Workflow trước. Bài này giả định bạn đã dùng được workflow Git hằng ngày.


Bản đồ an toàn trước khi đi sâu

Git thường làm việc với bốn vùng:

Working tree  →  Staging area  →  Local commits  →  Remote
 file đang sửa      git add          git commit       git push
MỘT THAY ĐỔI ĐI QUA GIT NHƯ THẾ NÀO? Working tree file đang sửa git diff Index nội dung sẽ commit diff --staged Local history commit trong máy git log Remote lịch sử đã chia sẻ origin/* add -p commit push restore --staged · giữ code, bỏ stage fetch · cập nhật hiểu biết về remote
Mỗi lệnh Git tác động lên một vùng cụ thể; quan sát đúng vùng giúp tránh hoàn tác quá tay.

Một lệnh đáng sợ hay không phụ thuộc vào vùng nó tác động:

LệnhTác động chínhMức cần thận trọng
git status, git diffchỉ đọcthấp
git add -pstaging areathấp
git switch, git worktreeworking tree / branchvừa
git restore -pxóa một phần thay đổi localcao
git rebaseviết lại commit localcao
git push --force-with-leasecập nhật lịch sử remoterất cao

Hai thói quen giúp giảm phần lớn tai nạn:

git status --short
git diff

Chạy chúng trước một thao tác thay đổi dữ liệu. Sau thao tác, chạy lại để xác nhận kết quả đúng như mình nghĩ.


1. git switch: đổi branch mà không nhập nhằng

Trước đây, git checkout làm quá nhiều việc: đổi branch, tạo branch, detach HEAD, thậm chí khôi phục file. git switch tách riêng nhóm thao tác liên quan đến branch nên câu lệnh dễ đọc và khó dùng nhầm hơn.

Đổi và tạo branch

git switch main
git switch -c feature/payment-retry

-c là viết tắt của --create. Nếu muốn tạo branch từ một điểm cụ thể:

git switch -c hotfix/token-expiry origin/main

Quay lại branch vừa đứng trước đó:

git switch -

Đây là lệnh nhỏ nhưng rất tiện khi phải qua lại giữa feature và branch review.

Xem một commit mà không tạo branch

git switch --detach <commit>

Chế độ detached HEAD phù hợp để build hoặc kiểm tra một phiên bản cũ. Nếu sau đó bạn sửa code và muốn giữ lại công việc, hãy tạo branch trước khi rời đi:

git switch -c experiment/from-old-commit

Bẫy cần nhớ

Git có thể mang thay đổi chưa commit sang branch mới nếu chúng không xung đột. Điều đó hợp lệ nhưng dễ khiến bạn tưởng code thuộc branch kia. Vì vậy, luôn kiểm tra trước khi switch:

git status --short

Nếu thay đổi đang dở cần giữ lâu, một WIP commit trên branch riêng thường rõ ràng hơn stash. Nếu chỉ đổi ngữ cảnh vài phút, dùng:

git stash push -u -m "WIP payment retry"

-u đưa cả file chưa được track vào stash.


2. “Git patch” thực ra là gì?

Git không có một lệnh tên là git patch. Trong thực tế, “dùng patch” có thể nói đến ba nhu cầu khác nhau:

  1. chọn từng phần thay đổi với git add -p hoặc git restore -p;
  2. chuyển nội dung diff bằng git diffgit apply;
  3. chuyển nguyên commit bằng git format-patchgit am.

Chọn sai workflow có thể làm mất metadata commit hoặc mang theo nhiều thay đổi hơn mong muốn.

Bạn muốn di chuyển gì? chọn đơn vị nhỏ nhất đúng với mục tiêu Một phần file chọn từng hunk git add -p tạo commit sạch Nội dung diff không cần metadata diff → apply working tree mới Series commit giữ author + message format-patch → am commit đầy đủ Commit đã có cùng repo / đã fetch cherry-pick -x backport nhanh diff/apply chuyển nội dung · format-patch/am chuyển lịch sử
Chọn công cụ theo đơn vị cần di chuyển: hunk, diff thô, series commit hay một commit đã tồn tại.

Bảng chọn nhanh

Bạn muốnCông cụ phù hợp
Tách một file thành nhiều commitgit add -p
Bỏ một vài hunk chưa commitgit restore -p
Gửi phần nội dung sửa, không cần commitgit diff + git apply
Gửi commit kèm author và messagegit format-patch + git am
Lấy một commit sang branch hiện tạigit cherry-pick

3. git add -p: biến một working tree lộn xộn thành commit sạch

Giả sử src/auth.ts đang chứa cả bugfix, refactor đổi tên và một dòng log debug. git add src/auth.ts sẽ đưa tất cả vào cùng một commit. Thay vào đó:

git add -p src/auth.ts

Git hiển thị từng hunk và hỏi bạn muốn làm gì:

PhímTác dụng
ystage hunk hiện tại
nkhông stage hunk này
schia hunk thành các phần nhỏ hơn
esửa patch thủ công trước khi stage
jchuyển đến hunk tiếp theo
kquay lại hunk trước
qthoát
?xem trợ giúp đầy đủ

Sau khi chọn:

git diff --staged
git commit -m "Fix expired token handling"

Phần refactor và debug log vẫn ở working tree để bạn xử lý sau.

Khi s không chia nhỏ được

Git chỉ split được khi giữa các nhóm thay đổi có đủ dòng context không đổi. Nếu hai thay đổi nằm sát nhau, thử e.

Trong chế độ edit patch:

  • xóa dòng bắt đầu bằng + nếu không muốn stage dòng được thêm;
  • đổi dòng bắt đầu bằng - thành dòng context (dấu cách) nếu không muốn stage việc xóa dòng đó;
  • không chỉnh nội dung code tùy ý như khi đang dùng editor thông thường.

Nếu patch sau khi sửa không hợp lệ, Git sẽ từ chối thay vì âm thầm stage sai. Dù vậy, hãy luôn đọc lại bằng git diff --staged.

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

Stage có chọn lọc mọi thay đổi, kể cả file mới:

git add -N path/to/new-file
git add -p

-N đánh dấu “intent to add”, nhờ đó file mới xuất hiện trong patch mode.

Bỏ stage một vài hunk nhưng giữ code:

git restore --staged -p

Bỏ một vài hunk khỏi working tree:

git restore -p

Lệnh cuối xóa thay đổi chưa commit ở những hunk bạn chọn. Hãy xem diff trước:

git diff -- path/to/file
git restore -p -- path/to/file

4. git diff + git apply: chuyển nội dung thay đổi

Workflow này phù hợp khi người nhận chỉ cần phần code thay đổi, không cần giữ author, commit message hoặc cấu trúc commit.

Tạo patch từ toàn bộ thay đổi so với HEAD, gồm cả phần đã stage và chưa stage:

git diff HEAD > changes.patch

Nếu patch có thay đổi binary:

git diff --binary HEAD > changes.patch

Ở working tree đích, kiểm tra trước khi áp dụng:

git apply --check changes.patch
git apply changes.patch

--check chỉ xác nhận patch có áp dụng được hay không, chưa sửa file. Nếu code đích đã lệch nhẹ so với nơi tạo patch, thử three-way merge:

git apply --3way changes.patch

Với git apply changes.patch, thay đổi nằm trong working tree và chưa được stage hoặc commit. Riêng --3way có thể cập nhật cả index để Git quản lý three-way merge và conflict. Luôn kiểm tra trạng thái thay vì đoán:

git status --short
git diff

Hoàn tác một patch vừa apply

Nếu working tree trước đó sạch và patch chưa bị sửa thêm:

git apply --check --reverse changes.patch
git apply --reverse changes.patch

--reverse áp dụng diff theo chiều ngược lại. Đừng dùng một cách máy móc trên working tree đang có thay đổi khác; hãy đọc git diff trước.


5. git format-patch + git am: chuyển commit có đầy đủ lịch sử

Khác với git diff, git format-patch tạo patch theo định dạng email và giữ thông tin commit như author, ngày, subject và message.

Tạo patch cho các commit trên branch hiện tại chưa có trong origin/main:

git format-patch origin/main..HEAD

Git tạo một file cho mỗi commit:

0001-Fix-token-expiry.patch
0002-Add-regression-test.patch

Ở repository hoặc branch đích:

git am --3way 0001-Fix-token-expiry.patch
git am --3way 0002-Add-regression-test.patch

Hoặc gom cả series vào một file:

git format-patch --stdout origin/main..HEAD > feature-series.patch
git am --3way feature-series.patch

Nếu gặp conflict:

git status
# sửa conflict
git add path/to/resolved-file
git am --continue

Hủy toàn bộ phiên am:

git am --abort

Quy tắc dễ nhớ:

  • diff/apply chuyển nội dung;
  • format-patch/am chuyển commit.

6. git cherry-pick: lấy commit mà không cần tạo file patch

Khi commit đã có trong cùng repository hoặc remote đã fetch, cherry-pick thường ngắn gọn hơn format-patch.

git switch release/2.4
git cherry-pick -x <commit>

-x thêm dòng tham chiếu đến commit gốc trong commit message. Dấu vết này hữu ích khi backport bugfix sang release branch.

Muốn lấy nội dung của nhiều commit nhưng tự gom thành một commit mới:

git cherry-pick --no-commit <commit-1> <commit-2>
git diff --staged
git commit

Khi conflict:

git status
# sửa conflict
git add path/to/file
git cherry-pick --continue

Hoặc quay về trạng thái trước khi bắt đầu:

git cherry-pick --abort

Không nên cherry-pick cả một branch dài rồi xem nó như chiến lược merge mặc định. Cherry-pick tạo commit mới với hash mới; nếu sau đó merge branch gốc, lịch sử sẽ khó đọc và conflict có thể lặp lại.


7. git worktree: làm nhiều branch song song, không cần stash

Tình huống: bạn đang chạy dev server trên feature branch, nhưng production cần hotfix. Switch ngay trong thư mục hiện tại sẽ ngắt context, đổi dependency hoặc làm server reload hàng loạt.

Tạo một working tree thứ hai:

git fetch origin
git worktree add -b hotfix/login ../personal-blog-hotfix origin/main

Bây giờ bạn có:

personal-blog/         → feature branch hiện tại
personal-blog-hotfix/  → hotfix/login

Hai thư mục dùng chung object database và lịch sử Git, nhưng có working tree và index riêng. Bạn có thể mở hai editor, chạy hai dev server và commit độc lập.

Các lệnh quản lý:

git worktree list
git worktree remove ../personal-blog-hotfix
git worktree prune

Chỉ remove khi thay đổi cần thiết đã được commit hoặc đẩy lên remote. Git thường từ chối xóa worktree bẩn, và đó là hàng rào an toàn đáng giữ.

Một branch không thể được checkout đồng thời ở hai worktree. Nếu Git báo branch đang được dùng, chạy git worktree list để tìm vị trí của nó.


8. git reflog: phao cứu sinh sau reset, rebase hoặc xóa branch

git log cho bạn lịch sử mà branch hiện tại còn trỏ tới. git reflog ghi lại các lần reference như HEAD hoặc branch đã di chuyển trong repository local.

Muốn hiểu sâu HEAD trỏ vào đâu, branch/ref di chuyển thế nào và cách chọn giữa reset, restore, revert, hãy đọc Hiểu sâu Git HEAD, refs và reflog.

git reflog --date=local

Ví dụ:

a81c2f1 HEAD@{0}: rebase (finish): returning to refs/heads/feature/auth
54b90de HEAD@{1}: commit: Add token refresh test
1c31a87 HEAD@{2}: switch: moving from main to feature/auth

Nếu commit 54b90de biến mất khỏi git log sau một lần rebase nhầm, đừng reset thêm. Tạo branch cứu hộ:

git switch -c rescue/token-refresh 54b90de

Kiểm tra code và lịch sử:

git log --oneline --decorate -10
git status

Sau đó bạn có thể cherry-pick commit cần thiết về branch đúng.

Giới hạn của reflog

Reflog:

  • chủ yếu là dữ liệu local, không phải backup trên remote;
  • có thời hạn lưu và có thể bị dọn bởi garbage collection;
  • không cứu được nội dung chưa từng được commit, stash hoặc đưa vào object database.

Vì vậy, khi nhận ra sai, hãy dừng thao tác viết lịch sử và tạo branch cứu hộ càng sớm càng tốt.


9. git bisect: tìm commit gây lỗi bằng tìm kiếm nhị phân

Giả sử test chạy đúng ở tag v2.0.0 nhưng hỏng ở HEAD. Giữa hai mốc có 200 commit. Kiểm tra lần lượt có thể cần 200 lần; bisect thường chỉ cần khoảng 8.

Bắt đầu:

git bisect start
git bisect bad HEAD
git bisect good v2.0.0

Git checkout một commit ở giữa. Chạy test rồi đánh dấu:

git bisect good
# hoặc
git bisect bad

Lặp lại đến khi Git chỉ ra commit đầu tiên gây lỗi. Kết thúc:

git bisect reset

Tự động hóa bisect

Nếu có một test tái hiện lỗi ổn định:

git bisect start HEAD v2.0.0
git bisect run npm test -- auth-refresh.test.ts

Script dùng cho bisect run phải trả exit code:

  • 0: commit tốt;
  • 1 đến 127, trừ 125: commit lỗi;
  • 125: bỏ qua commit vì không thể kiểm tra.

Bisect chỉ đáng tin khi test deterministic. Nếu test lúc pass lúc fail hoặc mỗi commit cần setup thủ công khác nhau, kết luận có thể sai.


10. git log -S, git log -Ggit blame -L: khảo cổ code

Khi câu hỏi là “thay đổi này xuất hiện từ khi nào?”, tìm theo diff hiệu quả hơn tìm commit message.

Tìm lúc một chuỗi được thêm hoặc xóa

git log -S "refreshToken" --oneline -- src
git log -S "refreshToken" -p -- src/auth

-S tìm commit làm thay đổi số lần xuất hiện của chuỗi.

Tìm diff khớp regular expression

git log -G "refresh(Token|Session)" -p -- src/auth

-G tìm commit có dòng được thêm hoặc xóa khớp regex. Đây là lựa chọn tốt khi tên hàm đã đổi nhưng vẫn có pattern chung.

Xem lịch sử một vùng nhỏ

git blame -L 40,80 src/auth/session.ts

blame không phải công cụ tìm người để trách. Nó là điểm bắt đầu để mở commit:

git show <commit>

Commit message, issue liên quan và các file đổi cùng lúc thường giải thích “vì sao” tốt hơn riêng dòng code hiện tại.


11. git rebase --autosquash: dọn branch trước khi review

Bạn vừa push một feature để tự kiểm tra và phát hiện typo thuộc commit cũ:

git commit --fixup <commit-can-sua>

Git tạo commit có prefix fixup!. Khi branch sẵn sàng:

git branch feature-before-rebase
git rebase -i --autosquash origin/main

Autosquash tự đặt fixup commit cạnh commit mục tiêu và đánh dấu để gộp. Sau khi review todo list, lưu lại và giải quyết conflict nếu có.

So sánh series commit trước và sau rebase:

git range-diff origin/main..feature-before-rebase origin/main..HEAD

range-diff đặc biệt hữu ích khi review phiên bản mới của một branch đã rebase: nó cho biết commit nào tương ứng, commit nào đổi nội dung và commit nào mới.

Khi nào không nên rebase

Rebase tạo commit mới với hash mới. Không rebase một branch chung khi người khác đang dựa trên lịch sử đó, trừ khi team đã thống nhất.

Nếu cần cập nhật remote branch của chính bạn sau rebase:

git push --force-with-lease

--force-with-lease kiểm tra remote có còn ở trạng thái bạn dự kiến hay không. Nó an toàn hơn --force, nhưng vẫn là thao tác viết lại lịch sử remote. Hãy đọc:

git status
git log --oneline --decorate --graph -15

trước khi push.


12. git rerere: đừng giải cùng một conflict hai lần

rerere là viết tắt của reuse recorded resolution. Khi bật, Git ghi nhớ cách bạn đã giải một conflict và có thể áp dụng lại nếu cùng conflict xuất hiện trong rebase, merge hoặc cherry-pick sau này.

Bật cho repository hiện tại:

git config rerere.enabled true

Sau đó xử lý conflict như bình thường:

git status
# sửa file
git add path/to/file
git rebase --continue

Lần sau gặp lại cùng conflict, kiểm tra phần Git đã khôi phục:

git diff
git status

Rerere tiết kiệm rất nhiều thời gian trên branch rebase thường xuyên, nhưng đừng bỏ qua review. Một resolution từng đúng có thể không còn đúng về mặt nghiệp vụ sau khi code xung quanh thay đổi.


Ba workflow hoàn chỉnh

Workflow A: một file có bugfix và refactor

git status --short
git diff -- src/auth.ts
git add -p src/auth.ts
git diff --staged
git commit -m "Fix expired token handling"

git add -p src/auth.ts
git diff --staged
git commit -m "Rename token parsing helpers"

Mỗi commit có một mục đích và có thể review hoặc revert độc lập.

Workflow B: backport hotfix sang release branch

git fetch origin
git worktree add -b hotfix/token ../app-release origin/release/2.4
cd ../app-release
git cherry-pick -x <fix-commit>
npm test
git push -u origin hotfix/token

Feature branch và môi trường hiện tại không bị xáo trộn.

Workflow C: tìm và cứu commit sau rebase nhầm

git reflog --date=local
git switch -c rescue/lost-work <commit-tu-reflog>
git log --oneline --decorate -10

git switch feature/auth
git cherry-pick <commit-can-cuu>

Tạo nhánh cứu hộ trước, chỉnh lịch sử sau. Đó là thứ tự an toàn.


Cheat sheet

# Branch
git switch -
git switch -c <branch> <start-point>
git switch --detach <commit>

# Chọn hunk
git add -p
git restore --staged -p
git restore -p

# Patch nội dung
git diff HEAD > changes.patch
git apply --check changes.patch
git apply --3way changes.patch

# Patch commit
git format-patch origin/main..HEAD
git am --3way <patch-file>

# Chuyển commit
git cherry-pick -x <commit>
git cherry-pick --abort

# Làm song song
git worktree add -b <branch> <path> <start-point>
git worktree list

# Cứu hộ và debug
git reflog --date=local
git bisect start HEAD <good-commit>
git bisect run <test-command>
git bisect reset

# Tìm lịch sử thay đổi
git log -S "<text>" -p -- <path>
git log -G "<regex>" -p -- <path>
git blame -L <start>,<end> <file>

# Dọn branch
git commit --fixup <commit>
git rebase -i --autosquash origin/main
git range-diff <old-range> <new-range>

Điều cốt lõi

Git nâng cao không có nghĩa là dùng lệnh phức tạp mọi lúc. Nó có nghĩa là biết thay đổi đang nằm ở đâu, mình muốn di chuyển đơn vị nàođường lui là gì:

  • cần tách nội dung: dùng patch mode;
  • cần chuyển diff: dùng apply;
  • cần giữ commit: dùng am hoặc cherry-pick;
  • cần làm song song: dùng worktree;
  • cần tìm lỗi: dùng bisect;
  • cần cứu lịch sử local: bắt đầu bằng reflog.

Trước thao tác, đọc statusdiff. Sau thao tác, đọc lại. Chính vòng lặp nhỏ đó — không phải khả năng thuộc lệnh — mới là kỹ năng Git đáng giá nhất.