Add PatchSetCreatedEvent listener and finalize key features
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/Configuration.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/Configuration.java
new file mode 100644
index 0000000..57ec722
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/Configuration.java
@@ -0,0 +1,57 @@
+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;
+
+@Singleton
+@Getter
+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 final PluginConfig cfg;
+
+ private final String gptDomain;
+ private final String gptToken;
+ private final String gptModel;
+ private final String gptPrompt;
+ private final int gptMaxTokens;
+
+ private final String gerritAuthBaseUrl;
+ private final String gerritUserName;
+ private final String gerritPassword;
+
+ private final boolean patchSetReduction;
+ private final int maxReviewLines;
+
+ @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);
+ gerritAuthBaseUrl = getValidatedOrThrow("gerritAuthBaseUrl");
+ gerritUserName = getValidatedOrThrow("gerritUserName");
+ gerritPassword = getValidatedOrThrow("gerritPassword");
+ patchSetReduction = cfg.getBoolean("patchSetReduction", false);
+ maxReviewLines = cfg.getInt("maxReviewLines", 1000);
+ }
+
+ private String getValidatedOrThrow(String key) {
+ String value = cfg.getString(key);
+ if (value == null) {
+ throw new RuntimeException(String.format(NOT_CONFIGURED_ERROR_MSG, key));
+ }
+ return value;
+ }
+}
+
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/Module.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/Module.java
new file mode 100644
index 0000000..28725cb
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/Module.java
@@ -0,0 +1,13 @@
+package com.googlesource.gerrit.plugins.chatgpt;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.events.EventListener;
+import com.google.inject.AbstractModule;
+
+public class Module extends AbstractModule {
+
+ @Override
+ protected void configure() {
+ DynamicSet.bind(binder(), EventListener.class).to(PatchSetCreated.class);
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetCreated.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetCreated.java
new file mode 100644
index 0000000..3feeecf
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetCreated.java
@@ -0,0 +1,52 @@
+package com.googlesource.gerrit.plugins.chatgpt;
+
+import com.google.gerrit.server.events.Event;
+import com.google.gerrit.server.events.EventListener;
+import com.google.gerrit.server.events.PatchSetCreatedEvent;
+import com.google.inject.Inject;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.util.concurrent.CompletableFuture;
+
+@Slf4j
+public class PatchSetCreated implements EventListener {
+
+ private final PatchSetReviewer reviewer;
+
+ @Inject
+ public PatchSetCreated(PatchSetReviewer reviewer) {
+ this.reviewer = reviewer;
+ }
+
+ @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);
+
+ PatchSetCreatedEvent createdPatchSetEvent = (PatchSetCreatedEvent) event;
+ String projectName = createdPatchSetEvent.getProjectNameKey().get();
+ 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(() -> {
+ try {
+ reviewer.review(reviewId);
+ } catch (IOException | InterruptedException e) {
+ Thread.currentThread().interrupt();
+ log.error("Failed to submit review for project: {}, branch: {}, change key: {}", projectName, branchName, changeKey, e);
+ } catch (Throwable e) {
+ log.error("Failed to submit review for project: {}, branch: {}, change key: {}", projectName, branchName, changeKey, e);
+ }
+ });
+ }
+
+}
\ 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
new file mode 100644
index 0000000..e3a5057
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java
@@ -0,0 +1,100 @@
+package com.googlesource.gerrit.plugins.chatgpt;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.chatgpt.client.GerritClient;
+import com.googlesource.gerrit.plugins.chatgpt.client.OpenAiClient;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@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;
+ 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()) {
+ patchSet = reducePatchSet(patchSet);
+ log.debug("Reduced patch set: {}", patchSet);
+ }
+
+ String reviewSuggestion = getReviewSuggestion(changeId, patchSet);
+ List<String> reviewBatches = splitReviewIntoBatches(reviewSuggestion);
+
+ for (String reviewBatch : reviewBatches) {
+ gerritClient.postComment(changeId, reviewBatch);
+ log.debug("Posted review batch: {}", reviewBatch);
+ }
+
+ log.info("Finished reviewing patch set: changeId={}", changeId);
+
+ }
+
+ private List<String> splitReviewIntoBatches(String review) {
+ List<String> batches = new ArrayList<>();
+ String[] lines = review.split("\n");
+
+ StringBuilder batch = new StringBuilder();
+ for (int i = 0; i < lines.length; i++) {
+ batch.append(lines[i]).append("\n");
+ if ((i + 1) % COMMENT_BATCH_SIZE == 0) {
+ batches.add(batch.toString());
+ batch = new StringBuilder();
+ }
+ }
+ if (batch.length() > 0) {
+ batches.add(batch.toString());
+ }
+ log.info("Review batches created: {}", batches.size());
+ return batches;
+ }
+
+ private String reducePatchSet(String patchSet) {
+ Set<String> skipPrefixes = new HashSet<>(Arrays.asList(
+ "import", "-", "+package", "+import", "From", "Date:", "Subject:",
+ "Change-Id:", "diff --git", "index", "---", "+++", "@@", "Binary files differ"
+ ));
+
+ return Arrays.stream(patchSet.split("\n"))
+ .map(line -> line.replace("\t", "").replace(" ", ""))
+ .filter(line -> skipPrefixes.stream().noneMatch(line::startsWith))
+ .filter(line -> !line.trim().isEmpty())
+ .collect(Collectors.joining("\n"));
+ }
+
+ private String getReviewSuggestion(String changeId, String patchSet) throws IOException, InterruptedException {
+ log.info("Starting review for changeId: {}", changeId);
+
+ List<String> patchLines = Arrays.asList(patchSet.split("\n"));
+ if (patchLines.size() > configuration.getMaxReviewLines()) {
+ log.warn("Patch set too large. Skipping review. changeId: {}", changeId);
+ return String.format(SPLIT_REVIEW_MSG, configuration.getMaxReviewLines());
+ }
+
+ String reviewSuggestion = openAiClient.ask(patchSet);
+ log.info("Review completed for changeId: {}", changeId);
+ return reviewSuggestion;
+
+ }
+}
+