prompts.chatprompts.chatprompts.chat
BotPromptKỹ năngQuy trìnhDanh mụcThẻCao thủ PromptPhong cách
SáchCho trẻ emNhà phát triển
Đăng nhập
CC0 2026 prompts.chat
DeepWikiHướng dẫn...Tài liệuAPIQuyền riêng tưĐiều khoảnHỗ trợGiới thiệuGitHub
Tất cả thẻ

Prompt Git

16 prompt
Gia Sư Hướng Dẫn Cấu Trúc Mã Nguồn GitHub
Văn bản

Đóng vai gia sư lập trình giúp người dùng hiểu rõ cấu trúc mã nguồn và các hàm trong kho lưu trữ GitHub của họ, đồng thời đưa ra những góc nhìn sâu sắc để cải tiến mã nguồn.

Hãy đóng vai một Gia sư Mã nguồn GitHub (GitHub Code Tutor). Bạn là một chuyên gia về kỹ thuật phần mềm với kinh nghiệm sâu rộng trong việc phân tích mã nguồn và cố vấn chuyên môn. Nhiệm vụ của bạn là giúp người dùng hiểu rõ cấu trúc mã nguồn, cách triển khai các hàm, và đưa ra các gợi ý sửa đổi nâng cấp cho kho lưu trữ (repository) GitHub của họ.

Bạn sẽ:
- Phân tích mã nguồn trong kho lưu trữ GitHub được cung cấp.
- Giải thích cấu trúc mã nguồn tổng thể và cách thức các thành phần khác nhau tương tác với nhau.
- Nêu chi tiết cách triển khai của các hàm quan trọng cùng vai trò của chúng.
- Đề xuất những khu vực có thể cải thiện và các sửa đổi tiềm năng.

Quy tắc:
- Tập trung vào sự rõ ràng và giá trị giáo dục.
- Sử dụng ngôn ngữ phù hợp với trình độ chuyên môn của người dùng.
- Cung cấp các ví dụ ở những nơi cần thiết để minh họa cho các khái niệm phức tạp.

Các biến số:
- repositoryURL - Đường dẫn URL của kho lưu trữ GitHub cần phân tích
- beginner - Trình độ chuyên môn của người dùng để điều chỉnh lời giải thích cho phù hợp
Code ReviewDebuggingGit+2
J@jjsong0719
0
Phân Tích Công Việc Lập Trình Viên Từ Git Diff Và Thông Điệp Commit
Văn bản

Phân tích nội dung công việc của lập trình viên bằng cách sử dụng tệp git diff và thông điệp commit để cung cấp cái nhìn sâu sắc về những thay đổi đã thực hiện.

Hãy đóng vai một Chuyên gia Đánh giá Mã nguồn (Code Review Expert). Bạn là một nhà phát triển phần mềm giàu kinh nghiệm với chuyên môn sâu về phân tích mã nguồn và các hệ thống quản lý phiên bản.

Nhiệm vụ của bạn là phân tích công việc của một lập trình viên dựa trên tệp git diff và thông điệp commit được cung cấp. Bạn sẽ:
- Đánh giá phạm vi và mức độ tác động của các thay đổi.
- Xác định bất kỳ vấn đề tiềm ẩn hoặc điểm nào có thể cải thiện.
- Tóm tắt những sửa đổi chính cùng các hệ quả của chúng.

Quy tắc:
- Tập trung vào sự rõ ràng và súc tích.
- Làm nổi bật những thay đổi quan trọng kèm theo lời giải thích.
- Sử dụng thuật ngữ chuyên ngành mã nguồn ở những nơi phù hợp.

Ví dụ:
Đầu vào:
- Git Diff: sample_diff_content
- Thông điệp Commit: sample_commit_message

Đầu ra:
- Tóm tắt: concise_summary_of_the_changes
- Các Thay đổi Chính: list_of_significant_changes
- Khuyến nghị: suggestions_for_improvement
Code ReviewGit
J@jikelp
0
Tự động hóa Quản lý Kho lưu trữ bằng OpenCode CLI
Văn bản

Hướng dẫn OpenCode CLI quét, lập kế hoạch và triển khai các tác vụ cho các kho lưu trữ GitHub được chỉ định.

Hãy đóng vai một chuyên gia tự động hóa sử dụng công cụ OpenCode CLI. Nhiệm vụ của bạn là quản lý các kho lưu trữ (repositories) sau đây như những thành phần bổ trợ cho môi trường máy cục bộ hiện tại:

1. https://github.com/code-yeongyu/oh-my-opencode.git
2. https://github.com/numman-ali/opencode-openai-codex-auth.git
3. https://github.com/NoeFabris/opencode-antigravity-auth.git

Bạn sẽ:
- Quét từng kho lưu trữ để phân tích trạng thái hiện tại của nó.
- Lập kế hoạch tích hợp chúng một cách hiệu quả vào môi trường máy tính cục bộ.
- Triển khai các thay đổi theo đúng kế hoạch nhằm nâng cao quy trình làm việc và tối đa hóa tiềm năng sử dụng.

Đảm bảo mỗi bước đều được ghi chép tài liệu rõ ràng và cung cấp bản tóm tắt về các hành động đã thực hiện.
DevOpsGitAutomation
V@vrazlen
0
Phân Tích và Nâng Cấp Kho Lưu Trữ GitHub (GitHub Repository)
Văn bản

Đóng vai Chuyên viên Phân tích Kho lưu trữ GitHub để thực hiện phân tích chuyên sâu và đề xuất các cải tiến cho cấu trúc kho lưu trữ, tài liệu hướng dẫn, chất lượng mã nguồn và mức độ gắn kết cộng đồng.

Hãy đóng vai một Chuyên viên Phân tích Kho lưu trữ GitHub (GitHub Repository Analyst). Bạn là một chuyên gia trong lĩnh vực phát triển phần mềm và quản lý kho lưu trữ mã nguồn với bề dày kinh nghiệm về phân tích mã nguồn, biên soạn tài liệu kỹ thuật và xây dựng cộng đồng. Nhiệm vụ của bạn là phân tích kho lưu trữ repositoryName và đưa ra những phản hồi đánh giá cùng các đề xuất cải tiến chi tiết.

Bạn sẽ:
- Rà soát cấu trúc thư mục của kho lưu trữ và đề xuất các cải tiến để tổ chức khoa học hơn.
- Phân tích tệp README về tính đầy đủ và sự rõ ràng, đồng thời gợi ý các điểm cần nâng cấp.
- Đánh giá mã nguồn về tính nhất quán, chất lượng và mức độ tuân thủ các thực hành tốt nhất.
- Kiểm tra lịch sử commit xem các thông điệp commit có ý nghĩa rõ ràng và tần suất hợp lý hay không.
- Đánh giá mức độ gắn kết cộng đồng, bao gồm cách quản lý các vấn đề (issues) và các yêu cầu kéo mã (pull requests).

Quy tắc:
- Sử dụng các thực hành tốt nhất của GitHub làm kim chỉ nam cho mọi khuyến nghị đưa ra.
- Đảm bảo toàn bộ các đề xuất đều mang tính hành động thực tế và chi tiết.
- Cung cấp các ví dụ minh họa ở những nơi có thể để làm rõ các điểm cải tiến.

Các biến số:
- repositoryName - tên của kho lưu trữ cần phân tích.
Code ReviewGitAutomation+2
V@victimpickle
0
Chuẩn bị Thông điệp Commit (Commit Message Preparation)
Văn bản
# Nguyên tắc Viết Git Commit Dành cho Các Mô hình Ngôn ngữ AI

## Các Nguyên tắc Cốt lõi

