blob: c1a50ec9b47d4276d00c0308cb2bb00641d2d74a [file] [log] [blame]
package com.googlesource.gerrit.plugins.chatgpt;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
import com.googlesource.gerrit.plugins.chatgpt.data.ChangeSetDataHandler;
import com.googlesource.gerrit.plugins.chatgpt.localization.Localizer;
import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritChange;
import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritClient;
import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.api.gerrit.GerritClientReview;
import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.messages.DebugCodeBlocksReview;
import com.googlesource.gerrit.plugins.chatgpt.mode.common.client.patch.comment.GerritCommentRange;
import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.chatgpt.ChatGptReplyItem;
import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.chatgpt.ChatGptResponseContent;
import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.gerrit.GerritCodeRange;
import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.api.gerrit.GerritComment;
import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.data.ChangeSetData;
import com.googlesource.gerrit.plugins.chatgpt.mode.common.model.review.ReviewBatch;
import com.googlesource.gerrit.plugins.chatgpt.mode.interfaces.client.api.chatgpt.IChatGptClient;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
@Slf4j
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 final Configuration config;
private final GerritClient gerritClient;
private final ChangeSetData changeSetData;
private final Provider<GerritClientReview> clientReviewProvider;
@Getter
private final IChatGptClient chatGptClient;
private final Localizer localizer;
private final DebugCodeBlocksReview debugCodeBlocksReview;
private GerritCommentRange gerritCommentRange;
private List<ReviewBatch> reviewBatches;
private List<GerritComment> commentProperties;
private List<Integer> reviewScores;
@Inject
PatchSetReviewer(
GerritClient gerritClient,
Configuration config,
ChangeSetData changeSetData,
Provider<GerritClientReview> clientReviewProvider,
IChatGptClient chatGptClient,
Localizer localizer
) {
this.config = config;
this.gerritClient = gerritClient;
this.changeSetData = changeSetData;
this.clientReviewProvider = clientReviewProvider;
this.chatGptClient = chatGptClient;
this.localizer = localizer;
debugCodeBlocksReview = new DebugCodeBlocksReview(localizer);
}
public void review(GerritChange change) throws Exception {
reviewBatches = new ArrayList<>();
reviewScores = new ArrayList<>();
commentProperties = gerritClient.getClientData(change).getCommentProperties();
gerritCommentRange = new GerritCommentRange(gerritClient, change);
String patchSet = gerritClient.getPatchSet(change);
if (patchSet.isEmpty()) {
log.info("No file to review has been found in the PatchSet");
return;
}
ChangeSetDataHandler.update(config, change, gerritClient, changeSetData, localizer);
if (changeSetData.shouldRequestChatGptReview()) {
ChatGptResponseContent reviewReply = getReviewReply(change, patchSet);
log.debug("ChatGPT response: {}", reviewReply);
retrieveReviewBatches(reviewReply, change);
}
clientReviewProvider.get().setReview(change, reviewBatches, changeSetData, getReviewScore());
}
private void setCommentBatchMap(ReviewBatch batchMap, Integer batchID) {
if (commentProperties != null && batchID < commentProperties.size()) {
GerritComment commentProperty = commentProperties.get(batchID);
if (commentProperty != null && (commentProperty.getLine() != null || commentProperty.getRange() != null)) {
String id = commentProperty.getId();
String filename = commentProperty.getFilename();
Integer line = commentProperty.getLine();
GerritCodeRange range = commentProperty.getRange();
if (range != null) {
batchMap.setId(id);
batchMap.setFilename(filename);
batchMap.setLine(line);
batchMap.setRange(range);
}
}
}
}
private void setPatchSetReviewBatchMap(ReviewBatch batchMap, ChatGptReplyItem replyItem) {
Optional<GerritCodeRange> optGerritCommentRange = gerritCommentRange.getGerritCommentRange(replyItem);
if (optGerritCommentRange.isPresent()) {
GerritCodeRange gerritCodeRange = optGerritCommentRange.get();
batchMap.setFilename(replyItem.getFilename());
batchMap.setLine(gerritCodeRange.getStartLine());
batchMap.setRange(gerritCodeRange);
}
}
private void retrieveReviewBatches(ChatGptResponseContent reviewReply, GerritChange change) {
if (reviewReply.getMessageContent() != null && !reviewReply.getMessageContent().isEmpty()) {
reviewBatches.add(new ReviewBatch(reviewReply.getMessageContent()));
return;
}
for (ChatGptReplyItem replyItem : reviewReply.getReplies()) {
String reply = replyItem.getReply();
Integer score = replyItem.getScore();
boolean isNotNegative = isNotNegativeReply(score);
boolean isIrrelevant = isIrrelevantReply(replyItem);
boolean isHidden = replyItem.isRepeated() || replyItem.isConflicting() || isIrrelevant || isNotNegative;
if (!replyItem.isConflicting() && !isIrrelevant && score != null) {
reviewScores.add(score);
}
if (changeSetData.getReplyFilterEnabled() && isHidden) {
continue;
}
if (changeSetData.getDebugReviewMode()) {
reply += debugCodeBlocksReview.getDebugCodeBlock(replyItem, isHidden);
}
ReviewBatch batchMap = new ReviewBatch(reply);
if (change.getIsCommentEvent() && replyItem.getId() != null) {
setCommentBatchMap(batchMap, replyItem.getId());
}
else {
setPatchSetReviewBatchMap(batchMap, replyItem);
}
reviewBatches.add(batchMap);
}
}
private ChatGptResponseContent getReviewReply(GerritChange change, String patchSet) throws Exception {
List<String> patchLines = Arrays.asList(patchSet.split("\n"));
if (patchLines.size() > config.getMaxReviewLines()) {
log.warn("Patch set too large. Skipping review. changeId: {}", change.getFullChangeId());
return new ChatGptResponseContent(String.format(SPLIT_REVIEW_MSG, config.getMaxReviewLines()));
}
return chatGptClient.ask(changeSetData, change, patchSet);
}
private Integer getReviewScore() {
if (config.isVotingEnabled()) {
return reviewScores.isEmpty() ? 0 : Collections.min(reviewScores);
}
else {
return null;
}
}
private boolean isNotNegativeReply(Integer score) {
return score != null &&
config.getFilterNegativeComments() &&
score >= config.getFilterCommentsBelowScore();
}
private boolean isIrrelevantReply(ChatGptReplyItem replyItem) {
return config.getFilterRelevantComments() &&
replyItem.getRelevance() != null &&
replyItem.getRelevance() < config.getFilterCommentsRelevanceThreshold();
}
}