diff --git a/server-core/src/main/java/io/onedev/server/ai/McpHelperResource.java b/server-core/src/main/java/io/onedev/server/ai/McpHelperResource.java index 91064ae14f..b44ffbe055 100644 --- a/server-core/src/main/java/io/onedev/server/ai/McpHelperResource.java +++ b/server-core/src/main/java/io/onedev/server/ai/McpHelperResource.java @@ -566,7 +566,7 @@ public class McpHelperResource { createPullRequestInputSchema.put("Type", "object"); createPullRequestInputSchema.put("Properties", createPullRequestProperties); - createPullRequestInputSchema.put("Required", List.of("sourceBranch", "title")); + createPullRequestInputSchema.put("Required", List.of("sourceBranch")); inputSchemas.put("createPullRequest", createPullRequestInputSchema); @@ -1432,13 +1432,6 @@ public class McpHelperResource { if (baseCommitId == null) throw new NotAcceptableException("No common base for source and target branches"); - var title = (String) data.remove("title"); - if (title == null) - throw new NotAcceptableException("Title is required"); - - request.cleanTitle(); - - request.setDescription((String) data.remove("description")); request.setTarget(target); request.setSource(source); request.setSubmitter(SecurityUtils.getUser()); @@ -1461,7 +1454,21 @@ public class McpHelperResource { update.setTargetHeadCommitHash(request.getTarget().getObjectName()); request.getUpdates().add(update); - request.generateTitleAndDescriptionIfEmpty(); + var title = (String) data.remove("title"); + if (title == null) + title = request.generateTitleFromCommits(); + else + title = request.cleanTitle(title); + + if (title == null) + title = request.generateTitleFromBranch(); + + request.setTitle(title); + + var description = (String) data.remove("description"); + if (description == null) + description = request.generateDescriptionFromCommits(); + request.setDescription(description); pullRequestService.checkReviews(request, false); diff --git a/server-core/src/main/java/io/onedev/server/model/PullRequest.java b/server-core/src/main/java/io/onedev/server/model/PullRequest.java index 29993946e0..e81003e2f4 100644 --- a/server-core/src/main/java/io/onedev/server/model/PullRequest.java +++ b/server-core/src/main/java/io/onedev/server/model/PullRequest.java @@ -49,7 +49,6 @@ import java.util.Stack; import java.util.UUID; import java.util.stream.Collectors; -import org.jspecify.annotations.Nullable; import javax.persistence.AttributeOverride; import javax.persistence.AttributeOverrides; import javax.persistence.CascadeType; @@ -68,6 +67,7 @@ import org.apache.commons.lang3.StringUtils; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.revwalk.RevCommit; import org.hibernate.annotations.DynamicUpdate; +import org.jspecify.annotations.Nullable; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; @@ -76,8 +76,6 @@ import com.google.common.collect.Lists; import io.onedev.server.OneDev; import io.onedev.server.attachment.AttachmentStorageSupport; -import io.onedev.server.service.PullRequestService; -import io.onedev.server.service.UserService; import io.onedev.server.entityreference.EntityReference; import io.onedev.server.entityreference.PullRequestReference; import io.onedev.server.git.GitUtils; @@ -96,6 +94,9 @@ import io.onedev.server.model.support.pullrequest.MergeStrategy; import io.onedev.server.rest.annotation.Api; import io.onedev.server.search.entity.SortField; import io.onedev.server.security.SecurityUtils; +import io.onedev.server.service.PullRequestService; +import io.onedev.server.service.UserService; +import io.onedev.server.util.BranchSemantic; import io.onedev.server.util.ComponentContext; import io.onedev.server.util.ProjectAndBranch; import io.onedev.server.web.asset.emoji.Emojis; @@ -1410,56 +1411,85 @@ public class PullRequest extends ProjectBelonging } // Remove issue number suffix if there is any - public void cleanTitle() { + public String cleanTitle(String title) { var cleanedTitle = title.replaceFirst("\\s*\\([a-zA-Z]+-\\d+\\)$", ""); cleanedTitle = cleanedTitle.replaceFirst("\\s*\\(#\\d+\\)$", "").trim(); if (cleanedTitle.length() != 0) - title = cleanedTitle; + return cleanedTitle; + else + return title; } - public void generateTitleAndDescriptionIfEmpty() { - if (title == null) { - var commits = getLatestUpdate().getCommits(); - if (commits.size() == 1) { - title = commits.get(0).getShortMessage(); - cleanTitle(); - } else { - title = getSource().getBranch().toLowerCase(); - boolean wip = false; - if (title.startsWith("wip-") || title.startsWith("wip_") || title.startsWith("wip/")) { - wip = true; - title = title.substring(4); - } - var commitTypes = CONVENTIONAL_COMMIT_TYPES; - var branchProtection = getProject().getBranchProtection(getTargetBranch(), getSubmitter()); - if (branchProtection.isEnforceConventionalCommits() && !branchProtection.getCommitTypes().isEmpty()) { - commitTypes = branchProtection.getCommitTypes(); - } - boolean found = false; - for (var commitType: commitTypes) { - if (title.startsWith(commitType + "-") || title.startsWith(commitType + "_") || title.startsWith(commitType + "/")) { - title = commitType + ": " + StringUtils.capitalize(title.substring(commitType.length() + 1).replace('-', ' ').replace('_', ' ').replace('/', ' ')); - found = true; - break; - } - } - if (!found) { - title = StringUtils.capitalize(title.replace('-', ' ').replace('_', ' ').replace('/', ' ')); - } - if (wip) - title = "[WIP] " + title; + public BranchSemantic getSourceBranchSemantic() { + var work = getSourceBranch(); + boolean workInProgress = false; + if (work.toLowerCase().startsWith("wip-") + || work.toLowerCase().startsWith("wip_") + || work.toLowerCase().startsWith("wip/")) { + workInProgress = true; + work = work.substring(4); + } + var commitTypes = CONVENTIONAL_COMMIT_TYPES; + var branchProtection = getProject().getBranchProtection(getTargetBranch(), getSubmitter()); + if (branchProtection.isEnforceConventionalCommits() && !branchProtection.getCommitTypes().isEmpty()) { + commitTypes = branchProtection.getCommitTypes(); + } + String workType = null; + for (var commitType: commitTypes) { + if (work.toLowerCase().startsWith(commitType + "-") + || work.toLowerCase().startsWith(commitType + "_") + || work.toLowerCase().startsWith(commitType + "/")) { + workType = commitType; + work = work.substring(commitType.length() + 1); + break; } } - if (description == null) { - var commits = getLatestUpdate().getCommits(); - if (commits.size() == 1) { - var commit = commits.get(0); - var shortMessage = commits.get(0).getShortMessage(); - description = commit.getFullMessage().substring(shortMessage.length()).trim(); - if (description.length() == 0) - description = null; - } - } + return new BranchSemantic(workInProgress, workType, StringUtils.trimToNull(work)); + } + + public String getTitlePrefix(BranchSemantic sourceBranchSemantic) { + var prefixBuilder = new StringBuilder(); + if (sourceBranchSemantic.isWorkInProgress()) { + prefixBuilder.append("[WIP] "); + } + if (sourceBranchSemantic.getWorkType() != null) { + prefixBuilder.append(sourceBranchSemantic.getWorkType()).append(": "); + } + return prefixBuilder.toString(); + } + + public String generateTitleFromBranch() { + var sourceBranchSemantic = getSourceBranchSemantic(); + var title = getTitlePrefix(sourceBranchSemantic); + if (sourceBranchSemantic.getWorkDescription() != null) { + title += StringUtils.capitalize(sourceBranchSemantic.getWorkDescription().toLowerCase().replace('-', ' ').replace('_', ' ').replace('/', ' ')); + } + return title.trim(); + } + + @Nullable + public String generateTitleFromCommits() { + var commits = getLatestUpdate().getCommits(); + if (commits.size() == 1) { + return cleanTitle(commits.get(0).getShortMessage()); + } else { + return null; + } + } + + @Nullable + public String generateDescriptionFromCommits() { + var commits = getLatestUpdate().getCommits(); + if (commits.size() == 1) { + var commit = commits.get(0); + var shortMessage = commit.getShortMessage(); + var description = commit.getFullMessage().substring(shortMessage.length()).trim(); + if (description.length() == 0) + description = null; + return description; + } else { + return null; + } } } diff --git a/server-core/src/main/java/io/onedev/server/util/BranchSemantic.java b/server-core/src/main/java/io/onedev/server/util/BranchSemantic.java new file mode 100644 index 0000000000..6f097d3864 --- /dev/null +++ b/server-core/src/main/java/io/onedev/server/util/BranchSemantic.java @@ -0,0 +1,33 @@ +package io.onedev.server.util; + +import org.jspecify.annotations.Nullable; + +public class BranchSemantic { + + private final boolean workInProgress; + + private final String workType; + + private final String workDescription; + + public BranchSemantic(boolean workInProgress, @Nullable String workType, @Nullable String workDescription) { + this.workInProgress = workInProgress; + this.workType = workType; + this.workDescription = workDescription; + } + + public boolean isWorkInProgress() { + return workInProgress; + } + + @Nullable + public String getWorkType() { + return workType; + } + + @Nullable + public String getWorkDescription() { + return workDescription; + } + +} \ No newline at end of file diff --git a/server-core/src/main/java/io/onedev/server/web/behavior/AgentQueryBehavior.java b/server-core/src/main/java/io/onedev/server/web/behavior/AgentQueryBehavior.java index bb3936b746..1050c95fc4 100644 --- a/server-core/src/main/java/io/onedev/server/web/behavior/AgentQueryBehavior.java +++ b/server-core/src/main/java/io/onedev/server/web/behavior/AgentQueryBehavior.java @@ -30,8 +30,6 @@ import static io.onedev.server.web.translation.Translation._T; import static java.util.Collections.sort; public class AgentQueryBehavior extends ANTLRAssistBehavior { - - private static final String FUZZY_SUGGESTION_DESCRIPTION_PREFIX = "enclose with ~"; private final boolean forExecutor; @@ -188,8 +186,8 @@ public class AgentQueryBehavior extends ANTLRAssistBehavior { @Override protected boolean isFuzzySuggestion(InputCompletion suggestion) { - return suggestion.getDescription() != null - && suggestion.getDescription().startsWith(FUZZY_SUGGESTION_DESCRIPTION_PREFIX); + return suggestion.getLabel().startsWith("~") + && suggestion.getLabel().endsWith("~"); } } diff --git a/server-core/src/main/java/io/onedev/server/web/behavior/BuildQueryBehavior.java b/server-core/src/main/java/io/onedev/server/web/behavior/BuildQueryBehavior.java index 018b1b2bb0..20bae9dd29 100644 --- a/server-core/src/main/java/io/onedev/server/web/behavior/BuildQueryBehavior.java +++ b/server-core/src/main/java/io/onedev/server/web/behavior/BuildQueryBehavior.java @@ -38,8 +38,6 @@ import io.onedev.server.web.behavior.inputassist.NaturalLanguageTranslator; import io.onedev.server.web.util.SuggestionUtils; public class BuildQueryBehavior extends ANTLRAssistBehavior { - - private static final String FUZZY_SUGGESTION_DESCRIPTION_PREFIX = "enclose with ~"; private final IModel projectModel; @@ -286,8 +284,8 @@ public class BuildQueryBehavior extends ANTLRAssistBehavior { @Override protected boolean isFuzzySuggestion(InputCompletion suggestion) { - return suggestion.getDescription() != null - && suggestion.getDescription().startsWith(FUZZY_SUGGESTION_DESCRIPTION_PREFIX); + return suggestion.getLabel().startsWith("~") + && suggestion.getLabel().endsWith("~"); } } diff --git a/server-core/src/main/java/io/onedev/server/web/behavior/CodeCommentQueryBehavior.java b/server-core/src/main/java/io/onedev/server/web/behavior/CodeCommentQueryBehavior.java index 374ca4637a..8c83297af9 100644 --- a/server-core/src/main/java/io/onedev/server/web/behavior/CodeCommentQueryBehavior.java +++ b/server-core/src/main/java/io/onedev/server/web/behavior/CodeCommentQueryBehavior.java @@ -30,8 +30,6 @@ import static io.onedev.server.search.entity.codecomment.CodeCommentQueryLexer.* import static io.onedev.server.web.translation.Translation._T; public class CodeCommentQueryBehavior extends ANTLRAssistBehavior { - - private static final String FUZZY_SUGGESTION_DESCRIPTION_PREFIX = "enclose with ~"; private final IModel projectModel; @@ -175,8 +173,8 @@ public class CodeCommentQueryBehavior extends ANTLRAssistBehavior { @Override protected boolean isFuzzySuggestion(InputCompletion suggestion) { - return suggestion.getDescription() != null - && suggestion.getDescription().startsWith(FUZZY_SUGGESTION_DESCRIPTION_PREFIX); + return suggestion.getLabel().startsWith("~") + && suggestion.getLabel().endsWith("~"); } } diff --git a/server-core/src/main/java/io/onedev/server/web/behavior/CommitQueryBehavior.java b/server-core/src/main/java/io/onedev/server/web/behavior/CommitQueryBehavior.java index 0833dfea54..382bc9d8e9 100644 --- a/server-core/src/main/java/io/onedev/server/web/behavior/CommitQueryBehavior.java +++ b/server-core/src/main/java/io/onedev/server/web/behavior/CommitQueryBehavior.java @@ -36,8 +36,6 @@ import io.onedev.server.xodus.CommitInfoService; public class CommitQueryBehavior extends ANTLRAssistBehavior { - private static final String FUZZY_SUGGESTION_DESCRIPTION_PREFIX = "enclose with ~"; - private final IModel projectModel; private final boolean withCurrentUserCriteria; @@ -198,8 +196,8 @@ public class CommitQueryBehavior extends ANTLRAssistBehavior { @Override protected boolean isFuzzySuggestion(InputCompletion suggestion) { - return suggestion.getDescription() != null - && suggestion.getDescription().startsWith(FUZZY_SUGGESTION_DESCRIPTION_PREFIX); + return suggestion.getLabel().startsWith("~") + && suggestion.getLabel().endsWith("~"); } } diff --git a/server-core/src/main/java/io/onedev/server/web/behavior/IssueQueryBehavior.java b/server-core/src/main/java/io/onedev/server/web/behavior/IssueQueryBehavior.java index e8855612f6..07de1a05b4 100644 --- a/server-core/src/main/java/io/onedev/server/web/behavior/IssueQueryBehavior.java +++ b/server-core/src/main/java/io/onedev/server/web/behavior/IssueQueryBehavior.java @@ -109,8 +109,6 @@ import io.onedev.server.web.util.SuggestionUtils; import io.onedev.server.web.util.WicketUtils; public class IssueQueryBehavior extends ANTLRAssistBehavior { - - private static final String FUZZY_SUGGESTION_DESCRIPTION_PREFIX = "enclose with ~"; private final IModel projectModel; @@ -439,7 +437,8 @@ public class IssueQueryBehavior extends ANTLRAssistBehavior { @Override protected boolean isFuzzySuggestion(InputCompletion suggestion) { - return suggestion.getDescription() != null && (suggestion.getDescription().startsWith(FUZZY_SUGGESTION_DESCRIPTION_PREFIX)); + return suggestion.getLabel().startsWith("~") + && suggestion.getLabel().endsWith("~"); } @Override diff --git a/server-core/src/main/java/io/onedev/server/web/behavior/PackQueryBehavior.java b/server-core/src/main/java/io/onedev/server/web/behavior/PackQueryBehavior.java index 8d784e22a3..000aca354e 100644 --- a/server-core/src/main/java/io/onedev/server/web/behavior/PackQueryBehavior.java +++ b/server-core/src/main/java/io/onedev/server/web/behavior/PackQueryBehavior.java @@ -33,8 +33,6 @@ import static io.onedev.server.web.translation.Translation._T; public class PackQueryBehavior extends ANTLRAssistBehavior { - private static final String FUZZY_SUGGESTION_DESCRIPTION_PREFIX = "enclose with ~"; - private final IModel projectModel; private final String packType; @@ -222,8 +220,8 @@ public class PackQueryBehavior extends ANTLRAssistBehavior { @Override protected boolean isFuzzySuggestion(InputCompletion suggestion) { - return suggestion.getDescription() != null - && suggestion.getDescription().startsWith(FUZZY_SUGGESTION_DESCRIPTION_PREFIX); + return suggestion.getLabel().startsWith("~") + && suggestion.getLabel().endsWith("~"); } } diff --git a/server-core/src/main/java/io/onedev/server/web/behavior/ProjectQueryBehavior.java b/server-core/src/main/java/io/onedev/server/web/behavior/ProjectQueryBehavior.java index 372a8afc65..c37862d6c1 100644 --- a/server-core/src/main/java/io/onedev/server/web/behavior/ProjectQueryBehavior.java +++ b/server-core/src/main/java/io/onedev/server/web/behavior/ProjectQueryBehavior.java @@ -37,8 +37,6 @@ import static io.onedev.server.web.translation.Translation._T; public class ProjectQueryBehavior extends ANTLRAssistBehavior { - private static final String FUZZY_SUGGESTION_DESCRIPTION_PREFIX = "enclose with ~"; - private final boolean childQuery; public ProjectQueryBehavior(boolean childQuery) { @@ -225,8 +223,8 @@ public class ProjectQueryBehavior extends ANTLRAssistBehavior { @Override protected boolean isFuzzySuggestion(InputCompletion suggestion) { - return suggestion.getDescription() != null - && suggestion.getDescription().startsWith(FUZZY_SUGGESTION_DESCRIPTION_PREFIX); + return suggestion.getLabel().startsWith("~") + && suggestion.getLabel().endsWith("~"); } } diff --git a/server-core/src/main/java/io/onedev/server/web/behavior/PullRequestQueryBehavior.java b/server-core/src/main/java/io/onedev/server/web/behavior/PullRequestQueryBehavior.java index 3b78abd13d..da1ec04616 100644 --- a/server-core/src/main/java/io/onedev/server/web/behavior/PullRequestQueryBehavior.java +++ b/server-core/src/main/java/io/onedev/server/web/behavior/PullRequestQueryBehavior.java @@ -56,8 +56,6 @@ import io.onedev.server.web.behavior.inputassist.NaturalLanguageTranslator; import io.onedev.server.web.util.SuggestionUtils; public class PullRequestQueryBehavior extends ANTLRAssistBehavior { - - private static final String FUZZY_SUGGESTION_DESCRIPTION_PREFIX = "enclose with ~"; private final IModel projectModel; @@ -270,8 +268,8 @@ public class PullRequestQueryBehavior extends ANTLRAssistBehavior { @Override protected boolean isFuzzySuggestion(InputCompletion suggestion) { - return suggestion.getDescription() != null - && suggestion.getDescription().startsWith(FUZZY_SUGGESTION_DESCRIPTION_PREFIX); + return suggestion.getLabel().startsWith("~") + && suggestion.getLabel().endsWith("~"); } @Override diff --git a/server-core/src/main/java/io/onedev/server/web/component/markdown/JavascriptTranslations.java b/server-core/src/main/java/io/onedev/server/web/component/markdown/JavascriptTranslations.java index 28114bf2f6..80bb706a48 100644 --- a/server-core/src/main/java/io/onedev/server/web/component/markdown/JavascriptTranslations.java +++ b/server-core/src/main/java/io/onedev/server/web/component/markdown/JavascriptTranslations.java @@ -12,31 +12,31 @@ import io.onedev.server.OneDev; public class JavascriptTranslations { static String get() { - try { - var map = new HashMap(); - - map.put("loading", _T("Loading...")); - map.put("loading-emojis", _T("Loading emojis...")); - map.put("commented-code-outdated", _T("Commented code is outdated")); - map.put("suggest-changes", _T("Suggest changes")); - map.put("upload-should-be-less-than", _T("Upload should be less than {0} Mb")); - map.put("uploading-file", _T("Uploading file")); - map.put("unable-to-connect-to-server", _T("Unable to connect to server")); - map.put("programming-language", _T("Programming language")); - map.put("copy-to-clipboard", _T("Copy to clipboard")); - map.put("suggestion-outdated", _T("Suggestion is outdated either due to code change or pull request close")); - map.put("remove-from-batch", _T("Remove from batch")); - map.put("add-to-batch", _T("Add to batch to commit with other suggestions later")); - map.put("commit-suggestion", _T("Commit suggestion")); - map.put("issue-not-exist-or-access-denied", _T("Issue not exist or access denied")); - map.put("pull-request-not-exist-or-access-denied", _T("Pull request not exist or access denied")); - map.put("build-not-exist-or-access-denied", _T("Build not exist or access denied")); - map.put("commit-not-exist-or-access-denied", _T("Commit not exist or access denied")); - map.put("enter-description-here", _T("Enter description here")); + try { + var map = new HashMap(); + + map.put("loading", _T("Loading...")); + map.put("loading-emojis", _T("Loading emojis...")); + map.put("commented-code-outdated", _T("Commented code is outdated")); + map.put("suggest-changes", _T("Suggest changes")); + map.put("upload-should-be-less-than", _T("Upload should be less than {0} Mb")); + map.put("uploading-file", _T("Uploading file")); + map.put("unable-to-connect-to-server", _T("Unable to connect to server")); + map.put("programming-language", _T("Programming language")); + map.put("copy-to-clipboard", _T("Copy to clipboard")); + map.put("suggestion-outdated", _T("Suggestion is outdated either due to code change or pull request close")); + map.put("remove-from-batch", _T("Remove from batch")); + map.put("add-to-batch", _T("Add to batch to commit with other suggestions later")); + map.put("commit-suggestion", _T("Commit suggestion")); + map.put("issue-not-exist-or-access-denied", _T("Issue not exist or access denied")); + map.put("pull-request-not-exist-or-access-denied", _T("Pull request not exist or access denied")); + map.put("build-not-exist-or-access-denied", _T("Build not exist or access denied")); + map.put("commit-not-exist-or-access-denied", _T("Commit not exist or access denied")); + map.put("enter-description-here", _T("Enter description here")); - return OneDev.getInstance(ObjectMapper.class).writeValueAsString(map); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + return OneDev.getInstance(ObjectMapper.class).writeValueAsString(map); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } } } \ No newline at end of file diff --git a/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/SymbolContext.java b/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/SymbolContext.java index bc74fcee0b..92824803ff 100644 --- a/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/SymbolContext.java +++ b/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/SymbolContext.java @@ -17,9 +17,9 @@ public class SymbolContext implements Serializable { private final List linesAtStart; - public SymbolContext(String fileName, String symbolLine, + public SymbolContext(String blobPath, String symbolLine, List linesBeforeSymbolLine, List linesAfterSymbolLine, List linesAtStart) { - this.blobPath = fileName; + this.blobPath = blobPath; this.symbolLine = symbolLine; this.linesBeforeSymbolLine = linesBeforeSymbolLine; this.linesAfterSymbolLine = linesAfterSymbolLine; diff --git a/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/SymbolTooltipPanel.java b/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/SymbolTooltipPanel.java index 2694be6df8..776dd5c5d5 100644 --- a/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/SymbolTooltipPanel.java +++ b/server-core/src/main/java/io/onedev/server/web/component/symboltooltip/SymbolTooltipPanel.java @@ -37,8 +37,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import dev.langchain4j.data.message.SystemMessage; import dev.langchain4j.data.message.UserMessage; import io.onedev.commons.jsymbol.Symbol; +import io.onedev.commons.utils.ExplicitException; import io.onedev.commons.utils.LinearRange; import io.onedev.commons.utils.PlanarRange; +import io.onedev.server.exception.ExceptionUtils; import io.onedev.server.git.Blob; import io.onedev.server.git.BlobIdent; import io.onedev.server.model.Project; @@ -59,7 +61,7 @@ import io.onedev.server.web.page.project.blob.render.BlobRenderer; public abstract class SymbolTooltipPanel extends Panel { - private static final int QUERY_ENTRIES = 20; + private static final int QUERY_ENTRIES = 25; private static final int BEFORE_CONTEXT_SIZE = 5; @@ -321,6 +323,7 @@ public abstract class SymbolTooltipPanel extends Panel { mapperCopy.addMixIn(PlanarRange.class, IgnorePlanarRangeMixin.class); mapperCopy.addMixIn(LinearRange.class, IgnoreLinearRangeMixin.class); var jsonOfSymbolHits = mapperCopy.writeValueAsString(symbolHits); + var symbolContext = getSymbolContext(symbolPosition, BEFORE_CONTEXT_SIZE, AFTER_CONTEXT_SIZE, AT_START_CONTEXT_SIZE); var jsonOfSymbolContext = mapperCopy.writeValueAsString(symbolContext); @@ -349,9 +352,14 @@ public abstract class SymbolTooltipPanel extends Panel { if (index < 0 || index >= symbolHits.size()) Session.get().warn("Unable to find most likely definition"); } catch (Exception e) { + var explicitException = ExceptionUtils.find(e, ExplicitException.class); + if (explicitException != null) { + Session.get().error(explicitException.getMessage()); + } else { + logger.error("Error inferring most likely symbol definition", e); + Session.get().error("Error inferring most likely symbol definition, check server log for details"); + } index = -1; - logger.error("Error inferring most likely symbol definition", e); - Session.get().error("Error inferring most likely symbol definition, check server log for details"); } var script = String.format("onedev.server.symboltooltip.doneInfer('%s', %d);", getMarkupId() + "-symbol-tooltip", index); diff --git a/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/create/NewPullRequestPage.html b/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/create/NewPullRequestPage.html index 6af51d9fa3..8f80ea3765 100644 --- a/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/create/NewPullRequestPage.html +++ b/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/create/NewPullRequestPage.html @@ -67,13 +67,13 @@
-
+
Prefix the title with WIP or [WIP] to mark the pull request as work in progress
-
+
diff --git a/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/create/NewPullRequestPage.java b/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/create/NewPullRequestPage.java index 859a745fb4..6e47421343 100644 --- a/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/create/NewPullRequestPage.java +++ b/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/create/NewPullRequestPage.java @@ -1,7 +1,11 @@ package io.onedev.server.web.page.project.pullrequests.create; +import static io.onedev.server.model.PullRequest.MAX_DESCRIPTION_LEN; +import static io.onedev.server.model.PullRequest.MAX_TITLE_LEN; import static io.onedev.server.search.commit.Revision.Type.COMMIT; import static io.onedev.server.web.translation.Translation._T; +import static org.apache.wicket.ajax.attributes.CallbackParameter.explicit; +import static org.unbescape.javascript.JavaScriptEscape.escapeJavaScript; import java.text.MessageFormat; import java.util.ArrayList; @@ -16,10 +20,11 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; -import org.jspecify.annotations.Nullable; +import javax.inject.Inject; import org.apache.wicket.Component; import org.apache.wicket.RestartResponseAtInterceptPageException; +import org.apache.wicket.Session; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.attributes.AjaxRequestAttributes; import org.apache.wicket.ajax.form.OnChangeAjaxBehavior; @@ -27,8 +32,9 @@ import org.apache.wicket.ajax.markup.html.AjaxLink; import org.apache.wicket.behavior.AttributeAppender; import org.apache.wicket.extensions.ajax.markup.html.AjaxLazyLoadPanel; import org.apache.wicket.feedback.FencedFeedbackPanel; -import org.apache.wicket.markup.head.CssHeaderItem; import org.apache.wicket.markup.head.IHeaderResponse; +import org.apache.wicket.markup.head.JavaScriptHeaderItem; +import org.apache.wicket.markup.head.OnLoadHeaderItem; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.Button; @@ -47,9 +53,17 @@ import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.revwalk.RevCommit; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Lists; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.UserMessage; +import io.onedev.commons.utils.ExplicitException; import io.onedev.commons.utils.PlanarRange; import io.onedev.server.OneDev; import io.onedev.server.attachment.AttachmentSupport; @@ -58,11 +72,7 @@ import io.onedev.server.codequality.CodeProblem; import io.onedev.server.codequality.CodeProblemContribution; import io.onedev.server.codequality.CoverageStatus; import io.onedev.server.codequality.LineCoverageContribution; -import io.onedev.server.service.CodeCommentService; -import io.onedev.server.service.CodeCommentReplyService; -import io.onedev.server.service.CodeCommentStatusChangeService; -import io.onedev.server.service.LabelSpecService; -import io.onedev.server.service.PullRequestService; +import io.onedev.server.exception.ExceptionUtils; import io.onedev.server.git.GitUtils; import io.onedev.server.git.service.GitService; import io.onedev.server.git.service.RefFacade; @@ -87,10 +97,17 @@ import io.onedev.server.search.commit.CommitQuery; import io.onedev.server.search.commit.Revision; import io.onedev.server.search.commit.RevisionCriteria; import io.onedev.server.security.SecurityUtils; +import io.onedev.server.service.CodeCommentReplyService; +import io.onedev.server.service.CodeCommentService; +import io.onedev.server.service.CodeCommentStatusChangeService; +import io.onedev.server.service.LabelSpecService; +import io.onedev.server.service.PullRequestService; +import io.onedev.server.service.SettingService; import io.onedev.server.util.Pair; import io.onedev.server.util.ProjectAndBranch; import io.onedev.server.util.diff.WhitespaceOption; import io.onedev.server.web.ajaxlistener.DisableGlobalAjaxIndicatorListener; +import io.onedev.server.web.behavior.AbstractPostAjaxBehavior; import io.onedev.server.web.behavior.ReferenceInputBehavior; import io.onedev.server.web.component.branch.BranchLink; import io.onedev.server.web.component.branch.picker.AffinalBranchPicker; @@ -119,10 +136,18 @@ import io.onedev.server.web.util.editbean.LabelsBean; public class NewPullRequestPage extends ProjectPage implements RevisionAnnotationSupport { + private static final Logger logger = LoggerFactory.getLogger(NewPullRequestPage.class); + private static final String TABS_ID = "tabs"; private static final String TAB_PANEL_ID = "tabPanel"; + @Inject + private SettingService settingService; + + @Inject + private ObjectMapper objectMapper; + private ProjectAndBranch target; private ProjectAndBranch source; @@ -262,7 +287,11 @@ public class NewPullRequestPage extends ProjectPage implements RevisionAnnotatio update.setHeadCommitHash(source.getObjectName()); update.setTargetHeadCommitHash(request.getTarget().getObjectName()); - request.generateTitleAndDescriptionIfEmpty(); + var title = request.generateTitleFromCommits(); + if (title == null && settingService.getAISetting().getLiteModelSetting() == null) + title = request.generateTitleFromBranch(); + request.setTitle(title); + request.setDescription(request.generateDescriptionFromCommits()); getPullRequestService().checkReviews(request, false); @@ -645,7 +674,140 @@ public class NewPullRequestPage extends ProjectPage implements RevisionAnnotatio } private Fragment newCanSendFrag() { - Fragment fragment = new Fragment("status", "canSendFrag", this); + var behavior = new AbstractPostAjaxBehavior() { + + @Override + protected void respond(AjaxRequestTarget target) { + var params = RequestCycle.get().getRequest().getPostParameters(); + var suggestTitle = params.getParameterValue("suggestTitle").toBoolean(); + var suggestDescription = params.getParameterValue("suggestDescription").toBoolean(); + + String title; + String description; + try { + if (suggestTitle || suggestDescription) { + var chatModel = settingService.getAISetting().getLiteModel(); + var sourceBranchSemantic = getPullRequest().getSourceBranchSemantic(); + String titleSuggestInstruction; + if (sourceBranchSemantic.isWorkInProgress()) { + if (sourceBranchSemantic.getWorkType() != null) { + titleSuggestInstruction = """ + When suggesting pull request title, you should not add work in progress prefix + or conventional commit type prefix to the title even if commit messages + indicate that. + """; + } else { + titleSuggestInstruction = """ + When suggesting pull request title, you should not add work in progress prefix + to the title even if commit messages indicate that. + """; + } + } else { + if (sourceBranchSemantic.getWorkType() != null) { + titleSuggestInstruction = """ + When suggesting pull request title, you should not add conventional commit type prefix + to the title even if commit messages indicate that. + """; + } else { + titleSuggestInstruction = ""; + } + } + + var userPrompt = getPullRequest().getLatestUpdate().getCommits().stream() + .map(it -> it.getFullMessage()) + .collect(Collectors.toList()); + var userMessage = new UserMessage("A json array of commit messages:\n" + objectMapper.writeValueAsString(userPrompt)); + if (!suggestTitle) { + var systemMessage = new SystemMessage(String.format(""" + You are a helpful assistant that can suggest pull request description by + summarizing multiple commit messages. Maximum %d characters allowed for + the description. + + IMPORTANT: only return the description, no other text or comments. + """, MAX_DESCRIPTION_LEN)); + description = chatModel.chat(systemMessage, userMessage).aiMessage().text(); + title = ""; + } else if (!suggestDescription) { + var systemMessage = new SystemMessage(String.format(""" + You are a helpful assistant that can suggest pull request title by summarizing + multiple commit messages. %s Maximum %d characters allowed for the title. + + IMPORTANT: only return the title, no other text or comments. + """, titleSuggestInstruction, MAX_TITLE_LEN)); + title = (getPullRequest().getTitlePrefix(sourceBranchSemantic) + chatModel.chat(systemMessage, userMessage).aiMessage().text()).trim(); + description = ""; + } else { + var systemMessage = new SystemMessage(String.format(""" + You are a helpful assistant that can suggest pull request title and description + by summarizing multiple commit messages. %s Maximum %d characters allowed for the title, + and %d characters allowed for the description. + + IMPORTANT: only return a VALID json object with "title" property set to the title and "description" + property set to the description, no other text or comments. + """, titleSuggestInstruction, MAX_TITLE_LEN, MAX_DESCRIPTION_LEN)); + var responseText = chatModel.chat(systemMessage, userMessage).aiMessage().text(); + if (responseText.startsWith("```json")) + responseText = responseText.substring("```json".length()); + if (responseText.endsWith("```")) + responseText = responseText.substring(0, responseText.length() - "```".length()); + var response = objectMapper.readTree(responseText); + if (response.has("title")) { + title = (getPullRequest().getTitlePrefix(sourceBranchSemantic) + response.get("title").asText()).trim(); + } else { + title = ""; + } + if (response.has("description")) + description = response.get("description").asText(); + else + description = ""; + } + } else { + title = ""; + description = ""; + } + } catch (Exception e) { + title = ""; + description = ""; + var explicitException = ExceptionUtils.find(e, ExplicitException.class); + if (explicitException != null) { + Session.get().error(explicitException.getMessage()); + } else { + logger.error("Error suggesting title/description", e); + Session.get().error("Error suggesting title/description, check server log for details"); + } + } + var script = String.format("onedev.server.newPullRequest.titleAndDescriptionSuggested('%s', '%s');", + escapeJavaScript(title), escapeJavaScript(description)); + target.appendJavaScript(script); + } + + }; + Fragment fragment = new Fragment("status", "canSendFrag", this) { + + @Override + public void renderHead(IHeaderResponse response) { + super.renderHead(response); + + CharSequence callback; + if (settingService.getAISetting().getLiteModelSetting() != null) + callback = behavior.getCallbackFunction(explicit("suggestTitle"), explicit("suggestDescription")); + else + callback = "undefined"; + var translations = new HashMap(); + translations.put("suggesting-title", _T("Suggesting title...")); + translations.put("suggesting-description", _T("Suggesting description...")); + try { + var script = String.format("onedev.server.newPullRequest.onCanSendLoad(%s, %s);", + callback, objectMapper.writeValueAsString(translations)); + response.render(OnLoadHeaderItem.forScript(script)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + }; + fragment.add(behavior); + Form form = new Form("form"); fragment.add(form); @@ -763,7 +925,7 @@ public class NewPullRequestPage extends ProjectPage implements RevisionAnnotatio }; descriptionInput.add(validatable -> { - if (validatable.getValue().length() > PullRequest.MAX_DESCRIPTION_LEN) { + if (validatable.getValue().length() > MAX_DESCRIPTION_LEN) { validatable.error(messageSource -> _T("Description too long")); } }); @@ -943,7 +1105,7 @@ public class NewPullRequestPage extends ProjectPage implements RevisionAnnotatio @Override public void renderHead(IHeaderResponse response) { super.renderHead(response); - response.render(CssHeaderItem.forReference(new NewPullRequestCssResourceReference())); + response.render(JavaScriptHeaderItem.forReference(new NewPullRequestResourceReference())); } @Override diff --git a/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/create/NewPullRequestCssResourceReference.java b/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/create/NewPullRequestResourceReference.java similarity index 60% rename from server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/create/NewPullRequestCssResourceReference.java rename to server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/create/NewPullRequestResourceReference.java index a4f069f55a..7be4201ab6 100644 --- a/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/create/NewPullRequestCssResourceReference.java +++ b/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/create/NewPullRequestResourceReference.java @@ -7,18 +7,21 @@ import org.apache.wicket.markup.head.HeaderItem; import io.onedev.server.web.asset.revisioncompare.RevisionCompareCssResourceReference; import io.onedev.server.web.page.base.BaseDependentCssResourceReference; +import io.onedev.server.web.page.base.BaseDependentResourceReference; -public class NewPullRequestCssResourceReference extends BaseDependentCssResourceReference { +public class NewPullRequestResourceReference extends BaseDependentResourceReference { private static final long serialVersionUID = 1L; - public NewPullRequestCssResourceReference() { - super(NewPullRequestCssResourceReference.class, "new-pull-request.css"); + public NewPullRequestResourceReference() { + super(NewPullRequestResourceReference.class, "new-pull-request.js"); } @Override public List getDependencies() { List dependencies = super.getDependencies(); + dependencies.add(CssHeaderItem.forReference(new BaseDependentCssResourceReference( + NewPullRequestResourceReference.class, "new-pull-request.css"))); dependencies.add(CssHeaderItem.forReference(new RevisionCompareCssResourceReference())); return dependencies; } diff --git a/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/create/new-pull-request.js b/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/create/new-pull-request.js new file mode 100644 index 0000000000..3951eafaa9 --- /dev/null +++ b/server-core/src/main/java/io/onedev/server/web/page/project/pullrequests/create/new-pull-request.js @@ -0,0 +1,83 @@ +onedev.server.newPullRequest = { + onCanSendLoad: function(titleAndDescriptionSuggestionCallback, translations) { + // Do below logic in onLoad as unsaved description will be set in onLoad event of markdown editor, + // and we do not want to suggest description if unsaved description is loaded + if (titleAndDescriptionSuggestionCallback) { + var icon = onedev.server.isDarkMode()? "sparkle.gif": "sparkle-dark.gif"; + var indicatorHtml = "
"; + + var $title = $(".pull-request-title"); + var $titleInput = $title.find("input"); + if ($titleInput.val().length == 0) { + $title.css("position", "relative"); + $titleInput.prop("readonly", true); + $titleInput.data("placeholder", $titleInput.attr("placeholder")); + $titleInput.removeAttr("placeholder"); + + var $indicator = $(indicatorHtml); + $indicator.append("" + translations["suggesting-title"] + ""); + + var titleCoord = $title.offset(); + var titleInputCoord = $titleInput.offset(); + var left = titleInputCoord.left - titleCoord.left + 5; + var top = titleInputCoord.top - titleCoord.top + 10; + + $indicator.css({ + position: "absolute", + left: left + "px", + top: top + "px", + zIndex: 1000 + }); + $title.append($indicator); + } + + var $description = $(".pull-request-description"); + var $descriptionInput = $description.find("textarea"); + if ($descriptionInput.val().length == 0) { + $description.css("position", "relative"); + $descriptionInput.prop("readonly", true); + + $indicator = $(indicatorHtml); + $indicator.append("" + translations["suggesting-description"] + ""); + + var descriptionCoord = $description.offset(); + var descriptionInputCoord = $descriptionInput.offset(); + var left = descriptionInputCoord.left - descriptionCoord.left + 5; + var top = descriptionInputCoord.top - descriptionCoord.top + 10; + + $indicator.css({ + position: "absolute", + left: left + "px", + top: top + "px", + zIndex: 1000 + }); + $description.append($indicator); + } + + titleAndDescriptionSuggestionCallback($titleInput.val().length == 0, $descriptionInput.val().length == 0); + } + }, + titleAndDescriptionSuggested: function(title, description) { + var $title = $(".pull-request-title"); + var $titleInput = $title.find("input"); + $titleInput.prop("readonly", false); + $title.children(".suggesting-indicator").remove(); + var placeholder = $titleInput.data("placeholder"); + if (placeholder) + $titleInput.attr("placeholder", placeholder); + + if (title) + $titleInput.val(title); + + var $description = $(".pull-request-description"); + var $descriptionInput = $description.find("textarea"); + $descriptionInput.prop("readonly", false); + $description.children(".suggesting-indicator").remove(); + + if (description) { + $descriptionInput.val(description); + $descriptionInput.trigger("input"); + } + } + +} \ No newline at end of file