Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: commit message problem #49

Merged
merged 1 commit into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,26 @@

import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.Presentation;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.diff.impl.patch.FilePatch;
import com.intellij.openapi.diff.impl.patch.IdeaTextPatchBuilder;
import com.intellij.openapi.diff.impl.patch.UnifiedDiffWriter;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.VcsDataKeys;
import com.intellij.openapi.vcs.changes.ui.ChangesBrowserBase;
import com.intellij.openapi.vcs.changes.ui.CommitDialogChangesBrowser;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.CurrentContentRevision;
import com.intellij.openapi.vcs.ui.CommitMessage;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ObjectUtils;
import com.intellij.vcs.commit.AbstractCommitWorkflowHandler;
import com.zhongan.devpilot.DevPilotIcons;
import com.zhongan.devpilot.actions.notifications.DevPilotNotification;
import com.zhongan.devpilot.constant.DefaultConst;
Expand All @@ -18,27 +30,39 @@
import com.zhongan.devpilot.integrations.llms.entity.DevPilotChatCompletionRequest;
import com.zhongan.devpilot.integrations.llms.entity.DevPilotChatCompletionResponse;
import com.zhongan.devpilot.integrations.llms.entity.DevPilotMessage;
import com.zhongan.devpilot.settings.state.LanguageSettingsState;
import com.zhongan.devpilot.util.DevPilotMessageBundle;
import com.zhongan.devpilot.util.DocumentUtil;
import com.zhongan.devpilot.util.MessageUtil;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;

import static com.intellij.util.ObjectUtils.tryCast;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import git4idea.repo.GitRepository;
import git4idea.repo.GitRepositoryManager;

