Merge pull request !2850 from rhodecode-enterprise-ce feature/RCCE-334_Button-for-Ai-code-review
feature: implements button to start AI code review
This commit is contained in:
commit
9fe203b496
13 changed files with 454 additions and 51 deletions
|
|
@ -100,7 +100,7 @@ PYRAMID_SETTINGS = {}
|
|||
EXTENSIONS = {}
|
||||
|
||||
__version__ = ".".join((str(each) for each in VERSION[:3]))
|
||||
__dbversion__ = 119 # defines current db version for migrations
|
||||
__dbversion__ = 120 # defines current db version for migrations
|
||||
__license__ = "AGPLv3, and Commercial License"
|
||||
__author__ = "RhodeCode GmbH"
|
||||
__url__ = "https://code.rhodecode.com"
|
||||
|
|
|
|||
|
|
@ -79,7 +79,8 @@ class HomeView(BaseAppView, DataGridAppView):
|
|||
_users = UserModel().get_users(name_contains=query, only_active=active)
|
||||
|
||||
def maybe_skip_user(usr):
|
||||
if skip_default_user and usr["username"] in [UserModel.cls.DEFAULT_USER, UserModel.cls.GHOST_USER]:
|
||||
if (skip_default_user and usr["username"] == UserModel.cls.DEFAULT_USER
|
||||
or usr["username"] == UserModel.cls.GHOST_USER):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -825,6 +825,20 @@ def includeme(config):
|
|||
renderer="json_ext",
|
||||
)
|
||||
|
||||
config.add_route(
|
||||
name="pullrequest_start_ai_code_review",
|
||||
pattern=r"/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/ai/review",
|
||||
repo_route=True,
|
||||
repo_forbid_when_archived=True,
|
||||
)
|
||||
config.add_view(
|
||||
RepoPullRequestsView,
|
||||
attr="add_ai_user_to_reviewers",
|
||||
route_name="pullrequest_start_ai_code_review",
|
||||
request_method="POST",
|
||||
renderer="json_ext",
|
||||
)
|
||||
|
||||
config.add_route(
|
||||
name="pullrequest_merge",
|
||||
pattern=r"/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge",
|
||||
|
|
|
|||
91
rhodecode/apps/repository/tests/test_ai_code_review.py
Normal file
91
rhodecode/apps/repository/tests/test_ai_code_review.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import pytest
|
||||
from mock.mock import patch
|
||||
|
||||
from rhodecode.model.db import PullRequestReviewers, User
|
||||
from rhodecode.tests.routes import route_path
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("autologin_user", "app")
|
||||
@pytest.mark.backends("git", "hg")
|
||||
class TestAiCodeReview:
|
||||
def test_pr_closed_review_not_possible(self, backend, pr_util):
|
||||
pull_request = pr_util.create_pull_request()
|
||||
pull_request.status = pull_request.STATUS_CLOSED
|
||||
|
||||
with patch("rhodecode.apps.repository.views.repo_pull_requests.run_task") as run_task_mock:
|
||||
response = self.app.post(
|
||||
route_path(
|
||||
"pullrequest_start_ai_code_review",
|
||||
repo_name=pull_request.target_repo.repo_name,
|
||||
pull_request_id=pull_request.pull_request_id
|
||||
)
|
||||
)
|
||||
|
||||
session = response.get_session_from_response()
|
||||
err_msg = session.peek_flash('error')
|
||||
assert response.status_code == 200
|
||||
assert response.json_body == {'response': True, 'redirect_url': None}
|
||||
assert len(err_msg) == 1
|
||||
assert err_msg[0] == "Cannot start review for closed pull requests."
|
||||
assert run_task_mock.call_count == 0
|
||||
|
||||
def test_ai_review_finished(self, backend, pr_util):
|
||||
pull_request = pr_util.create_pull_request()
|
||||
pull_request.ai_code_review_state = {
|
||||
"review_state": "finished"
|
||||
}
|
||||
|
||||
repo_name = pull_request.target_repo.repo_name
|
||||
pull_request_id = pull_request.pull_request_id
|
||||
|
||||
with patch("rhodecode.apps.repository.views.repo_pull_requests.run_task") as run_task_mock:
|
||||
response = self.app.post(
|
||||
route_path(
|
||||
"pullrequest_start_ai_code_review",
|
||||
repo_name=repo_name,
|
||||
pull_request_id=pull_request_id
|
||||
)
|
||||
)
|
||||
redirect_url = route_path(
|
||||
"pullrequest_show",
|
||||
repo_name=repo_name,
|
||||
pull_request_id=pull_request_id,
|
||||
)
|
||||
|
||||
session = response.get_session_from_response()
|
||||
err_msg = session.peek_flash('error')
|
||||
success_message = session.peek_flash('success')
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json_body == {'response': True, 'redirect_url': redirect_url}
|
||||
assert len(err_msg) == 0
|
||||
assert len(success_message) == 0
|
||||
assert run_task_mock.call_count == 0
|
||||
|
||||
def test_ai_user_added_to_reviewers_and_review_started(self, backend, pr_util):
|
||||
pull_request = pr_util.create_pull_request()
|
||||
pr_id = pull_request.pull_request_id
|
||||
|
||||
with patch("rhodecode.apps.repository.views.repo_pull_requests.run_task") as run_task_mock:
|
||||
response = self.app.post(
|
||||
route_path(
|
||||
"pullrequest_start_ai_code_review",
|
||||
repo_name=pull_request.target_repo.repo_name,
|
||||
pull_request_id=pr_id
|
||||
)
|
||||
)
|
||||
|
||||
existing_reviewers = PullRequestReviewers.get_pull_request_reviewers(
|
||||
pr_id, role=PullRequestReviewers.ROLE_REVIEWER
|
||||
)
|
||||
|
||||
ai_user = User.get_ai_user()
|
||||
|
||||
session = response.get_session_from_response()
|
||||
success_message = session.peek_flash('success')
|
||||
|
||||
assert response.status_code == 200
|
||||
assert run_task_mock.call_count == 1
|
||||
assert ai_user.user_id in [u.user_id for u in existing_reviewers]
|
||||
assert len(success_message) == 1
|
||||
assert success_message[0] == "AI code review has started. The review will be ready in about 1–3 minutes."
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
import logging
|
||||
import collections
|
||||
from copy import deepcopy
|
||||
|
||||
import formencode
|
||||
import formencode.htmlfill
|
||||
|
|
@ -61,6 +62,7 @@ from rhodecode.model.forms import PullRequestForm
|
|||
from rhodecode.model.meta import Session
|
||||
from rhodecode.model.pull_request import PullRequestModel, MergeCheck
|
||||
from rhodecode.model.scm import ScmModel
|
||||
from rhodecode.model.settings import SettingsModel
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -360,6 +362,9 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
|
|||
c.state_progressing = pull_request.is_state_changing()
|
||||
c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
|
||||
|
||||
rc_settings = SettingsModel()
|
||||
c.ai_enabled = rc_settings.get_setting_by_name("ai_features_enabled")
|
||||
|
||||
_new_state = {
|
||||
"created": PullRequest.STATE_CREATED,
|
||||
}.get(self.request.GET.get("force_state"))
|
||||
|
|
@ -440,6 +445,11 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
|
|||
c.renderer = pull_request_at_ver.description_renderer or c.renderer
|
||||
c.pull_request_latest = pull_request_latest
|
||||
|
||||
c.ai_code_review_state = None
|
||||
ai_pr_state = pull_request.ai_code_review_state
|
||||
if ai_pr_state:
|
||||
c.ai_code_review_state = ai_pr_state.get("review_state", None)
|
||||
|
||||
# inject latest version
|
||||
latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
|
||||
c.versions = versions + [latest_ver]
|
||||
|
|
@ -1280,6 +1290,116 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
|
|||
h.route_path("pullrequest_show", repo_name=target_repo, pull_request_id=pull_request.pull_request_id)
|
||||
)
|
||||
|
||||
@LoginRequired()
|
||||
@HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin")
|
||||
def add_ai_user_to_reviewers(self):
|
||||
_ = self.request.translate
|
||||
c = self.load_default_context()
|
||||
redirect_url = None
|
||||
|
||||
pull_request = PullRequest.get_or_404(self.request.matchdict["pull_request_id"])
|
||||
if pull_request.is_closed():
|
||||
log.debug("AI code review: forbidden because pull request is closed")
|
||||
message = _("Cannot start review for closed pull requests.")
|
||||
h.flash(message, category="error")
|
||||
return {"response": True, "redirect_url": redirect_url}
|
||||
|
||||
redirect_url = h.route_path(
|
||||
"pullrequest_show",
|
||||
repo_name=self.db_repo_name,
|
||||
pull_request_id=pull_request.pull_request_id,
|
||||
)
|
||||
|
||||
self._try_start_ai_code_review(_, pull_request, add_existing_users=True)
|
||||
|
||||
return {"response": True, "redirect_url": redirect_url}
|
||||
|
||||
def _set_ai_pr_state_pending(self, pull_request: PullRequest):
|
||||
ai_state = deepcopy(pull_request.ai_code_review_state)
|
||||
|
||||
if not ai_state:
|
||||
ai_state = {
|
||||
"review_state": "pending"
|
||||
}
|
||||
else:
|
||||
ai_state["review_state"] = "pending"
|
||||
|
||||
pull_request.ai_code_review_state = ai_state
|
||||
Session().commit()
|
||||
|
||||
def _try_start_ai_code_review(self, _, pull_request: PullRequest, add_existing_users: bool = False) -> bool:
|
||||
already_finished = False
|
||||
ai_state = pull_request.ai_code_review_state
|
||||
|
||||
if ai_state:
|
||||
already_finished = ai_state.get("review_state") in ["pending", "finished"]
|
||||
|
||||
if already_finished:
|
||||
log.warning(f"Code review already finished.")
|
||||
return False
|
||||
|
||||
admin_permissions = (
|
||||
"repository.admin",
|
||||
"hg.admin",
|
||||
)
|
||||
is_admin = h.HasRepoPermissionAny(*admin_permissions)(
|
||||
user=self._rhodecode_user, repo_name=pull_request.target_repo.repo_name
|
||||
)
|
||||
|
||||
if is_admin:
|
||||
ai_user = User.get_ai_user()
|
||||
log.debug("Initiating code review for pull request id: %s", pull_request.pull_request_id)
|
||||
|
||||
reviewers_data = []
|
||||
|
||||
if add_existing_users:
|
||||
current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
|
||||
pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER
|
||||
)
|
||||
for existing_reviewer in current_reviewers:
|
||||
reviewers_data.append(
|
||||
(
|
||||
existing_reviewer.user_id,
|
||||
existing_reviewer.reasons,
|
||||
existing_reviewer.mandatory,
|
||||
existing_reviewer.role,
|
||||
existing_reviewer.rule_data,
|
||||
)
|
||||
)
|
||||
|
||||
reviewers_data.append(
|
||||
(
|
||||
ai_user.user_id,
|
||||
["adds system ai user for review"],
|
||||
False,
|
||||
PullRequestReviewers.ROLE_REVIEWER,
|
||||
pull_request.reviewer_data
|
||||
)
|
||||
)
|
||||
|
||||
PullRequestModel().update_reviewers(
|
||||
pull_request, reviewers_data, self._rhodecode_db_user
|
||||
)
|
||||
|
||||
Session().commit()
|
||||
|
||||
run_task(tasks.start_ai_code_review, pull_request.pull_request_id)
|
||||
self._set_ai_pr_state_pending(pull_request)
|
||||
|
||||
msg = _("AI code review has started. The review will be ready in about 1–3 minutes.")
|
||||
h.flash(msg, category="success")
|
||||
else:
|
||||
log.debug("AI code review canceled, only admin can initiate AI code review.")
|
||||
msg = _(
|
||||
"AI code review is not available. This feature is in BETA and can only be initiated by an admin."
|
||||
)
|
||||
h.flash(
|
||||
msg,
|
||||
category="warning",
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
@LoginRequired()
|
||||
@NotAnonymous()
|
||||
@HasRepoPermissionAnyDecorator("repository.read", "repository.write", "repository.admin")
|
||||
|
|
@ -1320,30 +1440,7 @@ class RepoPullRequestsView(RepoAppView, DataGridAppView):
|
|||
user_id = safe_int(member_data.get("user_id", -1))
|
||||
|
||||
if ai_user_id == user_id:
|
||||
admin_permissions = (
|
||||
"repository.admin",
|
||||
"hg.admin",
|
||||
)
|
||||
is_admin = h.HasRepoPermissionAny(*admin_permissions)(
|
||||
user=self._rhodecode_user, repo_name=pull_request.target_repo.repo_name
|
||||
)
|
||||
|
||||
if is_admin:
|
||||
log.debug("Initiating code review for pull request id: %s", pull_request.pull_request_id)
|
||||
run_task(tasks.start_ai_code_review, pull_request.pull_request_id)
|
||||
msg = _("AI code review has started. The review will be ready in about 1–3 minutes.")
|
||||
h.flash(msg, category="success")
|
||||
else:
|
||||
log.debug("AI code review canceled, only admin can initiate AI code review.")
|
||||
msg = _(
|
||||
"AI code review is not available. This feature is in BETA and can only be initiated by an admin."
|
||||
)
|
||||
h.flash(
|
||||
msg,
|
||||
category="warning",
|
||||
)
|
||||
del members_[i]
|
||||
|
||||
self._try_start_ai_code_review(_, pull_request)
|
||||
break
|
||||
|
||||
self._update_reviewers(
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ AI_MODEL_VERSION = "ai_model_version"
|
|||
ACTIONS_V1 = {
|
||||
"ai.code-review.start": {PR_ID: 0, AI_MODEL: "", AI_MODEL_VERSION: ""},
|
||||
"ai.code-review.finish": {PR_ID: 0, AI_MODEL: "", "error": False, "error_message": ""},
|
||||
"ai.code-review.error": {PR_ID: 0, AI_MODEL: "", "error": False, "error_message": ""},
|
||||
"user.login.success": {"user_agent": ""},
|
||||
"user.login.failure": {"user_agent": ""},
|
||||
"user.logout": {"user_agent": ""},
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ by celery daemon
|
|||
|
||||
import os
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from logging import Logger
|
||||
from typing import Any
|
||||
|
||||
|
|
@ -503,40 +504,72 @@ def start_ai_code_review(pull_request_id):
|
|||
log = get_logger(start_ai_code_review)
|
||||
log.info("Starting AI code review for pull request id: %s", pull_request_id)
|
||||
rc_settings = SettingsModel()
|
||||
ai_enabled = rc_settings.get_setting_by_name("ai_features_enabled")
|
||||
ai_setting = rc_settings.get_setting_by_name("ai_features_enabled")
|
||||
ai_enabled = False
|
||||
ai_user = User.get_ai_user()
|
||||
|
||||
if ai_setting:
|
||||
ai_enabled = ai_setting.app_settings_value
|
||||
|
||||
if not ai_enabled:
|
||||
log.info("AI code review is disabled.")
|
||||
err_msg = f"AI code review is disabled. Pull request id: {pull_request_id}"
|
||||
log.warning(err_msg)
|
||||
audit_logger.store(
|
||||
"ai.code-review.error",
|
||||
user=ai_user,
|
||||
action_data={
|
||||
audit_logger.PR_ID: pull_request_id,
|
||||
audit_logger.AI_MODEL: None,
|
||||
"error": True,
|
||||
"error_message": err_msg,
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
pull_request: PullRequest = PullRequest.get(pull_request_id)
|
||||
|
||||
if not pull_request:
|
||||
log.info("Pull request id: %s not found, review not possible.", pull_request_id)
|
||||
err_msg = f"Pull request id: {pull_request_id} not found, review not possible."
|
||||
log.error(err_msg)
|
||||
_set_ai_pr_state(pull_request, "error")
|
||||
audit_logger.store(
|
||||
"ai.code-review.error",
|
||||
user=ai_user,
|
||||
action_data={
|
||||
audit_logger.PR_ID: pull_request_id,
|
||||
audit_logger.AI_MODEL: None,
|
||||
"error": True,
|
||||
"error_message": err_msg,
|
||||
},
|
||||
)
|
||||
return
|
||||
|
||||
if pull_request.is_state_changing():
|
||||
log.info("Pull request id: %s is in state changing, review not possible.", pull_request_id)
|
||||
return
|
||||
|
||||
settings_valid = _validate_settings(rc_settings, log)
|
||||
settings_valid, err_msg = _validate_settings(rc_settings)
|
||||
if not settings_valid:
|
||||
log.error(err_msg)
|
||||
_set_ai_pr_state(pull_request, "error")
|
||||
audit_logger.store(
|
||||
"ai.code-review.error",
|
||||
user=ai_user,
|
||||
action_data={
|
||||
audit_logger.PR_ID: pull_request_id,
|
||||
audit_logger.AI_MODEL: None,
|
||||
"error": True,
|
||||
"error_message": err_msg,
|
||||
},
|
||||
repo=pull_request.target_repo,
|
||||
)
|
||||
return
|
||||
|
||||
ai_user = User.get_ai_user()
|
||||
|
||||
diffset = _get_diffset(pull_request)
|
||||
ai_model = rc_settings.get_setting_by_name("ai_model").app_settings_value
|
||||
ai_model_version = rc_settings.get_setting_by_name("ai_model_version").app_settings_value
|
||||
ai_api_key = rc_settings.get_setting_by_name("ai_api_key").app_settings_value
|
||||
|
||||
if not ai_model or not ai_model or not ai_api_key:
|
||||
log.info("AI model or model version or API key is not set, review not possible.")
|
||||
_set_ai_pr_state(pull_request, "error")
|
||||
return
|
||||
|
||||
instructions = rc_settings.get_setting_by_name("ai_code_review_instructions")
|
||||
if instructions:
|
||||
instructions = instructions.app_settings_value.split("\r\n")
|
||||
|
||||
audit_logger.store(
|
||||
"ai.code-review.start",
|
||||
user=ai_user,
|
||||
|
|
@ -549,6 +582,42 @@ def start_ai_code_review(pull_request_id):
|
|||
commit=True,
|
||||
)
|
||||
|
||||
if not pull_request.revisions:
|
||||
err_msg = f"Current revision not found for pull request id: {pull_request_id} not found, review not possible."
|
||||
log.error(err_msg)
|
||||
_set_ai_pr_state(pull_request, "error")
|
||||
audit_logger.store(
|
||||
"ai.code-review.finish",
|
||||
user=ai_user,
|
||||
action_data={
|
||||
audit_logger.PR_ID: pull_request_id,
|
||||
audit_logger.AI_MODEL: ai_model_version,
|
||||
"error": True,
|
||||
"error_message": err_msg,
|
||||
},
|
||||
repo=pull_request.target_repo,
|
||||
)
|
||||
return
|
||||
|
||||
if pull_request.is_state_changing():
|
||||
err_msg = f"Pull request id: {pull_request_id} is in state changing, review not possible."
|
||||
log.error(err_msg)
|
||||
_set_ai_pr_state(pull_request, "error")
|
||||
audit_logger.store(
|
||||
"ai.code-review.finish",
|
||||
user=ai_user,
|
||||
action_data={
|
||||
audit_logger.PR_ID: pull_request_id,
|
||||
audit_logger.AI_MODEL: ai_model_version,
|
||||
"error": True,
|
||||
"error_message": err_msg,
|
||||
},
|
||||
repo=pull_request.target_repo,
|
||||
)
|
||||
return
|
||||
|
||||
diffset = _get_diffset(pull_request)
|
||||
|
||||
try:
|
||||
service = get_ai_service(
|
||||
api_key=ai_api_key,
|
||||
|
|
@ -556,8 +625,18 @@ def start_ai_code_review(pull_request_id):
|
|||
api_model_version_name=ai_model_version,
|
||||
)
|
||||
|
||||
instructions = rc_settings.get_setting_by_name("ai_code_review_instructions")
|
||||
if instructions:
|
||||
instructions = instructions.app_settings_value.split("\r\n")
|
||||
else:
|
||||
log.warning("instructions not set, using default instructions.")
|
||||
instructions = service.DEFAULT_BASIC_REVIEW_POINTS
|
||||
|
||||
response = service.code_review(diffset, instructions=instructions)
|
||||
_add_comments(response, pull_request, log, ai_user)
|
||||
|
||||
_set_ai_pr_state(pull_request, "finished")
|
||||
|
||||
audit_logger.store(
|
||||
"ai.code-review.finish",
|
||||
user=ai_user,
|
||||
|
|
@ -571,6 +650,7 @@ def start_ai_code_review(pull_request_id):
|
|||
)
|
||||
except AIServiceError as e:
|
||||
log.error("AI service error: %s", e)
|
||||
_set_ai_pr_state(pull_request, "error")
|
||||
audit_logger.store(
|
||||
"ai.code-review.finish",
|
||||
user=ai_user,
|
||||
|
|
@ -584,6 +664,20 @@ def start_ai_code_review(pull_request_id):
|
|||
)
|
||||
|
||||
|
||||
def _set_ai_pr_state(pull_request: PullRequest, state: str | None):
|
||||
ai_state = deepcopy(pull_request.ai_code_review_state)
|
||||
|
||||
if not ai_state:
|
||||
ai_state = {
|
||||
"review_state": state
|
||||
}
|
||||
else:
|
||||
ai_state["review_state"] = state
|
||||
|
||||
pull_request.ai_code_review_state = ai_state
|
||||
Session().commit()
|
||||
|
||||
|
||||
def _add_comments(response: Response, pull_request: PullRequest, log: Logger | Any, ai_user: User):
|
||||
log.debug(
|
||||
"Response from AI service: %s",
|
||||
|
|
@ -620,17 +714,21 @@ def insert_comment(line_number: int, text: str, f_path: str, pull_request: PullR
|
|||
)
|
||||
|
||||
|
||||
def _validate_settings(rc_settings, log):
|
||||
def _validate_settings(rc_settings):
|
||||
message = None
|
||||
valid = True
|
||||
|
||||
if not rc_settings.get_setting_by_name("ai_api_key"):
|
||||
log.info("AI API key is not set, review not possible.")
|
||||
return False
|
||||
message = "AI API key is not set, review not possible."
|
||||
valid = False
|
||||
if not rc_settings.get_setting_by_name("ai_model"):
|
||||
log.info("AI model is not set, review not possible.")
|
||||
return False
|
||||
message = "AI model is not set, review not possible."
|
||||
valid = False
|
||||
if not rc_settings.get_setting_by_name("ai_model_version"):
|
||||
log.info("AI model version is not set, review not possible.")
|
||||
return False
|
||||
return True
|
||||
message = "AI model version is not set, review not possible."
|
||||
valid = False
|
||||
|
||||
return valid, message
|
||||
|
||||
|
||||
def _get_diffset(
|
||||
|
|
|
|||
48
rhodecode/lib/dbmigrate/versions/120_version_5_8_0.py
Normal file
48
rhodecode/lib/dbmigrate/versions/120_version_5_8_0.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import json
|
||||
import logging
|
||||
|
||||
from psycopg2.errorcodes import DUPLICATE_COLUMN
|
||||
from sqlalchemy import *
|
||||
from sqlalchemy.engine import reflection
|
||||
|
||||
from alembic.migration import MigrationContext
|
||||
from alembic.operations import Operations
|
||||
|
||||
from rhodecode.lib.dbmigrate.versions import _reset_base
|
||||
from rhodecode.lib.jsonalchemy import MutationObj, JsonType
|
||||
from rhodecode.model import meta, init_model_encryption
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
"""
|
||||
Upgrade operations go here.
|
||||
Don't create your own engine; bind migrate_engine to your metadata
|
||||
"""
|
||||
_reset_base(migrate_engine)
|
||||
|
||||
from rhodecode.lib.dbmigrate.schema import db_4_20_0_1 as db
|
||||
|
||||
init_model_encryption(db)
|
||||
|
||||
context = MigrationContext.configure(migrate_engine.connect())
|
||||
op = Operations(context)
|
||||
inspector = inspect(migrate_engine)
|
||||
|
||||
pr_tables = [db.PullRequest.__table__, db.PullRequestVersion.__table__]
|
||||
for pr_table in pr_tables:
|
||||
existing_columns = [col["name"] for col in inspector.get_columns(pr_table.name)]
|
||||
new_column_name = "ai_code_review_state"
|
||||
if new_column_name not in existing_columns:
|
||||
with op.batch_alter_table(pr_table.name) as batch_op:
|
||||
new_column = Column(
|
||||
new_column_name,
|
||||
MutationObj.as_mutable(
|
||||
JsonType(dialect_map=dict(mysql=UnicodeText(16384))),
|
||||
),
|
||||
default=dict,
|
||||
)
|
||||
batch_op.add_column(new_column)
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
pass
|
||||
|
|
@ -4503,6 +4503,14 @@ class _PullRequestBase(BaseModel):
|
|||
default=dict,
|
||||
)
|
||||
|
||||
ai_code_review_state = Column(
|
||||
"ai_code_review_state",
|
||||
MutationObj.as_mutable(
|
||||
JsonType(dialect_map=dict(mysql=UnicodeText(DEFAULT_JSON_OBJ_SIZE)))
|
||||
),
|
||||
default=dict
|
||||
)
|
||||
|
||||
@property
|
||||
def reviewer_data_json(self):
|
||||
return str_json(self.reviewer_data)
|
||||
|
|
|
|||
|
|
@ -1532,9 +1532,10 @@ class PullRequestModel(BaseModel):
|
|||
raise ValueError("This pull request is closed")
|
||||
|
||||
reviewers = {}
|
||||
for user_id, reasons, mandatory, role, rules in reviewer_data:
|
||||
for user_id, reasons, mandatory, role, _ in reviewer_data:
|
||||
if isinstance(user_id, (int, str)):
|
||||
user_id = self._get_user(user_id).user_id
|
||||
|
||||
reviewers[user_id] = {"reasons": reasons, "mandatory": mandatory, "role": role}
|
||||
|
||||
reviewers_ids = set(reviewers.keys())
|
||||
|
|
|
|||
|
|
@ -417,6 +417,15 @@ window.ReviewersController = function () {
|
|||
|
||||
};
|
||||
|
||||
this.startAiCodeReview = function (repo_name, pull_request_id) {
|
||||
const url = `/${repo_name}/pull-request/${pull_request_id}/ai/review`
|
||||
const postData = {};
|
||||
const success = function (o) {
|
||||
window.location.reload();
|
||||
};
|
||||
ajaxPOST(url, postData, success);
|
||||
};
|
||||
|
||||
this.updateReviewers = function (repo_name, pull_request_id, role) {
|
||||
if (role === 'reviewer') {
|
||||
var postData = $('#reviewers input').serialize();
|
||||
|
|
|
|||
|
|
@ -615,7 +615,6 @@
|
|||
</div>
|
||||
|
||||
<div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
|
||||
|
||||
<div id="review_rules" style="display: none" class="">
|
||||
|
||||
<strong>${_('Reviewer rules')}</strong>
|
||||
|
|
@ -707,6 +706,31 @@
|
|||
</div>
|
||||
% endif
|
||||
|
||||
% if c.ai_enabled:
|
||||
<div id="ai-section" class="sidebar-element clear-both">
|
||||
<div class="right-sidebar-expanded-state pr-details-title">
|
||||
<span class="sidebar-heading">
|
||||
<i class="icon-info"></i>
|
||||
${_('AI')}
|
||||
</span>
|
||||
</div>
|
||||
% if not c.ai_code_review_state:
|
||||
<div class="btn-group btn-group-actions" style="margin-top: 10px">
|
||||
<button id="ai-code-review" class="btn btn-primary no-margin">
|
||||
${_('Start AI code review')}
|
||||
</button>
|
||||
</div>
|
||||
% else:
|
||||
<div style="margin-top: 10px">
|
||||
<span>${_('AI review already %s' %c.ai_code_review_state)}</span>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
##<input id="ai-code-review" class="btn" name="save" type="submit" value="${_('Start AI code review')}">
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
## TODOs
|
||||
<div id="todosTable" class="sidebar-element clear-both">
|
||||
<div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
|
||||
|
|
@ -981,6 +1005,16 @@ window.setObserversData = ${c.pull_request_set_observers_data_json | n};
|
|||
return true;
|
||||
});
|
||||
|
||||
const $aiCodeReviewBtn = $('#ai-code-review');
|
||||
$aiCodeReviewBtn.on('click', function () {
|
||||
$aiCodeReviewBtn.attr('disabled', 'disabled');
|
||||
$aiCodeReviewBtn.addClass('disabled');
|
||||
reviewersController.startAiCodeReview(
|
||||
templateContext.repo_name,
|
||||
templateContext.pull_request_data.pull_request_id,
|
||||
);
|
||||
});
|
||||
|
||||
$('#edit_pull_request').on('click', function (e) {
|
||||
var title = $('#pr-title-input').val();
|
||||
var description = codeMirrorInstance.getValue();
|
||||
|
|
|
|||
|
|
@ -262,6 +262,7 @@ def get_url_defs():
|
|||
"pullrequest_repo_refs": "/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}",
|
||||
"pullrequest_repo_targets": "/{repo_name}/pull-request/repo-destinations",
|
||||
"pullrequest_new": "/{repo_name}/pull-request/new",
|
||||
"pullrequest_start_ai_code_review": "/{repo_name}/pull-request/{pull_request_id}/ai/review",
|
||||
"pullrequest_create": "/{repo_name}/pull-request/create",
|
||||
"pullrequest_update": "/{repo_name}/pull-request/{pull_request_id}/update",
|
||||
"pullrequest_merge": "/{repo_name}/pull-request/{pull_request_id}/merge",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue