Skip to content
Merged

Dev #18

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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/

*.pyc
14 changes: 12 additions & 2 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -28,6 +28,16 @@ runs:
with:
python-version: '3.12'

- name: Restore CodeFox cache
uses: actions/cache@v4
with:
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 }}-
${{ runner.os }}-codefox-${{ github.repository }}-
${{ runner.os }}-codefox-

- name: Install CodeFox
shell: bash
run: |
Expand Down
35 changes: 35 additions & 0 deletions codefox-template.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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
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}"
echo "GITLAB_URL=${CI_SERVER_URL}"
echo "GITLAB_REPOSITORY=${CI_PROJECT_ID}"
echo "PR_NUMBER=${CI_MERGE_REQUEST_IID}"
} > "$CI_PROJECT_DIR/.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"'
56 changes: 56 additions & 0 deletions codefox/bots/gitlab_bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import os

from gitlab import Gitlab
from gitlab.exceptions import GitlabCreateError, GitlabGetError


class GitLabBot:
def __init__(self) -> None:
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_TOKEN environment variable is not set. "
"This token is required to authenticate with the GitLab API."
)

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(
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,
)

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(repository))
mr = project.mergerequests.get(int(mr_iid))
mr.notes.create({"body": message})
except GitlabGetError as exc:
raise RuntimeError(
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
22 changes: 22 additions & 0 deletions codefox/cli/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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]")
19 changes: 16 additions & 3 deletions codefox/cli/init.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from pathlib import Path
from typing import Any

import yaml
from dotenv import load_dotenv, set_key
Expand All @@ -11,9 +12,18 @@


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):
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(resolved_args["provider"])

self.api_class: type[BaseAPI] = self.model_enum.api_class

self.args: dict[str, Any] = resolved_args

self.config_path = Path(".codefoxenv")
self.ignore_path = Path(".codefoxignore")
self.yaml_config_path = Path(".codefox.yml")
Expand All @@ -28,7 +38,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
Expand Down
10 changes: 9 additions & 1 deletion codefox/cli/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.get("ci", False) and os.getenv("GITLAB_TOKEN"):
self.gitlab_bot = GitLabBot()

def execute(self) -> None:
source_branch, target_branch = self._get_branchs()
Expand Down Expand Up @@ -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:
Expand Down
9 changes: 8 additions & 1 deletion codefox/cli_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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
Expand Down Expand Up @@ -35,6 +36,12 @@ def run(self) -> None:
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()
list_model = List(api_class, self.args)
Expand All @@ -54,7 +61,7 @@ def run(self) -> None:
return

if self.command == "init":
init = Init()
init = Init(self.args or {})
init.execute()
return

Expand Down
32 changes: 25 additions & 7 deletions codefox/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -24,14 +24,32 @@ 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",
Expand All @@ -43,7 +61,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",
Expand Down
8 changes: 6 additions & 2 deletions codefox/utils/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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

# ------------------------------------------------------------------
Expand Down
Loading
Loading