Luyện Phỏng Vấn IT — 2000+ Câu Hỏi Phỏng Vấn IT Có Đáp Án 2026
Git
Git Flow có 5 loại branch: main (production), develop (tích hợp code), feature/ (tính năng mới), release/ (chuẩn bị release), hotfix/* (sửa lỗi khẩn cấp).
- Trunk-based Development là hướng ngược lại: commit thẳng vào
mainvới feature flags, phù hợp team CI/CD mature. - Thực tế phổ biến nhất: feature branch từ develop → PR review → merge develop → staging → merge main → production.
- Naming convention quan trọng:
feature/JIRA-123-add-logingiúp trace thay đổi về requirement.
Git Flow hotfix:
git checkout main
git pull origin main
git checkout -b hotfix/critical-payment-bug
# Fix the bug
git commit -m "fix: resolve payment calculation overflow"
git checkout main && git merge --no-ff hotfix/critical-payment-bug
git tag -a v1.2.1 -m "hotfix: payment bug"
git push origin main --tags
# Backport to develop
git checkout develop && git merge --no-ff hotfix/critical-payment-bug
git branch -d hotfix/critical-payment-bugGitHub Flow hotfix (đơn giản hơn):
git checkout main && git pull
git checkout -b hotfix/payment-bug
# Fix, test
git push origin hotfix/payment-bug
# Tạo PR vào main, mark emergency, get fast review
# Merge → deploy ngayQuan trọng: luôn merge hotfix vào cả production branch VÀ development branch — đừng để fix bị mất ở lần deploy tới.
- Tag version sau hotfix để dễ rollback.
- Viết post-mortem sau khi hệ thống ổn định.
Amend last commit — chỉ dùng khi commit CHƯA push:
# Chỉ sửa message:
git commit --amend -m "feat: correct message here"
# Thêm file bị quên:
git add forgotten-file.ts
git commit --amend --no-edit # giữ nguyên message
# Sửa cả file lẫn message:
git add forgotten-file.ts
git commit --amend -m "feat: complete implementation with tests"Nếu đã push (chỉ branch của riêng bạn):
git commit --amend -m "corrected message"
git push --force-with-lease origin feature/my-branchLưu ý quan trọng:
- --amend tạo commit MỚI với hash khác — không phải edit in-place
- KHÔNG amend commits trên main/develop hoặc bất kỳ shared branch
- --force-with-lease an toàn hơn --force — sẽ fail nếu remote có commits mới mà local chưa có (tránh overwrite người khác)
- Nếu cần sửa commit cũ hơn (không phải last): dùng git rebase -i
- Merge: tạo merge commit, giữ history đầy đủ, safe cho shared branches.
- Rebase: rewrite history thành linear, clean hơn, KHÔNG dùng cho shared branches.
- Best practice: rebase feature branch lên develop trước khi merge (hoặc squash merge).
git rebase -iđể clean commits.
Git Flow phù hợp khi: release cycle dài (weeks/months), cần maintain nhiều version song song (SaaS với enterprise customers), team lớn cần isolation mạnh. Nhược điểm thực tế: develop branch trở thành "integration hell", feature branches sống lâu gây merge conflict khổng lồ, hotfix phải merge vào cả main và develop.
Trunk-based development (TBD): tất cả developer push thẳng vào main (hoặc branch ngắn <1 ngày). Deploy nhiều lần/ngày.
Dấu hiệu nên chuyển sang TBD: feature branches sống >3 ngày, merge conflict mất >30 phút/tuần, CI pipeline chạy trên develop nhưng main vẫn broken, team muốn CD thực sự.
Migration path: bật feature flags để tách deploy khỏi release, commit nhỏ hơn, CI/CD bắt buộc pass trước merge, pair programming/code review nhanh hơn.
Lưu ý: TBD yêu cầu kỷ luật cao — không có "tôi sẽ fix sau khi merge". Mọi commit vào main phải deploy-ready.
GitHub Flow (đơn giản): main là production-ready, mọi tính năng làm trên branch từ main, PR → review → merge → deploy ngay. Chỉ có 1 loại branch ngoài main.
Git Flow (phức tạp): main + develop + feature/ + release/ + hotfix/*. Overhead cao nhưng kiểm soát release tốt hơn.
Startup 5 người → GitHub Flow:
- ít overhead, không cần ceremony
- deploy liên tục không cần release branch
- hotfix đơn giản — branch từ main, fix, PR, merge, deploy
- team nhỏ → pair review nhanh hơn formal release process
Khi nào startup cần Git Flow: có enterprise customers yêu cầu quarterly release, app mobile cần app store review cycle, compliance yêu cầu release notes chính thức.
Branch naming convention cho GitHub Flow: feature/user-auth, fix/login-bug, chore/update-deps, docs/api-readme. Xóa branch ngay sau merge — đừng để branch zombie tích tụ.
Monorepo không thay đổi branching strategy cơ bản nhưng tăng complexity của conflict và CI.
Recommended: trunk-based + CODEOWNERS + scoped CI
CODEOWNERS (/.github/CODEOWNERS): định nghĩa team nào review phần nào:
/apps/frontend/ @frontend-team
/apps/backend/ @backend-team
/packages/shared/ @core-teamPR chỉ require approval từ owner của file được thay đổi → teams không block nhau.
Scoped CI: chỉ chạy test của packages bị ảnh hưởng — dùng nx affected, turborepo --filter, hoặc changesets. Đừng chạy full test suite cho mọi PR.
Branch per team: mỗi team có team/frontend/feature-x namespace riêng, merge vào main thường xuyên.
Pitfall: shared packages (/packages/shared) là bottleneck — thay đổi đây require tất cả teams test. Giải pháp: versioned internal packages với changelogs, không mutate shared package mà không notify.
Release branch (release/1.5.0) tách quá trình stabilize một version khỏi ongoing development. Flow: feature freeze → tạo release branch → chỉ bug fixes được merge vào → QA → deploy → merge về main + tag.
Cần release branch khi:
- QA cycle dài (days/weeks) — cần isolate khỏi new features
- Multiple release tracks song song (v1.5 và v2.0 cùng development)
- Regulatory/compliance cần sign-off trước deploy
- Mobile apps với app store review lag
Overhead không cần thiết khi:
- Continuous deployment (deploy mỗi merged PR)
- Team nhỏ, QA cycle <1 ngày
- SaaS web app với rollback dễ dàng
Thực tế: nhiều teams tạo release branch vì "đó là best practice" nhưng chưa bao giờ thực sự cần. Nếu bạn deploy 5 lần/ngày, release branch là waste. Nếu deploy 1 lần/tháng với sign-off process, release branch là cần thiết.
Vào GitHub repo → Settings → Branches → Add branch protection rule cho main:
Tối thiểu cần bật:
- Require a pull request before merging — không ai push thẳng
- Require approvals (1-2 reviewers)
- Dismiss stale pull request approvals when new commits are pushed — re-review sau mỗi force push
- Require status checks to pass — CI phải xanh trước khi merge
- Require branches to be up to date before merging — không merge stale branch
- Include administrators — áp dụng cả cho repo owner
Nâng cao:
- Require signed commits — GPG/SSH signature bắt buộc
- Require linear history — chỉ cho phép squash/rebase merge (không merge commit)
- Restrict who can push to matching branches — chỉ CI bot được push sau review
Lưu ý: Include administrators hay bị bỏ quên — không bật thì owner vẫn có thể bypass. Với GitHub Enterprise: dùng ruleset thay vì branch protection — linh hoạt hơn, có thể apply cho pattern nhiều branches.
Conventional Commits là convention format: type(scope): description. Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert.
Tại sao không chỉ "agree": agreement không có enforcement = mọi người quên hoặc bỏ qua khi deadline gần. Sau 3 tháng, git log trở thành "fix stuff", "update", "wip", "asdfgh".
Setup commitlint + husky:
npm install --save-dev @commitlint/cli @commitlint/config-conventional husky
# commitlint.config.js:
module.exports = { extends: ["@commitlint/config-conventional"] }
# .husky/commit-msg:
npx --no -- commitlint --edit $1Lợi ích thực tế:
- semantic-release tự động bump version và generate CHANGELOG từ commits
- git log --oneline readable: thấy ngay đây là feature hay fix
- PR title auto-generate từ commit message
- Breaking changes: feat!: hoặc BREAKING CHANGE: footer tự động trigger major version bump
Lưu ý: đừng reject commit của mọi người khi scope sai — chỉ enforce type và description format, để scope optional.
Interactive rebase (git rebase -i) cho phép edit lịch sử commit local trước khi share.
Workflow chuẩn trước merge PR:
git rebase -i HEAD~5 # edit 5 commits gần nhất
# hoặc
git rebase -i origin/main # tất cả commits chưa mergeTrong editor hiện ra:
pick abc123 feat: add user model
pick def456 fix typo
pick ghi789 wip: half done
pick jkl012 fix tests
pick mno345 final cleanupThay đổi:
- squash (s): merge commit này vào trước, giữ message
- fixup (f): merge vào trước, BỎ message (dùng cho "fix typo", "wip")
- reword (r): giữ commit, chỉ edit message
- drop (d): xóa commit hoàn toàn
- edit (e): dừng lại tại commit để amend
Ví dụ kết quả tốt: 5 WIP commits → 1 clean commit feat(auth): implement JWT login with refresh tokens
Lưu ý quan trọng: CHỈ rebase commits chưa push lên remote (hoặc trên branch riêng của bạn). Rebase shared commits = disaster cho teammates.
3 merge strategies trên GitHub:
Merge commit (--no-ff): tạo merge commit, giữ toàn bộ history của branch. git log --graph thấy branch structure. Tốt cho: feature branches quan trọng muốn preserve full context.
Squash merge: tất cả commits của PR → 1 commit trên main. History linear và clean. Xấu: mất toàn bộ individual commit history của feature.
Rebase merge: replay từng commit của PR lên main, không có merge commit. Linear history nhưng giữ commit granularity.
Nên dùng squash khi:
- PR có nhiều WIP/fixup commits không meaningful
- Team muốn main branch history = one-line-per-feature
- Semantic release dựa trên commit messages trên main
Nên dùng merge commit khi:
- Branch history có value (nhiều meaningful commits)
- Muốn git bisect hoạt động trên từng small commit
- Audit trail quan trọng
Thực tế: squash merge phổ biến nhất cho application code, merge commit cho library/SDK với semantic versioning chi tiết.
Signed commits đảm bảo commit thực sự từ người có private key tương ứng — chống giả mạo identity (bất kỳ ai cũng có thể set git config user.email thành email của bạn).
Setup GPG signing:
gpg --gen-key
gpg --list-secret-keys --keyid-format=long
# Lấy key ID, ví dụ: 3AA5C34371567BD2
git config --global user.signingkey 3AA5C34371567BD2
git config --global commit.gpgsign true
# Upload public key lên GitHub Settings → SSH and GPG keysSSH signing (mới hơn, dễ hơn GPG):
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign trueGitHub hiển thị badge Verified trên commit khi signature hợp lệ.
Khi nào enforce:
- Regulated industries (fintech, healthcare) — audit trail
- Open source projects — chống supply chain attack (ai đó push code giả tên maintainer)
- Khi GitHub Actions deploy từ commits — đảm bảo chỉ deploy signed commits
Lưu ý: GPG key rotation phức tạp — SSH signing dễ manage hơn cho enterprise.
Atomic commit: mỗi commit chứa MỘT logical change hoàn chỉnh — pass tests độc lập, có thể revert độc lập, message mô tả đủ ý.
Tại sao commit "fat" là anti-pattern:
1. Revert nightmare: cần revert chỉ navbar fix nhưng buộc phải revert cả auth + deps update
2. git bisect bị nhiễu: khi tìm regression, mỗi commit nên là one thing — commit fat khó xác định cái gì gây bug
3. Code review khó: reviewer phải context-switch giữa auth logic, CSS, và package.json
4. git blame vô nghĩa: blame trên navbar file thấy commit "add user auth" → confusing
Ví dụ tách đúng:
git add src/auth/ && git commit -m "feat(auth): implement JWT authentication"
git add src/navbar/ && git commit -m "fix(navbar): correct mobile menu z-index"
git add package.json && git commit -m "chore(deps): upgrade react to 18.3.1"Trick: git add -p (patch mode) — interactive staging theo từng hunk, không phải toàn bộ file.
Cho phép tách 1 file có nhiều thay đổi thành nhiều commits.
Merge: tạo merge commit, preserve history đầy đủ. git log --graph thấy nhánh. Non-destructive — không thay đổi existing commits.
Rebase: replay commits lên đỉnh của branch khác, tạo linear history. Rewrite commit hashes.
Golden Rule of Rebasing: KHÔNG BAO GIỜ rebase branch đã được share/push lên remote mà người khác đang dùng.
Vì sao: rebase tạo commits MỚI với hash khác. Nếu teammate đã pull branch của bạn, họ có commits với hash cũ. Khi bạn force push sau rebase, history của họ và remote diverge → họ phải resolve "fake conflicts" khi pull.
Khi dùng rebase (an toàn):
- Trên local feature branch chưa push
- git pull --rebase để sync với remote (thay vì tạo merge commit)
- Clean up commits trước khi tạo PR: git rebase -i origin/main
Khi dùng merge:
- Merge feature branch vào main (qua PR)
- Tích hợp upstream changes vào long-lived branch của team
- Khi branch đã được share
Thực tế: rebase locally, merge publicly (via PR).
Fast-forward merge (mặc định khi có thể): nếu branch hiện tại là ancestor trực tiếp của branch cần merge, git chỉ di chuyển HEAD pointer lên — không tạo merge commit.
History linear.
git checkout main
git merge feature/login # nếu main chưa có commit mới → fast-forward
# History: A - B - C(feature) ← HEAD/mainNo-ff merge (--no-ff): luôn tạo merge commit dù có thể fast-forward.
git merge --no-ff feature/login
# History: A - B - M ← merge commit
# \
# C(feature)Trade-offs:
- Fast-forward: history clean, dễ git log --oneline, nhưng mất context "đây là feature branch"
- No-ff: thấy rõ feature grouping, dễ revert cả feature (git revert -m 1 <merge-commit>), nhưng log lộn xộn
GitHub default: merge commit (no-ff) — squash và rebase phải enable riêng trong Settings. Không fast-forward trên remote.
Recommendation: enforce squash merge (1 commit/PR) để main history clean, hoặc no-ff nếu cần preserve feature grouping. Đừng mix strategies trong 1 repo.
Worst case conflict: không phải thêm/xóa dòng đơn giản mà là structural refactor — function bị rename, logic được reorganize.
Quy trình an toàn:
1. Hiểu context trước khi resolve:
git log --oneline main..feature/my-branch # xem commits của feature
git log --oneline feature/my-branch..main # xem commits của main
git diff $(git merge-base HEAD feature/my-branch) feature/my-branch -- src/file.ts
# Xem feature branch đã thay đổi gì**2.
Dùng 3-panel merge tool (không resolve bằng tay trong terminal):**
git mergetool # opens configured tool
# Left: ours, Right: theirs, Center: base (common ancestor)
# Bottom: result**3.
Resolve và verify:**
git add src/file.ts
# CHƯA commit — chạy tests trước:
npm test -- --testPathPattern=file.ts
# Nếu pass:
git merge --continue**4.
Sau merge — review lại result:**
git diff main~1..main -- src/file.ts # xem diff của merge commitNguyên tắc: khi không chắc, chọn solution bảo toàn logic của cả 2 sides thay vì chọn 1 side.
Đừng dùng -X ours hay -X theirs với structural conflicts.
Reset (git reset --hard HEAD~1): di chuyển HEAD pointer về commit cũ, xóa commits khỏi history. KHÔNG dùng trên main/shared branch — sẽ cần force push, gây disaster cho mọi người đã pull.
Revert (git revert <commit>): tạo commit MỚI undo changes của commit cũ. An toàn cho shared branches vì không rewrite history.
# Revert 1 commit:
git revert abc123
# Revert merge commit (phải chỉ định mainline parent):
git revert -m 1 abc123 # -m 1 = giữ lại parent thứ 1 (main branch)
# Revert range commits:
git revert abc123..def456
# Revert nhiều commits nhưng chỉ 1 commit revert:
git revert --no-commit abc123 def456
git commit -m "revert: remove broken payment feature"Ví dụ thực tế: deploy feature lên production, users báo lỗi nghiêm trọng → git revert -m 1 <merge-commit> → push → deploy → feature bị undo ngay mà không ảnh hưởng history.
Khi nào dùng reset: trên local branch chưa share, hoặc clean up local staging area.
Lưu ý revert merge commit: nếu sau đó muốn re-merge feature đó, phải revert lại revert commit trước — không thể merge thẳng vì git sẽ "thấy" commits đã được merge rồi.
Pickaxe search — tìm commits đã thêm/xóa một chuỗi cụ thể trong code.
git log -S "string" (pickaxe): tìm commits mà số lần xuất hiện của string thay đổi (thêm hoặc xóa).
git log -S "calculateDiscount" --oneline
# Tìm commit đã thêm hoặc xóa function này
git log -S "SECRET_KEY" --all # tìm trong mọi branches — security auditgit log -G "regex": tìm commits mà diff chứa regex pattern (line added/removed matching pattern).
git log -G "discountRate\s*=\s*[0-9]+" --oneline
# Tìm mọi lần giá trị discountRate bị thay đổiKhác nhau: -S đếm occurrences (chính xác hơn khi rename/move), -G match regex trong diff lines.
Ví dụ thực tế: bug production — giá tính sai. Không biết ai sửa khi nào:
git log -S "applyTax" --since="2024-01-01" -p
# -p: show patch (diff) của commit đó
# Thấy ngay ai thay đổi logic thuế lần cuốiKết hợp với blame:
git blame src/pricing.ts -L 45,60 # ai viết dòng 45-60
# Sau đó git show <commit> để xem full contextBranch chỉ là pointer đến commit. Xóa branch không xóa commits — chỉ xóa pointer. Commits vẫn tồn tại trong git object store cho đến khi git gc chạy.
Recovery qua reflog:
git reflog --all | grep "feature/deleted-branch"
# Output: abc123 refs/heads/feature/deleted-branch@{0}: commit: feat: last work
# Recreate branch tại commit đó:
git checkout -b feature/deleted-branch abc123
# hoặc:
git branch feature/deleted-branch abc123Nếu không nhớ tên branch:
git reflog | grep "checkout: moving from"
# Tìm lần cuối bạn checkout khỏi branch đóNếu reflog không có (branch trên remote bị xóa):
git fetch origin # nếu remote còn lưu
# Hoặc hỏi teammate — họ có thể có local copyNếu đã git gc chạy:
git fsck --lost-found
# Tìm "dangling commit" — đó là commits không còn branch nào trỏ đến
for sha in .git/lost-found/commit/*; do git log --oneline -1 $sha; done # xem commits tìm đượcPhòng tránh: trước khi xóa branch, push lên remote hoặc tag commit cuối: git tag backup/feature-before-delete <branch>.
git blame trong thực tế: nó không chỉ để "đổ lỗi" — những use case thực sự hữu ích là gì?git blame <file> hiển thị mỗi dòng: commit hash, author, date, line content — ai viết dòng đó khi nào.
Use cases thực sự hữu ích:
1. Hiểu context của code lạ:
git blame src/utils/pricing.ts -L 23,35
# Thấy: dòng 28 được viết bởi john 6 tháng trước
# git show <hash> → xem full commit, đọc message, hiểu lý do**2.
Tìm commit introduce bug:**
git blame -w src/auth.ts # -w: ignore whitespace changes
git blame -M src/auth.ts # -M: detect moved lines
git blame -C src/auth.ts # -C: detect copied lines from other files3. Trong VSCode/IDE: GitLens extension hiển thị blame inline → hover để xem commit, click để open PR → truy ngược decision history.
4. Khi integrate với external PR/issue tracker:
git log --follow -p src/payments/stripe.ts
# Xem full history kể cả khi file bị rename**5.
Security audit**: tìm khi nào secret/credentials bị commit vào:
git log -S "password=" --all -pLưu ý: blame thường trỏ về commit "refactor" hay "format code" — dùng git log --follow và -w để skip qua những thay đổi không có ý nghĩa, tìm commit thực sự thay đổi logic.
--force vs --force-with-lease: tại sao force-with-lease an toàn hơn và team nên enforce như thế nào?git push --force: overwrite remote không kiểm tra gì — nếu teammate đã push commits mới lên remote mà bạn chưa fetch, bạn sẽ overwrite và mất work của họ.
git push --force-with-lease: kiểm tra remote ref trước khi overwrite — chỉ force push nếu remote ref khớp với lần fetch cuối của bạn. Nếu ai đó đã push thêm → lệnh fail, bạn được nhắc fetch trước.
# An toàn:
git push --force-with-lease origin feature/my-branch
# Error nếu remote đã thay đổi:
# ! [rejected] feature/my-branch -> feature/my-branch (stale info)
# Sau đó:
git fetch origin
git rebase origin/feature/my-branch
git push --force-with-lease origin feature/my-branchEnforce trong team:
# .gitconfig alias:
git config --global alias.pushf "push --force-with-lease"
# Sau đó dùng: git pushfHoặc dùng git alias để enforce convention:
# .gitconfig alias (thực tế và đơn giản hơn hook):
git config --global alias.pushf "push --force-with-lease"
# Dùng: git pushf origin feature/branchLưu ý: không thể dùng git push --dry-run --force để detect --force trong pre-push hook vì git không output chuỗi "Would force push" — muốn block phải dùng git alias wrapper thay vì hook parsing.
Cẩn thận: --force-with-lease vẫn không an toàn nếu bạn vừa git fetch (remote ref match nhưng có thể còn work của người khác chưa pushed).
Force push ACCEPTABLE:
1. Feature branch của riêng bạn, chưa ai review/pull: sau git rebase -i để clean up commits trước PR
2. Personal fork: không ảnh hưởng ai
3. Sau git commit --amend trên branch riêng: cần update remote
4. Reset --hard và force push trên branch riêng nếu bạn vô tình push nhầm (credentials, large file)
Force push KHÔNG BAO GIỜ:
1. main / master / develop — bất kỳ shared long-lived branch
2. Branch đang có open PR mà người khác đang review
3. Branch mà CI/CD đang build/deploy từ đó
4. Tag đã được release — git tags không nên bị move
Kiểm tra trước khi force push:
# Ai đã pull branch này?
git log origin/feature/my-branch..feature/my-branch # commits chỉ có local
git log feature/my-branch..origin/feature/my-branch # commits chỉ có remote
# Nếu remote có commits bạn không có → người khác đã push → KHÔNG force pushRule đơn giản: nếu phải hỏi "force push được không?" → không.
Chỉ force push khi chắc 100% bạn là người duy nhất dùng branch đó.
Credentials đã push = credentials bị compromise. Rotate ngay lập tức — đây là ưu tiên số 1.
Bước 1: Rotate credentials (NGAY LẬP TỨC):
- API keys, DB passwords, JWT secrets → revoke và generate mới
- Không chờ xóa khỏi git — ai đó có thể đã crawl rồi
Bước 2: Xóa khỏi git history:
# Dùng git filter-repo (khuyến nghị, thay thế filter-branch):
pip install git-filter-repo
git filter-repo --path .env --invert-paths
# Hoặc BFG Repo Cleaner (nhanh hơn cho large repos):
java -jar bfg.jar --delete-files .env
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push --force --allBước 3: Thông báo team re-clone (sau force push, local repos của mọi người bị stale).
Bước 4: Thêm .env vào .gitignore:
echo ".env" >> .gitignore
echo ".env.local" >> .gitignore
git add .gitignore && git commit -m "chore: add .env to gitignore"Phòng tránh: pre-commit hook detect secrets (git-secrets, detect-secrets, Gitleaks).
GitHub secret scanning tự động notify nếu detect credentials trong push.
git stash nâng cao: push, pop, apply, list, show, drop — những pattern thực tế cần biết?Cơ bản hay bị hiểu nhầm:
- git stash = git stash push — stash working dir + index
- pop = apply + drop (xóa stash sau khi apply)
- apply = apply nhưng GIỮ stash (dùng khi muốn apply vào nhiều branches)
Patterns thực tế:
# Stash có tên (dễ nhận dạng):
git stash push -m "WIP: cart refactor — half done"
# Stash kể cả untracked files:
git stash push -u # --include-untracked
# Stash chỉ 1 file (non-interactive):
git stash push -- src/cart.ts # pathspec — stash chính xác 1 file
# Hoặc interactive patch (chọn từng hunk):
git stash push -p # -p mở interactive hunk selection, không phải file filter đơn thuần
# Xem nội dung stash trước khi apply:
git stash show -p stash@{2} # show diff
# Apply stash cụ thể (không nhất thiết top):
git stash apply stash@{2}
# Xóa stash cụ thể:
git stash drop stash@{2}
# Apply vào branch mới (tạo branch từ stash):
git stash branch feature/new-branch stash@{0}Pitfalls:
- Stash không track untracked files mặc định → dùng -u
- Stash conflict khi apply: resolve như merge conflict, sau đó git stash drop thủ công
- Stash list không hiển thị branch name mặc định → luôn đặt tên với -m
Thực tế: nhiều devs lạm dụng stash thay vì commit WIP. Commit WIP với feat(wip): prefix tốt hơn — có history, không bị mất.
git cherry-pick trong thực tế: khi nào là công cụ đúng và khi nào là dấu hiệu branching strategy có vấn đề?git cherry-pick <commit> copy 1 commit từ branch khác vào branch hiện tại — tạo commit mới với cùng changes nhưng hash khác.
Khi cherry-pick là đúng:
1. Hotfix cần apply vào nhiều release branches:
git checkout release/2.1
git cherry-pick abc123 # apply hotfix commit từ main
git checkout release/2.0
git cherry-pick abc1232. Lấy 1 commit cụ thể từ colleague's branch mà bạn cần ngay:
git cherry-pick def456 # chỉ cần 1 commit, không cần cả branch3. Recover commit từ deleted branch:
git cherry-pick abc123 # commit vẫn tồn tại dù branch bị xóaKhi cherry-pick là dấu hiệu vấn đề:
- Cherry-pick từ develop vào feature hàng ngày → nên rebase thay thế
- "Chúng ta cherry-pick hotfix vào 5 branches" → cần reconsider branching strategy, quá nhiều long-lived branches
- Cherry-pick để share code giữa features → nên extract thành shared module
Lưu ý:
git cherry-pick abc123..def456 # range of commits
git cherry-pick -n abc123 # apply changes nhưng không commit (staging chỉ)Conflict cherry-pick: resolve, git add, git cherry-pick --continue.
Vấn đề: bạn có local commits chưa push.
Teammate đã push lên remote. git pull = git fetch + git merge → tạo merge commit "Merge branch 'main' of github.com/..." không có ý nghĩa.
# git log trông giống:
# * Merge branch 'main' of ... ← không có ý nghĩa
# |\
# | * teammate: feat: add cart
# * | your: feat: add login
# |/
# * old commitFix: dùng git pull --rebase:
git pull --rebase origin main
# = git fetch + git rebase origin/main
# Kết quả: history linear, commits của bạn replay trên topSet làm default:
git config --global pull.rebase true
# Hoặc per-repo:
git config pull.rebase trueTrường hợp pull --rebase gặp conflict:
# Resolve conflict trong file
git add resolved-file.ts
git rebase --continue
# Hoặc abort:
git rebase --abort # về trạng thái trước pullKhi nào dùng git pull (merge) thay vì rebase:
- Branch đã có merge commits quan trọng (merge commit là intentional)
- Long-lived feature branch muốn preserve merge history
Tip: git pull --rebase --autostash tự động stash local changes trước khi rebase, unstash sau.
Vấn đề nếu lint cả project trên mỗi commit:
- Project lớn → eslint . mất 30-60 giây → developers tắt hooks
- Không liên quan: bạn sửa 1 file nhưng phải đợi 500 files khác lint
lint-staged giải pháp: chỉ chạy linter trên files đang trong git staging area.
Setup:
npm install --save-dev husky lint-staged
npx husky initpackage.json:
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{css,scss}": ["prettier --write"],
"*.{json,md}": ["prettier --write"]
}
}.husky/pre-commit:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-stagedKết quả: chỉ 2-3 files được lint → hook chạy trong <3 giây → developers không bỏ hook.
Lưu ý quan trọng về type check trong lint-staged:tsc --noEmit luôn chạy TOÀN BỘ project typecheck — TypeScript cần full project graph để type-check chính xác, không thể chạy per-file. Nên đặt tsc trong CI thay vì lint-staged, hoặc chấp nhận chi phí full-project check mỗi commit.
Lưu ý: --fix auto-fix và re-stage files đã fix — không cần commit lại. Nhưng nếu auto-fix thay đổi logic → developer phải review.
commit-msg hook chạy sau khi developer viết commit message — có thể reject nếu message không hợp lệ.
Setup:
npm install --save-dev @commitlint/cli @commitlint/config-conventionalcommitlint.config.js:
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [2, "always", [
"feat", "fix", "docs", "style", "refactor",
"test", "chore", "perf", "ci", "build", "revert", "wip"
]],
"subject-max-length": [1, "always", 100], // warn, not error
"body-max-line-length": [0], // disable
}
};.husky/commit-msg:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit $1Cho phép WIP commits (không block nhưng vẫn có format):
- Thêm "wip" vào type-enum
- wip: payment refactor halfway → pass
Bypass khi thực sự cần (emergency/merge commit):
git commit --no-verify -m "emergency fix"
# --no-verify bỏ qua TẤT CẢ client-side hooksLưu ý: commitlint enforce trên client — developer vẫn có thể bypass bằng --no-verify.
Server-side enforcement cần CI check (dùng commitlint trong GitHub Actions trên PR title).
pre-push hook chạy trước khi git push — có thể block push nếu tests fail.
Trade-off chính:
- Bảo vệ remote branch khỏi broken code
- Nhưng full test suite có thể mất 5-10 phút → developers làm theo cách khác (push thẳng không qua hook)
Setup thực tế — chỉ chạy tests liên quan:
# .husky/pre-push:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# Lấy danh sách files đang thay đổi so với main:
CHANGED=$(git diff --name-only origin/main HEAD | grep -E "\.(ts|tsx)$")
if [ -n "$CHANGED" ]; then
# Chỉ chạy tests của files bị thay đổi:
npx jest --findRelatedTests $CHANGED --passWithNoTests
fiHoặc chạy fast subset:
npm run test:unit # unit tests only (~30s), không integration testsPattern phổ biến: pre-commit → lint/format (fast), pre-push → unit tests (medium), CI → full test suite (slow).
Client hooks vs Server hooks:
- Client hooks: developer có thể bypass (--no-verify)
- Server hooks (GitHub Actions): không thể bypass, bắt buộc
- Recommendation: pre-push là safety net cho developer bản thân, CI là enforcement thực sự
Bypass khi cần:
git push --no-verify # skip pre-push hookBranch sống lâu là nợ kỹ thuật về integration. Xử lý từng bước:
1. Trước khi làm bất cứ điều gì — tạo backup:
git branch backup/feature-before-rebase**2.
Rebase từng phần (incremental rebase) thay vì một lần:**
git fetch origin
git rebase origin/mainNếu conflict quá nhiều, rebase tới mid-point commit của main trước. (Lưu ý: --onto yêu cầu 3 tham số: git rebase --onto <newbase> <upstream> [<branch>] — không dùng redundant ở đây.)
3. Resolve conflicts theo từng commit — git rebase --continue sau mỗi conflict. Dùng git mergetool (vimdiff, VSCode, IntelliJ) để visual diff.
4. Sau rebase — chạy full test suite trước khi push.
5. Force push (với lease) lên feature branch:
git push --force-with-lease origin feature/my-branchPhòng tránh lần sau: rebase lên main mỗi ngày (hoặc dùng git fetch && git rebase origin/main), chia feature lớn thành nhiều PR nhỏ, dùng feature flags để merge code chưa hoàn thiện vào main sớm.
Polyrepo: mỗi service/app có repo riêng. Branching đơn giản hơn, CI riêng biệt, team độc lập. Nhược điểm: cross-repo changes phức tạp (cần coordinate nhiều PRs), version hell khi shared library update, hard để làm atomic change spanning nhiều services.
Monorepo: tất cả code trong 1 repo. Atomic changes, single source of truth cho shared code, dễ refactor cross-cutting concerns. Nhược điểm: CI chậm hơn, cần tooling (Nx, Turborepo), git clone lớn.
Git workflow differences:
- Polyrepo: mỗi repo có branch protection, deploy pipeline riêng
- Monorepo: cần scoped CI (nx affected), CODEOWNERS phân quyền theo path, tag versioning phức tạp hơn
Khi nên migrate sang Monorepo: thường xuyên cần thay đổi shared library và update consumers đồng thời, khó maintain consistency (linting, tsconfig, deps) across repos, cross-repo PR coordination tốn >2 giờ/sprint.
Migration: không phải "big bang" — dùng git subtree để import history, giữ polyrepo cũ read-only.
git reset --hard xóa mất 3 commit chưa push, làm sao recover?git reflog lưu lại MỌI thay đổi của HEAD trong ~90 ngày (mặc định), kể cả các commit "đã mất".
Quy trình recover:
git reflog
# Output:
# abc123 HEAD@{0}: reset: moving to HEAD~3
# def456 HEAD@{1}: commit: feat: add payment service
# ghi789 HEAD@{2}: commit: feat: add cart logic
# jkl012 HEAD@{3}: commit: feat: add product modelTìm hash của commit cuối cùng trước khi reset, rồi:
# Recover tất cả 3 commits:
git reset --hard def456
# Hoặc cherry-pick từng commit:
git cherry-pick jkl012 ghi789 def456Ví dụ: mất 3 commit liên tiếp — dùng git reset --hard def456 (hash của commit cuối trước khi reset, tức HEAD@{1} trong ví dụ trên). Không dùng HEAD@{4} vì sẽ đi xa hơn cần thiết.
Lưu ý:
- reflog chỉ tồn tại trong local repo — clone lại từ remote sẽ KHÔNG có. Phải recover trên máy đã reset.
- git gc chạy định kỳ có thể xóa unreachable commit sau thời gian ngắn. Recover sớm.
- Stash bị mất: git fsck --lost-found rồi tìm dangling commit.
Phòng tránh: trước khi reset --hard luôn tạo backup branch: git branch backup/safe-point.
CODEOWNERS (/.github/CODEOWNERS) định nghĩa ai là owner của file/directory — GitHub tự động add họ làm required reviewers khi PR chạm vào files đó.
Format:
# Syntax: pattern @user-or-team
* @default-reviewer # catch-all
/src/auth/ @security-team # auth code
/src/payments/ @payments-team @cto # payments: 2 owners
*.sql @dba-team # all SQL files
/docs/ @tech-writer # documentation
/.github/ @devops-team # CI/CD configs
/package.json @lead-dev # dependency changesRules:
- Patterns match như .gitignore — last matching rule wins
- Cần enable "Require review from Code Owners" trong branch protection
- Team owners: @org/team-name (không phải individual)
Pitfalls:
- Quá nhiều owners → bottleneck (mọi PR cần 5 người approve)
- Không review CODEOWNERS định kỳ → departed employees vẫn là required reviewers
- Forgetting catch-all * rule → new files có thể không có owner
Best practice: ít owners per path, dùng teams thay vì individuals, review CODEOWNERS mỗi quarter.
Rebase hell xảy ra khi: branch sống lâu, conflict ở nhiều commits, resolve sai rồi continue → conflict tiếp theo phức tạp hơn.
Chiến lược:
1. Abort và squash trước:
git rebase --abort
# Squash branch thành 1-2 commits
git rebase -i origin/main # squash all → fixup
# Sau đó rebase lại — chỉ cần resolve 1 lần**2.
Rebase từng bước nhỏ:**
# Thay vì rebase lên main hiện tại (50 commits ahead),
# rebase lên commit ở giữa trước
git rebase abc123 # commit từ 3 tuần trước
# Resolve conflicts
git rebase origin/main # từ đó lên hiện tại**3.
Dùng rerere (Reuse Recorded Resolution):**
git config --global rerere.enabled true
# Git ghi nhớ cách bạn resolve conflict → tự replay lần sau**4.
Khi bị stuck — inspect từng bước:**
git status # xem files nào conflict
git diff # xem diff trước khi resolve
git rebase --skip # bỏ qua commit này (chỉ khi commit thực sự empty sau conflict)
git rebase --abort # về trạng thái trước rebase**5.
Dùng merge tool:**
git mergetool # mở vimdiff / VSCode / IntelliJ merge view3-way merge: khi merge 2 branches, git dùng 3 snapshots:
1. Base (common ancestor): commit nơi 2 branches diverge
2. Ours (current branch HEAD)
3. Theirs (branch đang merge vào)
Git auto-resolve khi: thay đổi ở 2 branches không chạm cùng vùng code — git lấy thay đổi từ cả hai.
Conflict xảy ra khi: cả 2 sides thay đổi cùng dòng, hoặc 1 side delete file mà side kia modify.
Conflict markers:
<<<<<<< HEAD
console.log("our change")
=======
console.log("their change")
>>>>>>> feature/loginManual resolve: xóa markers, giữ đúng code, git add <file>, git merge --continue.
Chiến lược merge:
git merge -X ours feature/login # ưu tiên our side khi conflict
git merge -X theirs feature/login # ưu tiên their side
# Dùng thận trọng — có thể silently mất codeKhi nào dùng ours/theirs strategy: merge release branch về main khi biết chắc 1 side luôn đúng (ví dụ: hotfix đã được test kỹ).
Không dùng cho feature merge thông thường.
git rerere là gì? Khi nào nó cứu bạn khỏi resolve cùng conflict nhiều lần?rerere = Reuse Recorded Resolution: git ghi nhớ cách bạn resolve conflict, và tự động replay resolution đó khi gặp cùng conflict.
Enable:
git config --global rerere.enabled trueKhi nào cứu bạn:
Scenario 1 - Long rebase: feature branch 20 commits, rebase lên main, commit thứ 3 có conflict X. Bạn resolve. Commit thứ 15 có cùng conflict X → rerere tự resolve.
Scenario 2 - Release branch: merge release branch vào main mỗi tháng, cùng một config conflict xuất hiện mỗi lần → rerere nhớ, không cần resolve lại.
Scenario 3 - Topic branches: nhiều feature branches merge vào integration branch để test, cùng conflict xuất hiện nhiều lần qua ngày → rerere tái dùng resolution.
Cách hoạt động:
git rerere diff # xem resolutions đã ghi nhớ
git rerere forget # xóa 1 resolution nếu saiLưu ý: rerere chỉ hoạt động nếu conflict markers giống hệt nhau (same file, same context lines).
Nếu code xung quanh thay đổi, rerere không match được.
edit trong interactive rebase dừng tại 1 commit cụ thể, cho phép bạn amend nó trước khi tiếp tục rebase.
Những gì edit làm được mà squash không:
1. Tách 1 commit thành nhiều commits:
git rebase -i HEAD~3
# Đánh dấu commit cần tách là: edit abc123
# Git dừng tại abc123
git reset HEAD~ # unstage commit, giữ files
git add src/auth/
git commit -m "feat(auth): add login logic"
git add src/tests/
git commit -m "test(auth): add login tests"
git rebase --continue**2.
Chèn commit mới vào giữa history:**
# Dừng tại commit cần chèn trước
# Tạo file mới, commit
git add new-file.ts
git commit -m "feat: add missing helper"
git rebase --continue**3.
Thay đổi nội dung file trong commit cũ (không chỉ message):**
# Dừng tại commit cần sửa
# Edit files
git add changed-file.ts
git commit --amend --no-edit
git rebase --continueCẩn thận: edit mode tạo ra commits mới với hash khác → mọi commits phía sau cũng có hash mới → cần force push.
Tình huống: bạn có commits ABC trên branch, teammate force push → remote history khác, commits của bạn "mất" trên remote.
Recovery (nếu bạn chưa pull):
# Commits của bạn vẫn còn local
git log --oneline # thấy commits của bạn
# Remote đã overwrite, nhưng local chưa bị xóa
git push --force-with-lease origin feature/shared
# EXPECTED: sẽ fail vì remote có commits mới (của teammate) — đây là safety behavior đúng đắn
# Không phải lỗi của recovery process mà là --force-with-lease bảo vệ bạn khỏi overwriteĐúng quy trình (sau khi --force-with-lease fail):
git fetch origin
git rebase origin/feature/shared # replay your commits on top of teammate's
# Resolve conflicts nếu có
git push origin feature/sharedNếu bạn đã pull (local bị overwrite):
git reflog # tìm commit trước khi pull
# Ví dụ: abc123 HEAD@{3}: commit: feat: my work
git cherry-pick abc123 # recover commitNếu teammate force push tới main: không recover bằng force push ngược lại (sẽ mất work của teammate). Coordinate với team, dùng git cherry-pick để apply lại commits bị mất lên main mới.
Phòng tránh: enable --force-with-lease và branch protection cho shared branches.
git bisect hoạt động như thế nào? Scenario thực tế dùng nó tìm commit gây regression?git bisect dùng binary search để tìm commit đầu tiên gây ra bug — thay vì manual check từng commit.
Workflow:
git bisect start
git bisect bad # current HEAD là bad
git bisect good v1.3.0 # version này còn good
# Git checkout commit ở giữa good và bad
# Test: chạy test hoặc reproduce bug thủ công
git bisect good # commit này không có bug
# Git checkout commit tiếp theo
git bisect bad # commit này có bug
# ... tiếp tục ~7-8 lần cho 100 commits
# Git output: "abc123 is the first bad commit"
git bisect reset # về HEAD ban đầuTự động hóa với script:
git bisect start HEAD v1.3.0
git bisect run npm test -- --testNamePattern="payment calculation"
# Git tự chạy test và tự đánh good/badVí dụ thực tế: release mới bị báo cáo "số tiền hiển thị sai". git log --oneline thấy 87 commits từ lần release cuối. Bisect tự động với test case → tìm ra commit trong 7 bước thay vì check 87 commits.
Lưu ý: script phải return exit code 0 (good) hoặc non-zero (bad). Dùng git bisect skip khi commit không thể test (build broken).
git revert hay deploy từ previous tag?Tình huống khẩn cấp — ưu tiên speed và safety:
Option 1: Deploy từ previous tag (nhanh nhất, an toàn nhất):
git checkout v2.3.1 # version trước khi deploy — sẽ ở detached HEAD state
# Tạo branch tạm hoặc trigger CI/CD từ tag trực tiếp
# Rebuild và deploy
# Git history không thay đổi gìPhù hợp khi: CI/CD pipeline deploy từ tag, có artifact của version trước sẵn sàng. Lưu ý: git checkout <tag> đặt repo vào detached HEAD — tốt nhất là trigger CI/CD pipeline từ tag thay vì build thủ công.
Option 2: git revert (nếu không có artifact):
git revert -m 1 <merge-commit-hash> # revert PR merge
# hoặc revert từng commit:
git revert HEAD~3..HEAD
git push origin main
# Trigger deploySo sánh:
- Tag redeploy: nhanh hơn, không thay đổi git history, artifact cũ có thể đã có sẵn
- Revert: tạo rõ ràng record "chúng ta đã revert", dễ re-apply sau khi fix
Sau emergency:
# Fix bug trên feature branch
# Revert the revert (nếu dùng revert):
git revert <revert-commit> # re-enables the feature
# Test kỹ, deploy lạiQuan trọng: không xóa revert commit hay dùng reset trên main — cần audit trail.
Post-mortem sau khi ổn định: tại sao bug qua được CI?
Đây là tình huống nghiêm trọng. Hành động ngay:
Bước 1: Không làm gì thêm trên main, thông báo team stop pulling.
Bước 2: Tìm commit cuối cùng đúng trên GitHub:
- Vào GitHub → repo → Commits history → tìm commit trước force push
- Hoặc: GitHub lưu reflog nội bộ, Contact GitHub Support nếu critical
Bước 3: Tìm trên local của bất kỳ teammate nào chưa pull:
# Trên máy teammate chưa pull (họ vẫn có local history cũ):
git log --oneline # KHÔNG có origin/ — local log thấy history cũ
# git log --oneline origin/main sẽ thấy history MỚI (đã bị overwrite)
# Nhờ teammate dùng local commit hash của họ để restore:
git push --force-with-lease origin <good-hash>:main # dùng force-with-lease an toàn hơnBước 4: Nếu có backup tag:
git checkout v2.3.0 # tag trước force push
git checkout -b main-recovery
git push --force origin main-recovery
# Tạo PR vào mainBước 5: Nếu không ai có — reflog local:
git reflog # trên máy bạn
# Tìm hash trước khi force push
git push --force origin <old-hash>:mainPhòng tránh: branch protection với Include administrators, không bao giờ dùng --force trên main.
File lớn trong git history không thể xóa bằng git rm đơn giản — vẫn tồn tại trong object store.
Identify large files:
git rev-list --objects --all | sort -k 2 > allfiles.txt
git gc && git verify-pack -v .git/objects/pack/*.idx | sort -k 3 -n | tail -10Remove với git filter-repo (khuyến nghị):
pip install git-filter-repo
git filter-repo --path large-video.mp4 --invert-paths
git filter-repo --strip-blobs-bigger-than 10M # xóa tất cả blobs >10MBRemove với BFG (nhanh hơn cho history dài):
java -jar bfg.jar --strip-blobs-bigger-than 50M
git reflog expire --expire=now --all
git gc --prune=nowSau khi xóa — coordinate với team:
git push --force --all
git push --force --tags
# Thông báo team:
# "Repo history rewritten — please re-clone: git clone <url>"
# Đừng pull — pull sẽ không clean đượcPhòng tránh lần sau:
- Git LFS cho binary assets (design files, videos, binaries)
- Custom pre-commit hook để check file size (ví dụ: find . -size +10M | grep -v .git và exit 1 nếu tìm thấy) — git config hooks.maxfilesize KHÔNG phải git config thực; phải viết script thủ công
- .gitignore cho build outputs, node_modules, log files.
git worktree là gì? Khi nào nó tốt hơn stash hoặc tạo clone thứ 2?git worktree cho phép checkout nhiều branches vào các thư mục riêng biệt, cùng chia sẻ 1 git object store — không cần clone lần 2.
Khi stash không đủ:
- Đang code feature phức tạp → urgent hotfix cần làm ngay → stash feature, switch, code hotfix, unstash → context switching overhead cao.
- Clone lần 2: tốn disk, download lại tất cả objects.
Worktree solution:
# Thêm worktree cho hotfix trong thư mục riêng:
git worktree add ../my-repo-hotfix hotfix/critical-bug
# hoặc tạo branch mới:
git worktree add -b hotfix/urgent ../my-repo-hotfix main
# Làm việc trong thư mục hotfix:
cd ../my-repo-hotfix
# Code, test, commit — hoàn toàn độc lập
# Xóa worktree sau khi xong:
git worktree remove ../my-repo-hotfixƯu điểm:
- Chia sẻ git objects → không tốn disk thêm
- Không phải stash/unstash
- Build artifacts của mỗi worktree độc lập (2 terminal chạy npm run dev đồng thời)
Hạn chế: 1 branch chỉ có thể được checkout trong 1 worktree tại 1 thời điểm.
Ideal use case: monorepo, cần chạy 2 versions đồng thời để compare behavior.
Full clone của large monorepo (Facebook-scale) có thể mất 30+ phút. CI/CD cần tối ưu.
Shallow clone — chỉ clone N commits gần nhất:
git clone --depth 1 https://github.com/org/repo
# Chỉ lấy latest commit, không có history
# Tốt cho: CI build, Docker build layer
# Trong GitHub Actions:
- uses: actions/checkout@v4
with:
fetch-depth: 1 # shallow
# fetch-depth: 0 = full history (cần cho semantic-release)Partial clone — clone repo nhưng defer downloading large blobs:
# Blobless clone: download trees nhưng defer blobs — phù hợp nhất cho CI
git clone --filter=blob:none https://github.com/org/repo
# Blobs tải về khi bạn thực sự checkout file đó (không phải "chỉ metadata")
# Treeless clone (filter=tree:0): không tải trees — checkout gần như không dùng được
# Chỉ dùng cho special cases (shallow inspection), không phải CI thông thường
git clone --filter=tree:0 https://github.com/org/repoSparse checkout — chỉ checkout 1 subdirectory của monorepo:
git clone --no-checkout https://github.com/org/monorepo
cd monorepo
git sparse-checkout init --cone
git sparse-checkout set apps/my-app packages/shared
git checkout mainCI/CD best practice: --depth 1 + --filter=blob:none + sparse checkout = clone time giảm 90% cho large monorepos.
Lưu ý: shallow clone làm git bisect và git log --all không hoạt động đầy đủ.
Git Submodules: link đến commit cụ thể của repo khác, lưu trong .gitmodules.
git submodule add https://github.com/org/shared-lib libs/shared
# .gitmodules lưu URL + path
# Clone cần: git clone --recursive hoặc git submodule update --initVấn đề thực tế của submodules:
- git clone không tự clone submodules — newbies hay quên --recursive
- Update submodule: phải cd vào, pull, cd ra, commit .gitmodules update
- Detached HEAD state thường xuyên trong submodule directory
- CI/CD phức tạp hơn
- "Why does the build fail?" → submodule pointer pointing to deleted commit
Git Subtree: copy code vào subdirectory, không có reference to external repo.
git subtree add --prefix libs/shared https://github.com/org/shared-lib main --squash
git subtree pull --prefix libs/shared https://github.com/org/shared-lib main --squashVấn đề subtree: merge history phức tạp, git log prefix cần, push changes ngược lại upstream khó.
Tại sao hầu hết teams tránh cả hai:
Như là monorepo tool: npm workspaces, Nx, Turborepo tốt hơn nhiều. Như là dependency: publish package lên npm/private registry. Như là vendor code: copy + commit (vendor) đơn giản và transparent hơn.
Git LFS là giải pháp cho binary assets — tốt hơn submodules cho design files, videos.
Git LFS (Large File Storage): thay vì lưu binary files trực tiếp trong git objects (làm repo phình to), LFS lưu pointer nhỏ trong git, còn actual file lưu trên LFS server riêng.
Setup:
# Install:
brew install git-lfs # macOS
git lfs install
# Track file patterns:
git lfs track "*.psd"
git lfs track "*.figma"
git lfs track "*.mp4"
git lfs track "*.zip"
git lfs track "dist/**" # build artifacts
# Commit .gitattributes:
git add .gitattributes
git commit -m "chore: configure Git LFS tracking"
# Push (LFS files upload automatically):
git push origin mainKiểm tra files đang track:
git lfs ls-files # files đang trong LFS
git lfs status # pending uploads
git lfs migrate info # phân tích files lớn chưa migrateMigrate history cũ vào LFS:
git lfs migrate import --include="*.psd" --everything
git push --force --all # cần force push vì history rewriteCân nhắc:
- GitHub: 1GB LFS storage miễn phí, sau đó tính phí bandwidth
- CI/CD: cần cấu hình để pull LFS files (GIT_LFS_SKIP_SMUDGE=1 git clone <url> để skip LFS download hoàn toàn)
- Clone: git lfs install --skip-smudge cũng có thể dùng — --no-local KHÔNG liên quan đến LFS
Alternatives: Cloudinary/S3 cho media assets (không cần trong git), artifact registry cho build outputs.
semantic-release phân tích commit messages từ merged PRs → tự động tính version bump → publish npm package/create GitHub Release → generate CHANGELOG.
Setup:
npm install --save-dev semantic-release @semantic-release/changelog @semantic-release/git.releaserc.json:
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
["@semantic-release/changelog", { "changelogFile": "CHANGELOG.md" }],
"@semantic-release/npm",
["@semantic-release/git", { "assets": ["CHANGELOG.md", "package.json"] }],
"@semantic-release/github"
]
}Version bump rules (tự động):
- fix: → patch (1.2.3 → 1.2.4)
- feat: → minor (1.2.3 → 1.3.0)
- feat!: hoặc BREAKING CHANGE: → major (1.2.3 → 2.0.0)
- chore:, docs:, style: → không bump
GitHub Actions workflow:
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-releaseLợi ích: zero human decision về version number, CHANGELOG luôn up-to-date, consistent release process.
Nhược điểm: yêu cầu mọi developer follow Conventional Commits nghiêm túc.
Client-side hooks (Husky): chạy trên máy developer.
- pre-commit: lint, format, type-check
- commit-msg: commitlint
- pre-push: unit tests
- Có thể bypass: git commit --no-verify, git push --no-verify
- Share được qua git (Husky v7+ lưu hook files trong .husky/ — committed vào repo, cài tự động qua npm prepare; .git/hooks/ vẫn là client-side và vẫn bypassable)
Server-side hooks (GitHub Actions/CI): chạy trên server, developer không thể bypass.
# .github/workflows/ci.yml
on: [pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm run test
- run: npx commitlint --from ${{ github.event.pull_request.base.sha }}Phân chia trách nhiệm:
| Task | Client Hook | Server Hook |
|---|---|---|
| Fast lint/format | pre-commit ✓ | không cần |
| Type check | pre-commit ✓ | CI backup ✓ |
| Unit tests | pre-push ✓ | CI ✓ |
| Integration tests | không | CI ✓ |
| Commit message | commit-msg ✓ | CI ✓ |
| Security scan | không | CI ✓ |
Nguyên tắc: client hooks = fast feedback loop cho developer, server hooks = enforcement không thể bypass. Không dùng client hooks thay thế server hooks — chỉ dùng bổ sung.