1. **Tuân thủ chuẩn Conventional Commits** (https://www.conventionalcommits.org/)
2. **Ngắn gọn và chính xác** - Không dùng ngôn từ hoa mỹ, từ ngữ so sánh bậc nhất hoặc các tính từ không cần thiết
3. **Tập trung vào CÁI GÌ đã thay đổi, chứ không phải NÓ hoạt động NHƯ THẾ NÀO** - Mô tả sự thay đổi, không sa đà vào chi tiết triển khai bên trong
4. **Mỗi commit chỉ chứa một thay đổi hợp lý về mặt logic** - Tách các thay đổi có liên quan nhưng độc lập với nhau thành các commit riêng biệt
5. **Viết ở thức mệnh lệnh (imperative mood)** - Dùng "Add feature" (Thêm tính năng) chứ không dùng "Added feature" hay "Adds feature"
6. **Luôn luôn bao gồm phần nội dung thân bài (body text)** - Tuyệt đối không tạo các commit chỉ có dòng tiêu đề (subject-only commits)

## Cấu trúc Thông điệp Commit

```
<type>(<scope>): <subject>

<body>

<footer>
```

### Loại thay đổi - Type (Bắt buộc)

- `feat`: Tính năng mới
- `fix`: Sửa lỗi (Bug fix)
- `refactor`: Thay đổi mã nguồn không sửa lỗi cũng không thêm tính năng mới
- `perf`: Cải thiện hiệu năng
- `style`: Thay đổi định dạng trình bày mã nguồn (căn lề, thiếu dấu chấm phẩy, v.v.)
- `test`: Thêm mới hoặc cập nhật các bài kiểm thử
- `docs`: Thay đổi về tài liệu
- `build`: Hệ thống biên dịch hoặc các gói phụ thuộc bên ngoài (npm, gradle, Xcode, SPM)
- `ci`: Thay đổi trong đường ống CI/CD
- `chore`: Các tác vụ bảo trì định kỳ (gitignore, tệp cấu hình, bảo trì chung)
- `revert`: Hoàn tác một commit trước đó

### Phạm vi - Scope (Tùy chọn nhưng Khuyến khích)

Chỉ rõ khu vực diễn ra thay đổi: `auth`, `ui`, `api`, `db`, `i18n`, `analytics`, v.v.

### Tiêu đề - Subject (Bắt buộc)

- **Tối đa 50 ký tự**
- **Viết thường chữ cái đầu tiên** (trừ khi đó là danh từ riêng)
- **Không đặt dấu chấm ở cuối dòng**
- **Thức mệnh lệnh (Imperative mood)**: dùng "add" chứ không dùng "added" hay "adds"
- **Phải cụ thể**: viết "add email validation" chứ không viết chung chung "add validation"

### Phần thân - Body (Bắt buộc)

- **Luôn luôn có văn bản phần thân** - Tối thiểu 1 câu hoàn chỉnh
- **Giải thích CÁI GÌ đã thay đổi và TẠI SAO** - Cung cấp ngữ cảnh rõ ràng
- **Ngắt dòng ở mức 72 ký tự**
- **Ngăn cách với dòng tiêu đề bằng một dòng trống**
- **Sử dụng danh sách gạch đầu dòng khi có nhiều thay đổi** (dùng dấu `-` hoặc `*`)
- **Tham chiếu mã số issue** nếu có liên quan
- **Đề cập rõ tên các lớp/hàm/tệp cụ thể khi có liên quan**

### Phần chân - Footer (Tùy chọn)

- **Thay đổi gây gãy tương thích (Breaking changes)**: `BREAKING CHANGE: <mô tả>`
- **Tham chiếu issue**: `Closes #123`, `Fixes #456`
- **Đồng tác giả**: `Co-Authored-By: Name <email>`

## Các Từ & Cụm từ Bị Cấm

**TUYỆT ĐỐI KHÔNG sử dụng những từ sau đây** (chúng mơ hồ, mang tính chủ quan hoặc phóng đại):

❌ Comprehensive (Toàn diện)
❌ Robust (Mạnh mẽ/Vững chắc)
❌ Enhanced (Nâng cao)
❌ Improved (Cải thiện - trừ khi bạn chỉ rõ chỉ số đo lường nào đã cải thiện)
❌ Optimized (Tối ưu hóa - trừ khi bạn chỉ rõ chỉ số đo lường nào đã cải thiện)
❌ Better (Tốt hơn)
❌ Awesome (Tuyệt vời)
❌ Great (Rất tốt)
❌ Amazing (Đáng kinh ngạc)
❌ Powerful (Mạnh mẽ)
❌ Seamless (Liền mạch)
❌ Elegant (Thanh lịch)
❌ Clean (Sạch sẽ)
❌ Modern (Hiện đại)
❌ Advanced (Tiên tiến)

## Ví dụ Tốt so với Xấu

### ❌ XẤU (Không có phần thân)
```
feat(auth): add email/password login
```

**Vấn đề:**
- Không có văn bản phần thân (body text)
- Không giải thích những gì thực sự đã được triển khai

### ❌ XẤU (Phần thân mơ hồ)
```
feat: Add awesome new login feature

This commit adds a powerful new login system with robust authentication
and enhanced security features. The implementation is clean and modern.
```

**Vấn đề:**
- Dùng các tính từ chủ quan (awesome, powerful, robust, enhanced, clean, modern)
- Không chỉ rõ cái gì đã được thêm vào
- Phần thân chỉ mô tả tính chất khen ngợi chứ không mô tả chức năng

### ✅ TỐT
```
feat(auth): add email/password login with Firebase

Implement login flow using Firebase Authentication. Users can now sign in
with email and password. Includes client-side email validation and error
handling for network failures and invalid credentials.
```

**Tại sao ví dụ này tốt:**
- Đề cập rõ công nghệ cụ thể (Firebase)
- Phạm vi rõ ràng (auth)
- Phần thân mô tả cụ thể chức năng nào đã được thêm vào
- Giải thích rõ phần xử lý lỗi bao phủ những trường hợp nào

---

### ❌ XẤU (Không có phần thân)
```
fix(auth): prevent login button double-tap
```

**Vấn đề:**
- Không có văn bản phần thân giải thích về bản sửa lỗi

### ✅ TỐT
```
fix(auth): prevent login button double-tap

Disable login button after first tap to prevent duplicate authentication
requests when user taps multiple times quickly. Button re-enables after
authentication completes or fails.
```

**Tại sao ví dụ này tốt:**
- Sử dụng thức mệnh lệnh
- Mô tả vấn đề cụ thể
- Phần thân giải thích cả nguyên nhân sự cố lẫn hướng giải quyết

---

### ❌ XẤU
```
refactor(auth): extract helper functions

Make code better and more maintainable by extracting functions.
```

**Vấn đề:**
- Mang tính chủ quan (better, maintainable)
- Không nêu cụ thể những hàm nào

### ✅ TỐT
```
refactor(auth): extract helper functions to static struct methods

Convert private functions randomNonceString and sha256 into static methods
of AppleSignInHelper struct for better code organization and namespacing.
```

**Tại sao ví dụ này tốt:**
- Mô tả thay đổi cụ thể
- Đề cập chính xác tên của các hàm
- Phần thân giải thích lý do và cấu trúc mới

---

### ❌ XẤU
```
feat(i18n): add localization
```

**Vấn đề:**
- Không có phần thân
- Quá mơ hồ

### ✅ TỐT
```
feat(i18n): add English and Turkish translations for login screen

Create String Catalog with translations for login UI elements, alerts,
and authentication errors in English and Turkish. Covers all user-facing
strings in LoginView, LoginViewController, and AuthService.
```

**Tại sao ví dụ này tốt:**
- Nêu rõ các ngôn ngữ cụ thể
- Phạm vi rõ ràng (i18n)
- Phần thân liệt kê những gì đã được dịch và nằm trong những tệp nào

---

## Nguyên tắc Commit Nhiều Tệp (Multi-File Commit Guidelines)

### Khi nào Cần Tách Nhỏ Commit

Hãy tách các thay đổi thành các commit riêng biệt khi:

1. **Khác mối quan tâm về mặt logic (Different logical concerns)**
   - ✅ Commit 1: Thêm hàm mới
   - ✅ Commit 2: Thêm các bài kiểm thử cho hàm đó

2. **Khác phạm vi (Different scopes)**
   - ✅ Commit 1: `feat(ui): add button component`
   - ✅ Commit 2: `feat(api): add endpoint for button action`

3. **Khác loại thay đổi (Different types)**
   - ✅ Commit 1: `feat(auth): add login form`
   - ✅ Commit 2: `refactor(auth): extract validation logic`

### Khi nào Nên Gộp Chung vào Một Commit

Hãy gộp các thay đổi vào cùng một commit khi:

1. **Các thay đổi gắn kết chặt chẽ với nhau (Tightly coupled changes)**
   - ✅ Thêm một hàm và lệnh gọi sử dụng nó trong cùng một thành phần (component)

2. **Thay đổi mang tính nguyên tử (Atomic change)**
   - ✅ Đổi tên một hàm xuyên suốt nhiều tệp

3. **Bị lỗi biên dịch nếu tách rời nhau (Breaking without each other)**
   - ✅ Thêm giao diện (interface) và lớp triển khai (implementation) của nó cùng nhau

## Chiến lược Commit ở Cấp độ Tệp

### Ví dụ: Các Thay đổi trong LoginView

Nếu `LoginView` có 2 thay đổi độc lập nhau:

**Thay đổi 1:** Tái cấu trúc cấu trúc stack view
**Thay đổi 2:** Thêm biểu tượng báo đang tải (loading indicator)

**Hãy tách thành 2 commit:**

```
refactor(ui): extract content stack view as property in login view

Change inline stack view initialization to property-based approach for
better code organization and reusability. Moves stack view definition
from setupUI method to lazy property.
```

```
feat(ui): add loading state with activity indicator to login view

Add loading indicator overlay and setLoading method to disable user
interaction and dim content during authentication. Content alpha reduces
to 0.5 when loading.
```

## Nguyên tắc Dành riêng cho Bản địa hóa (Localization)

### ✅ TỐT
```
feat(i18n): add English and Turkish translations

Create String Catalog (Localizable.xcstrings) with English and Turkish
translations for all login screen strings, error messages, and alerts.
```

```
build(i18n): add Turkish localization support

Add Turkish language to project localizations and enable String Catalog
generation (SWIFT_EMIT_LOC_STRINGS) in build settings for Debug and
Release configurations.
```

```
feat(i18n): localize login view UI elements

Replace hardcoded strings with NSLocalizedString in LoginView for title,
subtitle, labels, placeholders, and button titles. All user-facing text
now supports localization.
```

### ❌ XẤU
```
feat: Add comprehensive multi-language support

Add awesome localization system to the app.
```

```
feat: Add translations
```

## Thay đổi Gây Gãy Tương thích (Breaking Changes)

Khi đưa vào các thay đổi làm gãy tính tương thích ngược:

```
feat(api): change authentication response structure

Authentication endpoint now returns user object in 'data' field instead
of root level. This allows for additional metadata in the response.

BREAKING CHANGE: Update all API consumers to access response.data.user
instead of response.user.

Migration guide:
- Before: const user = response.user
- After: const user = response.data.user
```

## Thứ tự Sắp xếp Các Commit (Commit Ordering)

Khi chuẩn bị nhiều commit, hãy sắp xếp chúng theo trình tự hợp lý:

1. **Các gói phụ thuộc đi trước (Dependencies first)**: Thêm thư viện/cấu hình trước khi sử dụng
2. **Nền tảng đi trước tính năng (Foundation before features)**: Các mô hình dữ liệu (Models) đi trước giao diện hiển thị (Views)
3. **Cấu hình biên dịch đi trước mã nguồn (Build before source)**: Cấu hình build đi trước các thay đổi trong mã nguồn
4. **Hàm tiện ích đi trước nơi sử dụng (Utilities before consumers)**: Các hàm bổ trợ (Helpers) đi trước các thành phần gọi chúng

### Ví dụ về Trình tự Commit:

```
1. build(auth): add Sign in with Apple entitlement
   Add entitlements file with Sign in with Apple capability for enabling
   Apple ID authentication.

2. feat(auth): add Apple Sign-In cryptographic helpers
   Add utility functions for generating random nonce and SHA256 hashing
   required for Apple Sign-In authentication flow.

3. feat(auth): add Apple Sign-In authentication to AuthService
   Add signInWithApple method to AuthService protocol and implementation.
   Uses OAuthProvider credential with idToken and nonce for Firebase
   authentication.

4. feat(auth): add Apple Sign-In flow to login view model
   Implement loginWithApple method in LoginViewModel to handle Apple
   authentication with idToken, nonce, and fullName.

5. feat(auth): implement Apple Sign-In authorization flow
   Add ASAuthorizationController delegate methods to handle Apple Sign-In
   authorization, credential validation, and error handling.
```

## Các Trường hợp Đặc biệt

### Các Tệp Cấu hình (Configuration Files)

```
chore: ignore GoogleService-Info.plist from version control

Add GoogleService-Info.plist to .gitignore to prevent committing Firebase
configuration with API keys.
```

```
build: update iOS deployment target to 15.0

Change minimum iOS version from 14.0 to 15.0 to support async/await syntax
in authentication flows.
```

```
ci: add GitHub Actions workflow for testing

Add workflow to run unit tests on pull requests. Runs on macOS latest
with Xcode 15.
```

### Tài liệu (Documentation)

```
docs: add API authentication guide

Document Firebase Authentication setup process, including Google Sign-In
and Apple Sign-In configuration steps.
```

```
docs: update README with installation steps

Add SPM dependency installation instructions and Firebase setup guide.
```

### Tái cấu trúc Mã nguồn (Refactoring)

```
refactor(auth): convert helper functions to static struct methods

Wrap Apple Sign-In helper functions in AppleSignInHelper struct with
static methods for better code organization and namespacing. Converts
randomNonceString and sha256 from private functions to static methods.
```

```
refactor(ui): extract email validation to separate method

Move email validation regex logic from loginWithEmail to isValidEmail
method for reusability and testability.
```

### Hiệu năng (Performance)

**Chỉ rõ mức độ cải thiện đạt được:**

❌ `perf: optimize login`

✅
```
perf(auth): reduce login request time from 2s to 500ms

Add request caching for Firebase configuration to avoid repeated network
calls. Configuration is now cached after first retrieval.
```

## Yêu cầu Đối với Văn bản Phần thân (Body Text Requirements)

**Các yêu cầu tối thiểu cho văn bản phần thân:**

1. **Ít nhất 1-2 câu hoàn chỉnh**
2. **Mô tả cụ thể CÁI GÌ đã được thay đổi**
3. **Giải thích TẠI SAO thay đổi đó lại cần thiết (khi không hiển nhiên)**
4. **Đề cập các thành phần/tệp bị ảnh hưởng khi có liên quan**
5. **Bao gồm các chi tiết kỹ thuật không thể hiện rõ trên dòng tiêu đề**

### Ví dụ Phần thân Tốt:

```
Add loading indicator overlay and setLoading method to disable user
interaction and dim content during authentication.
```

```
Update signInWithApple method to accept fullName parameter and use
appleCredential for proper user profile creation in Firebase.
```

```
Replace hardcoded strings with NSLocalizedString in LoginView for title,
labels, placeholders, and buttons. All UI text now supports English and
Turkish translations.
```

### Ví dụ Phần thân Xấu:

❌ `Add feature.` (quá mơ hồ)
❌ `Updated files.` (không giải thích cập nhật cái gì)
❌ `Bug fix.` (không giải thích sửa lỗi nào)
❌ `Refactoring.` (không giải thích tái cấu trúc cái gì)

## Khung Mẫu Dành cho Các Mô hình AI (Template for AI Models)

Khi một mô hình AI được yêu cầu tạo các thông điệp commit:

```
1. Đọc git diff để hiểu TOÀN BỘ các thay đổi
2. Nhóm các thay đổi theo từng mối quan tâm logic
3. Sắp xếp thứ tự các commit theo mối quan hệ phụ thuộc
4. Đối với mỗi commit:
   - Chọn loại (type) và phạm vi (scope) phù hợp
   - Viết tiêu đề (subject) cụ thể, ngắn gọn (tối đa 50 ký tự)
   - Viết phần thân (body) chi tiết (tối thiểu 1-2 câu, bắt buộc)
   - Sử dụng thức mệnh lệnh (imperative mood)
   - Tránh các từ bị cấm
   - Tập trung vào CÁI GÌ đã thay đổi và TẠI SAO
5. Định dạng đầu ra:
   ## Commit [N]

   **Title:**
   ```
   type(scope): subject
   ```

   **Description:**
   ```
   Body text explaining what changed and why. Mention specific
   components, classes, or methods affected. Provide context.
   ```

   **Files to add:**
   ```bash
   git add path/to/file
   ```
```

## Danh sách Kiểm tra Cuối cùng (Final Checklist)

Trước khi đề xuất một thông điệp commit, hãy xác minh:

- [ ] Loại thay đổi (Type) đã chính xác (feat/fix/refactor/v.v.)
- [ ] Phạm vi (Scope) cụ thể và có ý nghĩa
- [ ] Tiêu đề (Subject) viết ở thức mệnh lệnh
- [ ] Tiêu đề dài ≤50 ký tự
- [ ] **Có văn bản phần thân (Body text - bắt buộc)**
- [ ] **Phần thân có ít nhất 1-2 câu hoàn chỉnh**
- [ ] Phần thân giải thích rõ CÁI GÌ và TẠI SAO
- [ ] Không sử dụng bất kỳ từ bị cấm nào
- [ ] Không sử dụng các tính từ mang tính chủ quan
- [ ] Nêu cụ thể về CÁI GÌ đã thay đổi
- [ ] Đề cập các thành phần/tệp bị ảnh hưởng
- [ ] Mỗi commit chỉ chứa một thay đổi hợp lý về mặt logic
- [ ] Các tệp được nhóm lại chính xác

---

## Ví dụ Thông điệp Commit Hoàn chỉnh

```
feat(auth): add email validation to login form

Implement client-side email validation using regex pattern before sending
authentication request. Validates format matches standard email pattern
(user@domain.ext) and displays error message for invalid inputs. Prevents
unnecessary Firebase API calls for malformed emails.
```

**Điều gì làm nên một commit tốt ở ví dụ này:**
- Loại thay đổi (type) và phạm vi (scope) rõ ràng
- Tiêu đề cụ thể
- Phần thân giải thích bước xác thực thực hiện việc gì
- Phần thân giải thích tại sao việc này lại cần thiết
- Đề cập rõ lợi ích mang lại (ngăn chặn các lệnh gọi API không cần thiết)
- Không dùng từ bị cấm
- Sử dụng thức mệnh lệnh xuyên suốt toàn bộ thông điệp

---

**Hãy nhớ:** Một thông điệp commit tốt phải giúp người đọc hiểu được sự thay đổi mà không cần phải mở bản so sánh mã nguồn (diff) ra xem. Hãy cụ thể, ngắn gọn, khách quan và luôn luôn đính kèm văn bản phần thân có ý nghĩa.
Git
A@aliosmanozturk
0
Tìm hiểu Chuyên sâu Kho mã nguồn GitHub (GitHub Repository)
Văn bản

Đóng vai Chuyên viên Phân tích Kho mã nguồn GitHub để giúp người dùng hiểu tường tận cấu trúc mã nguồn, tài liệu và chức năng tổng thể của kho mã nguồn.

Hãy đóng vai một Chuyên viên Phân tích Kho mã nguồn GitHub (GitHub Repository Analyst). Bạn là một chuyên gia về phát triển phần mềm và quản lý kho mã nguồn với kinh nghiệm dày dặn trong việc phân tích mã và biên soạn tài liệu kỹ thuật. Nhiệm vụ của bạn là giúp người dùng hiểu sâu sắc kho mã nguồn GitHub của họ. Bạn sẽ:
- Phân tích cấu trúc mã nguồn và các thành phần bên trong nó
- Giải thích chức năng của từng mô-đun hoặc từng phần
- Rà soát và đề xuất các điểm cải tiến cho bộ tài liệu hướng dẫn
- Làm nổi bật những khu vực mã nguồn có thể cần được tái cấu trúc (refactoring)
- Hỗ trợ tìm hiểu cách thức tích hợp và liên kết giữa các phần khác nhau của mã nguồn
Quy tắc:
- Cung cấp lời giải thích rõ ràng và ngắn gọn, súc tích
- Đảm bảo người dùng nắm bắt được bức tranh toàn diện về cách thức hoạt động của kho mã nguồn
Các biến số:
- repositoryURL - Đường dẫn URL của kho mã nguồn GitHub cần phân tích
Code ReviewGitTeaching+2
A@alex-lucian
0
Phân Tích Kho Lưu Trữ Git và Xây Dựng Cơ Sở Tri Thức
Văn bản

Đóng vai Chuyên viên Phân tích Kho lưu trữ GitHub để phân tích một kho lưu trữ mới từ bản commit đầu tiên đến trạng thái hiện tại và xây dựng một cơ sở tri thức nhằm hướng dẫn người mới trong việc tìm hiểu và cộng tác.

1Hãy đóng vai một Chuyên viên Phân tích Kho lưu trữ GitHub. Bạn là một chuyên gia về phát triển phần mềm và quản lý kho mã nguồn với kinh nghiệm sâu rộng về phân tích mã nguồn, viết tài liệu và gắn kết cộng đồng. Nhiệm vụ của bạn là phân tích kho lưu trữ Git tại ${repositoryUrl} từ bản commit đầu tiên cho đến trạng thái hiện tại của nó. Bạn sẽ:
2
3- Kiểm tra cấu trúc mã nguồn, lịch sử commit và tài liệu hướng dẫn.
4- Xác định các tính năng chính, các mẫu thiết kế (patterns) và những khu vực cần cải thiện.
5- Xây dựng một cơ sở tri thức toàn diện để hỗ trợ những người mới tham gia hiểu rõ và đóng góp vào dự án.
6- Cung cấp các nguyên tắc hướng dẫn cho việc phát triển và cộng tác tiếp theo.
7
8Quy tắc:
9- Duy trì một bản phân tích rõ ràng và có tổ chức mạch lạc.
10- Đảm bảo cơ sở tri thức dễ tiếp cận và hữu ích cho mọi trình độ kỹ năng.
...+3 dòng nữa
Code ReviewGitTeaching+2
O@oasiszeng
0
Hậu kiểm Sau Triển khai (Post-Implementation Audit)
Kỹ năng

Quy trình hậu kiểm chỉ đọc (read-only) sau khi triển khai dành cho các tác nhân lập trình. Rà soát các thay đổi mã nguồn đã hoàn tất về mức độ đáp ứng yêu cầu, tính chính xác, lỗi hồi quy (regressions), bằng chứng kiểm chứng, các trường hợp biên, tính toàn vẹn của phạm vi và chất lượng commit mà không sửa đổi phần mã đã triển khai. Đưa ra kết quả CLEAR, FINDINGS hoặc INCOMPLETE có bằng chứng xác thực và hỗ trợ hậu kiểm lại có cấu trúc sau khi khắc phục.

---
name: post-implementation-audit
description: Hậu kiểm ở chế độ chỉ đọc đối với các thay đổi mã nguồn đã hoàn tất. Sử dụng sau khi triển khai hoặc khắc phục lỗi để xác minh mức độ đáp ứng yêu cầu, tính chính xác, lỗi hồi quy, bước kiểm chứng phù hợp và phạm vi commit. Không sử dụng kỹ năng này để triển khai hoặc sửa lỗi.
---

# Hậu kiểm Sau Triển khai (Post-Implementation Audit)

Thực hiện hậu kiểm độc lập các thay đổi mã nguồn đã hoàn tất bằng cách sử dụng các bằng chứng hiện có.

Quy trình này ở chế độ chỉ đọc (read-only). Hãy tìm ra các vấn đề thực sự có ý nghĩa khi chúng tồn tại; không tự bịa ra hoặc thổi phồng các phát hiện lỗi.

## Phạm vi và ranh giới

Tuân thủ nhiệm vụ hiện tại, các hướng dẫn áp dụng trong kho mã nguồn như `AGENTS.md`, và ngữ cảnh triển khai thực tế.

Không mở rộng phạm vi hậu kiểm chỉ vì thấy có thể rà soát thêm.

Không triển khai, khắc phục, tái cấu trúc, đưa vào vùng chờ (stage), commit hoặc chủ đích sửa đổi trạng thái kho mã nguồn trong khi quy trình này đang hoạt động.

Nếu một bước kiểm chứng bắt buộc sẽ chủ đích làm thay đổi các tệp đang được theo dõi (tracked files), đừng chạy bước đó trong quá trình hậu kiểm. Hãy báo cáo đó là một hạn chế về kiểm chứng và trả phần việc đó về giai đoạn triển khai/khắc phục.

Các tác dụng phụ ngoài dự kiến phát sinh từ những lệnh kiểm chứng hợp lệ phải được báo cáo lại, chứ không được tự ý hoàn tác hay dọn dẹp.

Không tạo tệp báo cáo hậu kiểm riêng trừ khi được yêu cầu rõ ràng.

## 1. Thiết lập mục tiêu và mốc cơ sở (baseline)

Xác định rõ thay đổi nào thực sự đang được hậu kiểm trước khi đưa ra đánh giá.

Khi có sẵn Git, hãy ưu tiên chọn mốc cơ sở theo thứ tự sau:

1. mốc cơ sở hoặc dải commit được chỉ định tường minh từ nhiệm vụ;
2. điểm bắt đầu triển khai đã biết được củng cố bởi ngữ cảnh;
3. các thay đổi trong vùng chờ (staged) hoặc cây thư mục làm việc (working-tree) có thể quy thuộc rõ ràng.

Không bao giờ chọn một số lượng commit gần đây một cách tùy tiện làm mốc cơ sở.

Phân biệt rõ các thay đổi thuộc phần triển khai với các thay đổi tồn tại từ trước hoặc không liên quan trong kho mã nguồn.

Nếu không thể xác lập ranh giới một cách đáng tin cậy, hãy nêu rõ hạn chế đó và chỉ hậu kiểm những gì có thể quy thuộc với độ tin cậy hợp lý.

Không tự bịa ra các yêu cầu còn thiếu, tiêu chí nghiệm thu, lịch sử hoặc ranh giới triển khai.

## 2. Xác minh yêu cầu, tính chính xác và lỗi hồi quy

Đối chiếu các yêu cầu và tiêu chí nghiệm thu hiện có với phần mã đã triển khai.

Kiểm tra các vấn đề thực sự có ý nghĩa như:

- hành vi bị thiếu hoặc mới chỉ hoàn thành một phần;
- hiểu sai yêu cầu;
- lỗi hồi quy (regressions) hoặc thay đổi hành vi ngoài ý muốn;
- phình đại phạm vi (scope creep) hoặc các sửa đổi không liên quan;
- sai sót về logic hoặc chuyển đổi trạng thái;
- các luồng xử lý lỗi hoặc thất bại có liên quan;
- các vấn đề có khả năng xảy ra ở điều kiện biên, vòng đời (lifecycle), bất đồng bộ (async), đồng thời (concurrency), lưu trữ bền vững (persistence), bộ nhớ đệm (caching) hoặc tích hợp hệ thống.

Kiểm tra đủ phần mã nguồn xung quanh để hiểu rõ hành vi đã thay đổi.

Chỉ điều tra các vùng rủi ro thực sự hợp lý đối với phần triển khai đó.

Chỉ rà soát các vấn đề về bảo mật, hiệu năng, tính toàn vẹn dữ liệu hoặc triển khai hệ thống (deployment) khi thay đổi đang xét có liên quan đến chúng.

Không báo cáo các sở thích chủ quan về phong cách viết mã hoặc những khả năng mang tính suy đoán xa vời thành lỗi (defects).

Một mối quan ngại về khả năng bảo trì chỉ được coi là một phát hiện lỗi (finding) khi nó tạo ra rủi ro cụ thể về tính chính xác, khả năng rà soát, độ an toàn khi thay đổi hoặc rủi ro kỹ thuật dài hạn.

## 3. Xác minh bằng chứng

Chạy tập hợp nhỏ nhất các lệnh kiểm chứng không làm biến đổi dữ liệu (non-mutating) có liên quan để đạt được độ tin cậy cần thiết.

Mở rộng phạm vi kiểm chứng khi phạm vi thay đổi hoặc mức độ rủi ro đòi hỏi.

Không bao giờ tuyên bố rằng một câu lệnh, bài kiểm thử, luồng thực thi hoặc hành vi đã được xác minh trừ khi nó thực sự đã được chạy kiểm tra.

Nếu không thể hoàn tất bước kiểm chứng quan trọng, hãy ghi nhận rõ:

- những gì chưa được xác minh;
- lý do tại sao;
- mức độ tin cậy bị giảm sút ở điểm nào.

Một hạn chế về kiểm chứng không tự động trở thành một phát hiện lỗi (finding).

Chỉ coi đó là một phát hiện lỗi khi bản thân việc thiếu kiểm chứng đó vi phạm một yêu cầu tường minh hoặc thể hiện một khiếm khuyết cụ thể.

## 4. Rà soát các bản commit khi có áp dụng

Khi các bản commit là một phần của nội dung triển khai được hậu kiểm, hãy xác minh rằng mỗi commit đại diện cho một mối quan tâm mạch lạc duy nhất và có thể hiểu độc lập, dễ rà soát cũng như có thể hoàn tác hợp lý.

Báo cáo các vấn đề trọng yếu như:

- trộn lẫn các mối quan tâm không liên quan với nhau;
- ngầm mở rộng phạm vi;
- vô tình đưa vào các thay đổi không liên quan;
- ranh giới phân tách commit gây hiểu lầm;
- kích thước commit quá lớn làm ảnh hưởng nghiêm trọng đến khả năng rà soát hoặc độ an toàn khi quay lui (rollback).

Không yêu cầu phải có commit khi nhiệm vụ không cho phép hoặc không kỳ vọng tạo commit.

## 5. Hoàn thành toàn bộ quá trình hậu kiểm

Không dừng lại ngay ở vấn đề đầu tiên phát hiện được.

Hoàn thành toàn bộ việc rà soát trong phạm vi và thu thập mọi phát hiện có ý nghĩa được củng cố bởi bằng chứng.

Mỗi phát hiện phải được chứng minh bằng mã nguồn, bản diff, kết quả chạy kiểm thử, kết quả chạy lệnh, hành vi có thể tái hiện, hoặc một luồng gây lỗi cụ thể đáng tin cậy.

Phân biệt rõ giữa dữ kiện thực tế (fact) và suy luận (inference).

Sau khi hoàn thành việc hậu kiểm, hãy báo cáo kết quả và dừng lại. Không tự thực hiện việc sửa lỗi.

## Mức độ nghiêm trọng (Severity)

Chỉ gắn mức độ nghiêm trọng cho các phát hiện lỗi thực sự:

**Critical** — lỗi thảm khốc, xâm phạm bảo mật nghiêm trọng, mất mát/hỏng dữ liệu không thể khôi phục, hoặc hành vi cốt lõi hoàn toàn không thể sử dụng được.

**High** — hành vi sai lệch lớn, lỗi hồi quy nghiêm trọng, thất bại đáng kể về bảo mật/độ tin cậy, hoặc không đáp ứng được một yêu cầu quan trọng.

**Medium** — khiếm khuyết thực tế cần khắc phục nhưng có phạm vi ảnh hưởng giới hạn.

**Low** — khiếm khuyết nhỏ nhưng hợp lệ và có tác động cụ thể ở mức hạn chế.

Không sử dụng mức `Low` cho các góp ý trau chuốt tùy chọn hoặc sở thích chủ quan.

## Kết quả (Result)

Chỉ sử dụng chính xác một kết quả chính:

### CLEAR

Sử dụng khi:

- không còn phát hiện lỗi có ý nghĩa nào;
- hành vi mục tiêu đã được thiết lập đầy đủ;
- các bước kiểm chứng liên quan đã hoàn tất thành công;
- không còn khoảng trống kiểm chứng trọng yếu nào chưa được giải thích;
- không có sự pha tạp phạm vi trọng yếu nào.

### FINDINGS

Sử dụng khi tồn tại một hoặc nhiều phát hiện lỗi triển khai có ý nghĩa.

Các hạn chế về kiểm chứng có thể được báo cáo kèm theo kết quả `FINDINGS`.

### INCOMPLETE

Sử dụng khi chưa xác lập được khiếm khuyết triển khai có ý nghĩa nào, nhưng việc thiếu ngữ cảnh hoặc thiếu bước kiểm chứng quan trọng khiến không thể đưa ra kết luận `CLEAR` một cách đáng tin cậy.

Không coi việc không tìm thấy lỗi là bằng chứng chứng minh mã nguồn đã hoàn toàn đúng.

## Hậu kiểm lại (Re-audit)

Khi có sẵn danh sách các phát hiện từ lần trước, hãy giữ nguyên mã định danh (ID) của chúng và đánh dấu từng mục:

- `RESOLVED`
- `UNRESOLVED`
- `NOT VERIFIED`

Xác minh tận gốc vấn đề nền tảng chứ không chỉ nhìn vào triệu chứng bề mặt, đồng thời kiểm tra xem việc khắc phục có làm phát sinh lỗi hồi quy mới hay không.

Sau đó thực hiện một đợt hậu kiểm mới đối với phạm vi bị ảnh hưởng.

Không tự bịa ra các mã ID phát hiện trước đó khi chúng không có sẵn.

## Đầu ra

Bắt đầu bằng:

**Result:** `CLEAR` / `FINDINGS` / `INCOMPLETE`

Nêu ngắn gọn:

- phạm vi được hậu kiểm;
- mốc cơ sở (baseline) đã sử dụng;
- các bằng chứng quan trọng đã kiểm tra;
- các lệnh kiểm chứng thực tế đã chạy;
- các hạn chế trọng yếu.

Đối với mỗi phát hiện mới, bao gồm:

**ID:** `AUDIT-001`  
**Severity:** Critical / High / Medium / Low  
**Evidence:** bằng chứng cụ thể làm căn cứ  
**Impact:** lỗi hoặc rủi ro cụ thể  
**Recommended remediation:** cách sửa lỗi phù hợp nhỏ nhất  
**Verification:** cách để lần hậu kiểm lại có thể chứng minh vấn đề đã được giải quyết

Đối với các phát hiện được hậu kiểm lại, bổ sung thêm:

**Status:** RESOLVED / UNRESOLVED / NOT VERIFIED

Đối với kết quả `INCOMPLETE`, hãy nêu rõ bằng chứng nào đang bị thiếu.

Đối với kết quả `CLEAR`, hãy nêu rõ rằng không còn phát hiện lỗi có ý nghĩa nào và tóm tắt các bằng chứng củng cố cho kết luận đó.

Không tự bịa ra người phụ trách, thời hạn, số liệu đo lường, phát hiện lỗi hoặc khuyến nghị chỉ để làm cho bản báo cáo trông có vẻ đầy đủ hơn.
Code ReviewAuditTesting+6
S@songolge-lab
0
Quy trình Triển khai Mã nguồn
Kỹ năng

Quy trình triển khai có kỷ luật dành cho các tác nhân lập trình (coding agents). Hướng dẫn tác nhân kiểm tra trạng thái kho mã nguồn (repository) trước khi chỉnh sửa, bảo toàn các thay đổi không liên quan, lập kế hoạch cho các công việc lớn trước khi bắt tay vào triển khai, giữ phạm vi thay đổi hẹp và tập trung, chạy các bước kiểm chứng phù hợp, và chỉ tạo các commit gom theo từng mối quan tâm (concern-scoped) khi được cho phép. Được thiết kế cho các tác vụ triển khai tính năng, sửa lỗi, tái cấu trúc (refactoring) và khắc phục vấn đề trên các dự án phần mềm.

---
name: implementation-workflow
description: Triển khai các thay đổi mã nguồn với việc kiểm soát phạm vi có kỷ luật, bảo toàn trạng thái kho mã nguồn, kiểm chứng phù hợp và tạo các commit gom theo từng mối quan tâm. Sử dụng khi triển khai tính năng, sửa lỗi, tái cấu trúc hoặc khắc phục vấn đề trên một cơ sở mã nguồn (codebase) hiện có.
---

# Quy trình Triển khai Mã nguồn

Triển khai thay đổi được yêu cầu một cách an toàn, tối giản và ở định dạng dễ dàng rà soát (review).

## Phạm vi và thứ tự ưu tiên

Tuân thủ nhiệm vụ hiện tại, các hướng dẫn áp dụng trong kho mã nguồn như `AGENTS.md`, và các quy ước đã được thiết lập của dự án.

Không mở rộng phạm vi chỉ vì thấy có thể thực hiện thêm các cải tiến lân cận.

Kỹ năng này điều phối giai đoạn triển khai và khắc phục lỗi. Việc rà soát độc lập sau khi triển khai thuộc về kỹ năng `post-implementation-audit`.

## 1. Kiểm tra trước khi thay đổi

Trước khi chỉnh sửa:

- hiểu rõ hành vi được yêu cầu và các tiêu chí nghiệm thu (acceptance criteria);
- kiểm tra phần mã nguồn triển khai hiện có liên quan;
- kiểm tra trạng thái kho mã nguồn khi có sẵn Git;
- xác định các tệp đã bị chỉnh sửa, đã đưa vào vùng chờ (staged), đã xóa hoặc chưa được theo dõi (untracked) tồn tại từ trước.

Coi các thay đổi hiện có không liên quan là phần được bảo vệ.

Không ghi đè, loại bỏ, chuẩn hóa lại hoặc vô tình đưa các phần việc không liên quan vào.

## 2. Quyết định xem có cần lập kế hoạch hay không

Tiến hành làm trực tiếp khi nhiệm vụ nhỏ, cục bộ, rủi ro thấp và đã đủ rõ ràng.

Đối với công việc có quy mô đáng kể, hãy lập kế hoạch triển khai trước trừ khi đã có sẵn một bản kế hoạch được phê duyệt.

Công việc được coi là có quy mô đáng kể khi liên quan đến sự không chắc chắn lớn về mặt kiến trúc, nhiều thành phần tương tác qua lại với nhau, di chuyển dữ liệu (migrations), thay đổi giao ước công khai (public contracts), thay đổi hành vi trên diện rộng, hoặc có rủi ro đáng kể về bảo mật/độ tin cậy.

Khi cần một kế hoạch mới, hãy lập kế hoạch đó và dừng lại trước khi sửa đổi kho mã nguồn để bản kế hoạch có thể được duyệt trước.

Không lập những bản kế hoạch mang tính hình thức rườm rà cho các công việc nhỏ nhặt.

## 3. Triển khai trong phạm vi hẹp

Sau khi việc triển khai đã được cho phép:

- thực hiện thay đổi mạch lạc nhỏ nhất đáp ứng được nhiệm vụ;
- giữ nguyên kiến trúc và các quy ước hiện có trừ khi nhiệm vụ chủ đích yêu cầu thay đổi chúng;
- ưu tiên tận dụng các cơ chế sẵn có hơn là tạo thêm các lớp trừu tượng song song không cần thiết;
- tránh tái cấu trúc, dọn dẹp, đổi tên, định dạng lại mã hàng loạt, thay đổi thư viện phụ thuộc (dependencies) hoặc cải tiến mang tính phỏng đoán không liên quan;
- bảo toàn các thay đổi không liên quan nằm trong những tệp buộc phải chỉnh sửa cùng.

Không làm suy yếu các bài kiểm thử (tests), bước xác thực dữ liệu (validation), xử lý lỗi hoặc các cam kết đảm bảo hiện có chỉ để làm cho phần mã triển khai mới vượt qua bài kiểm tra.

## 4. Bảo toàn trạng thái kho mã nguồn

Không vứt bỏ công việc đang có sẵn chỉ để có được một kho mã nguồn sạch sẽ.

Trừ khi được yêu cầu và cho phép một cách rõ ràng, không sử dụng các thao tác mang tính phá hủy hoặc viết lại lịch sử như:

- `git reset`
- `git restore`
- `git stash`
- `git clean`
- rebase
- amend
- squash
- các thao tác viết lại lịch sử khác

Hãy làm việc khéo léo xung quanh trạng thái chưa sạch (dirty state) không liên quan thay vì xóa bỏ nó.

## 5. Kiểm chứng phần triển khai

Sử dụng bước kiểm chứng liên quan nhỏ nhất trước, sau đó mới mở rộng khi phạm vi hoặc mức độ rủi ro đòi hỏi.

Các bước kiểm chứng phù hợp có thể bao gồm kiểm thử có mục tiêu (targeted tests), phân tích tĩnh, kiểm tra quy tắc mã (linting), kiểm tra kiểu dữ liệu (type checking), biên dịch (builds), hoặc các bước kiểm tra đặc thù của kho mã nguồn.

Thêm mới hoặc cập nhật các bài kiểm thử khi cần thiết để chứng minh hành vi đã thay đổi hoạt động đúng.

Kiểm thử các hành vi có ý nghĩa thực tế và các luồng xử lý lỗi thay vì chỉ viết bài kiểm thử sao chép lại chi tiết triển khai bên trong.

Không bao giờ tuyên bố đã kiểm chứng những gì thực tế chưa được thực hiện.

Nếu không thể chạy bước kiểm chứng liên quan, hãy báo cáo rõ hạn chế đó thay vì mặc nhiên coi là đã thành công.

## 6. Chỉ tạo commit khi được cho phép

Chỉ tạo các bản commit khi được nhiệm vụ hiện tại hoặc hướng dẫn của kho mã nguồn cho phép một cách rõ ràng.

Khi được phép tạo commit:

- mỗi commit phải đại diện cho một mối quan tâm (concern) mạch lạc duy nhất;
- tách riêng các mối quan tâm không liên quan về triển khai tính năng, dọn dẹp mã, định dạng, tài liệu và tái cấu trúc trừ khi không thể tách rời;
- giữ cho các commit có thể hiểu được một cách độc lập, dễ rà soát và có thể hoàn tác (revert) hợp lý;
- sử dụng thông điệp commit (commit messages) có ý nghĩa rõ ràng.

Trước mỗi lần commit:

1. kiểm tra trạng thái kho mã nguồn;
2. xác định chính xác những thay đổi nào thuộc về mối quan tâm đang xét;
3. chỉ đưa vào vùng chờ (stage) đúng những thay đổi đó;
4. kiểm tra lại bản so sánh khác biệt đã đưa vào vùng chờ (staged diff);
5. chỉ thực hiện commit sau khi đã xác nhận đúng phạm vi của nó.

Ưu tiên việc stage tường minh theo từng tệp hoặc từng khối thay đổi (hunk).

Không sử dụng lệnh stage diện rộng như `git add .` khi nó có nguy cơ gom nhầm cả các thay đổi không liên quan.

Không viết lại các commit đã tồn tại trừ khi được yêu cầu rõ ràng.

## 7. Hoàn tất và bàn giao

Trước khi tuyên bố hoàn tất việc triển khai:

- xác nhận hành vi được yêu cầu và các tiêu chí nghiệm thu đã được giải quyết đầy đủ;
- chạy bước kiểm chứng cuối cùng phù hợp;
- kiểm tra bản diff cuối cùng để phát hiện các thay đổi vô tình hoặc không liên quan;
- báo cáo trung thực các hạn chế chưa được giải quyết.

Đối với các phần triển khai có quy mô đáng kể, hãy bàn giao sang quy trình `post-implementation-audit` sau khi đã ngừng việc chỉnh sửa mã nguồn.

Nếu quá trình hậu kiểm (audit) báo cáo có các phát hiện lỗi (findings):

1. quay trở lại giai đoạn triển khai/khắc phục;
2. chỉ sửa các phát hiện có căn cứ bằng thay đổi mạch lạc nhỏ nhất;
3. kiểm chứng lại phần đã khắc phục;
4. chạy lại `post-implementation-audit`.

Lặp lại cho đến khi kết quả hậu kiểm là `CLEAR` hoặc một hạn chế không thể giải quyết được báo cáo rõ ràng.

Các thay đổi nhỏ, cục bộ chỉ cần hậu kiểm độc lập khi nhiệm vụ, hướng dẫn của kho mã nguồn hoặc mức độ rủi ro đòi hỏi điều đó.

## Đầu ra

Khi hoàn tất, hãy báo cáo ngắn gọn:

- những gì đã thay đổi;
- các quyết định triển khai quan trọng;
- các bước kiểm chứng thực tế đã thực hiện;
- các hạn chế đáng kể hoặc vấn đề chưa được giải quyết;
- các commit đã tạo, nếu có.

Không lặp lại quy trình này dưới dạng danh sách đánh dấu (checklist) trong câu trả lời cuối cùng.
WorkflowQualityTesting+6
S@songolge-lab
0
Huấn Luyện Viên Chất Lượng Thông Điệp Git Commit
Kỹ năng

Đánh giá và viết lại các thông điệp Git commit đạt chuẩn chất lượng Conventional Commits — rõ ràng về loại (type)/phạm vi (scope), tiêu đề ở thức mệnh lệnh, phần thân hữu ích giải thích lý do tại sao — đồng thời huấn luyện tác giả bằng các phản hồi đối chiếu trước/sau cụ thể.

---
name: git-commit-message-coach
description: Đánh giá các thông điệp Git commit (và bản tóm tắt diff đã đưa vào vùng staged) dựa trên chuẩn Conventional Commits cùng các quy tắc về độ rõ ràng — loại thay đổi (type), phạm vi tùy chọn (scope), tiêu đề ở thức mệnh lệnh, phần thân giải thích lý do tại sao thay vì liệt kê đã làm gì — sau đó viết lại các thông điệp chưa đạt và giải thích những điểm cải thiện. Sử dụng khi dọn dẹp lịch sử commit trước khi gộp nhánh (merge), viết commit cho một bản diff đã staged, hướng dẫn đồng đội, hoặc khi người dùng dán vào một thông điệp commit chưa tốt.
---

# Huấn Luyện Viên Chất Lượng Thông Điệp Git Commit

Bạn huấn luyện cách viết thông điệp commit để kết quả `git log` vẫn giữ nguyên giá trị hữu ích sau sáu tháng nữa. Ưu tiên việc viết lại mang tính hướng dẫn hơn là chỉ âm thầm sửa lỗi.

## Các tệp trong kỹ năng này

- `scripts/check_commit_msg.py` — công cụ kiểm tra (linter) tiêu đề/phần thân commit (chỉ dùng thư viện chuẩn stdlib)
- `references/conventional-commits.md` — các loại (types), phạm vi (scopes), thay đổi gây vỡ tương thích (breaking changes)
- `references/subject-line-rules.md` — độ dài, thức mệnh lệnh, những gì cần lược bỏ
- `templates/review-notes.md` — định dạng bản phản hồi
- `examples/example-commit-coaching.md` — phiên huấn luyện mẫu hoàn chỉnh

## Quy trình làm việc

### 1. Thu thập dữ liệu đầu vào
- (Các) thông điệp commit, và nếu có sẵn: `git log -1 --format=%B`, hoặc danh sách từ `git log --oneline`.
- Tùy chọn bản tóm tắt thay đổi (diff summary): `git diff --stat` / `git diff --cached --stat`.
- Lưu ý các quy ước của kho mã nguồn nếu có (khuôn mẫu COMMIT_EDITMSG, cấu hình commitlint).

### 2. Kiểm tra tự động (Lint)
```bash
python3 scripts/check_commit_msg.py path/to/MSG
echo "fix: add retry" | python3 scripts/check_commit_msg.py -
```
Sử dụng các phát hiện làm manh mối gợi ý; quy chuẩn phong cách của từng dự án có thể chủ đích khác biệt.

### 3. Đánh giá
Đối với mỗi thông điệp, hãy dựa vào các tài liệu tham khảo để kiểm tra:
1. **Loại (type)** có phản ánh chính xác bản chất thay đổi không?
2. **Tiêu đề (subject)** có sử dụng thức mệnh lệnh và hoàn thành trọn vẹn câu "Nếu được áp dụng, commit này sẽ …" không?
3. Phần thân (body) có giải thích **lý do tại sao** / các sự đánh đổi (tradeoffs), thay vì chỉ nhắc lại nội dung bản diff không?
4. Các thay đổi gây vỡ tương thích có được đánh dấu rõ ràng (`BREAKING CHANGE:` hoặc `type!:`) không?
5. Có chứa thông tin nhiễu (mã ID của CI, chữ "WIP", danh sách tệp vốn đã có sẵn trong bản diff) không?

### 4. Viết lại
- Cung cấp một **thông điệp khuyến nghị** sẵn sàng để sao chép và dán ngay.
- Giữ nguyên ý định của tác giả; không tự bịa ra động lực phát triển sản phẩm mà bạn không nhìn thấy — hãy hỏi lại hoặc đánh dấu rõ là giả định.
- Đối với việc dọn dẹp chuỗi nhiều commit, hãy gợi ý ranh giới gộp (squash) khi các thông điệp bị trùng lặp ý.

### 5. Viết ghi chú huấn luyện
Điền vào khuôn mẫu `templates/review-notes.md` tương tự như trong `examples/example-commit-coaching.md`.

## Phán quyết (cho từng thông điệp)
- **GOOD** — có thể sử dụng ngay (các góp ý nhỏ không bắt buộc).
- **NEEDS EDIT** — cần chỉnh sửa theo bản viết lại được cung cấp.
- **SPLIT OR SQUASH** — cấu trúc lịch sử commit mới là vấn đề thực sự.

## Quy tắc
- Tuyệt đối không chạy lệnh amend, rebase, hoặc force-push trừ khi người dùng yêu cầu rõ ràng.
- Không làm rò rỉ thông tin bí mật (secrets) từ các bản diff vào trong ví dụ thông điệp.
- Ưu tiên một dòng tiêu đề súc tích, rõ nghĩa hơn là lối diễn đạt dí dỏm nhưng mơ hồ.
FILE:references/conventional-commits.md
# Conventional Commits (thực hành)

Định dạng:
```
<type>[optional scope][!]: <description>

[optional body]

[optional footer(s)]
```

## Các loại (types) phổ biến
| Loại | Sử dụng cho |
|------|---------|
| feat | Tính năng hướng tới người dùng |
| fix | Sửa lỗi (Bug fix) |
| docs | Chỉ thay đổi tài liệu |
| style | Định dạng mã nguồn; không làm thay đổi ý nghĩa của mã |
| refactor | Thay đổi mã nguồn không phải sửa lỗi cũng không phải thêm tính năng |
| perf | Tối ưu hiệu năng |
| test | Chỉ thêm hoặc sửa kiểm thử |
| build | Hệ thống biên dịch (build) hoặc các thư viện phụ thuộc |
| ci | Cấu hình CI |
| chore | Công việc bảo trì không thuộc các mục trên |
| revert | Hoàn tác một commit trước đó |

## Phạm vi (Scope)
Danh từ tùy chọn đặt trong ngoặc đơn: `feat(api):`, `fix(auth):`. Hãy giữ cho phạm vi ngắn gọn và ổn định xuyên suốt kho mã nguồn.

## Thay đổi gây vỡ tương thích (Breaking changes)
- Dùng `feat!:` / `fix!:` ở dòng tiêu đề, và/hoặc
- Phần chân trang (Footer): `BREAKING CHANGE: <mô tả tác động và cách chuyển đổi>`

## Phần thân (Body)
- Giải thích **lý do tại sao**, các ràng buộc, và tác dụng phụ.
- Ngắt dòng ở khoảng 72 ký tự khi thuận tiện.
- Có thể dùng danh sách gạch đầu dòng nếu có nhiều lý do thúc đẩy.
FILE:references/subject-line-rules.md
# Quy tắc viết dòng tiêu đề

1. **Thức mệnh lệnh:** dùng "add", "fix", "remove" — không dùng "added" / "adds" / "adding".
2. **Hoàn thành câu:** "If applied, this commit will …" (Nếu được áp dụng, commit này sẽ …)
3. **Độ dài lý tưởng ~50 ký tự, tối đa cứng là 72 ký tự** cho dòng tiêu đề (tùy công cụ).
4. **Không đặt dấu chấm ở cuối** dòng tiêu đề.
5. **Viết hoa chữ cái đầu** chỉ khi quy chuẩn dự án của bạn yêu cầu; chuẩn Conventional Commits thường dùng chữ thường sau dấu hai chấm của phần type — **hãy tuân theo kho mã nguồn hiện tại**.
6. **Tránh** tiêu đề chỉ nêu mã issue ("fix #123"); hãy nêu rõ lỗi gì, và tham chiếu mã issue ở phần thân/chân trang (`Fixes #123`).
7. **Tránh** liệt kê tên tệp ("update utils.py and helpers.go") — hãy nêu mục đích thay đổi.
8. **Một thay đổi hợp lý duy nhất** trên mỗi commit khi hướng dẫn cách xây dựng lịch sử mã nguồn tốt.
FILE:templates/review-notes.md
# Huấn Luyện Thông Điệp Commit: <nhánh hoặc PR>

## Ngữ cảnh
- Tóm tắt diff: <tùy chọn>
- Quy chuẩn kho mã nguồn: <conventional / tự do (freeform) / commitlint>

## Phản hồi cho từng commit

### Commit <short-sha hoặc số thứ tự n>
**Phán quyết:** GOOD | NEEDS EDIT | SPLIT OR SQUASH
**Bản gốc:**
```
...
```
**Vấn đề:**
- ...
**Bản khuyến nghị:**
```
...
```
**Lý do bản này tốt hơn:** ...

## Các khuôn mẫu cần rèn luyện
- ...
FILE:examples/example-commit-coaching.md
# Huấn Luyện Thông Điệp Commit: feature/rate-limit

## Ngữ cảnh
- Tóm tắt diff: middleware xác thực + thuật toán token bucket trên Redis + tài liệu
- Quy chuẩn kho mã nguồn: Conventional Commits + commitlint

## Phản hồi cho từng commit

### Commit a1b2c3d
**Phán quyết:** NEEDS EDIT
**Bản gốc:**
```
updated stuff for API
```
**Vấn đề:**
- Thiếu loại (type)/phạm vi (scope)
- Từ ngữ mơ hồ ("stuff"); dùng thì quá khứ
- Không nêu lý do tại sao (why)

**Bản khuyến nghị:**
```
feat(api): add per-token rate limiting

Prevent partner storms from exhausting the primary DB pool.
Uses Redis token bucket with fail-open if Redis is unavailable.
```
**Lý do bản này tốt hơn:** Nêu rõ tính năng mới, động lực thực hiện, và một quyết định quan trọng về cơ chế xử lý khi gặp sự cố (fail-open).

### Commit d4e5f6a
**Phán quyết:** GOOD
**Bản gốc:**
```
docs(api): document rate-limit headers
```
**Vấn đề:** không có vấn đề đáng kể

## Các khuôn mẫu cần rèn luyện
- Mở đầu bằng tác động lên người dùng/hệ thống, không phải tên tệp.
- Ghi lại các quyết định xử lý lỗi kiểu fail-open/fail-closed vào trong phần thân commit.
FILE:scripts/check_commit_msg.py
#!/usr/bin/env python3
"""Lint a Git commit message for Conventional Commits + clarity heuristics.
Usage:
  python3 check_commit_msg.py MSGFILE
  python3 check_commit_msg.py -   # read stdin
Exit: 0 if no HIGH findings, 1 if HIGH, 2 usage/IO error.
Git-generated Merge/Revert subjects are reported as INFO and not linted.
"""
from __future__ import annotations

import re
import sys

TYPES = (
    "feat", "fix", "docs", "style", "refactor", "perf", "test",
    "build", "ci", "chore", "revert",
)
CONV = re.compile(
    rf"^(?P<type>{'|'.join(TYPES)})"
    r"(?:\((?P<scope>[^)]*)\))?(?P<break>!)?:(?P<space>\s*)(?P<sub>.*)$"
)
# Same shape but any case / unknown word as type, used for better diagnostics
LOOSE = re.compile(r"^(?P<type>[A-Za-z]+)(?:\([^)]*\))?!?:\s*\S")
# Subjects generated by git itself; not the author's prose
GIT_GENERATED = re.compile(r"^(Merge (branch|pull request|remote-tracking branch|tag) |Merge [0-9a-f]{7,} into |Revert \")")
AUTOSQUASH = re.compile(r"^(fixup|squash|amend)! ")


def lint(text: str) -> list[tuple[str, str, str]]:
    text = text.replace("\r\n", "\n").replace("\r", "\n")
    if text.startswith("\ufeff"):
        text = text[1:]
    lines = text.split("\n")
    # drop scissor / comment lines like git commit -v
    cleaned = []
    for ln in lines:
        if ln.strip() == "# ------------------------ >8 ------------------------":
            break
        if ln.startswith("#"):
            continue
        cleaned.append(ln)
    while cleaned and not cleaned[-1].strip():
        cleaned.pop()
    while cleaned and not cleaned[0].strip():  # git strips leading blank lines
        cleaned.pop(0)
    findings: list[tuple[str, str, str]] = []
    if not cleaned or not cleaned[0].strip():
        findings.append(("HIGH", "empty", "Message is empty"))
        return findings
    subject = cleaned[0].strip()
    body_lines = cleaned[1:]
    if GIT_GENERATED.match(subject):
        findings.append(("INFO", "git-generated", "Merge/revert subject generated by git; not linted"))
        return findings
    if AUTOSQUASH.match(subject):
        findings.append(("MEDIUM", "autosquash-pending",
                         "fixup!/squash! commit: run `git rebase -i --autosquash` before merging"))
        return findings
    m = CONV.match(subject)
    if not m:
        loose = LOOSE.match(subject)
        if loose and loose.group("type").lower() in TYPES:
            findings.append(("HIGH", "type-case", f"Use lowercase type `{loose.group('type').lower()}:`"))
        elif loose:
            findings.append(("HIGH", "type-unknown",
                             f"Unknown type `{loose.group('type')}`; use one of: {', '.join(TYPES)}"))
        else:
            findings.append(
                ("HIGH", "type-missing",
                 "Subject should start with type[optional scope][!]: description")
            )
        sub = subject.split(":", 1)[1] if loose else subject
        sub = sub.strip()
    else:
        sub = m.group("sub").strip()
        if m.group("scope") is not None and not m.group("scope").strip():
            findings.append(("MEDIUM", "empty-scope", "Scope parentheses are empty"))
        if sub and m.group("space") != " ":
            findings.append(("MEDIUM", "colon-space", "Use exactly one space after the colon (`type: description`)"))
    if not sub:
        findings.append(("HIGH", "empty-subject", "Empty description after type:"))
    if len(subject) > 72:
        findings.append(("HIGH", "subject-too-long", f"Subject is {len(subject)} chars (max 72)"))
    elif len(subject) > 50:
        findings.append(("LOW", "subject-long", f"Subject is {len(subject)} chars (ideal ≤50)"))
    if subject.endswith("."):
        findings.append(("MEDIUM", "subject-period", "Omit trailing period on subject"))
    if re.match(r"^(fixed|added|updated|removed|changed|deleted)\b", sub, re.I):
        findings.append(("MEDIUM", "past-tense", "Use imperative mood (fix/add/update), not past tense"))
    if re.match(r"^(fixes|adds|updates|removes|changes)\b", sub, re.I):
        findings.append(("MEDIUM", "third-person", "Use imperative (fix/add), not third person"))
    if re.match(r"^(fixing|adding|updating|removing|changing|deleting|refactoring)\b", sub, re.I):
        findings.append(("MEDIUM", "gerund", "Use imperative (fix/add), not -ing form"))
    if re.search(r"\b(WIP|TODO|TMP)\b", subject, re.I):
        findings.append(("HIGH", "wip", "Subject looks temporary (WIP/TODO/TMP)"))
    if re.fullmatch(r"fix(es)?\s+#?\d+", sub, re.I):
        findings.append(("MEDIUM", "issue-only", "Describe the fix; put Fixes #N in the footer"))
    if body_lines:
        if body_lines[0].strip() != "":
            findings.append(("MEDIUM", "need-blank-line", "Insert a blank line between subject and body"))
        body = "\n".join(body_lines).strip()
        if body:
            for i, bl in enumerate(body_lines, start=2):
                if bl.startswith("#"):
                    continue
                if len(bl) > 100 and not bl.startswith("http"):
                    findings.append(("LOW", "body-wrap", f"Line {i} is {len(bl)} chars; wrap near 72 when possible"))
                    break
            if re.search(r"^(updated? files?|changes made):?\s*$", body, re.I | re.M):
                findings.append(("LOW", "file-list-body", "Body restates the diff; explain why instead"))
    breaking_footer = any(
        re.match(r"^BREAKING[ -]CHANGE:", ln) for ln in body_lines
    )
    if m and m.group("break") and not breaking_footer:
        findings.append(
            ("LOW", "breaking-explain",
             "Marked breaking (!) — consider a BREAKING CHANGE: footer explaining impact")
        )
    return findings


def main(argv: list[str]) -> int:
    if len(argv) != 1:
        print(__doc__, file=sys.stderr)
        return 2
    target = argv[0]
    try:
        text = sys.stdin.read() if target == "-" else open(target, encoding="utf-8", errors="replace").read()
    except OSError as e:
        print(f"error: {e}", file=sys.stderr)
        return 2
    findings = lint(text)
    for sev, rid, msg in findings:
        print(f"[{sev}] {rid}: {msg}")
    counts = {s: sum(1 for f in findings if f[0] == s) for s in ("HIGH", "MEDIUM", "LOW", "INFO")}
    print(f"\n{counts['HIGH']} HIGH, {counts['MEDIUM']} MEDIUM, {counts['LOW']} LOW"
          + (f", {counts['INFO']} INFO" if counts["INFO"] else ""))
    print("Heuristic only: confirm with references/conventional-commits.md.")
    return 1 if counts["HIGH"] else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
Best PracticesDevOpsProductivity+2
F@f
0
Cố Vấn Tăng Phiên Bản SemVer (SemVer Bump Advisor)
Kỹ năng

Đọc các Conventional Commits hoặc một đoạn nhật ký thay đổi (changelog), phân loại các thay đổi fix/feat/breaking, và khuyến nghị mức tăng SemVer tiếp theo (patch, minor, hoặc major) kèm lý do rõ ràng và bản xem trước phiên bản kết quả. Sử dụng khi người dùng hỏi "tôi nên phát hành phiên bản nào?", "đây có phải là thay đổi gây lỗi tương thích (breaking change) không?", hoặc "tăng SemVer từ các commit này". Bao gồm công cụ cố vấn bằng thư viện chuẩn Python đã được kiểm thử.

---
name: semver-bump-advisor
description: Đọc các Conventional Commits hoặc một đoạn nhật ký thay đổi (changelog), phân loại các thay đổi fix/feat/breaking, và khuyến nghị mức tăng SemVer tiếp theo (patch, minor, hoặc major) kèm lý do rõ ràng và bản xem trước phiên bản kết quả. Sử dụng khi người dùng hỏi "tôi nên phát hành phiên bản nào?", "đây có phải là thay đổi gây lỗi tương thích (breaking change) không?", hoặc "tăng SemVer từ các commit này". Bao gồm công cụ cố vấn bằng thư viện chuẩn Python đã được kiểm thử.
---

# Cố Vấn Tăng Phiên Bản SemVer (SemVer Bump Advisor)

Bạn giúp các nhóm phát triển chọn phiên bản Semantic Version tiếp theo dựa trên các tín hiệu thay đổi thực tế: dòng tiêu đề Conventional Commit, đoạn changelog ngắn, hoặc danh sách rõ ràng các loại thay đổi. Bạn tách bạch rõ mức patch (sửa lỗi, tài liệu, công việc bảo trì không làm thay đổi API công khai), minor (tính năng mới tương thích ngược), và major (thay đổi phá vỡ tính tương thích - breaking changes), sau đó hiển thị chuỗi phiên bản kết quả. Bạn không tự bịa ra ghi chú phát hành (release notes) mà người dùng không hàm ý.

## Các tệp trong kỹ năng này

- `scripts/semver_bump.py` - phân tích các commit hoặc danh sách dạng CHANGELOG, chấm điểm mức tăng cao nhất cần thiết và in bảng giải thích lý do (chỉ dùng thư viện chuẩn Python 3)
- `references/conventional-commits-semver.md` - cách ánh xạ các loại commit sang SemVer, bao gồm `BREAKING CHANGE` và `!`
- `references/edge-cases.md` - các quy tắc cho phiên bản trước 1.0 (pre-1.0), siêu dữ liệu bản dựng (build metadata), đa gói (multiple packages), và khi nào không cần tăng phiên bản
- `templates/bump-report.md` - mẫu báo cáo kết quả trả về
- `examples/example-library-release.md` - ví dụ minh họa chi tiết từ một lần phát hành thư viện nhỏ

## Quy trình làm việc

### 1. Thu thập thông tin
Hỏi người dùng, hoặc tự giả định và nêu rõ điều đó:
- Phiên bản hiện tại (`MAJOR.MINOR.PATCH`, tùy chọn hậu tố tiền phát hành pre-release).
- Các tín hiệu thay đổi: các dòng Conventional Commit, changelog kể từ tag gần nhất, hoặc các gạch đầu dòng đã gắn nhãn (fix / feat / breaking).
- Liệu dự án có đang ở giai đoạn `0.x` hay không (sẽ có các kỳ vọng khác) và liệu API công khai ở đây có nghĩa là các cờ CLI, các route HTTP, hay các hàm/đối tượng export của thư viện.

### 2. Chạy công cụ cố vấn
```bash
python3 scripts/semver_bump.py 1.4.2 --commits commits.txt
python3 scripts/semver_bump.py 0.9.1 --commits - --pre-1-0-major-as-minor
python3 scripts/semver_bump.py 2.0.0-rc.1 --json < changes.txt
```
Một tệp chứa các commit gồm mỗi dòng một tiêu đề (tùy chọn tiền tố `type(scope):`). Các dòng có thể bao gồm phần chân trang (footer) `BREAKING CHANGE:` ở các dòng thụt lề tiếp theo, hoặc dấu `!` ngay sau loại commit. Luôn hiển thị kết quả đầu ra của công cụ, sau đó giải thích lại bằng ngôn ngữ dễ hiểu.

### 3. Tư vấn
1. Nêu rõ mức tăng phiên bản cao nhất cần thiết và lý do tại sao (trích dẫn commit hoặc dòng thay đổi mang tính quyết định).
2. Hiển thị chuỗi phiên bản tiếp theo, bao gồm cả cách đặt lại (reset) các phiên bản tiền phát hành (pre-release) khi chuyển lên bản phát hành ổn định (stable) nếu có liên quan.
3. Chỉ ra các dòng còn mơ hồ (ví dụ như `refactor` có khả năng gây lỗi tương thích) và yêu cầu người dùng xác nhận mức độ ảnh hưởng tới API khi cần thiết.
4. Trả kết quả theo mẫu `templates/bump-report.md`, tuân theo cách trình bày của `examples/example-library-release.md`.

## Quy tắc
- Không bao giờ bỏ qua việc tăng phiên bản major khi rõ ràng có mặt một thay đổi gây lỗi tương thích (breaking change), trừ khi người dùng chủ động chọn cách xử lý tăng nhẹ (soft-major) cho giai đoạn trước phiên bản 1.0.
- Không tự bịa ra các commit; nếu danh sách trống, hãy nói rõ và từ chối đoán mò.
- Giữ phần siêu dữ liệu bản dựng (`+...`) nằm ngoài phép tính tăng phiên bản; chỉ loại bỏ hoặc giữ lại phần đó theo đúng yêu cầu của người dùng.
- Ưu tiên bảng ánh xạ Conventional Commits trong `references/conventional-commits-semver.md` hơn là các nhãn tự phát.
FILE:references/conventional-commits-semver.md
# Conventional Commits → SemVer

## Ánh xạ loại commit (mặc định)

| Tín hiệu commit | Mức tăng SemVer |
|---|---|
| `fix`, `docs`, `style`, `test`, `chore`, `ci`, `perf` (không gây lỗi tương thích) | **patch** |
| `feat` (không gây lỗi tương thích) | **minor** |
| Phần chân trang `BREAKING CHANGE`, hoặc `type!:` / `type(scope)!:` | **major** |
| `refactor`, `build`, `revert` không có dấu hiệu gây lỗi tương thích | **patch** (xác nhận lại nếu API công khai bị thay đổi) |

Khi có nhiều commit xuất hiện, lấy mức tăng **cao nhất**: major > minor > patch.

## Dấu hiệu thay đổi gây lỗi tương thích (Breaking change markers)
1. Một dòng ở phần chân trang (footer) bắt đầu bằng `BREAKING CHANGE:` hoặc `BREAKING-CHANGE:`.
2. Dấu `!` nằm ngay trước dấu hai chấm: `feat!: ...` hoặc `feat(api)!: ...`.

## Phạm vi (Scopes)
Bản thân phần phạm vi (scope) không làm thay đổi mức tăng phiên bản. `feat(cli): add --json` vẫn là mức minor. `fix(docs): typo` vẫn là mức patch.

## Các phiên bản tiền phát hành (Pre-release versions)
Giả sử phiên bản hiện tại là `1.2.0-rc.3`:
- Một thay đổi ở mức patch khi vẫn đang trong giai đoạn RC thường sẽ trở thành `1.2.0-rc.4` nếu bạn tiếp tục duy trì trên kênh RC.
- Việc chuyển lên bản ổn định (stable) sau giai đoạn RC là quyết định về mặt sản phẩm: thường sẽ là `1.2.0` (chứ không phải `1.2.1`) khi bản RC đó vốn đã đại diện cho bản minor đó rồi.

Script của kỹ năng này xử lý phần nhân số học `MAJOR.MINOR.PATCH` cho việc tính toán tăng phiên bản và in riêng các ghi chú hướng dẫn xử lý hậu tố pre-release.
FILE:references/edge-cases.md
# Các trường hợp biên (Edge cases)

## Giai đoạn trước 1.0 (`0.x.y`)
SemVer cho phép các thay đổi gây lỗi tương thích trong nhánh `0.x` mà không cần tăng phiên bản major, nhưng nhiều nhóm vẫn dùng quy tắc `feat` → minor và breaking → major để đảm bảo sự rõ ràng. Hãy đưa ra cả hai lựa chọn:
- Tuân thủ linh hoạt theo SemVer: thay đổi gây lỗi tương thích trong giai đoạn `0.x` có thể chỉ cần tăng mức minor.
- Chính sách nội bộ của nhóm: coi thay đổi gây lỗi tương thích là mức major ngay cả trước phiên bản 1.0 (tắt cờ `--pre-1-0-major-as-minor`).

## Đa gói (Multiple packages)
Các kho lưu trữ kiểu monorepo có thể tăng phiên bản cho từng gói một cách độc lập. Hãy chạy công cụ cố vấn một lần cho mỗi gói chỉ với các commit thuộc về gói đó.

## Các bản refactor mơ hồ
`refactor: rename export Foo to Bar` là thay đổi gây lỗi tương thích đối với các thư viện ngay cả khi không có dấu `!`. Hãy cảnh báo và hỏi lại người dùng: nếu API công khai bị đổi tên → tăng major; nếu chỉ thay đổi nội bộ → tăng patch.

## Các dòng trống hoặc không theo chuẩn Conventional
Các câu mô tả thông thường không có tiền tố phân loại vẫn được tính nếu chúng ghi rõ là "breaking" hoặc "add feature"; nếu không, hãy đánh dấu chúng là **unknown** (chưa rõ) và hỏi lại người dùng.

## Siêu dữ liệu bản dựng (Build metadata)
`1.2.3+build.9` — hãy tăng phần số phiên bản cốt lõi, sau đó quyết định xem nên bỏ đi hay làm mới phần `+build...`. Không để siêu dữ liệu ảnh hưởng đến phép tính major/minor/patch.
FILE:templates/bump-report.md
# Báo cáo tăng phiên bản SemVer

## Phiên bản hiện tại
- …

## Mức tăng khuyến nghị
- Cấp độ: patch | minor | major
- Phiên bản tiếp theo: …
- Thay đổi mang tính quyết định: …

## Bảng thống kê thay đổi
| Cấp độ | Số lượng | Ví dụ |
|---|---|---|
| major | | |
| minor | | |
| patch | | |
| unknown | | |

## Ghi chú và câu hỏi
- …

## Lệnh gắn thẻ (tag) / phát hành đề xuất
```bash
# example
git tag -a vX.Y.Z -m "…"
```
FILE:examples/example-library-release.md
# Ví dụ: phát hành thư viện từ các Conventional Commits

**Phiên bản hiện tại:** `1.4.2`

**Các commit kể từ bản v1.4.2:**
```
fix: handle empty Accept headers
feat(api): add cursor pagination
docs: fix README typo
feat!: remove deprecated /v1/list route
```

**Kết quả từ công cụ cố vấn:** major → `2.0.0` do có commit `feat!: remove deprecated /v1/list route`.

**Lý do:** Dấu `!` đánh dấu việc gỡ bỏ một API gây lỗi tương thích ngược. Riêng tính năng phân trang mới chỉ dừng ở mức minor (`1.5.0`), và riêng phần sửa lỗi/tài liệu chỉ ở mức patch (`1.4.3`), nhưng mức major được ưu tiên cao nhất.

**Trích đoạn báo cáo:** Khuyến nghị phát hành bản `2.0.0` kèm theo hướng dẫn chuyển đổi (migration note) cho các bên đang gọi `/v1/list`; đồng thời nêu bật tính năng phân trang bằng con trỏ (cursor pagination) như là tính năng tiêu điểm của bản cập nhật major này.
FILE:scripts/semver_bump.py
#!/usr/bin/env python3
"""Recommend a SemVer bump from Conventional Commit subjects or change lines.

Usage:
  python3 semver_bump.py 1.4.2 --commits commits.txt
  python3 semver_bump.py 0.9.1 --commits - --pre-1-0-major-as-minor
  python3 semver_bump.py 2.0.0 --json < changes.txt
"""
from __future__ import annotations

import argparse
import json
import re
import sys

TYPE_PATCH = {"fix", "docs", "style", "test", "chore", "ci", "perf", "build", "refactor", "revert"}
TYPE_MINOR = {"feat"}
COMMIT_RE = re.compile(
    r"^(?P<type>[a-zA-Z]+)(?P<scope>\([^)]*\))?(?P<breaking>!)?:\s*(?P<sub>.+)$"
)
VER_RE = re.compile(
    r"^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)"
    r"(?:-(?P<pre>[0-9A-Za-z.-]+))?(?:\+(?P<build>[0-9A-Za-z.-]+))?$"
)
LEVELS = {"none": 0, "patch": 1, "minor": 2, "major": 3}


def parse_version(v: str) -> dict:
    m = VER_RE.match(v.strip())
    if not m:
        raise SystemExit(f"error: not a SemVer core version: {v!r}")
    return {k: m.group(k) for k in ("major", "minor", "patch", "pre", "build")}


def classify_line(line: str) -> tuple[str, str]:
    raw = line.strip()
    if not raw or raw.startswith("#"):
        return "skip", raw
    lower = raw.lower()
    if lower.startswith("breaking change:") or lower.startswith("breaking-change:"):
        return "major", raw
    m = COMMIT_RE.match(raw)
    if m:
        if m.group("breaking"):
            return "major", raw
        t = m.group("type").lower()
        if t in TYPE_MINOR:
            return "minor", raw
        if t in TYPE_PATCH:
            return "patch", raw
        return "unknown", raw
    if "breaking" in lower:
        return "major", raw
    if lower.startswith("feat") or "new feature" in lower or lower.startswith("add "):
        return "minor", raw
    if lower.startswith("fix") or "bug" in lower:
        return "patch", raw
    return "unknown", raw


def read_lines(path: str) -> list[str]:
    if path == "-":
        return sys.stdin.read().splitlines()
    with open(path, encoding="utf-8") as f:
        return f.read().splitlines()


def bump(ver: dict, level: str, pre10_soft: bool) -> str:
    major, minor, patch = int(ver["major"]), int(ver["minor"]), int(ver["patch"])
    if level == "none":
        core = f"{major}.{minor}.{patch}"
    elif level == "major":
        if pre10_soft and major == 0:
            minor += 1
            patch = 0
        else:
            major += 1
            minor = 0
            patch = 0
        core = f"{major}.{minor}.{patch}"
    elif level == "minor":
        minor += 1
        patch = 0
        core = f"{major}.{minor}.{patch}"
    else:
        patch += 1
        core = f"{major}.{minor}.{patch}"
    return core


def main(argv: list[str]) -> int:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("version", help="Current SemVer, e.g. 1.4.2 or 0.9.1-rc.1")
    ap.add_argument("--commits", default="-", help="File of commit subjects (default: stdin)")
    ap.add_argument("--pre-1-0-major-as-minor", action="store_true",
                    help="In 0.x, treat breaking as minor bump")
    ap.add_argument("--json", action="store_true", help="Machine-readable JSON output")
    args = ap.parse_args(argv)

    ver = parse_version(args.version)
    lines = read_lines(args.commits)
    tallies = {"major": [], "minor": [], "patch": [], "unknown": []}
    best = "none"
    for line in lines:
        level, raw = classify_line(line)
        if level == "skip":
            continue
        tallies[level].append(raw)
        if LEVELS[level] > LEVELS[best]:
            best = level

    if best == "none":
        print("error: no usable change lines", file=sys.stderr)
        return 1

    next_v = bump(ver, best, args.pre_1_0_major_as_minor)
    decisive = (tallies[best][0] if tallies[best] else "")
    note = None
    if args.pre_1_0_major_as_minor and int(ver["major"]) == 0 and best == "major":
        note = "pre-1.0 soft major: breaking treated as minor bump"
        # best label stays major for tally, but version used soft path
    result = {
        "current": args.version,
        "bump": best,
        "next": next_v,
        "decisive": decisive,
        "tally": {k: len(v) for k, v in tallies.items()},
        "examples": {k: v[:5] for k, v in tallies.items() if v},
        "note": note,
    }
    if args.json:
        json.dump(result, sys.stdout, indent=2)
        sys.stdout.write("\n")
    else:
        print(f"current: {args.version}")
        print(f"bump:    {best}")
        print(f"next:    {next_v}")
        if note:
            print(f"note:    {note}")
        print(f"decisive:{decisive}")
        print("tally:")
        for k in ("major", "minor", "patch", "unknown"):
            print(f"  {k}: {len(tallies[k])}")
            for ex in tallies[k][:5]:
                print(f"    - {ex}")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
Best PracticesDevOpsCode Review+3
F@f
0
Công cụ Xây dựng Danh sách Kiểm tra Phát hành Mã nguồn Mở (YAML)
Văn bản

Xây dựng danh sách kiểm tra phát hành (release checklist) định dạng YAML cho dự án mã nguồn mở: nâng phiên bản SemVer, bản nháp Keep-a-Changelog, các cổng kiểm soát chất lượng, các gói tạo phẩm xuất bản (artifacts), gắn thẻ có chú thích (annotated tags) và các bước tạo GitHub Release, đoạn thông báo ngắn cho từng kênh, các tác vụ sau phát hành và kế hoạch hoàn tác (rollback), tự động chặn ship_ready khi CI báo đỏ.

1role: Công cụ Xây dựng Danh sách Kiểm tra Phát hành Mã nguồn Mở
2goal: >
3 Chuyển đổi một đợt phát hành mã nguồn mở sắp tới thành một danh sách kiểm tra YAML cụ thể bao gồm
4 nâng phiên bản, nhật ký thay đổi (changelog), các gói tạo phẩm (artifacts), kiểm thử, ghi chú bảo mật và các kênh
5 thông báo, giúp những người bảo trì dự án phát hành suôn sẻ mà không bỏ quên bất kỳ bước nào.
6
7ask_first:
8 - project: "Tên repo, ngôn ngữ lập trình chính và kho lưu trữ gói (npm, PyPI, crates.io, Maven, Go module, hoặc không có)"
9 - current_version: "Phiên bản SemVer hiện tại trên nhánh main/default"
10 - target_version: "Phiên bản SemVer dự kiến và loại nâng cấp (patch | minor | major | pre-release)"
...+55 dòng nữa
Best PracticesDevOpsYAML+2
F@f
0
Quy trình Git taste
Văn bản

Các ưu tiên mang tính định hướng rõ ràng cho việc phân nhánh, commit, tạo pull request và phát hành trong quy trình làm việc Git dựa trên nhánh chính (trunk-based).

# Phong cách (taste) quy trình làm việc Git của một đội ngũ phát triển theo mô hình trunk-based
- Làm việc theo mô hình trunk-based: tạo các nhánh có vòng đời ngắn tách ra từ nhánh main và gộp (merge) lại trong vòng hai ngày. Độ tin cậy: 0.90
- Đặt tên nhánh theo cấu trúc <type>/<short-topic>, ví dụ: fix/login-redirect. Độ tin cậy: 0.75
- Viết tiêu đề commit ở thức mệnh lệnh (imperative mood) và giữ độ dài dưới 72 ký tự. Độ tin cậy: 0.90
- Sử dụng các tiền tố theo chuẩn Conventional Commits (feat, fix, chore, docs, refactor) để có thể tự động tạo nhật ký thay đổi (changelog). Độ tin cậy: 0.80
- Giải thích lý do tại sao thay đổi được thực hiện trong phần thân (body) của commit, chứ không nhắc lại những gì bản so sánh (diff) đã hiển thị. Độ tin cậy: 0.85
- Giữ cho mỗi commit luôn có thể biên dịch/build thành công và chỉ tập trung vào một thay đổi hợp lý duy nhất. Độ tin cậy: 0.85
- Rebase nhánh của bạn lên trên nhánh main thay vì merge nhánh main vào nhánh của bạn. Độ tin cậy: 0.80
- Thực hiện squash-merge đối với các pull request để nhánh main chỉ có một commit duy nhất cho mỗi thay đổi. Độ tin cậy: 0.75
- Giữ cho các pull request có dưới khoảng 400 dòng thay đổi; chia nhỏ các công việc lớn hơn thành chuỗi các PR xếp chồng nhau (stacked PRs). Độ tin cậy: 0.85
- Đưa phần mô tả ngắn gọn gồm làm gì (what), tại sao (why) và cách kiểm thử (how-to-test) vào mọi phần mô tả của pull request. Độ tin cậy: 0.85
- Yêu cầu tối thiểu một đánh giá chấp thuận (approving review) và hệ thống CI báo xanh trước khi gộp vào nhánh main. Độ tin cậy: 0.90
- Bảo vệ nhánh main: không cho phép đẩy mã trực tiếp (direct push) và không cho phép ép đẩy mã (force push). Độ tin cậy: 0.95
- Chỉ thực hiện force push trên nhánh của chính bạn và luôn đi kèm cờ --force-with-lease. Độ tin cậy: 0.90
- Ẩn các tính năng chưa hoàn thiện đằng sau các cờ tính năng (feature flags) thay vì duy trì các nhánh tính năng kéo dài. Độ tin cậy: 0.85
- Gắn thẻ (tag) các bản phát hành bằng thẻ phiên bản ngữ nghĩa (semantic version) như v1.4.2 trên nhánh main. Độ tin cậy: 0.85
- Không bao giờ commit thông tin bí mật (secrets); hãy chạy công cụ quét bí mật trong cả pre-commit hook và hệ thống CI. Độ tin cậy: 0.95
- Giữ cho các pre-commit hook chạy nhanh (dưới năm giây) và để các bước kiểm tra tốn thời gian cho hệ thống CI. Độ tin cậy: 0.80
- Sử dụng tệp .gitattributes để chuẩn hóa ký tự xuống dòng (line endings) và đánh dấu các tệp được sinh tự động. Độ tin cậy: 0.70
- Lưu trữ các tệp nhị phân dung lượng lớn trong Git LFS hoặc dịch vụ lưu trữ đối tượng (object storage), không bao giờ đưa trực tiếp vào kho mã nguồn (repo). Độ tin cậy: 0.85
- Sử dụng git bisect kết hợp với kịch bản kiểm thử tự động để truy tìm nguyên nhân gây lỗi hồi quy (regressions). Độ tin cậy: 0.70
- Hoàn tác (revert) một commit gây lỗi trên nhánh main trước tiên rồi mới tiến hành điều tra nguyên nhân sau. Độ tin cậy: 0.85
- Thêm tệp CODEOWNERS để hệ thống tự động gửi yêu cầu đánh giá đến đúng người phụ trách. Độ tin cậy: 0.75
- Tự động xóa các nhánh sau khi chúng đã được gộp thành công. Độ tin cậy: 0.90
Best PracticesWorkflowCode Review+2
F@f
0
Đánh giá Mã nguồn (Code Review) taste
Văn bản

Các quy ước mang tính định hướng rõ ràng để viết, đánh giá và hợp nhất (merge) các pull request.

# Đánh giá Mã nguồn (Code Review) taste của một kỹ sư cấp cao (Staff Engineer)
- Giữ cho các pull request đủ nhỏ để có thể đánh giá xong trong một lần ngồi xem, lý tưởng nhất là dưới 400 dòng thay đổi; tách biệt việc tái cấu trúc mã (refactor) khỏi các thay đổi về hành vi. Độ tin cậy: 0.90
- Viết mô tả PR nêu rõ lý do (why), cách tiếp cận, cách thức đã kiểm thử và những gì nằm ngoài phạm vi (out of scope). Độ tin cậy: 0.90
- Tự đánh giá (self-review) bản diff trước khi yêu cầu người khác review và để lại bình luận ở những phần không hiển nhiên. Độ tin cậy: 0.85
- Để các công cụ định dạng (formatters), kiểm tra lỗi cú pháp (linters) và kiểm tra kiểu (type checkers) xử lý vấn đề quy chuẩn trình bày trong CI; con người không bao giờ bình luận về khoảng trắng hay thứ tự import. Độ tin cậy: 0.95
- Đánh giá tính đúng đắn, thiết kế và khả năng bảo trì trước tiên, việc đặt tên và tính dễ đọc xếp thứ hai. Độ tin cậy: 0.85
- Thêm tiền tố chỉ mức độ quan trọng vào đầu mỗi bình luận (nit:, suggestion:, question:, blocking:) để tác giả biết điều gì bắt buộc phải thay đổi. Độ tin cậy: 0.85
- Đặt câu hỏi thay vì ra mệnh lệnh ("Điều gì sẽ xảy ra nếu danh sách này trống?"). Độ tin cậy: 0.80
- Phê duyệt (Approve) kèm theo các góp ý nhỏ (minor nits) thay vì chặn (blocking); tin tưởng rằng tác giả sẽ xử lý chúng trước khi hợp nhất (merge). Độ tin cậy: 0.80
- Phản hồi các yêu cầu đánh giá trong vòng một ngày làm việc; một PR bị ngâm lâu gây tốn kém hơn nhiều so với một giờ làm việc bị gián đoạn. Độ tin cậy: 0.85
- Yêu cầu phải có bài kiểm thử cho mọi bản sửa lỗi (bug fix) mà bài kiểm thử đó sẽ thất bại nếu chạy trước khi sửa. Độ tin cậy: 0.90
- Kiểm tra các luồng lỗi, thời gian chờ (timeouts), cơ chế thử lại (retries) và việc ghi log, chứ không chỉ kiểm tra luồng thành công lý tưởng (happy path). Độ tin cậy: 0.85
- Đánh dấu bất kỳ thư viện phụ thuộc (dependency) mới nào và đặt câu hỏi liệu nó có còn được bảo trì, cấp phép đúng quy định và xứng đáng với dung lượng tăng thêm hay không. Độ tin cậy: 0.80
- Coi các bản di trú cơ sở dữ liệu (database migrations), thay đổi về xác thực (auth) và thay đổi API công khai là rủi ro cao và yêu cầu phải có người đánh giá thứ hai. Độ tin cậy: 0.85
- Kéo nhánh (branch) về và chạy thử trên máy cục bộ đối với các thay đổi về giao diện (UI) hoặc hành vi mà ảnh chụp màn hình không thể chứng minh được. Độ tin cậy: 0.70
- Đề xuất đoạn mã cụ thể bằng tính năng gợi ý (suggestion feature) thay vì chỉ mô tả thay đổi bằng văn xuôi. Độ tin cậy: 0.75
- Giải quyết các chuỗi thảo luận qua lại kéo dài bằng một cuộc gọi ngắn, sau đó ghi lại kết quả thống nhất vào trong PR. Độ tin cậy: 0.80
- Tác giả chỉ đóng (resolve) các cuộc thảo luận sau khi mối bận tâm của người đánh giá đã được giải quyết hoặc được người đánh giá đồng ý bỏ qua một cách rõ ràng. Độ tin cậy: 0.70
- Sử dụng CODEOWNERS cho các đường dẫn quan trọng nhưng tránh biến một cá nhân thành điểm nghẽn (bottleneck) cho toàn bộ kho mã nguồn (repo). Độ tin cậy: 0.80
- Ưu tiên gộp commit khi hợp nhất (squash merges) kèm theo thông điệp conventional commit sạch sẽ để duy trì lịch sử tuyến tính. Độ tin cậy: 0.75
- Khen ngợi rõ ràng các giải pháp tốt; quá trình đánh giá mã nguồn cũng là cách để cả nhóm học hỏi thế nào là một mã nguồn tốt. Độ tin cậy: 0.75
- Không bao giờ tự hợp nhất (merge) PR của chính mình mà không qua đánh giá trên các nhánh dùng chung, ngoại trừ các bản vá khẩn cấp (emergency hotfixes) đã được ghi nhận thành tài liệu và sẽ được đánh giá bổ sung ngay sau đó. Độ tin cậy: 0.85
Best PracticesProductivityCollaboration+2
F@f
0
Trình kiểm toán dọn dẹp nhánh Git cũ
Kỹ năng

Kiểm toán các nhánh từ xa (remote branch) của kho lưu trữ và lập kế hoạch dọn dẹp an toàn. Phân loại từng nhánh thành xóa (đã gộp, đã squash-merge hoặc trống), lưu trữ rồi xóa (công việc chưa gộp bị bỏ không), hỏi chủ sở hữu, PR đang mở, đang hoạt động hoặc được bảo vệ, đồng thời nhóm các yêu cầu hỏi theo tác giả. Đánh dấu các tên trùng lặp không phân biệt hoa thường, PR cũ, các nhánh tụt hậu quá xa so với main, các nhánh tồn tại lâu và tên đặt sai quy cách, đồng thời in ra kịch bản gắn thẻ rồi xóa có thể xem xét trước. Bao gồm một trình kiểm toán Python dùng thư viện chuẩn với tùy chọn --self-test.

---
name: stale-git-branch-cleanup-auditor
description: Kiểm toán các nhánh từ xa (remote branch) của kho lưu trữ và lập kế hoạch dọn dẹp an toàn. Phân loại từng nhánh thành xóa (đã gộp, đã squash-merge hoặc trống), lưu trữ rồi xóa (công việc chưa gộp bị bỏ không), hỏi chủ sở hữu, PR đang mở, đang hoạt động hoặc được bảo vệ, đồng thời nhóm các yêu cầu hỏi theo tác giả. Đánh dấu các tên trùng lặp không phân biệt hoa thường, PR cũ, các nhánh tụt hậu quá xa so với main, các nhánh tồn tại lâu và tên đặt sai quy cách, đồng thời in ra kịch bản gắn thẻ rồi xóa có thể xem xét trước. Bao gồm một trình kiểm toán Python dùng thư viện chuẩn với tùy chọn --self-test.
---

# Trình kiểm toán dọn dẹp nhánh Git cũ

Bạn giúp các nhóm dọn dẹp hàng chục hoặc hàng trăm nhánh bị lãng quên trên một remote dùng chung mà không làm mất công việc của bất kỳ ai. Bạn quyết định, từng nhánh một, nhánh nào an toàn để xóa, nhánh nào nên được lưu trữ dưới dạng thẻ (tag) trước, và ai cần được hỏi ý kiến, sau đó tạo ra một kịch bản dọn dẹp để mọi người xem xét trước khi bất kỳ thứ gì bị gỡ bỏ.

## Các tệp trong kỹ năng này

- `scripts/branch_audit.py` - phân loại các nhánh từ xa từ tệp xuất TSV hoặc CSV hoặc trực tiếp từ bản clone cục bộ (`--repo`), in ra các phát hiện, danh sách chủ sở hữu và kịch bản dọn dẹp (chỉ dùng thư viện chuẩn Python 3; không bao giờ tự xóa bất kỳ thứ gì)
- `references/cleanup-rules.md` - các quy tắc quyết định, phát hiện squash-merge, thẻ lưu trữ, nhánh được bảo vệ, quy ước đặt tên và cách khôi phục nhánh đã xóa
- `references/exporting-branch-data.md` - các lệnh git và GitHub/GitLab để tạo dữ liệu đầu vào, bao gồm cả trạng thái PR
- `templates/cleanup-announcement.md` - thông báo cho nhóm kèm theo kế hoạch, các câu hỏi dành cho chủ sở hữu và thời hạn chót
- `examples/webshop-branches.csv` - 16 nhánh của kho lưu trữ cửa hàng trực tuyến với các nhánh đã gộp, đã squash-merge, cũ, xung đột tên và được bảo vệ

## Quy trình làm việc

### 1. Lấy dữ liệu nhánh
Ưu tiên: một bản clone cục bộ mới (`git fetch --prune origin`) và chạy kịch bản với `--repo`. Hỏi nhánh cơ sở (main, master hoặc develop), trạng thái PR từ `gh pr list` hoặc GitLab API (squash merge không thể thấy được trong lịch sử git), các mẫu nhánh được bảo vệ và quy ước đặt tên của nhóm. Nếu họ dán một danh sách thay thế, hãy chuyển đổi nó thành các cột CSV trong `references/exporting-branch-data.md`.

### 2. Chạy trình kiểm toán
```
python3 scripts/branch_audit.py examples/webshop-branches.csv --today 2026-10-24
python3 scripts/branch_audit.py --repo . --base main --prs prs.tsv --stale-days 90 --ask-days 30
python3 scripts/branch_audit.py --self-test
```
Mã thoát 1 có nghĩa là có các phát hiện mức HIGH, 2 có nghĩa là đầu vào không hợp lệ.

### 3. Xem xét cùng người dùng
Đi qua các hành động theo thứ tự sau: DELETE_MERGED (an toàn, công việc đã nằm trong nhánh cơ sở), ARCHIVE_AND_DELETE (có thể khôi phục từ thẻ lưu trữ), ASK_OWNER (cần câu trả lời), sau đó là các phát hiện. Sửa lỗi CASE_COLLISION trước tiên vì nó làm hỏng các bản clone trên macOS và Windows. Điều chỉnh các ngưỡng theo tốc độ làm việc của nhóm (một kho lưu trữ nghiên cứu có thể muốn 180 ngày không hoạt động). Sử dụng `references/cleanup-rules.md` để giải thích từng quyết định.

### 4. Bàn giao
Điền vào `templates/cleanup-announcement.md` với số lượng thống kê, danh sách theo từng chủ sở hữu và thời hạn chót (thường là 7 ngày). Bàn giao kịch bản đã tạo cùng với lời khuyên nên chạy các dòng DELETE_MERGED trước và các dòng lưu trữ sau thời hạn chót.

## Kết quả ví dụ
Đối với `examples/webshop-branches.csv` (tính đến 2026-10-24, cũ 90 ngày, hỏi 30 ngày): 16 nhánh -> 3 xóa (đã gộp, PR đã squash-merge, không có commit nào đi trước), 3 lưu trữ và xóa (spike/graphql-gateway, feature/gift-cards, johns-test), 2 hỏi chủ sở hữu, 3 PR đang mở, 1 đang hoạt động, 4 được bảo vệ. Các phát hiện: HIGH CASE_COLLISION (fix/search-timeout so với fix/Search-Timeout), MEDIUM PR_STALE (feature/checkout-redesign, không hoạt động 205 ngày), 3 MEDIUM FAR_BEHIND, 2 LOW BAD_NAME, 2 LOW LONG_LIVED, LOW OLD_PROTECTED (release/2.3).

## Quy tắc

- Không bao giờ tự ý xóa các nhánh và không bao giờ chạy kịch bản thay cho người dùng; hãy in kịch bản ra để họ xem xét.
- Công việc chưa gộp sẽ được lưu trữ dưới dạng thẻ (`archive/<branch>`) trước khi nhánh bị xóa, để có thể khôi phục lại.
- Squash merge và rebase merge khiến nhánh trông như "chưa gộp" trong git; hãy tin tưởng trạng thái PR chứ không chỉ dựa vào `--merged`.
- Các nhánh được bảo vệ, nhánh phát hành (release) và nhánh cơ sở (base) không bao giờ nằm trong danh sách xóa, ngay cả khi chúng có vẻ không hoạt động.
- Hỏi ý kiến chủ sở hữu trước khi chạm vào công việc chưa gộp và đưa ra thời hạn chót rõ ràng.
FILE:references/cleanup-rules.md
# Quy tắc dọn dẹp

## Thứ tự quyết định (khớp đầu tiên sẽ được áp dụng)
| Hành động | Điều kiện | Tại sao an toàn hoặc không an toàn |
|---|---|---|
| KEEP_PROTECTED | khớp với `--protect` (main, master, develop, trunk, gh-pages, release/*, hotfix/*) | Các đợt triển khai, bảo trì bản phát hành và quy tắc bảo vệ nhánh phụ thuộc vào chúng |
| DELETE_MERGED | có thể truy cập từ nhánh cơ sở, 0 commit đi trước, hoặc PR của nó đã được gộp | Mọi commit (hoặc bản tương đương đã squash của nó) đều đã nằm trong nhánh cơ sở |
| KEEP_OPEN_PR | yêu cầu pull/merge đang mở | Xóa nhánh sẽ đóng PR; hãy hỏi ý kiến thay vì xóa (xem PR_STALE) |
| ARCHIVE_AND_DELETE | chưa gộp và không hoạt động >= số ngày cũ (mặc định 90) | Các commit duy nhất: gắn thẻ trước để công việc có thể khôi phục được |
| ASK_OWNER | chưa gộp và không hoạt động >= số ngày cần hỏi (mặc định 30) | Có thể vẫn đang được sử dụng; chủ sở hữu sẽ quyết định |
| KEEP_ACTIVE | chưa gộp, có các commit gần đây | Công việc đang tiến hành |

"Không hoạt động" (Idle) là ngày committer của commit đầu mút nhánh. Thao tác rebase sẽ cập nhật ngày này, vì vậy một nhánh được rebase ngày hôm qua vẫn được tính là đang hoạt động ngay cả khi không ai làm việc trên đó trong nhiều tháng.

## Squash merge và rebase merge
`git branch -r --merged main` chỉ tìm thấy các nhánh có commit đầu mút nằm trong main. Khi một PR được squash-merge hoặc rebase-merge, main nhận các commit mới còn nhánh đó trông vẫn như chưa gộp với N commit đi trước. Hãy sử dụng trạng thái PR (`pr = merged`) để bắt các trường hợp này. Nếu không có dữ liệu PR, hãy kiểm tra thủ công:
```
git diff --stat origin/main...origin/<branch>     # trống hoặc rất nhỏ = các thay đổi đã có trong main
git log --oneline origin/main --grep "<PR title or number>"
```
Một PR đã đóng (nhưng không gộp) với các commit cũ là công việc đã bị từ bỏ: hãy lưu trữ nó.

## Thẻ lưu trữ
```
git tag archive/<branch> origin/<branch>
git push origin refs/tags/archive/<branch>
git push origin --delete <branch>
```
Khôi phục sau này bằng lệnh `git push origin archive/<branch>:refs/heads/<branch>`. Các thẻ giúp giữ cho các commit tồn tại mãi mãi (không bị dọn rác - garbage collection). Nếu các thẻ lưu trữ làm rối danh sách thẻ, hãy đẩy chúng lên một remote lưu trữ riêng biệt.

Đã xóa mà không gắn thẻ? Trên GitHub, một nhánh bị xóa khỏi trang PR có thể được khôi phục từ PR đó trong một khoảng thời gian; nếu không, bản clone cục bộ của ai đó hoặc `git reflog` trên bản sao (mirror) phía máy chủ là cách duy nhất để lấy lại. Đó là lý do tại sao kịch bản luôn gắn thẻ trước.

## Các phát hiện
| Mã | Mức độ nghiêm trọng | Ý nghĩa | Hành động |
|---|---|---|---|
| CASE_COLLISION | HIGH | `fix/Login` và `fix/login` cùng tồn tại; hệ thống tệp không phân biệt hoa thường lưu trữ các tham chiếu (ref) dưới dạng tệp, nên thao tác fetch sẽ thất bại hoặc một tham chiếu sẽ che khuất tham chiếu kia | Xóa hoặc đổi tên một nhánh ngay hôm nay |
| PR_STALE | MEDIUM | PR đang mở không có commit nào trong >= số ngày cũ | Chủ sở hữu hoàn thiện, tách nhỏ hoặc đóng nó |
| FAR_BEHIND | MEDIUM | Nhánh chưa gộp tụt hậu > 300 commit so với nhánh cơ sở | Sớm rebase/merge nhánh cơ sở hoặc lưu trữ; xung đột sẽ tăng lên mỗi ngày |
| LONG_LIVED | LOW | > 50 commit chưa gộp và không hoạt động >= số ngày cần hỏi | Tách thành các PR nhỏ hơn; ẩn phần công việc chưa hoàn thành sau một cờ tính năng (feature flag) |
| OLD_PROTECTED | LOW | release/* hoặc hotfix/* không hoạt động >= số ngày cũ | Gắn thẻ cho bản phát hành (`v2.3.x`), gỡ bảo vệ, rồi xóa |
| BAD_NAME | LOW | Không có tiền tố `<type>/` được phép, hoặc chứa chữ hoa/khoảng trắng | Đổi tên thành `feature/...`, `fix/...`, `chore/...` |

## Quy ước đặt tên
`<type>/<short-kebab-description>` với mã ticket tùy chọn: `feature/cart-coupons`, `fix/1423-vat-rounding`, `chore/bump-node-22`. Các loại được phép mặc định: feature, feat, fix, bugfix, hotfix, release, chore, docs, refactor, test, ci, perf, build, deps, dependabot, renovate, experiment, spike. Ghi đè bằng `--prefixes`.

## Giữ kho lưu trữ sạch sẽ sau đó
- Bật tùy chọn "Automatically delete head branches" (GitHub) hoặc "Delete source branch" (GitLab) khi gộp.
- Chỉ bảo vệ những nhánh thực sự cần thiết, bằng các mẫu (pattern).
- Chạy kiểm toán hàng tháng; hầu hết các nhóm ổn định ở mức 90 ngày cũ và 30 ngày cần hỏi.
- Các bot (dependabot, renovate) sẽ tự tạo lại nhánh của chúng; hãy giữ cho các PR của chúng được xử lý liên tục thay vì xóa nhánh.
FILE:references/exporting-branch-data.md
# Xuất dữ liệu nhánh

## Tùy chọn A: để kịch bản đọc bản clone
```
git fetch --prune origin
python3 scripts/branch_audit.py --repo . --remote origin --base main
```
Kịch bản sẽ chạy `git for-each-ref`, `git branch -r --merged origin/main` và `git rev-list --left-right --count origin/main...origin/<branch>` cho mọi nhánh. Các kho lưu trữ lớn với hàng nghìn nhánh sẽ mất khoảng một phút.

## Tùy chọn B: tự tạo tệp TSV (hoặc CSV)
Các cột (phân tách bằng tab hoặc dấu phẩy, bắt buộc có dòng tiêu đề; ký tự phân tách được phát hiện từ dòng tiêu đề): `branch last_commit author ahead behind merged pr`

```
base=origin/main
printf 'branch\tlast_commit\tauthor\tahead\tbehind\tmerged\tpr\n' > branches.tsv
git branch -r --merged "$base" | sed 's/^ *//' > /tmp/merged.txt
git for-each-ref --format='%(refname:short)%09%(committerdate:short)%09%(authorname)' refs/remotes/origin |
while IFS="$(printf '\t')" read -r ref date author; do
  case "$ref" in origin|origin/HEAD|"$base") continue;; esac
  set -- $(git rev-list --left-right --count "$base...$ref")
  merged=no; grep -qx "$ref" /tmp/merged.txt && merged=yes
  printf '%s\t%s\t%s\t%s\t%s\t%s\tnone\n' "ref#origin/" "$date" "$author" "$2" "$1" "$merged" >> branches.tsv
done
```
`$1` là "behind" (các commit chỉ có trong nhánh cơ sở) và `$2` là "ahead" (các commit chỉ có trong nhánh đang xét).

## Trạng thái PR
GitHub CLI (`gh pr list` trả về các PR mới nhất trước; awk giữ lại dòng đầu tiên, tức là PR mới nhất, cho mỗi nhánh):
```
gh pr list --state all --limit 1000 --json headRefName,state \
  -q '.[] | [.headRefName, (.state|ascii_downcase)] | @tsv' | awk -F'\t' '!seen[$1]++' > prs.tsv
python3 scripts/branch_audit.py --repo . --prs prs.tsv
```
Các trạng thái trên GitHub ánh xạ thành open, merged, closed. GitLab: `glab mr list --all` hoặc API `GET /projects/:id/merge_requests?state=all` (`source_branch`, `state`: opened -> open, merged, closed).

Đối với tệp TSV hoặc CSV, hãy đặt trạng thái vào cột `pr` (open, merged, closed hoặc none).

## Các ngưỡng
| Tùy chọn | Mặc định | Ghi chú |
|---|---|---|
| `--stale-days` | 90 | Các nhánh chưa gộp không hoạt động vượt quá mức này sẽ được lưu trữ |
| `--ask-days` | 30 | Các nhánh chưa gộp không hoạt động vượt quá mức này cần câu trả lời từ chủ sở hữu |
| `--behind-limit` | 300 | Ngưỡng FAR_BEHIND tính bằng số commit |
| `--protect` | main,master,develop,trunk,gh-pages,release/*,hotfix/* | Các mẫu fnmatch, có phân biệt hoa thường |
| `--prefixes` | feature,fix,chore,... | Các tiền tố tên được phép đối với lỗi BAD_NAME |
| `--today` | today | Cố định ngày để tạo các báo cáo có thể lặp lại |

## Quyền riêng tư
Tên tác giả được lấy từ các commit. Chỉ chia sẻ danh sách theo từng chủ sở hữu trong nội bộ nhóm.
FILE:templates/cleanup-announcement.md
# Dọn dẹp nhánh trên <repository> - vui lòng kiểm tra trước <deadline date>

Chào cả nhóm,

Chúng ta hiện có <N> nhánh trên `<remote>`. Vào ngày <cleanup date>, chúng ta sẽ tiến hành dọn dẹp:

- **<n> nhánh đã gộp** sẽ bị xóa (các thay đổi của chúng đã có trong `<base>`).
- **<n> nhánh chưa gộp không hoạt động** (không có commit nào trong hơn <stale days> ngày) sẽ được lưu thành các thẻ `archive/<branch>` và sau đó bị xóa. Bạn có thể khôi phục bất kỳ nhánh nào trong số đó bằng lệnh `git push origin archive/<branch>:refs/heads/<branch>`.
- **<n> nhánh cần câu trả lời của bạn** (không có commit nào trong hơn <ask days> ngày): giữ lại, gộp, hay lưu trữ?

## Các nhánh của bạn
| Chủ sở hữu | Các nhánh | Cần bạn phản hồi |
|---|---|---|
| <name> | <branch, branch> | <giữ lại / lưu trữ / gộp trước thời hạn> |

## Vui lòng khắc phục
- <CASE_COLLISION: fix/search-timeout và fix/Search-Timeout - Lena, hãy xóa một nhánh ngay hôm nay>
- <PR_STALE: feature/checkout-redesign - Marco, hãy hoàn thiện, tách nhỏ hoặc đóng lại>

## Từ nay trở đi
- Tên nhánh: `<type>/<short-description>` (feature/, fix/, chore/, ...)
- Các nhánh đầu mút (head branch) sẽ tự động bị xóa sau khi gộp.
- Đợt kiểm toán này sẽ chạy vào thứ Hai đầu tiên của mỗi tháng.

Hãy phản hồi tại <channel/thread> nếu có nhánh nào bắt buộc phải giữ lại. Cảm ơn mọi người!
FILE:examples/webshop-branches.csv
branch,last_commit,author,ahead,behind,merged,pr
main,2026-10-23,Selin Kaya,0,0,yes,none
develop,2026-10-22,Selin Kaya,4,0,no,none
release/2.3,2025-11-12,Selin Kaya,2,611,no,none
release/2.8,2026-09-30,Selin Kaya,1,48,no,none
feature/cart-coupons,2026-10-20,Marco Rossi,12,6,no,open
feature/checkout-redesign,2026-04-02,Marco Rossi,134,742,no,open
feature/wishlist-sharing,2026-09-08,Aiko Tanaka,7,95,no,none
feature/gift-cards,2026-05-19,Aiko Tanaka,23,401,no,closed
fix/vat-rounding,2026-10-02,Lena Fischer,3,22,yes,merged
fix/search-timeout,2026-08-14,Lena Fischer,5,130,no,merged
fix/Search-Timeout,2026-08-13,Lena Fischer,1,131,no,none
chore/bump-node-22,2026-07-01,Omar Haddad,0,210,no,none
spike/graphql-gateway,2026-02-10,Omar Haddad,58,988,no,none
johns-test,2026-06-21,John Miller,2,260,no,none
dependabot/npm_and_yarn/axios-1.7.4,2026-10-18,dependabot[bot],1,9,no,open
experiment/ab-banner,2026-10-12,Aiko Tanaka,4,14,no,none
FILE:scripts/branch_audit.py
#!/usr/bin/env python3
"""Stale Git branch cleanup auditor (Python 3 standard library only).

Sorts every remote branch into an action, flags risky branches and prints a reviewable
cleanup script. Nothing is deleted: the script only prints commands.

Input, either:
  1. a TSV or CSV export (header required, delimiter detected from the header, extra columns ignored):
       branch  last_commit  author  ahead  behind  merged  pr
     last_commit = YYYY-MM-DD, ahead/behind = commits vs the base branch,
     merged = yes/no (reachable from base), pr = open | merged | closed | none
  2. --repo PATH: reads the remote branches straight from a local clone with git
     (run `git fetch --prune` first). PR state comes from --prs FILE (lines "branch<TAB>state",
     e.g. from `gh pr list --state all --json headRefName,state`), otherwise "none".
Actions (first match wins):
  KEEP_PROTECTED      name matches --protect patterns (main, master, develop, release/*, ...)
  DELETE_MERGED       merged into base, 0 commits ahead, or its PR was merged (squash/rebase)
  KEEP_OPEN_PR        has an open pull request
  ARCHIVE_AND_DELETE  unmerged and no commit for >= --stale-days (tag archive/<name>, then delete)
  ASK_OWNER           unmerged and no commit for >= --ask-days
  KEEP_ACTIVE         recent unmerged work
Findings (severity):
  HIGH   CASE_COLLISION  branch names that differ only by case (break on macOS/Windows clones)
  MEDIUM PR_STALE        open PR whose branch has had no commit for >= --stale-days
  MEDIUM FAR_BEHIND      unmerged branch more than --behind-limit commits behind base
  LOW    LONG_LIVED      unmerged branch more than 50 commits ahead and older than --ask-days
  LOW    OLD_PROTECTED   protected release/hotfix branch older than --stale-days (tag it, unprotect, delete)
  LOW    BAD_NAME        name without an allowed prefix, or with uppercase/space characters
Usage:
  python3 branch_audit.py branches.csv --today 2026-10-24
  python3 branch_audit.py --repo . --base main --prs prs.tsv
  python3 branch_audit.py branches.tsv --json
  python3 branch_audit.py --self-test
Exit code: 0 no HIGH findings, 1 HIGH findings, 2 bad input.
"""
import argparse
import csv
import datetime as dt
import fnmatch
import json
import shlex
import subprocess
import sys

DEFAULT_PROTECT = "main,master,develop,trunk,gh-pages,release/*,hotfix/*"
DEFAULT_PREFIXES = "feature,feat,fix,bugfix,hotfix,release,chore,docs,refactor,test,ci,perf,build,deps,dependabot,renovate,experiment,spike"
ACTION_ORDER = ["DELETE_MERGED", "ARCHIVE_AND_DELETE", "ASK_OWNER", "KEEP_OPEN_PR", "KEEP_ACTIVE", "KEEP_PROTECTED"]
SEV_ORDER = {"HIGH": 0, "MEDIUM": 1, "LOW": 2}


class DataError(ValueError):
    pass


def yes(v):
    return str(v).strip().lower() in ("yes", "y", "true", "1")


def parse_tsv(text):
    lines = text.splitlines()
    delim = "\t" if lines and "\t" in lines[0] else ","
    rows = list(csv.DictReader(lines, delimiter=delim))
    need = {"branch", "last_commit", "author", "ahead", "behind", "merged"}
    if not rows:
        raise DataError("no branches in input")
    missing = need - set(rows[0].keys())
    if missing:
        raise DataError("missing columns: " + ", ".join(sorted(missing)))
    out = []
    for i, r in enumerate(rows, 2):
        try:
            out.append({"branch": r["branch"].strip(), "last_commit": dt.date.fromisoformat(r["last_commit"].strip()[:10]),
                        "author": r["author"].strip() or "unknown", "ahead": int(r["ahead"]), "behind": int(r["behind"]),
                        "merged": yes(r["merged"]), "pr": (r.get("pr") or "none").strip().lower() or "none"})
        except (ValueError, TypeError) as e:
            raise DataError(f"line {i}: {e}")
    return out


def git(repo, *args):
    r = subprocess.run(["git", "-C", repo, *args], capture_output=True, text=True)
    if r.returncode != 0:
        raise DataError(f"git {' '.join(args)}: {r.stderr.strip()}")
    return r.stdout


def read_repo(repo, remote, base, prs):
    base_ref = f"{remote}/{base}"
    git(repo, "rev-parse", "--verify", base_ref)
    merged = {l.strip() for l in git(repo, "branch", "-r", "--merged", base_ref).splitlines()}
    out = []
    fmt = "%(refname:short)%09%(committerdate:short)%09%(authorname)"
    for line in git(repo, "for-each-ref", f"--format={fmt}", f"refs/remotes/{remote}").splitlines():
        ref, date, author = line.split("\t")
        if ref in (remote, f"{remote}/HEAD") or ref == base_ref:
            continue
        behind, ahead = git(repo, "rev-list", "--left-right", "--count", f"{base_ref}...{ref}").split()
        name = ref[len(remote) + 1:]
        out.append({"branch": name, "last_commit": dt.date.fromisoformat(date), "author": author,
                    "ahead": int(ahead), "behind": int(behind), "merged": ref in merged, "pr": prs.get(name, "none")})
    return out


def audit(branches, today, base="main", remote="origin", stale_days=90, ask_days=30, behind_limit=300,
          protect=DEFAULT_PROTECT, prefixes=DEFAULT_PREFIXES):
    protect_pats = [p.strip() for p in protect.split(",") if p.strip()]
    allowed = {p.strip() for p in prefixes.split(",") if p.strip()}
    findings, rows = [], []

    def add(sev, code, branch, msg):
        findings.append({"severity": sev, "code": code, "branch": branch, "message": msg})

    for b in branches:
        name, age = b["branch"], (today - b["last_commit"]).days
        prot = name == base or any(fnmatch.fnmatchcase(name, p) for p in protect_pats)
        if prot:
            action = "KEEP_PROTECTED"
        elif b["merged"] or b["ahead"] == 0 or b["pr"] == "merged":
            action = "DELETE_MERGED"
        elif b["pr"] == "open":
            action = "KEEP_OPEN_PR"
        elif age >= stale_days:
            action = "ARCHIVE_AND_DELETE"
        elif age >= ask_days:
            action = "ASK_OWNER"
        else:
            action = "KEEP_ACTIVE"
        why = {"KEEP_PROTECTED": "protected pattern",
               "DELETE_MERGED": "merged into " + base if b["merged"] else ("no commits ahead" if b["ahead"] == 0 else "PR merged (squash/rebase)"),
               "KEEP_OPEN_PR": "open pull request", "ARCHIVE_AND_DELETE": f"unmerged, idle {age} days",
               "ASK_OWNER": f"unmerged, idle {age} days", "KEEP_ACTIVE": f"active {age} days ago"}[action]
        rows.append({**b, "last_commit": b["last_commit"].isoformat(), "age_days": age, "action": action, "reason": why})
        unmerged = action in ("ARCHIVE_AND_DELETE", "ASK_OWNER", "KEEP_OPEN_PR", "KEEP_ACTIVE")
        if action == "KEEP_OPEN_PR" and age >= stale_days:
            add("MEDIUM", "PR_STALE", name, f"open PR but no commit for {age} days; ask {b['author']} to finish or close it")
        if unmerged and b["behind"] > behind_limit:
            add("MEDIUM", "FAR_BEHIND", name, f"{b['behind']} commits behind {base}; rebase soon or expect painful conflicts")
        if unmerged and b["ahead"] > 50 and age >= ask_days:
            add("LOW", "LONG_LIVED", name, f"{b['ahead']} unmerged commits; split into smaller PRs or put it behind a feature flag")
        if prot and name != base and age >= stale_days and (name.startswith("release/") or name.startswith("hotfix/")):
            add("LOW", "OLD_PROTECTED", name, f"protected branch idle {age} days; tag it, remove protection and delete")
        if not prot:
            prefix = name.split("/", 1)[0] if "/" in name else ""
            if prefix not in allowed or name != name.lower() or " " in name:
                add("LOW", "BAD_NAME", name, "use lowercase <type>/<short-description> (e.g. feature/cart-coupons)")
    seen = {}
    for b in branches:
        seen.setdefault(b["branch"].lower(), []).append(b["branch"])
    for names in seen.values():
        if len(names) > 1:
            add("HIGH", "CASE_COLLISION", " / ".join(names), "names differ only by case; clones on macOS/Windows see one ref and fetches fail; delete or rename one")
    findings.sort(key=lambda f: (SEV_ORDER[f["severity"]], f["code"], f["branch"]))
    rows.sort(key=lambda r: (ACTION_ORDER.index(r["action"]), -r["age_days"]))
    counts = {a: sum(r["action"] == a for r in rows) for a in ACTION_ORDER}
    owners = {}
    for r in rows:
        if r["action"] in ("ASK_OWNER", "ARCHIVE_AND_DELETE"):
            owners.setdefault(r["author"], []).append(r["branch"])
    script = ["#!/bin/sh", "# Review before running. Generated by branch_audit.py; nothing has been deleted.", "set -e"]
    for r in rows:
        q = shlex.quote(r["branch"])
        if r["action"] == "DELETE_MERGED":
            script.append(f"git push {remote} --delete {q}   # {r['reason']}")
    for r in rows:
        if r["action"] == "ARCHIVE_AND_DELETE":
            q = shlex.quote(r["branch"])
            tag = shlex.quote("archive/" + r["branch"])
            script.append(f"git tag {tag} {shlex.quote(remote + '/' + r['branch'])} && git push {remote} {shlex.quote('refs/tags/archive/' + r['branch'])} && git push {remote} --delete {q}")
    return {"today": today.isoformat(), "base": base, "remote": remote, "stale_days": stale_days, "ask_days": ask_days,
            "branches": len(rows), "counts": counts, "rows": rows, "owners": owners, "findings": findings,
            "cleanup_script": "\n".join(script) + "\n"}


def render(r):
    c = r["counts"]
    out = [f"# Branch cleanup audit ({r['remote']}, base {r['base']}, as of {r['today']})", "",
           f"{r['branches']} branches: {c['DELETE_MERGED']} delete (merged), {c['ARCHIVE_AND_DELETE']} archive and delete, "
           f"{c['ASK_OWNER']} ask owner, {c['KEEP_OPEN_PR']} open PR, {c['KEEP_ACTIVE']} active, {c['KEEP_PROTECTED']} protected",
           f"Rules: stale >= {r['stale_days']} days idle, ask owner >= {r['ask_days']} days", "",
           "| Action | Branch | Last commit | Author | Ahead/behind | Reason |", "|---|---|---|---|---|---|"]
    for row in r["rows"]:
        out.append(f"| {row['action']} | {row['branch']} | {row['last_commit']} | {row['author']} | {row['ahead']}/{row['behind']} | {row['reason']} |")
    out += ["", "## Ask the owners"]
    if not r["owners"]:
        out.append("- nobody")
    for who, names in sorted(r["owners"].items()):
        out.append(f"- {who}: {', '.join(names)}")
    out += ["", "## Findings"]
    if not r["findings"]:
        out.append("- none")
    for f in r["findings"]:
        out.append(f"- {f['severity']} {f['code']} {f['branch']}: {f['message']}")
    out += ["", "## Cleanup script (review first, run after owners agree)", "```sh", r["cleanup_script"].rstrip(), "```"]
    return "\n".join(out) + "\n"


def self_test():
    tests, ok = [], 0

    def check(name, cond):
        nonlocal ok
        tests.append(name)
        print(("PASS " if cond else "FAIL ") + name)
        ok += bool(cond)

    today = dt.date(2026, 10, 24)

    def b(name, days, ahead=3, behind=10, merged=False, pr="none", author="ana"):
        return {"branch": name, "last_commit": today - dt.timedelta(days=days), "author": author,
                "ahead": ahead, "behind": behind, "merged": merged, "pr": pr}

    def act(**kw):
        name = kw.pop("name", "feature/x")
        days = kw.pop("days", 10)
        return audit([b(name, days, **kw)], today)["rows"][0]["action"]

    check("main protected", act(name="main", days=500) == "KEEP_PROTECTED")
    check("release/* protected", act(name="release/2.4", days=200) == "KEEP_PROTECTED")
    check("merged -> DELETE_MERGED", act(merged=True, days=5) == "DELETE_MERGED")
    check("0 ahead -> DELETE_MERGED", act(ahead=0) == "DELETE_MERGED")
    check("squash-merged PR -> DELETE_MERGED", act(pr="merged", ahead=4) == "DELETE_MERGED")
    check("open PR kept", act(pr="open", days=200) == "KEEP_OPEN_PR")
    check("idle 120 d unmerged -> ARCHIVE", act(days=120) == "ARCHIVE_AND_DELETE")
    check("idle 45 d -> ASK_OWNER", act(days=45) == "ASK_OWNER")
    check("idle 5 d -> KEEP_ACTIVE", act(days=5) == "KEEP_ACTIVE")
    check("closed PR idle 100 d -> ARCHIVE", act(pr="closed", days=100) == "ARCHIVE_AND_DELETE")

    def codes(rows, **kw):
        return {f["code"] for f in audit(rows, today, **kw)["findings"]}

    check("case collision HIGH", "CASE_COLLISION" in codes([b("feature/Login", 3), b("feature/login", 3)]))
    check("stale PR MEDIUM", "PR_STALE" in codes([b("feature/a", 100, pr="open")]))
    check("far behind MEDIUM", "FAR_BEHIND" in codes([b("feature/a", 10, behind=900)]))
    check("merged far behind not flagged", "FAR_BEHIND" not in codes([b("feature/a", 10, behind=900, merged=True)]))
    check("long lived LOW", "LONG_LIVED" in codes([b("feature/a", 40, ahead=80)]))
    check("old release LOW", "OLD_PROTECTED" in codes([b("release/1.0", 400)]))
    check("bad name LOW", "BAD_NAME" in codes([b("johns-test", 3)]) and "BAD_NAME" in codes([b("feature/NewThing", 3)]))
    check("good name not flagged", "BAD_NAME" not in codes([b("fix/cart-total", 3)]))
    r = audit([b("feature/old", 200), b("fix/done", 3, merged=True)], today)
    check("script deletes merged branch", "git push origin --delete fix/done" in r["cleanup_script"])
    check("script tags before deleting stale", "git tag archive/feature/old origin/feature/old && git push origin refs/tags/archive/feature/old && git push origin --delete feature/old" in r["cleanup_script"])
    r = audit([b("main", 1, ahead=0, behind=0), b("feature/old", 200), b("develop", 300)], today)
    check("script never deletes protected branches", "--delete main" not in r["cleanup_script"] and "--delete develop" not in r["cleanup_script"]
          and r["cleanup_script"].rstrip().endswith("git push origin --delete feature/old"))
    r = audit([b("feature/it's", 200)], today)
    check("odd names are shell-quoted", "'feature/it'\"'\"'s'" in r["cleanup_script"])
    r = audit([b("feature/a", 200, author="ana"), b("feature/b", 40, author="ana"), b("feature/c", 2, author="bo")], today)
    check("owners grouped", r["owners"] == {"ana": ["feature/a", "feature/b"]})
    rows = parse_tsv("branch\tlast_commit\tauthor\tahead\tbehind\tmerged\tpr\nfeature/x\t2026-10-01\tAna\t2\t5\tno\topen\n")
    check("TSV parsed", rows[0]["ahead"] == 2 and rows[0]["pr"] == "open" and not rows[0]["merged"])
    rows = parse_tsv("branch,last_commit,author,ahead,behind,merged,pr\nfix/y,2026-09-01,Bo,0,3,yes,none\n")
    check("CSV parsed", rows[0]["branch"] == "fix/y" and rows[0]["merged"] and rows[0]["behind"] == 3)
    try:
        parse_tsv("branch\tauthor\nx\ty\n")
        check("missing columns rejected", False)
    except DataError:
        check("missing columns rejected", True)
    # real git repository round trip (skipped if git is missing)
    import os, shutil, tempfile
    if shutil.which("git"):
        d = tempfile.mkdtemp()
        try:
            env = {**os.environ, "GIT_AUTHOR_NAME": "Ana", "GIT_AUTHOR_EMAIL": "a@x", "GIT_COMMITTER_NAME": "Ana", "GIT_COMMITTER_EMAIL": "a@x"}
            run = lambda *a, cwd=d: subprocess.run(["git", *a], cwd=cwd, env=env, capture_output=True, check=True)
            os.makedirs(f"{d}/remote.git"); run("init", "-q", "--bare", "-b", "main", cwd=f"{d}/remote.git")
            run("clone", "-q", f"{d}/remote.git", "w"); w = f"{d}/w"
            run("checkout", "-q", "-b", "main", cwd=w); run("commit", "-q", "--allow-empty", "-m", "init", cwd=w)
            run("checkout", "-q", "-b", "fix/done", cwd=w); run("commit", "-q", "--allow-empty", "-m", "fix", cwd=w)
            run("checkout", "-q", "main", cwd=w); run("merge", "-q", "--ff-only", "fix/done", cwd=w)
            run("checkout", "-q", "-b", "feature/wip", cwd=w); run("commit", "-q", "--allow-empty", "-m", "wip", cwd=w)
            run("push", "-q", "origin", "main", "fix/done", "feature/wip", cwd=w)
            r = audit(read_repo(w, "origin", "main", {}), today)
            acts = {x["branch"]: x["action"] for x in r["rows"]}
            check("git repo: merged and active branches read", acts == {"fix/done": "DELETE_MERGED", "feature/wip": "KEEP_ACTIVE"})
        finally:
            shutil.rmtree(d, ignore_errors=True)
    print(f"{ok}/{len(tests)} passed")
    return 0 if ok == len(tests) else 1


def main():
    ap = argparse.ArgumentParser(description="Stale Git branch cleanup auditor")
    ap.add_argument("tsv", nargs="?", help="branch TSV or CSV export")
    ap.add_argument("--repo", help="read remote branches from this local clone instead of a TSV")
    ap.add_argument("--prs", help="TSV 'branch<TAB>state' with PR states (open/merged/closed), used with --repo")
    ap.add_argument("--remote", default="origin")
    ap.add_argument("--base", default="main")
    ap.add_argument("--today", help="YYYY-MM-DD (default: today)")
    ap.add_argument("--stale-days", type=int, default=90)
    ap.add_argument("--ask-days", type=int, default=30)
    ap.add_argument("--behind-limit", type=int, default=300)
    ap.add_argument("--protect", default=DEFAULT_PROTECT)
    ap.add_argument("--prefixes", default=DEFAULT_PREFIXES)
    ap.add_argument("--json", action="store_true")
    ap.add_argument("--self-test", action="store_true")
    a = ap.parse_args()
    if a.self_test:
        return self_test()
    try:
        today = dt.date.fromisoformat(a.today) if a.today else dt.date.today()
        if a.repo:
            prs = {}
            if a.prs:
                with open(a.prs, encoding="utf-8") as fh:
                    for line in fh:
                        if "\t" in line:
                            k, v = line.rstrip("\n").split("\t", 1)
                            prs[k] = v.strip().lower()
            branches = read_repo(a.repo, a.remote, a.base, prs)
        elif a.tsv:
            with open(a.tsv, encoding="utf-8") as fh:
                branches = parse_tsv(fh.read())
        else:
            ap.error("give a TSV/CSV file or --repo PATH")
        r = audit(branches, today, a.base, a.remote, a.stale_days, a.ask_days, a.behind_limit, a.protect, a.prefixes)
    except (OSError, DataError, ValueError) as e:
        print(f"error: {e}", file=sys.stderr)
        return 2
    print(json.dumps(r, indent=2) if a.json else render(r), end="\n" if a.json else "")
    return 1 if any(f["severity"] == "HIGH" for f in r["findings"]) else 0


if __name__ == "__main__":
    sys.exit(main())
PythonDevOpsProductivity+2
F@f
0
Công cụ Kiểm thử và Đánh giá Mẫu Gitignore
Kỹ năng

Giải thích từng dòng của tệp .gitignore, kiểm thử tệp đó với các đường dẫn bắt buộc phải bỏ qua hoặc bắt buộc phải giữ lại để có thể commit, chỉ rõ quy tắc quyết định cho từng đường dẫn, đồng thời cảnh báo các lỗi sai như quy tắc phủ định bị chặn bởi thư mục cha đã bị loại trừ, chú thích nội dòng (inline comment), khoảng trắng ở đầu dòng, dấu gạch chéo ngược (backslash), ký tự ** thừa, thông tin bí mật (secrets) chưa được bao phủ và tệp .env.example bị bỏ qua. Trả về một tệp đã được sửa lỗi. Bao gồm một trình kiểm tra viết bằng thư viện chuẩn Python có hỗ trợ --self-test, đối chiếu khớp với git thực tế.

---
name: gitignore-pattern-tester-and-reviewer
description: Giải thích từng dòng của tệp .gitignore, kiểm thử tệp đó với các đường dẫn bắt buộc phải bỏ qua hoặc bắt buộc phải giữ lại để có thể commit, chỉ rõ quy tắc quyết định cho từng đường dẫn, đồng thời cảnh báo các lỗi sai như quy tắc phủ định bị chặn bởi thư mục cha đã bị loại trừ, chú thích nội dòng (inline comment), khoảng trắng ở đầu dòng, dấu gạch chéo ngược (backslash), ký tự ** thừa, thông tin bí mật (secrets) chưa được bao phủ và tệp .env.example bị bỏ qua. Trả về một tệp đã được sửa lỗi. Bao gồm một trình kiểm tra viết bằng thư viện chuẩn Python có hỗ trợ --self-test, đối chiếu khớp với git thực tế.
---

# Công cụ Kiểm thử và Đánh giá Mẫu Gitignore

Bạn giúp các lập trình viên hiểu và sửa lỗi các tệp `.gitignore`. Bạn giải thích những gì mà mỗi mẫu (pattern) thực sự khớp, chứng minh điều đó bằng các bài kiểm thử đường dẫn, và tìm ra những sai lầm khiến git bỏ qua các tệp mã nguồn hoặc vô tình commit các thông tin bí mật (secrets) cùng kết quả biên dịch (build output). Bạn trả về một tệp đã được hiệu chỉnh kèm theo các bài kiểm thử vượt qua thành công, chứ không chỉ đưa ra lời khuyên suông.

## Các tệp trong kỹ năng này

- `scripts/gitignore_check.py` - giải thích từng quy tắc, chạy các bài kiểm thử "+ path" (bắt buộc phải bỏ qua) và "- path" (bắt buộc phải giữ lại), chỉ rõ dòng quyết định cho từng đường dẫn, và báo cáo các phát hiện ở mức HIGH, MEDIUM, LOW và INFO (chỉ dùng thư viện chuẩn Python 3)
- `references/gitignore-rules.md` - cách git đọc các tệp ignore: các nguồn và thứ tự ưu tiên, cú pháp dòng, neo đường dẫn (anchoring), ký tự đại diện (wildcards), quy tắc thư mục cha, các tệp đã được theo dõi (tracked files) và các lệnh để xác nhận
- `references/common-pitfalls.md` - mọi mã phát hiện kèm cách khắc phục, các bẫy thường gặp mà kịch bản không thể nhìn thấy (tên quá rộng, tệp đã được theo dõi, tệp lồng nhau, phân biệt chữ hoa chữ thường, tệp khóa lock files, sự khác biệt với .dockerignore) và các khối mẫu khởi đầu tốt
- `templates/gitignore-review.md` - bản đánh giá để bàn giao lại cho người dùng
- `examples/webapp.gitignore` - một tệp ignore của ứng dụng web chứa sáu lỗi thực tế
- `examples/webapp-fixed.gitignore` - tệp đã được sửa lỗi (tất cả các bài kiểm thử đều vượt qua)
- `examples/webapp-paths.txt` - 19 bài kiểm thử đường dẫn được sử dụng với cả hai tệp trên

## Quy trình làm việc

### 1. Thu thập dữ liệu đầu vào
Yêu cầu cung cấp tệp `.gitignore` (và bất kỳ tệp `.gitignore` lồng nhau nào), ngăn xếp công nghệ (stack), và các đường dẫn quan trọng: những gì bắt buộc phải bỏ qua (kết quả biên dịch, các gói phụ thuộc, nhật ký log, thông tin bí mật) và những gì bắt buộc phải giữ lại để có thể commit (các thư mục mã nguồn có tên phổ biến như `build/` hoặc `lib/`, các tệp `.gitkeep`, `.env.example`, các tệp khóa lock files). Nếu người dùng chỉ hỏi "tại sao X lại bị bỏ qua?", hãy hỏi đường dẫn và sử dụng kết quả đầu ra của lệnh `git check-ignore -v X` nếu họ có sẵn.

### 2. Chạy trình kiểm tra
```
python3 scripts/gitignore_check.py examples/webapp.gitignore --test examples/webapp-paths.txt
python3 scripts/gitignore_check.py .gitignore --test paths.txt --json
python3 scripts/gitignore_check.py --self-test
```
Tệp đường dẫn (Paths file): `+ path` là bắt buộc phải bỏ qua, `- path` là bắt buộc phải giữ lại, kết thúc tên thư mục bằng dấu `/`. Mã thoát (Exit) 1 có nghĩa là có phát hiện mức HIGH hoặc có bài kiểm thử thất bại, mã thoát 2 có nghĩa là dữ liệu đầu vào không thể đọc được. Trình kiểm tra mô hình hóa một tệp ignore ở thư mục gốc của kho lưu trữ (repo root); đối với tệp lồng bên trong, hãy cung cấp các đường dẫn tương đối tính từ thư mục chứa tệp đó.

### 3. Giải thích và sửa lỗi
- Đi qua bảng Quy tắc (Rules table) bằng ngôn từ dễ hiểu, đặc biệt là quy tắc neo đường dẫn (dấu gạch chéo ở đầu hoặc ở giữa = tương đối so với thư mục chứa `.gitignore`) và quy tắc thư mục cha (xem `references/gitignore-rules.md`).
- Đối với mỗi trường hợp FAIL, hãy trích dẫn dòng quyết định từ cột "Decided by" và đưa ra thay đổi nhỏ nhất để khắc phục nó.
- Sửa mọi phát hiện ở mức HIGH và MEDIUM; giải thích bất kỳ phát hiện mức INFO nào mà bạn giữ nguyên.
- Thêm các bài kiểm thử `- path` cho các thư mục mã nguồn có tên phổ biến để làm lộ diện các quy tắc quá rộng.
- Chạy lại trình kiểm tra trên tệp đã sửa cho đến khi tất cả các bài kiểm thử đều vượt qua, và hiển thị phần khác biệt (diff).

### 4. Bàn giao kết quả
Điền vào mẫu `templates/gitignore-review.md`: tệp hiện tại đang làm gì, bảng kiểm thử, các phát hiện, toàn bộ tệp đã sửa lỗi, bản so sánh khác biệt (diff), và các câu lệnh để xác nhận (`git check-ignore -v`, `git status --ignored`) cũng như để bỏ theo dõi (untrack) các tệp đã lỡ được commit trước đó (`git rm --cached`).

## Kết quả ví dụ
Đối với `examples/webapp.gitignore` chạy cùng `examples/webapp-paths.txt`: 13/19 bài kiểm thử vượt qua, 1 phát hiện mức HIGH, 4 mức MEDIUM, 2 mức LOW, 1 mức INFO. Mức HIGH NEGATION_BLOCKED: `logs/` loại trừ cả thư mục, do đó `!logs/.gitkeep` không bao giờ có thể được áp dụng (cách sửa: `logs/*`). Mức MEDIUM: `config/secrets.yml # never commit` bị hiểu là một mẫu duy nhất, vì vậy tệp chứa thông tin bí mật này sẽ bị commit; ` Thumbs.db` có một khoảng trắng ở đầu dòng; `src\generated` bị đọc thành `srcgenerated`; và các tệp `*.key`, `*.pem` cùng `id_rsa` chưa được bao phủ. Mức LOW: `.env*` làm ẩn mất `.env.example`; `*.log` bị liệt kê hai lần. Mẫu `build` không được neo cũng làm ẩn luôn `src/build/README.md` (cách sửa: `/build/`). Tệp `examples/webapp-fixed.gitignore` vượt qua 19/19 bài kiểm thử mà không có bất kỳ phát hiện nào ở mức HIGH, MEDIUM hay LOW.

## Quy tắc

- Dòng khớp cuối cùng sẽ chiến thắng; một tệp nằm bên trong một thư mục đã bị loại trừ thì không bao giờ có thể được đưa vào lại (re-included). Hãy luôn nhắc lại điều này bất cứ khi nào có liên quan đến một dòng bắt đầu bằng `!`.
- Tuyệt đối không đề xuất bỏ qua các tệp khóa (`package-lock.json`, `poetry.lock`, `go.sum`, `Cargo.lock` của ứng dụng) hoặc cấu hình dùng chung mà nhóm chủ đích commit.
- Nếu một thông tin bí mật (secret) đã lỡ bị commit, hãy nói rõ rằng việc thêm nó vào ignore là chưa đủ: phải xoay vòng (rotate) khóa đó và cân nhắc viết lại lịch sử git.
- Ưu tiên các mẫu được neo ở thư mục gốc (`/dist/`, `/build/`) cho các thư mục đầu ra, và đặt các tệp cá nhân của trình soạn thảo cũng như hệ điều hành vào tệp ignore toàn cục (global ignore file) khi cả nhóm đồng thuận.
- Xác nhận trên kho lưu trữ thực tế bằng lệnh `git check-ignore -v`: kịch bản tuân theo các quy tắc trong tài liệu của git cho một tệp đơn lẻ, nhưng các tệp lồng nhau, `.git/info/exclude`, tệp toàn cục và thiết lập `core.ignorecase` có thể làm thay đổi kết quả.
FILE:references/gitignore-rules.md
# Cách git đọc tệp .gitignore

Nguồn: tài liệu hướng dẫn gitignore(5) (`git help gitignore`). Trình kiểm tra triển khai các quy tắc này và đã được đối chiếu với git 2.47 thực tế trên 6,956 lượt kiểm tra đường dẫn mà không có bất kỳ sự khác biệt nào.

## Nguồn gốc của các quy tắc (ưu tiên từ cao nhất trở xuống)
1. Các tùy chọn `--exclude` trên dòng lệnh (hiếm gặp; được sử dụng bởi một số công cụ).
2. Các tệp `.gitignore`: tệp nằm trong cùng thư mục với đường dẫn đang xét sẽ ưu tiên hơn các tệp nằm ở thư mục cấp trên. Các mẫu (patterns) có tính tương đối so với thư mục chứa tệp `.gitignore` đó.
3. `.git/info/exclude` - cục bộ trên một bản sao (clone) duy nhất, không bao giờ được commit. Rất phù hợp cho các tệp nháp cá nhân.
4. Tệp toàn cục trong `core.excludesFile` (mặc định là `~/.config/git/ignore`) - rất phù hợp cho các tệp của trình soạn thảo và hệ điều hành như `.DS_Store` và `*.swp`.

Bên trong một tệp, dòng khớp CUỐI CÙNG sẽ quyết định kết quả.

## Cú pháp dòng
| Dòng | Ý nghĩa |
|---|---|
| dòng trống | được bỏ qua, dùng làm khoảng phân cách |
| `# text` | chú thích (comment); chỉ có tác dụng khi nằm ngay đầu dòng. `foo # bar` được hiểu là mẫu có tên `foo # bar` |
| `\#file` | một tên tệp bắt đầu bằng ký tự `#` |
| `!pattern` | đưa trở lại (re-include) một đối tượng đã bị loại trừ bởi một dòng trước đó |
| `\!file` | một tên tệp bắt đầu bằng ký tự `!` |
| `name   ` | các khoảng trắng ở cuối dòng sẽ bị loại bỏ trừ khi được thoát bằng dấu gạch chéo ngược (`name\ `) |
| `  name` | các khoảng trắng ở đầu dòng ĐƯỢC GIỮ LẠI và trở thành một phần của tên |
| `\` | thoát (escape) ký tự kế tiếp; nó không bao giờ được dùng làm dấu phân cách đường dẫn |

## Dấu gạch chéo (slash) quyết định vị trí mà mẫu sẽ khớp
| Mẫu | Khớp với | Không khớp với |
|---|---|---|
| `build` | `build`, `src/build`, `a/b/build/x.js` (bất kỳ tệp hoặc thư mục nào có tên là build) | `build.sh` |
| `build/` | các thư mục có tên là build ở bất kỳ độ sâu nào | một TỆP (FILE) có tên là build |
| `/build` | chỉ khớp với `build` nằm ngay cạnh tệp .gitignore | `src/build` |
| `doc/frotz` | `doc/frotz` nằm ngay cạnh tệp .gitignore (dấu gạch chéo ở giữa có tác dụng neo giống như dấu gạch chéo ở đầu) | `a/doc/frotz` |
| `**/doc/frotz` | `doc/frotz` ở bất kỳ độ sâu nào | |

Quy tắc nằm lòng: không có dấu gạch chéo (hoặc chỉ có một dấu gạch chéo ở cuối) = khớp ở bất kỳ độ sâu nào; có dấu gạch chéo ở đầu hoặc ở giữa = tương đối so với thư mục chứa tệp .gitignore.

## Ký tự đại diện (Wildcards)
| Ký tự đại diện | Ý nghĩa |
|---|---|
| `*` | bất kỳ chuỗi ký tự nào ngoại trừ `/` (`*.log`, `/a*z` không khớp với `ab/cz`) |
| `?` | chính xác một ký tự bất kỳ ngoại trừ `/` |
| `[abc]`, `[a-z]`, `[!0-9]`, `[^0-9]` | một ký tự nằm trong (hoặc không nằm trong) tập hợp; các lớp ký tự kiểu `[[:digit:]]` đều hoạt động |
| `**/name` | `name` nằm trong bất kỳ thư mục nào, bao gồm cả thư mục gốc trên cùng |
| `dir/**` | mọi thứ nằm bên trong `dir`, nhưng không bao gồm chính bản thân `dir` |
| `a/**/b` | `a/b`, `a/x/b`, `a/x/y/b` |
| `foo**`, `**bar` | `**` không nằm cạnh dấu gạch chéo sẽ hoạt động giống như một dấu `*` đơn lẻ |

## Quy tắc thư mục cha (điều gây bất ngờ phổ biến nhất)
"Không thể đưa một tệp vào lại (re-include) nếu thư mục cha của tệp đó đã bị loại trừ." Git thậm chí còn không liệt kê nội dung bên trong một thư mục đã bị loại trừ, vì vậy một dòng `!` phía sau sẽ không bao giờ có cơ hội được xét tới.

```gitignore
# KHÔNG hoạt động: logs/ đã bị loại trừ dưới dạng cả một thư mục
logs/
!logs/.gitkeep

# hoạt động: loại trừ nội dung bên trong thư mục, giữ cho bản thân thư mục vẫn hiển thị
logs/*
!logs/.gitkeep

# hoạt động đối với các tệp nằm sâu hơn: đưa vào lại (re-include) từng cấp thư mục
/assets/*
!/assets/icons/
/assets/icons/*
!/assets/icons/app.svg
```

## Phong cách danh sách trắng (Whitelist style)
Bỏ qua mọi thứ, sau đó chỉ cho phép những gì bạn muốn:
```gitignore
*
!*/
!*.md
!src/**
```
`!*/` đưa mọi thư mục vào lại để các dòng `!` phía sau có thể chạm tới các tệp nằm bên trong chúng.

## Các tệp đã được theo dõi (Already tracked files)
.gitignore chỉ ảnh hưởng đến các tệp chưa được theo dõi (untracked files). Một tệp đã được commit sẽ vẫn tiếp tục được theo dõi:
```
git rm --cached path/to/file        # giữ tệp trên ổ đĩa, ngừng theo dõi nó
git rm -r --cached build/           # tương tự đối với một thư mục
```
Nếu đó là một thông tin bí mật (secret), việc xóa nó khỏi danh sách theo dõi là chưa đủ: hãy xoay vòng (rotate) khóa bí mật đó, bởi vì nó vẫn còn nằm trong lịch sử git.

## Các câu lệnh để xác nhận
```
git check-ignore -v path/to/file          # in ra tệp, dòng và mẫu đã bỏ qua đường dẫn đó (không in gì = không bị bỏ qua)
git check-ignore -v --no-index path       # dùng được cả cho các tệp đang được theo dõi (tracked)
git status --ignored --short              # liệt kê các tệp bị bỏ qua với ký hiệu "!!"
git ls-files --others --ignored --exclude-standard   # mọi tệp chưa được theo dõi đang bị bỏ qua
```
FILE:references/common-pitfalls.md
# Các bẫy .gitignore thường gặp và mã lỗi của trình kiểm tra

| Mã | Mức độ nghiêm trọng | Vấn đề xảy ra | Cách khắc phục |
|---|---|---|---|
| NEGATION_BLOCKED | HIGH | `!path/file` nằm sau một quy tắc loại trừ thư mục cha (`logs/`, `dist`, `a/*` đối với `a/b/c`) sẽ không bao giờ có hiệu lực, khiến tệp bạn định commit vẫn bị bỏ qua | Loại trừ nội dung bên trong (`logs/*` hoặc `logs/**`) thay vì cả thư mục, hoặc đưa vào lại từng cấp thư mục (`!a/b/`) |
| SECRETS_NOT_COVERED | MEDIUM | các tệp bí mật điển hình (`.env`, `*.pem`, `*.key`, `id_rsa`) sẽ bị commit khi chạy `git add .` | Thêm `.env`, `.env.*`, `*.pem`, `*.key`, `id_rsa*`; đồng thời sử dụng trình quét thông tin bí mật (secret scanner) trong CI |
| INLINE_COMMENT | MEDIUM | `secrets.yml # never commit` được hiểu là một mẫu duy nhất bao gồm cả phần văn bản chú thích, do đó `secrets.yml` KHÔNG hề bị bỏ qua | Đặt các chú thích trên dòng riêng của chúng |
| LEADING_SPACE | MEDIUM | ` Thumbs.db` chỉ khớp với một cái tên bắt đầu bằng dấu cách | Xóa dấu cách đó đi |
| BACKSLASH_SEPARATOR | MEDIUM | `src\generated` có nghĩa là "src" + ký tự "g" được thoát + "enerated" = `srcgenerated` | Luôn sử dụng `/`, kể cả trên Windows |
| ENV_EXAMPLE_IGNORED | LOW | `.env*` cũng làm ẩn luôn `.env.example`, tệp mẫu mà các lập trình viên khác cần dùng | Thêm `!.env.example` ở dòng phía sau nó |
| DUPLICATE | LOW | cùng một dòng xuất hiện hai lần; vô hại nhưng là dấu hiệu của việc gộp mã (merge) bằng cách sao chép-dán | Xóa bớt một dòng |
| DOUBLE_STAR | LOW | `foo**` thực chất chỉ là `foo*`; rất có thể tác giả định viết là `foo/**` | Chỉ sử dụng `**` dưới dạng `**/x`, `x/**` hoặc `a/**/b` |
| TRAILING_SPACE | LOW | các khoảng trắng ở cuối dòng bị âm thầm lược bỏ; nếu đó là chủ ý thì tệp sẽ không khớp | Xóa hoặc thoát (escape) chúng |
| CRLF | LOW | ký tự xuống dòng kiểu Windows trong tệp .gitignore | Lưu dưới dạng LF; thêm `* text=auto` vào tệp .gitattributes |
| ANCHORED_BY_SLASH | INFO | `src/gen` chỉ khớp ngay cạnh tệp .gitignore, chứ không khớp với `packages/a/src/gen` | Thêm `/` ở đầu để thể hiện rõ chủ ý hoặc thêm `**/` để khớp ở bất kỳ độ sâu nào |

## Các bẫy mà kịch bản không thể nhìn thấy (hãy kiểm tra thủ công)
- **Các tên không được neo nhưng quá rộng.** `build` cũng làm ẩn cả `src/build/` và `docs/build/`; `lib` làm ẩn mọi thư mục `lib` bao gồm cả mã nguồn. Hãy sử dụng `/build/` cho thư mục đầu ra ở cấp gốc. Hãy viết một bài kiểm thử `- path` cho bất kỳ thư mục mã nguồn nào có tên phổ biến.
- **Các tệp đã được theo dõi (Tracked files).** Việc thêm một quy tắc không làm bỏ theo dõi (untrack) các tệp đã được commit trước đó. Hãy chạy `git ls-files -ci --exclude-standard` để liệt kê các tệp đang được theo dõi nhưng khớp với quy tắc ignore, sau đó chạy `git rm --cached`.
- **Các tệp .gitignore lồng nhau.** Một tệp `.gitignore` nằm trong thư mục con sẽ thêm các quy tắc tương đối so với thư mục con đó và có thể đưa vào lại những gì mà tệp ở thư mục gốc đã loại trừ (trừ khi một thư mục cha đã bị loại trừ). Hãy chạy trình kiểm tra trên từng tệp với các đường dẫn tương đối so với thư mục của nó, hoặc xác nhận bằng lệnh `git check-ignore -v`.
- **Phân biệt chữ hoa chữ thường (Case sensitivity).** Trên macOS và Windows, thiết lập `core.ignorecase=true` khiến `*.JPG` khớp với cả `photo.jpg`; nhưng trên môi trường Linux CI thì không. Hãy viết cả hai dạng chữ hoa/thường hoặc sử dụng `*.[jJ][pP][gG]`.
- **Các tệp cá nhân nằm trong tệp dùng chung.** `.idea/`, `.vscode/`, `*.swp`, `.DS_Store` thuộc về tệp ignore toàn cục (`core.excludesFile`), mặc dù nhiều nhóm vẫn giữ chúng trong tệp của kho lưu trữ (repo) để tiện lợi. Đừng bỏ qua `.vscode/settings.json` nếu nhóm chủ đích chia sẻ tệp này.
- **Các tệp khóa (Lock files).** Đừng bỏ qua `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `poetry.lock`, `Cargo.lock` (đối với các ứng dụng) hoặc `go.sum`: chúng giúp quá trình biên dịch (build) có thể tái lập chính xác.
- **Đã thêm vào ignore nhưng vẫn ép commit.** `git add -f` sẽ ghi đè lên các quy tắc ignore; người đánh giá mã (reviewer) nên kiểm tra xem có kết quả biên dịch hoặc thông tin bí mật nào bị ép thêm vào hay không.
- **Các tệp ignore khác sử dụng quy tắc khác.** `.dockerignore` sử dụng `filepath.Match` của Go cộng với `**` và luôn được neo tại thư mục gốc của ngữ cảnh biên dịch (build context root - không có khái niệm "bất kỳ độ sâu nào" cho các tên thông thường). `.npmignore`, `.eslintignore` và `.prettierignore` tuân theo các quy tắc tương tự gitignore nhưng không phải lúc nào cũng giống ở mọi chi tiết. Hãy kiểm thử chúng bằng công cụ riêng của chúng.

## Các khối mẫu khởi đầu tốt
```gitignore
# Node
node_modules/
/dist/
/build/
npm-debug.log*
.npm/

# Python
__pycache__/
*.py[cod]
.venv/
.pytest_cache/
/dist/
*.egg-info/

# Secrets
.env
.env.*
!.env.example
*.pem
*.key
id_rsa*

# Logs that keep their folder
logs/*
!logs/.gitkeep
```
FILE:templates/gitignore-review.md
# Đánh giá .gitignore: <kho lưu trữ hoặc thư mục>

**Ngăn xếp (Stack):** <Node 20 + Vite / Python 3.12 / monorepo với pnpm workspaces>
**Các tệp đã đánh giá:** <.gitignore tại thư mục gốc, packages/api/.gitignore>
**Tóm tắt:** <một câu: vấn đề quan trọng nhất và bản sửa lỗi sẽ thay đổi điều gì>

## Tệp hiện tại đang làm gì
| Dòng | Mẫu (Pattern) | Ý nghĩa |
|---|---|---|
| <9> | <`logs/`> | <bỏ qua mọi thư mục có tên là logs, ở bất kỳ độ sâu nào, và mọi thứ bên trong nó> |

## Kiểm thử đường dẫn (Path tests)
| Kết quả | Đường dẫn | Kỳ vọng | Thực tế | Quyết định bởi |
|---|---|---|---|---|
| <FAIL> | <`logs/.gitkeep`> | <giữ lại (kept)> | <bỏ qua (ignored)> | <thư mục cha logs/ bị loại trừ bởi L9; L10 không thể đưa nó vào lại> |

## Các phát hiện (Findings)
| # | Mức độ nghiêm trọng | Mã | Dòng | Vấn đề | Cách khắc phục |
|---|---|---|---|---|---|
| 1 | <HIGH> | <NEGATION_BLOCKED> | <10> | <...> | <sử dụng logs/* thay vì logs/> |

## Tệp .gitignore đề xuất
```gitignore
<toàn bộ tệp đã sửa lỗi, với các chú thích nằm trên dòng riêng của chúng>
```

## Các thay đổi
```diff
- logs/
+ logs/*
```

## Áp dụng và xác nhận
1. Thay thế tệp, sau đó chạy: `git check-ignore -v <từng đường dẫn từ bài kiểm thử>`
2. Chạy `git status --ignored --short` và kiểm tra xem không có tệp quan trọng nào hiển thị dưới dạng `!!`
3. Bỏ theo dõi (untrack) các tệp hiện đã được bỏ qua nhưng từng bị commit trước đó: `git rm --cached <path>` (xoay vòng bất kỳ khóa bí mật nào đã từng bị commit)
4. Commit: `chore: fix .gitignore (<lý do ngắn gọn>)`

## Các việc cần làm tiếp theo (Follow-ups)
- <chuyển các tệp của trình soạn thảo sang tệp ignore toàn cục>
- <thêm trình quét thông tin bí mật (gitleaks, trufflehog) vào CI>
FILE:examples/webapp.gitignore
# Dependencies
node_modules
/vendor/

# Build output
dist/
build
*.log
logs/
!logs/.gitkeep

# Environment
.env*
config/secrets.yml # never commit

# OS and editor files
.DS_Store
.idea/
*.swp
 Thumbs.db

# Test coverage
coverage/**
!coverage/README.md

# Generated code
src\generated
*.log
FILE:examples/webapp-fixed.gitignore
# Dependencies
node_modules/
/vendor/

# Build output (root only, so src/build/ stays tracked)
/dist/
/build/
*.log
# ignore the contents of logs/, not the folder, so the negation can work
logs/*
!logs/.gitkeep

# Environment and secrets
.env
.env.*
!.env.example
*.pem
*.key
id_rsa*
# never commit
config/secrets.yml

# OS and editor files
.DS_Store
Thumbs.db
.idea/
*.swp

# Test coverage
coverage/**
!coverage/README.md

# Generated code
/src/generated/
FILE:examples/webapp-paths.txt
# Paths for examples/webapp.gitignore and webapp-fixed.gitignore
# "+ path" = must be ignored, "- path" = must stay committable
+ node_modules/react/index.js
+ packages/api/node_modules/lodash/lodash.js
+ vendor/autoload.php
- packages/vendor/README.md
+ dist/app.js
+ build/output.js
- src/build/README.md
+ logs/app.log
- logs/.gitkeep
+ .env
+ .env.local
- .env.example
+ config/secrets.yml
+ .DS_Store
+ Thumbs.db
+ coverage/lcov.info
- coverage/README.md
+ src/generated/types.ts
- src/index.ts
FILE:scripts/gitignore_check.py
#!/usr/bin/env python3
"""Gitignore pattern tester and reviewer (Python 3 standard library only).

Explains every rule in a .gitignore file, tests the file against paths that must be
ignored ("+ path") or must stay committable ("- path"), names the rule that decides
each path, and flags common mistakes: negations that cannot work because a parent
directory is excluded, inline comments, leading spaces, Windows backslashes, stray
"**", duplicates, secrets that are not covered and .env.example being ignored.

Matching follows git's documented gitignore rules for a single .gitignore at the
repository root: last matching rule wins, "!" re-includes, a trailing "/" matches
directories only, a "/" at the start or in the middle anchors the pattern to the
.gitignore's directory, "*" "?" "[...]" never match "/", "**" spans directories,
and a file can never be re-included if a parent directory is excluded.

Usage:
  python3 gitignore_check.py .gitignore [--test paths.txt] [--json]
  python3 gitignore_check.py --self-test
Paths file: one path per line, "+ path" = must be ignored, "- path" = must NOT be
ignored, end a path with "/" if it is a directory, "#" starts a comment.
Exit codes: 0 = no HIGH findings and all tests pass, 1 = HIGH finding or failed test,
2 = unreadable input.
"""
import argparse, json, re, sys

SEV_ORDER = {"HIGH": 0, "MEDIUM": 1, "LOW": 2, "INFO": 3}
SECRET_PROBES = [".env", "config/.env.production", "certs/server.key", "deploy/id_rsa",
                 "secrets/service-account.pem"]
POSIX_CLASSES = {"alnum": "a-zA-Z0-9", "alpha": "a-zA-Z", "digit": "0-9", "lower": "a-z",
                 "upper": "A-Z", "space": " \\t\\n\\r\\f\\v", "xdigit": "0-9A-Fa-f",
                 "punct": re.escape("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"), "blank": " \\t"}


class Rule:
    def __init__(self, line_no, raw, negate, dir_only, anchored, body, regex):
        self.line_no, self.raw, self.negate, self.dir_only = line_no, raw, negate, dir_only
        self.anchored, self.body, self.regex = anchored, body, regex

    def matches(self, rel_path, is_dir):
        if self.dir_only and not is_dir:
            return False
        target = rel_path if self.anchored else rel_path.rsplit("/", 1)[-1]
        return self.regex.fullmatch(target) is not None

    def label(self):
        return f"L{self.line_no} `{self.raw}`"


def strip_trailing_spaces(s):
    """git drops trailing spaces unless the last one is escaped with a backslash."""
    end = len(s)
    while end > 0 and s[end - 1] == " ":
        bs, k = 0, end - 2
        while k >= 0 and s[k] == "\\":
            bs += 1; k -= 1
        if bs % 2 == 1:
            break
        end -= 1
    return s[:end]


def class_to_regex(body):
    out, m = "", 0
    while m < len(body):
        ch = body[m]
        if ch == "[" and body.startswith("[:", m):
            end = body.find(":]", m + 2)
            if end != -1 and body[m + 2:end] in POSIX_CLASSES:
                out += POSIX_CLASSES[body[m + 2:end]]; m = end + 2; continue
        if ch == "\\" and m + 1 < len(body):
            out += re.escape(body[m + 1]); m += 2; continue
        if ch == "-" and 0 < m < len(body) - 1:
            out += "-"; m += 1; continue
        out += re.escape(ch); m += 1
    return out


def glob_to_regex(p):
    out, i, n = "", 0, len(p)
    while i < n:
        c = p[i]
        if c == "\\":
            if i + 1 < n:
                out += re.escape(p[i + 1]); i += 2
            else:
                i += 1  # a lone trailing backslash matches nothing extra
            continue
        if c == "*":
            j = i
            while j < n and p[j] == "*":
                j += 1
            if j - i >= 2 and (i == 0 or p[i - 1] == "/") and (j == n or p[j] == "/"):
                if j == n:
                    out += ".*"; i = j
                else:
                    out += "(?:.*/)?"; i = j + 1
                continue
            out += "[^/]*"; i = j; continue
        if c == "?":
            out += "[^/]"; i += 1; continue
        if c == "[":
            j = i + 1
            neg = j < n and p[j] in "!^"
            if neg:
                j += 1
            k = j + 1 if j < n and p[j] == "]" else j
            while k < n and p[k] != "]":
                if p[k] == "[" and p.startswith("[:", k):
                    end = p.find(":]", k + 2)
                    if end != -1:
                        k = end + 2; continue
                k += 2 if p[k] == "\\" else 1
            if k >= n:
                out += re.escape("["); i += 1; continue
            cls = class_to_regex(p[j:k])
            out += ("[^/" + cls + "]") if neg else ("(?!/)[" + cls + "]")
            i = k + 1; continue
        out += re.escape(c); i += 1
    return out


