Allow specified projects to configure in their project.config file
diff --git a/README.md b/README.md
index fc33c3b..57cf1cf 100644
--- a/README.md
+++ b/README.md
@@ -19,34 +19,49 @@
3. **Verify:** After installing the plugin, you can see the following information in Gerrit's logs:
```bash
- INFO com.google.gerrit.server.plugins.PluginLoader : Loaded plugin chatgpt-code-review-gerrit-plugin, version 3.3.0
+ INFO com.google.gerrit.server.plugins.PluginLoader : Loaded plugin chatgpt-code-review-gerrit-plugin, version 1.0.0
```
You can also check the status of the chatgpt-code-review-gerrit-plugin on Gerrit's plugin page as Enabled.
## Configuration Parameters
+You have the option to establish global settings, or independently configure specific projects. If you choose
+independent configuration, the corresponding project settings will override the global parameters.
+
+### Global Configuration
+
To configure these parameters, you need to modify your Gerrit configuration file (`gerrit.config`). The file format is
as follows:
```
[plugin "chatgpt-code-review-gerrit-plugin"]
-
# Required parameters
gptToken = {gptToken}
gerritAuthBaseUrl = {gerritAuthBaseUrl}
- gerritUserName = {gerritUserName}
- gerritPassword = {gerritPassword}
+ ...
# Optional parameters
gptModel = {gptModel}
gptPrompt = {gptPrompt}
- gptMaxTokens = {gptMaxTokens}
- gptTemperature = {gptTemperature}
- globalEnable = {globalEnable}
- enabledRepos = {enabledRepos}
- patchSetReduction = {patchSetReduction}
- maxReviewLines = {maxReviewLines}
+ ...
+```
+
+### Project Configuration
+
+To add the following content, please edit the project.config file in refs/meta/config:
+
+```
+[plugin "chatgpt-code-review-gerrit-plugin"]
+ # Required parameters
+ gptToken = {gptToken}
+ gerritAuthBaseUrl = {gerritAuthBaseUrl}
+ ...
+
+ # Optional parameters
+ gptModel = {gptModel}
+ gptPrompt = {gptPrompt}
+ ...
```
### Required Parameters
@@ -55,24 +70,25 @@
- `gerritAuthBaseUrl`: The URL of your Gerrit instance. Similar to: https://gerrit.local.team/a
- `gerritUserName`: Gerrit username.
- `gerritPassword`: Gerrit password.
+- `globalEnable`: Default value is false. The plugin will only review specified repositories. If set to true, the plugin
+ will by default review all pull requests.
### Optional Parameters
- `gptModel`: The default model is gpt-3.5-turbo. You can also configure it to gpt-3.5-turbo-16k, gpt-4 or gpt-4-32k.
- `gptPrompt`: The default prompt is "Act as a Code Review Helper, please review this patch set:". You can modify it to
your preferred prompt.
-- `gptMaxTokens`: The default value is 4096 tokens. This determines the maximum dialogue length of ChatGPT. [Click
- here](https://platform.openai.com/tokenizer) to check the content token count.
- `gptTemperature`: The default value is 1. What sampling temperature to use, between 0 and 2. Higher values like 0.8
will make the output more random, while lower values like 0.2 will make it more focused and deterministic.
-- `globalEnable`: The default value is true. If set to false, the plugin will only review the repositories specified in
- the `enabledRepos`.
-- `enabledRepos` : The default value is empty. If `globalEnable` is set to false, the plugin will operate exclusively on
- the repositories specified here. The value should be a comma-separated list of repository names, for example: "
- repo1,repo2,repo3".
- `patchSetReduction`: The default value is false. If set to true, the plugin will attempt to reduce patch content by
compressing redundant blank lines, tabs, import statements, etc., in order to decrease the token count.
- `maxReviewLines`: The default value is 1000. This sets a limit on the number of lines of code included in the review.
+- `enabledProjects (for global configuration only)`:
+ The default value is an empty string. If globalEnable is set to false, the plugin will only run in the repositories
+ specified here. The value should be a comma-separated list of repository names, for example: "
+ project1,project2,project3".
+- `isEnabled (for project configuration only)`: The default is false. If set to true, the plugin will review the
+ patchSet of this project.
## Testing
diff --git a/README.zh.md b/README.zh.md
index ef63728..c4adea0 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -18,31 +18,46 @@
3. **确认:** 安装好插件后,你可以在 Gerrit 的日志中看到以下信息:
```bash
- INFO com.google.gerrit.server.plugins.PluginLoader : Loaded plugin chatgpt-code-review-gerrit-plugin, version 3.3.0
+ INFO com.google.gerrit.server.plugins.PluginLoader : Loaded plugin chatgpt-code-review-gerrit-plugin, version 1.0.0
```
并可以在 Gerrit 的插件页面中看到 chatgpt-code-review-gerrit-plugin 插件的状态为 Enabled。
## 配置参数
-要配置这些参数,你需要修改你的 Gerrit 配置文件(`gerrit.config`)。文件格式如下:
+你可以选择设定全局参数,或对特定项目进行独立配置。若进行独立配置,则相应的项目配置将覆盖全局参数。
+
+### 全局配置
+
+编辑 $gerrit_site/etc/`gerrit.config` 文件,添加以下内容:
```
[plugin "chatgpt-code-review-gerrit-plugin"]
# 必填参数
gptToken = {gptToken}
gerritAuthBaseUrl = {gerritAuthBaseUrl}
- gerritUserName = {gerritUserName}
- gerritPassword = {gerritPassword}
-
+ ...
+
# 可选参数
gptModel = {gptModel}
gptPrompt = {gptPrompt}
- gptMaxTokens = {gptMaxTokens}
- gptTemperature = {gptTemperature}
- globalEnable = {globalEnable}
- enabledRepos = {enabledRepos}
- patchSetReduction = {patchSetReduction}
- maxReviewLines = {maxReviewLines}
+ ...
+```
+
+### 项目配置
+
+编辑 refs/meta/config 的 project.config 文件,添加以下内容:
+
+```
+[plugin "chatgpt-code-review-gerrit-plugin"]
+ # 必填参数
+ gptToken = {gptToken}
+ gerritAuthBaseUrl = {gerritAuthBaseUrl}
+ ...
+
+ # 可选参数
+ gptModel = {gptModel}
+ gptPrompt = {gptPrompt}
+ ...
```
### 必填参数
@@ -51,21 +66,20 @@
- `gerritAuthBaseUrl`:Gerrit 实例的 URL。类似于:https://gerrit.local.team/a
- `gerritUserName`:Gerrit 用户名。
- `gerritPassword`:Gerrit 密码。
+- `globalEnable`: 默认值为 false。插件将只会审查指定的仓库,如果设为 true,插件默认会审查所有 review 请求。
### 可选参数
- `gptModel`:默认模型是 gpt-3.5-turbo。你也可以配置成 gpt-3.5-turbo-16k、gpt-4 或 gpt-4-32k。
- `gptPrompt`:默认提示是 "Act as a Code Review Helper, please review this patch set:"。你可以修改成自己喜欢的 prompt。
-- `gptMaxTokens`:默认值是 4096 tokens。这决定了 ChatGPT 最大对话长度。[点击这里](https://platform.openai.com/tokenizer)
- 检查内容 token 数量。
-- `gptTemperature`: 默认值为 1。范围在0到2之间。较高的值如 0.8 会使输出结果更具随机性,而较低的值如 0.2 则会让输出更加集中和确定性强。
-- `globalEnable`: 默认值为true。如果设为false,插件将只会审查在enabledRepos参数中指定的仓库。
-- `enabledRepos`:
- 默认值为空字符串。如果globalEnable被设为false,插件将仅在这里指定的仓库中运行。值应为仓库名称的逗号分隔列表,例如:"
- repo1,repo2,repo3"。
-- `patchSetReduction`:默认值是 false。如果设置为 true,插件会尝试压缩patch内容,包括但不限于多余的空行、制表符、import语句等,以便减少
- token 数量等。
+- `gptTemperature`: 默认值为 1。范围在 0 到 2 之间。较高的值如 1.8 会使输出结果更具随机性,而较低的值如 0.2 则会让输出更加集中和确定性强。
+- `patchSetReduction`:默认值是 false。如果设置为 true,插件会尝试压缩 patch 内容,包括但不限于多余的空行、制表符、import
+ 语句等,以便减少 token 数量等。
- `maxReviewLines`:默认值是 1000。这设置了审查中包含的代码行数限制。
+- `enabledProjects(仅用于全局配置)`:
+ 默认值为空字符串。如果 globalEnable 被设为 false,插件将仅在这里指定的仓库中运行。值应为仓库名称的逗号分隔列表,例如:"
+ project1,project2,project3"。
+- `isEnabled(仅用于项目配置)`: 默认值为 false。如果设为 true,插件将会 review 这个项目的 patchSet。
## 测试
diff --git a/pom.xml b/pom.xml
index 0ec18f6..6fb790c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
<groupId>com.googlesource.gerrit.plugins</groupId>
<artifactId>chatgpt-code-review-gerrit-plugin</artifactId>
<packaging>jar</packaging>
- <version>1.0.0-SNAPSHOT</version>
+ <version>1.0.0</version>
<properties>
<Gerrit-ApiType>plugin</Gerrit-ApiType>
<Gerrit-ApiVersion>3.3.0</Gerrit-ApiVersion>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/ConfigCreator.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/ConfigCreator.java
new file mode 100644
index 0000000..e3877b5
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/ConfigCreator.java
@@ -0,0 +1,34 @@
+package com.googlesource.gerrit.plugins.chatgpt;
+
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import lombok.extern.slf4j.Slf4j;
+
+@Singleton
+@Slf4j
+public class ConfigCreator {
+
+ private final String pluginName;
+
+ private final PluginConfigFactory configFactory;
+
+ @Inject
+ ConfigCreator(@PluginName String pluginName, PluginConfigFactory configFactory) {
+ this.pluginName = pluginName;
+ this.configFactory = configFactory;
+ }
+
+ public Configuration createConfig(Project.NameKey projectName)
+ throws NoSuchProjectException {
+ PluginConfig globalConfig = configFactory.getFromGerritConfig(pluginName);
+ log.info("The names in global config: {}", globalConfig.getNames());
+ PluginConfig projectConfig = configFactory.getFromProjectConfig(projectName, pluginName);
+ log.info("The names in project config: {}", projectConfig.getNames());
+ return new Configuration(globalConfig, projectConfig);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/Configuration.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/Configuration.java
index 2fa0bc7..f99f743 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/Configuration.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/Configuration.java
@@ -1,65 +1,129 @@
package com.googlesource.gerrit.plugins.chatgpt;
-import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.server.config.PluginConfig;
-import com.google.gerrit.server.config.PluginConfigFactory;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import lombok.Getter;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
-@Singleton
-@Getter
+@Slf4j
+@AllArgsConstructor
public class Configuration {
public static final String OPENAI_DOMAIN = "https://api.openai.com";
public static final String DEFAULT_GPT_MODEL = "gpt-3.5-turbo";
public static final String DEFAULT_GPT_PROMPT = "Act as a Code Review Helper, please review this patch set: ";
public static final String NOT_CONFIGURED_ERROR_MSG = "%s is not configured";
+ private static final String DEFAULT_GPT_TEMPERATURE = "1";
+ private static final boolean DEFAULT_GLOBAL_ENABLE = false;
+ private static final String DEFAULT_ENABLED_PROJECTS = "";
+ private static final boolean DEFAULT_PATCH_SET_REDUCTION = false;
+ private static final boolean DEFAULT_PROJECT_ENABLE = false;
+ private static final int DEFAULT_MAX_REVIEW_LINES = 1000;
- private final PluginConfig cfg;
+ private static final String KEY_GPT_TOKEN = "gptToken";
+ private static final String KEY_GERRIT_AUTH_BASE_URL = "gerritAuthBaseUrl";
+ private static final String KEY_GERRIT_USERNAME = "gerritUserName";
+ private static final String KEY_GERRIT_PASSWORD = "gerritPassword";
+ private static final String KEY_GPT_DOMAIN = "gptDomain";
+ private static final String KEY_GPT_MODEL = "gptModel";
+ private static final String KEY_GPT_PROMPT = "gptPrompt";
+ private static final String KEY_GPT_TEMPERATURE = "gptTemperature";
+ private static final String KEY_PROJECT_ENABLE = "isEnabled";
+ private static final String KEY_GLOBAL_ENABLE = "globalEnable";
+ private static final String KEY_ENABLED_PROJECTS = "enabledProjects";
+ private static final String KEY_PATCH_SET_REDUCTION = "patchSetReduction";
+ private static final String KEY_MAX_REVIEW_LINES = "maxReviewLines";
- private final String gptDomain;
- private final String gptToken;
- private final String gptModel;
- private final String gptPrompt;
- private final int gptMaxTokens;
- private final double gptTemperature;
+ private PluginConfig globalConfig;
- private final String gerritAuthBaseUrl;
- private final String gerritUserName;
- private final String gerritPassword;
+ private PluginConfig projectConfig;
- private final boolean globalEnable;
- private final String enabledRepos;
- private final boolean patchSetReduction;
- private final int maxReviewLines;
+ public String getGptToken() {
+ return getValidatedOrThrow(KEY_GPT_TOKEN);
+ }
- @Inject
- Configuration(PluginConfigFactory cfgFactory, @PluginName String pluginName) {
- cfg = cfgFactory.getFromGerritConfig(pluginName);
- gptDomain = cfg.getString("gptUrl", OPENAI_DOMAIN);
- gptToken = getValidatedOrThrow("gptToken");
- gptModel = cfg.getString("gptModel", DEFAULT_GPT_MODEL);
- gptPrompt = cfg.getString("gptPrompt", DEFAULT_GPT_PROMPT);
- gptMaxTokens = cfg.getInt("gptMaxTokens", 4096);
+ public String getGerritAuthBaseUrl() {
+ return getValidatedOrThrow(KEY_GERRIT_AUTH_BASE_URL);
+ }
- gptTemperature = Double.parseDouble(cfg.getString("gptTemperature", "1"));
- gerritAuthBaseUrl = getValidatedOrThrow("gerritAuthBaseUrl");
- gerritUserName = getValidatedOrThrow("gerritUserName");
- gerritPassword = getValidatedOrThrow("gerritPassword");
+ public String getGerritUserName() {
+ return getValidatedOrThrow(KEY_GERRIT_USERNAME);
+ }
- globalEnable = cfg.getBoolean("globalEnable", true);
- enabledRepos = cfg.getString("enabledRepos", "");
- patchSetReduction = cfg.getBoolean("patchSetReduction", false);
- maxReviewLines = cfg.getInt("maxReviewLines", 1000);
+ public String getGerritPassword() {
+ return getValidatedOrThrow(KEY_GERRIT_PASSWORD);
+ }
+
+ public String getGptDomain() {
+ return getString(KEY_GPT_DOMAIN, OPENAI_DOMAIN);
+ }
+
+ public String getGptModel() {
+ return getString(KEY_GPT_MODEL, DEFAULT_GPT_MODEL);
+ }
+
+ public String getGptPrompt() {
+ return getString(KEY_GPT_PROMPT, DEFAULT_GPT_PROMPT);
+ }
+
+ public double getGptTemperature() {
+ return Double.parseDouble(getString(KEY_GPT_TEMPERATURE, DEFAULT_GPT_TEMPERATURE));
+ }
+
+ public boolean isProjectEnable() {
+ return projectConfig.getBoolean(KEY_PROJECT_ENABLE, DEFAULT_PROJECT_ENABLE);
+ }
+
+ public boolean isGlobalEnable() {
+ return globalConfig.getBoolean(KEY_GLOBAL_ENABLE, DEFAULT_GLOBAL_ENABLE);
+ }
+
+ public String getEnabledProjects() {
+ return globalConfig.getString(KEY_ENABLED_PROJECTS, DEFAULT_ENABLED_PROJECTS);
+ }
+
+ public boolean isPatchSetReduction() {
+ return getBoolean(KEY_PATCH_SET_REDUCTION, DEFAULT_PATCH_SET_REDUCTION);
+ }
+
+ public int getMaxReviewLines() {
+ return getInt(KEY_MAX_REVIEW_LINES, DEFAULT_MAX_REVIEW_LINES);
}
private String getValidatedOrThrow(String key) {
- String value = cfg.getString(key);
+ String value = projectConfig.getString(key);
+ if (value == null) {
+ value = globalConfig.getString(key);
+ }
if (value == null) {
throw new RuntimeException(String.format(NOT_CONFIGURED_ERROR_MSG, key));
}
return value;
}
+
+ private String getString(String key, String defaultValue) {
+ String value = projectConfig.getString(key);
+ if (value != null) {
+ return value;
+ }
+ return globalConfig.getString(key, defaultValue);
+ }
+
+ private int getInt(String key, int defaultValue) {
+ int valueForProject = projectConfig.getInt(key, defaultValue);
+ if (valueForProject != defaultValue) {
+ return valueForProject;
+ }
+ return globalConfig.getInt(key, defaultValue);
+ }
+
+ private boolean getBoolean(String key, boolean defaultValue) {
+ boolean valueForProject = projectConfig.getBoolean(key, defaultValue);
+ if (projectConfig.getString(key) != null) {
+ return valueForProject;
+ }
+ return globalConfig.getBoolean(key, defaultValue);
+ }
+
+
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetCreated.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetCreated.java
index a0ec3e7..5732bf1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetCreated.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetCreated.java
@@ -1,6 +1,7 @@
package com.googlesource.gerrit.plugins.chatgpt;
import com.google.common.base.Splitter;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.server.events.Event;
import com.google.gerrit.server.events.EventListener;
import com.google.gerrit.server.events.PatchSetCreatedEvent;
@@ -12,49 +13,53 @@
@Slf4j
public class PatchSetCreated implements EventListener {
-
- private final Configuration configuration;
-
+ private final ConfigCreator configCreator;
private final PatchSetReviewer reviewer;
-
- private final List<String> enabledRepos;
+ private CompletableFuture<Void> latestFuture;
@Inject
- public PatchSetCreated(Configuration configuration, PatchSetReviewer reviewer) {
- this.configuration = configuration;
+ PatchSetCreated(ConfigCreator configCreator, PatchSetReviewer reviewer) {
+ this.configCreator = configCreator;
this.reviewer = reviewer;
- this.enabledRepos = Splitter.on(",").omitEmptyStrings().splitToList(configuration.getEnabledRepos());
+ }
+
+ public static String buildFullChangeId(String projectName, String branchName, String changeKey) {
+ return String.join("~", projectName, branchName, changeKey);
}
@Override
public void onEvent(Event event) {
- if (!(event instanceof PatchSetCreatedEvent)) {
- log.debug("The event is not a PatchSetCreatedEvent, it is: {}", event);
- return;
- }
-
- log.info("Processing event: {}", event);
- log.debug("The configuration is: {}", configuration);
-
- PatchSetCreatedEvent createdPatchSetEvent = (PatchSetCreatedEvent) event;
- String projectName = createdPatchSetEvent.getProjectNameKey().get();
-
-
- if (!configuration.isGlobalEnable() && !enabledRepos.contains(projectName)) {
- log.info("The project {} is not enabled for review", projectName);
- return;
- }
-
- String branchName = createdPatchSetEvent.getBranchNameKey().shortName();
- String changeKey = createdPatchSetEvent.getChangeKey().get();
-
- log.info("Processing patch set for project: {}, branch: {}, change key: {}", projectName, branchName, changeKey);
-
- String reviewId = String.join("~", projectName, branchName, changeKey);
// Execute the potentially time-consuming operation asynchronously
- CompletableFuture.runAsync(() -> {
+ latestFuture = CompletableFuture.runAsync(() -> {
+ if (!(event instanceof PatchSetCreatedEvent)) {
+ log.debug("The event is not a PatchSetCreatedEvent, it is: {}", event);
+ return;
+ }
+
+ log.info("Processing event: {}", event);
+ PatchSetCreatedEvent createdPatchSetEvent = (PatchSetCreatedEvent) event;
+ Project.NameKey projectNameKey = createdPatchSetEvent.getProjectNameKey();
+ String projectName = projectNameKey.get();
+ String branchName = createdPatchSetEvent.getBranchNameKey().shortName();
+ String changeKey = createdPatchSetEvent.getChangeKey().get();
+
+ log.info("Processing patch set for project: {}, branch: {}, change key: {}", projectName, branchName, changeKey);
+
try {
- reviewer.review(reviewId);
+ Configuration config = configCreator.createConfig(projectNameKey);
+ List<String> enabledProjects = Splitter.on(",").omitEmptyStrings()
+ .splitToList(config.getEnabledProjects());
+ if (!config.isGlobalEnable() &&
+ !enabledProjects.contains(projectName) &&
+ !config.isProjectEnable()) {
+ log.info("The project {} is not enabled for review", projectName);
+ return;
+ }
+
+ String fullChangeId = buildFullChangeId(projectName, branchName, changeKey);
+
+ reviewer.review(config, fullChangeId);
+ log.info("Review completed for project: {}, branch: {}, change key: {}", projectName, branchName, changeKey);
} catch (Exception e) {
log.error("Failed to submit review for project: {}, branch: {}, change key: {}", projectName, branchName, changeKey, e);
if (e instanceof InterruptedException) {
@@ -64,4 +69,8 @@
});
}
+ public CompletableFuture<Void> getLatestFuture() {
+ return latestFuture;
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java
index e3a5057..2dc7ed2 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java
@@ -1,5 +1,6 @@
package com.googlesource.gerrit.plugins.chatgpt;
+import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.googlesource.gerrit.plugins.chatgpt.client.GerritClient;
@@ -13,41 +14,30 @@
@Slf4j
@Singleton
public class PatchSetReviewer {
-
private static final String SPLIT_REVIEW_MSG = "Too many changes. Please consider splitting into patches smaller than %s lines for review.";
-
private static final int COMMENT_BATCH_SIZE = 25;
-
- private final Configuration configuration;
private final GerritClient gerritClient;
private final OpenAiClient openAiClient;
@Inject
- public PatchSetReviewer(Configuration configuration, GerritClient gerritClient, OpenAiClient openAiClient) {
- this.configuration = configuration;
+ PatchSetReviewer(GerritClient gerritClient, OpenAiClient openAiClient) {
this.gerritClient = gerritClient;
this.openAiClient = openAiClient;
}
- public void review(String changeId) throws IOException, InterruptedException {
- log.info("Starting to review patch set: changeId={}", changeId);
-
- String patchSet = gerritClient.getPatchSet(changeId);
- if (configuration.isPatchSetReduction()) {
+ public void review(Configuration config, String fullChangeId) throws IOException, InterruptedException, NoSuchProjectException {
+ String patchSet = gerritClient.getPatchSet(config, fullChangeId);
+ if (config.isPatchSetReduction()) {
patchSet = reducePatchSet(patchSet);
log.debug("Reduced patch set: {}", patchSet);
}
- String reviewSuggestion = getReviewSuggestion(changeId, patchSet);
+ String reviewSuggestion = getReviewSuggestion(config, fullChangeId, patchSet);
List<String> reviewBatches = splitReviewIntoBatches(reviewSuggestion);
for (String reviewBatch : reviewBatches) {
- gerritClient.postComment(changeId, reviewBatch);
- log.debug("Posted review batch: {}", reviewBatch);
+ gerritClient.postComment(config, fullChangeId, reviewBatch);
}
-
- log.info("Finished reviewing patch set: changeId={}", changeId);
-
}
private List<String> splitReviewIntoBatches(String review) {
@@ -82,19 +72,14 @@
.collect(Collectors.joining("\n"));
}
- private String getReviewSuggestion(String changeId, String patchSet) throws IOException, InterruptedException {
- log.info("Starting review for changeId: {}", changeId);
-
+ private String getReviewSuggestion(Configuration config, String changeId, String patchSet)
+ throws IOException, InterruptedException {
List<String> patchLines = Arrays.asList(patchSet.split("\n"));
- if (patchLines.size() > configuration.getMaxReviewLines()) {
+ if (patchLines.size() > config.getMaxReviewLines()) {
log.warn("Patch set too large. Skipping review. changeId: {}", changeId);
- return String.format(SPLIT_REVIEW_MSG, configuration.getMaxReviewLines());
+ return String.format(SPLIT_REVIEW_MSG, config.getMaxReviewLines());
}
-
- String reviewSuggestion = openAiClient.ask(patchSet);
- log.info("Review completed for changeId: {}", changeId);
- return reviewSuggestion;
-
+ return openAiClient.ask(config, patchSet);
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClient.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClient.java
index 43bdfa2..df0cecf 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClient.java
@@ -2,7 +2,6 @@
import com.google.common.net.HttpHeaders;
import com.google.gson.Gson;
-import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.googlesource.gerrit.plugins.chatgpt.Configuration;
import lombok.extern.slf4j.Slf4j;
@@ -31,15 +30,12 @@
.connectTimeout(Duration.ofMinutes(5))
.build();
- @Inject
- private Configuration configuration;
-
- public String getPatchSet(String changeId) throws IOException, InterruptedException {
+ public String getPatchSet(Configuration config, String fullChangeId) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder()
- .header(HttpHeaders.AUTHORIZATION, generateBasicAuth(getConfiguration().getGerritUserName(),
- getConfiguration().getGerritPassword()))
- .uri(URI.create(getConfiguration().getGerritAuthBaseUrl()
- + UriResourceLocator.gerritPatchSetUri(changeId)))
+ .header(HttpHeaders.AUTHORIZATION, generateBasicAuth(config.getGerritUserName(),
+ config.getGerritPassword()))
+ .uri(URI.create(config.getGerritAuthBaseUrl()
+ + UriResourceLocator.gerritPatchSetUri(fullChangeId)))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
@@ -59,17 +55,17 @@
return "Basic " + Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
}
- public void postComment(String changeId, String message) throws IOException, InterruptedException {
+ public void postComment(Configuration config, String fullChangeId, String message) throws IOException, InterruptedException {
Map<String, String> map = new HashMap<>();
map.put("message", message);
String json = gson.toJson(map);
HttpRequest request = HttpRequest.newBuilder()
- .header(HttpHeaders.AUTHORIZATION, generateBasicAuth(getConfiguration().getGerritUserName(),
- getConfiguration().getGerritPassword()))
+ .header(HttpHeaders.AUTHORIZATION, generateBasicAuth(config.getGerritUserName(),
+ config.getGerritPassword()))
.header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
- .uri(URI.create(getConfiguration().getGerritAuthBaseUrl()
- + UriResourceLocator.gerritCommentUri(changeId)))
+ .uri(URI.create(config.getGerritAuthBaseUrl()
+ + UriResourceLocator.gerritCommentUri(fullChangeId)))
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
@@ -80,8 +76,4 @@
}
}
- public Configuration getConfiguration() {
- return this.configuration;
- }
-
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/OpenAiClient.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/OpenAiClient.java
index ea3cd2f..a591ba4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/OpenAiClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/OpenAiClient.java
@@ -2,7 +2,6 @@
import com.google.common.net.HttpHeaders;
import com.google.gson.Gson;
-import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.googlesource.gerrit.plugins.chatgpt.Configuration;
import lombok.extern.slf4j.Slf4j;
@@ -30,11 +29,8 @@
.connectTimeout(Duration.ofMinutes(5))
.build();
- @Inject
- private Configuration configuration;
-
- public String ask(String patchSet) throws IOException, InterruptedException {
- HttpRequest request = createRequest(patchSet);
+ public String ask(Configuration config, String patchSet) throws IOException, InterruptedException {
+ HttpRequest request = createRequest(config, patchSet);
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != HTTP_OK) {
@@ -56,25 +52,21 @@
return finalContent.toString();
}
- private HttpRequest createRequest(String patchSet) {
- String domain = getConfiguration().getGptDomain();
- String model = getConfiguration().getGptModel();
- String prompt = getConfiguration().getGptPrompt();
-
- String jsonRequest = createJsonRequest(model, prompt, patchSet);
+ private HttpRequest createRequest(Configuration config, String patchSet) {
+ String jsonRequest = createRequestBody(config, patchSet);
return HttpRequest.newBuilder()
- .header(HttpHeaders.AUTHORIZATION, "Bearer " + getConfiguration().getGptToken())
+ .header(HttpHeaders.AUTHORIZATION, "Bearer " + config.getGptToken())
.header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
- .uri(URI.create(URI.create(domain) + UriResourceLocator.chatCompletionsUri()))
+ .uri(URI.create(URI.create(config.getGptDomain()) + UriResourceLocator.chatCompletionsUri()))
.POST(HttpRequest.BodyPublishers.ofString(jsonRequest))
.build();
}
- private String createJsonRequest(String model, String prompt, String patchSet) {
+ private String createRequestBody(Configuration config, String patchSet) {
ChatCompletionRequest.Message systemMessage = ChatCompletionRequest.Message.builder()
.role("system")
- .content(prompt)
+ .content(config.getGptPrompt())
.build();
ChatCompletionRequest.Message userMessage = ChatCompletionRequest.Message.builder()
.role("user")
@@ -84,9 +76,9 @@
List<ChatCompletionRequest.Message> messages = List.of(systemMessage, userMessage);
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
- .model(model)
+ .model(config.getGptModel())
.messages(messages)
- .temperature(getConfiguration().getGptTemperature())
+ .temperature(config.getGptTemperature())
.stream(true)
.build();
@@ -105,8 +97,4 @@
return Optional.ofNullable(content);
}
- public Configuration getConfiguration() {
- return this.configuration;
- }
-
}
\ No newline at end of file
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/UriResourceLocator.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/UriResourceLocator.java
index 31cced9..f194792 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/UriResourceLocator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/UriResourceLocator.java
@@ -6,12 +6,12 @@
throw new IllegalStateException("Utility class");
}
- public static String gerritPatchSetUri(String changeId) {
- return "/changes/" + changeId + "/revisions/current/patch";
+ public static String gerritPatchSetUri(String fullChangeId) {
+ return "/changes/" + fullChangeId + "/revisions/current/patch";
}
- public static String gerritCommentUri(String changeId) {
- return "/changes/" + changeId + "/revisions/current/review";
+ public static String gerritCommentUri(String fullChangeId) {
+ return "/changes/" + fullChangeId + "/revisions/current/review";
}
public static String chatCompletionsUri() {
diff --git a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewerTests.java b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewerTests.java
index 5a1d2ca..3f34a1e 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewerTests.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewerTests.java
@@ -1,8 +1,14 @@
package com.googlesource.gerrit.plugins.chatgpt;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder;
import com.github.tomakehurst.wiremock.verification.LoggedRequest;
import com.google.common.net.HttpHeaders;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.gerrit.server.project.NoSuchProjectException;
import com.googlesource.gerrit.plugins.chatgpt.client.GerritClient;
import com.googlesource.gerrit.plugins.chatgpt.client.OpenAiClient;
import com.googlesource.gerrit.plugins.chatgpt.client.UriResourceLocator;
@@ -12,73 +18,57 @@
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
-import java.io.IOException;
import java.net.URI;
import java.util.Base64;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static com.googlesource.gerrit.plugins.chatgpt.PatchSetCreated.buildFullChangeId;
+import static com.googlesource.gerrit.plugins.chatgpt.client.UriResourceLocator.gerritCommentUri;
+import static com.googlesource.gerrit.plugins.chatgpt.client.UriResourceLocator.gerritPatchSetUri;
import static java.net.HttpURLConnection.HTTP_OK;
import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@Slf4j
@RunWith(MockitoJUnitRunner.class)
public class PatchSetReviewerTests {
-
+ private static final String PROJECT_NAME = "myProject";
private static final String CHANGE_ID = "myChangeId";
+ private static final String BRANCH_NAME = "myBranchName";
@Rule
public WireMockRule wireMockRule = new WireMockRule(9527);
- private Configuration configuration;
-
- private GerritClient gerritClient;
-
- private OpenAiClient openAiClient;
-
- private PatchSetReviewer patchSetReviewer;
-
- private PatchSetCreated patchSetCreated;
+ private Configuration config;
@Before
public void before() {
- initializePatchSetReviewer();
+ initConfig();
setupMockRequests();
}
- private void initializePatchSetReviewer() {
- configuration = Mockito.mock(Configuration.class);
- when(configuration.getGerritAuthBaseUrl()).thenReturn("http://localhost:9527");
- when(configuration.getGptDomain()).thenReturn("http://localhost:9527");
- when(configuration.getGptTemperature()).thenReturn(1.0);
- when(configuration.getMaxReviewLines()).thenReturn(500);
- when(configuration.getEnabledRepos()).thenReturn("");
+ private void initConfig() {
+ config = Mockito.mock(Configuration.class);
+ when(config.getGerritAuthBaseUrl()).thenReturn("http://localhost:9527");
+ when(config.getGptDomain()).thenReturn("http://localhost:9527");
+ when(config.getGptTemperature()).thenReturn(1.0);
+ when(config.getMaxReviewLines()).thenReturn(500);
+ when(config.getEnabledProjects()).thenReturn("");
+ when(config.isProjectEnable()).thenReturn(true);
- gerritClient = Mockito.spy(new GerritClient() {
- @Override
- public Configuration getConfiguration() {
- return configuration;
- }
- });
-
- openAiClient = Mockito.spy(new OpenAiClient() {
- @Override
- public Configuration getConfiguration() {
- return configuration;
- }
- });
-
- patchSetReviewer = new PatchSetReviewer(configuration, gerritClient, openAiClient);
- patchSetCreated = new PatchSetCreated(configuration, patchSetReviewer);
}
private void setupMockRequests() {
// Mocks the behavior of the getPatchSet request
- stubFor(get(urlEqualTo(URI.create(configuration.getGerritAuthBaseUrl() +
- UriResourceLocator.gerritPatchSetUri(CHANGE_ID)).getPath()))
+ stubFor(get(gerritPatchSetUri(buildFullChangeId(PROJECT_NAME, BRANCH_NAME, CHANGE_ID)))
.willReturn(aResponse()
.withStatus(HTTP_OK)
.withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
@@ -95,7 +85,7 @@
"2JqZWN0IjoiY2hhdC5jb21wbGV0aW9uLmNodW5rIiwiY3JlYXRlZCI6MTY4NjgxOTQ1NywibW9kZWwiOiJncHQtMy41LXR1cm" +
"JvLTAzMDEiLCJjaG9pY2VzIjpbeyJkZWx0YSI6eyJjb250ZW50IjoiISJ9LCJpbmRleCI6MCwiZmluaXNoX3JlYXNvbiI" +
"6bnVsbH1dfQ==");
- stubFor(post(urlEqualTo(URI.create(configuration.getGptDomain()
+ stubFor(post(urlEqualTo(URI.create(config.getGptDomain()
+ UriResourceLocator.chatCompletionsUri()).getPath()))
.willReturn(aResponse()
.withStatus(HTTP_OK)
@@ -103,19 +93,34 @@
.withBody(new String(gptAnswer))));
// Mocks the behavior of the postReview request
- stubFor(post(urlEqualTo(URI.create(configuration.getGerritAuthBaseUrl()
- + UriResourceLocator.gerritCommentUri(CHANGE_ID)).getPath()))
+ stubFor(post(gerritCommentUri(buildFullChangeId(PROJECT_NAME, BRANCH_NAME, CHANGE_ID)))
.willReturn(aResponse()
.withStatus(HTTP_OK)));
}
@Test
- public void review() throws IOException, InterruptedException {
- patchSetReviewer.review(CHANGE_ID);
+ public void review() throws InterruptedException, NoSuchProjectException, ExecutionException {
+ GerritClient gerritClient = new GerritClient();
+ OpenAiClient openAiClient = new OpenAiClient();
+ PatchSetReviewer patchSetReviewer = new PatchSetReviewer(gerritClient, openAiClient);
+ ConfigCreator mockConfigCreator = mock(ConfigCreator.class);
+ when(mockConfigCreator.createConfig(ArgumentMatchers.any())).thenReturn(config);
- LoggedRequest firstMatchingRequest = findAll(postRequestedFor(urlEqualTo(UriResourceLocator
- .gerritCommentUri(CHANGE_ID)))).get(0);
- String requestBody = firstMatchingRequest.getBodyAsString();
+ PatchSetCreatedEvent event = mock(PatchSetCreatedEvent.class);
+ when(event.getProjectNameKey()).thenReturn(Project.NameKey.parse(PROJECT_NAME));
+ when(event.getBranchNameKey()).thenReturn(BranchNameKey.create(Project.NameKey.parse(PROJECT_NAME), BRANCH_NAME));
+ when(event.getChangeKey()).thenReturn(Change.Key.parse(CHANGE_ID));
+
+ PatchSetCreated patchSetCreated = new PatchSetCreated(mockConfigCreator, patchSetReviewer);
+ patchSetCreated.onEvent(event);
+ CompletableFuture<Void> future = patchSetCreated.getLatestFuture();
+ future.get();
+
+ RequestPatternBuilder requestPatternBuilder = postRequestedFor(
+ urlEqualTo(gerritCommentUri(buildFullChangeId(PROJECT_NAME, BRANCH_NAME, CHANGE_ID))));
+ List<LoggedRequest> loggedRequests = findAll(requestPatternBuilder);
+ assertEquals(1, loggedRequests.size());
+ String requestBody = loggedRequests.get(0).getBodyAsString();
assertEquals("{\"message\":\"Hello!\\n\"}", requestBody);
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/integration/CodeReviewPluginIT.java b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/integration/CodeReviewPluginIT.java
index 9823de9..1b2c204 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/chatgpt/integration/CodeReviewPluginIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/chatgpt/integration/CodeReviewPluginIT.java
@@ -4,6 +4,7 @@
import com.googlesource.gerrit.plugins.chatgpt.client.GerritClient;
import com.googlesource.gerrit.plugins.chatgpt.client.OpenAiClient;
import lombok.extern.slf4j.Slf4j;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
@@ -12,15 +13,16 @@
import java.io.IOException;
-import static org.junit.Assert.assertNotNull;
+import static junit.framework.TestCase.assertNotNull;
import static org.mockito.Mockito.when;
-//@Ignore("This test suite is designed for integration testing and is not intended to be executed during the regular build process")
+@Ignore("This test suite is designed to demonstrate how to test the Gerrit and GPT interfaces in a real environment. " +
+ "It is not intended to be executed during the regular build process")
@Slf4j
@RunWith(MockitoJUnitRunner.class)
public class CodeReviewPluginIT {
@Mock
- private Configuration configuration;
+ private Configuration config;
@InjectMocks
private GerritClient gerritClient;
@@ -30,33 +32,33 @@
@Test
public void sayHelloToGPT() throws IOException, InterruptedException {
- when(configuration.getGptDomain()).thenReturn(Configuration.OPENAI_DOMAIN);
- when(configuration.getGptToken()).thenReturn("Your GPT token");
- when(configuration.getGptModel()).thenReturn(Configuration.DEFAULT_GPT_MODEL);
- when(configuration.getGptPrompt()).thenReturn(Configuration.DEFAULT_GPT_PROMPT);
+ when(config.getGptDomain()).thenReturn(Configuration.OPENAI_DOMAIN);
+ when(config.getGptToken()).thenReturn("Your GPT token");
+ when(config.getGptModel()).thenReturn(Configuration.DEFAULT_GPT_MODEL);
+ when(config.getGptPrompt()).thenReturn(Configuration.DEFAULT_GPT_PROMPT);
- String answer = openAiClient.ask("hello");
+ String answer = openAiClient.ask(config, "hello");
log.info("answer: {}", answer);
assertNotNull(answer);
}
@Test
public void getPatchSet() throws IOException, InterruptedException {
- when(configuration.getGerritAuthBaseUrl()).thenReturn("Your Gerrit URL");
- when(configuration.getGerritUserName()).thenReturn("Your Gerrit username");
- when(configuration.getGerritPassword()).thenReturn("Your Gerrit password");
+ when(config.getGerritAuthBaseUrl()).thenReturn("Your Gerrit URL");
+ when(config.getGerritUserName()).thenReturn("Your Gerrit username");
+ when(config.getGerritPassword()).thenReturn("Your Gerrit password");
- String patchSet = gerritClient.getPatchSet("${changeId}");
+ String patchSet = gerritClient.getPatchSet(config, "${changeId}");
log.info("patchSet: {}", patchSet);
assertNotNull(patchSet);
}
@Test
public void postComment() throws IOException, InterruptedException {
- when(configuration.getGerritAuthBaseUrl()).thenReturn("Your Gerrit URL");
- when(configuration.getGerritUserName()).thenReturn("Your Gerrit username");
- when(configuration.getGerritPassword()).thenReturn("Your Gerrit password");
+ when(config.getGerritAuthBaseUrl()).thenReturn("Your Gerrit URL");
+ when(config.getGerritUserName()).thenReturn("Your Gerrit username");
+ when(config.getGerritPassword()).thenReturn("Your Gerrit password");
- gerritClient.postComment("Your changeId", "message");
+ gerritClient.postComment(config, "Your changeId", "message");
}
}