# Ruff: lint và format repo Python của khách mà không tạo ra một PR nghìn dòng

> Ở repo của khách, công cụ chạy nhanh vẫn chưa đủ: bạn còn phải giữ diff nhỏ để code review không biến thành cuộc tranh cãi.

Bản gốc: https://fdetimes.net/vi/cong-cu/ruff-lint-format-nhanh-trong-repo-python-cua-khach/

Tuần đầu ở chỗ khách, bạn mở repo Python và thấy cùng lúc ba thứ: một file cấu hình Flake8, một hook isort và một dòng Black trong CI mà chẳng ai còn chắc nó có chạy hay không. Bạn muốn dọn lại, nhưng chỉ cần một PR "chuẩn hóa style" làm đổi hàng nghìn dòng là team của khách sẽ nghi ngờ bạn ngay.

Ruff hợp với tình huống này vì formatter của nó được thiết kế để thay Black và cho ra kết quả gần như giống hệt trên code đã format bằng Black.

Charlie Marsh của Astral cho biết trên các dự án như vậy, Ruff đạt độ tương thích hơn 99,9% tính theo số dòng thay đổi. Nghĩa là bạn có thể thay công cụ mà gần như không làm đổi code.

Với FDE, điều này quan trọng hơn tốc độ. Một PR vài chục dòng thì khách đọc được và duyệt được. Một PR vài nghìn dòng thì khách chỉ có thể tin hoặc không tin.

## Một binary thay cho cả chồng công cụ

Ruff là linter kiêm formatter, viết bằng Rust, phát hành theo giấy phép MIT. Đứng sau nó là Astral, nhóm cũng làm ra uv và ty. Tài liệu chính thức nói Ruff có thể thay Flake8 cùng hàng chục plugin, Black, isort, pydocstyle, pyupgrade, autoflake và một số công cụ khác.

Cũng theo tài liệu, Ruff nhanh hơn từ 10 đến 100 lần so với các linter và formatter hiện có. Nó còn có cache sẵn, nên những file không đổi sẽ không bị phân tích lại. Riêng phần formatter, Astral công bố tốc độ hơn 30 lần so với Black.

Ở repo khách, tốc độ này cho bạn chạy lint toàn repo trong lúc ngồi cùng team của họ, không cần đợi CI. Nhưng lợi ích lớn hơn là cả repo chỉ còn một chỗ cấu hình, thay vì mỗi công cụ một file riêng.

## Đo trước, rồi bốn commit nhỏ

Thử hình dung một repo nội bộ có thư mục `legacy/` không ai dám động vào và một thư mục `services/` đang được phát triển tích cực. Trình tự an toàn là đo trước khi đụng vào bất cứ thứ gì, rồi tách việc thành bốn commit, mỗi commit chỉ làm một việc.

Việc đo không tạo ra commit nào. `ruff check .` liệt kê vi phạm mà không sửa gì, vì Ruff chỉ sửa khi bạn truyền cờ `--fix`. Kết quả này là bản đồ nợ kỹ thuật, và bạn có thể mang nó vào buổi họp với khách.

Commit đầu tiên thêm cấu hình Ruff và đánh dấu nợ cũ thay vì sửa ngay. `ruff check . --add-noqa` tự chèn comment `noqa` vào mọi dòng đang vi phạm. Từ đó trở đi, chỉ code mới bị chặn. Với những thư mục cần nới lỏng lâu dài, hãy dùng `per-file-ignores` trong `pyproject.toml`:

```toml
[tool.ruff.lint.per-file-ignores]
"legacy/*" = ["E501"]
```

Commit thứ hai là `ruff format`, đứng riêng và không chứa thay đổi nào khác. Chỉ sau khi commit này tồn tại bạn mới có hash của nó, nên commit thứ ba là một commit tiếp theo, thêm file `.git-blame-ignore-revs` chứa hash đó để giao diện blame của GitHub bỏ qua commit format.

Như vậy người đọc lịch sử vẫn thấy ai viết dòng nào, chứ không phải ai bấm format.

