GitLab整合GPT進行自動化CodeReview實戰

PetterLiu發表於2024-10-17

GitLab整合GPT進行自動化CodeReview實戰

image

背景

GitLab基於Merge Request的Code Review流程是一個團隊協作中至關重要的環節,它確保了程式碼質量並促進了團隊成員之間的有效溝通。CodeReview準備工作如下

  • 為了確保Code Review的有效性,需要設定分支的合併許可權。通常,只有專案維護者(maintainers)才擁有合併許可權,而開發者只能提交Merge Request並等待稽核。
  • 在GitLab的專案設定中,找到“Repository”下的“Protected Branches”,將需要保護的分支(如master、develop等)設定為只允許維護者合併,不允許其他人推送。
建立Merge Request併發起Code Review
  1. 建立Merge Request
    • 在GitLab的專案頁面上,找到“Merge Requests”選項,並點選“New merge request”按鈕。
    • 在彈出的頁面中,選擇源分支(即你剛剛推送的feature-branch)和目標分支(如developmaster),並填寫必要的描述資訊。
    • 點選“Submit merge request”按鈕,提交Merge Request。
  2. 指派稽核者與發起Code Review
    • 在Merge Request的頁面中,可以指派一個或多個稽核者(reviewer)來審查程式碼。通常,稽核者應該是除了開發者自己之外的其他開發者或專案維護者。
    • 稽核者透過Merge Request頁面可以看到程式碼修改記錄,並可以在“Changes”標籤頁中逐行審查程式碼。審查過程中,稽核者可以新增評論、提出修改意見或直接批准Merge Request。
  3. 討論與修改
    • 如果稽核者提出修改意見,開發者需要根據意見進行相應的修改,並將修改後的程式碼再次推送到遠端倉庫。
    • 稽核者和開發者可以在Merge Request的評論區域中進行討論,直到所有問題都得到解決。

如您相本地部署,請前文我們有介紹Docker Compose部署GitLab

目標

我們的目標是在 提交Merge Request後,由AI大模型(大型語言模型(Large Language Models)的介紹)自動對Code diff進行程式碼審查,生成改進建議。之前文章也寫過輕鬆連線 ChatGPT實現程式碼審查。今天我們再來實戰基於Gitlab.com的自動化CodeReview。
流程如下

image


實戰開始

.gitlab-ci.yml


.gitlab-ci.yml檔案是GitLab CI/CD流程的核心配置檔案,它在軟體開發過程中起著至關重要的作用。以下是.gitlab-ci.yml檔案的主要作用:

  1. 定義CI/CD任務
    • .gitlab-ci.yml檔案用於定義專案中各個階段的CI/CD任務,包括構建、測試、部署等,以及它們之間的依賴關係和執行順序。這使得開發者能夠清晰地規劃和管理專案的自動化流程。
  2. 版本控制
    • 將CI/CD配置與程式碼儲存在同一個版本控制系統中,使得配置變更能夠與程式碼變更保持一致,更易於管理和維護。這確保了CI/CD流程的穩定性和可追溯性。
  3. 自動化流程
    • 透過配置CI/CD流程,可以實現自動化構建、測試和部署,從而提高開發團隊的效率和產品質量。自動化流程減少了人為錯誤,並加速了軟體的交付速度。
  4. 規範化流程
    • .gitlab-ci.yml檔案定義了統一的CI/CD配置檔案結構和語法規則,有助於規範團隊的開發流程。這降低了錯誤發生的可能性,並提高了流程的可維護性。
  5. 分階段定義任務
    • 將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


核心程式碼

安裝依賴

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 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環境變數配置

clipboard

PAT 是 Gitlab 個人token

OPENAI_API_BASE 是為了替換 AI請求基礎地址, 我們在這兒使用的是https://vip.apiyi.com/v1/,你也可以使用其他廠商代理。

OPENAI_API_KEY 是對應申請的Key


Job 詳情

