From 7a277df9b09ce26d042e965e21f41a1037b68e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80?= <78488229+viktor138irk@users.noreply.github.com> Date: Mon, 11 May 2026 22:24:22 +0900 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=B0=D0=BD=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=82=D0=BE?= =?UTF-8?q?=D1=80=20GitHub=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=B8=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/project_analyzer.py | 171 ++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 backend/project_analyzer.py diff --git a/backend/project_analyzer.py b/backend/project_analyzer.py new file mode 100644 index 0000000..dde4b7d --- /dev/null +++ b/backend/project_analyzer.py @@ -0,0 +1,171 @@ +import json +import os +import re +import shutil +import subprocess +from dataclasses import dataclass, asdict +from pathlib import Path +from urllib.parse import urlparse + +DATA_DIR = Path(os.getenv('DEVCONSOLE_DATA_DIR', '/var/lib/devconsole')) +PROJECTS_DIR = DATA_DIR / 'projects' + + +@dataclass +class DependencyStep: + title: str + command: str + cwd: str + safe: bool = True + + +@dataclass +class ProjectAnalysis: + repo_url: str + workspace: str + detected_stack: list[str] + files_found: list[str] + dependency_steps: list[DependencyStep] + notes: list[str] + + def to_dict(self) -> dict: + data = asdict(self) + data['dependency_steps'] = [asdict(step) for step in self.dependency_steps] + return data + + +def _slug_from_repo_url(repo_url: str) -> str: + parsed = urlparse(repo_url) + path = parsed.path.strip('/').replace('.git', '') + slug = re.sub(r'[^a-zA-Z0-9_.-]+', '-', path) + return slug or 'project' + + +def clone_or_update_project(repo_url: str) -> Path: + PROJECTS_DIR.mkdir(parents=True, exist_ok=True) + slug = _slug_from_repo_url(repo_url) + workspace = PROJECTS_DIR / slug + + if (workspace / '.git').exists(): + subprocess.run(['git', '-C', str(workspace), 'fetch', '--all'], check=True) + subprocess.run(['git', '-C', str(workspace), 'pull', '--ff-only'], check=False) + else: + if workspace.exists(): + shutil.rmtree(workspace) + subprocess.run(['git', 'clone', repo_url, str(workspace)], check=True) + + return workspace + + +def _exists(root: Path, relative: str) -> bool: + return (root / relative).exists() + + +def _find_files(root: Path, names: set[str], limit: int = 80) -> list[Path]: + result: list[Path] = [] + ignored_dirs = {'.git', 'node_modules', 'vendor', '.venv', 'venv', 'build', 'dist', '.dart_tool', '.gradle'} + + for current, dirs, files in os.walk(root): + dirs[:] = [d for d in dirs if d not in ignored_dirs] + for file in files: + if file in names: + result.append(Path(current) / file) + if len(result) >= limit: + return result + return result + + +def analyze_workspace(repo_url: str, workspace: Path) -> ProjectAnalysis: + detected_stack: list[str] = [] + files_found: list[str] = [] + steps: list[DependencyStep] = [] + notes: list[str] = [] + + marker_files = _find_files( + workspace, + { + 'package.json', 'requirements.txt', 'pyproject.toml', 'Pipfile', + 'composer.json', 'pubspec.yaml', 'build.gradle', 'build.gradle.kts', + 'settings.gradle', 'settings.gradle.kts', 'Dockerfile', 'docker-compose.yml', + 'docker-compose.yaml', 'go.mod', 'Cargo.toml' + } + ) + + for marker in marker_files: + relative = marker.relative_to(workspace).as_posix() + files_found.append(relative) + cwd = marker.parent.as_posix() + + if marker.name == 'package.json': + detected_stack.append('Node.js') + package_lock = marker.parent / 'package-lock.json' + yarn_lock = marker.parent / 'yarn.lock' + pnpm_lock = marker.parent / 'pnpm-lock.yaml' + if pnpm_lock.exists(): + steps.append(DependencyStep('Install Node dependencies with pnpm', 'corepack enable && pnpm install', cwd)) + elif yarn_lock.exists(): + steps.append(DependencyStep('Install Node dependencies with yarn', 'corepack enable && yarn install', cwd)) + elif package_lock.exists(): + steps.append(DependencyStep('Install Node dependencies with npm ci', 'npm ci', cwd)) + else: + steps.append(DependencyStep('Install Node dependencies with npm', 'npm install', cwd)) + + elif marker.name == 'requirements.txt': + detected_stack.append('Python') + steps.append(DependencyStep('Create Python virtualenv', 'python3 -m venv .venv', cwd)) + steps.append(DependencyStep('Install Python requirements', '.venv/bin/pip install --upgrade pip && .venv/bin/pip install -r requirements.txt', cwd)) + + elif marker.name == 'pyproject.toml': + detected_stack.append('Python') + steps.append(DependencyStep('Install Python project', 'python3 -m venv .venv && .venv/bin/pip install --upgrade pip && .venv/bin/pip install -e .', cwd)) + + elif marker.name == 'composer.json': + detected_stack.append('PHP/Composer') + steps.append(DependencyStep('Install PHP Composer dependencies', 'composer install', cwd)) + + elif marker.name == 'pubspec.yaml': + detected_stack.append('Flutter/Dart') + steps.append(DependencyStep('Install Flutter/Dart dependencies', 'flutter pub get', cwd)) + + elif marker.name in {'build.gradle', 'build.gradle.kts'}: + detected_stack.append('Gradle/Android') + gradlew = marker.parent / 'gradlew' + command = './gradlew tasks' if gradlew.exists() else 'gradle tasks' + steps.append(DependencyStep('Prepare Gradle project', command, cwd)) + + elif marker.name in {'docker-compose.yml', 'docker-compose.yaml'}: + detected_stack.append('Docker Compose') + steps.append(DependencyStep('Pull Docker Compose images', 'docker compose pull', cwd)) + + elif marker.name == 'Dockerfile': + detected_stack.append('Docker') + image_name = _slug_from_repo_url(repo_url).replace('/', '-').lower() + steps.append(DependencyStep('Build Docker image', f'docker build -t {image_name}:dev .', cwd)) + + elif marker.name == 'go.mod': + detected_stack.append('Go') + steps.append(DependencyStep('Download Go modules', 'go mod download', cwd)) + + elif marker.name == 'Cargo.toml': + detected_stack.append('Rust') + steps.append(DependencyStep('Fetch Cargo dependencies', 'cargo fetch', cwd)) + + if not files_found: + notes.append('No known dependency manifest found. AI/manual analysis required.') + + unique_stack = list(dict.fromkeys(detected_stack)) + unique_files = list(dict.fromkeys(files_found)) + + return ProjectAnalysis( + repo_url=repo_url, + workspace=workspace.as_posix(), + detected_stack=unique_stack, + files_found=unique_files, + dependency_steps=steps, + notes=notes, + ) + + +def analyze_github_project(repo_url: str) -> ProjectAnalysis: + workspace = clone_or_update_project(repo_url) + return analyze_workspace(repo_url, workspace)