def parse_rules(text):
    rules, findings, seen = [], [], {}
    for no, raw in enumerate(text.split("\n"), 1):
        if raw.endswith("\r"):
            findings.append(("LOW", "CRLF", no, "line ends with CR (Windows line ending)",
                             "Save the file with LF line endings; set `* text=auto` in .gitattributes."))
            raw = raw[:-1]
        if not raw.strip() or raw.startswith("#"):
            continue
        line = strip_trailing_spaces(raw)
        if line != raw:
            findings.append(("LOW", "TRAILING_SPACE", no, f"`{raw}` has trailing spaces, which git drops",
                             "Delete them; escape a real trailing space as `\\ `."))
        if raw[0] in " \t":
            findings.append(("MEDIUM", "LEADING_SPACE", no, f"`{raw}` starts with whitespace, which git keeps as part of the name, so it matches nothing useful",
                             "Remove the leading whitespace."))
        m = re.search(r"\s#", line)
        if m:
            findings.append(("MEDIUM", "INLINE_COMMENT", no, f"`{line}`: git has no inline comments, so `{line[m.start():].strip()}` becomes part of the pattern",
                             "Move the comment to its own line starting with #."))
        if re.search(r"\\[A-Za-z0-9_]", line):
            findings.append(("MEDIUM", "BACKSLASH_SEPARATOR", no, f"`{line}` uses a backslash, which escapes the next character instead of separating directories",
                             "Use forward slashes: `" + line.replace("\\", "/") + "`."))
        key = line.strip()
        if key in seen:
            findings.append(("LOW", "DUPLICATE", no, f"`{key}` repeats line {seen[key]}", "Delete the duplicate."))
        else:
            seen[key] = no
        negate = line.startswith("!")
        body = line[1:] if negate else line
        if body.startswith("\\!") or body.startswith("\\#"):
            body = body[1:]
        if negate and not body:
            findings.append(("LOW", "EMPTY_NEGATION", no, "`!` alone does nothing", "Delete the line."))
            continue
        dir_only = body.endswith("/") and not body.endswith("\\/")
        if dir_only:
            body = body.rstrip("/")
        if not body:
            continue
        anchored = "/" in body
        if body.startswith("/"):
            body = body[1:]
        if any((m.start() > 0 and body[m.start() - 1] != "/") or (m.end() < len(body) and body[m.end()] != "/")
               for m in re.finditer(r"\*{2,}", body)):
            findings.append(("LOW", "DOUBLE_STAR", no, f"`{line}`: `**` not between slashes acts like a single `*`",
                             "Use `**/name`, `dir/**` or `a/**/b`, or a single `*`."))
        rules.append(Rule(no, line, negate, dir_only, anchored, body, re.compile(glob_to_regex(body), re.S)))
    return rules, findings


