This commit adds a new infrastructure for CI-based testing that can leverage Anthropic AI's Claude sonnet model to assist when doing code reviews. The eventual goal is for the 0-day Robot to run this automatically for each series that comes into the mailing list.
Rather than relying on the GitHub infrastructure, this provides a few base tools for doing reviews as well as a prompting infrastructure that we can extend for domain specific hints to the LLM for analysis. The prompt is most useful for inline diffs, mimicing a real interactive development session on the mailing list. This can help to do a few rounds of internal commenting and development before submitting to the list (using your own API key), and then run a final pass based on the upstream. This is a first cut of the functionality. A good chunk of it was generated using Claude LLM, but there is also a heavy amount of manual editing involved as well. A first pass run has been posted to the list against the series found at: https://mail.openvswitch.org/pipermail/ovs-dev/2025-November/427714.html This can give a good idea of what currently is possible. Signed-off-by: Aaron Conole <[email protected]> --- .ci/ai_review/README.md | 197 +++++++++++ .ci/ai_review/prompts/review-start.md | 110 ++++++ .ci/ai_review/requirements.txt | 1 + .ci/ai_review/review.py | 411 +++++++++++++++++++++++ .ci/ai_review/run_code_review_session.sh | 302 +++++++++++++++++ 5 files changed, 1021 insertions(+) create mode 100644 .ci/ai_review/README.md create mode 100644 .ci/ai_review/prompts/review-start.md create mode 100644 .ci/ai_review/requirements.txt create mode 100755 .ci/ai_review/review.py create mode 100755 .ci/ai_review/run_code_review_session.sh diff --git a/.ci/ai_review/README.md b/.ci/ai_review/README.md new file mode 100644 index 0000000000..497fbc745a --- /dev/null +++ b/.ci/ai_review/README.md @@ -0,0 +1,197 @@ +# AI Review Suite for OVS + +This directory contains tools for conducting AI-powered code reviews of OVS +patches using Claude (Anthropic API). + +## Setup + +1. Install the required Python packages: + ```bash + pip install -r .ci/ai_review/requirements.txt + ``` + + Or install directly: + ```bash + pip install anthropic + ``` + +2. Set your Anthropic API key: + ```bash + export ANTHROPIC_API_KEY='your-api-key-here' + ``` + +## Usage + +### Batch Review Multiple Commits (Recommended) + +The easiest way to review multiple commits or email patches is using the +`run_code_review_session.sh` script: + +```bash +# Review last 3 commits +.ci/ai_review/run_code_review_session.sh HEAD~2 HEAD~1 HEAD + +# Review a range of commits from a branch +.ci/ai_review/run_code_review_session.sh $(git rev-list main..feature-branch) + +# Review specific commits +.ci/ai_review/run_code_review_session.sh abc123 def456 789ghi + +# Review patch files from email (with automatic email reply formatting) +.ci/ai_review/run_code_review_session.sh patch1.patch patch2.patch + +# Custom output directory +.ci/ai_review/run_code_review_session.sh HEAD~2 HEAD~1 HEAD \ + --output-dir /tmp/reviews +``` + +The script will: +1. Generate patches for each commit using `git format-patch` (or use provided +patch files) +2. Reset the tree to each commit's parent (for git commits) +3. Run the AI review +4. **If patch has email headers**: Format output as email reply with proper +To:, Subject:, In-Reply-To:, and References: headers +5. Save outputs as `message_0001`, `message_0002`, etc. +6. Restore your original git state when done + +#### Email Header Support + +When reviewing patches from email (e.g., from a mailing list), the script +automatically: +- Detects email headers (Message-ID, From, Subject, etc.) +- Formats the review output as a proper email reply: + - `To:` set to the original patch author (From:) + - `Subject:` prefixed with "Re:" + - `In-Reply-To:` set to original Message-ID + - `References:` includes the original Message-ID and any existing references + +This makes it easy to send the review back to the mailing list as a proper +threaded reply. + +### Basic Review (Single Patch) + +To review a patch file directly: + +```bash +.ci/ai_review/review.py path/to/patch.patch +``` + +This will output the review to stdout. + +### Save Review to File + +```bash +.ci/ai_review/review.py path/to/patch.patch --output review.txt +``` + +### Custom Prompt + +By default, the script uses the `review-start` prompt. To use a different +prompt: + +```bash +.ci/ai_review/review.py path/to/patch.patch --prompt custom-prompt +``` + +(This will look for `.ci/ai_review/prompts/custom-prompt.md`) + +### Advanced Options + +```bash +.ci/ai_review/review.py \ + path/to/patch.patch \ + --prompt review-start \ + --model claude-sonnet-4-20250514 \ + --max-tokens 16000 \ + --output review.txt +``` + +### Including Additional Context + +If the prompt references other scripts or files that Claude needs access to, you +can provide them as context: + +```bash +.ci/ai_review/review.py path/to/patch.patch \ + --context lib/odp-util.c \ + --context utilities/checkpatch.py \ + --output review.txt +``` + +You can specify `--context` multiple times to include multiple files. These +files will be included in the API call so Claude can reference them during the +review. + +### Disable Git Context + +By default, the script automatically extracts git context (current branch, +recent commits, commit SHA from patch). To disable this: + +```bash +.ci/ai_review/review.py path/to/patch.patch --no-git-context +``` + +## Command Line Options + +- `patch_file` (required): Path to the patch file to review +- `--prompt PROMPT`: Name of the prompt file to use (default: review-start) +- `--model MODEL`: Claude model to use (default: claude-sonnet-4-20250514) +- `--max-tokens N`: Maximum tokens for response (default: 16000) +- `--output FILE`: Output file for the review (default: stdout) +- `--context FILE`: Additional context file to include (can be used multiple +times) +- `--no-git-context`: Disable automatic git context extraction + +## Prompts + +Review prompts are stored in `.ci/ai_review/prompts/` as Markdown files. The +script will automatically load the specified prompt and combine it with the +patch content before sending to Claude. + +To create a new prompt, add a new `.md` file to the prompts directory. + +### Context Files for Prompts + +If your prompt references scripts, tools, or specific source files that Claude +should have access to, you have two options: + +#### Option 1: Automatic Loading with @ref: (Recommended) + +Add `@ref:filename` references directly in your prompt file. The script will +automatically detect and load these files: + +```markdown +<!-- In .ci/ai_review/prompts/review-start.md --> + +Please review this patch according to the coding standards in @ref:CODING_STYLE.md +and compare against the reference implementation in @ref:lib/common-functions.c + +When checking for memory leaks, refer to @ref:memory-patterns.md +``` + +The script will automatically: +- Detect all `@ref:filename` patterns in the prompt +- Load the referenced files (searches in prompts directory first, then relative +paths) +- Include them in the context sent to Claude + +#### Option 2: Manual Context with --context Flag + +You can also provide context files manually using the `--context` flag: + +```bash +.ci/ai_review/review.py patch.patch \ + --context CODING_STYLE.md \ + --context lib/common-functions.c +``` + +**Note:** Both methods can be combined. Files specified with `@ref:` in prompts +will be merged with files specified via `--context` flags. + +## Requirements + +- Python 3.6 or later +- anthropic Python package +- Valid Anthropic API key +- Git repository (script auto-detects git root) diff --git a/.ci/ai_review/prompts/review-start.md b/.ci/ai_review/prompts/review-start.md new file mode 100644 index 0000000000..55d2dea7ae --- /dev/null +++ b/.ci/ai_review/prompts/review-start.md @@ -0,0 +1,110 @@ +Produce a report of regressions found based on this template. + +- Reviews should be in plain text only. Do not use markdown, special +characters, emoji-alikes. Only plain text is suitable for the ovs-dev mailing +list. + +- Any long lines present in the unified diff should be preserved, but any +summary, comments, or questions you add should be wrapped at 79 characters. + +- Never mention line numbers when referencing code locations. Instead +use the function name and also call chain if it makes it more clear. Avoid +complex paragraphs, and instead use call chains fA()->fB() to explain. + +- Always end the report with a blank line. + +- The report must be conversational with undramatic wording, fit for sending +as a reply to the patch being analyzed to the ovs-dev mailing list. + +- Explain any regressions as questions about the code, but do not mention +the authors. Don't say "Did you do X?" but rather say, "Can this X?" or +"Does this code X?" + +- Vary question phrasing. Do not always start all questions in the same +manner. + +- Ask your question specifically about the sources you're referencing. + - If you suspect a leak, ask specifically about the resource being leaked. + "Does this code leak this thing?" + - Don't say "Does this loop have a bounds checking issue?" Name the variable + you think is overflowing: "Does this code overflow xyz[]" + +- Don't make long paragraphs, ask short questions backed up by code snippets, +or call chains. + +- Ensure that the code follows the official coding style guide found in +https://github.com/openvswitch/ovs/blob/main/Documentation/internals/contributing/coding-style.rst + +- Verify that the commit subject and message comply with the project's +submission guide found in +https://github.com/openvswitch/ovs/blob/main/Documentation/internals/contributing/submitting-patches.rst + +- For dynamic string management, confirm that functions like `ds_init()` are +not being called repeatedly in a loop when `ds_clear()` should be used to +reuse the buffer. + +- Verify proper use of `ds_init()`, `ds_clear()`, and `ds_destroy()` (no +redundant init, no leaks inside loops). + +- Check that all dynamically allocated resources (`xmalloc()`, `json_*()`, etc.) +are properly freed or reused. + +- Portability: Verify that the patch does not rely on undefined or +platform-specific behavior. + +- Error Handling: Ensure proper error detection, logging, and cleanup on +failure. + +- Readability & Maintainability: Evaluate naming, comments, and modularity. + +- Be sure to wrap all comments at 79 characters. + +- Make sure to check for whitespace errors (things like aligned whitespace at +the start of a line, and incorrect whitespace in includes). + +- Check for common mistake patters such as calling `strcmp` with NULL. + +Create a TodoWrite for these items, all of which your report should include: + +- [ ] git sha of the commit +- [ ] Author: line from the commit +- [ ] One line subject of the commit + +- [ ] A brief summary of the commit. Use the full commit message if the bug is +in the commit message itself. + +- [ ] A unified diff of the commit, quoted as though it's in an email reply. + - [ ] The diff must not be generated from existing context. + - [ ] You must regenerate the diff by calling out to semcode's commit + function, + using git log, or re-reading any patch files you were asked to review. + - [ ] You must ensure the quoted portions of the diff exactly match the + original commit or patch. + +- [ ] Place your questions about the regressions you found alongside the code + in the diff that introduced them. Do not put the quoting '> ' characters in + front of your new text. +- [ ] Place your questions as close as possible to the buggy section of code. + +- [ ] Snip portions of the quoted content unrelated to your review + - [ ] Create a TodoWrite with every hunk in the diff. Check every hunk + to see if it is relevant to the review comments. + - [ ] ensure diff headers are retained for the files owning any hunks keep + - [ ] Replace any content you snip with [ ... ] + - [ ] Never include diff headers for entirely snipped files + - [ ] snip entire files unrelated to the review comments + - [ ] snip entire hunks from quoted files if they unrelated to the review + - [ ] snip entire functions from the quoted hunks unrelated to the review + - [ ] snip any portions of large functions from quoted hunks if unrelated to + the review + - [ ] ensure you only keep enough quoted material for the review to make sense + - [ ] snip trailing hunks and files after your last review comments unless + you need them for the review to make sense + - [ ] The review should contain only the portions of hunks needed to explain the review's concerns. + +Use the following sample as a guideline: + +``` + +``` + diff --git a/.ci/ai_review/requirements.txt b/.ci/ai_review/requirements.txt new file mode 100644 index 0000000000..5df62e6a29 --- /dev/null +++ b/.ci/ai_review/requirements.txt @@ -0,0 +1 @@ +anthropic>=0.39.0 diff --git a/.ci/ai_review/review.py b/.ci/ai_review/review.py new file mode 100755 index 0000000000..3efc31132f --- /dev/null +++ b/.ci/ai_review/review.py @@ -0,0 +1,411 @@ +#!/usr/bin/env python3 +""" +AI-powered code review script for OVS patches. + +This script uses Claude (Anthropic API) to review git patches based on +prompts defined in .ci/ai_review/prompts/. +""" + +import argparse +import os +import re +import subprocess +import sys +from pathlib import Path +from typing import Optional, List, Dict, Set + +try: + import anthropic +except ImportError: + print("Error: anthropic package not installed.", file=sys.stderr) + print("Please install it with: pip install anthropic", file=sys.stderr) + sys.exit(1) + + +def find_git_root() -> Path: + """Find the root of the git repository.""" + current = Path.cwd() + while current != current.parent: + if (current / ".git").exists(): + return current + current = current.parent + raise RuntimeError("Not in a git repository") + + +def find_referenced_files(content: str, prompts_dir: Path) -> Set[Path]: + """ + Find all files referenced in the content using @ref:filename pattern. + + Args: + content: The text content to scan for references + prompts_dir: Directory where prompt files are located + + Returns: + Set of Path objects for referenced files + """ + # Pattern to match @ref:filename.md or @ref:path/to/file.ext + pattern = r'@ref:([^\s\)]+)' + matches = re.findall(pattern, content) + + referenced_files = set() + for match in matches: + # Try relative to prompts directory first + ref_path = prompts_dir / match + if ref_path.exists(): + referenced_files.add(ref_path) + else: + # Try as absolute or relative to git root + ref_path = Path(match) + if ref_path.exists(): + referenced_files.add(ref_path) + else: + print(f"Warning: Referenced file not found: {match}", file=sys.stderr) + + return referenced_files + + +def read_prompt_file(prompts_dir: Path, prompt_name: str) -> tuple[str, Set[Path]]: + """ + Read a prompt file from the prompts directory and find referenced files. + + Args: + prompts_dir: Directory where prompt files are located + prompt_name: Name of the prompt (without .md extension) + + Returns: + Tuple of (prompt_content, set_of_referenced_files) + """ + prompt_file = prompts_dir / f"{prompt_name}.md" + if not prompt_file.exists(): + raise FileNotFoundError(f"Prompt file not found: {prompt_file}") + + with open(prompt_file, 'r', encoding='utf-8') as f: + content = f.read() + + # Find all referenced files + referenced_files = find_referenced_files(content, prompts_dir) + + return content, referenced_files + + +def read_patch_file(patch_path: Path) -> str: + """Read the patch file contents.""" + if not patch_path.exists(): + raise FileNotFoundError(f"Patch file not found: {patch_path}") + + with open(patch_path, 'r', encoding='utf-8') as f: + return f.read() + + +def get_api_key() -> str: + """Get the Anthropic API key from environment.""" + api_key = os.environ.get("ANTHROPIC_API_KEY") + if not api_key: + raise RuntimeError( + "ANTHROPIC_API_KEY environment variable not set.\n" + "Please set it with: export ANTHROPIC_API_KEY='your-api-key'" + ) + return api_key + + +def run_git_command(git_root: Path, command: List[str]) -> Optional[str]: + """ + Run a git command and return its output. + + Args: + git_root: Root of the git repository + command: Git command as list (e.g., ['log', '--oneline', '-1']) + + Returns: + Command output as string, or None if command fails + """ + try: + result = subprocess.run( + ['git'] + command, + cwd=git_root, + capture_output=True, + text=True, + timeout=30 + ) + if result.returncode == 0: + return result.stdout.strip() + else: + print(f"Git command failed: {' '.join(command)}", file=sys.stderr) + print(f"Error: {result.stderr}", file=sys.stderr) + return None + except subprocess.TimeoutExpired: + print(f"Git command timed out: {' '.join(command)}", file=sys.stderr) + return None + except Exception as e: + print(f"Error running git command: {e}", file=sys.stderr) + return None + + +def extract_git_context(git_root: Path, patch_content: str) -> Dict[str, str]: + """ + Extract git repository context that might be useful for the review. + + Args: + git_root: Root of the git repository + patch_content: The patch content (may contain commit info) + + Returns: + Dictionary with git context information + """ + context = {} + + # Try to extract commit SHA from patch if it's a git format-patch style + lines = patch_content.split('\n') + for line in lines[:50]: # Check first 50 lines + if line.startswith('From '): + parts = line.split() + if len(parts) >= 2 and len(parts[1]) >= 7: + context['commit_sha'] = parts[1][:40] # Full or short SHA + break + + # Get current branch info + branch = run_git_command(git_root, ['rev-parse', '--abbrev-ref', 'HEAD']) + if branch: + context['current_branch'] = branch + + # Get recent commits for context + recent_log = run_git_command(git_root, ['log', '--oneline', '-5']) + if recent_log: + context['recent_commits'] = recent_log + + return context + + +def read_context_files(context_paths: List[Path]) -> Dict[str, str]: + """ + Read additional context files (e.g., related source files, scripts). + + Args: + context_paths: List of file paths to include as context + + Returns: + Dictionary mapping file paths to their contents + """ + context_files = {} + + for path in context_paths: + if not path.exists(): + print(f"Warning: Context file not found: {path}", file=sys.stderr) + continue + + try: + with open(path, 'r', encoding='utf-8') as f: + content = f.read() + context_files[str(path)] = content + print(f"Loaded context file: {path}", file=sys.stderr) + except Exception as e: + print(f"Warning: Could not read {path}: {e}", file=sys.stderr) + + return context_files + + +def conduct_review( + patch_content: str, + prompt_content: str, + git_context: Dict[str, str] = None, + context_files: Dict[str, str] = None, + model: str = "claude-sonnet-4-20250514", + max_tokens: int = 16000 +) -> str: + """ + Conduct a code review using Claude API. + + Args: + patch_content: The patch file content to review + prompt_content: The review instructions/prompt + git_context: Git repository context (commit info, branches, etc.) + context_files: Additional context files (scripts, related sources) + model: The Claude model to use + max_tokens: Maximum tokens for the response + + Returns: + The review text from Claude + """ + api_key = get_api_key() + client = anthropic.Anthropic(api_key=api_key) + + # Construct the full message with all context + message_parts = [prompt_content] + + # Add git context if available + if git_context: + message_parts.append("\n## Git Repository Context\n") + for key, value in git_context.items(): + message_parts.append(f"{key}: {value}\n") + + # Add context files if provided + if context_files: + message_parts.append("\n## Additional Context Files\n") + for file_path, content in context_files.items(): + message_parts.append(f"\n### File: {file_path}\n") + message_parts.append(f"```\n{content}\n```\n") + + # Add the patch to review + message_parts.append("\n## Patch to Review\n") + message_parts.append(patch_content) + + message_content = "".join(message_parts) + + print(f"Starting review with model: {model}", file=sys.stderr) + print(f"Patch size: {len(patch_content)} characters", file=sys.stderr) + if git_context: + print(f"Git context items: {len(git_context)}", file=sys.stderr) + if context_files: + print(f"Context files: {len(context_files)}", file=sys.stderr) + + try: + message = client.messages.create( + model=model, + max_tokens=max_tokens, + messages=[ + { + "role": "user", + "content": message_content + } + ] + ) + + # Extract the text response + response_text = "" + for block in message.content: + if block.type == "text": + response_text += block.text + + return response_text + + except anthropic.APIError as e: + print(f"Anthropic API error: {e}", file=sys.stderr) + raise + except Exception as e: + print(f"Unexpected error during review: {e}", file=sys.stderr) + raise + + +def main(): + """Main entry point for the review script.""" + parser = argparse.ArgumentParser( + description="Conduct AI-powered code review of OVS patches using Claude" + ) + parser.add_argument( + "patch_file", + type=Path, + help="Path to the patch file to review" + ) + parser.add_argument( + "--prompt", + type=str, + default="review-start", + help="Name of the prompt to use (without .md extension). Default: review-start" + ) + parser.add_argument( + "--model", + type=str, + #default="claude-sonnet-4-20250929-v1", + default="claude-sonnet-4-20250514", + help="Claude model to use. Default: claude-sonnet-4-20250514" + ) + parser.add_argument( + "--max-tokens", + type=int, + default=16000, + help="Maximum tokens for the response. Default: 16000" + ) + parser.add_argument( + "--output", + type=Path, + help="Output file for the review (default: stdout)" + ) + parser.add_argument( + "--context", + type=Path, + action="append", + dest="context_files", + help="Additional context files to include (can be specified multiple times)" + ) + parser.add_argument( + "--no-git-context", + action="store_true", + help="Disable automatic git context extraction" + ) + + args = parser.parse_args() + + try: + # Find git root and construct paths + git_root = find_git_root() + prompts_dir = git_root / ".ci" / "ai_review" / "prompts" + + if not prompts_dir.exists(): + print(f"Error: Prompts directory not found: {prompts_dir}", file=sys.stderr) + sys.exit(1) + + # Read the prompt and find referenced files + print(f"Reading prompt: {args.prompt}", file=sys.stderr) + prompt_content, referenced_files = read_prompt_file(prompts_dir, args.prompt) + + if referenced_files: + print(f"Found {len(referenced_files)} referenced file(s) in prompt:", file=sys.stderr) + for ref_file in referenced_files: + print(f" - {ref_file}", file=sys.stderr) + + print(f"Reading patch: {args.patch_file}", file=sys.stderr) + patch_content = read_patch_file(args.patch_file) + + # Extract git context unless disabled + git_context = None + if not args.no_git_context: + print("Extracting git context...", file=sys.stderr) + git_context = extract_git_context(git_root, patch_content) + + # Merge referenced files with explicitly provided context files + all_context_paths = list(referenced_files) + if args.context_files: + all_context_paths.extend(args.context_files) + + # Read all context files + context_files = None + if all_context_paths: + print(f"Loading {len(all_context_paths)} total context file(s)...", file=sys.stderr) + context_files = read_context_files(all_context_paths) + + # Conduct the review + review_result = conduct_review( + patch_content=patch_content, + prompt_content=prompt_content, + git_context=git_context, + context_files=context_files, + model=args.model, + max_tokens=args.max_tokens + ) + + # Output the result + if args.output: + with open(args.output, 'w', encoding='utf-8') as f: + f.write(review_result) + print(f"Review written to: {args.output}", file=sys.stderr) + else: + print(review_result) + + print("\nReview completed successfully!", file=sys.stderr) + + except FileNotFoundError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + except RuntimeError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + except KeyboardInterrupt: + print("\nReview interrupted by user", file=sys.stderr) + sys.exit(130) + except Exception as e: + print(f"Unexpected error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/.ci/ai_review/run_code_review_session.sh b/.ci/ai_review/run_code_review_session.sh new file mode 100755 index 0000000000..333b9a5510 --- /dev/null +++ b/.ci/ai_review/run_code_review_session.sh @@ -0,0 +1,302 @@ +#!/bin/bash +# Script to review multiple git commits using AI + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REVIEW_SCRIPT="$SCRIPT_DIR/review.py" +GIT_ROOT="$(git rev-parse --show-toplevel)" +WORK_DIR="$GIT_ROOT/.ci/ai_review/reviews" + +# Check if API key is set +if [ -z "$ANTHROPIC_API_KEY" ]; then + echo "Error: ANTHROPIC_API_KEY environment variable not set" + echo "Please set it with: export ANTHROPIC_API_KEY='your-api-key'" + exit 1 +fi + +# Function to extract email header +extract_header() { + local file="$1" + local header="$2" + # Extract header value, handling multi-line headers + awk -v header="$header:" ' + tolower($0) ~ "^" tolower(header) { + sub(/^[^:]+: */, "") + value = $0 + # Handle continuation lines (starting with whitespace) + while (getline > 0 && /^[ \t]/) { + sub(/^[ \t]+/, " ") + value = value $0 + } + print value + exit + } + ' "$file" +} + +# Function to check if patch has email headers +has_email_headers() { + local file="$1" + grep -q "^Message-ID:" "$file" || grep -q "^Message-Id:" "$file" +} + +# Function to generate email headers for reply +generate_email_headers() { + local to="$1" + local subject="$2" + local message_id="$3" + local references="$4" + + # Generate To: header + if [ -n "$to" ]; then + echo "To: $to" + fi + + # Generate Subject: header + if [ -n "$subject" ]; then + # Remove any existing Re: prefix and add our own + subject=$(echo "$subject" | sed 's/^[Rr][Ee]: *//') + echo "Subject: Re: $subject" + fi + + # Generate In-Reply-To: header + if [ -n "$message_id" ]; then + echo "In-Reply-To: $message_id" + fi + + # Generate References: header + if [ -n "$message_id" ]; then + # Add the original Message-ID to references + if [ -n "$references" ]; then + # Append to existing references + echo "References: $references $message_id" + else + echo "References: $message_id" + fi + elif [ -n "$references" ]; then + echo "References: $references" + fi + + echo "" +} + +usage() { + cat <<EOF +Usage: $0 <commit1> <commit2> [commit3 ...] +Usage: $0 <patch or mbox file> + +Review multiple git commits using AI-powered code review. + +Arguments: + commit1 commit2 ... Git commit IDs to review (can be SHAs, branches, tags, + etc.) + patch or mbox file... Patch file or MBOX file. + +Options: + --output-dir DIR Directory to save reviews (default: .ci/ai_review/reviews) + --prompt PROMPT Prompt to use (default: review-start) + --help Show this help message + +Examples: + # Review last 3 commits + $0 HEAD~2 HEAD~1 HEAD + + # Review a range of commits + $0 \$(git rev-list main..feature-branch) + + # Review specific commits + $0 abc123 def456 789ghi + +Output: + Reviews will be saved in the output directory as: + message_0001 (review of first commit) + message_0002 (review of second commit) + ... +EOF + exit 0 +} + +# Parse arguments +OUTPUT_DIR="" +PROMPT="review-start" +COMMITS=() + +while [[ $# -gt 0 ]]; do + case $1 in + --help|-h) + usage + ;; + --output-dir) + OUTPUT_DIR="$2" + shift 2 + ;; + --prompt) + PROMPT="$2" + shift 2 + ;; + *) + COMMITS+=("$1") + shift + ;; + esac +done + +# Check if commits were provided +if [ ${#COMMITS[@]} -eq 0 ]; then + echo "Error: No commits specified" + echo "" + usage +fi + +# Set default output directory if not specified +if [ -z "$OUTPUT_DIR" ]; then + OUTPUT_DIR="$WORK_DIR" +fi + +# Create output directory +mkdir -p "$OUTPUT_DIR" + +echo "===================================" +echo "AI Review Tool - Batch Mode" +echo "===================================" +echo "Git Root: $GIT_ROOT" +echo "Output Directory: $OUTPUT_DIR" +echo "Prompt: $PROMPT" +echo "Number of commits: ${#COMMITS[@]}" +echo "" + +# Save current branch/HEAD for restoration +ORIGINAL_HEAD=$(git rev-parse HEAD) +ORIGINAL_BRANCH=$(git symbolic-ref -q HEAD || echo "") + +# Function to restore git state +restore_git_state() { + echo "" + echo "Restoring git state..." + if [ -n "$ORIGINAL_BRANCH" ]; then + git checkout -q "${ORIGINAL_BRANCH#refs/heads/}" + else + git checkout -q "$ORIGINAL_HEAD" + fi +} + +# Set trap to restore state on exit +trap restore_git_state EXIT + +# Process each commit +counter=1 +for commit in "${COMMITS[@]}"; do + # Format counter with leading zeros (0001, 0002, etc.) + counter_formatted=$(printf "%04d" $counter) + PATCH_FILE="$OUTPUT_DIR/patch_${counter_formatted}.patch" + + if [ ! -f "$commit" ]; then + echo "===================================" + echo "Processing commit $counter_formatted: $commit" + echo "===================================" + + # Verify commit exists + if ! git rev-parse --verify "$commit" >/dev/null 2>&1; then + echo "Error: Invalid commit: $commit" + echo "Skipping..." + echo "" + counter=$((counter + 1)) + continue + fi + + # Get full commit SHA + COMMIT_SHA=$(git rev-parse "$commit") + echo "Commit SHA: $COMMIT_SHA" + + # Reset tree to COMMIT~1 + PARENT_COMMIT="${COMMIT_SHA}~1" + + # Check if parent exists (not initial commit) + if git rev-parse --verify "$PARENT_COMMIT" >/dev/null 2>&1; then + echo "Checking out parent: $PARENT_COMMIT" + git checkout -q "$PARENT_COMMIT" + else + echo "This is the initial commit, checking out commit itself" + git checkout -q "$COMMIT_SHA" + fi + + # Generate patch file + echo "Generating patch: $PATCH_FILE" + git format-patch -1 "$COMMIT_SHA" --stdout > "$PATCH_FILE" + else + echo "===================================" + echo "Processing Patch $counter_formatted: $commit" + echo "===================================" + + cp "$commit" "$PATCH_FILE" + fi + + # Run review to temporary file first + TEMP_REVIEW=$(mktemp) + echo "Running AI review..." + echo "Output: $OUTPUT_FILE" + + if "$REVIEW_SCRIPT" "$PATCH_FILE" --prompt "$PROMPT" --output "$TEMP_REVIEW"; then + echo "✓ Review completed successfully" + + # Check if patch has email headers + if has_email_headers "$PATCH_FILE"; then + echo " Detected email headers, formatting as reply..." + + # Extract email headers + FROM=$(extract_header "$PATCH_FILE" "From") + SUBJECT=$(extract_header "$PATCH_FILE" "Subject") + MESSAGE_ID=$(extract_header "$PATCH_FILE" "Message-ID") + [ -z "$MESSAGE_ID" ] && MESSAGE_ID=$(extract_header "$PATCH_FILE" "Message-Id") + REFERENCES=$(extract_header "$PATCH_FILE" "References") + + # Create output with email headers + OUTPUT_FILE="$OUTPUT_DIR/message_${counter_formatted}" + { + generate_email_headers "$FROM" "$SUBJECT" "$MESSAGE_ID" "$REFERENCES" + cat "$TEMP_REVIEW" + } > "$OUTPUT_FILE" + + echo " To: $FROM" + echo " Subject: Re: $(echo "$SUBJECT" | sed 's/^[Rr][Ee]: *//')" + else + # No email headers, just copy the review + OUTPUT_FILE="$OUTPUT_DIR/message_${counter_formatted}" + cp "$TEMP_REVIEW" "$OUTPUT_FILE" + fi + + rm -f "$TEMP_REVIEW" + + # Show summary + REVIEW_SIZE=$(wc -l < "$OUTPUT_FILE") + echo " Review size: $REVIEW_SIZE lines" + else + echo "✗ Review failed" + rm -f "$TEMP_REVIEW" + echo " Check $OUTPUT_FILE for details" + fi + + if [ -f "$commit" ]; then + echo "✓ Advancing tree by running [ git am \"$commit\" ]..." + git am "$commit" + fi + + echo "" + counter=$((counter + 1)) +done + +# Restore git state (will also be called by trap) +restore_git_state + +echo "===================================" +echo "All reviews completed!" +echo "===================================" +echo "Output directory: $OUTPUT_DIR" +echo "Files generated:" +ls -1 "$OUTPUT_DIR"/message_* 2>/dev/null | while read -r file; do + echo " - $(basename "$file")" +done +echo "" +echo "To view a review:" +echo " cat $OUTPUT_DIR/message_0001" -- 2.51.0 _______________________________________________ dev mailing list [email protected] https://mail.openvswitch.org/mailman/listinfo/ovs-dev