clipboard

Running logs

clipboard

ChangeSet

我們用於測試程式碼,刻意加了一條程式碼

clipboard


MergeRequest自動插入呼叫GPT審查程式碼評價

結果如下

Code Review for HelloWorld.java

Overall Code Quality

  • The code provides a simple implementation to print a greeting and the current time using DateTimeFormatter and LocalDateTime.
  • While the implementation achieves its goal, there are several aspects that can be improved for better readability, maintainability, and functionality.

Problematic Code Snippets & Recommendations

  1. Static Context Issue:
    • The line System.out.println("Begin line"); is placed directly in the class body which could cause a compilation error since it is not inside a method.

Recommendation: Move the System.out.println("Begin line"); statement into a method, such as 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)); } }

  1. Use of final for Instance Variables:
    • While it is often good practice to use final for constants, using final for now makes it initialized with the value at the creation time of an object. It would not change over time. If the intent is to always show the current time whenever the greeting method is called, consider initializing it inside the method.

Recommendation: Move the initialization of now into the greet() method to show the current time at each invocation.

public void greet() { LocalDateTime now = LocalDateTime.now(); System.out.println("Hello, World! The current time is " + dtf.format(now)); }

  1. Code Readability:
    • The time format string could be extracted into a constant or configurable setting for better readability and maintainability.

Recommendation: Define the date-time pattern as a constant.

private static final String DATE_TIME_PATTERN = "h:mm:ss a 'on' MMMM d, yyyy"; final DateTimeFormatter dtf = DateTimeFormatter.ofPattern(DATE_TIME_PATTERN);

  1. Error Handling:
    • Although this example works under normal circumstances, consider implementing error handling or checks in production code, especially when dealing with date and time formats.

Recommendation: You could add a try-catch block around the date formatting within the greet() method.

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()); } }

Summary

Your current approach in HelloWorld.java works but requires some modifications to ensure better practices in Java. Make sure to encapsulate code in methods, consider the context of variable initialization, promote code readability, and handle potential exceptions or errors gracefully. Overall, these changes will enhance the robustness and maintainability of your code.

中文翻譯後參考

程式碼審查對於HelloWorld.java

整體程式碼質量

該程式碼提供了一個簡單的實現來列印問候語以及使用DateTimeFormatter和LocalDateTime獲取的當前時間。

儘管實現達到了目的,但在可讀性、可維護性和功能性方面還有幾個可以改進的地方。

有問題的程式碼片段及建議

靜態上下文問題:

直接在類體中放置了System.out.println("Begin line");這一行程式碼,這可能會導致編譯錯誤,因為它不在任何方法內。

建議:將System.out.println("Begin line");語句移到某個方法(如main)內。

```java

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()方法內以在每次呼叫時顯示當前時間。

```java

public void greet() {

LocalDateTime now = LocalDateTime.now();

System.out.println("Hello, World! The current time is " + dtf.format(now));

}

```

程式碼可讀性:

時間格式字串可以提取成常量或可配置設定,以便提高可讀性和可維護性。

建議:定義日期時間模式為常量。

```java

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塊。

