chore: Add MCP support for issue operations

This commit is contained in:
Robin Shen 2025-08-10 17:32:08 +08:00
parent dc4af70ff1
commit d9f9fa03e6
57 changed files with 1581 additions and 252 deletions

175
CLAUDE.md Normal file
View File

@ -0,0 +1,175 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build Commands
OneDev uses Maven as its build system with a multi-module structure.
### Essential Commands
- **Build the project**: `mvn clean compile`
- **Run tests**: `mvn test`
- **Package the application**: `mvn clean package`
- **Build without tests**: `mvn clean package -DskipTests`
- **Build specific module**: `mvn clean package -pl server-core`
- **Install to local repository**: `mvn clean install`
### Profiles
- **Community Edition**: `mvn clean package -Pce` (excludes enterprise features)
- **Default/Enterprise**: `mvn clean package` (includes all features)
### Testing
- **Run all tests**: `mvn test`
- **Run specific test class**: `mvn test -Dtest=ClassName`
- **Run tests for specific module**: `mvn test -pl server-core`
## Architecture Overview
OneDev is a comprehensive DevOps platform built with a sophisticated multi-module Maven architecture:
### Core Technology Stack
- **Web Framework**: Apache Wicket 7.18.0 (component-based UI)
- **REST API**: Jersey 2.38 (JAX-RS implementation)
- **Database/ORM**: Hibernate 5.4.24.Final with HikariCP connection pooling
- **Web Server**: Embedded Jetty 9.4.57
- **Dependency Injection**: Google Guice with custom plugin loading
- **Security**: Apache Shiro for authentication/authorization
- **Search**: Apache Lucene 8.7.0
- **Git**: JGit 5.13.3 for Git operations
- **Clustering**: Hazelcast 5.3.5 for distributed coordination
### Module Structure
- **server-core**: Core application logic, entities, and services
- **server-ee**: Enterprise edition features
- **server-plugin**: Plugin framework and all plugin implementations
- **server-product**: Final packaging and deployment artifacts
### Key Subsystems
#### 1. Application Bootstrap
- Main entry point: `server-core/src/main/java/io/onedev/server/OneDev.java`
- Module configuration: `server-core/src/main/java/io/onedev/server/CoreModule.java`
- Handles server lifecycle, clustering, and graceful shutdown
#### 2. Entity Management
Key domain entities and managers in `server-core/src/main/java/io/onedev/server/model/`:
- Project, User, Group, Role management
- Issue tracking with customizable workflows
- Pull request lifecycle and code review
- Build and CI/CD pipeline management
- Package registry operations
#### 3. Git Integration
- Full Git repository management via JGit
- Git hooks for policy enforcement in `server-core/src/main/java/io/onedev/server/git/`
- Code browsing, diff visualization, and blame tracking
- SSH server for Git operations
#### 4. Web Layer (Wicket)
- Component-based UI in `server-core/src/main/java/io/onedev/server/web/`
- AJAX-heavy interface with WebSocket support
- Project browsing, issue boards, pull request review interface
#### 5. REST API (Jersey)
- RESTful services in `server-core/src/main/java/io/onedev/server/rest/`
- Project, User, Build, Issue resources
- WebHook endpoints and package registry APIs
#### 6. CI/CD System
- YAML-based build specifications
- Multi-executor support (Kubernetes, Docker, Shell)
- Real-time log streaming and artifact management
#### 7. Plugin Architecture
- Extensible plugin system in `server-plugin/`
- Categories: build specs, executors, authenticators, importers, notifications, package registries, report processors
- Plugin contributions via Guice modules
## Development Patterns
### Code Organization
- **Package-by-feature**: Organized around business capabilities
- **Dependency Injection**: Guice-based DI throughout the application
- **Interface-based design**: For testability and modularity
- **Custom annotations**: Extensive use for validation and metadata
### Design Patterns Used
- Repository Pattern for data access
- Observer Pattern for event handling
- Command Pattern for Git operations
- Strategy Pattern for pluggable components
- Template Method for build processing
### Testing Strategy
- Unit tests in `src/test/java` directories
- Git operation tests with test repositories
- Component and integration tests
- Utility method tests
- Focus on testing business logic and Git operations
## Common Development Tasks
### Working with Entities
- Entities are in `server-core/src/main/java/io/onedev/server/model/`
- Use corresponding managers for database operations
- Follow JPA/Hibernate patterns for persistence
### Adding REST Endpoints
- Create resources in `server-core/src/main/java/io/onedev/server/rest/resource/`
- Follow Jersey/JAX-RS patterns
- Use existing security annotations for authentication
### Creating Plugins
- Extend `AbstractPlugin` class
- Implement appropriate interfaces for the plugin category
- Add Guice module configuration
- Place in appropriate `server-plugin/server-plugin-*` module
### Working with Git
- Use JGit APIs through OneDev's Git service layer
- Follow patterns in `server-core/src/main/java/io/onedev/server/git/`
- Handle Git operations asynchronously when possible
### Adding Web Components
- Create Wicket components in `server-core/src/main/java/io/onedev/server/web/`
- Follow existing component patterns and CSS frameworks
- Use AJAX for dynamic behavior
## Configuration and Deployment
### Key Configuration Files
- `server-product/system/conf/server.properties`: HTTP/SSH ports, clustering
- `server-product/system/conf/hibernate.properties`: Database configuration
- `server-product/system/conf/logback.xml`: Logging configuration
### Deployment Options
- Standalone JAR with embedded Jetty
- Docker containers (see `server-product/docker/`)
- Kubernetes via Helm charts (see `server-product/helm/`)
### Database Support
- PostgreSQL (recommended for production)
- MySQL/MariaDB
- HSQLDB (development/testing)
## Performance Considerations
### Caching
- Hibernate second-level cache with Hazelcast
- Build artifact caching
- Git object caching
- Web resource bundling
### Clustering
- Hazelcast-based clustering for high availability
- Distributed session management
- Leader election for coordinated operations
## Important Notes
- OneDev uses a custom plugin loading framework
- Git operations are central to the application architecture
- The system supports both community (CE) and enterprise (EE) editions
- Extensive use of Guice for dependency injection and plugin management
- Focus on performance and resource efficiency
- Battle-tested in production environments for 5+ years

View File

