Git hằng ngày: lệnh và config giúp mình tiết kiệm hàng giờ mỗi tuần
Một file ~/.gitconfig giải thích từng dòng, cách giữ history sạch bằng add -p và fixup, cứu commit bằng reflog, để bisect run tự tìm commit gây bug và làm hotfix bằng worktree mà không cần stash.
Một buổi làm việc rất bình thường có thể mất cả tiếng chỉ vì Git: rebase nhánh feature lên main thì gặp lại đúng conflict đã giải hôm qua, lỡ git stash mà quên file mới tạo, rồi giữa chừng có hotfix gấp nên phải stash tiếp, checkout qua lại, cài lại node_modules. Không có bước nào khó, nhưng cộng lại thì rất tốn thời gian.
Phần lớn thời gian đó tiết kiệm được bằng vài dòng config và một nhóm lệnh ít người dùng. Bài này gom lại những thứ mình thực sự dùng hằng ngày: file ~/.gitconfig có giải thích từng dòng, cách giữ history sạch trước khi mở PR, cách cứu commit tưởng đã mất, để git bisect run tự tìm commit gây bug, và git worktree để làm hotfix song song với nhánh đang dở.
Mọi lệnh trong bài chạy được với Git 2.40 trở lên. Kiểm tra bằng git --version. Trên macOS, bản Git đi kèm Xcode thường cũ hơn bản Homebrew, nên nếu một option báo lỗi thì đó là lý do đầu tiên cần kiểm tra.
TL;DR
- Bật
pull.rebase,rebase.autoStash,rerere.enabled,merge.conflictStyle zdiff3: pull ít conflict hơn và conflict dễ đọc hơn. - Dùng
git switch/git restorethay chogit checkoutđa năng, ít gõ nhầm làm mất file hơn. git add -p+git commit --fixup+rebase --autosquashcho ra history sạch mà không phải sửa tay.git reflogcứu được gần như mọi thứ đã từng commit; force push thì luôn dùng--force-with-lease --force-if-includes.git bisect run+ một script test giúp tìm commit gây bug trong vài phút, kể cả khi có hàng trăm commit.git worktreecho bạn một thư mục làm việc thứ hai cho hotfix, không cần stash.
Một file ~/.gitconfig đáng copy (và lý do của từng dòng)
Đây là phần config mình mang theo qua mọi máy. Copy vào ~/.gitconfig (hoặc chạy từng dòng git config --global <key> <value>), nhưng hãy đọc phần giải thích trước, vì vài option thay đổi hành vi mặc định.
# ~/.gitconfig
[init]
defaultBranch = main
[pull]
rebase = true # pull = fetch + rebase, không tạo merge commit "Merge branch 'main' of ..."
[rebase]
autoStash = true # tự stash thay đổi chưa commit trước khi rebase, xong thì apply lại
autoSquash = true # rebase -i tự xếp các commit fixup!/squash! vào đúng chỗ
updateRefs = true # rebase nhánh trên cùng thì cập nhật luôn các nhánh xếp chồng bên dưới
[push]
autoSetupRemote = true # lần push đầu không cần --set-upstream origin <branch>
useForceIfIncludes = true # đi kèm --force-with-lease, an toàn hơn khi force push
[fetch]
prune = true # xóa remote-tracking branch đã bị xóa trên server
[rerere]
enabled = true # nhớ cách bạn giải conflict, gặp lại thì tự áp dụng
autoUpdate = true # file được rerere giải xong sẽ tự được stage
[diff]
algorithm = histogram # diff dễ đọc hơn khi có nhiều dòng giống nhau ({ } , return)
colorMoved = default # đoạn code bị di chuyển được tô màu khác với code thêm/xóa
[merge]
conflictStyle = zdiff3 # conflict hiển thị cả phiên bản gốc (base) ở giữa
[branch]
sort = -committerdate # git branch liệt kê nhánh mới làm gần đây lên đầu
[tag]
sort = version:refname # v1.10.0 đứng sau v1.9.0 thay vì sau v1.1.0
[column]
ui = auto # git branch / git tag hiển thị nhiều cột khi danh sách dài
[help]
autocorrect = prompt # gõ "git stauts" thì hỏi có phải "status" không
[commit]
verbose = true # hiện diff ngay trong editor khi viết commit message
[alias]
st = status -sb
lg = log --oneline --graph --decorate -20
last = log -1 --stat
amend = commit --amend --no-edit
undo = reset --soft HEAD~1
pushf = push --force-with-lease --force-if-includes
recent = for-each-ref --sort=-committerdate --count=15 --format='%(committerdate:relative)%09%(refname:short)' refs/heads/
fixup = "!f() { git commit --fixup=\"$1\" && GIT_SEQUENCE_EDITOR=: git rebase -i --autosquash \"$1~1\"; }; f"
gone = "!git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads | awk '$2 == \"[gone]\" { print $1 }'"
Vài dòng cần giải thích kỹ hơn:
pull.rebase = true: commit local của bạn được đặt lại lên trên commit mới từ remote, history thẳng hàng. Cái giá là nếu nhánh của bạn có merge commit có chủ đích, rebase sẽ làm phẳng chúng. Khi đó dùng pull.rebase = merges để giữ cấu trúc merge.
rerere.enabled (reuse recorded resolution): mỗi lần bạn giải một conflict, Git lưu cặp "conflict → kết quả" trong .git/rr-cache. Khi rebase lại nhánh dài nhiều lần, conflict giống hệt sẽ được giải tự động. Edge case: nếu lần đầu bạn giải sai, rerere sẽ lặp lại cái sai đó. Gỡ bằng git rerere forget path/to/file khi đang ở trạng thái conflict.
merge.conflictStyle = zdiff3 (từ Git 2.35): thay vì chỉ thấy "của mình" và "của họ", bạn thấy thêm đoạn code gốc trước khi cả hai bên sửa. Đây là khác biệt giữa đoán và biết:
<<<<<<< HEAD
export const VAT_RATE = 0.08
||||||| 3f2a1bc
export const VAT_RATE = 0.1
=======
export const VAT_RATE = 0.1
export const VAT_RATE_REDUCED = 0.05
>>>>>>> main
Nhìn phần giữa (|||||||, phiên bản gốc) là hiểu ngay: nhánh của mình đổi 0.1 thành 0.08, còn main không đổi dòng đó mà chỉ thêm VAT_RATE_REDUCED. Kết quả đúng là giữ cả hai thay đổi. Không có phần base, bạn rất dễ chọn nhầm một bên và làm mất thay đổi của người kia.
push.autoSetupRemote (từ Git 2.37): hết cảnh git push báo "The current branch has no upstream branch". Chỉ có tác dụng khi push.default là simple (mặc định), upstream hoặc current.
Alias fixup: git fixup a1b2c3d tạo commit fixup! ... từ những gì đang stage, rồi rebase non-interactive để gộp nó vào a1b2c3d. GIT_SEQUENCE_EDITOR=: bỏ qua bước mở editor vì danh sách đã được autosquash sắp xếp sẵn. Không dùng được nếu a1b2c3d là root commit (không có ~1).
git switch và git restore thay cho checkout
git checkout làm quá nhiều việc: đổi nhánh, tạo nhánh, khôi phục file, detach HEAD. Vấn đề là git checkout orders sẽ đổi sang nhánh orders nếu có nhánh tên đó, còn nếu không có nhánh mà có thư mục orders/ thì nó... ghi đè thay đổi chưa commit trong thư mục đó. Từ Git 2.23 có hai lệnh tách vai trò rõ ràng:
# Đổi nhánh / tạo nhánh: git switch
git switch feature/invoice-export # đổi nhánh
git switch -c fix/order-total origin/main # tạo nhánh mới từ origin/main
git switch - # quay về nhánh trước đó (giống cd -)
git switch --detach v2.4.0 # xem code tại một tag, không tạo nhánh
# Khôi phục file: git restore
git restore src/orders/total.ts # bỏ thay đổi chưa stage của file
git restore --staged src/orders/total.ts # unstage, giữ nguyên thay đổi trong working tree
git restore -p src/orders/total.ts # chọn từng hunk để bỏ
git restore --source=main -- src/config.ts # lấy phiên bản của file từ nhánh main
Lợi ích lớn nhất là git switch không bao giờ đụng vào nội dung file của bạn theo kiểu bất ngờ, còn git restore luôn rõ là đang thao tác trên file. git switch cũng từ chối đổi nhánh nếu thay đổi chưa commit sẽ bị ghi đè, thay vì âm thầm làm hỏng.
Lưu ý: git restore không có thùng rác. Thay đổi chưa từng được stage hay commit mà bị restore thì mất hẳn, reflog cũng không cứu được. Trước khi restore một file lớn, chạy git diff path để chắc chắn.
Giữ history sạch: add -p, commit --fixup và autosquash
Reviewer đọc PR theo từng commit sẽ rất khổ nếu history trông như "wip", "fix", "fix again", "address review". Mình làm theo ba bước.
Bước 1: stage theo hunk. git add -p cho bạn duyệt từng đoạn thay đổi và chọn y (stage), n (bỏ qua), s (tách hunk nhỏ hơn), e (sửa tay). Đây cũng là bước self-review miễn phí: bạn sẽ thấy ngay console.log quên xóa hay file .env.local lỡ sửa.
Bước 2: sửa commit cũ bằng fixup. Giả sử nhánh có 3 commit và reviewer chỉ ra bug trong commit đầu tiên:
git lg
# * 9c41e2a (HEAD -> feature/invoice-export) Add CSV download button
# * 5d7f310 Add invoice export API route
# * a1b2c3d Add invoice export query
# * 77e0b91 (origin/main) Release 2.4.0
# Sửa code, stage đúng phần liên quan
git add -p src/invoices/export-query.ts
git commit --fixup=a1b2c3d
# [feature/invoice-export 0e8d4f2] fixup! Add invoice export query
# Gộp các fixup! vào đúng commit gốc
git rebase -i --autosquash origin/main
Với rebase.autoSquash = true trong config, git rebase -i origin/main sẽ tự xếp dòng fixup 0e8d4f2 ngay dưới pick a1b2c3d. Bạn chỉ cần lưu và thoát editor. Kết quả vẫn là 3 commit, mỗi commit đúng và tự đứng được.
Ngoài --fixup=<sha> còn có --fixup=amend:<sha> (sửa cả nội dung lẫn message) và --fixup=reword:<sha> (chỉ sửa message).
Bước 3: push lại an toàn. Rebase thay đổi hash, nên push cần force. Phần sau giải thích vì sao luôn dùng git pushf (alias ở trên) chứ không phải --force.
Khi nào không nên làm vậy: khi PR đã có người review dở và team bạn review theo "changes since last review". Rebase làm GitHub khó hiển thị chính xác phần thay đổi mới. Khi đó cứ đẩy commit fixup lên như bình thường, đợi approve rồi mới autosquash trước khi merge (hoặc để squash merge làm việc đó).
Stash đúng cách và những cái bẫy
git stash không có tham số là nguồn mất code phổ biến nhất mình từng thấy. Dùng dạng đầy đủ:
git stash push -m "invoice-export: WIP filter theo tháng" # có tên, sau này biết là gì
git stash push -u -m "kèm file mới" # -u: gồm cả file untracked
git stash push -m "chỉ file config" -- next.config.ts # chỉ stash một số path
git stash push --staged -m "chỉ phần đã stage" # Git 2.35+
git stash list
# stash@{0}: On feature/invoice-export: kèm file mới
# stash@{1}: On feature/invoice-export: invoice-export: WIP filter theo tháng
git stash show -p stash@{1} # xem nội dung trước khi apply
git stash apply stash@{1} # apply nhưng giữ lại trong list
git stash drop stash@{1} # xóa khi chắc chắn đã ổn
git stash branch feature/month-filter stash@{1} # tạo nhánh từ commit gốc lúc stash rồi apply
Những cái bẫy cần nhớ:
- Mặc định không gồm file untracked. Bạn tạo
src/invoices/csv.tsmới,git stash, đổi nhánh, và file mới vẫn nằm đó, rồi lẫn vào nhánh khác. Dùng-u. Muốn gồm cả file bị.gitignorethì-a, nhưng cẩn thận vì nó sẽ stash luônnode_moduleshay.next. popgặp conflict thì không drop. Stash vẫn nằm trong list, nhưng nhiều người tưởng đã mất và stash lại lần nữa, rồi apply chồng lên nhau. Sau khi giải conflict, tựgit stash drop.- Số thứ tự thay đổi.
stash@{0}luôn là cái mới nhất, mỗi lần push thì các stash cũ bị đẩy xuống. Đừng nhớ theo số, hãy đặt tên bằng-m. - Stash không gắn với nhánh. Apply stash của nhánh A lên nhánh B là hoàn toàn hợp lệ với Git, và thường tạo conflict khó hiểu.
git stash branchan toàn hơn vì nó dựng lại đúng commit gốc. - Lỡ drop thì sao? Stash thực chất là commit. Commit bị drop thành "dangling" và vẫn tìm lại được cho tới khi
git gcdọn:
git fsck --no-reflog | awk '/dangling commit/ { print $3 }' \
| xargs git log --no-walk --oneline --grep='WIP'
# 4be19a7 On feature/invoice-export: invoice-export: WIP filter theo tháng
git stash apply 4be19a7
Quy tắc của mình: stash chỉ dùng cho việc vài phút. Việc lâu hơn thì commit WIP lên nhánh (git commit -m "wip"), sau đó git undo (alias reset --soft HEAD~1) khi quay lại. Hoặc tốt hơn, dùng worktree ở phần dưới.
Cứu commit "mất" bằng reflog và force push an toàn
Reflog là nhật ký mọi lần HEAD di chuyển trên máy bạn: commit, rebase, reset, switch. Rebase hỏng, reset --hard nhầm, xóa nhánh chưa merge: chỉ cần commit từng tồn tại là còn trong reflog.
git reflog -15 # output minh họa
# 9c41e2a HEAD@{0}: rebase (finish): returning to refs/heads/feature/invoice-export
# 3e5a7c0 HEAD@{1}: rebase (pick): Add CSV download button
# ...
# f02d9b4 HEAD@{6}: commit: Add CSV download button <- trạng thái trước khi rebase
# 5d7f310 HEAD@{7}: commit: Add invoice export API route
# Quay nhánh về đúng trạng thái trước rebase
git reset --hard f02d9b4
# Hoặc an toàn hơn: tạo nhánh mới trỏ vào đó rồi so sánh
git branch rescue/before-rebase f02d9b4
git diff rescue/before-rebase feature/invoice-export
# Xóa nhầm nhánh chưa merge: Git in ra hash ngay khi xóa
# "Deleted branch feature/old-report (was 8a1c2e3)."
git branch feature/old-report 8a1c2e3
Giới hạn cần biết: reflog chỉ có ở local, không push lên server, và mặc định entry hết hạn sau 90 ngày (30 ngày với commit không còn reachable từ nhánh nào). Thay đổi chưa từng commit thì reflog không giúp được.
Force push: vì sao --force-with-lease --force-if-includes. git push --force ghi đè remote bằng bản local, bất kể đồng nghiệp vừa push gì lên nhánh đó. --force-with-lease chỉ ghi đè nếu remote vẫn đang ở đúng commit mà bạn biết lần cuối (theo remote-tracking branch origin/<branch>).
Nhưng lease có lỗ hổng: nhiều IDE tự chạy git fetch ngầm, làm origin/<branch> được cập nhật mà bạn không hề nhìn thấy commit mới. Lúc đó lease vẫn "khớp" và commit của đồng nghiệp vẫn bị ghi đè. --force-if-includes (Git 2.30+) bổ sung điều kiện: commit trên remote phải đã từng nằm trong lịch sử local của bạn (kiểm tra qua reflog). Alias pushf gộp cả hai, còn push.useForceIfIncludes = true bật nó mặc định cho mọi --force-with-lease.
Nếu push bị từ chối với "stale info", đừng thêm --force cho xong. Hãy git fetch, xem git log HEAD..origin/feature/invoice-export có gì, rebase lên rồi mới push lại.
git bisect run: để máy tìm commit gây bug
Tình huống: bản v2.4.0 tính tổng đơn hàng đúng, main hiện tại tính sai, và ở giữa có 180 commit. Bisect tìm kiếm nhị phân, nên chỉ cần khoảng 8 lần kiểm tra (log2 của 180). Nếu viết được một lệnh trả về exit code đúng/sai, bạn có thể để Git tự chạy hết.
Đầu tiên viết test tái hiện bug và lưu nó ngoài repo, ví dụ /tmp/bisect/. Lý do: nếu test được commit, nó sẽ biến mất khi bisect checkout các commit cũ. Script sẽ copy test vào repo ngay trước khi chạy (vì Vitest chỉ chạy file nằm trong project) và xóa đi sau đó, để working tree luôn sạch giữa các bước.
// /tmp/bisect/order-total.test.ts
import { describe, expect, it } from 'vitest'
import { calculateOrderTotal } from './total' // sẽ được copy vào src/orders/ lúc chạy
describe('calculateOrderTotal', () => {
it('áp dụng giảm giá trước rồi mới tính VAT', () => {
const total = calculateOrderTotal({
items: [{ unitPrice: 200_000, quantity: 2 }],
discountPercent: 10,
vatRate: 0.1,
})
expect(total).toBe(396_000) // (400_000 - 40_000) * 1.1
})
})
#!/usr/bin/env bash
# /tmp/bisect/check.sh
# Exit code: 0 = good, 1-124/126/127 = bad, 125 = skip (commit không build/chạy được)
set -u
cd "$(git rev-parse --show-toplevel)"
[ -f src/orders/total.ts ] || exit 125 # module chưa tồn tại ở commit này
pnpm install --frozen-lockfile --silent >/dev/null 2>&1 || exit 125
cp /tmp/bisect/order-total.test.ts src/orders/__bisect__.test.ts
trap 'rm -f src/orders/__bisect__.test.ts' EXIT
pnpm vitest run src/orders/__bisect__.test.ts >/dev/null 2>&1
chmod +x /tmp/bisect/check.sh
git bisect start main v2.4.0 # bad trước, good sau
git bisect run /tmp/bisect/check.sh
# ... Git tự checkout, chạy script, đánh dấu good/bad ...
# 6f3e9d1 is the first bad commit (output minh họa)
# commit 6f3e9d1
# Refactor discount calculation into pricing module
git bisect reset # quay về nhánh ban đầu
Những điểm hay bị bỏ qua:
- Exit code 125 nghĩa là skip. Commit nào không cài được dependency hay không compile thì trả 125, Git sẽ bỏ qua thay vì đánh dấu bad. Nếu script trả về 128 trở lên, bisect sẽ dừng hẳn.
- Code đã di chuyển. Nếu
src/orders/total.tsbị đổi tên ở giữa đoạn lịch sử, import trong test sẽ fail ở các commit cũ và bị coi là bad. Dòng[ -f ... ] || exit 125trong script xử lý đúng trường hợp đó. - Exit code của script là của Vitest.
trap ... EXITchỉ dọn file, không làm thay đổi exit code, nên Git vẫn nhận đúng kết quả good/bad. - Lịch sử có nhiều merge.
git bisect start --first-parentchỉ đi theo các merge commit trênmain, cho bạn biết PR nào gây bug thay vì một commit lẻ bên trong PR. - Bisect thủ công khi bug chỉ kiểm tra được bằng mắt:
git bisect good/git bisect badsau mỗi lần thử,git bisect logđể xem lại các bước đã đánh dấu.
git worktree: hotfix mà không đụng nhánh đang làm
Worktree cho bạn thêm một thư mục làm việc dùng chung .git với repo hiện tại. Nhánh feature đang dở vẫn nằm yên ở thư mục cũ, dev server vẫn chạy, còn hotfix làm ở thư mục bên cạnh.
# Đang ở ~/code/shop, nhánh feature/invoice-export có thay đổi dở dang
git fetch origin
git worktree add ../shop-hotfix -b hotfix/vat-rounding origin/main
cd ../shop-hotfix
pnpm install --frozen-lockfile # node_modules không dùng chung giữa các worktree
cp ../shop/.env.local .env.local # file untracked không được copy sang
# sửa, test, commit, push như bình thường
git commit -am "Fix VAT rounding for invoices"
git push # autoSetupRemote lo phần upstream
cd ../shop
git worktree list
# /Users/me/code/shop 9c41e2a [feature/invoice-export]
# /Users/me/code/shop-hotfix b7d2a10 [hotfix/vat-rounding]
git worktree remove ../shop-hotfix # xóa thư mục khi xong (từ chối nếu còn thay đổi chưa commit)
git worktree prune # dọn metadata nếu bạn đã rm -rf thư mục bằng tay
Chi phí và giới hạn:
- Một nhánh chỉ được checkout ở một worktree tại một thời điểm.
git switch maintrong worktree chính sẽ báo lỗi nếumainđang mở ở worktree khác. - Mỗi worktree cần
node_modulesriêng. Với pnpm thì nhanh vì package được hard link từ store chung. Với npm, lần cài đầu sẽ tốn thời gian và dung lượng đĩa. - Hook (Husky, lefthook) và config local vẫn dùng chung vì nằm trong
.git.
Worktree cũng hữu ích để review PR của người khác: git worktree add ../shop-review origin/feature/payment-retry, chạy thử mà không phải stash gì cả.
Tìm trong lịch sử và dọn dẹp repo
Khi cần biết "ai đổi logic này, lúc nào, vì sao", đây là các lệnh mình dùng theo thứ tự:
# Commit nào thêm hoặc xóa chuỗi này (số lần xuất hiện thay đổi)
git log -S 'applyLoyaltyDiscount' --oneline
# Commit nào có dòng diff khớp regex (kể cả chỉ sửa dòng chứa nó)
git log -G 'discount(Rate|Percent)' --oneline -- src/orders/
# Lịch sử của riêng một hàm
git log -L :calculateOrderTotal:src/orders/total.ts
# Lịch sử file kể cả khi đã đổi tên
git log --follow --oneline -- src/orders/total.ts
# Toàn cảnh nhánh
git log --oneline --graph --decorate --all -30
# Blame bỏ qua thay đổi whitespace (-w) và dò code bị di chuyển/copy từ file khác (-C -C -C)
git blame -w -C -C -C src/orders/total.ts
-S (pickaxe) và -G khác nhau ở chỗ: -S chỉ báo commit làm thay đổi số lần xuất hiện của chuỗi, nên commit chỉ sửa tham số của applyLoyaltyDiscount(...) sẽ không hiện. -G báo mọi commit có dòng thêm/xóa khớp regex. Bắt đầu bằng -S vì kết quả ít nhiễu hơn.
Với blame, một commit "format toàn bộ repo bằng Prettier" có thể làm mọi dòng trỏ về nó. Tạo file .git-blame-ignore-revs chứa hash các commit kiểu đó, rồi git config blame.ignoreRevsFile .git-blame-ignore-revs trong repo. Giao diện blame của GitHub cũng tự đọc file này.
Dọn nhánh đã merge. Sau vài tháng, git branch dài cả trăm dòng:
git fetch --prune
# Nhánh đã merge thật (merge commit hoặc fast-forward) vào main
git branch --merged main | grep -vE '^\*|^\+|^[[:space:]]*(main|develop)$' | xargs git branch -d
# Nhánh được squash merge trên GitHub sẽ không hiện trong --merged.
# Dấu hiệu: upstream đã bị xóa trên remote -> "[gone]"
git gone # alias ở đầu bài, liệt kê để kiểm tra trước
git gone | xargs git branch -D
^\+ loại các nhánh đang checkout ở worktree khác. Dùng -d cho nhóm đầu vì Git sẽ từ chối nếu nhánh chưa merge thật. Nhóm gone cần -D vì sau squash merge Git không nhận ra commit đã vào main, nên luôn xem danh sách trước khi xóa.
Repo lớn chạy chậm. Với monorepo nhiều năm tuổi, git status và git fetch có thể mất vài giây mỗi lần:
git maintenance start # đăng ký repo và lên lịch tác vụ nền (launchd/systemd/cron)
git config core.fsmonitor true # macOS/Windows: daemon theo dõi file, status nhanh hơn
git config core.untrackedCache true
git maintenance start chạy định kỳ các tác vụ như prefetch, cập nhật commit-graph và repack tăng dần, để lệnh hằng ngày không phải gánh việc đó. Gỡ bằng git maintenance unregister nếu không cần nữa.
Những lỗi thường gặp
1. Force push sau rebase
# Sai: ghi đè mọi thứ trên remote, kể cả commit đồng nghiệp vừa push
git push --force
# Đúng
git push --force-with-lease --force-if-includes # hoặc: git pushf
2. git add . rồi commit luôn
# Sai: lẫn .env.local, file debug, thay đổi format của editor
git add . && git commit -m "fix"
# Đúng: duyệt từng hunk, xem lại trước khi commit
git add -p
git diff --staged --stat
git commit
3. Dùng checkout để bỏ thay đổi một file
# Dễ nhầm: nếu "orders" là tên nhánh, lệnh sẽ đổi nhánh thay vì khôi phục thư mục
git checkout orders
# Rõ nghĩa
git restore orders/
4. Stash mà quên file mới
# Sai: src/invoices/csv.ts (untracked) vẫn ở lại working tree
git stash
# Đúng
git stash push -u -m "invoice-export: WIP csv"
5. Rebase nhánh dùng chung. Rebase main hoặc develop mà người khác đã dựa vào làm lại hash của họ. Chỉ rebase nhánh của riêng bạn, hoặc nhánh mà team đã thống nhất là được viết lại.
6. reset --hard khi chưa kiểm tra. git reset --hard origin/main xóa sạch thay đổi chưa commit, và reflog không cứu được phần đó. Dùng git reset --keep origin/main: nó từ chối nếu thay đổi local sẽ bị mất.
7. Bisect với test đã commit trong repo. Test vừa viết đã commit thì không tồn tại ở commit cũ, mọi bước đều fail và bisect chỉ sai commit. Giữ test và script ngoài repo, copy vào lúc chạy như ví dụ ở trên.
Checklist
- Thêm các option
pull.rebase,rebase.autoStash,rerere.enabled,merge.conflictStyle zdiff3,push.autoSetupRemotevào~/.gitconfig. - Tạo alias
pushfvà bỏ hẳn thói quen gõgit push --force. - Dùng
git switch/git restorethay chogit checkouttrong một tuần, rồi xem còn nhớ checkout nữa không. - Trước khi mở PR:
git add -p, commit fixup cho từng sửa đổi,git rebase -i --autosquash origin/main. - Đặt tên mọi stash bằng
-mvà dùng-ukhi có file mới. - Lần tới có bug "trước đây chạy được", viết script test và chạy
git bisect runthay vì đọc log thủ công. - Làm hotfix tiếp theo trong
git worktreethay vì stash. - Thêm
.git-blame-ignore-revscho các commit format lớn và dọn nhánh[gone]mỗi tháng.