```java

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消耗

clipboard

模型倍率 0.25,分組倍率 1,補全倍率 3,充值轉換率 1,用時 16秒


總結

本文基於GitLab實戰配置ChatGPT自動化程式碼審查(CodeReview)過程。 使用AI工具進行程式碼審查(CodeReview)在軟體工程領域具有重要意義,它代表著軟體開發流程的一次革新。

一、AI工具進行程式碼審查的意義
  1. 提高程式碼質量和安全性

    • AI工具透過機器學習、自然語言處理等技術,能夠基於大量的程式碼資料和規則集對程式碼進行智慧分析,識別潛在問題並提出改進建議。
    • 這些工具能夠檢測到人工審查可能遺漏的細微問題,如安全漏洞、程式碼異味(Code Smells)等,從而顯著提升程式碼的整體質量和安全性。
  2. 提升開發效率

    • AI程式碼審查工具通常能夠在幾秒鐘內完成對大量程式碼的分析,顯著縮短了程式碼審查的週期。
    • 透過自動化程式碼審查,開發團隊能夠減少人工審查的時間和精力,使開發者能夠更專注於創新和功能實現。
  3. 保持程式碼風格的一致性

    • AI工具能夠基於團隊的編碼規範和最佳實踐對程式碼進行審查,確保程式碼風格的一致性。
    • 這有助於維護程式碼的可讀性和可維護性,降低因程式碼風格不一致而導致的溝通成本。
  4. 輔助新手開發者成長

    • AI工具能夠提供實時的程式碼審查建議,幫助新手開發者快速適應團隊的編碼規範和最佳實踐。
    • 透過不斷學習和更新,AI工具還能夠適應團隊的特定需求,為開發者提供更個性化的支援。
二、AI工具與人工程式碼審查的關係
  1. 互補而非替代

    • 儘管AI工具在程式碼審查中表現出色,但它們並不能完全替代人工審查。
    • AI工具在處理複雜的業務邏輯、理解程式碼上下文和意圖方面仍有侷限。因此,開發者需要對AI工具的審查結果進行人工複核,以確保準確性。
  2. 協同工作

    • AI工具可以處理重複性、機械性的任務,如語法檢查、程式碼風格檢查等。
    • 人類審查者則專注於複雜的邏輯和業務需求審查,以及對AI工具審查結果的複核和確認。
    • 這種協作模式能夠大大提升程式碼審查的效率和準確性。

使用AI工具進行程式碼審查對於軟體工程具有重要意義。它不僅能夠提高程式碼質量和安全性、提升開發效率、保持程式碼風格的一致性,還能夠輔助新手開發者成長。然而,AI工具並不能完全替代人工審查,而是與人工審查形成互補和協同工作的關係。因此,在軟體工程實踐中,應充分利用AI工具的優勢,同時結合人工審查的經驗和判斷力,共同推動程式碼審查流程的最佳化和升級。



今天先到這兒,希望對雲原生,技術領導力, 企業管理,系統架構設計與評估,團隊管理, 專案管理, 產品管理,資訊保安,團隊建設 有參考作用 , 您可能感興趣的文章:
構建創業公司突擊小團隊
國際化環境下系統架構演化
微服務架構設計
影片直播平臺的系統架構演化
微服務與Docker介紹
Docker與CI持續整合/CD
網際網路電商購物車架構演變案例
網際網路業務場景下訊息佇列架構
網際網路高效研發團隊管理演進之一
訊息系統架構設計演進
網際網路電商搜尋架構演化之一
企業資訊化與軟體工程的迷思
企業專案化管理介紹
軟體專案成功之要素
人際溝通風格介紹一
精益IT組織與分享式領導
學習型組織與企業
企業創新文化與等級觀念
組織目標與個人目標
初創公司人才招聘與管理
人才公司環境與企業文化
企業文化、團隊文化與知識共享
高效能的團隊建設
專案管理溝通計劃
構建高效的研發與自動化運維
某大型電商雲平臺實踐
網際網路資料庫架構設計思路
IT基礎架構規劃方案一(網路系統規劃)
餐飲行業解決方案之客戶分析流程
餐飲行業解決方案之採購戰略制定與實施流程
餐飲行業解決方案之業務設計流程
供應鏈需求調研CheckList
企業應用之效能實時度量系統演變

如有想了解更多軟體設計與架構, 系統IT,企業資訊化, 團隊管理 資訊,請關注我的微信訂閱號:

image_thumb2_thumb_thumb_thumb_thumb[1]

作者:Petter Liu
出處:http://www.cnblogs.com/wintersun/
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連線,否則保留追究法律責任的權利。 該文章也同時釋出在我的獨立部落格中-Petter Liu Blog。

相關文章