**Điểm mấu chốt:** Commit format một lần, tách riêng, rồi giấu nó khỏi git blame.

Commit cuối là `ruff check --fix`. Mặc định Ruff chỉ áp dụng các bản sửa được xếp loại an toàn. Những bản sửa có thể làm đổi hành vi code phải bật thêm `--unsafe-fixes`. Ở repo khách, bạn nên để cờ này cho từng PR nhỏ có người review.

## Khi nào diff vẫn phình to?

Con số hơn 99,9% chỉ đúng với code đã format bằng Black. Tài liệu Ruff nói rõ formatter cố ý khác Black ở một vài điểm, chẳng hạn cách xử lý dòng trống đầu block, f-string và tuple một phần tử. Nếu repo của khách chưa từng dùng Black, lần format đầu tiên có thể tạo diff đáng kể.

Vì thế, câu đầu tiên nên hỏi khách là "CI có thật sự chạy Black không?", chứ không phải "dùng linter gì?". Nếu có, bạn có thể kỳ vọng diff nhỏ. Nếu không, hãy báo trước rằng có thể sẽ có một commit format lớn, và chính vì vậy cần `.git-blame-ignore-revs`.

## Bẫy cấu hình trong monorepo

Ruff hỗ trợ monorepo bằng cấu hình phân cấp, đọc từ `pyproject.toml`, `ruff.toml` hoặc `.ruff.toml`. Có một chi tiết dễ làm bạn mất cả buổi chiều. Với mỗi file, Ruff dùng file cấu hình gần nhất trong cây thư mục và không gộp cài đặt giữa các file.

Giả sử `services/billing/` có `ruff.toml` riêng chỉ để đổi độ dài dòng. Khi đó, mọi rule bạn đặt ở `pyproject.toml` gốc sẽ không áp dụng cho thư mục này nữa. Trước khi kết luận "Ruff bỏ sót lỗi", hãy kiểm tra xem cấu hình nào đang áp dụng cho file đó.

## Nên học gì trước

Hãy học trình tự đo rồi bốn commit trước khi học danh sách rule. Lý do là rủi ro ở repo khách nằm ở quy trình áp dụng, không nằm ở việc chọn rule nào. Sau đó, đọc trang liệt kê khác biệt giữa Ruff và Black, vì đó là nơi bạn cần câu trả lời khi khách hỏi vì sao một dòng bị đổi.

Về hồ sơ xin việc, một dòng như "di chuyển repo Python từ Flake8, isort và Black sang Ruff, giữ diff format dưới mức X dòng, không làm hỏng git blame" có giá trị hơn nhiều so với việc chỉ ghi "thành thạo Ruff".

Công cụ nhanh giúp bạn tiết kiệm vài phút. Một PR nhỏ mà khách đọc hiểu được mới là thứ giúp bạn có được lòng tin của họ.

**Thử ngay tuần này:**

- Clone một repo Python mã nguồn mở đang dùng Black, chạy ruff format trên một nhánh riêng và đếm số dòng thay đổi bằng git diff --stat.
- Trên repo đó, chạy ruff check --add-noqa, commit lại, sau đó thử viết code mới vi phạm rule để thấy chỉ code mới bị báo lỗi.
- Sau khi commit format, tạo một commit mới thêm file .git-blame-ignore-revs chứa hash của commit format, rồi mở git blame trên GitHub để kiểm tra kết quả.

## Nguồn

- [Ruff](https://docs.astral.sh/ruff/)

- [GitHub - astral-sh/ruff](https://github.com/astral-sh/ruff)

- [The Ruff Formatter | Ruff](https://docs.astral.sh/ruff/formatter/)

- [The Ruff Formatter](https://astral.sh/blog/the-ruff-formatter)

- [The Ruff Linter | Ruff](https://docs.astral.sh/ruff/linter/)

- [Configuring Ruff | Ruff](https://docs.astral.sh/ruff/configuration/)

- [Viewing and understanding files](https://docs.github.com/en/repositories/working-with-files/using-files/viewing-and-understanding-files#ignore-commits-in-the-blame-view)