public class GenerateGitCommitMessageAction extends AnAction {

private static final Logger log = Logger.getInstance(GenerateGitCommitMessageAction.class);

public GenerateGitCommitMessageAction() {
super(DevPilotMessageBundle.get("devpilot.action.changesview.generateCommit"), DevPilotMessageBundle.get("devpilot.action.changesview.generateCommit"), DevPilotIcons.SYSTEM_ICON);
}

@Override
public void update(@NotNull AnActionEvent e) {
super.update(e);
Presentation presentation = e.getPresentation();
presentation.setText(DevPilotMessageBundle.get("devpilot.action.changesview.generateCommit"));
presentation.setDescription(DevPilotMessageBundle.get("devpilot.action.changesview.generateCommit"));
presentation.setIcon(DevPilotIcons.SYSTEM_ICON);
}

@Override
Expand All @@ -49,70 +73,124 @@ public void actionPerformed(@NotNull AnActionEvent e) {
}

try {
String gitDiff = getGitDiff(project, getReferencedFilePaths(e));

if (DocumentUtil.experienceEstimatedTokens(gitDiff) + DocumentUtil.experienceEstimatedTokens(PromptConst.GENERATE_COMMIT) > DefaultConst.GPT_35_TOKEN_MAX_LENGTH) {
DevPilotNotification.warn(DevPilotMessageBundle.get("devpilot.changesview.tokens.estimation.overflow"));
List<Change> changeList = getReferencedFilePaths(e);
String diff = getGitDiff(e, changeList);
if (StringUtils.isEmpty(diff)) {
DevPilotNotification.info("no changes selected");
return;
}

var commitMessage = tryCast(e.getData(VcsDataKeys.COMMIT_MESSAGE_CONTROL), CommitMessage.class);
var commitMessage = ObjectUtils.tryCast(e.getData(VcsDataKeys.COMMIT_MESSAGE_CONTROL), CommitMessage.class);
var editor = commitMessage != null ? commitMessage.getEditorField().getEditor() : null;
if (editor != null) {
((EditorEx) editor).setCaretVisible(false);

DevPilotMessage userMessage = MessageUtil.createUserMessage(gitDiff, "-1");
DevPilotChatCompletionRequest devPilotChatCompletionRequest = new DevPilotChatCompletionRequest();
devPilotChatCompletionRequest.getMessages().add(MessageUtil.createSystemMessage(PromptConst.GENERATE_COMMIT));
devPilotChatCompletionRequest.getMessages().add(userMessage);
devPilotChatCompletionRequest.setStream(Boolean.FALSE);

var llmProvider = new LlmProviderFactory().getLlmProvider(project);
DevPilotChatCompletionResponse result = llmProvider.chatCompletionSync(devPilotChatCompletionRequest);

if (result.isSuccessful()) {
var application = ApplicationManager.getApplication();
application.invokeLater(() ->
application.runWriteAction(() ->
WriteCommandAction.runWriteCommandAction(project, () ->
editor.getDocument().setText(result.getContent()))));
} else {
DevPilotNotification.warn(result.getContent());
}

}
ApplicationManager.getApplication().invokeLater(() ->
ApplicationManager.getApplication().runWriteAction(() ->
WriteCommandAction.runWriteCommandAction(project, () -> {
if (editor != null) {
editor.getDocument().setText(" ");
}
})));
generateCommitMessage(project, diff, editor);
} catch (Exception ex) {
DevPilotNotification.warn("Exception occurred while generating commit message");
}
}

private @NotNull List<String> getReferencedFilePaths(AnActionEvent event) {
var changesBrowserBase = event.getData(ChangesBrowserBase.DATA_KEY);
if (changesBrowserBase == null) {
return List.of();
private void generateCommitMessage(Project project, String diff, Editor editor) {
if (DocumentUtil.experienceEstimatedTokens(diff) + DocumentUtil.experienceEstimatedTokens(PromptConst.GENERATE_COMMIT) > DefaultConst.GPT_35_TOKEN_MAX_LENGTH) {
DevPilotNotification.warn(DevPilotMessageBundle.get("devpilot.changesview.tokens.estimation.overflow"));
}

var includedChanges = ((CommitDialogChangesBrowser) changesBrowserBase).getIncludedChanges();
return includedChanges.stream()
.filter(item -> item.getVirtualFile() != null)
.map(item -> item.getVirtualFile().getPath())
.collect(toList());
new Task.Backgroundable(project, DevPilotMessageBundle.get("devpilot.commit.tip"), true) {
@Override
public void run(@NotNull ProgressIndicator progressIndicator) {
if (editor != null) {
((EditorEx) editor).setCaretVisible(false);

String prompt = constructPrompt(PromptConst.GENERATE_COMMIT);
String diffPrompt = PromptConst.DIFF_PREVIEW.replace("{diff}", diff);
DevPilotMessage userMessage = MessageUtil.createUserMessage(diffPrompt, "-1");
DevPilotChatCompletionRequest devPilotChatCompletionRequest = new DevPilotChatCompletionRequest();
devPilotChatCompletionRequest.getMessages().add(MessageUtil.createSystemMessage(prompt));
devPilotChatCompletionRequest.getMessages().add(userMessage);
devPilotChatCompletionRequest.setStream(Boolean.FALSE);
var llmProvider = new LlmProviderFactory().getLlmProvider(project);
DevPilotChatCompletionResponse result = llmProvider.chatCompletionSync(devPilotChatCompletionRequest);

if (result.isSuccessful()) {
var application = ApplicationManager.getApplication();
application.invokeLater(() ->
application.runWriteAction(() ->
WriteCommandAction.runWriteCommandAction(project, () ->
editor.getDocument().setText(result.getContent()))));
} else {
DevPilotNotification.warn(result.getContent());
}
}
}
}.queue();
}

private Process createGitDiffProcess(String projectPath, List<String> filePaths) throws IOException {
var command = new ArrayList<String>();
command.add("git");
command.add("diff");
command.addAll(filePaths);
private @NotNull List<Change> getReferencedFilePaths(AnActionEvent event) {

var processBuilder = new ProcessBuilder(command);
processBuilder.directory(new File(projectPath));
return processBuilder.start();
var workflowHandler = event.getDataContext().getData(VcsDataKeys.COMMIT_WORKFLOW_HANDLER);
List<Change> changeList = new ArrayList<>();
if (workflowHandler instanceof AbstractCommitWorkflowHandler) {
List<Change> includedChanges = ((AbstractCommitWorkflowHandler<?, ?>) workflowHandler).getUi().getIncludedChanges();
if (!includedChanges.isEmpty()) {
changeList.addAll(includedChanges);
}
List<FilePath> filePaths = ((AbstractCommitWorkflowHandler<?, ?>) workflowHandler).getUi().getIncludedUnversionedFiles();
if (!filePaths.isEmpty()) {
for (FilePath filePath : filePaths) {
Change change = new Change(null, new CurrentContentRevision(filePath));
changeList.add(change);
}
}
}
return changeList;
}

private String getGitDiff(Project project, List<String> filePaths) throws IOException {
var process = createGitDiffProcess(project.getBasePath(), filePaths);
var reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
return reader.lines().collect(joining("\n"));
private String getGitDiff(AnActionEvent event, List<Change> includedChanges) {
if (includedChanges.isEmpty()) {
return null;
}
StringBuilder result = new StringBuilder();
Project project = event.getProject();
if (project == null) {
return null;
}
GitRepositoryManager gitRepositoryManager = GitRepositoryManager.getInstance(project);
Map<GitRepository, List<Change>> changesByRepository = new HashMap<>();
for (Change change : includedChanges) {
VirtualFile file = change.getVirtualFile();
if (file != null) {
GitRepository repository = gitRepositoryManager.getRepositoryForFileQuick(file);
changesByRepository.computeIfAbsent(repository, k -> new ArrayList<>()).add(change);
}
}

changesByRepository.forEach((gitRepository, changes) -> {
if (gitRepository != null) {
try {
if (project.getBasePath() == null) {
return;
}
List<FilePatch> filePatches = IdeaTextPatchBuilder.buildPatch(project, changes, Path.of(project.getBasePath()), false, true);
StringWriter stringWriter = new StringWriter();
stringWriter.write("Repository: " + gitRepository.getRoot().getPath() + "\n");

UnifiedDiffWriter.write(project, filePatches, stringWriter, "\n", null);

result.append(stringWriter);
} catch (Exception e) {
log.info(e.getMessage());
}
}
});
return result.toString();
}

public String constructPrompt(String promptContent) {
Integer languageIndex = LanguageSettingsState.getInstance().getLanguageIndex();
Locale locale = languageIndex == 0 ? Locale.ENGLISH : Locale.SIMPLIFIED_CHINESE;
return promptContent.replace("{locale}", locale.getDisplayLanguage());
}
}
6 changes: 4 additions & 2 deletions src/main/java/com/zhongan/devpilot/constant/PromptConst.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ private PromptConst() {

public static final String ANSWER_IN_CHINESE = "\n\n请用中文回答";

public static final String GENERATE_COMMIT = "Summarize the git diff with a concise and descriptive commit message. Adopt the imperative mood, present tense, active voice, and include relevant verbs. Remember that your entire response will be directly used as the git commit message.";

public static final String GENERATE_COMMIT = "Write a clean and comprehensive commit message that accurately summarizes the changes made in the given `git diff` output, following the best practices and conventional commit convention. Remember that your entire response will be directly used as the git commit message. The response should be in the language {locale}.";

public static final String DIFF_PREVIEW = "This is the `git diff`:\n" + "{diff}";

public final static String MOCK_WEB_MVC = "please use MockMvc to mock web requests, ";

}
2 changes: 2 additions & 0 deletions src/main/resources/messages/devpilot_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ devpilot.action.edit.settings=Edit Settings...
devpilot.chatWindow.context.overflow=This model's maximum context length is 16K tokens.
devpilot.chatWindow.response.null=Nothing to see here.

devpilot.commit.tip=Generating commit message

devpilot.alter.file.exist=File already exists.
devpilot.alter.file.not.exist=File does not exist.
devpilot.alter.code.not.selected=Please select the code block first.
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/messages/devpilot_zh.properties
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ devpilot.action.edit.settings=\u7F16\u8F91\u8BBE\u7F6E
devpilot.chatWindow.context.overflow=\u6A21\u578B\u4E0A\u4E0B\u6587\u6700\u5927\u4E3A 16k tokens.
devpilot.chatWindow.response.null=\u65E0\u54CD\u5E94\uFF0C\u8BF7\u91CD\u8BD5

devpilot.commit.tip=\u751f\u6210\u63d0\u4ea4\u4fe1\u606f\u4e2d

devpilot.alter.file.exist=\u6587\u4EF6\u5DF2\u5B58\u5728
devpilot.alter.file.not.exist=\u6587\u4EF6\u4E0D\u5B58\u5728
devpilot.alter.code.not.selected=\u8BF7\u5148\u9009\u62E9\u4EE3\u7801\u5757
Expand Down
Loading