Skill: neuyazvimyi/gitlab-code-reviewer (version 1.0.2)

--- name: gitlab-code-review description: Performs structured code reviews on GitLab Merge Requests. Use when the user provides a GitLab MR URL and wants to analyze the diff, generate engineering feedback, and/or post inline review comments back to the MR via GitLab API. Triggers on requests like: "review this MR", "check this merge request", "post review comments to GitLab", "analyze the diff at a gitlab URL", "do a code review for a merge request URL". Covers Java/Spring Boot, MongoDB, PostgreSQL, React, and TypeScript codebases. --- # GitLab MR Code Review ## Workflow ### 1. Read credentials and check token scope Credentials: `~/.openclaw/credentials/gitlab.json` ```json { "token": "glpat-xxx", "host": "https://gitlab.com", "ignore_patterns": ["*.min.js", "*.lock", "forms/*.json"] } ``` Required API scopes: - `api` — required for posting inline comments - `read_api` — sufficient for analysis only (no comment posting) **Always run token check first** to know upfront whether comments can be posted: ```bash python scripts/gitlab_client.py check-token <mr_url> ``` Output includes `"can_write": true/false`. If `false`, skip step 6 and inform the user that the token needs the `api` scope to post comments. Do NOT proceed to analysis and then fail at step 6. ### 2. Fetch MR metadata and diff ```bash python scripts/gitlab_client.py fetch-mr <mr_url> python scripts/gitlab_client.py fetch-diff <mr_url> ``` `fetch-diff` returns a JSON array. Each entry contains `new_path`, `old_path`, `diff` (unified diff text), and boolean flags `new_file`, `deleted_file`, `renamed_file`. > **Fallback**: if the `/diffs` endpoint returns HTTP 500 (some self-hosted GitLab instances), the script automatically retries via `/changes`. No manual intervention needed. ### 3. Filter files Use `ignore_matcher.py` to exclude files before analysis: ```python from ignore_matcher import filter_diffs reviewable = filter_diffs(all_diffs) # merges defaults + credentials ignore_patterns ``` **Default ignore patterns** (always applied, even without credentials file): `*.min.js`, `*.min.css`, `*.lock`, `package-lock.json`, `pnpm-lock.yaml`, `forms/*.json` Binary extensions (`.png`, `.jar`, `.class`, `.map`, etc.) are always skipped. ### 4. Analyze the diff - Analyze only modified lines (added/removed in the diff). Do not comment on unchanged context lines. - If the total diff is large, process file-by-file and aggregate results. - Read `references/review-guidelines.md` for all review rules, severity definitions, and comment format. **Focus areas:** - Java / Spring Boot — Clean Code, SOLID, transaction boundaries, lazy loading - MongoDB — query correctness, index coverage, atomicity - PostgreSQL — SQL correctness, isolation levels, index/schema migrations - React / TypeScript — hooks correctness, type safety, XSS, stale closures ### 5. Structure the chat summary Group findings by severity: ``` ## Code Review — <MR title> (<source_branch> → <target_branch>) ### Critical - `UserService.java:42` — Transaction wraps HTTP call; holds DB lock during network I/O. ### Major - `OrderRepository.java:87` — N+1: `findRolesByUserId` called inside loop. Use batch query. ### Minor - `PaymentDto.java:15` — Field name `val` is not descriptive. ### Decision: Needs changes ``` Decision options: **Pass** / **Needs changes** / **Reject** - Pass: no Critical or Major findings - Needs changes: one or more Major findings, no Critical - Reject: one or more Critical findings ### 6. Post inline comments to GitLab Only execute this step if `check-token` (step 1) returned `"can_write": true`. **Write comments to a temp JSON file, then post via `post_comments.py`.** Never use `python -c` with inline comment bodies — backticks and special characters break shell escaping. ```bash # 1. Write all findings to a JSON file cat > /tmp/mr_comments.json << 'EOF' [ { "file_path": "src/main/UserService.java", "line": 42, "body": "[CRITICAL] Transaction wraps HTTP call...\n\nSuggestion:\n```java\n// fix\n```" } ] EOF # 2. Post via script python scripts/post_comments.py <mr_url> /tmp/mr_comments.json ``` **How to determine the correct line number** from a diff hunk: ``` @@ -375,6 +375,8 @@ ← new file starts at line 375 unchanged line → 375 unchanged line → 376 unchanged line → 377 + added line → 378 ← use this number + added line → 379 ``` Count from the `+A` value in `@@ -X,Y +A,B @@` for new-file lines. Each comment body format (from `references/review-guidelines.md` §8): ``` [SEVERITY] <one-line issue> <2-4 sentence explanation referencing the diff.> Suggestion: ```<language> <corrected snippet> ``` ``` **Constraints:** - Do not auto-approve the MR. - Do not add labels or trigger pipelines. - Only post comment-type discussions (no approval API calls). - If a line is not in the diff, the API returns an error — log it and continue with the next comment. - On HTTP 403 `insufficient_scope`, the script stops immediately and prints a fix instruction. Do not retry. ## Behavior Rules - Strict engineering tone. No emotional language. No generic praise. - Analyze only the modified code in the diff. Do not speculate about code outside the diff. - Do not log or persist source code content. - Respect ignore patterns strictly. - For large diffs: process per file, deduplicate similar findings across files before final output. ## References - **Review rules, severity table, comment format**: `references/review-guidelines.md` - §2 Java & Spring Boot (Clean Code, transactions, N+1, concurrency) - §3 MongoDB (queries, indexes, atomicity) - §4 PostgreSQL (SQL correctness, isolation, migrations) - §5 React & TypeScript (hooks, type safety, security) - §6 SOLID & DDD alignment - §7 Severity classification table - §8 Inline comment format template

