Support git commit message generation (#276)

* Add git commit message generation feature using OpenAI service
This commit is contained in:
Carl-Robert 2023-11-17 01:20:00 +02:00 committed by GitHub
parent a53bc94d9f
commit 44e5aa79dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 197 additions and 4 deletions

View file

@ -0,0 +1,139 @@
package ee.carlrobert.codegpt.actions;
import static com.intellij.openapi.ui.Messages.OK;
import static com.intellij.util.ObjectUtils.tryCast;
import static ee.carlrobert.codegpt.util.file.FileUtils.getResourceContent;
import static java.util.stream.Collectors.joining;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vcs.VcsDataKeys;
import com.intellij.openapi.vcs.ui.CommitMessage;
import ee.carlrobert.codegpt.CodeGPTBundle;
import ee.carlrobert.codegpt.EncodingManager;
import ee.carlrobert.codegpt.Icons;
import ee.carlrobert.codegpt.completions.CompletionClientProvider;
import ee.carlrobert.codegpt.credentials.OpenAICredentialsManager;
import ee.carlrobert.codegpt.settings.state.OpenAISettingsState;
import ee.carlrobert.codegpt.util.OverlayUtils;
import ee.carlrobert.llm.client.openai.completion.ErrorDetails;
import ee.carlrobert.llm.client.openai.completion.chat.request.OpenAIChatCompletionMessage;
import ee.carlrobert.llm.client.openai.completion.chat.request.OpenAIChatCompletionRequest;
import ee.carlrobert.llm.completion.CompletionEventListener;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
import org.jetbrains.annotations.NotNull;
public class GenerateGitCommitMessageAction extends AnAction {
private final EncodingManager encodingManager;
public GenerateGitCommitMessageAction() {
super(
CodeGPTBundle.get("action.generateCommitMessage.title"),
CodeGPTBundle.get("action.generateCommitMessage.description"),
Icons.SparkleIcon);
encodingManager = EncodingManager.getInstance();
}
@Override
public void update(@NotNull AnActionEvent event) {
var apiKeyExists = OpenAICredentialsManager.getInstance().isApiKeySet();
event.getPresentation().setEnabled(apiKeyExists);
event.getPresentation().setText(CodeGPTBundle.get(apiKeyExists
? "action.generateCommitMessage.title"
: "action.generateCommitMessage.serviceWarning"));
}
@Override
public void actionPerformed(@NotNull AnActionEvent event) {
var project = event.getProject();
if (project == null || project.getBasePath() == null) {
return;
}
var gitDiff = getGitDiff(project);
var tokenCount = encodingManager.countTokens(gitDiff);
if (tokenCount > 4096 && OverlayUtils.showTokenSoftLimitWarningDialog(tokenCount) != OK) {
return;
}
var editor = getCommitMessageEditor(event);
if (editor != null) {
((EditorEx) editor).setCaretVisible(false);
generateMessage(project, editor, gitDiff);
}
}
private void generateMessage(Project project, Editor editor, String gitDiff) {
CompletionClientProvider.getOpenAIClient().getChatCompletion(
new OpenAIChatCompletionRequest.Builder(List.of(
new OpenAIChatCompletionMessage("system",
getResourceContent("/prompts/git-message.txt")),
new OpenAIChatCompletionMessage("user", gitDiff)))
.setModel(OpenAISettingsState.getInstance().getModel())
.build(),
getEventListener(project, editor.getDocument()));
}
private CompletionEventListener getEventListener(Project project, Document document) {
return new CompletionEventListener() {
private final StringBuilder messageBuilder = new StringBuilder();
@Override
public void onMessage(String message) {
messageBuilder.append(message);
var application = ApplicationManager.getApplication();
application.invokeLater(() ->
application.runWriteAction(() ->
WriteCommandAction.runWriteCommandAction(project, () ->
document.setText(messageBuilder))));
}
@Override
public void onError(ErrorDetails error, Throwable ex) {
Notifications.Bus.notify(new Notification(
"CodeGPT Notification Group",
"CodeGPT",
error.getMessage(),
NotificationType.ERROR));
}
};
}
private Editor getCommitMessageEditor(AnActionEvent event) {
var commitMessage = tryCast(
event.getData(VcsDataKeys.COMMIT_MESSAGE_CONTROL),
CommitMessage.class);
return commitMessage != null ? commitMessage.getEditorField().getEditor() : null;
}
// TODO: Get diff based on the user selection
private String getGitDiff(Project project) {
var process = createGitDiffProcess(project.getBasePath());
var reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
return reader.lines().collect(joining("\n"));
}
private Process createGitDiffProcess(String projectPath) {
var processBuilder = new ProcessBuilder("git", "diff", "--cached");
processBuilder.directory(new File(projectPath));
try {
return processBuilder.start();
} catch (IOException ex) {
throw new RuntimeException("Unable to start git diff process", ex);
}
}
}

View file

@ -21,6 +21,7 @@ public class ConfigurationState implements PersistentStateComponent<Configuratio
private int maxTokens = 1000;
private double temperature = 0.2;
private boolean createNewChatOnEachAction;
private boolean ignoreGitCommitTokenLimit;
private Map<String, String> tableData = EditorActionsUtil.DEFAULT_ACTIONS;
public static ConfigurationState getInstance() {
@ -77,4 +78,12 @@ public class ConfigurationState implements PersistentStateComponent<Configuratio
public void setTableData(Map<String, String> tableData) {
this.tableData = tableData;
}
public boolean isIgnoreGitCommitTokenLimit() {
return ignoreGitCommitTokenLimit;
}
public void setIgnoreGitCommitTokenLimit(boolean ignoreGitCommitTokenLimit) {
this.ignoreGitCommitTokenLimit = ignoreGitCommitTokenLimit;
}
}

View file

@ -3,6 +3,7 @@ package ee.carlrobert.codegpt.util;
import static com.intellij.openapi.ui.Messages.CANCEL;
import static com.intellij.openapi.ui.Messages.OK;
import static ee.carlrobert.codegpt.Icons.DefaultIcon;
import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import com.intellij.execution.ExecutionBundle;
@ -26,6 +27,7 @@ import com.intellij.util.ui.JBUI;
import ee.carlrobert.codegpt.CodeGPTBundle;
import ee.carlrobert.codegpt.conversations.ConversationsState;
import ee.carlrobert.codegpt.indexes.FolderStructureTreePanel;
import ee.carlrobert.codegpt.settings.configuration.ConfigurationState;
import java.awt.Point;
import java.awt.event.MouseEvent;
import javax.swing.JComponent;
@ -73,8 +75,8 @@ public class OverlayUtils {
return MessageDialogBuilder.okCancel(
CodeGPTBundle.get("dialog.tokenLimitExceeded.title"),
CodeGPTBundle.get("dialog.tokenLimitExceeded.description"))
.yesText(CodeGPTBundle.get("dialog.tokenLimitExceeded.continue"))
.noText(CodeGPTBundle.get("dialog.tokenLimitExceeded.cancel"))
.yesText(CodeGPTBundle.get("dialog.continue"))
.noText(CodeGPTBundle.get("dialog.cancel"))
.icon(DefaultIcon)
.doNotAsk(new DoNotAskOption.Adapter() {
@Override
@ -98,6 +100,35 @@ public class OverlayUtils {
.guessWindowAndAsk() ? OK : CANCEL;
}
public static int showTokenSoftLimitWarningDialog(int tokenCount) {
return MessageDialogBuilder.okCancel(
CodeGPTBundle.get("dialog.tokenSoftLimitExceeded.title"),
format(CodeGPTBundle.get("dialog.tokenSoftLimitExceeded.description"), tokenCount))
.yesText(CodeGPTBundle.get("dialog.continue"))
.noText(CodeGPTBundle.get("dialog.cancel"))
.icon(DefaultIcon)
.doNotAsk(new DoNotAskOption.Adapter() {
@Override
public void rememberChoice(boolean isSelected, int exitCode) {
if (isSelected) {
ConfigurationState.getInstance().setIgnoreGitCommitTokenLimit(true);
}
}
@NotNull
@Override
public String getDoNotShowMessage() {
return ExecutionBundle.message("don.t.ask.again");
}
@Override
public boolean shouldSaveOptionsOnCancel() {
return true;
}
})
.guessWindowAndAsk() ? OK : CANCEL;
}
public static void showSelectedEditorSelectionWarning(AnActionEvent event) {
var locationOnScreen = ((MouseEvent) event.getInputEvent()).getLocationOnScreen();
locationOnScreen.y = locationOnScreen.y - 16;