@ -71,6 +71,7 @@ import io.onedev.commons.utils.ExceptionUtils;
import io.onedev.commons.utils.StringUtils;
import io.onedev.k8shelper.KubernetesHelper;
import io.onedev.k8shelper.OsInfo;
import io.onedev.server.ai.McpHelperResource;
import io.onedev.server.attachment.AttachmentManager;
import io.onedev.server.attachment.DefaultAttachmentManager;
import io.onedev.server.buildspec.job.log.instruction.LogInstruction;
@ -647,6 +648,7 @@ public class CoreModule extends AbstractPluginModule {
contribute(FilterChainConfigurator.class, filterChainManager -> filterChainManager.createChain("/~api/**", "noSessionCreation, authcBasic, authcBearer"));
contribute(JerseyConfigurator.class, resourceConfig -> resourceConfig.packages(ProjectResource.class.getPackage().getName()));
contribute(JerseyConfigurator.class, resourceConfig -> resourceConfig.register(ClusterResource.class));
contribute(JerseyConfigurator.class, resourceConfig -> resourceConfig.register(McpHelperResource.class));
}
private void configureWeb() {

View File

@ -0,0 +1,986 @@
package io.onedev.server.ai;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.NotAcceptableException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Splitter;
import io.onedev.commons.utils.StringUtils;
import io.onedev.server.SubscriptionManager;
import io.onedev.server.entitymanager.IssueChangeManager;
import io.onedev.server.entitymanager.IssueCommentManager;
import io.onedev.server.entitymanager.IssueLinkManager;
import io.onedev.server.entitymanager.IssueManager;
import io.onedev.server.entitymanager.IssueWorkManager;
import io.onedev.server.entitymanager.IterationManager;
import io.onedev.server.entitymanager.LinkSpecManager;
import io.onedev.server.entitymanager.ProjectManager;
import io.onedev.server.entitymanager.SettingManager;
import io.onedev.server.entitymanager.UserManager;
import io.onedev.server.entityreference.IssueReference;
import io.onedev.server.exception.LinkValidationException;
import io.onedev.server.model.Issue;
import io.onedev.server.model.IssueComment;
import io.onedev.server.model.IssueLink;
import io.onedev.server.model.IssueSchedule;
import io.onedev.server.model.IssueWork;
import io.onedev.server.model.Iteration;
import io.onedev.server.model.Project;
import io.onedev.server.model.User;
import io.onedev.server.model.support.issue.field.EmptyFieldsException;
import io.onedev.server.model.support.issue.field.FieldUtils;
import io.onedev.server.model.support.issue.field.spec.BooleanField;
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.choicefield.ChoiceField;
import io.onedev.server.model.support.issue.field.spec.userchoicefield.UserChoiceField;
import io.onedev.server.model.support.issue.transitionspec.ManualSpec;
import io.onedev.server.rest.InvalidParamsException;
import io.onedev.server.rest.annotation.Api;
import io.onedev.server.rest.resource.support.RestConstants;
import io.onedev.server.search.entity.EntityQuery;
import io.onedev.server.search.entity.issue.IssueQuery;
import io.onedev.server.search.entity.issue.IssueQueryParseOption;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.util.DateUtils;
import io.onedev.server.util.ProjectScope;
@Api(internal = true)
@Path("/mcp-helper")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Singleton
public class McpHelperResource {
private final ObjectMapper objectMapper;
private final SettingManager settingManager;
private final UserManager userManager;
private final IssueManager issueManager;
private final ProjectManager projectManager;
private final LinkSpecManager linkSpecManager;
private final IssueLinkManager issueLinkManager;
private final IssueCommentManager issueCommentManager;
private final IterationManager iterationManager;
private final IssueChangeManager issueChangeManager;
private final IssueWorkManager issueWorkManager;
private final SubscriptionManager subscriptionManager;
@Inject
public McpHelperResource(ObjectMapper objectMapper, SettingManager settingManager,
UserManager userManager, IssueManager issueManager, ProjectManager projectManager,
LinkSpecManager linkSpecManager, IssueCommentManager issueCommentManager,
IterationManager iterationManager, SubscriptionManager subscriptionManager,
IssueChangeManager issueChangeManager, IssueLinkManager issueLinkManager,
IssueWorkManager issueWorkManager) {
this.objectMapper = objectMapper;
this.settingManager = settingManager;
this.issueManager = issueManager;
this.userManager = userManager;
this.projectManager = projectManager;
this.linkSpecManager = linkSpecManager;
this.issueCommentManager = issueCommentManager;
this.iterationManager = iterationManager;
this.subscriptionManager = subscriptionManager;
this.issueChangeManager = issueChangeManager;
this.issueLinkManager = issueLinkManager;
this.issueWorkManager = issueWorkManager;
}
private String getIssueQueryStringDescription() {
var stateNames = new StringBuilder();
for (var state: settingManager.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();
for (var field: settingManager.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");
} 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");
} else if (field instanceof GroupChoiceField) {
fieldCriterias.append("- " + field.getName().toLowerCase() + " criteria in form of: \""
+ field.getName() + "\" is \"<group name>\" (quotes are required)\n");
} 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");
} 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");
} 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");
} 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.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: linkSpecManager.query()) {
linkCriterias.append("- criteria to list issues with any " + linkSpec.getName().toLowerCase()
+ " issues 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()
+ " issues 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()
+ " issues 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()
+ " issues 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()
+ " issues 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()
+ " issues 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");
}
return
"A query string is one of below criteria:\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" +
"- Text based criteria in form of: ~<containing text>~\n" +
"- State criteria in form of: \"State\" is \"<state name>\" (quotes are required), where <state name> is one of below:\n" +
stateNames +
fieldCriterias +
linkCriterias +
"- submitter criteria in form of: \"Submitter\" is \"<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" +
"- and criteria in form of <criteria1> and <criteria2>\n" +
"- or criteria in form of <criteria1> or <criteria2>. Note that \"and criteria\" takes precedence over \"or criteria\", use braces to group \"or criteria\" like \"(criteria1 or criteria2) and criteria3\" if you want to override precedence\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" +
orderFields +
"\n" +
"Leave empty to search all issues";
}
private String getToolParamName(String fieldName) {
return fieldName.replace(" ", "_");
}
private String appendDescription(String description, String additionalDescription) {
if (description.length() > 0) {
if (description.endsWith("."))
return description + " " + additionalDescription;
else
return description + ". " + additionalDescription;
} else {
return additionalDescription;
}
}
private Project getProject(String projectPath) {
var project = projectManager.findByPath(projectPath);
if (project == null)
throw new NotFoundException("Project not found: " + projectPath);
if (!SecurityUtils.canAccessProject(project))
throw new UnauthorizedException("Unable to access project: " + projectPath);
return project;
}
private Map<String, Object> getFieldProperties(FieldSpec field) {
String fieldDescription;
if (field.getDescription() != null)
fieldDescription = field.getDescription().replace("\n", " ");
else
fieldDescription = "";
if (field instanceof ChoiceField) {
var choiceField = (ChoiceField) field;
if (field.isAllowMultiple())
fieldDescription = appendDescription(fieldDescription,
"Expects one or more of: " + String.join(", ", choiceField.getPossibleValues()));
else
fieldDescription = appendDescription(fieldDescription,
"Expects one of: " + String.join(", ", choiceField.getPossibleValues()));
} else if (field instanceof UserChoiceField) {
if (field.isAllowMultiple())
fieldDescription = appendDescription(fieldDescription, "Expects user login names");
else
fieldDescription = appendDescription(fieldDescription, "Expects user login name");
} else if (field instanceof GroupChoiceField) {
} else if (field instanceof BooleanField) {
fieldDescription = appendDescription(fieldDescription, "Expects boolean value, true or false");
} else if (field instanceof IntegerField) {
fieldDescription = appendDescription(fieldDescription, "Expects integer value");
} else if (field instanceof FloatField) {
fieldDescription = appendDescription(fieldDescription, "Expects float value");
} else if (field instanceof DateField || field instanceof DateTimeField) {
if (field.isAllowMultiple())
fieldDescription = appendDescription(fieldDescription,
"Expects unix timestamps in milliseconds since epoch");
else
fieldDescription = appendDescription(fieldDescription,
"Expects unix timestamp in milliseconds since epoch");
}
var fieldProperties = new HashMap<String, Object>();
if (field.isAllowMultiple()) {
fieldProperties.putAll(getArrayProperties(fieldDescription));
} else {
fieldProperties.put("type", "string");
fieldProperties.put("description", fieldDescription);
}
return fieldProperties;
}
private Map<String, Object> getArrayProperties(String description) {
return Map.of(
"type", "array",
"items", Map.of("type", "string"),
"uniqueItems", true,
"description", description);
}
@Path("/get-tool-input-schemas")
@GET
public Map<String, Object> getToolInputSchemas(@QueryParam("currentProject") @NotNull String currentProjectPath) {
if (SecurityUtils.getAuthUser() == null)
throw new UnauthenticatedException();
var currentProject = getProject(currentProjectPath);
Project.push(currentProject);
try {
var inputSchemas = new HashMap<String, Object>();
var queryIssuesInputSchema = new HashMap<String, Object>();
var queryIssuesProperties = new HashMap<String, Object>();
queryIssuesProperties.put("query", Map.of(
"type", "string",
"description", getIssueQueryStringDescription()));
queryIssuesProperties.put("offset", Map.of(
"type", "integer",
"description", "start position for the query (optional, defaults to 0)"));
queryIssuesProperties.put("count", Map.of(
"type", "integer",
"description", "number of issues to return (optional, defaults to 25, max 100)"));
queryIssuesInputSchema.put("Type", "object");
queryIssuesInputSchema.put("Properties", queryIssuesProperties);
queryIssuesInputSchema.put("Required", new ArrayList<>());
inputSchemas.put("queryIssues", queryIssuesInputSchema);
var createIssueInputSchema = new HashMap<String, Object>();
var createIssueProperties = new HashMap<String, Object>();
createIssueProperties.put("title", Map.of(
"type", "string",
"description", "title of the issue"));
createIssueProperties.put("description", Map.of(
"type", "string",
"description", "description of the issue"));
createIssueProperties.put("confidential", Map.of(
"type", "boolean",
"description", "whether the issue is confidential"));
if (SecurityUtils.canScheduleIssues(currentProject)) {
createIssueProperties.put("iterations", getArrayProperties("iteration names"));
if (subscriptionManager.isSubscriptionActive() && currentProject.isTimeTracking()) {
createIssueProperties.put("ownEstimatedTime", Map.of(
"type", "integer",
"description", "Estimated time in hours for this issue only (not including linked issues)"));
}
}
var createIssueRequiredProperties = new ArrayList<String>();
createIssueRequiredProperties.add("title");
for (var field: settingManager.getIssueSetting().getFieldSpecs()) {
if (field.isApplicable(currentProject)
&& field.isPromptUponIssueOpen()
&& SecurityUtils.canEditIssueField(currentProject, field.getName())) {
var paramName = getToolParamName(field.getName());
var fieldProperties = getFieldProperties(field);
createIssueProperties.put(paramName, fieldProperties);
if (!field.isAllowEmpty() && field.getShowCondition() == null) {
createIssueRequiredProperties.add(paramName);
}
}
}
createIssueInputSchema.put("Type", "object");
createIssueInputSchema.put("Properties", createIssueProperties);
createIssueInputSchema.put("Required", createIssueRequiredProperties);
inputSchemas.put("createIssue", createIssueInputSchema);
var updateIssueInputSchema = new HashMap<String, Object>();
var updateIssueProperties = new HashMap<String, Object>();
updateIssueProperties.put("issueReference", Map.of(
"type", "string",
"description", "reference of the issue to update"));
updateIssueProperties.put("title", Map.of(
"type", "string",
"description", "title of the issue"));
updateIssueProperties.put("description", Map.of(
"type", "string",
"description", "description of the issue"));
updateIssueProperties.put("confidential", Map.of(
"type", "boolean",
"description", "whether the issue is confidential"));
if (SecurityUtils.canScheduleIssues(currentProject)) {
updateIssueProperties.put("iterations", getArrayProperties("iterations to schedule the issue in"));
if (subscriptionManager.isSubscriptionActive() && currentProject.isTimeTracking()) {
updateIssueProperties.put("ownEstimatedTime", Map.of(
"type", "integer",
"description", "Estimated time in hours for this issue only (not including linked issues)"));
}
}
for (var field : settingManager.getIssueSetting().getFieldSpecs()) {
if (SecurityUtils.canEditIssueField(currentProject, field.getName())) {
var paramName = getToolParamName(field.getName());
var fieldProperties = getFieldProperties(field);
updateIssueProperties.put(paramName, fieldProperties);
}
}
updateIssueInputSchema.put("Type", "object");
updateIssueInputSchema.put("Properties", updateIssueProperties);
updateIssueInputSchema.put("Required", List.of("issueReference"));
inputSchemas.put("updateIssue", updateIssueInputSchema);
var toStates = new HashSet<String>();
for (var transition: settingManager.getIssueSetting().getTransitionSpecs()) {
if (transition instanceof ManualSpec) {
if (transition.getToStates().isEmpty()) {
toStates.addAll(settingManager.getIssueSetting().getStateSpecMap().keySet());
break;
} else {
toStates.addAll(transition.getToStates());
}
}
}
if (toStates.size() > 0) {
var transitionInputSchema = new HashMap<String, Object>();
transitionInputSchema.put("Type", "object");
var transitIssueProperties = new HashMap<String, Object>();
transitIssueProperties.put("issueReference", Map.of(
"type", "string",
"description", "reference of the issue to transit state"));
transitIssueProperties.put("state", Map.of(
"type", "string",
"description", "new state of the issue after transition. Must be one of: " + String.join(", ", toStates)));
transitIssueProperties.put("comment", Map.of(
"type", "string",
"description", "comment of the transition"));
for (var field : settingManager.getIssueSetting().getFieldSpecs()) {
if (SecurityUtils.canEditIssueField(currentProject, field.getName())) {
var paramName = getToolParamName(field.getName());
var fieldProperties = getFieldProperties(field);
transitIssueProperties.put(paramName, fieldProperties);
}
}
transitionInputSchema.put("Properties", transitIssueProperties);
transitionInputSchema.put("Required", List.of("issueReference", "state"));
inputSchemas.put("transitIssue", transitionInputSchema);
}
var linkSpecs = linkSpecManager.query();
if (!linkSpecs.isEmpty()) {
var linkInputSchema = new HashMap<String, Object>();
linkInputSchema.put("Type", "object");
var linkProperties = new HashMap<String, Object>();
linkProperties.put("sourceIssueReference", Map.of(
"type", "string",
"description", "Issue reference as source of the link"));
linkProperties.put("targetIssueReference", Map.of(
"type", "string",
"description", "Issue reference as target of the link"
));
var linkNames = new ArrayList<String>();
for (var linkSpec: linkSpecs) {
linkNames.add(linkSpec.getName());
if (linkSpec.getOpposite() != null)
linkNames.add(linkSpec.getOpposite().getName());
}
linkProperties.put("linkName", Map.of(
"type", "string",
"description", "Name of the link. Must be one of: " + String.join(", ", linkNames)));
linkInputSchema.put("Properties", linkProperties);
linkInputSchema.put("Required", List.of("sourceIssueReference", "targetIssueReference", "linkName"));
inputSchemas.put("linkIssues", linkInputSchema);
}
return inputSchemas;
} finally {
Project.pop();
}
}
@Path("/get-prompt-arguments")
@GET
public Map<String, Object> getPromptArguments(@QueryParam("currentProject") @NotNull String currentProjectPath) {
if (SecurityUtils.getAuthUser() == null)
throw new UnauthenticatedException();
var currentProject = getProject(currentProjectPath);
Project.push(currentProject);
try {
var arguments = new LinkedHashMap<String, Object>();
var createIssueArguments = new ArrayList<Map<String, Object>>();
createIssueArguments.add(Map.of(
"name", "title",
"description", "title of the issue",
"required", true));
createIssueArguments.add(Map.of(
"name", "description",
"description", "description of the issue",
"required", false));
createIssueArguments.add(Map.of(
"name", "confidential",
"description", "whether the issue is confidential",
"required", false));
if (SecurityUtils.canScheduleIssues(currentProject)) {
createIssueArguments.add(Map.of(
"name", "iterations",
"description", "iterations to schedule the issue in",
"required", false));
if (subscriptionManager.isSubscriptionActive() && currentProject.isTimeTracking()) {
createIssueArguments.add(Map.of(
"name", "ownEstimatedTime",
"description", "estimated time for this issue only in hours (excluding linked issues)",
"required", false));
}
}
for (var field: settingManager.getIssueSetting().getFieldSpecs()) {
if (field.isApplicable(currentProject)
&& field.isPromptUponIssueOpen()
&& field.getShowCondition() == null
&& SecurityUtils.canEditIssueField(currentProject, field.getName())) {
var paramName = getToolParamName(field.getName());
String description = "";
if (field.getDescription() != null)
description = field.getDescription().replace("\n", " ");
if (field instanceof ChoiceField) {
var choiceField = (ChoiceField) field;
if (field.isAllowMultiple())
description = appendDescription(description, "Expects one or more of: " + String.join(", ", choiceField.getPossibleValues()));
else
description = appendDescription(description, "Expects one of: " + String.join(", ", choiceField.getPossibleValues()));
}
var argumentMap = new HashMap<String, Object>();
argumentMap.put("name", paramName);
argumentMap.put("required", !field.isAllowEmpty());
if (description != null)
argumentMap.put("description", description);
createIssueArguments.add(argumentMap);
}
}
arguments.put("createIssue", createIssueArguments);
return arguments;
} finally {
Project.pop();
}
}
@Path("/get-login-name")
@GET
public String getLoginName(@QueryParam("userName") String userName) {
if (SecurityUtils.getAuthUser() == null)
throw new UnauthenticatedException();
User user;
if (userName != null) {
user = userManager.findByName(userName);
if (user == null)
user = userManager.findByFullName(userName);
if (user == null) {
var matchingUsers = new ArrayList<User>();
var lowerCaseUserName = userName.toLowerCase();
for (var eachUser: userManager.query()) {
if (eachUser.getFullName() != null) {
if (Splitter.on(" ").trimResults().omitEmptyStrings().splitToList(eachUser.getFullName().toLowerCase()).contains(lowerCaseUserName)) {
matchingUsers.add(eachUser);
}
}
}
if (matchingUsers.size() == 1) {
user = matchingUsers.get(0);
} else if (matchingUsers.size() > 1) {
throw new InvalidParamsException("Multiple users found: " + userName);
}
}
if (user == null)
throw new NotFoundException("User not found: " + userName);
} else {
user = SecurityUtils.getUser();
}
return user.getName();
}
@Path("/get-unix-timestamp")
@GET
public long getUnixTimestamp(@QueryParam("dateTimeDescription") @NotNull String dateTimeDescription) {
if (SecurityUtils.getAuthUser() == null)
throw new UnauthenticatedException();
return DateUtils.parseRelaxed(dateTimeDescription).getTime();
}
@Path("/query-issues")
@GET
public List<Map<String, Object>> queryIssues(@QueryParam("currentProject") @NotNull String currentProjectPath,
@QueryParam("query") String query, @QueryParam("offset") int offset, @QueryParam("count") int count) {
if (SecurityUtils.getAuthUser() == null)
throw new UnauthenticatedException();
if (count > RestConstants.MAX_PAGE_SIZE)
throw new InvalidParamsException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
var currentProject = getProject(currentProjectPath);
EntityQuery<Issue> parsedQuery;
if (query != null) {
var option = new IssueQueryParseOption();
option.withCurrentUserCriteria(true);
parsedQuery = IssueQuery.parse(currentProject, query, option, true);
} else {
parsedQuery = new IssueQuery();
}
var issues = new ArrayList<Map<String, Object>>();
for (var issue : issueManager.query(new ProjectScope(currentProject, true, false), parsedQuery, true, offset, count)) {
var issueMap = getIssueMap(currentProject, issue);
for (var entry: issue.getFieldInputs().entrySet()) {
issueMap.put(entry.getKey(), entry.getValue().getValues());
}
issues.add(issueMap);
}
return issues;
}
private Issue getIssue(Project currentProject, String referenceString) {
var issueReference = IssueReference.of(referenceString, currentProject);
var issue = issueManager.find(issueReference.getProject(), issueReference.getNumber());
if (issue != null) {
if (!SecurityUtils.canAccessIssue(issue))
throw new UnauthorizedException("No permission to access issue: " + referenceString);
return issue;
} else {
throw new NotFoundException("Issue not found: " + referenceString);
}
}
private Map<String, Object> getIssueMap(Project currentProject, Issue issue) {
var typeReference = new TypeReference<Map<String, Object>>() {};
var issueMap = objectMapper.convertValue(issue, typeReference);
issueMap.put("reference", issue.getReference().toString(currentProject));
issueMap.remove("submitterId");
issueMap.put("Submitter", issue.getSubmitter().getName());
issueMap.remove("projectId");
issueMap.put("Project", issue.getProject().getPath());
return issueMap;
}
@Path("/get-issue-detail")
@GET
public Map<String, Object> getIssueDetail(@QueryParam("currentProject") @NotNull String currentProjectPath,
@QueryParam("reference") @NotNull String issueReference) {
if (SecurityUtils.getAuthUser() == null)
throw new UnauthenticatedException();
var currentProject = getProject(currentProjectPath);
var issue = getIssue(currentProject, issueReference);
var issueMap = getIssueMap(currentProject, issue);
for (var entry : issue.getFieldInputs().entrySet()) {
issueMap.put(entry.getKey(), entry.getValue().getValues());
}
issueMap.put("comments", issue.getComments());
Map<String, Collection<String>> linkedIssues = new HashMap<>();
for (var link: issue.getTargetLinks()) {
linkedIssues.computeIfAbsent(link.getSpec().getName(), k -> new ArrayList<>())
.add(link.getTarget().getReference().toString(currentProject));
}
for (var link : issue.getSourceLinks()) {
if (link.getSpec().getOpposite() != null) {
linkedIssues.computeIfAbsent(link.getSpec().getOpposite().getName(), k -> new ArrayList<>())
.add(link.getSource().getReference().toString(currentProject));
} else {
linkedIssues.computeIfAbsent(link.getSpec().getName(), k -> new ArrayList<>())
.add(link.getSource().getReference().toString(currentProject));
}
}
issueMap.putAll(linkedIssues);
return issueMap;
}
@Path("/add-issue-comment")
@Consumes(MediaType.TEXT_PLAIN)
@POST
public String addIssueComment(@QueryParam("currentProject") String currentProjectPath,
@QueryParam("reference") @NotNull String issueReference, @NotNull String commentContent) {
if (SecurityUtils.getAuthUser() == null)
throw new UnauthenticatedException();
var currentProject = getProject(currentProjectPath);
var issue = getIssue(currentProject, issueReference);
var comment = new IssueComment();
comment.setIssue(issue);
comment.setContent(commentContent);
comment.setUser(SecurityUtils.getAuthUser());
comment.setDate(new Date());
issueCommentManager.create(comment);
return "Commented on issue " + issueReference;
}
@Path("/create-issue")
@POST
public String createIssue(@QueryParam("currentProject") @NotNull String currentProjectPath,
@NotNull @Valid Map<String, Serializable> data) {
if (SecurityUtils.getAuthUser() == null)
throw new UnauthenticatedException();
var currentProject = getProject(currentProjectPath);
normalizeData(data);
var issueSetting = settingManager.getIssueSetting();
Issue issue = new Issue();
var title = (String) data.remove("title");
if (title == null)
throw new InvalidParamsException("title is required");
issue.setTitle(title);
var description = (String) data.remove("description");
issue.setDescription(description);
var confidential = (Boolean) data.remove("confidential");
if (confidential != null)
issue.setConfidential(confidential);
Integer ownEstimatedTime = (Integer) data.remove("ownEstimatedTime");
if (ownEstimatedTime != null) {
if (!subscriptionManager.isSubscriptionActive())
throw new NotAcceptableException("An active subscription is required for this feature");
if (!currentProject.isTimeTracking())
throw new NotAcceptableException("Time tracking needs to be enabled for the project");
if (!SecurityUtils.canScheduleIssues(currentProject))
throw new UnauthorizedException("Issue schedule permission required to set own estimated time");
issue.setOwnEstimatedTime(ownEstimatedTime*60);
}
@SuppressWarnings("unchecked")
List<String> iterationNames = (List<String>) data.remove("iterations");
if (iterationNames != null) {
if (!SecurityUtils.canScheduleIssues(currentProject))
throw new UnauthorizedException("Issue schedule permission required to set iterations");
for (var iterationName : iterationNames) {
var iteration = iterationManager.findInHierarchy(currentProject, iterationName);
if (iteration == null)
throw new NotFoundException("Iteration '" + iterationName + "' not found");
IssueSchedule schedule = new IssueSchedule();
schedule.setIssue(issue);
schedule.setIteration(iteration);
issue.getSchedules().add(schedule);
}
}
issue.setProject(currentProject);
issue.setSubmitDate(new Date());
issue.setSubmitter(SecurityUtils.getAuthUser());
issue.setState(issueSetting.getInitialStateSpec().getName());
issue.setFieldValues(FieldUtils.getFieldValues(issue.getProject(), data));
try {
issueManager.open(issue);
} catch (EmptyFieldsException e) {
throw new InvalidParamsException("Missing parameters: " + String.join(", ", e.getEmptyFields()));
}
return "Created issue " + issue.getReference().toString(currentProject);
}
@Path("/update-issue")
@POST
public String updateIssue(@QueryParam("currentProject") @NotNull String currentProjectPath,
@QueryParam("reference") @NotNull String issueReference, @NotNull Map<String, Serializable> data) {
if (SecurityUtils.getAuthUser() == null)
throw new UnauthenticatedException();
var currentProject = getProject(currentProjectPath);
var issue = getIssue(currentProject, issueReference);
if (!SecurityUtils.canModifyIssue(issue))
throw new UnauthorizedException();
normalizeData(data);
var title = (String) data.remove("title");
if (title != null)
issueChangeManager.changeTitle(issue, title);
if (data.containsKey("description"))
issueChangeManager.changeDescription(issue, (String) data.remove("description"));
var confidential = (Boolean) data.remove("confidential");
if (confidential != null)
issueChangeManager.changeConfidential(issue, confidential);
Integer ownEstimatedTime = (Integer) data.remove("ownEstimatedTime");
if (ownEstimatedTime != null) {
if (!subscriptionManager.isSubscriptionActive())
throw new NotAcceptableException("An active subscription is required for this feature");
if (!issue.getProject().isTimeTracking())
throw new NotAcceptableException("Time tracking needs to be enabled for the project");
if (!SecurityUtils.canScheduleIssues(issue.getProject()))
throw new UnauthorizedException("Issue schedule permission required to set own estimated time");
issueChangeManager.changeOwnEstimatedTime(issue, ownEstimatedTime*60);
}
@SuppressWarnings("unchecked")
List<String> iterationNames = (List<String>) data.remove("iterations");
if (iterationNames != null) {
if (!SecurityUtils.canScheduleIssues(issue.getProject()))
throw new UnauthorizedException("Issue schedule permission required to set iterations");
var iterations = new ArrayList<Iteration>();
for (var iterationName : iterationNames) {
var iteration = iterationManager.findInHierarchy(issue.getProject(), iterationName);
if (iteration == null)
throw new NotFoundException("Iteration '" + iterationName + "' not found");
iterations.add(iteration);
}
issueChangeManager.changeIterations(issue, iterations);
}
if (!data.isEmpty()) {
var issueSetting = settingManager.getIssueSetting();
String initialState = issueSetting.getInitialStateSpec().getName();
if (!SecurityUtils.canManageIssues(issue.getProject())
&& !(issue.getSubmitter().equals(SecurityUtils.getAuthUser())
&& issue.getState().equals(initialState))) {
throw new UnauthorizedException("No permission to update issue fields");
}
try {
issueChangeManager.changeFields(issue, FieldUtils.getFieldValues(issue.getProject(), data));
} catch (EmptyFieldsException e) {
throw new InvalidParamsException("Missing parameters: " + String.join(", ", e.getEmptyFields()));
}
}
return "Updated issue " + issueReference;
}
@Path("/transit-issue")
@POST
public String transitIssue(@QueryParam("currentProject") @NotNull String currentProjectPath,
@QueryParam("reference") @NotNull String issueReference, @NotNull Map<String, Serializable> data) {
if (SecurityUtils.getAuthUser() == null)
throw new UnauthenticatedException();
var currentProject = getProject(currentProjectPath);
var issue = getIssue(currentProject, issueReference);
normalizeData(data);
var state = (String) data.remove("state");
if (state == null)
throw new InvalidParamsException("state is required");
var comment = (String) data.remove("comment");
ManualSpec transition = settingManager.getIssueSetting().getManualSpec(issue, state);
var fieldValues = FieldUtils.getFieldValues(issue.getProject(), data);
try {
issueChangeManager.changeState(issue, state, fieldValues, transition.getPromptFields(),
transition.getRemoveFields(), comment);
} catch (EmptyFieldsException e) {
throw new InvalidParamsException("Missing parameters: " + String.join(", ", e.getEmptyFields()));
}
return "Issue " + issueReference + " transited to state \"" + state + "\"";
}
@Path("/link-issues")
@GET
public String linkIssues(@QueryParam("currentProject") @NotNull String currentProjectPath,
@QueryParam("sourceReference") @NotNull String sourceReference,
@QueryParam("linkName") @Nullable String linkName,
@QueryParam("targetReference") @NotNull String targetReference) {
if (SecurityUtils.getAuthUser() == null)
throw new UnauthenticatedException();
var currentProject = getProject(currentProjectPath);
var sourceIssue = getIssue(currentProject, sourceReference);
var targetIssue = getIssue(currentProject, targetReference);
var linkSpec = linkSpecManager.find(linkName);
if (linkSpec == null)
throw new NotFoundException("Link spec not found: " + linkName);
if (!SecurityUtils.canEditIssueLink(sourceIssue.getProject(), linkSpec)
|| !SecurityUtils.canEditIssueLink(targetIssue.getProject(), linkSpec)) {
throw new UnauthorizedException("No permission to add specified link for specified issues");
}
var link = new IssueLink();
link.setSpec(linkSpec);
if (linkName.equals(linkSpec.getName())) {
link.setSource(sourceIssue);
link.setTarget(targetIssue);
} else {
link.setSource(targetIssue);
link.setTarget(sourceIssue);
}
try {
link.validate();
} catch (LinkValidationException e) {
throw new NotAcceptableException(e.getMessage());
}
issueLinkManager.create(link);
return "Issue " + targetReference + " added as \"" + linkName + "\" of " + sourceReference;
}
@Path("/log-work")
@Consumes(MediaType.TEXT_PLAIN)
@POST
public String logWork(@QueryParam("currentProject") @NotNull String currentProjectPath,
@QueryParam("reference") @NotNull String issueReference,
@QueryParam("spentHours") int spentHours, @Nullable String comment) {
if (SecurityUtils.getAuthUser() == null)
throw new UnauthenticatedException();
var currentProject = getProject(currentProjectPath);
var issue = getIssue(currentProject, issueReference);
if (!subscriptionManager.isSubscriptionActive())
throw new NotAcceptableException("An active subscription is required for this feature");
if (!issue.getProject().isTimeTracking())
throw new NotAcceptableException("Time tracking needs to be enabled for the project");
if (!SecurityUtils.canAccessIssue(issue))
throw new UnauthorizedException("No permission to access issue: " + issueReference);
var work = new IssueWork();
work.setIssue(issue);
work.setUser(SecurityUtils.getAuthUser());
work.setMinutes(spentHours * 60);
work.setNote(StringUtils.trimToNull(comment));
issueWorkManager.createOrUpdate(work);
return "Work logged for issue " + issueReference;
}
private void normalizeData(Map<String, Serializable> data) {
for (var entry: data.entrySet()) {
if (entry.getValue() instanceof String)
entry.setValue(StringUtils.trimToNull((String) entry.getValue()));
}
for (var field: settingManager.getIssueSetting().getFieldSpecs()) {
var paramName = getToolParamName(field.getName());
if (!paramName.equals(field.getName()) && data.containsKey(paramName)) {
data.put(field.getName(), data.get(paramName));
data.remove(paramName);
}
}
}
}

