Luyện Phỏng Vấn IT — 2000+ Câu Hỏi Phỏng Vấn IT Có Đáp Án 2026

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 main vớ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-login giúp trace thay đổi về requirement.

Git Flow hotfix:

bash
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-bug

GitHub Flow hotfix (đơn giản hơn):

bash
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 ngay

Quan 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:

bash
# 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):

bash
git commit --amend -m "corrected message"
git push --force-with-lease origin feature/my-branch

Lư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ả maindevelop.

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:

  1. ít overhead, không cần ceremony
  2. deploy liên tục không cần release branch
  3. hotfix đơn giản — branch từ main, fix, PR, merge, deploy
  4. 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-team

PR 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:

bash
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 $1

Lợ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:

bash
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 merge

Trong 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 cleanup

Thay đổ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:

bash
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 keys

SSH signing (mới hơn, dễ hơn GPG):

bash
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true

GitHub 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:

bash
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.

bash
git checkout main
git merge feature/login  # nếu main chưa có commit mới → fast-forward
# History: A - B - C(feature) ← HEAD/main

No-ff merge (--no-ff): luôn tạo merge commit dù có thể fast-forward.

bash
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:

bash
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):**

bash
git mergetool  # opens configured tool
# Left: ours, Right: theirs, Center: base (common ancestor)
# Bottom: result

**3.

Resolve và verify:**

bash
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:**

bash
git diff main~1..main -- src/file.ts  # xem diff của merge commit

Nguyê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.

bash
# 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).

bash
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 audit

git log -G "regex": tìm commits mà diff chứa regex pattern (line added/removed matching pattern).

bash
git log -G "discountRate\s*=\s*[0-9]+" --oneline
# Tìm mọi lần giá trị discountRate bị thay đổi

Khá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:

bash
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ối

Kết hợp với blame:

bash
git blame src/pricing.ts -L 45,60  # ai viết dòng 45-60
# Sau đó git show <commit> để xem full context

Branch 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:

bash
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 abc123

Nếu không nhớ tên branch:

bash
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):

bash
git fetch origin  # nếu remote còn lưu
# Hoặc hỏi teammate — họ có thể có local copy

Nếu đã git gc chạy:

bash
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 được

Phò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 <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ạ:

bash
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:**

bash
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 files

3. 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:

bash
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:

bash
git log -S "password=" --all -p

Lưu ý: blame thường trỏ về commit "refactor" hay "format code" — dùng git log --follow-w để skip qua những thay đổi không có ý nghĩa, tìm commit thực sự thay đổi logic.

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.

bash
# 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-branch

Enforce trong team:

bash
# .gitconfig alias:
git config --global alias.pushf "push --force-with-lease"
# Sau đó dùng: git pushf

Hoặc dùng git alias để enforce convention:

bash
# .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/branch

Lư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:

bash
# 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 push

Rule đơ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:

bash
# 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 --all

Bướ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:

bash
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.

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ế:

bash
# 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 <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:

bash
git checkout release/2.1
git cherry-pick abc123  # apply hotfix commit từ main
git checkout release/2.0
git cherry-pick abc123

2. Lấy 1 commit cụ thể từ colleague's branch mà bạn cần ngay:

bash
git cherry-pick def456  # chỉ cần 1 commit, không cần cả branch

3. Recover commit từ deleted branch:

bash
git cherry-pick abc123  # commit vẫn tồn tại dù branch bị xóa

Khi 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 ý:

bash
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.

bash
# git log trông giống:
# * Merge branch 'main' of ... ← không có ý nghĩa
# |\ 
# | * teammate: feat: add cart
# * | your: feat: add login
# |/
# * old commit

Fix: dùng git pull --rebase:

bash
git pull --rebase origin main
# = git fetch + git rebase origin/main
# Kết quả: history linear, commits của bạn replay trên top

Set làm default:

bash
git config --global pull.rebase true
# Hoặc per-repo:
git config pull.rebase true

Trường hợp pull --rebase gặp conflict:

bash
# 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 pull

Khi 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:

bash
npm install --save-dev husky lint-staged
npx husky init

package.json:

json
{
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,scss}": ["prettier --write"],
    "*.{json,md}": ["prettier --write"]
  }
}

.husky/pre-commit:

bash
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

Kế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:

bash
npm install --save-dev @commitlint/cli @commitlint/config-conventional

commitlint.config.js:

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:

bash
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit $1

Cho 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):

bash
git commit --no-verify -m "emergency fix"
# --no-verify bỏ qua TẤT CẢ client-side hooks

Lư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:

bash
# .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
fi

Hoặc chạy fast subset:

bash
npm run test:unit  # unit tests only (~30s), không integration tests

Pattern 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:

bash
git push --no-verify  # skip pre-push hook