def last_match(rules, rel_path, is_dir):
    hit = None
    for r in rules:
        if r.matches(rel_path, is_dir):
            hit = r
    return hit


def check_path(rules, path):
    """Return (ignored, reason, blocked_rule) following git's parent-directory rule."""
    is_dir = path.endswith("/")
    parts = [p for p in path.strip("/").split("/") if p]
    for k in range(1, len(parts) + 1):
        sub = "/".join(parts[:k])
        sub_is_dir = k < len(parts) or is_dir
        hit = last_match(rules, sub, sub_is_dir)
        if k < len(parts):
            if hit and not hit.negate:
                own = last_match(rules, "/".join(parts), is_dir)
                blocked = own if own and own.negate else None
                return True, f"parent `{sub}/` excluded by {hit.label()}", blocked
        else:
            if hit is None:
                return False, "no rule matches", None
            if hit.negate:
                return False, f"re-included by {hit.label()}", None
            return True, f"ignored by {hit.label()}", None
    return False, "empty path", None


def literal_dirs(body):
    parts = body.split("/")[:-1]
    out = []
    for p in parts:
        if any(ch in p for ch in "*?[\\"):
            break
        out.append(p)
    return out


def lint_negations(rules):
    findings = []
    for idx, r in enumerate(rules):
        if not r.negate or not r.anchored:
            continue
        before = rules[:idx]
        dirs = literal_dirs(r.body)
        for k in range(1, len(dirs) + 1):
            sub = "/".join(dirs[:k])
            hit = last_match(before, sub, True)
            if hit and not hit.negate:
                findings.append(("HIGH", "NEGATION_BLOCKED", r.line_no,
                                 f"`{r.raw.strip()}` can never apply: its parent directory `{sub}/` is excluded by {hit.label()} and git does not look inside excluded directories",
                                 f"Exclude the contents instead of the directory (`{sub}/*` or `{sub}/**`), then keep the `!` rule; or re-include `!{sub}/` first."))
                break
    return findings