View File

@ -14,7 +14,7 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import io.onedev.server.buildspec.param.spec.ParamSpec;
import io.onedev.server.util.Input;
import io.onedev.server.buildspecmodel.inputspec.Input;
public class ParamCombination implements Serializable {

View File

@ -5,8 +5,9 @@ import io.onedev.server.annotation.ChoiceProvider;
import io.onedev.server.annotation.Editable;
import io.onedev.server.buildspec.param.ParamCombination;
import io.onedev.server.buildspec.param.spec.ParamSpec;
import io.onedev.server.buildspecmodel.inputspec.Input;
import io.onedev.server.model.Build;
import io.onedev.server.util.Input;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

View File

@ -11,8 +11,8 @@ import javax.validation.constraints.NotEmpty;
import io.onedev.commons.utils.ExplicitException;
import io.onedev.server.buildspec.param.ParamCombination;
import io.onedev.server.buildspec.param.spec.ParamSpec;
import io.onedev.server.buildspecmodel.inputspec.Input;
import io.onedev.server.model.Build;
import io.onedev.server.util.Input;
import io.onedev.server.annotation.ChoiceProvider;
import io.onedev.server.annotation.Editable;

View File

@ -1,4 +1,4 @@
package io.onedev.server.util;
package io.onedev.server.buildspecmodel.inputspec;
import java.io.Serializable;
import java.util.List;
@ -11,8 +11,6 @@ import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.onedev.server.buildspecmodel.inputspec.InputSpec;
import io.onedev.server.buildspecmodel.inputspec.SecretInput;
import io.onedev.server.model.support.issue.field.spec.FieldSpec;
public class Input implements Serializable {
@ -82,7 +80,7 @@ public class Input implements Serializable {
else
displayValue = "" + getValues();
logger.error("Error converting field value (field: {}, value: {}, error: {})",
logger.error("Error converting input (name: {}, value: {}, error: {})",
getName(), displayValue, e.getMessage());
return null;
}

View File

@ -51,7 +51,7 @@ public interface IssueChangeManager extends EntityManager<IssueChange> {
void removeSchedule(Issue issue, Iteration iteration);
void changeState(Issue issue, String state, Map<String, Object> fieldValues,
Collection<String> removeFields, @Nullable String comment);
Collection<String> promptFields, Collection<String> removeFields, @Nullable String comment);
void batchUpdate(Iterator<? extends Issue> issues, @Nullable String state, @Nullable Boolean confidential,
@Nullable Collection<Iteration> iterations, Map<String, Object> fieldValues,

View File

@ -41,6 +41,7 @@ import io.onedev.commons.utils.match.Matcher;
import io.onedev.commons.utils.match.PathMatcher;
import io.onedev.commons.utils.match.StringMatcher;
import io.onedev.server.OneDev;
import io.onedev.server.buildspecmodel.inputspec.Input;
import io.onedev.server.cluster.ClusterManager;
import io.onedev.server.entitymanager.IssueChangeManager;
import io.onedev.server.entitymanager.IssueDescriptionRevisionManager;
@ -86,6 +87,7 @@ import io.onedev.server.model.support.issue.changedata.IssueStateChangeData;
import io.onedev.server.model.support.issue.changedata.IssueTitleChangeData;
import io.onedev.server.model.support.issue.changedata.IssueTotalEstimatedTimeChangeData;
import io.onedev.server.model.support.issue.changedata.IssueTotalSpentTimeChangeData;
import io.onedev.server.model.support.issue.field.EmptyFieldsException;
import io.onedev.server.model.support.issue.transitionspec.BranchUpdatedSpec;
import io.onedev.server.model.support.issue.transitionspec.BuildSuccessfulSpec;
import io.onedev.server.model.support.issue.transitionspec.IssueStateTransitedSpec;
@ -112,7 +114,6 @@ import io.onedev.server.search.entity.issue.StateCriteria;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.taskschedule.SchedulableTask;
import io.onedev.server.taskschedule.TaskScheduler;
import io.onedev.server.util.Input;
import io.onedev.server.util.ProjectScope;
import io.onedev.server.util.ProjectScopedCommit;
import io.onedev.server.util.concurrent.BatchWorkManager;
@ -377,13 +378,14 @@ public class DefaultIssueChangeManager extends BaseEntityManager<IssueChange>
create(change, null, sendNotifications);
}
}
@Transactional
@Override
public void changeFields(Issue issue, Map<String, Object> fieldValues) {
Map<String, Input> prevFields = issue.getFieldInputs();
issue.setFieldValues(fieldValues);
if (!prevFields.equals(issue.getFieldInputs())) {
issue.checkEmptyFields();
if (!prevFields.equals(issue.getFieldInputs())) {
issueFieldManager.saveFields(issue);
IssueChange change = new IssueChange();
@ -397,14 +399,16 @@ public class DefaultIssueChangeManager extends BaseEntityManager<IssueChange>
@Transactional
@Override
public void changeState(Issue issue, String state, Map<String, Object> fieldValues,
Collection<String> removeFields, @Nullable String comment) {
Collection<String> promptFields, Collection<String> removeFields, @Nullable String comment) {
String prevState = issue.getState();
Map<String, Input> prevFields = issue.getFieldInputs();
issue.setState(state);
issue.removeFields(removeFields);
issue.setFieldValues(fieldValues);
issue.addMissingFields(promptFields);
issue.checkEmptyFields();
issueFieldManager.saveFields(issue);
IssueChange change = new IssueChange();
@ -436,6 +440,12 @@ public class DefaultIssueChangeManager extends BaseEntityManager<IssueChange>
issueScheduleManager.syncIterations(issue, iterations);
issue.setFieldValues(fieldValues);
try {
issue.checkEmptyFields();
} catch (EmptyFieldsException e) {
Collection<String> emptyFields = e.getEmptyFields();
throw new EmptyFieldsException("The following fields must be set for issue " + issue.getReference().toString(issue.getProject()) + ": " + String.join(", ", emptyFields), emptyFields);
}
issueFieldManager.saveFields(issue);
if (!prevState.equals(issue.getState())
@ -496,7 +506,7 @@ public class DefaultIssueChangeManager extends BaseEntityManager<IssueChange>
String message = "State changed as issue " + issue.getReference().toString(each.getProject())
+ " transited to '" + issue.getState() + "'";
changeState(each, issueStateTransitedSpec.getToState(), new HashMap<>(),
transition.getRemoveFields(), message);
new ArrayList<>(), transition.getRemoveFields(), message);
}
} finally {
Issue.pop();
@ -545,7 +555,7 @@ public class DefaultIssueChangeManager extends BaseEntityManager<IssueChange>
for (Issue issue: issueManager.query(projectScope, query, true, 0, MAX_VALUE)) {
String message = "State changed as build " + build.getReference().toString(issue.getProject()) + " is successful";
changeState(issue, buildSuccessfulSpec.getToState(), new HashMap<>(),
transition.getRemoveFields(), message);
new ArrayList<>(), transition.getRemoveFields(), message);
}
} finally {
Build.pop();
@ -585,8 +595,7 @@ public class DefaultIssueChangeManager extends BaseEntityManager<IssueChange>
try {
for (Issue issue: issueManager.query(projectScope, query, true, 0, MAX_VALUE)) {
String statusName = request.getStatus().toString().toLowerCase();
changeState(issue, pullRequestSpec.getToState(), new HashMap<>(),
transition.getRemoveFields(),
changeState(issue, pullRequestSpec.getToState(), new HashMap<>(), new ArrayList<>(), transition.getRemoveFields(),
"State changed as pull request " + request.getReference().toString(issue.getProject()) + " is " + statusName);
}
} finally {
@ -697,8 +706,7 @@ public class DefaultIssueChangeManager extends BaseEntityManager<IssueChange>
String commitFQN = commit.name();
if (!project.equals(issue.getProject()))
commitFQN = project.getPath() + ":" + commitFQN;
changeState(issue, branchUpdatedSpec.getToState(), new HashMap<>(),
transition.getRemoveFields(),
changeState(issue, branchUpdatedSpec.getToState(), new HashMap<>(), new ArrayList<>(), transition.getRemoveFields(),
"State changed as code fixing the issue is committed (" + commitFQN + ")");
}
}
@ -745,7 +753,7 @@ public class DefaultIssueChangeManager extends BaseEntityManager<IssueChange>
query = new IssueQuery(Criteria.andCriterias(criterias), new ArrayList<>());
for (Issue issue: issueManager.query(null, query, true, 0, MAX_VALUE)) {
changeState(issue, noActivitySpec.getToState(), new HashMap<>(),
changeState(issue, noActivitySpec.getToState(), new HashMap<>(), new ArrayList<>(),
transition.getRemoveFields(), null);
}
}

View File

@ -241,7 +241,10 @@ public class DefaultIssueManager extends BaseEntityManager<Issue> implements Iss
lastActivity.setDescription("opened");
lastActivity.setDate(issue.getSubmitDate());
issue.setLastActivity(lastActivity);
issue.addMissingFields(settingManager.getIssueSetting().getPromptFieldsUponIssueOpen(issue.getProject()));
issue.checkEmptyFields();
dao.persist(issue);
fieldManager.saveFields(issue);

View File

@ -1,6 +1,7 @@
package io.onedev.server.event.project.issue;
import io.onedev.server.OneDev;
import io.onedev.server.buildspecmodel.inputspec.Input;
import io.onedev.server.entitymanager.GroupManager;
import io.onedev.server.entitymanager.UserManager;
import io.onedev.server.model.Group;
@ -8,7 +9,6 @@ import io.onedev.server.model.Issue;
import io.onedev.server.model.User;
import io.onedev.server.model.support.issue.field.spec.FieldSpec;
import io.onedev.server.util.CommitAware;
import io.onedev.server.util.Input;
import io.onedev.server.util.ProjectScopedCommit;
import io.onedev.server.util.commenttext.CommentText;
import io.onedev.server.util.commenttext.MarkdownText;

View File

@ -0,0 +1,11 @@
package io.onedev.server.exception;
import io.onedev.commons.utils.ExplicitException;
public class LinkValidationException extends ExplicitException {
public LinkValidationException(String message) {
super(message);
}
}

View File

@ -76,6 +76,7 @@ import io.onedev.server.buildspec.job.Job;
import io.onedev.server.buildspec.param.ParamCombination;
import io.onedev.server.buildspec.param.ParamUtils;
import io.onedev.server.buildspec.param.spec.ParamSpec;
import io.onedev.server.buildspecmodel.inputspec.Input;
import io.onedev.server.buildspecmodel.inputspec.SecretInput;
import io.onedev.server.cluster.ClusterManager;
import io.onedev.server.entitymanager.AccessTokenManager;
@ -94,7 +95,6 @@ import io.onedev.server.search.entity.SortField;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.util.ComponentContext;
import io.onedev.server.util.FilenameUtils;
import io.onedev.server.util.Input;
import io.onedev.server.util.artifact.ArtifactInfo;
import io.onedev.server.util.artifact.DirectoryInfo;
import io.onedev.server.util.criteria.Criteria;

View File

@ -23,6 +23,7 @@ import static java.util.Comparator.comparing;
import static java.util.Comparator.comparingInt;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
@ -70,6 +71,7 @@ import io.onedev.commons.utils.ExplicitException;
import io.onedev.server.OneDev;
import io.onedev.server.annotation.Editable;
import io.onedev.server.attachment.AttachmentStorageSupport;
import io.onedev.server.buildspecmodel.inputspec.Input;
import io.onedev.server.buildspecmodel.inputspec.InputSpec;
import io.onedev.server.entitymanager.GroupManager;
import io.onedev.server.entitymanager.PullRequestManager;
@ -81,13 +83,14 @@ import io.onedev.server.model.support.EntityWatch;
import io.onedev.server.model.support.LastActivity;
import io.onedev.server.model.support.ProjectBelonging;
import io.onedev.server.model.support.administration.GlobalIssueSetting;
import io.onedev.server.model.support.issue.field.FieldUtils;
import io.onedev.server.model.support.issue.field.EmptyFieldsException;
import io.onedev.server.model.support.issue.field.spec.FieldSpec;
import io.onedev.server.rest.annotation.Api;
import io.onedev.server.search.entity.EntitySort;
import io.onedev.server.search.entity.IssueSortField;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.util.ComponentContext;
import io.onedev.server.util.Input;
import io.onedev.server.util.ProjectScopedCommit;
import io.onedev.server.util.facade.IssueFacade;
import io.onedev.server.web.UrlManager;
@ -1023,6 +1026,37 @@ public class Issue extends ProjectBelonging implements AttachmentStorageSupport
public boolean isFieldVisible(String fieldName) {
return isFieldVisible(fieldName, Sets.newHashSet());
}
public void addMissingFields(Collection<String> fieldNames) {
try {
var fieldBean = FieldUtils.getFieldBeanClass().getConstructor().newInstance();
var existingFieldNames = getFieldNames();
var fieldValues = FieldUtils.getFieldValues(null, fieldBean, fieldNames);
for (var entry: fieldValues.entrySet()) {
if (!existingFieldNames.contains(entry.getKey()))
setFieldValue(entry.getKey(), entry.getValue());
}
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException | NoSuchMethodException | SecurityException e) {
throw new RuntimeException(e);
}
}
public void checkEmptyFields() {
var emptyFields = new ArrayList<String>();
for (var fieldName: getFieldNames()) {
var field = getIssueSetting().getFieldSpec(fieldName);
if (field != null
&& !field.isAllowEmpty()
&& isFieldVisible(fieldName)
&& SecurityUtils.canEditIssueField(getProject(), fieldName)
&& getFieldValue(fieldName) == null) {
emptyFields.add(field.getName());
}
}
if (emptyFields.size() > 0)
throw new EmptyFieldsException(emptyFields);
}
public List<User> getParticipants() {
if (participants == null) {

View File

@ -8,6 +8,8 @@ import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import io.onedev.server.exception.LinkValidationException;
@Entity
@Table(
indexes={@Index(columnList="o_source_id"), @Index(columnList="o_target_id"), @Index(columnList="o_spec_id")},
@ -62,4 +64,37 @@ public class IssueLink extends AbstractEntity {
return issue.equals(source)?target:source;
}
public void validate() {
if (getSource().equals(getTarget()))
throw new LinkValidationException("Can not link to self");
if (getSpec().getOpposite() != null) {
if (getSource().getTargetLinks().stream()
.anyMatch(it -> it.getSpec().equals(getSpec()) && it.getTarget().equals(getTarget())))
throw new LinkValidationException("Source issue already linked to target issue via specified link spec");
if (!getSpec().isMultiple()
&& getSource().getTargetLinks().stream().anyMatch(it -> it.getSpec().equals(getSpec())))
throw new LinkValidationException(
"Link spec is not multiple and the source issue is already linked to another issue via this link spec");
if (!getSpec().getParsedIssueQuery(getSource().getProject()).matches(getTarget()))
throw new LinkValidationException("Link spec not allowed to link to the target issue");
if (!getSpec().getOpposite().isMultiple()
&& getTarget().getSourceLinks().stream().anyMatch(it -> it.getSpec().equals(getSpec())))
throw new LinkValidationException(
"Opposite side of link spec is not multiple and the target issue is already linked to another issue via this link spec");
if (!getSpec().getOpposite().getParsedIssueQuery(getSource().getProject())
.matches(getSource()))
throw new LinkValidationException("Opposite side of link spec not allowed to link to the source issue");
} else {
if (getSource().getLinks().stream().anyMatch(it -> it.getSpec().equals(getSpec())
&& it.getLinked(getSource()).equals(getTarget())))
throw new LinkValidationException("Specified issues already linked via specified link spec");
if (!getSpec().isMultiple()
&& getSource().getLinks().stream().anyMatch(it -> it.getSpec().equals(getSpec())))
throw new LinkValidationException(
"Link spec is not multiple and source issue is already linked to another issue via this link spec");
var parsedIssueQuery = getSpec().getParsedIssueQuery(getSource().getProject());
if (!parsedIssueQuery.matches(getSource()) || !parsedIssueQuery.matches(getTarget()))
throw new LinkValidationException("Link spec not allowed to link specified issues");
}
}
}

View File

@ -1,6 +1,7 @@
package io.onedev.server.model.support.administration;
import java.io.Serializable;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
@ -17,8 +18,6 @@ import com.google.common.collect.Lists;
import edu.emory.mathcs.backport.java.util.Collections;
import io.onedev.commons.utils.ExplicitException;
import io.onedev.commons.utils.match.Matcher;
import io.onedev.commons.utils.match.PathMatcher;
import io.onedev.server.annotation.Editable;
import io.onedev.server.buildspecmodel.inputspec.choiceinput.choiceprovider.Choice;
import io.onedev.server.buildspecmodel.inputspec.choiceinput.choiceprovider.SpecifiedChoices;
@ -41,9 +40,9 @@ import io.onedev.server.model.support.issue.transitionspec.BranchUpdatedSpec;
import io.onedev.server.model.support.issue.transitionspec.IssueStateTransitedSpec;
import io.onedev.server.model.support.issue.transitionspec.ManualSpec;
import io.onedev.server.model.support.issue.transitionspec.TransitionSpec;
import io.onedev.server.rest.InvalidParamsException;
import io.onedev.server.search.entity.issue.IssueQuery;
import io.onedev.server.search.entity.issue.IssueQueryParseOption;
import io.onedev.server.util.patternset.PatternSet;
import io.onedev.server.util.usage.Usage;
import io.onedev.server.web.component.issue.workflowreconcile.ReconcileUtils;
import io.onedev.server.web.component.issue.workflowreconcile.UndefinedFieldResolution;
@ -750,17 +749,31 @@ public class GlobalIssueSetting implements Serializable {
}
return null;
}
public Collection<String> getPromptFieldsUponIssueOpen(Project project) {
Matcher matcher = new PathMatcher();
return getFieldSpecs().stream()
.filter(it->it.isPromptUponIssueOpen() && (it.getApplicableProjects() == null || PatternSet.parse(it.getApplicableProjects()).matches(matcher, project.getPath())))
.map(it->it.getName())
.collect(Collectors.toList());
}
public int getStateOrdinal(String state) {
return getStateSpecs().indexOf(getStateSpec(state));
}
public ManualSpec getManualSpec(Issue issue, String state) {
for (var transition: getTransitionSpecs()) {
if (transition instanceof ManualSpec) {
var manualSpec = (ManualSpec) transition;
if (manualSpec.canTransit(issue, state) && manualSpec.isAuthorized(issue)) {
return manualSpec;
}
}
}
var message = MessageFormat.format(
"No applicable manual transition spec found for current user (issue: {0}, from state: {1}, to state: {2})",
issue.getReference().toString(), issue.getState(), state);
throw new InvalidParamsException(message);
}
public Collection<String> getPromptFieldsUponIssueOpen(Project project) {
return getFieldSpecs().stream()
.filter(it -> it.isPromptUponIssueOpen() && it.isApplicable(project))
.map(it -> it.getName())
.collect(Collectors.toList());
}
}

View File

@ -6,8 +6,8 @@ import java.util.Map;
import java.util.stream.Collectors;
import io.onedev.commons.utils.StringUtils;
import io.onedev.server.buildspecmodel.inputspec.Input;
import io.onedev.server.model.Iteration;
import io.onedev.server.util.Input;
public class IssueBatchUpdateData extends IssueFieldChangeData {

View File

@ -12,6 +12,7 @@ import java.util.stream.Collectors;
import io.onedev.commons.utils.StringUtils;
import io.onedev.server.OneDev;
import io.onedev.server.buildspecmodel.inputspec.Input;
import io.onedev.server.buildspecmodel.inputspec.InputSpec;
import io.onedev.server.entitymanager.GroupManager;
import io.onedev.server.entitymanager.UserManager;
@ -20,7 +21,6 @@ import io.onedev.server.model.User;
import io.onedev.server.model.support.issue.field.spec.FieldSpec;
import io.onedev.server.notification.ActivityDetail;
import io.onedev.server.util.DateUtils;
import io.onedev.server.util.Input;
public class IssueFieldChangeData extends IssueChangeData {

View File

@ -3,8 +3,8 @@ package io.onedev.server.model.support.issue.changedata;
import java.util.LinkedHashMap;
import java.util.Map;
import io.onedev.server.buildspecmodel.inputspec.Input;
import io.onedev.server.notification.ActivityDetail;
import io.onedev.server.util.Input;
public class IssueStateChangeData extends IssueFieldChangeData {

View File

@ -0,0 +1,24 @@
package io.onedev.server.model.support.issue.field;
import java.util.Collection;
import io.onedev.commons.utils.ExplicitException;
public class EmptyFieldsException extends ExplicitException {
private final Collection<String> emptyFields;
public EmptyFieldsException(Collection<String> emptyFields) {
this("The following fields must be set: " + String.join(", ", emptyFields), emptyFields);
}
public EmptyFieldsException(String message, Collection<String> emptyFields) {
super(message);
this.emptyFields = emptyFields;
}
public Collection<String> getEmptyFields() {
return emptyFields;
}
}

View File

@ -1,6 +1,7 @@
package io.onedev.server.model.support.issue.field;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
@ -9,7 +10,9 @@ import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.validation.ValidationException;
import javax.ws.rs.BadRequestException;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.wicket.Component;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.MetaDataKey;
@ -20,16 +23,16 @@ import org.slf4j.LoggerFactory;
import com.google.common.base.Preconditions;
import io.onedev.server.OneDev;
import io.onedev.server.entitymanager.SettingManager;
import io.onedev.server.model.Project;
import io.onedev.server.model.support.administration.GlobalIssueSetting;
import io.onedev.server.buildspecmodel.inputspec.InputContext;
import io.onedev.server.buildspecmodel.inputspec.InputSpec;
import io.onedev.server.buildspecmodel.inputspec.SecretInput;
import io.onedev.server.model.support.issue.field.spec.FieldSpec;
import io.onedev.server.model.support.issue.field.spec.SecretField;
import io.onedev.server.entitymanager.SettingManager;
import io.onedev.server.model.Project;
import io.onedev.server.model.support.administration.GlobalIssueSetting;
import io.onedev.server.model.support.issue.field.instance.FieldInstance;
import io.onedev.server.model.support.issue.field.instance.SpecifiedValue;
import io.onedev.server.model.support.issue.field.spec.FieldSpec;
import io.onedev.server.model.support.issue.field.spec.SecretField;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.util.ComponentContext;
import io.onedev.server.util.EditContext;
@ -103,8 +106,9 @@ public class FieldUtils {
return null;
}
public static Map<String, Object> getFieldValues(ComponentContext context, Serializable fieldBean, Collection<String> fieldNames) {
ComponentContext.push(context);
public static Map<String, Object> getFieldValues(@Nullable ComponentContext context, Serializable fieldBean, Collection<String> fieldNames) {
if (context != null)
ComponentContext.push(context);
try {
Map<String, Object> fieldValues = new HashMap<>();
BeanDescriptor beanDescriptor = new BeanDescriptor(fieldBean.getClass());
@ -117,7 +121,8 @@ public class FieldUtils {
return fieldValues;
} finally {
ComponentContext.pop();
if (context != null)
ComponentContext.pop();
}
}
@ -172,6 +177,30 @@ public class FieldUtils {
validateFieldMap(fieldSpecs, fieldMap);
}
@SuppressWarnings("unchecked")
public static Map<String, Object> getFieldValues(Project project, Map<String, Serializable> fieldEdits) {
var settingManager = OneDev.getInstance(SettingManager.class);
var issueSetting = settingManager.getIssueSetting();
Map<String, Object> fieldValues = new HashMap<>();
for (Map.Entry<String, Serializable> entry : fieldEdits.entrySet()) {
var fieldName = entry.getKey();
var fieldSpec = issueSetting.getFieldSpec(fieldName);
if (fieldSpec == null)
throw new BadRequestException("Undefined field: " + fieldName);
if (!SecurityUtils.canEditIssueField(project, fieldName))
throw new UnauthorizedException("No permission to edit field: " + fieldName);
List<String> values = new ArrayList<>();
if (entry.getValue() instanceof String) {
values.add((String) entry.getValue());
} else if (entry.getValue() instanceof Collection) {
values.addAll((Collection<String>) entry.getValue());
}
fieldValues.put(entry.getKey(), fieldSpec.convertToObject(values));
}
return fieldValues;
}
public static boolean isFieldVisible(BeanDescriptor beanDescriptor, Serializable fieldBean, String fieldName) {
String propertyName = getPropertyName(beanDescriptor, fieldName);
PropertyDescriptor propertyDescriptor = new PropertyDescriptor(fieldBean.getClass(), propertyName);

View File

@ -1,32 +1,40 @@
package io.onedev.server.model.support.issue.field.spec;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import org.apache.wicket.MarkupContainer;
import io.onedev.commons.codeassist.InputSuggestion;
import io.onedev.commons.utils.match.PathMatcher;
import io.onedev.server.OneDev;
import io.onedev.server.annotation.Editable;
import io.onedev.server.annotation.FieldName;
import io.onedev.server.annotation.Multiline;
import io.onedev.server.entitymanager.SettingManager;
import io.onedev.server.model.Project;
import io.onedev.server.model.support.administration.GlobalIssueSetting;
import io.onedev.server.annotation.Patterns;
import io.onedev.server.buildspecmodel.inputspec.InputContext;
import io.onedev.server.buildspecmodel.inputspec.InputSpec;
import io.onedev.server.buildspecmodel.inputspec.choiceinput.choiceprovider.SpecifiedChoices;
import io.onedev.server.buildspecmodel.inputspec.showcondition.ShowCondition;
import io.onedev.server.buildspecmodel.inputspec.showcondition.ValueIsNotAnyOf;
import io.onedev.server.buildspecmodel.inputspec.showcondition.ValueIsOneOf;
import io.onedev.server.entitymanager.SettingManager;
import io.onedev.server.model.Project;
import io.onedev.server.model.support.administration.GlobalIssueSetting;
import io.onedev.server.util.ComponentContext;
import io.onedev.server.util.EditContext;
import io.onedev.server.util.patternset.PatternSet;
import io.onedev.server.util.usage.Usage;
import io.onedev.server.annotation.FieldName;
import io.onedev.server.web.component.issue.workflowreconcile.UndefinedFieldResolution;
import io.onedev.server.web.component.issue.workflowreconcile.UndefinedFieldValue;
import io.onedev.server.web.component.issue.workflowreconcile.UndefinedFieldValuesResolution;
import io.onedev.server.annotation.Editable;
import io.onedev.server.annotation.Patterns;
import io.onedev.server.web.util.SuggestionUtils;
import org.apache.wicket.MarkupContainer;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import java.util.*;
@Editable
public abstract class FieldSpec extends InputSpec {
@ -362,6 +370,11 @@ public abstract class FieldSpec extends InputSpec {
return usage.prefix("custom field '" + getName() + "'");
}
public boolean isApplicable(Project project) {
var matcher = new PathMatcher();
return getApplicableProjects() == null || PatternSet.parse(getApplicableProjects()).matches(matcher, project.getPath());
}
protected void onDeleteProject(Usage usage, String projectPath) {
}

View File

@ -1,17 +0,0 @@
package io.onedev.server.rest;
import javax.ws.rs.BadRequestException;
public class InvalidParamException extends BadRequestException {
private static final long serialVersionUID = 1L;
public InvalidParamException(String message) {
super(message);
}
public InvalidParamException(String message, Exception cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,17 @@
package io.onedev.server.rest;
import javax.ws.rs.BadRequestException;
public class InvalidParamsException extends BadRequestException {
private static final long serialVersionUID = 1L;
public InvalidParamsException(String message) {
super(message);
}
public InvalidParamsException(String message, Exception cause) {
super(message, cause);
}
}

View File

@ -45,12 +45,12 @@ public class ParamCheckFilter implements ContainerRequestFilter {
Set<String> suppliedQueryParams = new HashSet<>(uriInfo.getQueryParameters().keySet());
suppliedQueryParams.removeAll(definedQueryParams);
if (!suppliedQueryParams.isEmpty())
throw new InvalidParamException("Unexpected query params: " + suppliedQueryParams);
throw new InvalidParamsException("Unexpected query params: " + suppliedQueryParams);
}
requiredQueryParams.removeAll(uriInfo.getQueryParameters().keySet());
if (!requiredQueryParams.isEmpty())
throw new InvalidParamException("Missing query params: " + requiredQueryParams);
throw new InvalidParamsException("Missing query params: " + requiredQueryParams);
}
public static boolean isRequired(Parameter param) {

View File

@ -23,7 +23,7 @@ import io.onedev.server.entitymanager.AgentAttributeManager;
import io.onedev.server.entitymanager.AgentManager;
import io.onedev.server.entitymanager.AuditManager;
import io.onedev.server.model.Agent;
import io.onedev.server.rest.InvalidParamException;
import io.onedev.server.rest.InvalidParamsException;
import io.onedev.server.rest.annotation.Api;
import io.onedev.server.search.entity.agent.AgentQuery;
import io.onedev.server.security.SecurityUtils;
@ -78,7 +78,7 @@ public class AgentResource {
try {
parsedQuery = AgentQuery.parse(query, false);
} catch (Exception e) {
throw new InvalidParamException("Error parsing query", e);
throw new InvalidParamsException("Error parsing query", e);
}
return agentManager.query(parsedQuery, offset, count);

View File

@ -24,7 +24,7 @@ import io.onedev.server.entitymanager.AuditManager;
import io.onedev.server.model.Agent;
import io.onedev.server.model.AgentToken;
import io.onedev.server.persistence.dao.EntityCriteria;
import io.onedev.server.rest.InvalidParamException;
import io.onedev.server.rest.InvalidParamsException;
import io.onedev.server.rest.annotation.Api;
import io.onedev.server.rest.resource.support.RestConstants;
import io.onedev.server.security.SecurityUtils;
@ -76,7 +76,7 @@ public class AgentTokenResource {
throw new UnauthorizedException();
if (count > RestConstants.MAX_PAGE_SIZE)
throw new InvalidParamException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
throw new InvalidParamsException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
EntityCriteria<AgentToken> criteria = EntityCriteria.of(AgentToken.class);

View File

@ -31,7 +31,7 @@ import io.onedev.server.model.Build;
import io.onedev.server.model.BuildDependence;
import io.onedev.server.model.BuildLabel;
import io.onedev.server.model.BuildParam;
import io.onedev.server.rest.InvalidParamException;
import io.onedev.server.rest.InvalidParamsException;
import io.onedev.server.rest.annotation.Api;
import io.onedev.server.rest.resource.support.RestConstants;
import io.onedev.server.search.entity.build.BuildQuery;
@ -131,13 +131,13 @@ public class BuildResource {
@QueryParam("count") @Api(example="100") int count) {
if (!SecurityUtils.isAdministrator() && count > RestConstants.MAX_PAGE_SIZE)
throw new InvalidParamException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
throw new InvalidParamsException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
BuildQuery parsedQuery;
try {
parsedQuery = BuildQuery.parse(null, query, true, true);
} catch (Exception e) {
throw new InvalidParamException("Error parsing query", e);
throw new InvalidParamsException("Error parsing query", e);
}
return buildManager.query(null, parsedQuery, false, offset, count);

View File

@ -1,19 +1,28 @@
package io.onedev.server.rest.resource;
import io.onedev.server.entitymanager.IssueLinkManager;
import io.onedev.server.model.IssueLink;
import io.onedev.server.rest.annotation.Api;
import org.apache.shiro.authz.UnauthorizedException;
import static io.onedev.server.security.SecurityUtils.canAccessIssue;
import static io.onedev.server.security.SecurityUtils.canEditIssueLink;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.validation.constraints.NotNull;
import javax.ws.rs.*;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.NotAcceptableException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import static io.onedev.server.security.SecurityUtils.canAccessIssue;
import static io.onedev.server.security.SecurityUtils.canEditIssueLink;
import org.apache.shiro.authz.UnauthorizedException;
import io.onedev.server.entitymanager.IssueLinkManager;
import io.onedev.server.exception.LinkValidationException;
import io.onedev.server.model.IssueLink;
import io.onedev.server.rest.annotation.Api;
@Path("/issue-links")
@Consumes(MediaType.APPLICATION_JSON)
@ -41,31 +50,17 @@ public class IssueLinkResource {
@Api(order=200, description="Create new issue link")
@POST
public Long createLink(@NotNull IssueLink link) {
if (!canAccessIssue(link.getSource()) || !canAccessIssue(link.getTarget()))
throw new UnauthorizedException("No permission to access specified issues");
if (!canEditIssueLink(link.getSource().getProject(), link.getSpec())
&& !canEditIssueLink(link.getTarget().getProject(), link.getSpec())) {
throw new UnauthorizedException();
|| !canEditIssueLink(link.getTarget().getProject(), link.getSpec())) {
throw new UnauthorizedException("No permission to add specified link for specified issues");
}
if (link.getSource().equals(link.getTarget()))
throw new BadRequestException("Can not link to self");
if (link.getSpec().getOpposite() != null) {
if (link.getSource().getTargetLinks().stream().anyMatch(it -> it.getSpec().equals(link.getSpec()) && it.getTarget().equals(link.getTarget())))
throw new BadRequestException("Source issue already linked to target issue via specified link spec");
if (!link.getSpec().isMultiple() && link.getSource().getTargetLinks().stream().anyMatch(it -> it.getSpec().equals(link.getSpec())))
throw new BadRequestException("Link spec is not multiple and the source issue is already linked to another issue via this link spec");
if (!link.getSpec().getParsedIssueQuery(link.getSource().getProject()).matches(link.getTarget()))
throw new BadRequestException("Link spec not allowed to link to the target issue");
if (!link.getSpec().getOpposite().isMultiple() && link.getTarget().getSourceLinks().stream().anyMatch(it -> it.getSpec().equals(link.getSpec())))
throw new BadRequestException("Opposite side of link spec is not multiple and the target issue is already linked to another issue via this link spec");
if (!link.getSpec().getOpposite().getParsedIssueQuery(link.getSource().getProject()).matches(link.getSource()))
throw new BadRequestException("Opposite side of link spec not allowed to link to the source issue");
} else {
if (link.getSource().getLinks().stream().anyMatch(it -> it.getSpec().equals(link.getSpec()) && it.getLinked(link.getSource()).equals(link.getTarget())))
throw new BadRequestException("Specified issues already linked via specified link spec");
if (!link.getSpec().isMultiple() && link.getSource().getLinks().stream().anyMatch(it -> it.getSpec().equals(link.getSpec())))
throw new BadRequestException("Link spec is not multiple and source issue is already linked to another issue via this link spec");
var parsedIssueQuery = link.getSpec().getParsedIssueQuery(link.getSource().getProject());
if (!parsedIssueQuery.matches(link.getSource()) || !parsedIssueQuery.matches(link.getTarget()))
throw new BadRequestException("Link spec not allowed to link specified issues");
try {
link.validate();
} catch (LinkValidationException e) {
throw new NotAcceptableException(e.getMessage());
}
linkManager.create(link);

View File

@ -20,6 +20,7 @@ import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.NotAcceptableException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@ -34,6 +35,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.onedev.server.OneDev;
import io.onedev.server.SubscriptionManager;
import io.onedev.server.attachment.AttachmentManager;
import io.onedev.server.data.migration.VersionedXmlDoc;
import io.onedev.server.entitymanager.AuditManager;
@ -54,8 +56,10 @@ import io.onedev.server.model.Iteration;
import io.onedev.server.model.Project;
import io.onedev.server.model.PullRequest;
import io.onedev.server.model.User;
import io.onedev.server.model.support.issue.field.EmptyFieldsException;
import io.onedev.server.model.support.issue.field.FieldUtils;
import io.onedev.server.model.support.issue.transitionspec.ManualSpec;
import io.onedev.server.rest.InvalidParamException;
import io.onedev.server.rest.InvalidParamsException;
import io.onedev.server.rest.annotation.Api;
import io.onedev.server.rest.annotation.EntityCreate;
import io.onedev.server.rest.resource.support.RestConstants;
@ -94,12 +98,14 @@ public class IssueResource {
private final UrlManager urlManager;
private final SubscriptionManager subscriptionManager;
@Inject
public IssueResource(SettingManager settingManager, IssueManager issueManager,
IssueChangeManager issueChangeManager, IterationManager iterationManager,
ProjectManager projectManager, ObjectMapper objectMapper,
AuditManager auditManager, AttachmentManager attachmentManager,
UrlManager urlManager) {
UrlManager urlManager, SubscriptionManager subscriptionManager) {
this.settingManager = settingManager;
this.issueManager = issueManager;
this.issueChangeManager = issueChangeManager;
@ -109,6 +115,7 @@ public class IssueResource {
this.auditManager = auditManager;
this.attachmentManager = attachmentManager;
this.urlManager = urlManager;
this.subscriptionManager = subscriptionManager;
}
@Api(order=100)
@ -252,14 +259,14 @@ public class IssueResource {
@QueryParam("count") @Api(example="100") int count) {
if (!SecurityUtils.isAdministrator() && count > RestConstants.MAX_PAGE_SIZE)
throw new InvalidParamException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
throw new InvalidParamsException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
IssueQuery parsedQuery;
try {
IssueQueryParseOption option = new IssueQueryParseOption().withCurrentUserCriteria(true);
parsedQuery = IssueQuery.parse(null, query, option, true);
} catch (Exception e) {
throw new InvalidParamException("Error parsing query", e);
throw new InvalidParamsException("Error parsing query", e);
}
var typeReference = new TypeReference<Map<String, Object>>() {};
@ -291,8 +298,17 @@ public class IssueResource {
if (!SecurityUtils.canAccessProject(project))
throw new UnauthorizedException();
if (!data.getIterationIds().isEmpty() && !SecurityUtils.canScheduleIssues(project))
throw new UnauthorizedException("No permission to schedule issue");
if (data.getIterationIds() != null && !data.getIterationIds().isEmpty() && !SecurityUtils.canScheduleIssues(project))
throw new UnauthorizedException("No permission to schedule issue. Remove iterationIds if you want to create issue without scheduling it.");
if (data.getOwnEstimatedTime() != null) {
if (!subscriptionManager.isSubscriptionActive())
throw new NotAcceptableException("An active subscription is required for this feature");
if (!project.isTimeTracking())
throw new NotAcceptableException("Time tracking needs to be enabled for the project");
if (!SecurityUtils.canScheduleIssues(project))
throw new UnauthorizedException("Issue schedule permission required to set own estimated time. Remove ownEstimatedTime if you want to create issue without setting own estimated time.");
}
var issueSetting = settingManager.getIssueSetting();
@ -304,20 +320,29 @@ public class IssueResource {
issue.setSubmitDate(new Date());
issue.setSubmitter(user);
issue.setState(issueSetting.getInitialStateSpec().getName());
issue.setOwnEstimatedTime(data.getOwnEstimatedTime());
if (data.getOwnEstimatedTime() != null)
issue.setOwnEstimatedTime(data.getOwnEstimatedTime());
for (Long iterationId : data.getIterationIds()) {
Iteration iteration = iterationManager.load(iterationId);
if (!iteration.getProject().isSelfOrAncestorOf(project))
throw new BadRequestException("Iteration is not defined in project hierarchy of the issue");
IssueSchedule schedule = new IssueSchedule();
schedule.setIssue(issue);
schedule.setIteration(iteration);
issue.getSchedules().add(schedule);
if (data.getIterationIds() != null) {
for (Long iterationId : data.getIterationIds()) {
Iteration iteration = iterationManager.load(iterationId);
if (!iteration.getProject().isSelfOrAncestorOf(project))
throw new BadRequestException("Iteration is not defined in project hierarchy of the issue");
IssueSchedule schedule = new IssueSchedule();
schedule.setIssue(issue);
schedule.setIteration(iteration);
issue.getSchedules().add(schedule);
}
}
issue.setFieldValues(FieldUtils.getFieldValues(project, data.fields));
try {
issueManager.open(issue);
} catch (EmptyFieldsException e) {
throw new InvalidParamsException(e.getMessage());
}
issue.setFieldValues(getFieldObjs(issue, data.fields));
issueManager.open(issue);
return issue.getId();
}
@ -357,11 +382,15 @@ public class IssueResource {
@Api(order=1275)
@Path("/{issueId}/own-estimated-time")
@POST
public Response setOwnEstimatedTime(@PathParam("issueId") Long issueId, int hours) {
public Response setOwnEstimatedTime(@PathParam("issueId") Long issueId, int minutes) {
Issue issue = issueManager.load(issueId);
if (!subscriptionManager.isSubscriptionActive())
throw new NotAcceptableException("An active subscription is required for this feature");
if (!issue.getProject().isTimeTracking())
throw new NotAcceptableException("Time tracking needs to be enabled for the project");
if (!SecurityUtils.canScheduleIssues(issue.getProject()))
throw new UnauthorizedException();
issueChangeManager.changeOwnEstimatedTime(issue, hours);
throw new UnauthorizedException("Issue schedule permission required to set own estimated time");
issueChangeManager.changeOwnEstimatedTime(issue, minutes);
return Response.ok().build();
}
@ -371,13 +400,13 @@ public class IssueResource {
public Response setIterations(@PathParam("issueId") Long issueId, List<Long> iterationIds) {
Issue issue = issueManager.load(issueId);
if (!SecurityUtils.canScheduleIssues(issue.getProject()))
throw new UnauthorizedException("No permission to schedule issue");
throw new UnauthorizedException("Issue schedule permission required to set iterations");
Collection<Iteration> iterations = new HashSet<>();
for (Long iterationId: iterationIds) {
Iteration iteration = iterationManager.load(iterationId);
if (!iteration.getProject().isSelfOrAncestorOf(issue.getProject()))
throw new InvalidParamException("Iteration is not defined in project hierarchy of the issue");
throw new InvalidParamsException("Iteration is not defined in project hierarchy of the issue");
iterations.add(iteration);
}
@ -399,7 +428,11 @@ public class IssueResource {
throw new UnauthorizedException();
}
issueChangeManager.changeFields(issue, getFieldObjs(issue, fields));
try {
issueChangeManager.changeFields(issue, FieldUtils.getFieldValues(issue.getProject(), fields));
} catch (EmptyFieldsException e) {
throw new InvalidParamsException(e.getMessage());
}
return Response.ok().build();
}
@ -409,24 +442,20 @@ public class IssueResource {
example.put("field2", new String[]{"value1", "value2"});
return example;
}
@Api(order=1500)
@Path("/{issueId}/state-transitions")
@POST
public Response transitState(@PathParam("issueId") Long issueId, @NotNull @Valid StateTransitionData data) {
Issue issue = issueManager.load(issueId);
var applicableTransitions = new ArrayList<ManualSpec>();
for (var transition: settingManager.getIssueSetting().getTransitionSpecs()) {
if (transition instanceof ManualSpec && ((ManualSpec)transition).canTransit(issue, data.getState()))
applicableTransitions.add((ManualSpec) transition);
}
if (applicableTransitions.isEmpty())
throw new BadRequestException("No applicable transition spec for: " + issue.getState() + "->" + data.getState());
if (applicableTransitions.stream().noneMatch(it->it.isAuthorized(issue)))
throw new UnauthorizedException();
ManualSpec transition = settingManager.getIssueSetting().getManualSpec(issue, data.getState());
issueChangeManager.changeState(issue, data.getState(), getFieldObjs(issue, data.getFields()),
data.getRemoveFields(), data.getComment());
var fieldValues = FieldUtils.getFieldValues(issue.getProject(), data.getFields());
try {
issueChangeManager.changeState(issue, data.getState(), fieldValues, transition.getPromptFields(), transition.getRemoveFields(), data.getComment());
} catch (EmptyFieldsException e) {
throw new InvalidParamsException(e.getMessage());
}
return Response.ok().build();
}
@ -457,29 +486,6 @@ public class IssueResource {
auditManager.audit(issue.getProject(), "deleted issue \"" + issue.getReference().toString(issue.getProject()) + "\" via RESTful API", oldAuditContent, null);
return Response.ok().build();
}
@SuppressWarnings("unchecked")
private Map<String, Object> getFieldObjs(Issue issue, Map<String, Serializable> fields) {
var issueSetting = settingManager.getIssueSetting();
Map<String, Object> fieldObjs = new HashMap<>();
for (Map.Entry<String, Serializable> entry: fields.entrySet()) {
var fieldName = entry.getKey();
var fieldSpec = issueSetting.getFieldSpec(fieldName);
if (fieldSpec == null)
throw new BadRequestException("Undefined field: " + fieldName);
if (!SecurityUtils.canEditIssueField(issue.getProject(), fieldName))
throw new UnauthorizedException("No permission to edit field: " + fieldName);
List<String> values = new ArrayList<>();
if (entry.getValue() instanceof String) {
values.add((String) entry.getValue());
} else if (entry.getValue() instanceof Collection) {
values.addAll((Collection<String>) entry.getValue());
}
fieldObjs.put(entry.getKey(), fieldSpec.convertToObject(values));
}
return fieldObjs;
}
@EntityCreate(Issue.class)
public static class IssueOpenData implements Serializable {
@ -498,8 +504,8 @@ public class IssueResource {
@Api(order=400)
private boolean confidential;
@Api(order=450, description = "Own estimated time in hours")
private int ownEstimatedTime;
@Api(order=450, description = "Own estimated time in minutes. Only be used when subscription is active and time tracking is enabled")
private Integer ownEstimatedTime;
@Api(order=500)
private List<Long> iterationIds = new ArrayList<>();
@ -541,11 +547,11 @@ public class IssueResource {
this.confidential = confidential;
}
public int getOwnEstimatedTime() {
public Integer getOwnEstimatedTime() {
return ownEstimatedTime;
}
public void setOwnEstimatedTime(int ownEstimatedTime) {
public void setOwnEstimatedTime(Integer ownEstimatedTime) {
this.ownEstimatedTime = ownEstimatedTime;
}
@ -581,10 +587,7 @@ public class IssueResource {
@Api(order=200, exampleProvider = "getFieldsExample")
private Map<String, Serializable> fields = new HashMap<>();
@Api(order=300)
private Collection<String> removeFields = new HashSet<>();
@Api(order=400)
private String comment;
@ -606,15 +609,6 @@ public class IssueResource {
this.fields = fields;
}
@NotNull
public Collection<String> getRemoveFields() {
return removeFields;
}
public void setRemoveFields(Collection<String> removeFields) {
this.removeFields = removeFields;
}
public String getComment() {
return comment;
}

View File

@ -8,6 +8,7 @@ import javax.validation.constraints.NotNull;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.NotAcceptableException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@ -55,9 +56,9 @@ public class IssueWorkResource {
@POST
public Long createWork(@NotNull IssueWork work) {
if (!subscriptionManager.isSubscriptionActive())
throw new UnsupportedOperationException("This feature requires an active subscription");
throw new NotAcceptableException("This feature requires an active subscription");
if (!work.getIssue().getProject().isTimeTracking())
throw new UnsupportedOperationException("Time tracking not enabled for project");
throw new NotAcceptableException("Time tracking not enabled for project");
if (!SecurityUtils.canAccessIssue(work.getIssue())
|| !SecurityUtils.isAdministrator() && !work.getUser().equals(SecurityUtils.getAuthUser())) {

View File

@ -26,7 +26,7 @@ import io.onedev.server.model.Pack;
import io.onedev.server.model.PackBlob;
import io.onedev.server.model.PackBlobReference;
import io.onedev.server.model.PackLabel;
import io.onedev.server.rest.InvalidParamException;
import io.onedev.server.rest.InvalidParamsException;
import io.onedev.server.rest.annotation.Api;
import io.onedev.server.rest.resource.support.RestConstants;
import io.onedev.server.search.entity.pack.PackQuery;
@ -87,13 +87,13 @@ public class PackResource {
@QueryParam("count") @Api(example="100") int count) {
if (!SecurityUtils.isAdministrator() && count > RestConstants.MAX_PAGE_SIZE)
throw new InvalidParamException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
throw new InvalidParamsException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
PackQuery parsedQuery;
try {
parsedQuery = PackQuery.parse(null, query, true);
} catch (Exception e) {
throw new InvalidParamException("Error parsing query", e);
throw new InvalidParamsException("Error parsing query", e);
}
return packManager.query(null, parsedQuery, false, offset, count);

View File

@ -62,7 +62,7 @@ import io.onedev.server.model.support.issue.ProjectIssueSetting;
import io.onedev.server.model.support.pack.ProjectPackSetting;
import io.onedev.server.model.support.pullrequest.ProjectPullRequestSetting;
import io.onedev.server.persistence.dao.EntityCriteria;
import io.onedev.server.rest.InvalidParamException;
import io.onedev.server.rest.InvalidParamsException;
import io.onedev.server.rest.annotation.Api;
import io.onedev.server.rest.annotation.EntityCreate;
import io.onedev.server.rest.resource.support.RestConstants;
@ -192,13 +192,13 @@ public class ProjectResource {
@QueryParam("count") @Api(example="100") int count) {
if (!SecurityUtils.isAdministrator() && count > RestConstants.MAX_PAGE_SIZE)
throw new InvalidParamException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
throw new InvalidParamsException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
ProjectQuery parsedQuery;
try {
parsedQuery = ProjectQuery.parse(query);
} catch (Exception e) {
throw new InvalidParamException("Error parsing query", e);
throw new InvalidParamsException("Error parsing query", e);
}
return projectManager.query(parsedQuery, false, offset, count).stream()
@ -221,7 +221,7 @@ public class ProjectResource {
throw new UnauthorizedException();
if (count > RestConstants.MAX_PAGE_SIZE)
throw new InvalidParamException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
throw new InvalidParamsException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
EntityCriteria<Iteration> criteria = EntityCriteria.of(Iteration.class);
criteria.add(Restrictions.in(Iteration.PROP_PROJECT, project.getSelfAndAncestors()));
@ -254,7 +254,7 @@ public class ProjectResource {
throw new UnauthorizedException();
if (count > RestConstants.MAX_PAGE_SIZE)
throw new InvalidParamException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
throw new InvalidParamsException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
int sinceDay = (int) LocalDate.parse(since).toEpochDay();
int untilDay = (int) LocalDate.parse(until).toEpochDay();

View File

@ -51,7 +51,7 @@ import io.onedev.server.model.User;
import io.onedev.server.model.support.pullrequest.AutoMerge;
import io.onedev.server.model.support.pullrequest.MergePreview;
import io.onedev.server.model.support.pullrequest.MergeStrategy;
import io.onedev.server.rest.InvalidParamException;
import io.onedev.server.rest.InvalidParamsException;
import io.onedev.server.rest.annotation.Api;
import io.onedev.server.rest.annotation.EntityCreate;
import io.onedev.server.rest.resource.support.RestConstants;
@ -217,13 +217,13 @@ public class PullRequestResource {
@QueryParam("count") @Api(example="100") int count) {
if (!SecurityUtils.isAdministrator() && count > RestConstants.MAX_PAGE_SIZE)
throw new InvalidParamException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
throw new InvalidParamsException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
PullRequestQuery parsedQuery;
try {
parsedQuery = PullRequestQuery.parse(null, query, true);
} catch (Exception e) {
throw new InvalidParamException("Error parsing query", e);
throw new InvalidParamsException("Error parsing query", e);
}
return pullRequestManager.query(null, parsedQuery, false, offset, count);
@ -241,18 +241,18 @@ public class PullRequestResource {
throw new UnauthorizedException();
if (target.equals(source))
throw new InvalidParamException("Source and target are the same");
throw new InvalidParamsException("Source and target are the same");
PullRequest request = pullRequestManager.findOpen(target, source);
if (request != null)
throw new InvalidParamException("Another pull request already opened for this change");
throw new InvalidParamsException("Another pull request already opened for this change");
request = pullRequestManager.findEffective(target, source);
if (request != null) {
if (request.isOpen())
throw new InvalidParamException("Another pull request already opened for this change");
throw new InvalidParamsException("Another pull request already opened for this change");
else
throw new InvalidParamException("Change already merged");
throw new InvalidParamsException("Change already merged");
}
request = new PullRequest();
@ -261,7 +261,7 @@ public class PullRequestResource {
source.getProject(), source.getObjectId());
if (baseCommitId == null)
throw new InvalidParamException("No common base for target and source");
throw new InvalidParamsException("No common base for target and source");
request.setTitle(data.getTitle());
request.setTarget(target);
@ -276,7 +276,7 @@ public class PullRequestResource {
request.setMergeStrategy(request.getProject().findDefaultPullRequestMergeStrategy());
if (request.getBaseCommitHash().equals(source.getObjectName()))
throw new InvalidParamException("Change already merged");
throw new InvalidParamsException("Change already merged");
PullRequestUpdate update = new PullRequestUpdate();
update.setDate(new DateTime(request.getSubmitDate()).plusSeconds(1).toDate());

View File

@ -15,7 +15,7 @@ import io.onedev.server.git.service.GitService;
import io.onedev.server.git.service.RefFacade;
import io.onedev.server.model.Project;
import io.onedev.server.model.User;
import io.onedev.server.rest.InvalidParamException;
import io.onedev.server.rest.InvalidParamsException;
import io.onedev.server.rest.annotation.Api;
import io.onedev.server.rest.resource.support.FileCreateOrUpdateRequest;
import io.onedev.server.rest.resource.support.FileEditRequest;
@ -138,7 +138,7 @@ public class RepositoryResource {
if (!SecurityUtils.canWriteCode(project))
throw new UnauthorizedException();
else if (project.getBranchRef(request.getBranchName()) != null)
throw new InvalidParamException("Branch '" + request.getBranchName() + "' already exists");
throw new InvalidParamsException("Branch '" + request.getBranchName() + "' already exists");
else if (project.getBranchProtection(request.getBranchName(), user).isPreventCreation())
throw new ExplicitException("Branch creation prohibited by branch protection rule");
@ -217,7 +217,7 @@ public class RepositoryResource {
}
if (project.getTagRef(request.getTagName()) != null) {
throw new InvalidParamException("Tag '" + request.getTagName() + "' already exists");
throw new InvalidParamsException("Tag '" + request.getTagName() + "' already exists");
} else {
User user = SecurityUtils.getUser();
gitService.createTag(project, request.getTagName(), request.getRevision(), user.asPerson(),
@ -257,13 +257,13 @@ public class RepositoryResource {
}
if (count > MAX_COMMITS)
throw new InvalidParamException("Count should not be greater than " + MAX_COMMITS);
throw new InvalidParamsException("Count should not be greater than " + MAX_COMMITS);
CommitQuery parsedQuery;
try {
parsedQuery = CommitQuery.parse(project, query, true);
} catch (Exception e) {
throw new InvalidParamException("Error parsing query", e);
throw new InvalidParamsException("Error parsing query", e);
}
RevListOptions options = new RevListOptions();
@ -311,7 +311,7 @@ public class RepositoryResource {
BlobIdent blobIdent = new BlobIdent(project, revisionAndPathSegments);
if (!blobIdent.isTree()) {
throw new InvalidParamException("Specified path is not a directory: " + blobIdent.path);
throw new InvalidParamsException("Specified path is not a directory: " + blobIdent.path);
}
ObjectId revId = project.getObjectId(blobIdent.revision, true);
@ -343,7 +343,7 @@ public class RepositoryResource {
BlobIdent blobIdent = new BlobIdent(project, revisionAndPathSegments);
if (!blobIdent.isFile()) {
throw new InvalidParamException("Specified path is not a file: " + blobIdent.path);
throw new InvalidParamsException("Specified path is not a file: " + blobIdent.path);
}
Blob blob = project.getBlob(blobIdent, true);
@ -375,14 +375,14 @@ public class RepositoryResource {
revisionAndPath = RevisionAndPath.parse(project, revisionAndPathSegments);
RefFacade ref = project.getBranchRef(revisionAndPath.getRevision());
if (ref == null)
throw new InvalidParamException("Not a branch: " + revisionAndPath.getRevision());
throw new InvalidParamsException("Not a branch: " + revisionAndPath.getRevision());
refName = ref.getName();
oldCommitId = ref.getObjectId();
if (revisionAndPath.getPath() == null)
throw new InvalidParamException("Branch and file should be specified");
throw new InvalidParamsException("Branch and file should be specified");
} else {
if (revisionAndPathSegments.size() < 2)
throw new InvalidParamException("Branch and file should be specified");
throw new InvalidParamsException("Branch and file should be specified");
revisionAndPath = new RevisionAndPath(
revisionAndPathSegments.get(0),
StringUtils.join(revisionAndPathSegments.subList(1, revisionAndPathSegments.size())));

View File

@ -27,7 +27,7 @@ import io.onedev.server.entitymanager.AuditManager;
import io.onedev.server.entitymanager.RoleManager;
import io.onedev.server.model.Role;
import io.onedev.server.persistence.dao.EntityCriteria;
import io.onedev.server.rest.InvalidParamException;
import io.onedev.server.rest.InvalidParamsException;
import io.onedev.server.rest.annotation.Api;
import io.onedev.server.rest.resource.support.RestConstants;
import io.onedev.server.security.SecurityUtils;
@ -66,7 +66,7 @@ public class RoleResource {
throw new UnauthorizedException();
if (count > RestConstants.MAX_PAGE_SIZE)
throw new InvalidParamException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
throw new InvalidParamsException("Count should not be greater than " + RestConstants.MAX_PAGE_SIZE);
EntityCriteria<Role> criteria = EntityCriteria.of(Role.class);
if (name != null)

View File

@ -38,7 +38,7 @@ import io.onedev.server.model.support.administration.emailtemplates.EmailTemplat
import io.onedev.server.model.support.administration.jobexecutor.JobExecutor;
import io.onedev.server.model.support.administration.mailservice.MailService;
import io.onedev.server.model.support.administration.sso.SsoConnector;
import io.onedev.server.rest.InvalidParamException;
import io.onedev.server.rest.InvalidParamsException;
import io.onedev.server.rest.annotation.Api;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.web.page.layout.ContributedAdministrationSetting;
@ -211,7 +211,7 @@ public class SettingResource {
throw new UnauthorizedException();
String ingressUrl = OneDev.getInstance().getIngressUrl();
if (ingressUrl != null && !ingressUrl.equals(systemSetting.getServerUrl()))
throw new InvalidParamException("Server URL can only be \"" + ingressUrl + "\"");
throw new InvalidParamsException("Server URL can only be \"" + ingressUrl + "\"");
var oldAuditContent = VersionedXmlDoc.fromBean(settingManager.getSystemSetting()).toXML();
settingManager.saveSystemSetting(systemSetting);
auditManager.audit(null, "changed system setting via RESTful API",

View File

@ -6,7 +6,7 @@ import io.onedev.server.entitymanager.ProjectManager;
import io.onedev.server.git.GitUtils;
import io.onedev.server.job.JobManager;
import io.onedev.server.model.Project;
import io.onedev.server.rest.InvalidParamException;
import io.onedev.server.rest.InvalidParamsException;
import io.onedev.server.rest.annotation.Api;
import io.onedev.server.security.SecurityUtils;
import org.apache.shiro.authz.UnauthorizedException;
@ -92,11 +92,11 @@ public class TriggerJobResource {
String accessTokenValue, UriInfo uriInfo) {
Project project = projectManager.findByPath(projectPath);
if (project == null)
throw new InvalidParamException("Project not found: " + projectPath);
throw new InvalidParamsException("Project not found: " + projectPath);
var accessToken = accessTokenManager.findByValue(accessTokenValue);
if (accessToken == null)
throw new InvalidParamException("Invalid access token");
throw new InvalidParamsException("Invalid access token");
ThreadContext.bind(accessToken.asSubject());
try {
@ -104,7 +104,7 @@ public class TriggerJobResource {
throw new UnauthorizedException();
if (StringUtils.isNotBlank(branch) && StringUtils.isNotBlank(tag))
throw new InvalidParamException("Either branch or tag should be specified, but not both");
throw new InvalidParamsException("Either branch or tag should be specified, but not both");
String refName;
if (branch != null)
@ -116,7 +116,7 @@ public class TriggerJobResource {
RevCommit commit = project.getRevCommit(refName, false);
if (commit == null)
throw new InvalidParamException("Ref not found: " + refName);
throw new InvalidParamsException("Ref not found: " + refName);
Map<String, List<String>> jobParams = new HashMap<>();
for (Map.Entry<String, List<String>> entry: uriInfo.getQueryParameters().entrySet()) {

View File

@ -39,7 +39,7 @@ public class ComponentContext implements Serializable {
@Nullable
public static ComponentContext get() {
if (!stack.get().isEmpty()) {
if (!stack.get().isEmpty()) {
return stack.get().peek();
} else {
Page page = WicketUtils.getPage();

View File

@ -36,7 +36,7 @@ public class UserCache extends MapProxy<Long, UserFacade> {
@Nullable
public UserFacade findByFullName(String fullName) {
for (UserFacade facade: values()) {
if (fullName.equals(facade.getFullName()))
if (fullName.equalsIgnoreCase(facade.getFullName()))
return facade;
}
return null;

View File

@ -5,10 +5,10 @@ import io.onedev.commons.utils.StringUtils;
import io.onedev.server.buildspec.job.JobVariable;
import io.onedev.server.buildspec.param.ParamCombination;
import io.onedev.server.buildspec.param.spec.ParamSpec;
import io.onedev.server.buildspecmodel.inputspec.Input;
import io.onedev.server.model.Build;
import io.onedev.server.model.support.build.JobProperty;
import io.onedev.server.util.GroovyUtils;
import io.onedev.server.util.Input;
import io.onedev.server.util.interpolative.Interpolative.Segment;
import io.onedev.server.util.interpolative.Interpolative.Segment.Type;
import io.onedev.server.web.editable.EditableStringTransformer;

View File

@ -7,9 +7,9 @@ import org.apache.wicket.model.Model;
import io.onedev.commons.utils.StringUtils;
import io.onedev.server.buildspec.param.spec.ParamSpec;
import io.onedev.server.buildspecmodel.inputspec.Input;
import io.onedev.server.buildspecmodel.inputspec.SecretInput;
import io.onedev.server.util.DateUtils;
import io.onedev.server.util.Input;
public class ParamValuesLabel extends Label {

View File

@ -54,6 +54,7 @@ import com.google.common.collect.Sets;
import io.onedev.commons.utils.ExplicitException;
import io.onedev.server.OneDev;
import io.onedev.server.buildspecmodel.inputspec.Input;
import io.onedev.server.data.migration.VersionedXmlDoc;
import io.onedev.server.entitymanager.AuditManager;
import io.onedev.server.entitymanager.BuildManager;
@ -77,7 +78,6 @@ import io.onedev.server.security.SecurityUtils;
import io.onedev.server.security.permission.JobPermission;
import io.onedev.server.security.permission.RunJob;
import io.onedev.server.util.DateUtils;
import io.onedev.server.util.Input;
import io.onedev.server.web.WebConstants;
import io.onedev.server.web.WebSession;
import io.onedev.server.web.behavior.BuildQueryBehavior;

View File

@ -33,6 +33,7 @@ import com.google.common.collect.Sets;
import io.onedev.server.OneDev;
import io.onedev.server.buildspec.BuildSpec;
import io.onedev.server.buildspec.job.Job;
import io.onedev.server.buildspecmodel.inputspec.Input;
import io.onedev.server.entitymanager.BuildLabelManager;
import io.onedev.server.entityreference.EntityReference;
import io.onedev.server.git.BlobIdent;
@ -43,7 +44,6 @@ import io.onedev.server.model.Project;
import io.onedev.server.model.PullRequest;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.util.DateUtils;
import io.onedev.server.util.Input;
import io.onedev.server.util.criteria.Criteria;
import io.onedev.server.web.asset.emoji.Emojis;
import io.onedev.server.web.behavior.ChangeObserver;

View File

@ -108,7 +108,7 @@ public abstract class NewIssueEditor extends FormComponentPanel<Issue> implement
Class<?> fieldBeanClass = FieldUtils.getFieldBeanClass();
Serializable fieldBean = issue.getFieldBean(fieldBeanClass, true);
Collection<String> fieldNames = getIssueSetting().getPromptFieldsUponIssueOpen(getProject());
var fieldNames = getIssueSetting().getPromptFieldsUponIssueOpen(getProject());
issue.setFieldValues(FieldUtils.getFieldValues(new ComponentContext(this), fieldBean,
FieldUtils.getEditableFields(getProject(), fieldNames)));
@ -409,7 +409,8 @@ public abstract class NewIssueEditor extends FormComponentPanel<Issue> implement
issue.setConfidential(confidentialInput.getConvertedInput());
fieldEditor.convertInput();
Collection<String> fieldNames = getIssueSetting().getPromptFieldsUponIssueOpen(getProject());
var fieldNames = getIssueSetting().getPromptFieldsUponIssueOpen(getProject());
issue.setFieldValues(FieldUtils.getFieldValues(fieldEditor.newComponentContext(),
fieldEditor.getConvertedInput(), fieldNames));

View File

@ -29,6 +29,7 @@ import org.eclipse.jgit.lib.ObjectId;
import org.unbescape.html.HtmlEscape;
import io.onedev.server.OneDev;
import io.onedev.server.buildspecmodel.inputspec.Input;
import io.onedev.server.buildspecmodel.inputspec.InputContext;
import io.onedev.server.buildspecmodel.inputspec.InputSpec;
import io.onedev.server.buildspecmodel.inputspec.SecretInput;
@ -57,7 +58,6 @@ import io.onedev.server.util.ColorUtils;
import io.onedev.server.util.ComponentContext;
import io.onedev.server.util.DateUtils;
import io.onedev.server.util.EditContext;
import io.onedev.server.util.Input;
import io.onedev.server.web.ajaxlistener.AttachAjaxIndicatorListener;
import io.onedev.server.web.ajaxlistener.DisableGlobalAjaxIndicatorListener;
import io.onedev.server.web.component.MultilineLabel;

View File

@ -50,6 +50,7 @@ import io.onedev.server.model.Issue;
import io.onedev.server.model.Iteration;
import io.onedev.server.model.Project;
import io.onedev.server.model.support.administration.GlobalIssueSetting;
import io.onedev.server.model.support.issue.field.EmptyFieldsException;
import io.onedev.server.model.support.issue.field.FieldUtils;
import io.onedev.server.model.support.issue.field.spec.FieldSpec;
import io.onedev.server.search.entity.issue.IssueQuery;
@ -311,8 +312,14 @@ abstract class BatchEditPanel extends Panel implements InputContext {
Map<String, Object> fieldValues = FieldUtils.getFieldValues(customFieldsEditor.newComponentContext(),
customFieldsBean, selectedFields);
OneDev.getInstance(IssueChangeManager.class).batchUpdate(
getIssueIterator(), state, confidential, iterations, fieldValues, comment, sendNotifications);
try {
OneDev.getInstance(IssueChangeManager.class).batchUpdate(
getIssueIterator(), state, confidential, iterations, fieldValues, comment, sendNotifications);
} catch (EmptyFieldsException e) {
form.error(e.getMessage());
target.add(form);
return;
}
onUpdated(target);
}

View File

@ -76,6 +76,7 @@ import com.google.common.collect.Sets;
import edu.emory.mathcs.backport.java.util.Collections;
import io.onedev.commons.utils.ExplicitException;
import io.onedev.server.OneDev;
import io.onedev.server.buildspecmodel.inputspec.Input;
import io.onedev.server.data.migration.VersionedXmlDoc;
import io.onedev.server.entitymanager.AuditManager;
import io.onedev.server.entitymanager.IssueLinkManager;
@ -106,7 +107,6 @@ import io.onedev.server.security.SecurityUtils;
import io.onedev.server.security.permission.AccessProject;
import io.onedev.server.timetracking.TimeTrackingManager;
import io.onedev.server.util.DateUtils;
import io.onedev.server.util.Input;
import io.onedev.server.util.LinkDescriptor;
import io.onedev.server.util.ProjectScope;
import io.onedev.server.util.facade.ProjectCache;

View File

@ -111,10 +111,9 @@ public abstract class TransitionMenuLink extends MenuLink {
}
@Override
protected void onTransit(AjaxRequestTarget target, Map<String, Object> fieldValues,
String comment) {
protected void onTransit(AjaxRequestTarget target, Map<String, Object> fieldValues, String comment) {
IssueChangeManager manager = OneDev.getInstance(IssueChangeManager.class);
manager.changeState(getIssue(), toState, fieldValues, transition.getRemoveFields(), comment);
manager.changeState(getIssue(), toState, fieldValues, transition.getPromptFields(), transition.getRemoveFields(), comment);
((BasePage)getPage()).notifyObservablesChange(target, getIssue().getChangeObservables(true));
modal.close();
}

View File

@ -46,6 +46,7 @@ import org.apache.wicket.request.mapper.parameter.PageParameters;
import com.google.common.collect.Lists;
import io.onedev.server.OneDev;
import io.onedev.server.buildspecmodel.inputspec.Input;
import io.onedev.server.entitymanager.IssueChangeManager;
import io.onedev.server.entitymanager.IssueVoteManager;
import io.onedev.server.entitymanager.IssueWatchManager;
@ -63,7 +64,6 @@ import io.onedev.server.search.entity.issue.IssueQuery;
import io.onedev.server.search.entity.issue.IssueQueryLexer;
import io.onedev.server.search.entity.issue.StateCriteria;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.util.Input;
import io.onedev.server.util.Similarities;
import io.onedev.server.web.WebConstants;
import io.onedev.server.web.ajaxlistener.AttachAjaxIndicatorListener;

View File

@ -62,8 +62,8 @@ public class MultiChoiceEditor extends PropertyEditor<List<String>> {
@Override
protected Map<String, String> load() {
ComponentContext componentContext = new ComponentContext(MultiChoiceEditor.this);
ComponentContext.push(componentContext);
if (getChoiceProvider().displayNames().length() != 0) {
ComponentContext.push(componentContext);
try {
return (Map<String, String>) ReflectionUtils.invokeStaticMethod(descriptor.getBeanClass(), getChoiceProvider().displayNames());
} finally {
@ -81,8 +81,8 @@ public class MultiChoiceEditor extends PropertyEditor<List<String>> {
@Override
protected Map<String, String> load() {
ComponentContext componentContext = new ComponentContext(MultiChoiceEditor.this);
ComponentContext.push(componentContext);
if (getChoiceProvider().descriptions().length() != 0) {
ComponentContext.push(componentContext);
try {
return (Map<String, String>) ReflectionUtils.invokeStaticMethod(descriptor.getBeanClass(), getChoiceProvider().descriptions());
} finally {

View File

@ -60,8 +60,8 @@ public class SingleChoiceEditor extends PropertyEditor<String> {
@Override
protected Map<String, String> load() {
ComponentContext componentContext = new ComponentContext(SingleChoiceEditor.this);
ComponentContext.push(componentContext);
if (getChoiceProvider().displayNames().length() != 0) {
ComponentContext.push(componentContext);
try {
return (Map<String, String>) ReflectionUtils.invokeStaticMethod(descriptor.getBeanClass(), getChoiceProvider().displayNames());
} finally {
@ -79,8 +79,8 @@ public class SingleChoiceEditor extends PropertyEditor<String> {
@Override
protected Map<String, String> load() {
ComponentContext componentContext = new ComponentContext(SingleChoiceEditor.this);
ComponentContext.push(componentContext);
if (getChoiceProvider().descriptions().length() != 0) {
ComponentContext.push(componentContext);
try {
return (Map<String, String>) ReflectionUtils.invokeStaticMethod(descriptor.getBeanClass(), getChoiceProvider().descriptions());
} finally {

View File

@ -32,6 +32,7 @@ import org.apache.wicket.request.cycle.RequestCycle;
import org.hibernate.Hibernate;
import io.onedev.server.OneDev;
import io.onedev.server.buildspecmodel.inputspec.Input;
import io.onedev.server.entitymanager.IssueLinkManager;
import io.onedev.server.entitymanager.IssueManager;
import io.onedev.server.model.Issue;
@ -40,7 +41,6 @@ import io.onedev.server.model.Iteration;
import io.onedev.server.model.Project;
import io.onedev.server.model.support.issue.BoardSpec;
import io.onedev.server.model.support.issue.field.spec.FieldSpec;
import io.onedev.server.util.Input;
import io.onedev.server.util.LinkDescriptor;
import io.onedev.server.web.ajaxlistener.AttachAjaxIndicatorListener;
import io.onedev.server.web.ajaxlistener.AttachAjaxIndicatorListener.AttachMode;

View File

@ -427,7 +427,7 @@ abstract class BoardColumnPanel extends AbstractColumnPanel {
};
} else {
getIssueChangeManager().changeState(issue, getColumn(),
new HashMap<>(), transitionRef.get().getRemoveFields(), null);
new HashMap<>(), transitionRef.get().getPromptFields(), transitionRef.get().getRemoveFields(), null);
cardListPanel.onCardDropped(target, issueId, cardIndex, true);
}
} else {

View File

@ -61,7 +61,7 @@ abstract class StateTransitionPanel extends Panel implements InputContext {
Map<String, Object> fieldValues = FieldUtils.getFieldValues(
editor.newComponentContext(), fieldBean, editableFields);
OneDev.getInstance(IssueChangeManager.class).changeState(getIssue(),
getToState(), fieldValues, transition.getRemoveFields(), null);
getToState(), fieldValues, transition.getPromptFields(), transition.getRemoveFields(), null);
onSaved(target);
}

View File

@ -37,6 +37,7 @@ import org.apache.wicket.request.mapper.parameter.PageParameters;
import com.google.common.collect.Sets;
import io.onedev.server.OneDev;
import io.onedev.server.buildspecmodel.inputspec.Input;
import io.onedev.server.data.migration.VersionedXmlDoc;
import io.onedev.server.entitymanager.IssueLinkManager;
import io.onedev.server.entitymanager.IssueManager;
@ -50,7 +51,6 @@ import io.onedev.server.model.support.QueryPersonalization;
import io.onedev.server.model.support.issue.NamedIssueQuery;
import io.onedev.server.model.support.issue.ProjectIssueSetting;
import io.onedev.server.security.SecurityUtils;
import io.onedev.server.util.Input;
import io.onedev.server.util.LinkDescriptor;
import io.onedev.server.web.ajaxlistener.AttachAjaxIndicatorListener;
import io.onedev.server.web.behavior.ChangeObserver;

@ -1 +1 @@
Subproject commit 39fd6c34f64496cf7d7725aacfdfbcf4f37e47ce
Subproject commit b4df43a570ed7b071a49e45fa8dbb7cf208b170b