点击上方蓝字关注我们
背景
GitLab基于Merge Request的Code Review流程是一个团队协作中至关重要的环节,它确保了代码质量并促进了团队成员之间的有效沟通。CodeReview准备工作如下
为了确保Code Review的有效性,需要设置分支的合并权限。通常,只有项目维护者(maintainers)才拥有合并权限,而开发者只能提交Merge Request并等待审核。
在GitLab的项目设置中,找到“Repository”下的“Protected Branches”,将需要保护的分支(如master、develop等)设置为只允许维护者合并,不允许其他人推送。
创建Merge Request并发起Code Review
创建Merge Request:
在GitLab的项目页面上,找到“Merge Requests”选项,并点击“New merge request”按钮。在弹出的页面中,选择源分支(即你刚刚推送的feature-branch
)和目标分支(如develop
或master
),并填写必要的描述信息。点击“Submit merge request”按钮,提交Merge Request。
指派审核者与发起Code Review:
在Merge Request的页面中,可以指派一个或多个审核者(reviewer)来审查代码。通常,审核者应该是除了开发者自己之外的其他开发者或项目维护者。审核者通过Merge Request页面可以看到代码修改记录,并可以在“Changes”标签页中逐行审查代码。审查过程中,审核者可以添加评论、提出修改意见或直接批准Merge Request。
讨论与修改:如果审核者提出修改意见,开发者需要根据意见进行相应的修改,并将修改后的代码再次推送到远端仓库。审核者和开发者可以在Merge Request的评论区域中进行讨论,直到所有问题都得到解决。
如您计划在本地部署,请前文我们有介绍Docker Compose部署GitLab。
目标
我们的目标是在 提交Merge Request后,由AI大模型(大型语言模型(Large Language Models)的介绍)自动对Code diff进行代码审查,生成改进建议。之前文章也写过轻松连接 ChatGPT实现代码审查。今天我们再来实战基于Gitlab.com的自动化CodeReview。
流程如下
实战开始
.gitlab-ci.yml配置
.gitlab-ci.yml
文件是GitLab CI/CD流程的核心配置文件,它在软件开发过程中起着至关重要的作用。以下是.gitlab-ci.yml
文件的主要作用:
定义CI/CD任务:
.gitlab-ci.yml
文件用于定义项目中各个阶段的CI/CD任务,包括构建、测试、部署等,以及它们之间的依赖关系和执行顺序。这使得开发者能够清晰地规划和管理项目的自动化流程。
版本控制:
将CI/CD配置与代码存储在同一个版本控制系统中,使得配置变更能够与代码变更保持一致,更易于管理和维护。这确保了CI/CD流程的稳定性和可追溯性。
自动化流程:
通过配置CI/CD流程,可以实现自动化构建、测试和部署,从而提高开发团队的效率和产品质量。自动化流程减少了人为错误,并加速了软件的交付速度。
规范化流程:
.gitlab-ci.yml
文件定义了统一的CI/CD配置文件结构和语法规则,有助于规范团队的开发流程。这降低了错误发生的可能性,并提高了流程的可维护性。
分阶段定义任务:
将CI/CD流程划分为多个阶段(stages),每个阶段包含一个或多个任务(jobs)。这有助于组织和管理复杂的CI/CD流程,使得任务的执行顺序清晰可控。
我们只定义一个阶段用测试,实际CI过程应该是多阶段的,如下
stages:
- review
review:
stage: review
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
image: registry.gitlab.com/gitlab-ci-templates3/gitlab-ci-chatgpt:latest
script:
- python /app/main.py
核心代码
安装依赖requirements.txt
python-gitlab==3.15.0
openai==0.27.8
程序逻辑main.py
from typing import List, Any # 导入必要的类型提示模块
import gitlab # 导入GitLab SDK
import os # 导入操作系统相关模块
from itertools import dropwhile # 导入用于迭代器处理的函数
import openai # 导入OpenAI SDK
from dataclasses import dataclass # 导入用于创建数据类的装饰器
import logging # 导入日志记录模块
# 设置日志记录的基本配置,包括编码格式和日志级别
logging.basicConfig(encoding="utf-8", level=logging.INFO)
# 使用数据类定义一个存储文件路径和差异信息的对象
@dataclass
class Diff:
path: str
diff: str
# 初始化GitLab客户端,使用环境变量中存储的个人访问令牌(PAT)
gl = gitlab.Gitlab(private_token=os.environ["PAT"])
# 设置OpenAI API的密钥,从环境变量中读取
openai.api_key = os.environ["OPENAI_API_KEY"]
# 主函数定义
def main():
# 获取合并请求中的差异
diffs, mr = get_diffs_from_mr()
# 获取代码审查反馈
response = get_review(diffs)
# 记录审查反馈的日志
logging.info(response)
# 在合并请求中创建一条讨论,包含审查反馈
mr.discussions.create({"body": response})
# 函数用于从提供的差异中获取审查反馈
def get_review(diffs):
# 初始化用户消息,告知模型将进行代码审查
user_message_line = ["Review the following code:"]
# 遍历差异列表,构建完整的审查信息
for d in diffs:
user_message_line.append(f"PATH: {d.path}; DIFF: {d.diff}")
# 将差异信息转换为字符串
user_message = "\n".join(user_message_line)
# 创建OpenAI ChatCompletion请求
message = openai.ChatCompletion.create(
model="gpt-3.5-turbo", # 指定使用的模型
messages=[ # 构建对话消息
{
"role": "system", # 系统角色信息,定义模型的行为
"content": "You are a code reviewer on a Merge Request on Gitlab. Your responsibility is to review "
"the provided code and offer"
"recommendations for enhancement. Identify any problematic code snippets, "
"highlight potential issues, and evaluate the overall quality of the code you review. "
"You will be given input in the format PATH: <path of the file changed>; DIFF: <diff>. "
"In diffs, plus signs (+) will mean the line has been added and minus signs (-) will "
"mean that the line has been removed. Lines will be separated by \\n.",
},
{"role": "user", "content": user_message}, # 用户角色信息,提供审查的具体内容
],
)
# 从模型响应中提取审查反馈
response = message["choices"][0]["message"]["content"]
# 返回审查反馈
return response
# 函数用于从合并请求中获取差异信息
def get_diffs_from_mr() -> (List[Diff], Any):
# 获取项目信息
project = gl.projects.get(os.environ["CI_PROJECT_PATH"])
# 获取合并请求信息
mr = project.mergerequests.get(id=os.environ["CI_MERGE_REQUEST_IID"])
# 获取合并请求中的更改信息
changes = mr.changes()
# 清洗差异内容,并创建差异对象列表
diffs = [
Diff(c["new_path"], sanitize_diff_content(c["diff"]))
for c in changes["changes"]
]
# 返回差异对象列表和合并请求对象
return diffs, mr
# 函数用于清洗差异内容,去除不必要的头部信息
def sanitize_diff_content(diff: str):
# 使用dropwhile去除差异字符串前缀,直到遇到'@'字符
return "".join(list(dropwhile(lambda x: x != "@", diff[2:]))[2:])
# 如果该脚本作为主程序运行,则执行main函数
if __name__ == "__main__":
main()
以上可以看到其中包含提示词,可以进一步完善与修改。
自己部署GitLab集成
如果为支持自己部署GitLab, 支持通过环境变量配置 GitLab Server 的基础 URL,您可以在初始化 gitlab.Gitlab
对象时传入 base_url
参数。此外,您还需要确保 base_url
能够正确地从环境变量中读取。代码可以修改为
from typing import List, Any
import gitlab
import os
from itertools import dropwhile
import openai
from dataclasses import dataclass
import logging
logging.basicConfig(encoding='utf-8', level=logging.INFO)
@dataclass
class Diff:
path: str
diff: str
# 从环境变量读取 GitLab 基础 URL 和私有 token
gitlab_base_url = os.getenv("GITLAB_BASE_URL", "https://gitlab.example.com")
private_token = os.getenv("PAT")
gl = gitlab.Gitlab(url=gitlab_base_url, private_token=private_token)
openai.api_key = os.environ["OPENAI_API_KEY"]
def main():
diffs, mr = get_diffs_from_mr()
response = get_review(diffs)
logging.info(response)
mr.discussions.create({'body': response})
def get_review(diffs):
user_message_line = ["Review the following code:"]
for d in diffs:
user_message_line.append(f"PATH: {d.path}; DIFF: {d.diff}")
user_message = "\n".join(user_message_line)
message = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{
"role": "system",
"content": "You are a code reviewer on a Merge Request on Gitlab. Your responsibility is to review "
"the provided code and offer recommendations for enhancement. Identify any problematic "
"code snippets, highlight potential issues, and evaluate the overall quality of the code "
"you review. You will be given input in the format PATH: <path of the file changed>; "
"DIFF: <diff>. In diffs, plus signs (+) will mean the line has been added and minus "
"signs (-) will mean that the line has been removed. Lines will be separated by \\n."
},
{
"role": "user",
"content": user_message
}
],
)
response = message['choices'][0]['message']['content']
return response
def get_diffs_from_mr() -> (List[Diff], Any):
project = gl.projects.get(os.environ["CI_PROJECT_PATH"])
mr = project.mergerequests.get(id=os.environ["CI_MERGE_REQUEST_IID"])
changes = mr.changes()
diffs = [Diff(c['new_path'], sanitize_diff_content(c['diff'])) for c in changes['changes']]
return diffs, mr
def sanitize_diff_content(diff: str):
return "".join(list(dropwhile(lambda x: x != "@", diff[2:]))[2:])
if __name__ == "__main__":
main()
请确保在运行此脚本之前设置了正确的环境变量,例如通过命令行设置:
export GITLAB_BASE_URL=https://your-gitlab-server.example.com
Dockerfile
FROM python:3.9
WORKDIR /app
COPY main.py .
COPY requirements.txt .
RUN pip install -r requirements.txt
CMD ["/bin/bash"]
构建容器
docker login -u <gitlab-email> -p $PAT registry.gitlab.com
docker build -t registry.gitlab.com/gitlab-ci-templates3/gitlab-ci-code-reviewer .
docker push registry.gitlab.com/gitlab-ci-templates3/gitlab-ci-chatgpt
CICD环境变量配置
PAT 是 Gitlab 个人token
OPENAI_API_BASE 是为了替换 AI请求基础地址, 我们在这儿使用的是https://vip.apiyi.com/v1/,你也可以使用其他厂商代理。
OPENAI_API_KEY 是对应申请的Key
Job 详情
Running logs
ChangeSet
我们用于测试代码,刻意加了一条不太规范的代码,如下
MergeRequest自动插入调用GPT审查代码评价
结果如下
GPT的CodeReview评价中文翻译后参考
代码审查对于HelloWorld.java
整体代码质量
该代码提供了一个简单的实现来打印问候语以及使用DateTimeFormatter和LocalDateTime获取的当前时间。
尽管实现达到了目的,但在可读性、可维护性和功能性方面还有几个可以改进的地方。
有问题的代码片段及建议
静态上下文问题:
直接在类体中放置了System.out.println("Begin line");这一行代码,这可能会导致编译错误,因为它不在任何方法内。
建议:将System.out.println("Begin line");语句移到某个方法(如main)内。
public class HelloWorld {
final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("h:mm:ss a 'on' MMMM d, yyyy'");
final LocalDateTime now = LocalDateTime.now();
public static void main(String[] args) {
HelloWorld hello = new HelloWorld();
hello.greet();
}
public void greet() {
System.out.println("Begin line");
System.out.println("Hello, World! The current time is " + dtf.format(now));
}
}
实例变量使用final:
虽然通常使用final是一个好习惯,特别是对于常量来说,但是对now使用final意味着它会在对象创建时初始化,并且之后不会改变。如果目的是每次调用问候方法时显示当前时间,则应考虑在方法内部进行初始化。
建议:将now的初始化移到greet()方法内以在每次调用时显示当前时间。
public void greet() {
LocalDateTime now = LocalDateTime.now();
System.out.println("Hello, World! The current time is " + dtf.format(now));
}
代码可读性:
时间格式字符串可以提取成常量或可配置设置,以便提高可读性和可维护性。
建议:定义日期时间模式为常量。
private static final String DATE_TIME_PATTERN = "h:mm:ss a 'on' MMMM d, yyyy";
final DateTimeFormatter dtf = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN);
错误处理:
尽管这个例子在正常情况下可以工作,但在生产代码中考虑实现错误处理或检查,特别是在处理日期和时间格式时。
建议:可以在greet()方法中围绕日期格式化添加try-catch块。
public void greet() {
LocalDateTime now = LocalDateTime.now();
try {
System.out.println("Hello, World! The current time is " + dtf.format(now));
} catch (DateTimeException e) {
System.out.println("Error in formatting date and time: " + e.getMessage());
}
}
总结
你在HelloWorld.java中的当前方法是可行的,但需要一些修改以确保遵循Java中的最佳实践。确保将代码封装在方法内,考虑变量初始化的上下文,提高代码可读性,并优雅地处理潜在的异常或错误。总体上,这些改动将增强代码的健壮性和可维护性。
API调用Token消耗
模型倍率 0.25,分组倍率 1,补全倍率 3,充值转换率 1,用时 16秒
总结
本文基于GitLab实战配置ChatGPT自动化代码审查(CodeReview)过程。 使用AI工具进行代码审查(CodeReview)在软件工程领域具有重要意义,它代表着软件开发流程的一次革新。
一、AI工具进行代码审查的意义
提高代码质量和安全性:
AI工具通过机器学习、自然语言处理等技术,能够基于大量的代码数据和规则集对代码进行智能分析,识别潜在问题并提出改进建议。这些工具能够检测到人工审查可能遗漏的细微问题,如安全漏洞、代码异味(Code Smells)等,从而显著提升代码的整体质量和安全性。
提升开发效率:
AI代码审查工具通常能够在几秒钟内完成对大量代码的分析,显著缩短了代码审查的周期。通过自动化代码审查,开发团队能够减少人工审查的时间和精力,使开发者能够更专注于创新和功能实现。
保持代码风格的一致性:
AI工具能够基于团队的编码规范和最佳实践对代码进行审查,确保代码风格的一致性。这有助于维护代码的可读性和可维护性,降低因代码风格不一致而导致的沟通成本。
辅助新手开发者成长:
AI工具能够提供实时的代码审查建议,帮助新手开发者快速适应团队的编码规范和最佳实践。通过不断学习和更新,AI工具还能够适应团队的特定需求,为开发者提供更个性化的支持。
二、AI工具与人工代码审查的关系
互补而非替代:
尽管AI工具在代码审查中表现出色,但它们并不能完全替代人工审查。AI工具在处理复杂的业务逻辑、理解代码上下文和意图方面仍有局限。因此,开发者需要对AI工具的审查结果进行人工复核,以确保准确性。
协同工作:
AI工具可以处理重复性、机械性的任务,如语法检查、代码风格检查等。人类审查者则专注于复杂的逻辑和业务需求审查,以及对AI工具审查结果的复核和确认。这种协作模式能够大大提升代码审查的效率和准确性。
使用AI工具进行代码审查对于软件工程具有重要意义。它不仅能够提高代码质量和安全性、提升开发效率、保持代码风格的一致性,还能够辅助新手开发者成长。然而,AI工具并不能完全替代人工审查,而是与人工审查形成互补和协同工作的关系。因此,在软件工程实践中,应充分利用AI工具的优势,同时结合人工审查的经验和判断力,共同推动代码审查流程的优化和升级。