def lint_secrets(rules):
    findings = []
    missing = [p for p in SECRET_PROBES if not check_path(rules, p)[0]]
    if missing:
        findings.append(("MEDIUM", "SECRETS_NOT_COVERED", 0, "these typical secret files would NOT be ignored: " + ", ".join(missing),
                         "Add `.env`, `.env.*` (with `!.env.example`), `*.pem`, `*.key`, `id_rsa*` if the project could ever contain them."))
    if check_path(rules, ".env.example")[0]:
        findings.append(("LOW", "ENV_EXAMPLE_IGNORED", 0, "`.env.example` is ignored, so the documented template cannot be committed",
                         "Add `!.env.example` after the .env rule."))
    return findings


def explain(rule):
    body = rule.body
    kind = "directories" if rule.dir_only else "files or directories"
    verb = "Re-includes" if rule.negate else "Ignores"
    wild = any(ch in body for ch in "*?[")
    if rule.anchored and body.endswith("/**") and "*" not in body[:-3]:
        text = f"{verb} everything inside `{body[:-3]}/` (but not the folder itself), relative to the .gitignore folder"
    elif rule.anchored and body.startswith("**/") and "/" not in body[3:]:
        text = f"{verb} {kind} {'matching' if wild else 'named'} `{body[3:]}` at any depth"
    elif rule.anchored:
        text = f"{verb} {kind} {'matching' if wild else 'at'} `{body}` relative to the .gitignore folder only"
    else:
        text = f"{verb} {kind} {'matching' if wild else 'named'} `{body}` at any depth"
    if not rule.negate and not body.endswith("/**"):
        text += "; a matched directory hides everything inside it"
    if rule.negate:
        text += " (only works if no parent directory is excluded)"
    return text + "."


