Bạn biết cái đó rồi đấy. Nó xuất hiện trong queue review của bạn vào một chiều thứ Ba: một pull request tiêu đề "fixes", diff trải dài 2.347 dòng qua 41 file, không có mô tả, và một "PTAL" vui vẻ trong comment. Tim bạn chìm xuống. Bạn vẫn mở nó ra. Hai mươi phút sau bạn đã đọc cùng một method ba lần, vẫn không biết nó giải quyết vấn đề gì, và bạn đã để lại một comment lo lắng duy nhất: "nhìn chung ổn, có thể đổi tên biến này không?" Bạn vừa rubber-stamp code mà bạn không hiểu.
Tác giả không ác ý — có lẽ họ đang chú tâm làm việc, tự hào về công việc, và chỉ quên rằng ship code chỉ là một nửa công việc. Nửa kia là được review, được hiểu, và được merge an toàn. Một pull request không phải là thủ tục hành chính bạn dọn sạch trước khi merge; đó là một cuộc trò chuyện, và như mọi cuộc trò chuyện, nó diễn ra tốt hơn khi ai đó bỏ công sức để được hiểu.
Bài này nói về công sức đó: cách viết PR mà đồng đội thực sự thích review — nhỏ gọn, có mục đích, dễ theo dõi, và dễ nói có.
PR là giao tiếp, không chỉ là code
Trước một dòng diff, một pull request là một thông điệp đến một người khác. Người đó có thời gian hạn chế, ngữ cảnh hạn chế về những gì bạn đang nghĩ lúc 11 giờ đêm, và hàng chục thứ khác cạnh tranh sự chú ý của họ. Công việc của bạn với tư cách tác giả là làm công việc của họ dễ dàng.
Hãy nghĩ về reviewer như khán giả của bạn, không phải quan toà của bạn. Họ không ở đó để bắt bạn — họ là người cộng tác giúp bạn ship thứ gì đó vững chắc. Bạn giúp họ hiểu lý do đằng sau thay đổi càng nhiều, review sẽ càng nhanh và tốt hơn. Một mô tả PR xuất sắc như một thư xin việc cho code của bạn: nó đặt bối cảnh, giải thích động lực, và cho reviewer biết chính xác nên nhìn vào đâu.
Sự thay đổi tư duy này thay đổi tất cả. Một khi bạn ngừng viết PR cho chính mình và bắt đầu viết cho reviewer, bạn sẽ tự nhiên muốn làm chúng nhỏ hơn, mô tả tốt hơn, và dọn sạch trước khi đề nghị ai nhìn vào.
Nếu reviewer phải đọc code để hiểu tại sao thay đổi tồn tại, PR description chưa làm xong việc của nó. Code giải thích cái gì đã thay đổi; description giải thích tại sao nó cần thay đổi.
Giữ nhỏ và tập trung
Điều hiệu quả nhất bạn có thể làm để cải thiện pull request là làm chúng nhỏ hơn. PR càng lớn, review càng kém kỹ lưỡng, mất nhiều thời gian hơn để merge, và nhiều lỗi hơn lọt qua.
Một PR tốt chứa một thay đổi logic. Không phải một ticket, không phải một tính năng, không phải một sprint công việc — một ý tưởng mạch lạc có thể được hiểu, test, và revert độc lập. "Thêm avatar upload cho người dùng" là một thay đổi. "Thêm avatar upload và refactor profile controller và bump ba dependency" là ba thay đổi mặc cùng một chiếc áo.
Cách tách công việc lớn
Tính năng lớn không phải đến dưới dạng PR lớn. Một số chiến lược tách thực tế:
- Tách refactoring khỏi thay đổi hành vi. PR đầu tiên: đổi tên, tái cấu trúc, extract — không có logic mới. PR thứ hai: tính năng thực sự. Reviewer có thể xác minh refactor không có side effect, rồi tập trung hoàn toàn vào logic mới.
- Ship infrastructure trước. Thêm cột database, migration, type — tất cả không có UI. Rồi thêm UI sử dụng nó. Mỗi PR độc lập có thể deploy.
- Dùng feature flag. Merge công việc chưa hoàn chỉnh đằng sau flag tắt mặc định. PR nhỏ và an toàn; tính năng không "xong" cho đến khi bạn bật flag.
- Stacked PR. PR B phụ thuộc PR A. Một số nhóm dùng công cụ stacked-PR để review từng lớp độc lập. Nếu nhóm bạn không dùng stacking, hãy giữ đơn giản: hoàn thành và merge A trước, rồi mở B.
Nếu bạn không thể mô tả PR trong một câu mà không dùng từ "và", nó có thể chứa nhiều hơn một thay đổi logic. Tìm đường nối và tách ra.
Viết tiêu đề và mô tả đáng đọc
Tiêu đề là tít bài. Nó nên nói cái gì đã thay đổi và tại sao quan trọng, trong một dòng. Tránh các động từ mơ hồ như "update", "fix stuff", hay "changes" và hướng đến sự cụ thể. Đây là các mùi của tiêu đề PR phổ biến và cách sửa:
| Mùi PR | Vấn đề là gì | Phiên bản tốt hơn |
|---|---|---|
| "fixes" | Không có ngữ cảnh gì — reviewer phải đọc từng dòng để đoán chuyện gì đang xảy ra. | "Sửa race condition trong cart checkout khi concurrent session va chạm" |
| "WIP: various changes" | Báo hiệu tác giả chưa sẵn sàng. Draft PR tồn tại có lý do. | Mở như GitHub Draft cho đến khi thực sự sẵn sàng review. |
| "Update UserService" | Mô tả nơi, không phải cái gì hay tại sao. Mọi PR đều update thứ gì đó. | "Thêm luồng xác minh đổi email vào UserService" |
| "JIRA-1234" | Bắt reviewer mở tab thứ hai chỉ để hiểu động lực. | "Giới hạn API rate 100 req/min mỗi IP (JIRA-1234)" — ticket là ngữ cảnh, không phải thay thế. |
| "Refactor + new feature + bump deps" | Ba PR trong một cái áo khoác. Mỗi mối quan tâm xứng đáng có review riêng. | Tách thành ba PR riêng biệt, tập trung. |
Mô tả là nơi giao tiếp thật sự xảy ra. Một mô tả tốt trả lời ba câu hỏi mỗi reviewer có trong mười giây đầu tiên:
- Cái gì đã thay đổi và ở đâu?
- Tại sao thay đổi này cần thiết — ngữ cảnh, bug, user story?
- Làm thế nào để verify nó hoạt động — các bước test, screenshot, edge case bị ảnh hưởng?
Đây là trước/sau để làm rõ:
--- TRƯỚC (PR "fixes" 2.000 dòng) ---
Title: fixes
Description: (trống)
--- SAU (cùng thay đổi, viết cho reviewer) ---
Title: Sửa race condition trong cart checkout trên concurrent session
## What
Hai concurrent checkout request từ cùng một người dùng đều có thể pass
kiểm tra inventory trước khi cái nào giảm stock, dẫn đến overselling.
PR này thêm row-level DB lock quanh việc giảm stock.
## Why
Customer support đã ghi nhận 12 sự cố oversell trong 30 ngày qua (xem
Sentry issue #4421). Root cause là hai request đang đua giữa SELECT và
UPDATE trên bảng inventory.
## How to test
1. Chạy regression test concurrency mới: npm test -- cart.concurrency
2. Trên staging: mở hai tab trình duyệt, nhấn "Mua" đồng thời trên
sản phẩm có stock = 1. Chỉ một cái nên thành công.
## Risk / notes
- Lock được scope cho một row product; không ảnh hưởng throughput
khi các product khác nhau checkout cùng lúc.
- Không cần migration — dùng SELECT FOR UPDATE trên bảng hiện có.
Vệ sinh commit
Cách bạn tổ chức commit quan trọng — không chỉ để đáp ứng quy trình, mà vì commit là bản ghi vĩnh viễn về cách thay đổi ra đời. Một năm sau, ai đó (có thể là bạn) sẽ chạy git blame trên một dòng và theo commit hash vào lịch sử. Những gì họ tìm thấy nên kể một câu chuyện, không phải đọc như một dòng suy nghĩ hỗn loạn.
Một commit message tốt theo định dạng conventional: một dòng subject ngắn dạng imperative (tối đa 50 ký tự), một dòng trống tuỳ chọn, và một body tuỳ chọn giải thích tại sao — không phải những gì diff đã cho thấy.
# Định dạng conventional:
type(scope): short imperative subject
Body tuỳ chọn: tại sao thay đổi này được thực hiện, vấn đề nó giải quyết,
bất kỳ quyết định không rõ ràng nào và lý luận đằng sau.
# Ví dụ thực tế:
fix(cart): add row-level lock to prevent oversell on concurrent checkout
feat(auth): add email-change verification flow
refactor(profile): extract avatar upload to dedicated service
Ngoài định dạng message, hãy suy nghĩ về hình dạng commit. Mỗi commit nên đại diện cho một bước mạch lạc — một refactor, một test, một hành vi mới. Reviewer nhìn vào từng commit (thay vì toàn bộ diff) nên có thể theo dõi tiến trình logic: "extract method → add test → implement behaviour" kể câu chuyện tốt hơn hai mươi commit "WIP" được squash vào phút chót.
Dùng interactive rebase để dọn sạch branch lộn xộn trước khi mở PR. Bạn không cần commit hoàn hảo từ đầu — đánh bóng trước khi đề nghị ai nhìn vào.
Self-review trước khi đề nghị review
Trước khi ping đồng đội, hãy trở thành reviewer đầu tiên của chính bạn. Mở diff trên GitHub (hoặc code host của bạn) và đọc nó như thể bạn chưa bao giờ thấy code. Sự chuyển đổi ngữ cảnh này bắt được nhiều vấn đề hơn bạn nghĩ: debug output còn sót, một function đã lớn quá, một import bạn quên remove, một comment đã từng đúng.
Checklist self-review đáng chạy qua:
- Đọc diff từ đầu đến cuối. Đừng lướt qua. Giả vờ bạn đang thấy nó lần đầu.
- Kiểm tra debug còn sót.
console.log, comment TODO đánh dấu "remove", các block bị comment out, giá trị hardcode tạm thời. - Xác minh test có mặt. Nếu bạn thêm logic, có test không? Nếu bạn sửa bug, có regression test không?
- Tìm thay đổi không có ý định. Whitespace churn, file được reformat, scope creep vô tình. Chuyển chúng sang PR riêng hoặc revert chúng.
- Để lại comment hướng dẫn cho chính mình. Nếu một phần không rõ ràng, thêm PR comment giải thích lý luận trước khi reviewer thấy nó. Đây không phải dấu hiệu yếu đuối — đó là sự chu đáo.
Nếu bạn không thoải mái với một senior engineer review PR ngay bây giờ — không có giải thích của bạn — nó chưa sẵn sàng gửi ra. Sửa trước, rồi đề nghị review.
Self-review cũng mang lại lợi tức ích kỷ: nó bắt những lỗi đáng xấu hổ trước khi chúng trở thành comment review vĩnh viễn. Ai cũng có câu chuyện về typo họ phát hiện mười giây sau khi nhấn "Request review". Mười phút self-review loại bỏ hầu hết những cái đó.
Trả lời review
PR không xong khi bạn push code — nó xong khi merge. Giữa hai sự kiện đó là chu kỳ review, và cách bạn xử lý phản hồi quan trọng không kém bản thân code.
Một số nguyên tắc giữ review tiến lên và mối quan hệ lành mạnh:
- Trả lời mọi comment. Dù chỉ để nói "done" hay "bắt tốt, đã sửa trong commit mới nhất". Im lặng khiến reviewer tự hỏi liệu họ có xem note của mình không.
- Mô tả sửa chữa khi bạn push. "Đã sửa trong commit mới nhất — chuyển logic vào helper và thêm test" rõ ràng hơn nhiều so với một commit mới không có ngữ cảnh.
- Resolve thread một cách chu đáo. Trong hầu hết nhóm, người comment resolve khi họ hài lòng. Đừng self-resolve mà không thừa nhận phản hồi. Nếu không chắc về quy ước, hãy hỏi.
- Bất đồng một cách tôn trọng. Nếu bạn nghĩ một đề xuất sai, hãy nói rõ ràng với lý lẽ — đừng chỉ bỏ qua nó. "Tôi đã cân nhắc cách tiếp cận đó, nhưng [lý do] — sẵn sàng thảo luận nếu bạn thấy quan trọng" giữ cuộc trò chuyện chuyên nghiệp và hiệu quả. Xem thêm: cách review code nhã nhặn cho quan điểm của reviewer về cùng cuộc trò chuyện.
- Re-request review một cách rõ ràng. Sau khi xử lý phản hồi, đừng chờ reviewer tự nhận ra. Re-request review và để lại tóm tắt ngắn: "Đã xử lý tất cả comment. Thay đổi chính: extract util, thêm hai test, giữ cách tiếp cận ban đầu cho X vì Y."
Chuẩn mực PR mở rộng theo quy mô nhóm như thế nào
Điều gì tạo ra PR xuất sắc không cố định — nó thay đổi khi nhóm lớn lên và codebase trưởng thành. Đây là cách kỳ vọng phát triển:
| Ngữ cảnh nhóm | Điều quan trọng nhất | Chuẩn mực thực tế |
|---|---|---|
| Solo / freelance | Bạn-tương-lai là reviewer. PR là tài liệu về quyết định cho khi bạn quay lại code sau sáu tháng. | Kể cả không có reviewer: tiêu đề rõ ràng, "tại sao" ngắn gọn trong description. Commit message làm git log có thể đọc được sau này. |
| Nhóm nhỏ (2–8) | Tốc độ quan trọng. Mọi người biết codebase. Review là đàm thoại. Tin tưởng cao. | Giữ PR nhỏ và nhanh chóng. Description có thể nhẹ hơn — một hai câu ngữ cảnh. Review async với turnaround trong ngày là kỳ vọng hợp lý. |
| Nhóm cỡ trung (10–50) | Reviewer có thể không biết đầy đủ ngữ cảnh. Onboarding là liên tục. Thay đổi trải rộng các nhóm. | Mô tả what/why/how đầy đủ. Screenshot cho công việc UI. Test plan. Thay đổi xuyên nhóm cần thêm chú ý và có thể review rộng hơn. |
| Lớn / enterprise (50+) | Reviewer đôi khi là người lạ. Tuân thủ, audit trail, và review tính đúng đắn quan trọng. SLA turnaround tồn tại. | Template PR chính thức được thực thi bởi tooling. Required approval. Security review cho thay đổi nhạy cảm. Link đến design doc và ticket. Rollback plan cho thay đổi rủi ro. |
Một nhóm startup bốn người không cần rollback plan trong mọi PR description. Một ngân hàng thì có. Hãy khớp nghi lễ với mức độ quan trọng, và xem lại chuẩn mực khi nhóm lớn lên — những gì hiệu quả ở mười kỹ sư sẽ cảm thấy không đủ ở năm mươi.
Những điều cốt lõi cần nhớ
- PR là thông điệp đến người. Viết cho reviewer, không phải cho chính bạn. Thời gian và ngữ cảnh của họ hạn chế.
- Giữ nhỏ và tập trung. Một thay đổi logic mỗi PR. Tách refactoring khỏi thay đổi hành vi. Dùng feature flag để merge công việc chưa hoàn chỉnh an toàn đằng sau toggle.
- Tiêu đề + mô tả = định hướng. Trả lời cái gì đã thay đổi, tại sao cần thay đổi, và làm thế nào để verify nó. Code cho thấy cái gì; description cung cấp tại sao.
- Lịch sử commit sạch kể một câu chuyện. Message conventional, commit logic, rebase gọn gàng trước khi đề nghị review.
- Self-review trước. Đọc diff của chính mình trên GitHub trước khi ai khác làm. Để lại comment hướng dẫn. Bắt lỗi hiển nhiên cho chính mình.
- Đóng vòng lặp về phản hồi. Trả lời mọi comment, re-request review sau khi xử lý note, và bất đồng với lý lẽ — không bao giờ bằng im lặng.
- Khớp nghi lễ với quy mô nhóm. Startup bốn người và ngân hàng 500 người cần chuẩn mực PR khác nhau. Xem lại của bạn khi lớn lên.
Bước tiếp theo là phía bên kia của bàn: cách người nhận PR của bạn nên đưa ra phản hồi giúp ích chứ không gây đau. Nếu bạn từng viết một PR được tạo ra cẩn thận chỉ để nhận lại "cái này sai, viết lại đi", bạn sẽ đánh giá cao Phản hồi Đánh Đúng Chỗ — bài tiếp theo trong loạt này.