feat: Use natural language for various queries (OD-2600)

This commit is contained in:
Robin Shen 2025-11-15 22:36:02 +08:00
parent 706b2dbdba
commit c6a74dc008
16 changed files with 939 additions and 304 deletions

View File

@ -1,6 +1,7 @@
package io.onedev.server.ai;
import static io.onedev.server.ai.QueryDescriptions.getBuildQueryDescription;
import static io.onedev.server.ai.QueryDescriptions.getPackQueryDescription;
import static javax.ws.rs.core.Response.Status.NOT_ACCEPTABLE;
import static org.unbescape.html.HtmlEscape.escapeHtml5;
@ -66,6 +67,7 @@ import io.onedev.server.model.IssueSchedule;
import io.onedev.server.model.IssueWork;
import io.onedev.server.model.Iteration;
import io.onedev.server.model.LabelSpec;
import io.onedev.server.model.Pack;
import io.onedev.server.model.Project;
import io.onedev.server.model.PullRequest;
import io.onedev.server.model.PullRequestAssignment;
@ -95,6 +97,7 @@ import io.onedev.server.search.entity.EntityQuery;
import io.onedev.server.search.entity.build.BuildQuery;
import io.onedev.server.search.entity.issue.IssueQuery;
import io.onedev.server.search.entity.issue.IssueQueryParseOption;
import io.onedev.server.search.entity.pack.PackQuery;
import io.onedev.server.search.entity.pullrequest.PullRequestQuery;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.service.BuildParamService;
@ -107,6 +110,7 @@ import io.onedev.server.service.IssueWorkService;
import io.onedev.server.service.IterationService;
import io.onedev.server.service.LabelSpecService;
import io.onedev.server.service.LinkSpecService;
import io.onedev.server.service.PackService;
import io.onedev.server.service.ProjectService;
import io.onedev.server.service.PullRequestAssignmentService;
import io.onedev.server.service.PullRequestChangeService;
@ -166,6 +170,9 @@ public class McpHelperResource {
private final BuildService buildService;
@Inject
private PackService packService;
private final JobService jobService;
private final GitService gitService;
@ -623,6 +630,28 @@ public class McpHelperResource {
inputSchemas.put("editPullRequest", editPullRequestInputSchema);
var queryPacksInputSchema = new HashMap<String, Object>();
var queryPacksProperties = new HashMap<String, Object>();
queryPacksProperties.put("project", Map.of(
"type", "string",
"description", "Project to query packages in. Leave empty to query in current project"));
queryPacksProperties.put("query", Map.of(
"type", "string",
"description", escapeHtml5(getPackQueryDescription())));
queryPacksProperties.put("offset", Map.of(
"type", "integer",
"description", "start position for the query (optional, defaults to 0)"));
queryPacksProperties.put("count", Map.of(
"type", "integer",
"description", "number of packages to return (optional, defaults to 25, max 100)"));
queryPacksInputSchema.put("Type", "object");
queryPacksInputSchema.put("Properties", queryPacksProperties);
queryPacksInputSchema.put("Required", new ArrayList<>());
inputSchemas.put("queryPacks", queryPacksInputSchema);
return inputSchemas;
}
@ -1946,6 +1975,53 @@ public class McpHelperResource {
}
}
@Path("/query-packs")
@GET
public List<Map<String, Object>> queryPacks(
@QueryParam("currentProject") @NotNull String currentProjectPath,
@QueryParam("project") String projectPath,
@QueryParam("query") String query,
@QueryParam("offset") int offset,
@QueryParam("count") int count) {
var subject = SecurityUtils.getSubject();
if (SecurityUtils.getUser(subject) == null)
throw new UnauthenticatedException();
var projectInfo = getProjectInfo(projectPath, currentProjectPath);
if (count > RestConstants.MAX_PAGE_SIZE)
throw new NotAcceptableException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
EntityQuery<Pack> parsedQuery;
if (query != null) {
parsedQuery = PackQuery.parse(projectInfo.project, query, true);
} else {
parsedQuery = new PackQuery();
}
var packs = new ArrayList<Map<String, Object>>();
for (var pack : packService.query(subject, projectInfo.project, parsedQuery, false, offset, count)) {
var packMap = getPackMap(projectInfo.currentProject, pack);
packMap.put("link", urlService.urlFor(pack, true));
packs.add(packMap);
}
return packs;
}
private Map<String, Object> getPackMap(Project currentProject, Pack pack) {
var typeReference = new TypeReference<LinkedHashMap<String, Object>>() {};
var packMap = objectMapper.convertValue(pack, typeReference);
packMap.remove("id");
packMap.remove("userId");
packMap.put("user", pack.getUser().getName());
packMap.remove("buildId");
if (pack.getBuild() != null)
packMap.put("build", pack.getBuild().getReference().toString(currentProject));
packMap.put("project", pack.getProject().getPath());
return packMap;
}
private void normalizePullRequestData(Map<String, Serializable> data) {
for (var entry : data.entrySet()) {
if (entry.getValue() instanceof String)

View File

@ -1,29 +1,63 @@
package io.onedev.server.ai;
import static java.util.stream.Collectors.joining;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.List;
import java.util.stream.IntStream;
import io.onedev.server.OneDev;
import io.onedev.server.model.Agent;
import io.onedev.server.model.Build;
import io.onedev.server.model.Issue;
import io.onedev.server.model.LabelSpec;
import io.onedev.server.model.Pack;
import io.onedev.server.model.Project;
import io.onedev.server.model.PullRequest;
import io.onedev.server.model.support.issue.field.spec.BooleanField;
import io.onedev.server.model.support.issue.field.spec.BuildChoiceField;
import io.onedev.server.model.support.issue.field.spec.CommitField;
import io.onedev.server.model.support.issue.field.spec.DateField;
import io.onedev.server.model.support.issue.field.spec.DateTimeField;
import io.onedev.server.model.support.issue.field.spec.FloatField;
import io.onedev.server.model.support.issue.field.spec.GroupChoiceField;
import io.onedev.server.model.support.issue.field.spec.IntegerField;
import io.onedev.server.model.support.issue.field.spec.IssueChoiceField;
import io.onedev.server.model.support.issue.field.spec.IterationChoiceField;
import io.onedev.server.model.support.issue.field.spec.PullRequestChoiceField;
import io.onedev.server.model.support.issue.field.spec.TextField;
import io.onedev.server.model.support.issue.field.spec.choicefield.ChoiceField;
import io.onedev.server.model.support.issue.field.spec.userchoicefield.UserChoiceField;
import io.onedev.server.model.support.pullrequest.MergeStrategy;
import io.onedev.server.service.BuildParamService;
import io.onedev.server.service.BuildService;
import io.onedev.server.pack.PackSupport;
import io.onedev.server.service.AgentAttributeService;
import io.onedev.server.service.AgentService;
import io.onedev.server.service.LabelSpecService;
import io.onedev.server.service.LinkSpecService;
import io.onedev.server.service.SettingService;
public class QueryDescriptions {
private static String REACTION_CRITERIAS = """
| '"Reaction: Thumbs Up Count"' 'is' ('not')? '"'Number'"'
| '"Reaction: Thumbs Up Count"' 'is' ('greater'|'less') 'than' '"'Number'"'
| '"Reaction: Thumbs Down Count"' 'is' ('not')? '"'Number'"'
| '"Reaction: Thumbs Down Count"' 'is' ('greater'|'less') 'than' '"'Number'"'
| '"Reaction: Smile Count"' 'is' ('not')? '"'Number'"'
| '"Reaction: Smile Count"' 'is' ('greater'|'less') 'than' '"'Number'"'
| '"Reaction: Tada Count"' 'is' ('not')? '"'Number'"'
| '"Reaction: Tada Count"' 'is' ('greater'|'less') 'than' '"'Number'"'
| '"Reaction: Confused Count"' 'is' ('not')? '"'Number'"'
| '"Reaction: Confused Count"' 'is' ('greater'|'less') 'than' '"'Number'"'
| '"Reaction: Heart Count"' 'is' ('not')? '"'Number'"'
| '"Reaction: Heart Count"' 'is' ('greater'|'less') 'than' '"'Number'"'
| '"Reaction: Rocket Count"' 'is' ('not')? '"'Number'"'
| '"Reaction: Rocket Count"' 'is' ('greater'|'less') 'than' '"'Number'"'
| '"Reaction: Eyes Count"' 'is' ('not')? '"'Number'"'
| '"Reaction: Eyes Count"' 'is' ('greater'|'less') 'than' '"'Number'"'
""".trim();
private static SettingService getSettingService() {
return OneDev.getInstance(SettingService.class);
}
@ -31,270 +65,575 @@ public class QueryDescriptions {
private static LinkSpecService getLinkSpecService() {
return OneDev.getInstance(LinkSpecService.class);
}
private static LabelSpecService getLabelSpecService() {
return OneDev.getInstance(LabelSpecService.class);
}
private static BuildService getBuildService() {
return OneDev.getInstance(BuildService.class);
}
private static BuildParamService getBuildParamService() {
return OneDev.getInstance(BuildParamService.class);
}
public static String getIssueQueryDescription() {
var settingService = getSettingService();
var linkSpecService = getLinkSpecService();
var stateNames = new StringBuilder();
for (var state: settingService.getIssueSetting().getStateSpecs()) {
stateNames.append(" - ");
stateNames.append(state.getName());
if (state.getDescription() != null) {
stateNames.append(": ").append(state.getDescription().replace("\n", " "));
}
stateNames.append("\n");
}
var fieldCriterias = new StringBuilder();
var fieldCriterias = new ArrayList<String>();
var choiceFieldValueRules = new ArrayList<String>();
int choiceFieldValueRuleIndex = 0;
for (var field: settingService.getIssueSetting().getFieldSpecs()) {
if (field instanceof ChoiceField) {
var choiceField = (ChoiceField) field;
fieldCriterias.append("- " + field.getName().toLowerCase() + " criteria in form of: \""
+ field.getName() + "\" is \"<" + field.getName().toLowerCase()
+ " value>\" (quotes are required), where <" + field.getName().toLowerCase()
+ " value> is one of below:\n");
for (var choice : choiceField.getPossibleValues())
fieldCriterias.append(" - " + choice).append("\n");
if (field instanceof ChoiceField) {
var choiceField = (ChoiceField) field;
var fieldValueRuleName = "ChoiceFieldValue" + (choiceFieldValueRuleIndex++);
fieldCriterias.add("'\"" + field.getName() + "\"' 'is' ('not')? '\"'" + fieldValueRuleName + "'\"'");
choiceFieldValueRules.add(choiceField.getPossibleValues().stream().map(it->"'" + it.replace("'", "\\'") + "'").collect(joining("\n | ")));
} else if (field instanceof UserChoiceField) {
fieldCriterias.append("- " + field.getName().toLowerCase() + " criteria in form of: \""
+ field.getName() + "\" is \"<login name of a user>\" (quotes are required)\n");
fieldCriterias.append(
"- " + field.getName().toLowerCase() + " criteria for current user in form of: \""
+ field.getName() + "\" is me (quotes are required)\n");
fieldCriterias.add("'\"" + field.getName() + "\"' 'is' ('not')? '\"'LoginNameOfUser'\"'");
fieldCriterias.add("'\"" + field.getName() + "\"' 'is' ('not')? 'me'");
} else if (field instanceof GroupChoiceField) {
fieldCriterias.append("- " + field.getName().toLowerCase() + " criteria in form of: \""
+ field.getName() + "\" is \"<group name>\" (quotes are required)\n");
fieldCriterias.add("'\"" + field.getName() + "\"' 'is' ('not')? '\"'GroupName'\"'");
} else if (field instanceof BooleanField) {
fieldCriterias.append("- " + field.getName().toLowerCase() + " is true criteria in form of: \""
+ field.getName() + "\" is \"true\" (quotes are required)\n");
fieldCriterias.append("- " + field.getName().toLowerCase() + " is false criteria in form of: \""
+ field.getName() + "\" is \"false\" (quotes are required)\n");
fieldCriterias.add("'\"" + field.getName() + "\"' 'is' ('not')? '\"'('true'|'false')'\"'");
} else if (field instanceof DateField) {
fieldCriterias.append("- " + field.getName().toLowerCase()
+ " is before certain date criteria in form of: \"" + field.getName()
+ "\" is before \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD\n");
fieldCriterias.append("- " + field.getName().toLowerCase()
+ " is after certain date criteria in form of: \"" + field.getName()
+ "\" is after \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD\n");
fieldCriterias.add("'\"" + field.getName() + "\"' 'is' ('before'|'after') '\"'DateDescription'\"'");
} else if (field instanceof DateTimeField) {
fieldCriterias.append("- " + field.getName().toLowerCase()
+ " is before certain date time criteria in form of: \"" + field.getName()
+ "\" is before \"<date time>\" (quotes are required), where <date time> is of format YYYY-MM-DD HH:mm\n");
fieldCriterias.append("- " + field.getName().toLowerCase()
+ " is after certain date time criteria in form of: \"" + field.getName()
+ "\" is after \"<date time>\" (quotes are required), where <date time> is of format YYYY-MM-DD HH:mm\n");
fieldCriterias.add("'\"" + field.getName() + "\"' 'is' ('before'|'after') '\"'DateTimeDescription'\"'");
} else if (field instanceof IntegerField) {
fieldCriterias.append("- " + field.getName().toLowerCase()
+ " is equal to certain integer criteria in form of: \"" + field.getName()
+ "\" is \"<integer>\" (quotes are required), where <integer> is an integer\n");
fieldCriterias.append("- " + field.getName().toLowerCase()
+ " is greater than certain integer criteria in form of: \"" + field.getName()
+ "\" is greater than \"<integer>\" (quotes are required), where <integer> is an integer\n");
fieldCriterias.append("- " + field.getName().toLowerCase()
+ " is less than certain integer criteria in form of: \"" + field.getName()
+ "\" is less than \"<integer>\" (quotes are required), where <integer> is an integer\n");
fieldCriterias.add("'\"" + field.getName() + "\"' 'is' ('not')? '\"'Integer'\"'");
fieldCriterias.add("'\"" + field.getName() + "\"' 'is' ('greater'|'less') 'than' '\"'Integer'\"'");
} else if (field instanceof FloatField) {
fieldCriterias.add("'\"" + field.getName() + "\"' 'is' ('greater'|'less') 'than' '\"'Float'\"'");
} else if (field instanceof TextField) {
fieldCriterias.add("'\"" + field.getName() + "\"' 'is' ('not')? '\"'Text'\"'");
fieldCriterias.add("'\"" + field.getName() + "\"' 'contains' '\"'Text'\"'");
} else if (field instanceof PullRequestChoiceField || field instanceof BuildChoiceField || field instanceof IssueChoiceField) {
fieldCriterias.add("'\"" + field.getName() + "\"' 'is' ('not')? '\"'EntityReference'\"'");
} else if (field instanceof CommitField) {
fieldCriterias.add("'\"" + field.getName() + "\"' 'is' ('not')? '\"'CommitReference'\"'");
} else if (field instanceof IterationChoiceField) {
fieldCriterias.add("'\"" + field.getName() + "\"' 'is' ('not')? '\"'IterationNameOrPattern'\"'");
}
fieldCriterias.append("- " + field.getName().toLowerCase() + " is not set criteria in form of: \""
+ field.getName() + "\" is empty (quotes are required)\n");
}
var linkCriterias = new StringBuilder();
for (var linkSpec: linkSpecService.query()) {
linkCriterias.append("- criteria to list issues with any " + linkSpec.getName().toLowerCase()
+ " matching certain criteria in form of: any \"" + linkSpec.getName()
+ "\" matching(another criteria) (quotes are required)\n");
linkCriterias.append("- criteria to list issues with all " + linkSpec.getName().toLowerCase()
+ " matching certain criteria in form of: all \"" + linkSpec.getName()
+ "\" matching(another criteria) (quotes are required)\n");
linkCriterias.append("- criteria to list issues with some " + linkSpec.getName().toLowerCase()
+ " in form of: has any \"" + linkSpec.getName() + "\" (quotes are required)\n");
if (linkSpec.getOpposite() != null) {
linkCriterias.append("- criteria to list issues with any "
+ linkSpec.getOpposite().getName().toLowerCase()
+ " matching certain criteria in form of: any \"" + linkSpec.getOpposite().getName()
+ "\" matching(another criteria) (quotes are required)\n");
linkCriterias.append("- criteria to list issues with all "
+ linkSpec.getOpposite().getName().toLowerCase()
+ " matching certain criteria in form of: all \"" + linkSpec.getOpposite().getName()
+ "\" matching(another criteria) (quotes are required)\n");
linkCriterias.append("- criteria to list issues with some " + linkSpec.getOpposite().getName().toLowerCase()
+ " in form of: has any \"" + linkSpec.getOpposite().getName() + "\" (quotes are required)\n");
}
}
var orderFields = new StringBuilder();
for (var field: Issue.SORT_FIELDS.keySet()) {
orderFields.append("- ").append(field).append("\n");
fieldCriterias.add("'\"" + field.getName() + "\"' 'is' ('not')? 'empty'");
}
var description =
"A query string is one of below criteria:\n\n" +
"- issue with specified number in form of: \"Number\" is \"#<issue number>\", or in form of: \"Number\" is \"<project key>-<issue number>\" (quotes are required)\n" +
"- state criteria in form of: \"State\" is \"<state name>\" (quotes are required), where <state name> is one of below:\n" +
stateNames +
fieldCriterias +
linkCriterias +
"- submitted by specified user criteria in form of: submitted by \"<login name of a user>\" (quotes are required)\n" +
"- submitted by current user criteria in form of: submitted by me (quotes are required)\n" +
"- submitted before certain date criteria in form of: \"Submit Date\" is until \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
"- submitted after certain date criteria in form of: \"Submit Date\" is since \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
"- updated before certain date criteria in form of: \"Last Activity Date\" is until \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
"- updated after certain date criteria in form of: \"Last Activity Date\" is since \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
"- confidential criteria in form of: confidential\n" +
"- iteration criteria in form of: \"Iteration\" is \"<iteration name>\" (quotes are required)\n" +
"- title contains specified text criteria in form of: \"Title\" contains \"<containing text>\" (quotes are required)\n" +
"- description contains specified text criteria in form of: \"Description\" contains \"<containing text>\" (quotes are required)\n" +
"- comment contains specified text criteria in form of: \"Comment\" contains \"<containing text>\" (quotes are required)\n" +
"- project criteria in form of: \"Project\" is \"<project path pattern>\" (quotes are required)\n" +
"- and criteria in form of <criteria1> and <criteria2>\n" +
"- or criteria in form of <criteria1> or <criteria2>\n" +
"- operator 'and' takes precedence over 'or' when used together, unless parentheses are used to group 'or' criterias\n" +
"- not criteria in form of not(<criteria>)\n" +
"\n" +
"And can optionally add order clause at end of query string in form of: order by \"<field1>\" <asc|desc>,\"<field2>\" <asc|desc>,... (quotes are required), where <field> is one of below:\n\n" +
orderFields + "\n" +
"Issue, build or pull request can be referenced by their number in form of: #<number>, <project path>#<number> or <project key>-<number>\n" +
"\n" +
"Leave empty to list all accessible issues";
var linkCriterias = new ArrayList<String>();
for (var linkSpec: linkSpecService.query()) {
linkCriterias.add("'any' '\"" + linkSpec.getName() + "\"' 'matching' '('criteria')'");
linkCriterias.add("'all' '\"" + linkSpec.getName() + "\"' 'matching' '('criteria')'");
linkCriterias.add("'has any' '\"" + linkSpec.getName() + "\"'");
if (linkSpec.getOpposite() != null) {
linkCriterias.add("'any' '\"" + linkSpec.getOpposite().getName() + "\"' 'matching' '('criteria')'");
linkCriterias.add("'all' '\"" + linkSpec.getOpposite().getName() + "\"' 'matching' '('criteria')'");
linkCriterias.add("'has any' '\"" + linkSpec.getOpposite().getName() + "\"'");
}
}
var description = String.format("""
A structured query should conform to below ANTLR grammar:
issueQuery
: criteria ('order by' '"'OrderField'"' ('asc'|'desc') (',' '"'OrderField'"' ('asc'|'desc'))*)?
;
criteria
: '"Number"' 'is' ('not')? '"'EntityReference'"'
| '"Number"' 'is' ('greater'|'less') 'than' '"'EntityReference'"'
| '"State"' 'is' ('not')? '"'StateName'"'
| '"State"' 'is' ('after'|'before') '"'StateName'"'
%s
%s
| 'submitted by' '"'LoginNameOfUser'"'
| 'submitted by me'
| 'watched by' '"'LoginNameOfUser'"'
| 'watched by me'
| 'ignored by' '"'LoginNameOfUser'"'
| 'ignored by me'
| 'commented by' '"'LoginNameOfUser'"'
| 'commented by me'
| 'mentioned' '"'LoginNameOfUser'"'
| 'mentioned me'
| 'fixed in commit' '"'CommitReference'"'
| 'fixed in current commit'
| 'fixed in build' '"'EntityReference'"'
| 'fixed in current build'
| 'fixed in pull request' '"'EntityReference'"'
| 'fixed in current pull request'
| 'fixed between' revision 'and' revision
| '"Submit Date"' 'is' ('until'|'since') '"'DateDescription'"'
| '"Last Activity Date"' 'is' ('until'|'since') '"'DateDescription'"'
| 'confidential'
| '"Spent Time"' 'is' ('greater'|'less') 'than' '"'TimePeriodDescription'"'
| '"Spent Time"' 'is' ('not')? '"'TimePeriodDescription'"'
| '"Estimated Time"' 'is' ('greater'|'less') 'than' '"'TimePeriodDescription'"'
| '"Estimated Time"' 'is' ('not')? '"'TimePeriodDescription'"'
| '"Progress"' 'is' ('greater'|'less') 'than' '"'Float'"'
| '"Iteration"' 'is' ('not')? '"'IterationNameOrPattern'"'
| '"Iteration"' 'is' ('not')? 'empty'
| '"Title"' 'contains' '"'Text'"'
| '"Description"' 'contains' '"'Text'"'
| '"Comment"' 'contains' '"'Text'"'
| '"Comment Count"' 'is' ('not')? '"'Number'"'
| '"Comment Count"' 'is' ('greater'|'less') 'than' '"'Number'"'
| '"Vote Count"' 'is' ('not')? '"'Number'"'
| '"Vote Count"' 'is' ('greater'|'less') 'than' '"'Number'"'
%s
| '"Project"' 'is current'
| '"Project"' 'is' ('not')? '"'ProjectPathOrPattern'"'
| criteria 'and' criteria
| criteria 'or' criteria
| 'not('criteria')'
| '('criteria')'
;
StateName
: %s
;
%s
revision
: 'commit' '"'CommitReference'"'
| 'build' '"'EntityReference'"'
| 'branch' '"'BranchReference'"'
| 'tag' '"'TagReference'"'
;
EntityReference
: '#'Number
| ProjectPath'#'Number
| ProjectKey'-'Number
;
CommitReference
: (ProjectPath':')?CommitHash
;
BranchReference
: (ProjectPath':')?BranchName
;
TagReference
: (ProjectPath':')?TagName
;
OrderField
: %s
;
WS
: [ ]+ -> skip
;
Please note:
1. "LoginNameOfUser" should be retrieved via tool 'getLoginName' if available, with parameter set to user name
2. Use an empty query to list all accessible issues""",
fieldCriterias.stream().map(it->" | " + it + "\n").collect(joining("")).trim(),
linkCriterias.stream().map(it->" | " + it + "\n").collect(joining("")).trim(),
REACTION_CRITERIAS,
settingService.getIssueSetting().getStateSpecs().stream().map(it->"'" + it.getName() + "'").collect(joining("\n | ")).trim(),
IntStream.range(0, choiceFieldValueRules.size()).mapToObj(i -> "ChoiceFieldValue" + i + "\n : " + choiceFieldValueRules.get(i) + "\n ;\n\n").collect(joining("")).trim(),
Issue.SORT_FIELDS.keySet().stream().map(it->"'" + it + "'").collect(joining("\n | ")).trim());
return description;
}
public static String getPullRequestQueryDescription() {
var labelSpecService = getLabelSpecService();
var orderFields = new StringBuilder();
for (var field : PullRequest.SORT_FIELDS.keySet()) {
orderFields.append("- ").append(field).append("\n");
}
var description = String.format("""
A structured query should conform to below ANTLR grammar:
var labelNames = labelSpecService.query().stream().map(LabelSpec::getName).collect(Collectors.joining(", "));
var mergeStrategyNames = Arrays.stream(MergeStrategy.values()).map(MergeStrategy::name).collect(Collectors.joining(", "));
pullRequestQuery
: criteria ('order by' '"'OrderField'"' ('asc'|'desc') (',' '"'OrderField'"' ('asc'|'desc'))*)?
;
var description =
"A query string is one of below criteria:\n\n" +
"- pull request with specified number in form of: \"Number\" is \"#<pull request number>\", or in form of: \"Number\" is \"<project key>-<pull request number>\" (quotes are required)\n" +
"- open criteria in form of: open\n" +
"- merged criteria in form of: merged\n" +
"- discarded criteria in form of: discarded\n" +
"- source branch criteria in form of: \"Source Branch\" is \"<branch name>\" (quotes are required)\n" +
"- target branch criteria in form of: \"Target Branch\" is \"<branch name>\" (quotes are required)\n" +
"- merge strategy criteria in form of: \"Merge Strategy\" is \"<merge strategy>\" (quotes are required), where <merge strategy> is one of: " + mergeStrategyNames + "\n" +
"- label criteria in form of: \"Label\" is \"<label name>\" (quotes are required), where <label name> is one of: " + labelNames + "\n" +
"- ready to merge criteria in form of: ready to merge\n" +
"- waiting for someone to review criteria in form of: has pending reviews\n" +
"- some builds are unsuccessful criteria in form of: has unsuccessful builds\n" +
"- some builds are not finished criteria in form of: has unfinished builds\n" +
"- has merge conflicts criteria in form of: has merge conflicts\n" +
"- assigned to specified user criteria in form of: assigned to \"<login name of a user>\" (quotes are required)\n" +
"- approved by specified user criteria in form of: approved by \"<login name of a user>\" (quotes are required)\n" +
"- to be reviewed by specified user criteria in form of: to be reviewed by \"<login name of a user>\" (quotes are required)\n" +
"- to be changed by specified user criteria in form of: to be changed by \"<login name of a user>\" (quotes are required)\n" +
"- to be merged by specified user criteria in form of: to be merged by \"<login name of a user>\" (quotes are required)\n" +
"- requested for changes by specified user in form of: requested for changes by \"<login name of a user>\" (quotes are required)\n" +
"- need action of specified user criteria in form of: need action by \"<login name of a user>\" (quotes are required)\n" +
"- assigned to current user criteria in form of: assigned to me\n" +
"- approved by current user criteria in form of: approved by me\n" +
"- to be reviewed by current user criteria in form of: to be reviewed by me\n" +
"- to be changed by current user criteria in form of: to be changed by me\n" +
"- to be merged by current user criteria in form of: to be merged by me\n" +
"- requested for changes by current user in form of: requested for changes by me\n" +
"- requested for changes by any user criteria in form of: someone requested for changes\n" +
"- need action of current user criteria in form of: need my action\n" +
"- submitted by specified user criteria in form of: submitted by \"<login name of a user>\" (quotes are required)\n" +
"- submitted by current user criteria in form of: submitted by me (quotes are required)\n" +
"- submitted before certain date criteria in form of: \"Submit Date\" is until \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
"- submitted after certain date criteria in form of: \"Submit Date\" is since \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
"- updated before certain date criteria in form of: \"Last Activity Date\" is until \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
"- updated after certain date criteria in form of: \"Last Activity Date\" is since \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
"- closed (merged or discarded) before certain date criteria in form of: \"Close Date\" is until \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
"- closed (merged or discarded) after certain date criteria in form of: \"Close Date\" is since \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
"- includes specified issue criteria in form of: includes issue \"<issue reference>\" (quotes are required)\n" +
"- includes specified commit criteria in form of: includes commit \"<commit hash>\" (quotes are required)\n" +
"- title contains specified text criteria in form of: \"Title\" contains \"<containing text>\" (quotes are required)\n" +
"- description contains specified text criteria in form of: \"Description\" contains \"<containing text>\" (quotes are required)\n" +
"- comment contains specified text criteria in form of: \"Comment\" contains \"<containing text>\" (quotes are required)\n" +
"- project criteria in form of: \"Project\" is \"<project path pattern>\" (quotes are required)\n" +
"- and criteria in form of <criteria1> and <criteria2>\n" +
"- or criteria in form of <criteria1> or <criteria2>\n" +
"- operator 'and' takes precedence over 'or' when used together, unless parentheses are used to group 'or' criterias\n" +
"- not criteria in form of not(<criteria>)\n" +
"\n" +
"And can optionally add order clause at end of query string in form of: order by \"<field1>\" <asc|desc>,\"<field2>\" <asc|desc>,... (quotes are required), where <field> is one of below:\n\n" +
orderFields + "\n" +
"Issue, build or pull request can be referenced by their number in form of: #<number>, <project path>#<number> or <project key>-<number>\n" +
"\n" +
"Leave empty to list all accessible pull requests";
criteria
: '"Number"' 'is' ('not')? '"'EntityReference'"'
| '"Number"' 'is' ('greater'|'less') 'than' '"'EntityReference'"'
| 'open'
| 'merged'
| 'discarded'
| '"Source Branch"' 'is' ('not')? '"'BranchNameOrPattern'"'
| '"Souce Project"' 'is' ('not')? '"'ProjectPathOrPattern'"'
| '"Target Branch"' 'is' ('not')? '"'BranchNameOrPattern'"'
| '"Merge Strategy"' 'is' ('not')? '"'MergeStrategy'"'
| '"Label"' 'is' ('not')? '"'LabelName'"'
| 'ready to merge'
| 'has pending reviews'
| 'has unsuccessful builds'
| 'has unfinished builds'
| 'has merge conflicts'
| 'assigned to' '"'LoginNameOfUser'"'
| 'approved by' '"'LoginNameOfUser'"'
| 'to be reviewed by' '"'LoginNameOfUser'"'
| 'to be changed by' '"'LoginNameOfUser'"'
| 'to be merged by' '"'LoginNameOfUser'"'
| 'requested for changes by' '"'LoginNameOfUser'"'
| 'need action of' '"'LoginNameOfUser'"'
| 'assigned to me'
| 'approved by me'
| 'to be reviewed by me'
| 'to be changed by me'
| 'to be merged by me'
| 'requested for changes by me'
| 'someone requested for changes'
| 'mentioned' '"'LoginNameOfUser'"'
| 'mentioned me'
| 'need action of' '"'LoginNameOfUser'"'
| 'need my action'
| 'submitted by' '"'LoginNameOfUser'"'
| 'submitted by me'
| '"Submit Date"' 'is' ('until'|'since') '"'DateDescription'"'
| '"Last Activity Date"' 'is' ('until'|'since') '"'DateDescription'"'
| '"Close Date"' 'is' ('until'|'since') '"'DateDescription'"'
| 'includes issue' '"'EntityReference'"'
| 'includes commit' '"'CommitReference'"'
| '"Title"' 'contains' '"'Text'"'
| '"Description"' 'contains' '"'Text'"'
| '"Comment"' 'contains' '"'Text'"'
| '"Comment Count"' 'is' ('not')? '"'Number'"'
| '"Comment Count"' 'is' ('greater'|'less') 'than' '"'Number'"'
%s
| '"Project"' 'is' ('not')? '"'ProjectPathOrPattern'"'
| 'watched by' '"'LoginNameOfUser'"'
| 'watched by me'
| 'ignored by' '"'LoginNameOfUser'"'
| 'ignored by me'
| 'commented by' '"'LoginNameOfUser'"'
| 'commented by me'
| criteria 'and' criteria
| criteria 'or' criteria
| 'not('criteria')'
| '('criteria')'
;
EntityReference
: '#'Number
| ProjectPath'#'Number
| ProjectKey'-'Number
;
CommitReference
: (ProjectPath':')?CommitHash
;
BranchReference
: (ProjectPath':')?BranchName
;
TagReference
: (ProjectPath':')?TagName
;
MergeStrategy
: %s
;
LabelName
: %s
;
OrderField
: %s
;
WS
: [ ]+ -> skip
;
Please note:
1. "LoginNameOfUser" should be retrieved via tool 'getLoginName' if available, with parameter set to user name
2. Use an empty query to list all accessible pull requests""",
REACTION_CRITERIAS,
Arrays.stream(MergeStrategy.values()).map(it->"'" + it.name() + "'").collect(joining("\n | ")).trim(),
getLabelSpecs().stream().map(it->"'" + it.getName() + "'").collect(joining("\n | ")).trim(),
PullRequest.SORT_FIELDS.keySet().stream().map(it->"'" + it + "'").collect(joining("\n | ")).trim());
return description;
}
public static String getBuildQueryDescription() {
var buildService = getBuildService();
var buildParamService = getBuildParamService();
var labelSpecService = getLabelSpecService();
var orderFields = new StringBuilder();
for (var field : Build.SORT_FIELDS.keySet()) {
orderFields.append("- ").append(field).append("\n");
}
var description = String.format("""
A structured query should conform to below ANTLR grammar:
var jobNames = buildService.getJobNames(null).stream().collect(Collectors.joining(", "));
var paramNames = buildParamService.getParamNames(null).stream().collect(Collectors.joining(", "));
var labelNames = labelSpecService.query().stream().map(LabelSpec::getName).collect(Collectors.joining(", "));
buildQuery
: criteria ('order by' '"'OrderField'"' ('asc'|'desc') (',' '"'OrderField'"' ('asc'|'desc'))*)?
;
var description =
"A query string is one of below criteria:\n\n" +
"- build with specified number in form of: \"Number\" is \"#<build number>\", or in form of: \"Number\" is \"<project key>-<build number>\" (quotes are required)\n" +
"- criteria to check if version/job contains specified text in form of: ~<containing text>~\n" +
"- sucessful criteria in form of: sucessful\n" +
"- failed criteria in form of: failed\n" +
"- cancelled criteria in form of: cancelled\n" +
"- timed out criteria in form of: timed out\n" +
"- finished criteria in form of: finished\n" +
"- running criteria in form of: running\n" +
"- waiting criteria in form of: waiting\n" +
"- pending criteria in form of: pending\n" +
"- submitted by specified user criteria in form of: submitted by \"<login name of a user>\" (quotes are required)\n" +
"- submitted by current user criteria in form of: submitted by me (quotes are required)\n" +
"- cancelled by specified user criteria in form of: cancelled by \"<login name of a user>\" (quotes are required)\n" +
"- cancelled by current user criteria in form of: cancelled by me (quotes are required)\n" +
"- depends on specified build criteria in form of: depends on \"<build reference>\" (quotes are required)\n" +
"- dependencies of specified build criteria in form of: dependencies of \"<build reference>\" (quotes are required)\n" +
"- fixed specified issue criteria in form of: fixed issue \"<issue reference>\" (quotes are required)\n" +
"- job criteria in form of: \"Job\" is \"<job name>\" (quotes are required), where <job name> is one of: " + jobNames + "\n" +
"- version criteria in form of: \"Version\" is \"<version>\" (quotes are required)\n" +
"- branch criteria in form of: \"Branch\" is \"<branch name>\" (quotes are required)\n" +
"- tag criteria in form of: \"Tag\" is \"<tag name>\" (quotes are required)\n" +
"- param criteria in form of: \"<param name>\" is \"<param value>\" (quotes are required), where <param name> is one of: " + paramNames + "\n" +
"- label criteria in form of: \"Label\" is \"<label name>\" (quotes are required), where <label name> is one of: " + labelNames + "\n" +
"- pull request criteria in form of: \"Pull Request\" is \"<pull request reference>\" (quotes are required)\n" +
"- commit criteria in form of: \"Commit\" is \"<commit hash>\" (quotes are required)\n" +
"- before certain date criteria in form of: \"Submit Date\" is until \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
"- after certain date criteria in form of: \"Submit Date\" is since \"<date>\" (quotes are required), where <date> is of format YYYY-MM-DD HH:mm\n" +
"- project criteria in form of: \"Project\" is \"<project path pattern>\" (quotes are required)\n" +
"- and criteria in form of <criteria1> and <criteria2>\n" +
"- or criteria in form of <criteria1> or <criteria2>\n" +
"- operator 'and' takes precedence over 'or' when used together, unless parentheses are used to group 'or' criterias\n" +
"- not criteria in form of not(<criteria>)\n" +
"\n" +
"And can optionally add order clause at end of query string in form of: order by \"<field1>\" <asc|desc>,\"<field2>\" <asc|desc>,... (quotes are required), where <field> is one of below:\n\n" +
orderFields + "\n" +
"Issue, build or pull request can be referenced by their number in form of: #<number>, <project path>#<number> or <project key>-<number>\n" +
"\n" +
"Leave empty to list all accessible builds";
criteria
: '"Number"' 'is' ('not')? '"'EntityReference'"'
| '"Number"' 'is' ('greater'|'less') 'than' '"'EntityReference'"'
| 'sucessful'
| 'failed'
| 'cancelled'
| 'timed out'
| 'finished'
| 'running'
| 'waiting'
| 'pending'
| 'submitted by' '"'LoginNameOfUser'"'
| 'submitted by me'
| 'cancelled by' '"'LoginNameOfUser'"'
| 'cancelled by me'
| 'depends on' '"'EntityReference'"'
| 'dependencies of' '"'EntityReference'"'
| 'ran on' '"'AgentName'"'
| 'fixed issue' '"'EntityReference'"'
| '"Project"' 'is' ('not')? '"'ProjectPathOrPattern'"'
| '"Job"' 'is' ('not')? '"'JobNameOrPattern'"'
| '"Version"' 'is' ('not')? '"'VersionNameOrPattern'"'
| '"Branch"' 'is' ('not')? '"'BranchNameOrPattern'"'
| '"Tag"' 'is' ('not')? '"'TagNameOrPattern'"'
| '"Param"' 'is' ('not')? '"'ParamName'"'
| '"Label"' 'is' ('not')? '"'LabelName'"'
| '"Pull Request"' 'is' ('not')? '"'EntityReference'"'
| '"Commit"' 'is' ('not')? '"'CommitReference'"'
| '"Submit Date"' 'is' ('until'|'since') '"'DateDescription'"'
| '"Pending Date"' 'is' ('until'|'since') '"'DateDescription'"'
| '"Running Date"' 'is' ('until'|'since') '"'DateDescription'"'
| '"Finish Date"' 'is' ('until'|'since') '"'DateDescription'"'
| criteria 'and' criteria
| criteria 'or' criteria
| 'not('criteria')'
| '('criteria')'
;
EntityReference
: '#'Number
| ProjectPath'#'Number
| ProjectKey'-'Number
;
CommitReference
: (ProjectPath':')?CommitHash
;
LabelName
: %s
;
OrderField
: %s
;
WS
: [ ]+ -> skip
;
Please note:
1. "LoginNameOfUser" should be retrieved via tool 'getLoginName' if available, with parameter set to user name
2. Use an empty query to list all accessible builds""",
getLabelSpecs().stream().map(it->"'" + it.getName() + "'").collect(joining("\n | ")).trim(),
Build.SORT_FIELDS.keySet().stream().map(it->"'" + it + "'").collect(joining("\n | ")).trim());
return description;
}
private static List<LabelSpec> getLabelSpecs() {
return OneDev.getInstance(LabelSpecService.class).query();
}
public static String getPackQueryDescription() {
var packSupports = new ArrayList<>(OneDev.getExtensions(PackSupport.class));
var description = String.format("""
A structured query should conform to below ANTLR grammar:
packQuery
: criteria ('order by' '"'OrderField'"' ('asc'|'desc') (',' '"'OrderField'"' ('asc'|'desc'))*)?
;
criteria
: 'published by me'
| 'published by user' '"'LoginNameOfUser'"'
| 'published by build' '"'EntityReference'"'
| 'published by project' '"'ProjectPathOrPattern'"'
| '"Project"' 'is' ('not')? '"'ProjectPathOrPattern'"'
| '"Type"' 'is' ('not')? '"'PackType'"'
| '"Name"' 'is' ('not')? '"'PackName'"'
| '"Version"' 'is' ('not')? '"'PackVersion'"'
| '"Label"' 'is' ('not')? '"'LabelName'"'
| '"Publish Date"' 'is' ('until'|'since') '"'DateDescription'"'
| criteria 'and' criteria
| criteria 'or' criteria
| 'not('criteria')'
| '('criteria')'
;
EntityReference
: '#'Number
| ProjectPath'#'Number
| ProjectKey'-'Number
;
PackType
: %s
;
LabelName
: %s
;
OrderField
: %s
;
WS
: [ ]+ -> skip
;
Please note:
1. "LoginNameOfUser" should be retrieved via tool 'getLoginName' if available, with parameter set to user name
2. Use an empty query to list all accessible packages""",
packSupports.stream().map(it->"'" + it.getPackType() + "'").collect(joining("\n | ")).trim(),
getLabelSpecs().stream().map(it->"'" + it.getName() + "'").collect(joining("\n | ")).trim(),
Pack.SORT_FIELDS.keySet().stream().map(it->"'" + it + "'").collect(joining("\n | ")).trim());
return description;
}
public static String getCommitQueryDescription() {
var description = """
A structured query should conform to below ANTLR grammar:
commitQuery
: criteria+
;
criteria
: ('before('|'after(') DateDescription ')'
| 'committer(' CommitterNameAndEmail ')' // committer is specified user
| 'author(' AuthorNameAndEmail ')' // author is specified user
| 'message(' Text ')' // commit message contains specified text
| 'path(' FilePath ')' // commit touches specified file
| 'authored-by-me'
| 'committed-by-me'
| ('until')? revision // until specified revision
| 'since' revision // since specified revision
;
revision
: 'commit(' CommitHash ')'
| 'build(' '#'Number ')'
| 'branch(' BranchName ')'
| 'tag(' TagName ')'
| 'default-branch'
;
WS
: [ ]+ -> skip
;
""";
return description;
}
public static String getProjectQueryDescription() {
var description = String.format("""
A structured query should conform to below ANTLR grammar:
projectQuery
: criteria ('order by' '"'OrderField'"' ('asc'|'desc') (',' '"'OrderField'"' ('asc'|'desc'))*)?
;
criteria
: 'owned by' '"'LoginNameOfUser'"'
| 'owned by me'
| 'owned by none'
| 'has outdated replicas'
| 'without enough replicas'
| 'missing storage'
| 'children of' '"'ProjectPathOrPattern'"'
| 'forks of' '"'ProjectPathOrPattern'"'
| 'roots'
| 'leafs'
| 'fork roots'
| '"Name"' 'is' ('not')? '"'ProjectNameOrPattern'"'
| '"Key"' 'is' ('not')? '"'ProjectKeyOrPattern'"'
| '"Path"' 'is' ('not')? '"'ProjectPathOrPattern'"'
| '"Label"' 'is' ('not')? '"'LabelName'"'
| '"Description"' 'contains' '"'Text'"'
| '"Id"' 'is' ('not')? '"'Number'"'
| '"Id"' 'is' ('greater'|'less') 'than' '"'Number'"'
| '"Service Desk Email Address"' 'is' ('not')? '"'EmailAddressOrPattern'"'
| '"Last Activity Date"' 'is' ('until'|'since') '"'DateDescription'"'
| criteria 'and' criteria
| criteria 'or' criteria
| 'not('criteria')'
| '('criteria')'
;
LabelName
: %s
;
OrderField
: %s
;
WS
: [ ]+ -> skip
;
Please note:
1. "LoginNameOfUser" should be retrieved via tool 'getLoginName' if available, with parameter set to user name
2. Use an empty query to list all accessible projects""",
getLabelSpecs().stream().map(it->"'" + it.getName() + "'").collect(joining("\n | ")).trim(),
Project.SORT_FIELDS.keySet().stream().map(it->"'" + it + "'").collect(joining("\n | ")).trim());
return description;
}
public static String getAgentQueryDescription() {
var agentService = OneDev.getInstance(AgentService.class);
var attributeService = OneDev.getInstance(AgentAttributeService.class);
var description = String.format("""
A structured query should conform to below ANTLR grammar:
agentQuery
: criteria ('order by' '"'OrderField'"' ('asc'|'desc') (',' '"'OrderField'"' ('asc'|'desc'))*)?
;
criteria
: 'online'
| 'offline'
| 'paused'
| 'has running builds'
| 'has attribute' '"'AttributeName'"'
| 'not used since' '"'DateDescription'"'
| 'ever used since' '"'DateDescription'"'
| 'ran build' '"'EntityReference'"'
| '"Name"' 'is' ('not')? '"'AgentNameOrPattern'"'
| '"Ip Address"' 'is' ('not')? '"'IPAddressOrPattern'"'
| '"Os"' 'is' ('not')? '"'(OsName|OsNamePattern)'"'
| '"Os Arch"' 'is' ('not')? '"'(OsArch|OsArchPattern)'"'
| '"Os Version"' 'is' ('not')? '"'OsVersionOrPattern'"'
| '"'AttributeName'"' 'is' ('not')? '"'AttributeValue'"'
| criteria 'and' criteria
| criteria 'or' criteria
| 'not('criteria')'
| '('criteria')'
;
OsName
: %s
;
OsArch
: %s
;
AttributeName
: %s
;
OrderField
: %s
;
WS
: [ ]+ -> skip
;
Please note:
1. "LoginNameOfUser" should be retrieved via tool 'getLoginName' if available, with parameter set to user name
2. Use an empty query to list all accessible agents""",
agentService.getOsNames().stream().map(it->"'" + it + "'").collect(joining("\n | ")).trim(),
agentService.getOsArchs().stream().map(it->"'" + it + "'").collect(joining("\n | ")).trim(),
attributeService.getAttributeNames().stream().map(it->"'" + it + "'").collect(joining("\n | ")).trim(),
Agent.SORT_FIELDS.keySet().stream().map(it->"'" + it + "'").collect(joining("\n | ")).trim());
return description;
}
}

View File

@ -87,7 +87,15 @@ public abstract class EntityReference implements Serializable {
else
throw new ValidationException("Reference project not found with key: " + projectKey);
}
throw new ValidationException("Invalid entity reference: " + referenceString);
try {
var number = Long.valueOf(referenceString);
if (currentProject != null)
return EntityReference.of(type, currentProject, number);
else
throw new ValidationException("Reference project not specified: " + referenceString);
} catch (NumberFormatException e) {
throw new ValidationException("Invalid entity reference: " + referenceString);
}
}
@Override

View File

@ -7,17 +7,31 @@ import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import javax.persistence.criteria.Path;
import javax.validation.ValidationException;
import org.jspecify.annotations.Nullable;
import com.google.common.base.Splitter;
import io.onedev.commons.codeassist.FenceAware;
import io.onedev.commons.utils.ExplicitException;
import io.onedev.commons.utils.StringUtils;
import io.onedev.server.OneDev;
import io.onedev.server.entityreference.BuildReference;
import io.onedev.server.entityreference.IssueReference;
import io.onedev.server.entityreference.PullRequestReference;
import io.onedev.server.model.AbstractEntity;
import io.onedev.server.model.Build;
import io.onedev.server.model.Group;
import io.onedev.server.model.Issue;
import io.onedev.server.model.Iteration;
import io.onedev.server.model.LabelSpec;
import io.onedev.server.model.Project;
import io.onedev.server.model.PullRequest;
import io.onedev.server.model.User;
import io.onedev.server.service.BuildService;
import io.onedev.server.service.GroupService;
import io.onedev.server.service.IssueService;
import io.onedev.server.service.IterationService;
import io.onedev.server.service.LabelSpecService;
@ -25,17 +39,6 @@ import io.onedev.server.service.ProjectService;
import io.onedev.server.service.PullRequestService;
import io.onedev.server.service.SettingService;
import io.onedev.server.service.UserService;
import io.onedev.server.entityreference.BuildReference;
import io.onedev.server.entityreference.IssueReference;
import io.onedev.server.entityreference.PullRequestReference;
import io.onedev.server.model.AbstractEntity;
import io.onedev.server.model.Build;
import io.onedev.server.model.Issue;
import io.onedev.server.model.Iteration;
import io.onedev.server.model.LabelSpec;
import io.onedev.server.model.Project;
import io.onedev.server.model.PullRequest;
import io.onedev.server.model.User;
import io.onedev.server.util.DateUtils;
import io.onedev.server.util.ProjectScopedCommit;
import io.onedev.server.util.ProjectScopedRevision;
@ -139,6 +142,13 @@ public abstract class EntityQuery<T extends AbstractEntity> implements Serializa
throw new ExplicitException("Unable to find user with login: " + loginName);
return user;
}
public static Group getGroup(String groupName) {
Group group = OneDev.getInstance(GroupService.class).find(groupName);
if (group == null)
throw new ExplicitException("Unable to find group: " + groupName);
return group;
}
public static Project getProject(String projectPath) {
Project project = OneDev.getInstance(ProjectService.class).findByPath(projectPath);

View File

@ -0,0 +1,54 @@
package io.onedev.server.search.entity.issue;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import io.onedev.server.model.Issue;
import io.onedev.server.model.IssueField;
public class FloatFieldCriteria extends FieldCriteria {
private static final long serialVersionUID = 1L;
private final float value;
private final int operator;
public FloatFieldCriteria(String name, float value, int operator) {
super(name);
this.value = value;
this.operator = operator;
}
@Override
protected Predicate getValuePredicate(From<Issue, Issue> issueFrom, From<IssueField, IssueField> fieldFrom, CriteriaBuilder builder) {
Path<Float> attribute = fieldFrom.get(IssueField.PROP_ORDINAL);
if (operator == IssueQueryLexer.IsGreaterThan)
return builder.greaterThan(attribute, value);
else
return builder.lessThan(attribute, value);
}
@Override
public boolean matches(Issue issue) {
Float fieldValue = (Float) issue.getFieldValue(getFieldName());
if (operator == IssueQueryLexer.IsGreaterThan)
return fieldValue != null && fieldValue > value;
else
return fieldValue != null && fieldValue < value;
}
@Override
public String toStringWithoutParens() {
return quote(getFieldName()) + " "
+ IssueQuery.getRuleName(operator) + " "
+ quote(String.valueOf(value));
}
@Override
public void fill(Issue issue) {
}
}

View File

@ -10,8 +10,7 @@ import javax.persistence.criteria.Predicate;
import io.onedev.server.model.Issue;
import io.onedev.server.model.IssueField;
public class NumericFieldCriteria extends FieldCriteria {
public class IntegerFieldCriteria extends FieldCriteria {
private static final long serialVersionUID = 1L;
@ -19,7 +18,7 @@ public class NumericFieldCriteria extends FieldCriteria {
private final int operator;
public NumericFieldCriteria(String name, int value, int operator) {
public IntegerFieldCriteria(String name, int value, int operator) {
super(name);
this.value = value;
this.operator = operator;

View File

@ -1,5 +1,6 @@
package io.onedev.server.search.entity.issue;
import static io.onedev.server.model.AbstractEntity.NAME_NUMBER;
import static io.onedev.server.model.Issue.NAME_COMMENT;
import static io.onedev.server.model.Issue.NAME_COMMENT_COUNT;
import static io.onedev.server.model.Issue.NAME_CONFUSED_COUNT;
@ -56,8 +57,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.antlr.v4.runtime.BailErrorStrategy;
import org.antlr.v4.runtime.BaseErrorListener;
import org.antlr.v4.runtime.CharStream;
@ -65,11 +64,11 @@ import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.jspecify.annotations.Nullable;
import io.onedev.commons.codeassist.AntlrUtils;
import io.onedev.commons.utils.ExplicitException;
import io.onedev.server.OneDev;
import io.onedev.server.service.SettingService;
import io.onedev.server.model.Issue;
import io.onedev.server.model.IssueSchedule;
import io.onedev.server.model.Project;
@ -80,6 +79,7 @@ import io.onedev.server.model.support.issue.field.spec.CommitField;
import io.onedev.server.model.support.issue.field.spec.DateField;
import io.onedev.server.model.support.issue.field.spec.DateTimeField;
import io.onedev.server.model.support.issue.field.spec.FieldSpec;
import io.onedev.server.model.support.issue.field.spec.FloatField;
import io.onedev.server.model.support.issue.field.spec.GroupChoiceField;
import io.onedev.server.model.support.issue.field.spec.IntegerField;
import io.onedev.server.model.support.issue.field.spec.IssueChoiceField;
@ -106,6 +106,7 @@ import io.onedev.server.search.entity.issue.IssueQueryParser.ParensCriteriaConte
import io.onedev.server.search.entity.issue.IssueQueryParser.QueryContext;
import io.onedev.server.search.entity.issue.IssueQueryParser.ReferenceCriteriaContext;
import io.onedev.server.search.entity.issue.IssueQueryParser.RevisionCriteriaContext;
import io.onedev.server.service.SettingService;
import io.onedev.server.util.criteria.AndCriteria;
import io.onedev.server.util.criteria.Criteria;
import io.onedev.server.util.criteria.NotCriteria;
@ -363,7 +364,7 @@ public class IssueQuery extends EntityQuery<Issue> {
criterias.add(new RocketCountCriteria(getIntValue(value), operator));
} else if (fieldName.equals(NAME_EYES_COUNT)) {
criterias.add(new EyesCountCriteria(getIntValue(value), operator));
} else if (fieldName.equals(Issue.NAME_NUMBER)) {
} else if (fieldName.equals(NAME_NUMBER)) {
criterias.add(new ReferenceCriteria(project, value, operator));
} else if (fieldName.equals(NAME_ESTIMATED_TIME)) {
int intValue = timeTrackingSetting.parseWorkingPeriod(value);
@ -384,13 +385,16 @@ public class IssueQuery extends EntityQuery<Issue> {
} else if (field instanceof BooleanField) {
criterias.add(new BooleanFieldCriteria(fieldName, getBooleanValue(value), operator));
} else if (field instanceof IntegerField) {
criterias.add(new NumericFieldCriteria(fieldName, getIntValue(value), operator));
criterias.add(new IntegerFieldCriteria(fieldName, getIntValue(value), operator));
} else if (field instanceof ChoiceField) {
long ordinal = getValueOrdinal((ChoiceField) field, value);
criterias.add(new ChoiceFieldCriteria(fieldName, value, ordinal, operator, field.isAllowMultiple()));
} else if (field instanceof UserChoiceField
|| field instanceof GroupChoiceField) {
criterias.add(new ChoiceFieldCriteria(fieldName, value, -1, operator, field.isAllowMultiple()));
} else if (field instanceof UserChoiceField) {
// call getUser(value) to validate the user name
criterias.add(new ChoiceFieldCriteria(fieldName, getUser(value).getName(), -1, operator, field.isAllowMultiple()));
} else if (field instanceof GroupChoiceField) {
// call getGroup(value) to validate the group name
criterias.add(new ChoiceFieldCriteria(fieldName, getGroup(value).getName(), -1, operator, field.isAllowMultiple()));
} else if (field instanceof IterationChoiceField) {
criterias.add(new IterationFieldCriteria(fieldName, value, operator, field.isAllowMultiple()));
} else {
@ -443,10 +447,15 @@ public class IssueQuery extends EntityQuery<Issue> {
var floatValue = getFloatValue(value);
criterias.add(new ProgressCriteria(floatValue, operator));
break;
case NAME_NUMBER:
criterias.add(new ReferenceCriteria(project, value, operator));
break;
default:
FieldSpec field = getGlobalIssueSetting().getFieldSpec(fieldName);
if (field instanceof IntegerField) {
criterias.add(new NumericFieldCriteria(fieldName, getIntValue(value), operator));
criterias.add(new IntegerFieldCriteria(fieldName, getIntValue(value), operator));
} else if (field instanceof FloatField) {
criterias.add(new FloatFieldCriteria(fieldName, getFloatValue(value), operator));
} else {
long ordinal;
if (validate)
@ -600,7 +609,7 @@ public class IssueQuery extends EntityQuery<Issue> {
&& !fieldName.equals(Issue.NAME_HEART_COUNT)
&& !fieldName.equals(Issue.NAME_ROCKET_COUNT)
&& !fieldName.equals(Issue.NAME_EYES_COUNT)
&& !fieldName.equals(Issue.NAME_NUMBER)
&& !fieldName.equals(NAME_NUMBER)
&& !fieldName.equals(IssueSchedule.NAME_ITERATION)
&& !(fieldSpec instanceof IssueChoiceField)
&& !(fieldSpec instanceof PullRequestChoiceField)
@ -636,8 +645,9 @@ public class IssueQuery extends EntityQuery<Issue> {
&& !fieldName.equals(Issue.NAME_HEART_COUNT)
&& !fieldName.equals(Issue.NAME_ROCKET_COUNT)
&& !fieldName.equals(Issue.NAME_EYES_COUNT)
&& !fieldName.equals(Issue.NAME_NUMBER)
&& !fieldName.equals(NAME_NUMBER)
&& !(fieldSpec instanceof IntegerField)
&& !(fieldSpec instanceof FloatField)
&& !(fieldSpec instanceof ChoiceField && !fieldSpec.isAllowMultiple()))
throw newOperatorException(fieldName, operator);
break;

View File

@ -16,7 +16,6 @@ import io.onedev.server.model.Issue;
import io.onedev.server.util.ProjectScope;
import io.onedev.server.util.criteria.Criteria;
public class SpentTimeCriteria extends Criteria<Issue> {
private static final long serialVersionUID = 1L;

View File

@ -8,7 +8,6 @@ import javax.persistence.criteria.Predicate;
import io.onedev.server.model.Issue;
import io.onedev.server.model.IssueField;
public class StringFieldCriteria extends FieldCriteria {
private static final long serialVersionUID = 1L;

View File

@ -1,11 +1,13 @@
package io.onedev.server.search.entity.project;
import org.jspecify.annotations.Nullable;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.From;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import org.jspecify.annotations.Nullable;
import io.onedev.server.model.Project;
import io.onedev.server.util.ProjectScope;
import io.onedev.server.util.criteria.Criteria;
@ -25,18 +27,27 @@ public class IdCriteria extends Criteria<Project> {
@Override
public Predicate getPredicate(@Nullable ProjectScope projectScope, CriteriaQuery<?> query, From<Project, Project> from, CriteriaBuilder builder) {
Path<Long> attribute = from.get(Project.PROP_ID);
if (operator == ProjectQueryLexer.Is)
return builder.equal(from.get(Project.PROP_ID), value);
return builder.equal(attribute, value);
else if (operator == ProjectQueryLexer.IsNot)
return builder.not(builder.equal(attribute, value));
else if (operator == ProjectQueryLexer.IsGreaterThan)
return builder.greaterThan(attribute, value);
else
return builder.not(builder.equal(from.get(Project.PROP_ID), value));
return builder.lessThan(attribute, value);
}
@Override
public boolean matches(Project project) {
if (operator == ProjectQueryLexer.Is)
return project.getId().equals(value);
else
else if (operator == ProjectQueryLexer.IsNot)
return !project.getId().equals(value);
else if (operator == ProjectQueryLexer.IsGreaterThan)
return project.getId() > value;
else
return project.getId() < value;
}
@Override

View File

@ -9,7 +9,7 @@ query
criteria
: operator=(Roots|Leafs|ForkRoots|OwnedByMe|OwnedByNone|HasOutdatedReplicas|WithoutEnoughReplicas|MissingStorage) #OperatorCriteria
| operator=(OwnedBy|ForksOf|ChildrenOf) WS+ criteriaValue=multipleQuoted #OperatorValueCriteria
| criteriaField=Quoted WS+ operator=(Is|IsNot|Contains|IsUntil|IsSince) WS+ criteriaValue=multipleQuoted #FieldOperatorValueCriteria
| criteriaField=Quoted WS+ operator=(Is|IsNot|IsGreaterThan|IsLessThan|Contains|IsUntil|IsSince) WS+ criteriaValue=multipleQuoted #FieldOperatorValueCriteria
| criteria WS+ And WS+ criteria #AndCriteria
| criteria WS+ Or WS+ criteria #OrCriteria
| Not WS* LParens WS* criteria WS* RParens #NotCriteria
@ -37,6 +37,14 @@ IsNot
: 'is' WS+ 'not'
;
IsGreaterThan
: 'is' WS+ 'greater' WS+ 'than'
;
IsLessThan
: 'is' WS+ 'less' WS+ 'than'
;
OwnedBy
: 'owned' WS+ 'by'
;

View File

@ -16,6 +16,8 @@ import static io.onedev.server.search.entity.project.ProjectQueryParser.Leafs;
import static io.onedev.server.search.entity.project.ProjectQueryParser.MissingStorage;
import static io.onedev.server.search.entity.project.ProjectQueryParser.OwnedByMe;
import static io.onedev.server.search.entity.project.ProjectQueryParser.OwnedByNone;
import static io.onedev.server.search.entity.project.ProjectQueryParser.IsGreaterThan;
import static io.onedev.server.search.entity.project.ProjectQueryParser.IsLessThan;
import static io.onedev.server.search.entity.project.ProjectQueryParser.Roots;
import static io.onedev.server.search.entity.project.ProjectQueryParser.WithoutEnoughReplicas;
@ -177,6 +179,10 @@ public class ProjectQuery extends EntityQuery<Project> {
criterias.add(new LabelCriteria(getLabelSpec(value), operator));
}
break;
case IsGreaterThan:
case IsLessThan:
criterias.add(new IdCriteria(getLongValue(value), operator));
break;
case Contains:
criterias.add(new DescriptionCriteria(value));
break;
@ -266,6 +272,12 @@ public class ProjectQuery extends EntityQuery<Project> {
throw newOperatorException(fieldName, operator);
}
break;
case IsGreaterThan:
case IsLessThan:
if (!fieldName.equals(Project.NAME_ID)) {
throw newOperatorException(fieldName, operator);
}
break;
case IsUntil:
case IsSince:
if (!fieldName.equals(Project.NAME_LAST_ACTIVITY_DATE)

View File

@ -1,7 +1,16 @@
package io.onedev.server.web.behavior;
import static io.onedev.server.web.translation.Translation._T;
import static java.util.Collections.sort;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import io.onedev.commons.codeassist.FenceAware;
import io.onedev.commons.codeassist.InputCompletion;
import io.onedev.commons.codeassist.InputSuggestion;
@ -11,23 +20,19 @@ import io.onedev.commons.codeassist.parser.ParseExpect;
import io.onedev.commons.codeassist.parser.TerminalExpect;
import io.onedev.commons.utils.ExplicitException;
import io.onedev.server.OneDev;
import io.onedev.server.service.AgentAttributeService;
import io.onedev.server.service.AgentService;
import io.onedev.server.ai.QueryDescriptions;
import io.onedev.server.model.Agent;
import io.onedev.server.search.entity.agent.AgentQuery;
import io.onedev.server.search.entity.agent.AgentQueryLexer;
import io.onedev.server.search.entity.agent.AgentQueryParser;
import io.onedev.server.service.AgentAttributeService;
import io.onedev.server.service.AgentService;
import io.onedev.server.service.SettingService;
import io.onedev.server.util.DateUtils;
import io.onedev.server.web.behavior.inputassist.ANTLRAssistBehavior;
import io.onedev.server.web.behavior.inputassist.InputAssistBehavior;
import io.onedev.server.web.behavior.inputassist.NaturalLanguageTranslator;
import io.onedev.server.web.util.SuggestionUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
import static io.onedev.server.web.translation.Translation._T;
import static java.util.Collections.sort;
public class AgentQueryBehavior extends ANTLRAssistBehavior {
@ -48,11 +53,11 @@ public class AgentQueryBehavior extends ANTLRAssistBehavior {
@Override
protected List<InputSuggestion> match(String matchWith) {
AgentService agentService = OneDev.getInstance(AgentService.class);
AgentAttributeService attributeManager = OneDev.getInstance(AgentAttributeService.class);
AgentAttributeService attributeService = OneDev.getInstance(AgentAttributeService.class);
ParseExpect criteriaValueExpect;
if ("criteriaField".equals(spec.getLabel())) {
var fields = new ArrayList<>(Agent.QUERY_FIELDS);
var attributeNames = new ArrayList<>(attributeManager.getAttributeNames());
var attributeNames = new ArrayList<>(attributeService.getAttributeNames());
sort(attributeNames);
fields.addAll(attributeNames);
return SuggestionUtils.suggest(fields, matchWith);
@ -69,7 +74,7 @@ public class AgentQueryBehavior extends ANTLRAssistBehavior {
if (operator == AgentQueryLexer.EverUsedSince || operator == AgentQueryLexer.NotUsedSince) {
return SuggestionUtils.suggest(DateUtils.RELAX_DATE_EXAMPLES, matchWith);
} else if (operator == AgentQueryLexer.HasAttribute) {
var attributeNames = new ArrayList<>(attributeManager.getAttributeNames());
var attributeNames = new ArrayList<>(attributeService.getAttributeNames());
sort(attributeNames);
return SuggestionUtils.suggest(attributeNames, matchWith);
} else if (operator == AgentQueryLexer.RanBuild) {
@ -184,6 +189,27 @@ public class AgentQueryBehavior extends ANTLRAssistBehavior {
return hints;
}
@Override
protected NaturalLanguageTranslator getNaturalLanguageTranslator() {
var liteModel = getSettingService().getAISetting().getLiteModel();
if (liteModel != null) {
return new NaturalLanguageTranslator(liteModel) {
@Override
public String getQueryDescription() {
return QueryDescriptions.getAgentQueryDescription();
}
};
} else {
return null;
}
}
private SettingService getSettingService() {
return OneDev.getInstance(SettingService.class);
}
@Override
protected boolean isFuzzySuggestion(InputCompletion suggestion) {
return suggestion.getLabel().startsWith("~")

View File

@ -25,12 +25,15 @@ import io.onedev.commons.utils.StringUtils;
import io.onedev.commons.utils.match.PatternApplied;
import io.onedev.commons.utils.match.WildcardUtils;
import io.onedev.server.OneDev;
import io.onedev.server.ai.QueryDescriptions;
import io.onedev.server.model.Project;
import io.onedev.server.search.commit.CommitQueryParser;
import io.onedev.server.service.SettingService;
import io.onedev.server.util.DateUtils;
import io.onedev.server.util.NameAndEmail;
import io.onedev.server.web.behavior.inputassist.ANTLRAssistBehavior;
import io.onedev.server.web.behavior.inputassist.InputAssistBehavior;
import io.onedev.server.web.behavior.inputassist.NaturalLanguageTranslator;
import io.onedev.server.web.util.SuggestionUtils;
import io.onedev.server.xodus.CommitInfoService;
@ -194,6 +197,27 @@ public class CommitQueryBehavior extends ANTLRAssistBehavior {
return Optional.fromNullable(description);
}
@Override
protected NaturalLanguageTranslator getNaturalLanguageTranslator() {
var liteModel = getSettingService().getAISetting().getLiteModel();
if (liteModel != null) {
return new NaturalLanguageTranslator(liteModel) {
@Override
public String getQueryDescription() {
return QueryDescriptions.getCommitQueryDescription();
}
};
} else {
return null;
}
}
private SettingService getSettingService() {
return OneDev.getInstance(SettingService.class);
}
@Override
protected boolean isFuzzySuggestion(InputCompletion suggestion) {
return suggestion.getLabel().startsWith("~")

View File

@ -1,7 +1,24 @@
package io.onedev.server.web.behavior;
import static io.onedev.server.model.Pack.NAME_LABEL;
import static io.onedev.server.model.Pack.NAME_NAME;
import static io.onedev.server.model.Pack.NAME_TYPE;
import static io.onedev.server.model.Pack.NAME_VERSION;
import static io.onedev.server.model.Pack.PROP_NAME;
import static io.onedev.server.model.Pack.PROP_VERSION;
import static io.onedev.server.web.translation.Translation._T;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.wicket.Component;
import org.apache.wicket.model.IModel;
import org.jspecify.annotations.Nullable;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import io.onedev.commons.codeassist.AntlrUtils;
import io.onedev.commons.codeassist.FenceAware;
import io.onedev.commons.codeassist.InputCompletion;
@ -11,25 +28,19 @@ import io.onedev.commons.codeassist.parser.Element;
import io.onedev.commons.codeassist.parser.ParseExpect;
import io.onedev.commons.codeassist.parser.TerminalExpect;
import io.onedev.commons.utils.ExplicitException;
import io.onedev.server.OneDev;
import io.onedev.server.ai.QueryDescriptions;
import io.onedev.server.model.Pack;
import io.onedev.server.model.Project;
import io.onedev.server.search.entity.pack.PackQuery;
import io.onedev.server.search.entity.pack.PackQueryLexer;
import io.onedev.server.search.entity.pack.PackQueryParser;
import io.onedev.server.service.SettingService;
import io.onedev.server.util.DateUtils;
import io.onedev.server.web.behavior.inputassist.ANTLRAssistBehavior;
import io.onedev.server.web.behavior.inputassist.InputAssistBehavior;
import io.onedev.server.web.behavior.inputassist.NaturalLanguageTranslator;
import io.onedev.server.web.util.SuggestionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.wicket.Component;
import org.apache.wicket.model.IModel;
import org.jspecify.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import static io.onedev.server.model.Pack.*;
import static io.onedev.server.web.translation.Translation._T;
public class PackQueryBehavior extends ANTLRAssistBehavior {
@ -218,6 +229,27 @@ public class PackQueryBehavior extends ANTLRAssistBehavior {
return hints;
}
@Override
protected NaturalLanguageTranslator getNaturalLanguageTranslator() {
var liteModel = getSettingService().getAISetting().getLiteModel();
if (liteModel != null) {
return new NaturalLanguageTranslator(liteModel) {
@Override
public String getQueryDescription() {
return QueryDescriptions.getPackQueryDescription();
}
};
} else {
return null;
}
}
private SettingService getSettingService() {
return OneDev.getInstance(SettingService.class);
}
@Override
protected boolean isFuzzySuggestion(InputCompletion suggestion) {
return suggestion.getLabel().startsWith("~")

View File

@ -1,7 +1,22 @@
package io.onedev.server.web.behavior;
import static io.onedev.server.search.entity.project.ProjectQuery.getRuleName;
import static io.onedev.server.search.entity.project.ProjectQueryParser.ChildrenOf;
import static io.onedev.server.search.entity.project.ProjectQueryParser.HasOutdatedReplicas;
import static io.onedev.server.search.entity.project.ProjectQueryParser.Roots;
import static io.onedev.server.search.entity.project.ProjectQueryParser.WithoutEnoughReplicas;
import static io.onedev.server.web.translation.Translation._T;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import io.onedev.commons.codeassist.FenceAware;
import io.onedev.commons.codeassist.InputCompletion;
import io.onedev.commons.codeassist.InputSuggestion;
@ -11,29 +26,21 @@ import io.onedev.commons.codeassist.parser.ParseExpect;
import io.onedev.commons.codeassist.parser.TerminalExpect;
import io.onedev.commons.utils.ExplicitException;
import io.onedev.server.OneDev;
import io.onedev.server.ai.QueryDescriptions;
import io.onedev.server.cluster.ClusterService;
import io.onedev.server.service.ProjectService;
import io.onedev.server.service.SettingService;
import io.onedev.server.model.Project;
import io.onedev.server.search.entity.project.ProjectQuery;
import io.onedev.server.search.entity.project.ProjectQueryLexer;
import io.onedev.server.search.entity.project.ProjectQueryParser;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.security.permission.AccessProject;
import io.onedev.server.service.ProjectService;
import io.onedev.server.service.SettingService;
import io.onedev.server.util.DateUtils;
import io.onedev.server.util.facade.ProjectCache;
import io.onedev.server.web.behavior.inputassist.ANTLRAssistBehavior;
import io.onedev.server.web.behavior.inputassist.NaturalLanguageTranslator;
import io.onedev.server.web.util.SuggestionUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import static io.onedev.server.search.entity.project.ProjectQuery.getRuleName;
import static io.onedev.server.search.entity.project.ProjectQueryParser.*;
import static io.onedev.server.web.translation.Translation._T;
public class ProjectQueryBehavior extends ANTLRAssistBehavior {
@ -221,6 +228,27 @@ public class ProjectQueryBehavior extends ANTLRAssistBehavior {
return hints;
}
@Override
protected NaturalLanguageTranslator getNaturalLanguageTranslator() {
var liteModel = getSettingService().getAISetting().getLiteModel();
if (liteModel != null) {
return new NaturalLanguageTranslator(liteModel) {
@Override
public String getQueryDescription() {
return QueryDescriptions.getProjectQueryDescription();
}
};
} else {
return null;
}
}
private SettingService getSettingService() {
return OneDev.getInstance(SettingService.class);
}
@Override
protected boolean isFuzzySuggestion(InputCompletion suggestion) {
return suggestion.getLabel().startsWith("~")