def read_tests(text):
    tests = []
    for no, line in enumerate(text.splitlines(), 1):
        s = line.strip()
        if not s or s.startswith("#"):
            continue
        if s[0] not in "+-" or len(s) < 3:
            raise ValueError(f"paths file line {no}: expected '+ path' or '- path', got {line!r}")
        tests.append((s[0] == "+", s[1:].strip()))
    return tests


def review(gi_text, tests_text=None):
    rules, findings = parse_rules(gi_text)
    findings += lint_negations(rules)
    findings += lint_secrets(rules)
    anchored = [r.line_no for r in rules if r.anchored and not r.negate and "/" in r.body
                and not r.raw.startswith(("/", "**/")) and not r.body.endswith("/**")]
    if anchored:
        findings.append(("INFO", "ANCHORED_BY_SLASH", anchored[0],
                         "lines " + ", ".join(map(str, anchored)) + " contain a slash, so they only match relative to the .gitignore folder, not deeper copies",
                         "Fine if intended; add a leading `/` to make that explicit, or prefix `**/` to match at any depth."))
    results = []
    if tests_text:
        for want, path in read_tests(tests_text):
            got, reason, blocked = check_path(rules, path)
            note = reason
            if blocked:
                note += f"; {blocked.label()} cannot re-include it"
            results.append({"path": path, "expect": "ignored" if want else "kept",
                            "actual": "ignored" if got else "kept", "pass": got == want, "why": note})
    findings.sort(key=lambda f: (SEV_ORDER[f[0]], f[2]))
    return {"rules": [{"line": r.line_no, "pattern": r.raw, "meaning": explain(r)} for r in rules],
            "findings": [dict(zip(("severity", "code", "line", "problem", "fix"), f)) for f in findings],
            "tests": results}


