From 0ed3050bdd749838555c70dac24d04d0e3150555 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Thu, 12 Mar 2026 18:23:59 +0200 Subject: [PATCH 01/43] update: publish.yml --- .github/workflows/publish.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 402e848..9047a78 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,8 +23,5 @@ jobs: run: python -m build - name: Publish package - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: | - twine upload dist/* \ No newline at end of file + twine upload dist/* -u __token__ -p ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file From fb19f03ee07cc3a5218367950d14d5398ec51372 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Thu, 12 Mar 2026 18:25:45 +0200 Subject: [PATCH 02/43] update version --- pyproject.toml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index af2c7da..a930b48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "codefox" -version = "0.4.0" +version = "0.4.1" description = "CodeFox CLI - code auditing and code review tool" readme = "README.md" requires-python = ">=3.11" diff --git a/setup.py b/setup.py index 3186c9a..667a543 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="codefox", - version="0.4.0", + version="0.4.1", description="CodeFox CLI - code auditing and code review tool", long_description=README, long_description_content_type="text/plain", From 6f9f2412a90cd394f5ad0b92a39c8b6a0b90dc8c Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Thu, 12 Mar 2026 18:27:18 +0200 Subject: [PATCH 03/43] update: publish.yml --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9047a78..c974ea5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -24,4 +24,4 @@ jobs: - name: Publish package run: | - twine upload dist/* -u __token__ -p ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file + twine upload dist/* -u __token__ -p ${{ secrets.PYPI_API_TOKEN }} --verbose \ No newline at end of file From c9d38853098fe2035e1475fbaf46964b22781a4f Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Thu, 12 Mar 2026 18:28:21 +0200 Subject: [PATCH 04/43] update: publish.yml --- .github/workflows/publish.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c974ea5..402e848 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,5 +23,8 @@ jobs: run: python -m build - name: Publish package + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: | - twine upload dist/* -u __token__ -p ${{ secrets.PYPI_API_TOKEN }} --verbose \ No newline at end of file + twine upload dist/* \ No newline at end of file From fb4eb51d5a5148d74003a1cf8ea0685dc43ae5c6 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Thu, 12 Mar 2026 18:33:03 +0200 Subject: [PATCH 05/43] update version to 0.4.0 in pyproject.toml and setup.py --- pyproject.toml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a930b48..af2c7da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "codefox" -version = "0.4.1" +version = "0.4.0" description = "CodeFox CLI - code auditing and code review tool" readme = "README.md" requires-python = ">=3.11" diff --git a/setup.py b/setup.py index 667a543..3186c9a 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="codefox", - version="0.4.1", + version="0.4.0", description="CodeFox CLI - code auditing and code review tool", long_description=README, long_description_content_type="text/plain", From 5e416e9b1b7ef61a147b880bd7b1c421bafefb33 Mon Sep 17 00:00:00 2001 From: URLBug <79089275+URLbug@users.noreply.github.com> Date: Sat, 14 Mar 2026 17:20:37 +0200 Subject: [PATCH 06/43] Update README.md --- README.md | 107 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 71 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 1b0d929..3df93d5 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,14 @@

CodeFox-CLI

- Intelligent automated code review system + Diff-aware AI code review for terminal and CI workflows

CI License Python 3.11+ - Wiki + Wiki PyPI Downloads

@@ -21,18 +21,35 @@ 🐛 Report Issue

+--- ## 🦊 Overview -**CodeFox-CLI** is an intelligent automated code review system that takes over routine security and code quality checks, allowing senior developers to focus on architecture and complex tasks. +**CodeFox-CLI** is a CLI-first AI code review tool for **git diffs, pull requests, and CI workflows**. + +It analyzes code changes, retrieves relevant project context, and produces review feedback directly in the terminal or inside automated review pipelines. + +CodeFox supports both: +- **local reviews with Ollama** for self-hosted workflows +- **cloud LLM providers** such as Gemini and OpenRouter when remote inference is preferred -Unlike traditional linters, CodeFox understands the context of the entire project and its business logic, delivering not just review comments but **ready-to-apply fixes** (Auto-Fix). Works with **Gemini**, **Ollama**, and **OpenRouter** - use your preferred AI backend. +It is designed for developers and teams who want a **CLI-first review workflow** for local checks, pull requests, and CI/CD pipelines. + +--- -| vs Linters | vs AI code review (e.g. CodeRabbit) | -|------------|-------------------------------------| -| Understands full project context & business logic | Self-hosted / local (Ollama), no vendor lock-in | -| Suggests fixes, not only rules | Configurable models, security/performance/style rules | -| RAG over your codebase for relevant hints | CLI-first: `git diff` → review in seconds | +## Why CodeFox? + +- Reviews **git changes**, not just isolated files +- Uses **relevant codebase context** to improve review quality +- Works with **local or cloud models** +- Fits naturally into **terminal-based and CI workflows** +- Supports configurable review focus such as **security**, **performance**, and **style** + +| Compared to linters | Compared to hosted AI reviewers | +|---|---| +| Reviews diffs with codebase context, not only static rules | Can run locally with Ollama | +| Can suggest fixes, not only flag issues | No hard vendor lock-in | +| Flexible review focus: security, performance, style | CLI-first workflow for local and CI usage |

CodeFox scan demo @@ -40,65 +57,83 @@ Unlike traditional linters, CodeFox understands the context of the entire projec --- -## 📥 Installation +## What CodeFox is and is not -Choose the installation method that fits your workflow. +CodeFox is a **CLI for automated AI review of git changes**. -### 🔹 Install dependencies (local setup) +It is **not** an IDE coding assistant like Cursor or Claude Code. +It is built for **diff review workflows**, terminal usage, and CI/CD automation. -```bash -pip install -r requirements.txt -``` -### 🔹 Development mode (editable install) +--- -Provides the local codefox CLI command and enables live code changes. +## Integrations -```bash -python3 -m pip install -e . -``` +Current: +- GitHub Actions -### 🔹 Install from GitHub +Planned: +- GitLab +- Bitbucket -🐍 Using pip +--- + +## Privacy + +- With **Ollama**, reviews can run fully locally on your machine +- With **cloud providers**, code and context may be sent to external APIs depending on your configuration +- Use `.codefoxignore` to exclude files from analysis + +--- +## 📥 Installation + +### For users + +**uv** ```bash -python3 -m pip install codefox -# or python3 -m pip install git+https://github.com/URLbug/CodeFox-CLI.git@main +uv tool install codefox ``` -⚡ Using uv (recommended for CLI usage) +**pip** ```bash -uv tool install codefox -# or uv tool install git+https://github.com/URLbug/CodeFox-CLI.git@main +python3 -m pip install codefox ``` --- -✅ Verify installation +## Verify installation + ```bash codefox version ``` -Or -```bash -python3 -m codefox version -``` -## 🚀 Quick Start +--- -### Initialize (stores your API key) +## 🚀 Quick Start +1. Initialize CodeFox ```bash codefox init ``` -### Run a scan (uses the current git diff) +This stores your provider token locally and creates the initial config files. +2. Review your current git changes ```bash codefox scan ``` -### Show version +What happens during `scan`: + +- collects the current git diff + +- loads relevant project context based on your configuration + +- sends the review request to the configured model + +- returns review comments and optional fix suggestions +3. Show version ```bash codefox version ``` From 56dcc2009d1d9fbbb6fae0b7c24773763eb885ab Mon Sep 17 00:00:00 2001 From: URLBug <79089275+URLbug@users.noreply.github.com> Date: Sat, 14 Mar 2026 18:00:22 +0200 Subject: [PATCH 07/43] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3df93d5..d6a5833 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@

📚 Documentation • 🚀 Quick Start • - 🐛 Report Issue + 🐛 Report Issue • + 📝 Demo Pull Request

--- From 6733aac39f2d3649ad32bfff9ee03b3bccff45d7 Mon Sep 17 00:00:00 2001 From: URLBug <79089275+URLbug@users.noreply.github.com> Date: Sat, 14 Mar 2026 18:01:45 +0200 Subject: [PATCH 08/43] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d6a5833..4de8ff3 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ 📚 Documentation • 🚀 Quick Start • 🐛 Report Issue • - 📝 Demo Pull Request + 📝 Demo PRs

--- From abf412f6165cd464b1a2c26c48eaac11f06c37a5 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Sat, 14 Mar 2026 20:53:42 +0200 Subject: [PATCH 09/43] chore: enhance package metadata --- pyproject.toml | 36 +++++++++++++++++++++++++++++++++++- setup.py | 31 ++++++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index af2c7da..0d8395e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,8 +2,35 @@ name = "codefox" version = "0.4.0" description = "CodeFox CLI - code auditing and code review tool" -readme = "README.md" +readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" +license = { text = "MIT" } +authors = [ + { name = "CodeFox" }, +] +keywords = [ + "ai", + "code review", + "cli", + "static analysis", + "ollama", + "openai", + "gemini", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Topic :: Software Development", + "Topic :: Software Development :: Quality Assurance", + "Topic :: Utilities", +] dependencies = [ "bm25s==0.3.0", "qdrant-client>=1.7.0", @@ -38,6 +65,13 @@ dev = [ [project.scripts] codefox = "codefox.main:cli" +[project.urls] +Documentation = "https://github.com/codefox-lab/CodeFox-CLI/wiki" +Homepage = "https://github.com/URLbug/CodeFox-CLI" +Issues = "https://github.com/URLbug/CodeFox-CLI/issues" +Security = "https://github.com/URLbug/CodeFox-CLI/blob/main/SECURITY.md" +Source = "https://github.com/URLbug/CodeFox-CLI" + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/setup.py b/setup.py index 3186c9a..1fdc825 100644 --- a/setup.py +++ b/setup.py @@ -3,15 +3,32 @@ from setuptools import find_packages, setup HERE = pathlib.Path(__file__).parent -README = (HERE / "README.txt").read_text(encoding="utf-8") if (HERE / "README.txt").exists() else "" +README = (HERE / "README.md").read_text(encoding="utf-8") if (HERE / "README.md").exists() else "" setup( name="codefox", version="0.4.0", description="CodeFox CLI - code auditing and code review tool", long_description=README, - long_description_content_type="text/plain", + long_description_content_type="text/markdown", author="CodeFox", + license="MIT", + url="https://github.com/URLbug/CodeFox-CLI", + project_urls={ + "Documentation": "https://github.com/codefox-lab/CodeFox-CLI/wiki", + "Source": "https://github.com/URLbug/CodeFox-CLI", + "Issues": "https://github.com/URLbug/CodeFox-CLI/issues", + "Security": "https://github.com/URLbug/CodeFox-CLI/blob/main/SECURITY.md", + }, + keywords=[ + "ai", + "code review", + "cli", + "static analysis", + "ollama", + "openrouter", + "gemini", + ], packages=find_packages(), include_package_data=True, install_requires=[ @@ -40,10 +57,18 @@ ], }, classifiers=[ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", + "Topic :: Software Development", + "Topic :: Software Development :: Quality Assurance", + "Topic :: Utilities", ], python_requires=">=3.11", ) - From 5f0ab9b372ec3b96b0a483c4e1e6fd06cf9b7287 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Sat, 14 Mar 2026 20:58:26 +0200 Subject: [PATCH 10/43] chore: update minor version --- pyproject.toml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0d8395e..63e3802 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "codefox" -version = "0.4.0" +version = "0.4.1" description = "CodeFox CLI - code auditing and code review tool" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/setup.py b/setup.py index 1fdc825..7d099e8 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="codefox", - version="0.4.0", + version="0.4.1", description="CodeFox CLI - code auditing and code review tool", long_description=README, long_description_content_type="text/markdown", From 42f1b13ad19ca2976726d53896efd70cef30c472 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Mon, 16 Mar 2026 11:57:29 +0200 Subject: [PATCH 11/43] chore: updated action.yml --- action.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/action.yml b/action.yml index 84dae80..9c99f42 100644 --- a/action.yml +++ b/action.yml @@ -11,12 +11,12 @@ inputs: required: true codefox-branch: - description: 'Git branch of the CodeFox repository to install and run (e.g., main, dev, or a specific release branch).' + description: 'Git branch of the CodeFox repository to install and run.' default: 'main' required: false github-token: - description: 'GitHub token used to authenticate with the GitHub API for posting review comments and interacting with pull requests.' + description: 'GitHub token used to authenticate with the GitHub API.' default: ${{ github.token }} required: false @@ -28,6 +28,16 @@ runs: with: python-version: '3.12' + - name: Restore CodeFox cache + uses: actions/cache@v4 + with: + path: .codefox + key: ${{ runner.os }}-codefox-${{ github.repository }}-${{ github.base_ref }}-${{ inputs.codefox-branch }} + restore-keys: | + ${{ runner.os }}-codefox-${{ github.repository }}-${{ github.base_ref }}- + ${{ runner.os }}-codefox-${{ github.repository }}- + ${{ runner.os }}-codefox- + - name: Install CodeFox shell: bash run: | From 9939eda3a291c316e903855cf8589de3e66e333b Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Mon, 16 Mar 2026 14:26:13 +0200 Subject: [PATCH 12/43] update --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index 9c99f42..8994282 100644 --- a/action.yml +++ b/action.yml @@ -31,7 +31,7 @@ runs: - name: Restore CodeFox cache uses: actions/cache@v4 with: - path: .codefox + path: ${{ pwd }}/.codefox/ key: ${{ runner.os }}-codefox-${{ github.repository }}-${{ github.base_ref }}-${{ inputs.codefox-branch }} restore-keys: | ${{ runner.os }}-codefox-${{ github.repository }}-${{ github.base_ref }}- From 223ddc3b8d14f63e6bb085d947c6c4b43d8a982a Mon Sep 17 00:00:00 2001 From: Rui Carter Date: Tue, 17 Mar 2026 00:05:07 +0300 Subject: [PATCH 13/43] chore: add new command for index file. Add template for gitlab ci/cd. Add new params for command init and add new execute errors for method Helper.get_diff --- .gitignore | 15 ++++++---- codefox-template.yml | 23 +++++++++++++++ codefox/cli/index.py | 25 ++++++++++++++++ codefox/cli/init.py | 17 +++++++++-- codefox/cli_manager.py | 9 +++++- codefox/main.py | 30 ++++++++++++++----- codefox/utils/helper.py | 8 ++++-- codefox/utils/local_rag.py | 59 +++++++++++++++++++------------------- 8 files changed, 138 insertions(+), 48 deletions(-) create mode 100644 codefox-template.yml create mode 100644 codefox/cli/index.py diff --git a/.gitignore b/.gitignore index dea46fc..0e2db86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,18 @@ .vscode/ .idea/ .venv/ +.codefox/ .*_cache/ -.env + +__pycache__/ +build/ +codefox.egg-info/ + .codefoxenv .codefoxignore .codefox.yml +.env venv*/ -build/ -__pycache__/ -*.pyc -.codefox/ -codefox.egg-info/ \ No newline at end of file + +*.pyc \ No newline at end of file diff --git a/codefox-template.yml b/codefox-template.yml new file mode 100644 index 0000000..6b993c6 --- /dev/null +++ b/codefox-template.yml @@ -0,0 +1,23 @@ +.codefox_scan: + image: python:3.12-slim + variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + cache: + key: "codefox-$CI_PROJECT_ID" + paths: + - .codefox/ + - .cache/pip/ + before_script: + - apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates && rm -rf /var/lib/apt/lists/* + - python -m pip install --upgrade pip + - pip install "git+https://github.com/URLbug/CodeFox-CLI.git@${CODEFOX_BRANCH}" + - | + umask 077 + { + echo "CODEFOX_API_KEY=$CODEFOX_API_KEY" + echo "GITLAB_TOKEN=$GITLAB_TOKEN" + } > .codefoxenv + script: + - git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME":"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" || true + - git fetch origin "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME":"$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" || true + - codefox scan --target-branch "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" --source-branch "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" --ci diff --git a/codefox/cli/index.py b/codefox/cli/index.py new file mode 100644 index 0000000..8e35fc6 --- /dev/null +++ b/codefox/cli/index.py @@ -0,0 +1,25 @@ +import os +from rich import print +from rich.markup import escape + +from codefox.api.base_api import BaseAPI +from codefox.cli.base_cli import BaseCLI + + +class Index(BaseCLI): + def __init__(self, model: type[BaseAPI]): + self.model = model() + + def execute(self): + is_upload, error = self.model.upload_files(os.getcwd()) + if not is_upload: + print( + "[red]Failed to index files: " + + escape(str(error)) + + "[/red]" + ) + return + + print( + "[green]Successful to index files[/green]" + ) diff --git a/codefox/cli/init.py b/codefox/cli/init.py index 0d9b6b5..ac8971a 100644 --- a/codefox/cli/init.py +++ b/codefox/cli/init.py @@ -1,3 +1,4 @@ +from typing import Any from pathlib import Path import yaml @@ -11,9 +12,16 @@ class Init(BaseCLI): - def __init__(self, model_enum: ModelEnum | None = None): - self.model_enum = model_enum or self._ask_model() + def __init__(self, args: dict[str, Any] | None = None): + if not args.get("provider"): + self.model_enum = self._ask_model() + else: + self.model_enum = ModelEnum.by_name(args["provider"]) + self.api_class: type[BaseAPI] = self.model_enum.api_class + + self.args = args + self.config_path = Path(".codefoxenv") self.ignore_path = Path(".codefoxignore") self.yaml_config_path = Path(".codefox.yml") @@ -28,7 +36,10 @@ def _ask_model(self) -> ModelEnum: return ModelEnum.by_name(choice) def execute(self) -> None: - api_key = self._ask_api_key() or "" + if not self.args.get("token"): + api_key = self._ask_api_key() or "" + else: + api_key = self.args["token"] if not self._write_config(api_key): return diff --git a/codefox/cli_manager.py b/codefox/cli_manager.py index 935cebb..b53fa91 100644 --- a/codefox/cli_manager.py +++ b/codefox/cli_manager.py @@ -11,6 +11,7 @@ from codefox.cli.init import Init from codefox.cli.list import List from codefox.cli.scan import Scan +from codefox.cli.index import Index from codefox.utils.helper import Helper @@ -34,6 +35,12 @@ def run(self) -> None: version = importlib.metadata.version("codefox") print(f"[green]CodeFox CLI version {version}[/green]") return + + if self.command == "index": + api_class = self._get_api_class() + index = Index(api_class) + index.execute() + return if self.command == "list": api_class = self._get_api_class() @@ -54,7 +61,7 @@ def run(self) -> None: return if self.command == "init": - init = Init() + init = Init(self.args or {}) init.execute() return diff --git a/codefox/main.py b/codefox/main.py index 2344945..e1f0db6 100755 --- a/codefox/main.py +++ b/codefox/main.py @@ -8,9 +8,9 @@ @app.command("scan") def scan( - ci: bool = typer.Option(False, "--ci", help="CI mode"), - source_branch: str = typer.Option(None, help="Source branch"), - target_branch: str = typer.Option(None, help="Target branch"), + ci: bool = typer.Option(False, "--ci", help="CI mode."), + source_branch: str | None = typer.Option(None, help="Source branch."), + target_branch: str | None = typer.Option(None, help="Target branch."), ): """Run AI code review.""" manager = CLIManager( @@ -23,15 +23,31 @@ def scan( ) manager.run() +@app.command("index") +def index(): + """Index files""" + CLIManager( + command="index", args={} + ).run() @app.command("init") -def init(): +def init( + provider: str | None = typer.Option(None, help="Provider."), + token: str | None = typer.Option(None, help="Token provider.") +): """Initialize CodeFox.""" - CLIManager(command="init", args={}).run() + manager = CLIManager( + command="init", + args={ + "provider": provider, + "token": token, + } + ) + manager.run() @app.command("list") -def list_models(type_model: str = typer.Argument("models", help="Model type")): +def list_models(type_model: str = typer.Argument("models", help="Model type.")): """List available models.""" manager = CLIManager( command="list", @@ -43,7 +59,7 @@ def list_models(type_model: str = typer.Argument("models", help="Model type")): @app.command("clean") -def clean(type_cache: str = typer.Argument("all", help="Cache type")): +def clean(type_cache: str = typer.Argument("all", help="Cache type.")): """Clean cache""" manager = CLIManager( command="clean", diff --git a/codefox/utils/helper.py b/codefox/utils/helper.py index 6e4422c..2f4e96b 100644 --- a/codefox/utils/helper.py +++ b/codefox/utils/helper.py @@ -65,7 +65,7 @@ def get_diff( source_branch: str | None = None, target_branch: str | None = None ) -> str | None: try: - repo = git.Repo(".") + repo = git.Repo(".", search_parent_directories=True) if source_branch and target_branch: diff_text = repo.git.diff( @@ -75,7 +75,11 @@ def get_diff( diff_text = repo.git.diff() return cast(str | None, diff_text) - except git.exc.InvalidGitRepositoryError: + except ( + git.exc.InvalidGitRepositoryError, + git.exc.NoSuchPathError, + git.exc.GitCommandError, + ): return None # ------------------------------------------------------------------ diff --git a/codefox/utils/local_rag.py b/codefox/utils/local_rag.py index 27942ce..dcdde31 100644 --- a/codefox/utils/local_rag.py +++ b/codefox/utils/local_rag.py @@ -191,40 +191,41 @@ def build(self) -> None: if self.client.collection_exists(self.collection_name): self.client.delete_collection(self.collection_name) - with self.console.status( + self.console.print( "[magenta]Building Qdrant vector index...[/magenta]" + ) + + dim: int | None = None + for i in track( + range(0, len(texts), batch_size), + total=(len(texts) + batch_size - 1) // batch_size, + description="[blue]Generating embeddings...[/blue]", ): - dim: int | None = None - for i in track( - range(0, len(texts), batch_size), - total=(len(texts) + batch_size - 1) // batch_size, - description="[blue]Generating embeddings...[/blue]", - ): - batch = texts[i : i + batch_size] - emb = np.array(list(self.model.embed(batch)), dtype="float32") - - if dim is None: - dim = emb.shape[1] - self.client.create_collection( - collection_name=self.collection_name, - vectors_config=VectorParams( - size=dim, distance=Distance.COSINE - ), - ) - - points = [ - PointStruct( - id=j, - vector=vec.tolist(), - payload={"path": self.files[j]["path"]}, - ) - for j, vec in enumerate(emb, start=i) - ] - self.client.upsert( + batch = texts[i : i + batch_size] + emb = np.array(list(self.model.embed(batch)), dtype="float32") + + if dim is None: + dim = emb.shape[1] + self.client.create_collection( collection_name=self.collection_name, - points=points, + vectors_config=VectorParams( + size=dim, distance=Distance.COSINE + ), ) + points = [ + PointStruct( + id=j, + vector=vec.tolist(), + payload={"path": self.files[j]["path"]}, + ) + for j, vec in enumerate(emb, start=i) + ] + self.client.upsert( + collection_name=self.collection_name, + points=points, + ) + self.console.print("[green]✓[/green] Qdrant semantic index built.") self.console.print( "[bold green]RAG build complete and ready for " From 1ab6ae20686f5ac21355fd288856eb9b616ed3ff Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 12:31:31 +0200 Subject: [PATCH 14/43] feat: add stage to codefox-template.yml --- codefox-template.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/codefox-template.yml b/codefox-template.yml index 6b993c6..25c2800 100644 --- a/codefox-template.yml +++ b/codefox-template.yml @@ -1,4 +1,8 @@ -.codefox_scan: +stage: + - review + +codefox_review: + stage: review image: python:3.12-slim variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" From f772ab4b265be6586c7e188ea813406f27fdf752 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 12:35:26 +0200 Subject: [PATCH 15/43] chore: add rulers and fix stages --- codefox-template.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/codefox-template.yml b/codefox-template.yml index 25c2800..6c7be4e 100644 --- a/codefox-template.yml +++ b/codefox-template.yml @@ -1,16 +1,19 @@ -stage: +stages: - review codefox_review: stage: review image: python:3.12-slim + variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + cache: key: "codefox-$CI_PROJECT_ID" paths: - .codefox/ - .cache/pip/ + before_script: - apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates && rm -rf /var/lib/apt/lists/* - python -m pip install --upgrade pip @@ -21,7 +24,11 @@ codefox_review: echo "CODEFOX_API_KEY=$CODEFOX_API_KEY" echo "GITLAB_TOKEN=$GITLAB_TOKEN" } > .codefoxenv + script: - git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME":"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" || true - git fetch origin "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME":"$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" || true - codefox scan --target-branch "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" --source-branch "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" --ci + + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' \ No newline at end of file From 5df777cfd66bfd2b33551b62c94fa4c89f4d5b6f Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 12:50:25 +0200 Subject: [PATCH 16/43] chore: replace gitlab_token to ci_job_token --- codefox-template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codefox-template.yml b/codefox-template.yml index 6c7be4e..6147ff9 100644 --- a/codefox-template.yml +++ b/codefox-template.yml @@ -22,7 +22,7 @@ codefox_review: umask 077 { echo "CODEFOX_API_KEY=$CODEFOX_API_KEY" - echo "GITLAB_TOKEN=$GITLAB_TOKEN" + echo "GITLAB_TOKEN=$CI_JOB_TOKEN" } > .codefoxenv script: From ad3838489229fce49ab604d5ed9c5adc0366c90d Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 12:53:13 +0200 Subject: [PATCH 17/43] fix: CODEFOX_BRANCH --- codefox-template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codefox-template.yml b/codefox-template.yml index 6147ff9..3e14bc0 100644 --- a/codefox-template.yml +++ b/codefox-template.yml @@ -17,7 +17,7 @@ codefox_review: before_script: - apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates && rm -rf /var/lib/apt/lists/* - python -m pip install --upgrade pip - - pip install "git+https://github.com/URLbug/CodeFox-CLI.git@${CODEFOX_BRANCH}" + - pip install "git+https://github.com/URLbug/CodeFox-CLI.git@${$CODEFOX_BRANCH}" - | umask 077 { From 4c0b6526459f78b667e5d3182d36e54ae5ecb30f Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 12:56:10 +0200 Subject: [PATCH 18/43] feat: add default variable --- codefox-template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codefox-template.yml b/codefox-template.yml index 3e14bc0..7c279e8 100644 --- a/codefox-template.yml +++ b/codefox-template.yml @@ -17,7 +17,7 @@ codefox_review: before_script: - apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates && rm -rf /var/lib/apt/lists/* - python -m pip install --upgrade pip - - pip install "git+https://github.com/URLbug/CodeFox-CLI.git@${$CODEFOX_BRANCH}" + - pip install "git+https://github.com/URLbug/CodeFox-CLI.git@${CODEFOX_BRANCH:-main}" - | umask 077 { From 4b8d5481a56aaf4ee20fe5d83275b7909506942a Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 13:16:23 +0200 Subject: [PATCH 19/43] feat: make gitlab ci/cd integration --- codefox-template.yml | 6 +++++- codefox/bots/gitlab_bot.py | 39 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 codefox/bots/gitlab_bot.py diff --git a/codefox-template.yml b/codefox-template.yml index 7c279e8..c9b9b64 100644 --- a/codefox-template.yml +++ b/codefox-template.yml @@ -22,7 +22,11 @@ codefox_review: umask 077 { echo "CODEFOX_API_KEY=$CODEFOX_API_KEY" - echo "GITLAB_TOKEN=$CI_JOB_TOKEN" + echo "GITLAB_TOKEN=${GITLAB_TOKEN:-$CI_JOB_TOKEN}" + echo "GITLAB_BOT=${GITLAB_TOKEN:-$CI_JOB_TOKEN}" + echo "GITLAB_URL=${CI_SERVER_URL}" + echo "GITLAB_REPOSITORY=${CI_PROJECT_PATH}" + echo "PR_NUMBER=${CI_MERGE_REQUEST_IID}" } > .codefoxenv script: diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py new file mode 100644 index 0000000..9dfc380 --- /dev/null +++ b/codefox/bots/gitlab_bot.py @@ -0,0 +1,39 @@ +import os + +from gitlab import Gitlab + + +class GitLabBot: + def __init__(self): + self.gitlab_token = os.getenv("GITLAB_BOT") + self.gitlab_url = os.getenv("GITLAB_URL", "https://gitlab.com/") + self.repository = os.getenv("GITLAB_REPOSITORY") + self.pr_number = os.getenv("PR_NUMBER") + + if not self.gitlab_token: + raise ValueError( + "GITLAB_BOT environment variable is not set. " + "This token is required to authenticate with the Gitlab API." + ) + + if not self.repository: + raise ValueError( + "GITLAB_REPOSITORY environment variable is not set. " + "Expected format: 'owner/repository'." + ) + + self.gitlab = Gitlab( + url=self.gitlab_url, + private_token=self.gitlab_token + ) + + def send(self, message: str) -> None: + if not self.pr_number or not self.pr_number.isdigit(): + raise RuntimeError(f"Invalid PR_NUMBER: {self.pr_number}") + + self.gitlab.auth() + + repo = self.gitlab.projects.get(self.repository) + mr = repo.mergerequests.get(int(self.pr_number)) + + mr.notes.create({"body": message}) From 8b363cf6a3277bd7a6bb0e2c80825b9735c92d6c Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 13:19:47 +0200 Subject: [PATCH 20/43] feat: add gitlab bot to command scan --- codefox/cli/scan.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/codefox/cli/scan.py b/codefox/cli/scan.py index a34d791..db51112 100644 --- a/codefox/cli/scan.py +++ b/codefox/cli/scan.py @@ -9,6 +9,7 @@ from codefox.api.base_api import BaseAPI from codefox.bots.github_bot import GitHubBot +from codefox.bots.gitlab_bot import GitLabBot from codefox.cli.base_cli import BaseCLI from codefox.cli.list import List from codefox.utils.helper import Helper @@ -20,8 +21,11 @@ def __init__(self, model: type[BaseAPI], args: dict[str, Any]): self.args = args self.github_bot = None - if self.args.get("ci", False): + self.gitlab_bot = None + if self.args.get("ci", False) and os.getenv("GITHUB_BOT"): self.github_bot = GitHubBot() + elif self.args("ci", False) and os.getenv("GITLAB_BOT"): + self.gitlab_bot = GitLabBot() def execute(self) -> None: source_branch, target_branch = self._get_branchs() @@ -79,6 +83,10 @@ def _ci_response_answer(self, diff_text: str) -> None: response = self.model.execute(diff_text) if self.github_bot is not None: self.github_bot.send(response.text) + + if self.gitlab_bot is not None: + self.gitlab_bot.send(response.text) + self.model.remove_files() def _classic_response_answer(self, diff_text: str) -> None: From e74b0e154191c97303045de929358c3c330905f6 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 13:22:31 +0200 Subject: [PATCH 21/43] fix: add python-gitlab sdk to project --- pyproject.toml | 1 + requirements.in | 3 ++- requirements.txt | 3 ++- setup.py | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 63e3802..ab9b12f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ dependencies = [ "psutil==7.2.2", "PyGithub==2.8.1", "pygments==2.19.2", + "python-gitlab==8.1.0", ] [project.optional-dependencies] diff --git a/requirements.in b/requirements.in index df054fd..00355ca 100644 --- a/requirements.in +++ b/requirements.in @@ -15,4 +15,5 @@ typer==0.23.1 tree-sitter-language-pack==0.13.0 psutil==7.2.2 PyGithub==2.8.1 -pygments==2.19.2 \ No newline at end of file +pygments==2.19.2 +python-gitlab==8.1.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index df054fd..00355ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,4 +15,5 @@ typer==0.23.1 tree-sitter-language-pack==0.13.0 psutil==7.2.2 PyGithub==2.8.1 -pygments==2.19.2 \ No newline at end of file +pygments==2.19.2 +python-gitlab==8.1.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 7d099e8..3e18fcb 100644 --- a/setup.py +++ b/setup.py @@ -50,6 +50,7 @@ "psutil==7.2.2", "PyGithub==2.8.1", "pygments==2.19.2", + "python-gitlab==8.1.0", ], entry_points={ "console_scripts": [ From 62418ce8570fef897c94211491d436ecb7631ec7 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 13:39:53 +0200 Subject: [PATCH 22/43] debug --- codefox-template.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/codefox-template.yml b/codefox-template.yml index c9b9b64..44c18d7 100644 --- a/codefox-template.yml +++ b/codefox-template.yml @@ -15,6 +15,8 @@ codefox_review: - .cache/pip/ before_script: + - echo "CODEFOX_BRANCH=[$CODEFOX_BRANCH]" + - env | sort | grep '^CODEFOX' || true - apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates && rm -rf /var/lib/apt/lists/* - python -m pip install --upgrade pip - pip install "git+https://github.com/URLbug/CodeFox-CLI.git@${CODEFOX_BRANCH:-main}" From 18cc9de9b2ce4340742be0b0c3b7abe4aa0a3dcc Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 13:46:09 +0200 Subject: [PATCH 23/43] feat --- codefox-template.yml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/codefox-template.yml b/codefox-template.yml index 44c18d7..42b5839 100644 --- a/codefox-template.yml +++ b/codefox-template.yml @@ -1,25 +1,22 @@ -stages: - - review - codefox_review: stage: review image: python:3.12-slim variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + CODEFOX_BRANCH: "dev-docs" cache: - key: "codefox-$CI_PROJECT_ID" + key: codefox-$CI_PROJECT_ID paths: - - .codefox/ - - .cache/pip/ + - ".codefox/" + - ".cache/pip/" before_script: - echo "CODEFOX_BRANCH=[$CODEFOX_BRANCH]" - - env | sort | grep '^CODEFOX' || true - apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates && rm -rf /var/lib/apt/lists/* - python -m pip install --upgrade pip - - pip install "git+https://github.com/URLbug/CodeFox-CLI.git@${CODEFOX_BRANCH:-main}" + - pip install "git+https://github.com/URLbug/CodeFox-CLI.git@${CODEFOX_BRANCH}" - | umask 077 { From 7f149a87c674302243e446781416f2f795c81c34 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 14:03:18 +0200 Subject: [PATCH 24/43] fix: add method get --- codefox/cli/scan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codefox/cli/scan.py b/codefox/cli/scan.py index db51112..5c78d84 100644 --- a/codefox/cli/scan.py +++ b/codefox/cli/scan.py @@ -24,7 +24,7 @@ def __init__(self, model: type[BaseAPI], args: dict[str, Any]): self.gitlab_bot = None if self.args.get("ci", False) and os.getenv("GITHUB_BOT"): self.github_bot = GitHubBot() - elif self.args("ci", False) and os.getenv("GITLAB_BOT"): + elif self.args.get("ci", False) and os.getenv("GITLAB_BOT"): self.gitlab_bot = GitLabBot() def execute(self) -> None: From 10b33c327fd0d0d0748b227ab10da4778f73d3ee Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 14:19:57 +0200 Subject: [PATCH 25/43] feat --- codefox-template.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/codefox-template.yml b/codefox-template.yml index 42b5839..f5909f1 100644 --- a/codefox-template.yml +++ b/codefox-template.yml @@ -26,7 +26,8 @@ codefox_review: echo "GITLAB_URL=${CI_SERVER_URL}" echo "GITLAB_REPOSITORY=${CI_PROJECT_PATH}" echo "PR_NUMBER=${CI_MERGE_REQUEST_IID}" - } > .codefoxenv + } > "$CI_PROJECT_DIR/.codefoxenv" + - ls -la "$CI_PROJECT_DIR" script: - git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME":"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" || true From b8555c567a059c70ccf6ee9e675b8a9f9c62e88c Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 14:22:37 +0200 Subject: [PATCH 26/43] debug --- codefox-template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codefox-template.yml b/codefox-template.yml index f5909f1..0c8fea2 100644 --- a/codefox-template.yml +++ b/codefox-template.yml @@ -27,7 +27,7 @@ codefox_review: echo "GITLAB_REPOSITORY=${CI_PROJECT_PATH}" echo "PR_NUMBER=${CI_MERGE_REQUEST_IID}" } > "$CI_PROJECT_DIR/.codefoxenv" - - ls -la "$CI_PROJECT_DIR" + - cat "$CODEFOX_API_KEY" script: - git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME":"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" || true From ae32911a356b071745a84725865886f5569bf75c Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 14:27:47 +0200 Subject: [PATCH 27/43] debug --- codefox-template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codefox-template.yml b/codefox-template.yml index 0c8fea2..ea6f67b 100644 --- a/codefox-template.yml +++ b/codefox-template.yml @@ -27,7 +27,7 @@ codefox_review: echo "GITLAB_REPOSITORY=${CI_PROJECT_PATH}" echo "PR_NUMBER=${CI_MERGE_REQUEST_IID}" } > "$CI_PROJECT_DIR/.codefoxenv" - - cat "$CODEFOX_API_KEY" + - echo "$CODEFOX_API_KEY" script: - git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME":"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" || true From b45b897a618df00939853f4034e13037d0a8b3b5 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 14:51:31 +0200 Subject: [PATCH 28/43] debug --- codefox-template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codefox-template.yml b/codefox-template.yml index ea6f67b..4633d8f 100644 --- a/codefox-template.yml +++ b/codefox-template.yml @@ -27,7 +27,7 @@ codefox_review: echo "GITLAB_REPOSITORY=${CI_PROJECT_PATH}" echo "PR_NUMBER=${CI_MERGE_REQUEST_IID}" } > "$CI_PROJECT_DIR/.codefoxenv" - - echo "$CODEFOX_API_KEY" + - echo $CODEFOX_API_KEY script: - git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME":"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" || true From d04dad5bd0610becc474f107d9f558ddd5987ca8 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 14:58:50 +0200 Subject: [PATCH 29/43] debug --- codefox-template.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codefox-template.yml b/codefox-template.yml index 4633d8f..ecae5ff 100644 --- a/codefox-template.yml +++ b/codefox-template.yml @@ -27,7 +27,7 @@ codefox_review: echo "GITLAB_REPOSITORY=${CI_PROJECT_PATH}" echo "PR_NUMBER=${CI_MERGE_REQUEST_IID}" } > "$CI_PROJECT_DIR/.codefoxenv" - - echo $CODEFOX_API_KEY + - test -f .codefoxenv && echo ".codefoxenv exists" script: - git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME":"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" || true From 9c61328c37a0bee000f35e4bcad634e4d54d283b Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 16:46:48 +0200 Subject: [PATCH 30/43] feat: disable debug mod --- codefox-template.yml | 2 -- codefox/bots/gitlab_bot.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/codefox-template.yml b/codefox-template.yml index ecae5ff..1877adf 100644 --- a/codefox-template.yml +++ b/codefox-template.yml @@ -13,7 +13,6 @@ codefox_review: - ".cache/pip/" before_script: - - echo "CODEFOX_BRANCH=[$CODEFOX_BRANCH]" - apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates && rm -rf /var/lib/apt/lists/* - python -m pip install --upgrade pip - pip install "git+https://github.com/URLbug/CodeFox-CLI.git@${CODEFOX_BRANCH}" @@ -27,7 +26,6 @@ codefox_review: echo "GITLAB_REPOSITORY=${CI_PROJECT_PATH}" echo "PR_NUMBER=${CI_MERGE_REQUEST_IID}" } > "$CI_PROJECT_DIR/.codefoxenv" - - test -f .codefoxenv && echo ".codefoxenv exists" script: - git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME":"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" || true diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py index 9dfc380..f05730d 100644 --- a/codefox/bots/gitlab_bot.py +++ b/codefox/bots/gitlab_bot.py @@ -31,8 +31,6 @@ def send(self, message: str) -> None: if not self.pr_number or not self.pr_number.isdigit(): raise RuntimeError(f"Invalid PR_NUMBER: {self.pr_number}") - self.gitlab.auth() - repo = self.gitlab.projects.get(self.repository) mr = repo.mergerequests.get(int(self.pr_number)) From d7f930c0638e42679f8cfe32bee8cb397227cbc3 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 16:50:54 +0200 Subject: [PATCH 31/43] redfactor --- codefox/bots/gitlab_bot.py | 47 +++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py index f05730d..38e772e 100644 --- a/codefox/bots/gitlab_bot.py +++ b/codefox/bots/gitlab_bot.py @@ -1,37 +1,58 @@ import os +from urllib.parse import quote_plus from gitlab import Gitlab +from gitlab.exceptions import GitlabAuthenticationError, GitlabGetError, GitlabCreateError class GitLabBot: - def __init__(self): + def __init__(self) -> None: self.gitlab_token = os.getenv("GITLAB_BOT") - self.gitlab_url = os.getenv("GITLAB_URL", "https://gitlab.com/") + self.gitlab_url = os.getenv("GITLAB_URL", "https://gitlab.com") self.repository = os.getenv("GITLAB_REPOSITORY") - self.pr_number = os.getenv("PR_NUMBER") + self.mr_iid = os.getenv("PR_NUMBER") if not self.gitlab_token: raise ValueError( "GITLAB_BOT environment variable is not set. " - "This token is required to authenticate with the Gitlab API." + "This token is required to authenticate with the GitLab API." ) if not self.repository: raise ValueError( "GITLAB_REPOSITORY environment variable is not set. " - "Expected format: 'owner/repository'." + "Expected format: 'group/project' or 'group/subgroup/project'." + ) + + if not self.mr_iid or not self.mr_iid.isdigit(): + raise ValueError( + f"Invalid PR_NUMBER value: {self.mr_iid!r}. " + "Expected a numeric merge request IID." ) self.gitlab = Gitlab( url=self.gitlab_url, - private_token=self.gitlab_token + private_token=self.gitlab_token, ) - def send(self, message: str) -> None: - if not self.pr_number or not self.pr_number.isdigit(): - raise RuntimeError(f"Invalid PR_NUMBER: {self.pr_number}") - - repo = self.gitlab.projects.get(self.repository) - mr = repo.mergerequests.get(int(self.pr_number)) + try: + self.gitlab.auth() + except GitlabAuthenticationError as exc: + raise RuntimeError("Failed to authenticate to GitLab API.") from exc - mr.notes.create({"body": message}) + def send(self, message: str) -> None: + if not message or not message.strip(): + raise ValueError("Message must not be empty.") + + project_path = quote_plus(self.repository) + + try: + project = self.gitlab.projects.get(project_path) + mr = project.mergerequests.get(int(self.mr_iid)) + mr.notes.create({"body": message}) + except GitlabGetError as exc: + raise RuntimeError( + f"Failed to find project '{self.repository}' or merge request IID {self.mr_iid}." + ) from exc + except GitlabCreateError as exc: + raise RuntimeError("Failed to create merge request note.") from exc From 9200ecec94182794a3dfd2d638d0ee0c64438538 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 16:57:56 +0200 Subject: [PATCH 32/43] refactor: use job token --- codefox/bots/gitlab_bot.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py index 38e772e..aeec274 100644 --- a/codefox/bots/gitlab_bot.py +++ b/codefox/bots/gitlab_bot.py @@ -32,14 +32,9 @@ def __init__(self) -> None: self.gitlab = Gitlab( url=self.gitlab_url, - private_token=self.gitlab_token, + job_token=self.gitlab_token, ) - try: - self.gitlab.auth() - except GitlabAuthenticationError as exc: - raise RuntimeError("Failed to authenticate to GitLab API.") from exc - def send(self, message: str) -> None: if not message or not message.strip(): raise ValueError("Message must not be empty.") From 12fd1018edc6aeda580635988236f745e35d8807 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 17:02:48 +0200 Subject: [PATCH 33/43] feat --- codefox-template.yml | 2 +- codefox/bots/gitlab_bot.py | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/codefox-template.yml b/codefox-template.yml index 1877adf..4645853 100644 --- a/codefox-template.yml +++ b/codefox-template.yml @@ -23,7 +23,7 @@ codefox_review: echo "GITLAB_TOKEN=${GITLAB_TOKEN:-$CI_JOB_TOKEN}" echo "GITLAB_BOT=${GITLAB_TOKEN:-$CI_JOB_TOKEN}" echo "GITLAB_URL=${CI_SERVER_URL}" - echo "GITLAB_REPOSITORY=${CI_PROJECT_PATH}" + echo "GITLAB_REPOSITORY=${CI_PROJECT_ID}" echo "PR_NUMBER=${CI_MERGE_REQUEST_IID}" } > "$CI_PROJECT_DIR/.codefoxenv" diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py index aeec274..91f4958 100644 --- a/codefox/bots/gitlab_bot.py +++ b/codefox/bots/gitlab_bot.py @@ -2,7 +2,7 @@ from urllib.parse import quote_plus from gitlab import Gitlab -from gitlab.exceptions import GitlabAuthenticationError, GitlabGetError, GitlabCreateError +from gitlab.exceptions import GitlabGetError, GitlabCreateError class GitLabBot: @@ -18,11 +18,8 @@ def __init__(self) -> None: "This token is required to authenticate with the GitLab API." ) - if not self.repository: - raise ValueError( - "GITLAB_REPOSITORY environment variable is not set. " - "Expected format: 'group/project' or 'group/subgroup/project'." - ) + if not self.repository or not self.repository.isdigit(): + raise ValueError(f"Invalid GITLAB_REPOSITORY: {self.repository!r}") if not self.mr_iid or not self.mr_iid.isdigit(): raise ValueError( @@ -42,7 +39,7 @@ def send(self, message: str) -> None: project_path = quote_plus(self.repository) try: - project = self.gitlab.projects.get(project_path) + project = self.gitlab.projects.get(int(project_path)) mr = project.mergerequests.get(int(self.mr_iid)) mr.notes.create({"body": message}) except GitlabGetError as exc: From dbe9b28d98d12498eae07f2b2ccad9f8c8cd3138 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 17:11:56 +0200 Subject: [PATCH 34/43] feat --- codefox/bots/gitlab_bot.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py index 91f4958..c34c632 100644 --- a/codefox/bots/gitlab_bot.py +++ b/codefox/bots/gitlab_bot.py @@ -36,10 +36,8 @@ def send(self, message: str) -> None: if not message or not message.strip(): raise ValueError("Message must not be empty.") - project_path = quote_plus(self.repository) - try: - project = self.gitlab.projects.get(int(project_path)) + project = self.gitlab.projects.get(int(self.repository)) mr = project.mergerequests.get(int(self.mr_iid)) mr.notes.create({"body": message}) except GitlabGetError as exc: From ace19ceca52a349468b3eca7b6689862277e733e Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 17:13:54 +0200 Subject: [PATCH 35/43] debug --- codefox/bots/gitlab_bot.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py index c34c632..5a7b614 100644 --- a/codefox/bots/gitlab_bot.py +++ b/codefox/bots/gitlab_bot.py @@ -36,13 +36,14 @@ def send(self, message: str) -> None: if not message or not message.strip(): raise ValueError("Message must not be empty.") - try: - project = self.gitlab.projects.get(int(self.repository)) - mr = project.mergerequests.get(int(self.mr_iid)) - mr.notes.create({"body": message}) - except GitlabGetError as exc: - raise RuntimeError( - f"Failed to find project '{self.repository}' or merge request IID {self.mr_iid}." - ) from exc - except GitlabCreateError as exc: - raise RuntimeError("Failed to create merge request note.") from exc + project = self.gitlab.projects.get(int(self.repository)) + mr = project.mergerequests.get(int(self.mr_iid)) + mr.notes.create({"body": message}) + # try: + + # except GitlabGetError as exc: + # raise RuntimeError( + # f"Failed to find project '{self.repository}' or merge request IID {self.mr_iid}." + # ) from exc + # except GitlabCreateError as exc: + # raise RuntimeError("Failed to create merge request note.") from exc From ba40ab39d1fcc46243a836c38ba8fb14c0ed6520 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 17:18:39 +0200 Subject: [PATCH 36/43] debug --- codefox-template.yml | 3 +-- codefox/bots/gitlab_bot.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/codefox-template.yml b/codefox-template.yml index 4645853..1e13eee 100644 --- a/codefox-template.yml +++ b/codefox-template.yml @@ -20,8 +20,7 @@ codefox_review: umask 077 { echo "CODEFOX_API_KEY=$CODEFOX_API_KEY" - echo "GITLAB_TOKEN=${GITLAB_TOKEN:-$CI_JOB_TOKEN}" - echo "GITLAB_BOT=${GITLAB_TOKEN:-$CI_JOB_TOKEN}" + echo "GITLAB_TOKEN=${GITLAB_TOKEN}" echo "GITLAB_URL=${CI_SERVER_URL}" echo "GITLAB_REPOSITORY=${CI_PROJECT_ID}" echo "PR_NUMBER=${CI_MERGE_REQUEST_IID}" diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py index 5a7b614..cc36f11 100644 --- a/codefox/bots/gitlab_bot.py +++ b/codefox/bots/gitlab_bot.py @@ -7,14 +7,14 @@ class GitLabBot: def __init__(self) -> None: - self.gitlab_token = os.getenv("GITLAB_BOT") + self.gitlab_token = os.getenv("GITLAB_TOKEN") self.gitlab_url = os.getenv("GITLAB_URL", "https://gitlab.com") self.repository = os.getenv("GITLAB_REPOSITORY") self.mr_iid = os.getenv("PR_NUMBER") if not self.gitlab_token: raise ValueError( - "GITLAB_BOT environment variable is not set. " + "GITLAB_TOKEN environment variable is not set. " "This token is required to authenticate with the GitLab API." ) From b852a9367aacf5870edd2c2fc6cd9c66c375f928 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 17:20:19 +0200 Subject: [PATCH 37/43] feat --- codefox/bots/gitlab_bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py index cc36f11..875331f 100644 --- a/codefox/bots/gitlab_bot.py +++ b/codefox/bots/gitlab_bot.py @@ -29,7 +29,7 @@ def __init__(self) -> None: self.gitlab = Gitlab( url=self.gitlab_url, - job_token=self.gitlab_token, + private_token=self.gitlab_token, ) def send(self, message: str) -> None: From 5b8fa251ff518de23cb88eb6273d23dc9e1d88f7 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 17:26:16 +0200 Subject: [PATCH 38/43] fix: improve error handling in GitLabBot for merge request notes --- codefox/bots/gitlab_bot.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py index 875331f..ae1f29b 100644 --- a/codefox/bots/gitlab_bot.py +++ b/codefox/bots/gitlab_bot.py @@ -36,14 +36,14 @@ def send(self, message: str) -> None: if not message or not message.strip(): raise ValueError("Message must not be empty.") - project = self.gitlab.projects.get(int(self.repository)) - mr = project.mergerequests.get(int(self.mr_iid)) - mr.notes.create({"body": message}) - # try: - - # except GitlabGetError as exc: - # raise RuntimeError( - # f"Failed to find project '{self.repository}' or merge request IID {self.mr_iid}." - # ) from exc - # except GitlabCreateError as exc: - # raise RuntimeError("Failed to create merge request note.") from exc + + try: + project = self.gitlab.projects.get(int(self.repository)) + mr = project.mergerequests.get(int(self.mr_iid)) + mr.notes.create({"body": message}) + except GitlabGetError as exc: + raise RuntimeError( + f"Failed to find project '{self.repository}' or merge request IID {self.mr_iid}." + ) from exc + except GitlabCreateError as exc: + raise RuntimeError("Failed to create merge request note.") from exc From 91f7f732fb714b6ac9e7a19ffa73195caf37461d Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 23:03:25 +0200 Subject: [PATCH 39/43] debug --- codefox/bots/gitlab_bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py index ae1f29b..79c9cee 100644 --- a/codefox/bots/gitlab_bot.py +++ b/codefox/bots/gitlab_bot.py @@ -36,10 +36,10 @@ def send(self, message: str) -> None: if not message or not message.strip(): raise ValueError("Message must not be empty.") - try: project = self.gitlab.projects.get(int(self.repository)) mr = project.mergerequests.get(int(self.mr_iid)) + print(message, self.mr_iid, self.repository) mr.notes.create({"body": message}) except GitlabGetError as exc: raise RuntimeError( From b295fc798ac32d2b79f04a53ff4440a7f9a51efd Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 23:06:40 +0200 Subject: [PATCH 40/43] debug --- codefox/cli/scan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/codefox/cli/scan.py b/codefox/cli/scan.py index 5c78d84..744d2d8 100644 --- a/codefox/cli/scan.py +++ b/codefox/cli/scan.py @@ -84,6 +84,7 @@ def _ci_response_answer(self, diff_text: str) -> None: if self.github_bot is not None: self.github_bot.send(response.text) + print(self.gitlab_bot) if self.gitlab_bot is not None: self.gitlab_bot.send(response.text) From d15783d087bbaba4c902889b453f2b739c762190 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 23:09:06 +0200 Subject: [PATCH 41/43] fix: change GITLAB_BOT to GITLAB_TOKEN --- codefox/cli/scan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codefox/cli/scan.py b/codefox/cli/scan.py index 744d2d8..e354512 100644 --- a/codefox/cli/scan.py +++ b/codefox/cli/scan.py @@ -24,7 +24,7 @@ def __init__(self, model: type[BaseAPI], args: dict[str, Any]): self.gitlab_bot = None if self.args.get("ci", False) and os.getenv("GITHUB_BOT"): self.github_bot = GitHubBot() - elif self.args.get("ci", False) and os.getenv("GITLAB_BOT"): + elif self.args.get("ci", False) and os.getenv("GITLAB_TOKEN"): self.gitlab_bot = GitLabBot() def execute(self) -> None: From 1b6ba7664ffa6d3089ff2b82c1e57bb88fa0da92 Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 23:11:19 +0200 Subject: [PATCH 42/43] feat: removee debug --- codefox/bots/gitlab_bot.py | 1 - codefox/cli/scan.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py index 79c9cee..55bf4c4 100644 --- a/codefox/bots/gitlab_bot.py +++ b/codefox/bots/gitlab_bot.py @@ -39,7 +39,6 @@ def send(self, message: str) -> None: try: project = self.gitlab.projects.get(int(self.repository)) mr = project.mergerequests.get(int(self.mr_iid)) - print(message, self.mr_iid, self.repository) mr.notes.create({"body": message}) except GitlabGetError as exc: raise RuntimeError( diff --git a/codefox/cli/scan.py b/codefox/cli/scan.py index e354512..127c26a 100644 --- a/codefox/cli/scan.py +++ b/codefox/cli/scan.py @@ -83,8 +83,7 @@ def _ci_response_answer(self, diff_text: str) -> None: response = self.model.execute(diff_text) if self.github_bot is not None: self.github_bot.send(response.text) - - print(self.gitlab_bot) + if self.gitlab_bot is not None: self.gitlab_bot.send(response.text) From fe434dc30084733f60ed2a87460527f13f90159e Mon Sep 17 00:00:00 2001 From: Timur Davydov Date: Fri, 20 Mar 2026 23:26:13 +0200 Subject: [PATCH 43/43] chore: --- codefox/bots/gitlab_bot.py | 18 ++++++++--- codefox/cli/index.py | 11 +++---- codefox/cli/init.py | 16 ++++++---- codefox/cli_manager.py | 4 +-- codefox/main.py | 18 ++++++----- codefox/utils/local_rag.py | 2 +- tests/test_gitlab_bot.py | 65 ++++++++++++++++++++++++++++++++++++++ tests/test_init.py | 38 ++++++++++++++++++++++ uv.lock | 29 ++++++++++++++++- 9 files changed, 170 insertions(+), 31 deletions(-) create mode 100644 tests/test_gitlab_bot.py create mode 100644 tests/test_init.py diff --git a/codefox/bots/gitlab_bot.py b/codefox/bots/gitlab_bot.py index 55bf4c4..8a3a01d 100644 --- a/codefox/bots/gitlab_bot.py +++ b/codefox/bots/gitlab_bot.py @@ -1,8 +1,7 @@ import os -from urllib.parse import quote_plus from gitlab import Gitlab -from gitlab.exceptions import GitlabGetError, GitlabCreateError +from gitlab.exceptions import GitlabCreateError, GitlabGetError class GitLabBot: @@ -36,13 +35,22 @@ def send(self, message: str) -> None: if not message or not message.strip(): raise ValueError("Message must not be empty.") + repository = self.repository + mr_iid = self.mr_iid + + if repository is None or mr_iid is None: + raise RuntimeError( + "GitLab project or merge request is not configured." + ) + try: - project = self.gitlab.projects.get(int(self.repository)) - mr = project.mergerequests.get(int(self.mr_iid)) + project = self.gitlab.projects.get(int(repository)) + mr = project.mergerequests.get(int(mr_iid)) mr.notes.create({"body": message}) except GitlabGetError as exc: raise RuntimeError( - f"Failed to find project '{self.repository}' or merge request IID {self.mr_iid}." + f"Failed to find project '{repository}' " + f"or merge request IID {mr_iid}." ) from exc except GitlabCreateError as exc: raise RuntimeError("Failed to create merge request note.") from exc diff --git a/codefox/cli/index.py b/codefox/cli/index.py index 8e35fc6..325c7aa 100644 --- a/codefox/cli/index.py +++ b/codefox/cli/index.py @@ -1,4 +1,5 @@ import os + from rich import print from rich.markup import escape @@ -14,12 +15,8 @@ def execute(self): is_upload, error = self.model.upload_files(os.getcwd()) if not is_upload: print( - "[red]Failed to index files: " - + escape(str(error)) - + "[/red]" + "[red]Failed to index files: " + escape(str(error)) + "[/red]" ) return - - print( - "[green]Successful to index files[/green]" - ) + + print("[green]Successful to index files[/green]") diff --git a/codefox/cli/init.py b/codefox/cli/init.py index ac8971a..118ba79 100644 --- a/codefox/cli/init.py +++ b/codefox/cli/init.py @@ -1,5 +1,5 @@ -from typing import Any from pathlib import Path +from typing import Any import yaml from dotenv import load_dotenv, set_key @@ -13,15 +13,17 @@ class Init(BaseCLI): def __init__(self, args: dict[str, Any] | None = None): - if not args.get("provider"): + resolved_args = args if args is not None else {} + + if not resolved_args.get("provider"): self.model_enum = self._ask_model() else: - self.model_enum = ModelEnum.by_name(args["provider"]) - + self.model_enum = ModelEnum.by_name(resolved_args["provider"]) + self.api_class: type[BaseAPI] = self.model_enum.api_class - - self.args = args - + + self.args: dict[str, Any] = resolved_args + self.config_path = Path(".codefoxenv") self.ignore_path = Path(".codefoxignore") self.yaml_config_path = Path(".codefox.yml") diff --git a/codefox/cli_manager.py b/codefox/cli_manager.py index b53fa91..946d28e 100644 --- a/codefox/cli_manager.py +++ b/codefox/cli_manager.py @@ -8,10 +8,10 @@ from codefox.api.base_api import BaseAPI from codefox.api.model_enum import ModelEnum from codefox.cli.clean import Clean +from codefox.cli.index import Index from codefox.cli.init import Init from codefox.cli.list import List from codefox.cli.scan import Scan -from codefox.cli.index import Index from codefox.utils.helper import Helper @@ -35,7 +35,7 @@ def run(self) -> None: version = importlib.metadata.version("codefox") print(f"[green]CodeFox CLI version {version}[/green]") return - + if self.command == "index": api_class = self._get_api_class() index = Index(api_class) diff --git a/codefox/main.py b/codefox/main.py index e1f0db6..e7c1fa5 100755 --- a/codefox/main.py +++ b/codefox/main.py @@ -23,31 +23,33 @@ def scan( ) manager.run() + @app.command("index") def index(): """Index files""" - CLIManager( - command="index", args={} - ).run() + CLIManager(command="index", args={}).run() + @app.command("init") def init( - provider: str | None = typer.Option(None, help="Provider."), - token: str | None = typer.Option(None, help="Token provider.") + provider: str | None = typer.Option(None, help="Provider."), + token: str | None = typer.Option(None, help="Token provider."), ): """Initialize CodeFox.""" manager = CLIManager( - command="init", + command="init", args={ "provider": provider, "token": token, - } + }, ) manager.run() @app.command("list") -def list_models(type_model: str = typer.Argument("models", help="Model type.")): +def list_models( + type_model: str = typer.Argument("models", help="Model type."), +): """List available models.""" manager = CLIManager( command="list", diff --git a/codefox/utils/local_rag.py b/codefox/utils/local_rag.py index dcdde31..d6ca44f 100644 --- a/codefox/utils/local_rag.py +++ b/codefox/utils/local_rag.py @@ -194,7 +194,7 @@ def build(self) -> None: self.console.print( "[magenta]Building Qdrant vector index...[/magenta]" ) - + dim: int | None = None for i in track( range(0, len(texts), batch_size), diff --git a/tests/test_gitlab_bot.py b/tests/test_gitlab_bot.py new file mode 100644 index 0000000..c12de5c --- /dev/null +++ b/tests/test_gitlab_bot.py @@ -0,0 +1,65 @@ +"""Tests for GitLab bot.""" + +from unittest.mock import MagicMock, patch + +import pytest + +from codefox.bots.gitlab_bot import GitLabBot + + +def test_send_creates_merge_request_note( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("GITLAB_TOKEN", "token") + monkeypatch.setenv("GITLAB_REPOSITORY", "123") + monkeypatch.setenv("PR_NUMBER", "456") + + with patch("codefox.bots.gitlab_bot.Gitlab") as mock_gitlab_class: + mock_project = MagicMock() + mock_mr = MagicMock() + mock_gitlab = mock_gitlab_class.return_value + mock_gitlab.projects.get.return_value = mock_project + mock_project.mergerequests.get.return_value = mock_mr + + bot = GitLabBot() + bot.send("hello") + + mock_gitlab_class.assert_called_once_with( + url="https://gitlab.com", + private_token="token", + ) + mock_gitlab.projects.get.assert_called_once_with(123) + mock_project.mergerequests.get.assert_called_once_with(456) + mock_mr.notes.create.assert_called_once_with({"body": "hello"}) + + +def test_send_rejects_empty_message(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("GITLAB_TOKEN", "token") + monkeypatch.setenv("GITLAB_REPOSITORY", "123") + monkeypatch.setenv("PR_NUMBER", "456") + + with patch("codefox.bots.gitlab_bot.Gitlab"): + bot = GitLabBot() + + with pytest.raises(ValueError, match="Message must not be empty"): + bot.send(" ") + + +def test_send_raises_when_identifiers_are_not_configured( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("GITLAB_TOKEN", "token") + monkeypatch.setenv("GITLAB_REPOSITORY", "123") + monkeypatch.setenv("PR_NUMBER", "456") + + with patch("codefox.bots.gitlab_bot.Gitlab") as mock_gitlab_class: + bot = GitLabBot() + bot.repository = None + + with pytest.raises( + RuntimeError, + match="GitLab project or merge request is not configured", + ): + bot.send("hello") + + mock_gitlab_class.return_value.projects.get.assert_not_called() diff --git a/tests/test_init.py b/tests/test_init.py new file mode 100644 index 0000000..2b3cb78 --- /dev/null +++ b/tests/test_init.py @@ -0,0 +1,38 @@ +"""Tests for Init command.""" + +from unittest.mock import patch + +from codefox.api.model_enum import ModelEnum +from codefox.cli.init import Init + + +def test_init_normalizes_none_args() -> None: + with patch.object(Init, "_ask_model", return_value=ModelEnum.GEMINI): + init = Init() + + assert init.model_enum is ModelEnum.GEMINI + assert init.api_class is ModelEnum.GEMINI.api_class + assert init.args == {} + + +def test_execute_uses_provider_and_token_from_args() -> None: + init = Init({"provider": "gemini", "token": "test-token"}) + + with ( + patch.object(init, "_ask_api_key") as mock_ask_api_key, + patch.object(init, "_write_config", return_value=True) as mock_write, + patch.object(init, "_ensure_ignore_file") as mock_ignore, + patch.object(init, "_ensure_yaml_config") as mock_yaml, + patch.object(init, "_ensure_gitignore") as mock_gitignore, + patch.object( + init, "_check_connection", return_value=True + ) as mock_check, + ): + init.execute() + + mock_ask_api_key.assert_not_called() + mock_write.assert_called_once_with("test-token") + mock_ignore.assert_called_once() + mock_yaml.assert_called_once() + mock_gitignore.assert_called_once() + mock_check.assert_called_once() diff --git a/uv.lock b/uv.lock index 0d83e73..7c6871b 100644 --- a/uv.lock +++ b/uv.lock @@ -217,7 +217,7 @@ wheels = [ [[package]] name = "codefox" -version = "0.4.0" +version = "0.4.1" source = { editable = "." } dependencies = [ { name = "bm25s" }, @@ -232,6 +232,7 @@ dependencies = [ { name = "pygithub" }, { name = "pygments" }, { name = "python-dotenv" }, + { name = "python-gitlab" }, { name = "pyyaml" }, { name = "qdrant-client" }, { name = "requests" }, @@ -267,6 +268,7 @@ requires-dist = [ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0" }, { name = "python-dotenv", specifier = "==1.2.1" }, + { name = "python-gitlab", specifier = "==8.1.0" }, { name = "pyyaml", specifier = "==6.0.3" }, { name = "qdrant-client", specifier = ">=1.7.0" }, { name = "requests", specifier = ">=2.28.0" }, @@ -1742,6 +1744,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, ] +[[package]] +name = "python-gitlab" +version = "8.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "requests-toolbelt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/1d/a62fea1f3312fd9e58af41466ae072796a09684dd0cd825cc042ba39488c/python_gitlab-8.1.0.tar.gz", hash = "sha256:660f15e3f889ec430797d260322bc61d90f8d90accfc10ba37593b11aed371bd", size = 401576, upload-time = "2026-02-28T01:26:32.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/d4/9848be62ef23fcac203f4386faf43a2cc13a4888447b3f5fbf7346f31374/python_gitlab-8.1.0-py3-none-any.whl", hash = "sha256:b1a59e81e5e0363185b446a707dc92c27ee8bf1fc14ce75ed8eafa58cbdce63a", size = 144498, upload-time = "2026-02-28T01:26:31.14Z" }, +] + [[package]] name = "pywin32" version = "311" @@ -1953,6 +1968,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + [[package]] name = "rich" version = "14.3.2"