#!/usr/bin/env python3 # ================= MOMO 系統 - Aider Code Review 自動化腳本 ================= # 功能:程式完成後自動觸發 Aider 進行 Code Review # 作者:AI Assistant # 版本:1.0 # ======================================================================= import argparse import os import subprocess import sys from datetime import datetime from pathlib import Path # 導入工具類 try: from scripts.code_review_utils import CodeReviewLogger, CodeReviewConfig, CodeReviewValidator, GitHelper except ImportError: # 嘗試直接導入(當在scripts目錄中執行時) try: from code_review_utils import CodeReviewLogger, CodeReviewConfig, CodeReviewValidator, GitHelper except ImportError: print("❌ 無法導入工具類,請確保 code_review_utils.py 存在") sys.exit(1) class AiderCodeReview: def __init__(self, project_root=None): self.project_root = Path(project_root) if project_root else Path(__file__).parent.parent # 初始化工具類 self.logger = CodeReviewLogger(self.project_root) self.config = CodeReviewConfig(self.project_root) self.validator = CodeReviewValidator() self.git_helper = GitHelper(self.project_root) # 檢查Git倉庫 if not self.git_helper.is_git_repository(): self.logger.warning("當前目錄不是Git倉庫,某些功能可能無法使用") # 檢查Aider路徑 self.aider_path = self.config.get("aider.path", "/Users/ooo/.local/bin/aider") if not Path(self.aider_path).exists(): self.logger.error(f"Aider 路徑不存在: {self.aider_path}") def log_review(self, message): """記錄Code Review日誌(保持向後兼容)""" self.logger.info(message) def get_changed_files(self, git_diff_filter="ACM"): """獲取Git中變更的檔案""" try: # 使用GitHelper獲取變更檔案 all_changed = self.git_helper.get_changed_files( diff_filter=git_diff_filter, use_staged=self.config.get("git.use_staged", True) ) # 過滤允許的檔案類型 allowed_extensions = self.config.get("file_filters.allowed_extensions", ['.py', '.js', '.ts', '.jsx', '.tsx', '.html', '.css']) filtered_files = [] for file_path in all_changed: if any(file_path.endswith(ext) for ext in allowed_extensions): # 檢查檔案是否存在且大小合理 abs_path = self.project_root / file_path if abs_path.exists() and abs_path.is_file(): file_size = abs_path.stat().st_size max_size = self.config.get("file_filters.max_file_size", 10485760) if file_size <= max_size: filtered_files.append(file_path) else: self.logger.warning(f"檔案過大,跳過: {file_path} ({file_size} bytes)") else: self.logger.warning(f"檔案不存在,跳過: {file_path}") return filtered_files except Exception as e: self.logger.error("獲取變更檔案失敗", e) return [] def run_aider_review(self, files=None, review_type="basic"): """執行Aider Code Review""" if not files: files = self.get_changed_files() if not files: self.log_review("📝 沒有需要Review的檔案") return True # 驗證Review類型 if not self.validator.validate_review_type(review_type): self.logger.error(f"無效的Review類型: {review_type}") return False # 驗證檔案 valid_files = self.validator.validate_files(files, self.project_root) if not valid_files: self.logger.error("沒有有效的檔案可以Review") return False # 構建Aider命令 cmd = [self.aider_path] # 從配置獲取Review類型對應的訊息 review_message = self.config.get(f"review_types.{review_type}.message") if not review_message: self.logger.error(f"找不到Review類型 {review_type} 的配置") return False # 添加Aider參數 if self.config.get("aider.auto_yes", False): cmd.append("--yes") if self.config.get("aider.no_git", True): cmd.append("--no-git") cmd.extend(["--message", review_message]) # 添加要review的檔案 cmd.extend(valid_files) self.log_review(f"🔍 開始Code Review - 類型: {review_type}, 檔案: {', '.join(valid_files)}") try: # 設置環境變數 env = os.environ.copy() env["PYTHONPATH"] = str(self.project_root) # 執行Aider timeout = self.config.get("aider.timeout", 600) result = subprocess.run( cmd, cwd=self.project_root, capture_output=True, text=True, env=env, timeout=timeout ) if result.returncode == 0: self.log_review("✅ Code Review 完成") # 保存Review結果 self.save_review_result(valid_files, result.stdout, review_type) return True else: self.logger.error(f"Code Review 失敗: {result.stderr}") return False except subprocess.TimeoutExpired: self.logger.error("Code Review 超時") return False except Exception as e: self.logger.error("Code Review 異常", e) return False def save_review_result(self, files, output, review_type): """保存Review結果到檔案""" try: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") safe_review_type = self.validator.sanitize_filename(review_type) result_file = self.project_root / "logs" / f"review_{safe_review_type}_{timestamp}.md" content = f"""# Code Review 報告 **時間**: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} **類型**: {review_type} **檔案**: {', '.join(files)} **Aider版本**: {self.aider_path} ## Review 結果 ``` {output} ``` ## 執行資訊 - **專案根目錄**: {self.project_root} - **檔案數量**: {len(files)} - **Review類型**: {review_type} - **執行時間**: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")} --- *由 Aider 自動生成 - MOMO 系統 Code Review* """ with open(result_file, "w", encoding="utf-8") as f: f.write(content) self.log_review(f"📄 Review結果已保存: {result_file}") except Exception as e: self.logger.error("保存Review結果失敗", e) def review_specific_files(self, file_paths, review_type="basic"): """Review指定的檔案""" try: # 驗證Review類型 if not self.validator.validate_review_type(review_type): self.logger.error(f"無效的Review類型: {review_type}") return False # 驗證並處理檔案路徑 valid_files = self.validator.validate_files(file_paths, self.project_root) if not valid_files: self.logger.error("沒有有效的檔案可以Review") return False return self.run_aider_review(valid_files, review_type) except Exception as e: self.logger.error("Review指定檔案失敗", e) return False def main(): try: parser = argparse.ArgumentParser( description="MOMO系統 Aider Code Review 自動化工具", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" 使用範例: %(prog)s --auto # 自動Review暫存檔案 %(prog)s --files app.py routes/*.py # Review指定檔案 %(prog)s --type security --auto # 安全檢查暫存檔案 %(prog)s --project-root /path/to/project # 指定專案根目錄 """ ) parser.add_argument("--files", "-f", nargs="+", help="指定要Review的檔案(支援相對路徑和絕對路徑)") parser.add_argument("--type", "-t", choices=["basic", "security", "performance"], default="basic", help="Review類型 (預設: basic)") parser.add_argument("--project-root", "-p", help="專案根目錄路徑(預設: 自動偵測)") parser.add_argument("--auto", "-a", action="store_true", help="自動Review暫存的檔案 (Git staged files)") parser.add_argument("--verbose", "-v", action="store_true", help="顯示詳細輸出") parser.add_argument("--dry-run", action="store_true", help="僅顯示將要Review的檔案,不實際執行") args = parser.parse_args() # 初始化Code Review工具 reviewer = AiderCodeReview(args.project_root) if args.verbose: reviewer.logger.info(f"🔧 使用專案根目錄: {reviewer.project_root}") reviewer.logger.info(f"🔧 Aider 路徑: {reviewer.aider_path}") success = False if args.dry_run: # 乾運行模式 if args.auto: files = reviewer.get_changed_files() reviewer.logger.info(f"🔍 將Review的暫存檔案: {', '.join(files)}") elif args.files: valid_files = reviewer.validator.validate_files(args.files, reviewer.project_root) reviewer.logger.info(f"🔍 將Review的指定檔案: {', '.join(valid_files)}") else: files = reviewer.get_changed_files() reviewer.logger.info(f"🔍 將Review的變更檔案: {', '.join(files)}") success = True elif args.auto: # 自動Review暫存的檔案 reviewer.logger.info("🚀 開始自動Review暫存檔案...") success = reviewer.run_aider_review(review_type=args.type) elif args.files: # Review指定檔案 reviewer.logger.info(f"🚀 開始Review指定檔案: {', '.join(args.files)}") success = reviewer.review_specific_files(args.files, args.type) else: # Review所有變更檔案 reviewer.logger.info("🚀 開始Review所有變更檔案...") success = reviewer.run_aider_review(review_type=args.type) if success: reviewer.logger.info("✅ Code Review 流程完成") sys.exit(0) else: reviewer.logger.error("❌ Code Review 流程失敗") sys.exit(1) except KeyboardInterrupt: print("\n⚠️ 用戶中斷操作") sys.exit(130) except Exception as e: print(f"❌ 程式執行錯誤: {e}") if "--verbose" in sys.argv or "-v" in sys.argv: import traceback traceback.print_exc() sys.exit(1) if __name__ == "__main__": main()