mirror of
https://github.com/theonedev/onedev.git
synced 2025-12-10 00:07:37 -06:00
feat: Suggest pull request title/description via AI (OD-2602)
This commit is contained in:
parent
a62db8265f
commit
706b2dbdba
@ -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);
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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("~");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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<Project> 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("~");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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<Project> 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("~");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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<Project> 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("~");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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<Project> 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
|
||||
|
||||
@ -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<Project> 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("~");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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("~");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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<Project> 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
|
||||
|
||||
@ -12,31 +12,31 @@ import io.onedev.server.OneDev;
|
||||
public class JavascriptTranslations {
|
||||
|
||||
static String get() {
|
||||
try {
|
||||
var map = new HashMap<String, String>();
|
||||
|
||||
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<String, String>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -17,9 +17,9 @@ public class SymbolContext implements Serializable {
|
||||
|
||||
private final List<String> linesAtStart;
|
||||
|
||||
public SymbolContext(String fileName, String symbolLine,
|
||||
public SymbolContext(String blobPath, String symbolLine,
|
||||
List<String> linesBeforeSymbolLine, List<String> linesAfterSymbolLine, List<String> linesAtStart) {
|
||||
this.blobPath = fileName;
|
||||
this.blobPath = blobPath;
|
||||
this.symbolLine = symbolLine;
|
||||
this.linesBeforeSymbolLine = linesBeforeSymbolLine;
|
||||
this.linesAfterSymbolLine = linesAfterSymbolLine;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -67,13 +67,13 @@
|
||||
<div class="card can-send mb-5">
|
||||
<div class="card-body">
|
||||
<form wicket:id="form" class="leave-confirm">
|
||||
<div class="form-group">
|
||||
<div class="form-group pull-request-title">
|
||||
<label><wicket:t>Title</wicket:t> <span class="text-danger">*</span></label>
|
||||
<input wicket:id="title" type="text" class="form-control">
|
||||
<div wicket:id="titleFeedback"></div>
|
||||
<div class="text-muted form-text"><wicket:t>Prefix the title with <code>WIP</code> or <code>[WIP]</code> to mark the pull request as work in progress</wicket:t></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group pull-request-description">
|
||||
<label><wicket:t>Description</wicket:t></label>
|
||||
<div wicket:id="description"></div>
|
||||
<div wicket:id="descriptionFeedback"></div>
|
||||
|
||||
@ -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<String, String>();
|
||||
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<Void>("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
|
||||
|
||||
@ -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<HeaderItem> getDependencies() {
|
||||
List<HeaderItem> dependencies = super.getDependencies();
|
||||
dependencies.add(CssHeaderItem.forReference(new BaseDependentCssResourceReference(
|
||||
NewPullRequestResourceReference.class, "new-pull-request.css")));
|
||||
dependencies.add(CssHeaderItem.forReference(new RevisionCompareCssResourceReference()));
|
||||
return dependencies;
|
||||
}
|
||||
@ -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 = "<div class='ajax-loading-indicator suggesting-indicator'><img src='/~img/" + icon + "' width='16' height='16'></div>";
|
||||
|
||||
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("<span class='text-muted'>" + translations["suggesting-title"] + "</span>");
|
||||
|
||||
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("<span class='text-muted'>" + translations["suggesting-description"] + "</span>");
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user