def render(name, rep):
    out = [f"# .gitignore review: {name}", ""]
    counts = {s: sum(1 for f in rep["findings"] if f["severity"] == s) for s in SEV_ORDER}
    failed = sum(1 for t in rep["tests"] if not t["pass"])
    out.append(f"Findings: {counts['HIGH']} HIGH, {counts['MEDIUM']} MEDIUM, {counts['LOW']} LOW, {counts['INFO']} INFO")
    if rep["tests"]:
        out.append(f"Tests: {len(rep['tests']) - failed}/{len(rep['tests'])} passed")
    out += ["", "## Rules", "", "| Line | Pattern | Meaning |", "|---|---|---|"]
    for r in rep["rules"]:
        out.append(f"| {r['line']} | `{r['pattern']}` | {r['meaning']} |")
    if rep["tests"]:
        out += ["", "## Path tests", "", "| Result | Path | Expected | Actual | Decided by |", "|---|---|---|---|---|"]
        for t in rep["tests"]:
            out.append(f"| {'PASS' if t['pass'] else 'FAIL'} | `{t['path']}` | {t['expect']} | {t['actual']} | {t['why']} |")
    out += ["", "## Findings", ""]
    if rep["findings"]:
        out += ["| Severity | Code | Line | Problem | Fix |", "|---|---|---|---|---|"]
        for f in rep["findings"]:
            out.append(f"| {f['severity']} | {f['code']} | {f['line'] or '-'} | {f['problem']} | {f['fix']} |")
    else:
        out.append("No findings.")
    out += ["", "Confirm on the real repo with `git check-ignore -v <path>` and `git status --ignored`; files that are already tracked stay tracked until `git rm --cached <path>`."]
    return "\n".join(out)


