diff --git a/src/main/java/com/zhongan/devpilot/actions/changesview/GenerateGitCommitMessageAction.java b/src/main/java/com/zhongan/devpilot/actions/changesview/GenerateGitCommitMessageAction.java index 0c7f373d..107232a6 100644 --- a/src/main/java/com/zhongan/devpilot/actions/changesview/GenerateGitCommitMessageAction.java +++ b/src/main/java/com/zhongan/devpilot/actions/changesview/GenerateGitCommitMessageAction.java @@ -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; @@ -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 @@ -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 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 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 filePaths) throws IOException { - var command = new ArrayList(); - command.add("git"); - command.add("diff"); - command.addAll(filePaths); + private @NotNull List 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 changeList = new ArrayList<>(); + if (workflowHandler instanceof AbstractCommitWorkflowHandler) { + List includedChanges = ((AbstractCommitWorkflowHandler) workflowHandler).getUi().getIncludedChanges(); + if (!includedChanges.isEmpty()) { + changeList.addAll(includedChanges); + } + List 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 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 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> 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 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()); + } } diff --git a/src/main/java/com/zhongan/devpilot/constant/PromptConst.java b/src/main/java/com/zhongan/devpilot/constant/PromptConst.java index 6ba832c9..83de868c 100644 --- a/src/main/java/com/zhongan/devpilot/constant/PromptConst.java +++ b/src/main/java/com/zhongan/devpilot/constant/PromptConst.java @@ -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, "; } diff --git a/src/main/resources/messages/devpilot_en.properties b/src/main/resources/messages/devpilot_en.properties index 6b368952..426fa019 100644 --- a/src/main/resources/messages/devpilot_en.properties +++ b/src/main/resources/messages/devpilot_en.properties @@ -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. diff --git a/src/main/resources/messages/devpilot_zh.properties b/src/main/resources/messages/devpilot_zh.properties index 923ee25e..523724ec 100644 --- a/src/main/resources/messages/devpilot_zh.properties +++ b/src/main/resources/messages/devpilot_zh.properties @@ -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