path: scripts/ignore_matcher.py
content: #!/usr/bin/env python3
"""
File ignore matcher for MR diff filtering.

Usage:
    python ignore_matcher.py <file_path> [<pattern1> <pattern2> ...]

Reads ignore_patterns from ~/.openclaw/credentials/gitlab.json,
then merges with built-in defaults and any extra patterns provided on the CLI.

Exits 0 if the file should be IGNORED, exits 1 if it should be REVIEWED.
Prints "IGNORE" or "REVIEW" to stdout.

Importable API:
    from ignore_matcher import should_ignore
    should_ignore("src/main/App.java")         # False  -> review
    should_ignore("package-lock.json")         # True   -> ignore
"""

import fnmatch
import json
import sys
from pathlib import Path


CREDS_PATH = Path.home() / ".openclaw" / "credentials" / "gitlab.json"

DEFAULT_IGNORE_PATTERNS = [
    "*.min.js",
    "*.min.css",
    "*.lock",
    "package-lock.json",
    "pnpm-lock.yaml",
    "forms/*.json",
]

# Binary / generated file extensions always skipped
BINARY_EXTENSIONS = {
    ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".ico",
    ".pdf", ".zip", ".tar", ".gz", ".rar", ".7z",
    ".woff", ".woff2", ".ttf", ".eot",
    ".class", ".jar", ".war", ".ear",
    ".exe", ".dll", ".so", ".dylib",
    ".pyc", ".pyo",
    ".map",  # source maps
}


def _load_credential_patterns() -> list[str]:
    if not CREDS_PATH.exists():
        return []
    try:
        with open(CREDS_PATH) as f:
            creds = json.load(f)
        return creds.get("ignore_patterns", [])
    except Exception:
        return []


def build_patterns(extra: list[str] | None = None) -> list[str]:
    """Merge default + credential + extra patterns, deduplicated, order preserved."""
    seen = set()
    patterns = []
    for p in DEFAULT_IGNORE_PATTERNS + _load_credential_patterns() + (extra or []):
        if p not in seen:
            seen.add(p)
            patterns.append(p)
    return patterns


def should_ignore(file_path: str, extra_patterns: list[str] | None = None) -> bool:
    """
    Return True if file_path matches any ignore pattern or has a binary extension.
    Matching is done against the full path, basename, and all path suffixes
    (e.g. "forms/file.json", "resources/forms/file.json") so that patterns like
    "forms/*.json" correctly match deep paths such as
    "task-service/src/main/resources/forms/file.json".
    """
    path = Path(file_path)

    # Binary extension check
    if path.suffix.lower() in BINARY_EXTENSIONS:
        return True

    patterns = build_patterns(extra_patterns)

    # Build candidates: full path, all trailing sub-paths, and basename.
    # Normalise separators to forward slash for consistent fnmatch behaviour.
    normalised = file_path.replace("\\", "/")
    parts = normalised.split("/")
    candidates = ["/".join(parts[i:]) for i in range(len(parts))]  # includes full path and basename

    for pattern in patterns:
        for candidate in candidates:
            if fnmatch.fnmatch(candidate, pattern):
                return True
    return False


def filter_diffs(diffs: list[dict], extra_patterns: list[str] | None = None) -> list[dict]:
    """
    Filter a list of diff objects (as returned by gitlab_client.fetch_diff).
    Returns only entries that should be reviewed.
    Uses new_path for matching; falls back to old_path for deleted files.
    """
    result = []
    for d in diffs:
        path = d.get("new_path") or d.get("old_path", "")
        if not should_ignore(path, extra_patterns):
            result.append(d)
    return result


# ---------------------------------------------------------------------------
# CLI entry point
# ---------------------------------------------------------------------------

def main():
    if len(sys.argv) < 2:
        print(__doc__)
        sys.exit(2)

    file_path = sys.argv[1]
    extra = sys.argv[2:] if len(sys.argv) > 2 else None

    if should_ignore(file_path, extra):
        print("IGNORE")
        sys.exit(0)
    else:
        print("REVIEW")
        sys.exit(1)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)