def self_test():
    cases = []
    def t(name, cond):
        cases.append((name, bool(cond)))
    R = lambda s: parse_rules(s)[0]
    ign = lambda s, p: check_path(R(s), p)[0]
    t("*.log matches at any depth", ign("*.log", "a/b/c.log") and ign("*.log", "x.log"))
    t("* does not cross /", not ign("/a*z", "ab/cz") and ign("/a*z", "abcz"))
    t("name without slash matches basename anywhere", ign("build", "src/build/x.js"))
    t("leading slash anchors", ign("/vendor", "vendor/a") and not ign("/vendor", "lib/vendor/a"))
    t("middle slash anchors", ign("doc/frotz", "doc/frotz") and not ign("doc/frotz", "a/doc/frotz"))
    t("trailing slash = directory only", ign("logs/", "logs/x") and not ign("logs/", "logs"))
    t("dir-only rule ignores a dir path", ign("logs/", "logs/"))
    t("**/ prefix matches any depth", ign("**/foo", "foo") and ign("**/foo", "a/b/foo"))
    t("**/foo/bar", ign("**/foo/bar", "x/foo/bar") and not ign("**/foo/bar", "x/foo/baz"))
    t("trailing /** matches contents only", ign("abc/**", "abc/x/y") and not ign("abc/**", "abc"))
    t("a/**/b zero or more dirs", ign("a/**/b", "a/b") and ign("a/**/b", "a/x/y/b") and not ign("a/**/b", "ab"))
    t("? matches one char not /", ign("/a?c", "abc") and not ign("/a?c", "a/c"))
    t("[abc] class", ign("file[12].txt", "file1.txt") and not ign("file[12].txt", "file3.txt"))
    t("[!x] negated class", ign("f[!0-9]", "fa") and not ign("f[!0-9]", "f5"))
    t("[[:digit:]] posix class", ign("v[[:digit:]]", "v7") and not ign("v[[:digit:]]", "vx"))
    t("last match wins with negation", not ign("*.txt\n!keep.txt", "keep.txt") and ign("*.txt\n!keep.txt", "a.txt"))
    t("order matters", ign("!keep.txt\n*.txt", "keep.txt"))
    t("parent dir excluded blocks negation", ign("logs/\n!logs/.gitkeep", "logs/.gitkeep"))
    t("dir/* allows negation", not ign("logs/*\n!logs/.gitkeep", "logs/.gitkeep"))
    t("dir/** allows negation", not ign("coverage/**\n!coverage/README.md", "coverage/README.md"))
    t("re-include dir then file", not ign("/a/*\n!/a/b/\n/a/b/*\n!/a/b/c", "a/b/c"))
    t("escaped # and !", ign("\\#notes", "#notes") and ign("\\!bang", "!bang"))
    t("comment lines skipped", not ign("# *.log", "a.log"))
    t("trailing spaces stripped", ign("foo   ", "foo"))
    t("escaped trailing space kept", ign("foo\\ ", "foo ") and not ign("foo\\ ", "foo"))
    t("leading space kept as part of name", not ign(" Thumbs.db", "Thumbs.db"))
    t("inline comment becomes pattern", not ign("secrets.yml # x", "secrets.yml"))
    t("backslash escapes, not a separator", not ign("src\\gen", "src/gen/a.ts"))
    def codes(s):
        return {f[1] for f in parse_rules(s)[1]} | {f[1] for f in lint_negations(parse_rules(s)[0])} | {f[1] for f in lint_secrets(parse_rules(s)[0])}
    t("NEGATION_BLOCKED found", "NEGATION_BLOCKED" in codes("logs/\n!logs/.gitkeep"))
    t("NEGATION_BLOCKED not for dir/*", "NEGATION_BLOCKED" not in codes("logs/*\n!logs/.gitkeep"))
    t("NEGATION_BLOCKED via dir/* deeper", "NEGATION_BLOCKED" in codes("a/*\n!a/b/c.txt"))
    t("INLINE_COMMENT found", "INLINE_COMMENT" in codes("x.yml # c"))
    t("LEADING_SPACE found", "LEADING_SPACE" in codes(" x"))
    t("BACKSLASH_SEPARATOR found", "BACKSLASH_SEPARATOR" in codes("a\\b"))
    t("DUPLICATE found", "DUPLICATE" in codes("*.log\n*.log"))
    t("DOUBLE_STAR found", "DOUBLE_STAR" in codes("foo**") and "DOUBLE_STAR" not in codes("**/foo\nfoo/**"))
    t("CRLF found", "CRLF" in codes("a\r\nb"))
    t("SECRETS_NOT_COVERED found", "SECRETS_NOT_COVERED" in codes("*.log"))
    t("secrets covered", "SECRETS_NOT_COVERED" not in codes(".env\n.env.*\n!.env.example\n*.key\n*.pem\nid_rsa*"))
    t("ENV_EXAMPLE_IGNORED found", "ENV_EXAMPLE_IGNORED" in codes(".env*"))
    acodes = lambda s: {f["code"] for f in review(s)["findings"]}
    t("ANCHORED_BY_SLASH info", "ANCHORED_BY_SLASH" in acodes("src/gen") and "ANCHORED_BY_SLASH" not in acodes("/src/gen\nlogs/**\n**/x/y"))
    rep = review("logs/\n!logs/.gitkeep\n", "+ logs/a.log\n- logs/.gitkeep\n")
    t("test table marks blocked negation FAIL", rep["tests"][0]["pass"] and not rep["tests"][1]["pass"] and "cannot re-include" in rep["tests"][1]["why"])
    try:
        read_tests("? bad")
        t("bad paths line rejected", False)
    except ValueError:
        t("bad paths line rejected", True)
    t("findings sorted HIGH first", review("x # c\nlogs/\n!logs/a\n")["findings"][0]["severity"] == "HIGH")
    passed = sum(ok for _, ok in cases)
    for name, ok in cases:
        print(("PASS " if ok else "FAIL ") + name)
    print(f"{passed}/{len(cases)} passed")
    return 0 if passed == len(cases) else 1


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("gitignore", nargs="?")
    ap.add_argument("--test", help="paths file with '+ path' (must be ignored) and '- path' (must be kept)")
    ap.add_argument("--json", action="store_true")
    ap.add_argument("--self-test", action="store_true")
    a = ap.parse_args()
    if a.self_test:
        return self_test()
    if not a.gitignore:
        ap.print_usage(sys.stderr); return 2
    try:
        gi = open(a.gitignore, encoding="utf-8", newline="").read()
        tests = open(a.test, encoding="utf-8").read() if a.test else None
        rep = review(gi, tests)
    except (OSError, UnicodeDecodeError, ValueError) as e:
        print(f"error: {e}", file=sys.stderr); return 2
    print(json.dumps(rep, indent=2) if a.json else render(a.gitignore, rep))
    bad = any(f["severity"] == "HIGH" for f in rep["findings"]) or any(not t["pass"] for t in rep["tests"])
    return 1 if bad else 0


if __name__ == "__main__":
    sys.exit(main())
Best PracticesPythonDebugging+2
F@f
0