添加项目历史管理和Git类型处理功能,完美!
This commit is contained in:
parent
9d2579eb1d
commit
94e20755d1
@ -41,3 +41,15 @@ class ProjectConfigAdmin(AjaxAdmin):
|
|||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
obj.create_by = request.user.username
|
obj.create_by = request.user.username
|
||||||
return super().save_model(request, obj, form, change)
|
return super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.ProjectHistory)
|
||||||
|
class ProjectHistoryAdmin(admin.ModelAdmin):
|
||||||
|
"""Admin配置"""
|
||||||
|
|
||||||
|
list_display = ["project", "mr_url", "source_branch", "target_branch", "mr_title"]
|
||||||
|
list_filter = ["project"]
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
obj.create_by = request.user.username
|
||||||
|
return super().save_model(request, obj, form, change)
|
||||||
|
|||||||
108
apps/pr/migrations/0002_projecthistory.py
Normal file
108
apps/pr/migrations/0002_projecthistory.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# Generated by Django 5.1.6 on 2025-02-26 17:12
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import simplepro.components.fields
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("pr", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ProjectHistory",
|
||||||
|
fields=[
|
||||||
|
("id", models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
|
(
|
||||||
|
"uid",
|
||||||
|
models.UUIDField(
|
||||||
|
db_index=True,
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
verbose_name="UUID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"create_at",
|
||||||
|
simplepro.components.fields.DateTimeField(
|
||||||
|
auto_now_add=True, db_index=True, verbose_name="创建时间"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"update_at",
|
||||||
|
simplepro.components.fields.DateTimeField(
|
||||||
|
auto_now=True, verbose_name="更新时间"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"delete_at",
|
||||||
|
simplepro.components.fields.DateTimeField(
|
||||||
|
blank=True, null=True, verbose_name="删除时间"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"create_by",
|
||||||
|
simplepro.components.fields.CharField(
|
||||||
|
blank=True, max_length=32, null=True, verbose_name="创建人"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"detail",
|
||||||
|
simplepro.components.fields.CharField(
|
||||||
|
blank=True, max_length=200, null=True, verbose_name="备注信息"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"project_url",
|
||||||
|
simplepro.components.fields.CharField(
|
||||||
|
blank=True, max_length=256, null=True, verbose_name="项目地址"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"mr_url",
|
||||||
|
simplepro.components.fields.CharField(
|
||||||
|
blank=True, max_length=256, null=True, verbose_name="MR地址"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"source_branch",
|
||||||
|
simplepro.components.fields.CharField(
|
||||||
|
blank=True, max_length=16, null=True, verbose_name="源分支"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"target_branch",
|
||||||
|
simplepro.components.fields.CharField(
|
||||||
|
blank=True, max_length=16, null=True, verbose_name="目标分支"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"mr_title",
|
||||||
|
simplepro.components.fields.CharField(
|
||||||
|
blank=True, max_length=256, null=True, verbose_name="MR标题"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"source_data",
|
||||||
|
models.JSONField(blank=True, null=True, verbose_name="源数据"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"project",
|
||||||
|
simplepro.components.fields.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="pr.projectconfig",
|
||||||
|
verbose_name="项目配置",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "项目历史",
|
||||||
|
"verbose_name_plural": "项目历史",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -101,3 +101,40 @@ class ProjectConfig(BaseModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.project_name}-{self.project_id}"
|
return f"{self.project_name}-{self.project_id}"
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectHistory(BaseModel):
|
||||||
|
"""
|
||||||
|
项目历史表
|
||||||
|
"""
|
||||||
|
project = fields.ForeignKey(
|
||||||
|
ProjectConfig,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
verbose_name="项目配置",
|
||||||
|
)
|
||||||
|
project_url = fields.CharField(
|
||||||
|
null=True, blank=True, max_length=256, verbose_name="项目地址"
|
||||||
|
)
|
||||||
|
mr_url = fields.CharField(
|
||||||
|
null=True, blank=True, max_length=256, verbose_name="MR地址"
|
||||||
|
)
|
||||||
|
source_branch = fields.CharField(
|
||||||
|
null=True, blank=True, max_length=16, verbose_name="源分支"
|
||||||
|
)
|
||||||
|
target_branch = fields.CharField(
|
||||||
|
null=True, blank=True, max_length=16, verbose_name="目标分支"
|
||||||
|
)
|
||||||
|
mr_title = fields.CharField(
|
||||||
|
null=True, blank=True, max_length=256, verbose_name="MR标题"
|
||||||
|
)
|
||||||
|
source_data = models.JSONField(
|
||||||
|
null=True, blank=True, verbose_name="源数据"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "项目历史"
|
||||||
|
verbose_name_plural = "项目历史"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.mr_title
|
||||||
|
|||||||
111
apps/pr/views.py
111
apps/pr/views.py
@ -1,4 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from pr import models
|
from pr import models
|
||||||
from django.views import View
|
from django.views import View
|
||||||
@ -6,6 +7,7 @@ from django.http import JsonResponse
|
|||||||
|
|
||||||
from utils.pr_agent import cli
|
from utils.pr_agent import cli
|
||||||
from utils.pr_agent.config_loader import get_settings
|
from utils.pr_agent.config_loader import get_settings
|
||||||
|
from utils.git_config import GitLabProvider
|
||||||
from utils import constant
|
from utils import constant
|
||||||
|
|
||||||
|
|
||||||
@ -39,77 +41,60 @@ def load_project_config(
|
|||||||
|
|
||||||
|
|
||||||
class WebHookView(View):
|
class WebHookView(View):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def select_git_provider(git_type):
|
||||||
|
"""
|
||||||
|
根据git类型选择git服务提供商: 目前仅配置gitlab
|
||||||
|
:param git_type:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if git_type == "gitlab":
|
||||||
|
return GitLabProvider()
|
||||||
|
else:
|
||||||
|
return GitLabProvider()
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
data = json.loads(request.body.decode('utf-8'))
|
"""
|
||||||
if not data:
|
处理Git服务器的Webhook事件
|
||||||
|
:param request:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
# 校验请求头
|
||||||
|
headers = request.headers
|
||||||
|
GIT_TYPE = constant.UA_TYPE.get(headers.get("User-Agent").split("/")[0])
|
||||||
|
|
||||||
|
if not GIT_TYPE:
|
||||||
|
return JsonResponse(status=400, data={"error": "Invalid Git Type"})
|
||||||
|
|
||||||
|
# 校验请求体
|
||||||
|
json_data = json.loads(request.body.decode('utf-8'))
|
||||||
|
if not json_data:
|
||||||
return JsonResponse(status=400, data={"error": "Invalid JSON"})
|
return JsonResponse(status=400, data={"error": "Invalid JSON"})
|
||||||
|
|
||||||
project_id = data.get('project', {}).get('id') or data.get('project_id')
|
# 获取对应处理类
|
||||||
|
provider = self.select_git_provider(GIT_TYPE)
|
||||||
|
project_id = provider.get_project_id(request_json=json_data)
|
||||||
if not project_id:
|
if not project_id:
|
||||||
return JsonResponse(status=400, data={"error": "Missing project ID"})
|
return JsonResponse(status=400, data={"error": "Missing project ID"})
|
||||||
|
|
||||||
project_config = models.ProjectConfig.objects.filter(project_id=project_id).first()
|
# 获取项目配置
|
||||||
# AI模型配置
|
project_config = provider.get_project_config(project_id=project_id)
|
||||||
api_base = project_config.git_config.pr_ai.api_base
|
|
||||||
api_key = project_config.git_config.pr_ai.api_key
|
|
||||||
model = project_config.git_config.pr_ai.llm_model
|
|
||||||
# Git服务器配置
|
|
||||||
git_url = project_config.git_config.git_url
|
|
||||||
git_type = constant.GIT_TYPE[project_config.git_config.git_type][1]
|
|
||||||
access_token = project_config.git_config.access_token
|
|
||||||
project_secret = project_config.project_secret
|
|
||||||
project_commands = project_config.commands.split(",")
|
|
||||||
|
|
||||||
config = load_project_config(
|
# Token 校验
|
||||||
git_url=git_url,
|
provider.check_secret(request_headers=headers, project_secret=project_config.get("project_secret"))
|
||||||
access_token=access_token,
|
|
||||||
project_secret=project_secret,
|
provider.get_merge_request(
|
||||||
openai_api_base=api_base,
|
request_data=json_data,
|
||||||
openai_key=api_key,
|
git_url=project_config.get("git_url"),
|
||||||
llm_model=model
|
access_token=project_config.get("access_token"),
|
||||||
|
api_base=project_config.get("api_base"),
|
||||||
|
api_key=project_config.get("api_key"),
|
||||||
|
llm_model=project_config.get("llm_model"),
|
||||||
|
project_commands=project_config.get("commands")
|
||||||
)
|
)
|
||||||
token = request.headers.get('X-Gitlab-Token')
|
|
||||||
if token:
|
|
||||||
token = token.strip()
|
|
||||||
expected_token = config["secret"].strip() if config["secret"] else None
|
|
||||||
if token != expected_token:
|
|
||||||
return JsonResponse(status=403, data={"error": "Invalid token"})
|
|
||||||
|
|
||||||
# 处理Merge Request事件
|
# 记录请求日志
|
||||||
if data.get('object_kind') == 'merge_request':
|
provider.save_pr_agent_log(request_data=json_data, project_id=project_config.get("project_id"))
|
||||||
merge_request = data.get('object_attributes', {})
|
|
||||||
if merge_request.get('state') == 'opened':
|
|
||||||
# 获取Merge Request的详细信息
|
|
||||||
mr_url = merge_request.get('url')
|
|
||||||
mr_action = merge_request.get('action')
|
|
||||||
get_settings().set("config.git_provider", git_type)
|
|
||||||
get_settings().set("gitlab.url", git_url)
|
|
||||||
get_settings().set("gitlab.personal_access_token", access_token)
|
|
||||||
get_settings().set("openai.api_base", api_base)
|
|
||||||
get_settings().set("openai.key", api_key)
|
|
||||||
get_settings().set("llm.model", model)
|
|
||||||
|
|
||||||
if mr_action == "update":
|
|
||||||
old_rev = merge_request.get("oldrev")
|
|
||||||
new_rev = merge_request.get("newrev")
|
|
||||||
if old_rev == new_rev:
|
|
||||||
return JsonResponse(status=200, data={"status": "ignored (no code change)"})
|
|
||||||
|
|
||||||
import threading
|
|
||||||
|
|
||||||
def run_cmd(command):
|
|
||||||
cli.run_command(mr_url, command)
|
|
||||||
|
|
||||||
threads = []
|
|
||||||
for cmd in project_commands:
|
|
||||||
if cmd not in [cmd[1] for cmd in constant.DEFAULT_COMMANDS]:
|
|
||||||
continue
|
|
||||||
t = threading.Thread(target=run_cmd, args=(cmd,))
|
|
||||||
threads.append(t)
|
|
||||||
t.start()
|
|
||||||
# 记录MR信息
|
|
||||||
return JsonResponse(status=200, data={"status": "review started"})
|
|
||||||
return JsonResponse(status=400, data={"error": "Merge request URL not found or action not open"})
|
|
||||||
return JsonResponse(status=200, data={"status": "ignored"})
|
return JsonResponse(status=200, data={"status": "ignored"})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -9,3 +9,16 @@ DEFAULT_COMMANDS = (
|
|||||||
("/describe", "/describe"),
|
("/describe", "/describe"),
|
||||||
("/improve_code", "/improve_code"),
|
("/improve_code", "/improve_code"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
UA_TYPE = {
|
||||||
|
"GitLab": "gitlab",
|
||||||
|
"GitHub": "github",
|
||||||
|
"Go-http-client": "gitea"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_git_type_from_ua(ua_value):
|
||||||
|
for git_type, git_value in GIT_TYPE:
|
||||||
|
if git_value == ua_value:
|
||||||
|
return git_type
|
||||||
|
return None
|
||||||
|
|||||||
160
apps/utils/git_config.py
Normal file
160
apps/utils/git_config.py
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from django.http import JsonResponse
|
||||||
|
|
||||||
|
from pr import models
|
||||||
|
from utils.pr_agent import cli
|
||||||
|
from utils.pr_agent.config_loader import get_settings
|
||||||
|
from utils import constant
|
||||||
|
|
||||||
|
|
||||||
|
class GitProvider(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def get_project_config(self, project_id):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_merge_request(
|
||||||
|
self,
|
||||||
|
request_data,
|
||||||
|
git_url,
|
||||||
|
access_token,
|
||||||
|
api_base,
|
||||||
|
api_key,
|
||||||
|
llm_model,
|
||||||
|
project_commands
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def run_command(self, mr_url, project_commands):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GitLabProvider(GitProvider):
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def check_secret(request_headers, project_secret):
|
||||||
|
"""
|
||||||
|
检查密钥
|
||||||
|
:param request_headers:
|
||||||
|
:param project_secret:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
token = request_headers.get("X-Gitlab-Token")
|
||||||
|
if token != project_secret:
|
||||||
|
return JsonResponse(status=403, data={"error": "Invalid token"})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_project_id(request_json):
|
||||||
|
"""
|
||||||
|
获取项目ID
|
||||||
|
:param request_json:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return request_json.get("project", {}).get("id")
|
||||||
|
|
||||||
|
def get_project_config(self, project_id):
|
||||||
|
"""
|
||||||
|
实现GitLab项目配置获取逻辑
|
||||||
|
:param project_id:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
git_config = models.GitConfig.objects.filter(git_type=0).first()
|
||||||
|
project_config = models.ProjectConfig.objects.filter(
|
||||||
|
git_config=git_config, project_id=project_id
|
||||||
|
).first()
|
||||||
|
if not project_config:
|
||||||
|
return JsonResponse(status=400, data={"error": "Project not found"})
|
||||||
|
if not project_config.is_enabled:
|
||||||
|
return JsonResponse(status=400, data={"error": "Project is disabled"})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"api_base": git_config.pr_ai.api_base,
|
||||||
|
"api_key": git_config.pr_ai.api_key,
|
||||||
|
"llm_model": git_config.pr_ai.llm_model,
|
||||||
|
"git_url": git_config.git_url,
|
||||||
|
"git_type": "gitlab",
|
||||||
|
"access_token": git_config.access_token,
|
||||||
|
"project_secret": project_config.project_secret,
|
||||||
|
"commands": project_config.commands.split(","),
|
||||||
|
"project_id": project_config.id
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_merge_request(
|
||||||
|
self,
|
||||||
|
request_data,
|
||||||
|
git_url,
|
||||||
|
access_token,
|
||||||
|
api_base,
|
||||||
|
api_key,
|
||||||
|
llm_model,
|
||||||
|
project_commands,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
实现GitLab Merge Request获取逻辑
|
||||||
|
:param project_commands:
|
||||||
|
:param llm_model:
|
||||||
|
:param api_key:
|
||||||
|
:param api_base:
|
||||||
|
:param access_token:
|
||||||
|
:param git_url:
|
||||||
|
:param request_data:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if request_data.get('object_kind') == 'merge_request':
|
||||||
|
merge_request = request_data.get('object_attributes', {})
|
||||||
|
if merge_request.get('state') == 'opened':
|
||||||
|
mr_url = merge_request.get('url')
|
||||||
|
mr_action = merge_request.get('action')
|
||||||
|
get_settings().set("config.git_provider", "gitlab")
|
||||||
|
get_settings().set("gitlab.url", git_url)
|
||||||
|
get_settings().set("gitlab.personal_access_token", access_token)
|
||||||
|
get_settings().set("openai.api_base", api_base)
|
||||||
|
get_settings().set("openai.key", api_key)
|
||||||
|
get_settings().set("llm.model", llm_model)
|
||||||
|
if mr_action == "update":
|
||||||
|
old_rev = merge_request.get("oldrev")
|
||||||
|
new_rev = merge_request.get("newrev")
|
||||||
|
if old_rev == new_rev:
|
||||||
|
return JsonResponse(
|
||||||
|
status=200, data={"status": "ignored (no code change)"}
|
||||||
|
)
|
||||||
|
self.run_command(mr_url, project_commands)
|
||||||
|
# 数据库留存
|
||||||
|
return JsonResponse(status=200, data={"status": "review started"})
|
||||||
|
return JsonResponse(status=400, data={"error": "Merge request URL not found or action not open"})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save_pr_agent_log(request_data, project_id):
|
||||||
|
"""
|
||||||
|
记录pr agent日志
|
||||||
|
:param request_data:
|
||||||
|
:param project_id:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
models.ProjectHistory.objects.create(
|
||||||
|
project_id=project_id,
|
||||||
|
project_url=request_data.get("project", {}).get("web_url"),
|
||||||
|
mr_url=request_data.get('object_attributes', {}).get("url"),
|
||||||
|
source_branch=request_data.get('object_attributes', {}).get("source_branch"),
|
||||||
|
target_branch=request_data.get('object_attributes', {}).get("target_branch"),
|
||||||
|
mr_title=request_data.get('object_attributes', {}).get("title"),
|
||||||
|
source_data=request_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
def run_command(self, mr_url, project_commands):
|
||||||
|
"""
|
||||||
|
自定义指令
|
||||||
|
:param mr_url:
|
||||||
|
:param project_commands:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
threads = []
|
||||||
|
for cmd in project_commands:
|
||||||
|
if cmd not in [cmd[1] for cmd in constant.DEFAULT_COMMANDS]:
|
||||||
|
continue
|
||||||
|
t = Thread(target=cli.run_command, args=(mr_url, cmd))
|
||||||
|
threads.append(t)
|
||||||
|
t.start()
|
||||||
Loading…
x
Reference in New Issue
Block a user