return mapper(dbSession).selectByBranchKeys(branchKeyByProjectUuid);
}
- public List<BranchDto> selectByPullRequestKeys(DbSession dbSession, Map<String, String> prKeyByProjectUuid) {
- if (prKeyByProjectUuid.isEmpty()) {
- return emptyList();
- }
- return mapper(dbSession).selectByPullRequestKeys(prKeyByProjectUuid);
- }
-
public Optional<BranchDto> selectByPullRequestKey(DbSession dbSession, String projectUuid, String key) {
return selectByKey(dbSession, projectUuid, key, BranchType.PULL_REQUEST);
}
private String timeZone;
private Integer owaspAsvsLevel;
private List<String> codeVariants;
+ private String fixedInPullRequest;
public SearchRequest() {
// nothing to do here
this.cleanCodeAttributesCategories = cleanCodeAttributesCategories;
return this;
}
+
+ @CheckForNull
+ public String getFixedInPullRequest() {
+ return fixedInPullRequest;
+ }
+
+ public SearchRequest setFixedInPullRequest(@Nullable String fixedInPullRequest) {
+ this.fixedInPullRequest = fixedInPullRequest;
+ return this;
+ }
}
}
// Field Filters
- filters.addFilter(FIELD_ISSUE_KEY, new SimpleFieldFilterScope(FIELD_ISSUE_KEY), createTermsFilter(FIELD_ISSUE_KEY, query.issueKeys()));
+ filters.addFilter(FIELD_ISSUE_KEY, new SimpleFieldFilterScope(FIELD_ISSUE_KEY), createTermsFilterForNullableCollection(FIELD_ISSUE_KEY, query.issueKeys()));
filters.addFilter(FIELD_ISSUE_ASSIGNEE_UUID, ASSIGNEES.getFilterScope(), createTermsFilter(FIELD_ISSUE_ASSIGNEE_UUID, query.assignees()));
filters.addFilter(FIELD_ISSUE_SCOPE, SCOPES.getFilterScope(), createTermsFilter(FIELD_ISSUE_SCOPE, query.scopes()));
filters.addFilter(FIELD_ISSUE_LANGUAGE, LANGUAGES.getFilterScope(), createTermsFilter(FIELD_ISSUE_LANGUAGE, query.languages()));
return FACET_MODE_EFFORT.equals(query.facetMode());
}
+ /**
+ * This method is for creating a filter that passes null to the elasticsearch query whenever empty or null collection is passed.
+ * This means that filter will not filter anything, all the documents (issues) will be returned in this case.
+ */
@CheckForNull
private static QueryBuilder createTermsFilter(String field, Collection<?> values) {
return values.isEmpty() ? null : termsQuery(field, values);
}
+ /**
+ * This method is for creating a filter that passes null to the elasticsearch query only when null collection is passed.
+ * This ensures that whenever we pass empty collection to the filter, it will filter out all the documents (issues).
+ */
+ @CheckForNull
+ private static QueryBuilder createTermsFilterForNullableCollection(String field, @Nullable Collection<?> values) {
+ return values != null ? termsQuery(field, values) : null;
+ }
+
@CheckForNull
private static QueryBuilder createTermFilter(String field, @Nullable String value) {
return value == null ? null : termQuery(field, value);
private final Collection<String> cleanCodeAttributesCategories;
private IssueQuery(Builder builder) {
- this.issueKeys = defaultCollection(builder.issueKeys);
+ this.issueKeys = nullableDefaultCollection(builder.issueKeys);
this.severities = defaultCollection(builder.severities);
this.impactSeverities = defaultCollection(builder.impactSeverities);
this.impactSoftwareQualities = defaultCollection(builder.impactSoftwareQualities);
return c == null ? Collections.emptyList() : Collections.unmodifiableCollection(c);
}
+ private static <T> Collection<T> nullableDefaultCollection(@Nullable Collection<T> c) {
+ return c == null ? null : Collections.unmodifiableCollection(c);
+ }
+
private static <K, V> Map<K, V> defaultMap(@Nullable Map<K, V> map) {
return map == null ? Collections.emptyMap() : Collections.unmodifiableMap(map);
}
import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.issue.IssueFixedDto;
import org.sonar.db.permission.GlobalPermission;
+import org.sonar.db.project.ProjectDto;
import org.sonar.db.rule.RuleDto;
import org.sonar.server.issue.SearchRequest;
import org.sonar.server.issue.index.IssueQuery.PeriodStart;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_UUIDS;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_AFTER;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_IN_LAST;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_FIXED_IN_PULL_REQUEST;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_IN_NEW_CODE_PERIOD;
/**
.map(Enum::name)
.collect(Collectors.toSet());
private static final ComponentDto UNKNOWN_COMPONENT = new ComponentDto().setUuid(UNKNOWN).setBranchUuid(UNKNOWN);
- private static final Set<String> QUALIFIERS_WITHOUT_LEAK_PERIOD = new HashSet<>(Arrays.asList(Qualifiers.APP, Qualifiers.VIEW, Qualifiers.SUBVIEW));
+ private static final Set<String> QUALIFIERS_WITHOUT_LEAK_PERIOD = new HashSet<>(Arrays.asList(Qualifiers.APP, Qualifiers.VIEW,
+ Qualifiers.SUBVIEW));
private final DbClient dbClient;
private final Clock clock;
private final UserSession userSession;
Collection<RuleDto> ruleDtos = ruleKeysToRuleId(dbSession, request.getRules());
Collection<String> ruleUuids = ruleDtos.stream().map(RuleDto::getUuid).collect(Collectors.toSet());
+ Collection<String> issueKeys = collectIssueKeys(dbSession, request);
if (request.getRules() != null && request.getRules().stream().collect(Collectors.toSet()).size() != ruleDtos.size()) {
ruleUuids.add("non-existing-uuid");
}
IssueQuery.Builder builder = IssueQuery.builder()
- .issueKeys(request.getIssues())
+ .issueKeys(issueKeys)
.severities(request.getSeverities())
.cleanCodeAttributesCategories(request.getCleanCodeAttributesCategories())
.impactSoftwareQualities(request.getImpactSoftwareQualities())
}
}
+ private Collection<String> collectIssueKeys(DbSession dbSession, SearchRequest request) {
+ Collection<String> issueKeys = null;
+ if (request.getFixedInPullRequest() != null) {
+ issueKeys = getIssuesFixedByPullRequest(dbSession, request);
+ }
+ if (request.getIssues() != null && !request.getIssues().isEmpty()) {
+ if (issueKeys == null) {
+ issueKeys = new ArrayList<>();
+ }
+ issueKeys.addAll(request.getIssues());
+ }
+
+ return issueKeys;
+ }
+
+ private Collection<String> getIssuesFixedByPullRequest(DbSession dbSession, SearchRequest request) {
+ String fixedInPullRequest = request.getFixedInPullRequest();
+ List<String> componentKeys = request.getComponentKeys();
+ if (componentKeys == null || componentKeys.size() != 1) {
+ throw new IllegalArgumentException("Exactly one project needs to be provided in the " +
+ "'" + PARAM_COMPONENTS + "' param when used together with '" + PARAM_FIXED_IN_PULL_REQUEST + "' param");
+ }
+ String projectKey = componentKeys.get(0);
+ ProjectDto projectDto = dbClient.projectDao().selectProjectByKey(dbSession, projectKey)
+ .orElseThrow(() -> new IllegalArgumentException("Project with key '" + projectKey + "' does not exist"));
+ BranchDto pullRequest = dbClient.branchDao().selectByPullRequestKey(dbSession, projectDto.getUuid(), fixedInPullRequest)
+ .orElseThrow(() -> new IllegalArgumentException("Pull request with key '" + fixedInPullRequest + "' does not exist for a project " +
+ projectKey));
+
+ if (request.getBranch() != null) {
+ BranchDto targetBranch = dbClient.branchDao().selectByBranchKey(dbSession, projectDto.getUuid(), request.getBranch())
+ .orElseThrow(() -> new IllegalArgumentException("Branch with key '" + request.getBranch() + "' does not exist"));
+ if (!Objects.equals(targetBranch.getUuid(), pullRequest.getMergeBranchUuid())) {
+ throw new IllegalArgumentException("Pull request with key '" + fixedInPullRequest + "' does not target branch '" + request.getBranch() + "'");
+ }
+ }
+ return dbClient.issueFixedDao().selectByPullRequest(dbSession, pullRequest.getUuid())
+ .stream()
+ .map(IssueFixedDto::issueKey)
+ .collect(Collectors.toSet());
+ }
+
+
private static Optional<ZoneId> parseTimeZone(@Nullable String timeZone) {
if (timeZone == null) {
return Optional.empty();
}
}
- private void setCreatedAfterFromDates(IssueQuery.Builder builder, @Nullable Date createdAfter, @Nullable String createdInLast, boolean createdAfterInclusive) {
+ private void setCreatedAfterFromDates(IssueQuery.Builder builder, @Nullable Date createdAfter, @Nullable String createdInLast,
+ boolean createdAfterInclusive) {
Date actualCreatedAfter = createdAfter;
if (createdInLast != null) {
actualCreatedAfter = Date.from(
builder.createdAfter(actualCreatedAfter, createdAfterInclusive);
}
- private void setCreatedAfterFromRequest(DbSession dbSession, IssueQuery.Builder builder, SearchRequest request, List<ComponentDto> componentUuids, ZoneId timeZone) {
+ private void setCreatedAfterFromRequest(DbSession dbSession, IssueQuery.Builder builder, SearchRequest request,
+ List<ComponentDto> componentUuids, ZoneId timeZone) {
Date createdAfter = parseStartingDateOrDateTime(request.getCreatedAfter(), timeZone);
String createdInLast = request.getCreatedInLast();
if (notInNewCodePeriod(request)) {
- checkArgument(createdAfter == null || createdInLast == null, format("Parameters %s and %s cannot be set simultaneously", PARAM_CREATED_AFTER, PARAM_CREATED_IN_LAST));
+ checkArgument(createdAfter == null || createdInLast == null, format("Parameters %s and %s cannot be set simultaneously",
+ PARAM_CREATED_AFTER, PARAM_CREATED_IN_LAST));
setCreatedAfterFromDates(builder, createdAfter, createdInLast, true);
} else {
// If the filter is on leak period
- checkArgument(createdAfter == null, "Parameters '%s' and '%s' cannot be set simultaneously", PARAM_CREATED_AFTER, PARAM_IN_NEW_CODE_PERIOD);
+ checkArgument(createdAfter == null, "Parameters '%s' and '%s' cannot be set simultaneously", PARAM_CREATED_AFTER,
+ PARAM_IN_NEW_CODE_PERIOD);
checkArgument(createdInLast == null,
format("Parameters '%s' and '%s' cannot be set simultaneously", PARAM_CREATED_IN_LAST, PARAM_IN_NEW_CODE_PERIOD));
.collect(Collectors.toSet());
}
- private void addComponentsBasedOnQualifier(IssueQuery.Builder builder, DbSession dbSession, List<ComponentDto> components, SearchRequest request) {
+ private void addComponentsBasedOnQualifier(IssueQuery.Builder builder, DbSession dbSession, List<ComponentDto> components,
+ SearchRequest request) {
if (components.isEmpty()) {
return;
}
builder.viewUuids(filteredViewUuids);
}
- private void addApplications(IssueQuery.Builder builder, DbSession dbSession, List<ComponentDto> appBranchComponents, SearchRequest request) {
+ private void addApplications(IssueQuery.Builder builder, DbSession dbSession, List<ComponentDto> appBranchComponents,
+ SearchRequest request) {
Set<String> authorizedAppBranchUuids = appBranchComponents.stream()
.filter(app -> userSession.hasComponentPermission(USER, app) && userSession.hasChildProjectsPermission(USER, app))
.map(ComponentDto::uuid)
addCreatedAfterByProjects(builder, dbSession, request, authorizedAppBranchUuids);
}
- private void addCreatedAfterByProjects(IssueQuery.Builder builder, DbSession dbSession, SearchRequest request, Set<String> appBranchUuids) {
+ private void addCreatedAfterByProjects(IssueQuery.Builder builder, DbSession dbSession, SearchRequest request,
+ Set<String> appBranchUuids) {
if (notInNewCodePeriod(request) || request.getPullRequest() != null) {
return;
}
builder.directories(paths);
}
- private List<ComponentDto> getComponentsFromKeys(DbSession dbSession, Collection<String> componentKeys, @Nullable String branch, @Nullable String pullRequest) {
+ private List<ComponentDto> getComponentsFromKeys(DbSession dbSession, Collection<String> componentKeys, @Nullable String branch,
+ @Nullable String pullRequest) {
List<ComponentDto> componentDtos = dbClient.componentDao().selectByKeys(dbSession, componentKeys, branch, pullRequest);
if (!componentKeys.isEmpty() && componentDtos.isEmpty()) {
return singletonList(UNKNOWN_COMPONENT);
assertThat(query.codeVariants()).containsOnly("variant1", "variant2");
}
+ @Test
+ public void getIssuesFixedByPullRequest_returnIssuesFixedByThePullRequest() {
+ String ruleAdHocName = "New Name";
+ UserDto user = db.users().insertUser(u -> u.setLogin("joanna"));
+ ProjectData projectData = db.components().insertPrivateProject();
+ ComponentDto project = projectData.getMainBranchComponent();
+ ComponentDto file = db.components().insertComponent(newFileDto(project));
+
+ RuleDto rule1 = ruleDbTester.insert(r -> r.setAdHocName(ruleAdHocName));
+ RuleDto rule2 = ruleDbTester.insert(r -> r.setAdHocName(ruleAdHocName));
+ newRule(RuleKey.of("findbugs", "NullReference"));
+ SearchRequest request = new SearchRequest()
+ .setIssues(asList("anIssueKey"))
+ .setSeverities(asList("MAJOR", "MINOR"))
+ .setStatuses(asList("CLOSED"))
+ .setResolutions(asList("FALSE-POSITIVE"))
+ .setResolved(true)
+ .setProjectKeys(asList(project.getKey()))
+ .setDirectories(asList("aDirPath"))
+ .setFiles(asList(file.uuid()))
+ .setAssigneesUuid(asList(user.getUuid()))
+ .setScopes(asList("MAIN", "TEST"))
+ .setLanguages(asList("xoo"))
+ .setTags(asList("tag1", "tag2"))
+ .setAssigned(true)
+ .setCreatedAfter("2013-04-16T09:08:24+0200")
+ .setCreatedBefore("2013-04-17T09:08:24+0200")
+ .setRules(asList(rule1.getKey().toString(), rule2.getKey().toString()))
+ .setSort("CREATION_DATE")
+ .setAsc(true)
+ .setCodeVariants(asList("variant1", "variant2"));
+
+ IssueQuery query = underTest.create(request);
+
+ assertThat(query.issueKeys()).containsOnly("anIssueKey");
+ assertThat(query.severities()).containsOnly("MAJOR", "MINOR");
+ assertThat(query.statuses()).containsOnly("CLOSED");
+ assertThat(query.resolutions()).containsOnly("FALSE-POSITIVE");
+ assertThat(query.resolved()).isTrue();
+ assertThat(query.projectUuids()).containsOnly(projectData.projectUuid());
+ assertThat(query.files()).containsOnly(file.uuid());
+ assertThat(query.assignees()).containsOnly(user.getUuid());
+ assertThat(query.scopes()).containsOnly("TEST", "MAIN");
+ assertThat(query.languages()).containsOnly("xoo");
+ assertThat(query.tags()).containsOnly("tag1", "tag2");
+ assertThat(query.onComponentOnly()).isFalse();
+ assertThat(query.assigned()).isTrue();
+ assertThat(query.rules()).hasSize(2);
+ assertThat(query.ruleUuids()).hasSize(2);
+ assertThat(query.directories()).containsOnly("aDirPath");
+ assertThat(query.createdAfter().date()).isEqualTo(parseDateTime("2013-04-16T09:08:24+0200"));
+ assertThat(query.createdAfter().inclusive()).isTrue();
+ assertThat(query.createdBefore()).isEqualTo(parseDateTime("2013-04-17T09:08:24+0200"));
+ assertThat(query.sort()).isEqualTo(IssueQuery.SORT_BY_CREATION_DATE);
+ assertThat(query.asc()).isTrue();
+ assertThat(query.codeVariants()).containsOnly("variant1", "variant2");
+ }
+
@Test
public void create_with_rule_key_that_does_not_exist_in_the_db() {
db.users().insertUser(u -> u.setLogin("joanna"));
}
@Test
- public void collection_params_should_not_be_null_but_empty() {
+ public void collection_params_should_not_be_null_but_empty_except_issue_keys() {
IssueQuery query = IssueQuery.builder()
.issueKeys(null)
.projectUuids(null)
.cwe(null)
.createdAfterByProjectUuids(null)
.build();
- assertThat(query.issueKeys()).isEmpty();
+ assertThat(query.issueKeys()).isNull();
assertThat(query.projectUuids()).isEmpty();
assertThat(query.componentUuids()).isEmpty();
assertThat(query.statuses()).isEmpty();
@Test
public void test_default_query() {
IssueQuery query = IssueQuery.builder().build();
- assertThat(query.issueKeys()).isEmpty();
assertThat(query.projectUuids()).isEmpty();
assertThat(query.componentUuids()).isEmpty();
assertThat(query.statuses()).isEmpty();
assertThat(query.languages()).isEmpty();
assertThat(query.tags()).isEmpty();
assertThat(query.types()).isEmpty();
+ assertThat(query.issueKeys()).isNull();
assertThat(query.branchUuid()).isNull();
assertThat(query.assigned()).isNull();
assertThat(query.createdAfter()).isNull();
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchDto;
import org.sonar.db.component.BranchType;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ProjectData;
import org.sonar.db.issue.ImpactDto;
import org.sonar.db.issue.IssueChangeDto;
import org.sonar.db.issue.IssueDto;
+import org.sonar.db.issue.IssueFixedDto;
import org.sonar.db.permission.GroupPermissionDto;
import org.sonar.db.project.ProjectDto;
import org.sonar.db.protobuf.DbCommons;
@Test
public void fail_if_trying_to_filter_issues_by_hotspots() {
- ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
- ComponentDto file = db.components().insertComponent(newFileDto(project));
+ ComponentDto mainBranch = db.components().insertPublicProject().getMainBranchComponent();
+ ComponentDto file = db.components().insertComponent(newFileDto(mainBranch));
RuleDto hotspotRule = newHotspotRule();
- db.issues().insertHotspot(hotspotRule, project, file);
+ db.issues().insertHotspot(hotspotRule, mainBranch, file);
insertIssues(i -> i.setType(RuleType.BUG), i -> i.setType(RuleType.VULNERABILITY),
i -> i.setType(RuleType.CODE_SMELL));
indexPermissionsAndIssues();
"createdBefore", "createdInLast", "directories", "facets", "files", "issues", "scopes", "languages", "onComponentOnly",
"p", "projects", "ps", "resolutions", "resolved", "rules", "s", "severities", "statuses", "tags", "types", "pciDss-3.2", "pciDss-4.0", "owaspAsvs-4.0",
"owaspAsvsLevel", "owaspTop10", "owaspTop10-2021", "sansTop25", "cwe", "sonarsourceSecurity", "timeZone", "inNewCodePeriod", "codeVariants",
- "cleanCodeAttributeCategories", "impactSeverities", "impactSoftwareQualities", "issueStatuses");
+ "cleanCodeAttributeCategories", "impactSeverities", "impactSoftwareQualities", "issueStatuses", "fixedInPullRequest");
WebService.Param branch = def.param(PARAM_BRANCH);
assertThat(branch.isInternal()).isFalse();
.extracting(Issue::getRuleDescriptionContextKey).containsExactly("spring");
}
+ @Test
+ public void search_whenFixedInPullRequestSetAndNoComponentsSet_throwException() {
+ TestRequest request = ws.newRequest()
+ .setParam("fixedInPullRequest", "1000");
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Exactly one project needs to be provided in the 'components' param when used together with 'fixedInPullRequest' param");
+ }
+
+ @Test
+ public void search_whenFixedInPullRequestSetAndWrongBranchIsSet_throwException() {
+ String pullRequestId = "1000";
+ String pullRequestUuid = "pullRequestUuid";
+ userSession.logIn(db.users().insertUser());
+ ProjectData project = db.components().insertPublicProject();
+ db.getDbClient().branchDao().insert(session, new BranchDto()
+ .setUuid(pullRequestUuid)
+ .setProjectUuid(project.projectUuid())
+ .setKey(pullRequestId)
+ .setIsMain(false)
+ .setBranchType(BranchType.PULL_REQUEST)
+ .setMergeBranchUuid(project.mainBranchUuid()));
+ db.getDbClient().branchDao().insert(session, new BranchDto()
+ .setUuid("wrongBranchUuid")
+ .setProjectUuid(project.projectUuid())
+ .setKey("wrongBranch")
+ .setIsMain(false)
+ .setBranchType(BranchType.BRANCH)
+ .setMergeBranchUuid("wrongTargetBranchUuid"));
+
+ session.commit();
+ TestRequest request = ws.newRequest().setParam("fixedInPullRequest", pullRequestId).setParam("components", project.projectKey())
+ .setParam("branch", "wrongBranch");
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Pull request with key '1000' does not target branch 'wrongBranch'");
+ }
+
+ @Test
+ public void search_whenFixedInPullRequestSetAndProjectDoesNotExist_throwException() {
+ String pullRequestId = "1000";
+ String pullRequestUuid = "pullRequestUuid";
+ userSession.logIn(db.users().insertUser());
+ ProjectData project = db.components().insertPublicProject();
+ db.getDbClient().branchDao().insert(session, new BranchDto()
+ .setUuid(pullRequestUuid)
+ .setProjectUuid(project.projectUuid())
+ .setKey(pullRequestId)
+ .setIsMain(false)
+ .setBranchType(BranchType.PULL_REQUEST)
+ .setMergeBranchUuid(project.mainBranchUuid()));
+
+ session.commit();
+ TestRequest request = ws.newRequest().setParam("fixedInPullRequest", pullRequestId).setParam("components", "nonExistingProjectKey");
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Project with key 'nonExistingProjectKey' does not exist");
+ }
+
+ @Test
+ public void search_whenWrongFixedInPullRequestSet_throwException() {
+ String pullRequestId = "wrongPullRequest";
+ String pullRequestUuid = "pullRequestUuid";
+ userSession.logIn(db.users().insertUser());
+ ProjectData project = db.components().insertPublicProject();
+ db.getDbClient().branchDao().insert(session, new BranchDto()
+ .setUuid(pullRequestUuid)
+ .setProjectUuid(project.projectUuid())
+ .setKey("pullRequestId")
+ .setIsMain(false)
+ .setBranchType(BranchType.PULL_REQUEST)
+ .setMergeBranchUuid(project.mainBranchUuid()));
+
+ session.commit();
+ TestRequest request = ws.newRequest().setParam("fixedInPullRequest", pullRequestId).setParam("components", project.projectKey());
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Pull request with key 'wrongPullRequest' does not exist for a project " + project.projectKey());
+ }
+
+ @Test
+ public void search_whenFixedInPullRequestSetAndNonExistingBranchIsSet_throwException() {
+ String pullRequestId = "1000";
+ String pullRequestUuid = "pullRequestUuid";
+ userSession.logIn(db.users().insertUser());
+ ProjectData project = db.components().insertPublicProject();
+ db.getDbClient().branchDao().insert(session, new BranchDto()
+ .setUuid(pullRequestUuid)
+ .setProjectUuid(project.projectUuid())
+ .setKey(pullRequestId)
+ .setIsMain(false)
+ .setBranchType(BranchType.PULL_REQUEST)
+ .setMergeBranchUuid(project.mainBranchUuid()));
+
+ session.commit();
+ TestRequest request = ws.newRequest().setParam("fixedInPullRequest", pullRequestId).setParam("components", project.projectKey())
+ .setParam("branch", "nonExistingBranch");
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Branch with key 'nonExistingBranch' does not exist");
+ }
+
+ @Test
+ public void search_whenFixedInPullRequestSetAndComponentsIsSetButNoIssueFixedInPR_returnZeroIssues() {
+ String pullRequestId = "1000";
+ String pullRequestUuid = "pullRequestUuid";
+ String issueKey = "issueKey";
+ userSession.logIn(db.users().insertUser());
+ ProjectData project = db.components().insertPublicProject();
+ db.getDbClient().branchDao().insert(session, new BranchDto()
+ .setUuid(pullRequestUuid)
+ .setProjectUuid(project.projectUuid())
+ .setKey(pullRequestId)
+ .setIsMain(false)
+ .setBranchType(BranchType.PULL_REQUEST)
+ .setMergeBranchUuid(project.mainBranchUuid()));
+
+ TestRequest request = ws.newRequest().setParam("components", project.projectKey()).setParam("fixedInPullRequest", pullRequestId);
+ insertIssues(project.getMainBranchComponent(), i -> i.setKee(issueKey));
+ session.commit();
+ indexPermissionsAndIssues();
+
+ SearchWsResponse response = request.executeProtobuf(SearchWsResponse.class);
+
+ List<Issue> issuesList = response.getIssuesList();
+ assertThat(issuesList).isEmpty();
+ }
+
+ @Test
+ public void search_whenFixedInPullRequestSetAndComponentsIsSet_returnOneIssueFixedInPR() {
+ String pullRequestId = "1000";
+ String pullRequestUuid = "pullRequestUuid";
+ String issueKey = "issueKey";
+ userSession.logIn(db.users().insertUser());
+ ProjectData project = db.components().insertPublicProject();
+ db.getDbClient().branchDao().insert(session, new BranchDto()
+ .setUuid(pullRequestUuid)
+ .setProjectUuid(project.projectUuid())
+ .setKey(pullRequestId)
+ .setIsMain(false)
+ .setBranchType(BranchType.PULL_REQUEST)
+ .setMergeBranchUuid(project.mainBranchUuid()));
+
+ TestRequest request = ws.newRequest().setParam("components", project.projectKey()).setParam("fixedInPullRequest", pullRequestId);
+ insertIssues(project.getMainBranchComponent(), i -> i.setKee(issueKey));
+ db.getDbClient().issueFixedDao().insert(session, new IssueFixedDto(pullRequestUuid, issueKey));
+ session.commit();
+ indexPermissionsAndIssues();
+
+ SearchWsResponse response = request.executeProtobuf(SearchWsResponse.class);
+
+ List<Issue> issuesList = response.getIssuesList();
+ assertThat(issuesList).hasSize(1);
+ assertThat(issuesList.get(0).getKey()).isEqualTo(issueKey);
+ }
+
private RuleDto newIssueRule() {
RuleDto rule = newRule(XOO_X1, createDefaultRuleDescriptionSection(uuidFactory.create(), "Rule desc"))
.setLanguage("xoo")
UserDto john = db.users().insertUser();
userSession.logIn(john);
RuleDto rule = db.rules().insertIssueRule();
- ComponentDto project = db.components().insertPublicProject().getMainBranchComponent();
- ComponentDto file = db.components().insertComponent(newFileDto(project));
+ ComponentDto branch = db.components().insertPublicProject().getMainBranchComponent();
+ ComponentDto file = db.components().insertComponent(newFileDto(branch));
+ for (Consumer<IssueDto> populator : populators) {
+ db.issues().insertIssue(rule, branch, file, populator);
+ }
+ }
+
+ private void insertIssues(ComponentDto branch, Consumer<IssueDto>... populators) {
+ UserDto john = db.users().insertUser();
+ userSession.logIn(john);
+ RuleDto rule = db.rules().insertIssueRule();
+ ComponentDto file = db.components().insertComponent(newFileDto(branch));
for (Consumer<IssueDto> populator : populators) {
- db.issues().insertIssue(rule, project, file, populator);
+ db.issues().insertIssue(rule, branch, file, populator);
}
}
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CWE;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_DIRECTORIES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_FILES;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_FIXED_IN_PULL_REQUEST;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_IMPACT_SEVERITIES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_IMPACT_SOFTWARE_QUALITIES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_IN_NEW_CODE_PERIOD;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUES;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE_STATUSES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_LANGUAGES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ON_COMPONENT_ONLY;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_ASVS_40;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SANS_TOP_25;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SCOPES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SEVERITIES;
-import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE_STATUSES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SONARSOURCE_SECURITY;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_STATUSES;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TAGS;
public class SearchAction implements IssuesWsAction {
private static final String LOGIN_MYSELF = "__me__";
private static final Set<String> ISSUE_SCOPES = Arrays.stream(IssueScope.values()).map(Enum::name).collect(Collectors.toSet());
- private static final EnumSet<RuleType> ALL_RULE_TYPES_EXCEPT_SECURITY_HOTSPOTS = EnumSet.complementOf(EnumSet.of(RuleType.SECURITY_HOTSPOT));
+ private static final EnumSet<RuleType> ALL_RULE_TYPES_EXCEPT_SECURITY_HOTSPOTS =
+ EnumSet.complementOf(EnumSet.of(RuleType.SECURITY_HOTSPOT));
static final List<String> SUPPORTED_FACETS = List.of(
FACET_PROJECTS,
PARAM_IMPACT_SEVERITIES,
PARAM_ISSUE_STATUSES);
- private static final String INTERNAL_PARAMETER_DISCLAIMER = "This parameter is mostly used by the Issues page, please prefer usage of the componentKeys parameter. ";
+ private static final String INTERNAL_PARAMETER_DISCLAIMER = "This parameter is mostly used by the Issues page, please prefer usage of " +
+ "the componentKeys parameter. ";
private static final String NEW_FACET_ADDED_MESSAGE = "Facet '%s' has been added";
private static final String NEW_PARAM_ADDED_MESSAGE = "Param '%s' has been added";
private static final Set<String> FACETS_REQUIRING_PROJECT = newHashSet(PARAM_FILES, PARAM_DIRECTORIES);
private final System2 system2;
private final DbClient dbClient;
- public SearchAction(UserSession userSession, IssueIndex issueIndex, IssueQueryFactory issueQueryFactory, IssueIndexSyncProgressChecker issueIndexSyncProgressChecker,
+ public SearchAction(UserSession userSession, IssueIndex issueIndex, IssueQueryFactory issueQueryFactory,
+ IssueIndexSyncProgressChecker issueIndexSyncProgressChecker,
SearchResponseLoader searchResponseLoader, SearchResponseFormat searchResponseFormat, System2 system2, DbClient dbClient) {
this.userSession = userSession;
this.issueIndex = issueIndex;
+ "<br/>When issue indexation is in progress returns 503 service unavailable HTTP code.")
.setSince("3.6")
.setChangelog(
- new Change("10.4", "Value '%s' for 'transition' response field is deprecated, use '%s' instead".formatted(DefaultTransitions.WONT_FIX, DefaultTransitions.ACCEPT)),
+ new Change("10.4", "Added new param '%s'".formatted(PARAM_FIXED_IN_PULL_REQUEST)),
+ new Change("10.4",
+ "Value '%s' for 'transition' response field is deprecated, use '%s' instead".formatted(DefaultTransitions.WONT_FIX,
+ DefaultTransitions.ACCEPT)),
new Change("10.4", "Possible value '%s' for 'transition' response field has been added".formatted(DefaultTransitions.ACCEPT)),
new Change("10.4", format(NEW_PARAM_ADDED_MESSAGE, PARAM_ISSUE_STATUSES)),
new Change("10.4", format("Parameters '%s' and '%s' are deprecated in favor of '%s'.", PARAM_RESOLUTIONS, PARAM_STATUSES, PARAM_ISSUE_STATUSES)),
new Change("8.5", "Internal parameter 'fileUuids' has been dropped"),
new Change("8.4", "parameters 'componentUuids', 'projectKeys' has been dropped."),
new Change("8.2", "'REVIEWED', 'TO_REVIEW' status param values are no longer supported"),
- new Change("8.2", "Security hotspots are no longer returned as type 'SECURITY_HOTSPOT' is not supported anymore, use dedicated api/hotspots"),
+ new Change("8.2", "Security hotspots are no longer returned as type 'SECURITY_HOTSPOT' is not supported anymore, use dedicated " +
+ "api/hotspots"),
new Change("8.2", "response field 'fromHotspot' has been deprecated and is no more populated"),
new Change("8.2", "Status 'IN_REVIEW' for Security Hotspots has been deprecated"),
- new Change("7.8", format("added new Security Hotspots statuses : %s, %s and %s", STATUS_TO_REVIEW, STATUS_IN_REVIEW, STATUS_REVIEWED)),
+ new Change("7.8", format("added new Security Hotspots statuses : %s, %s and %s", STATUS_TO_REVIEW, STATUS_IN_REVIEW,
+ STATUS_REVIEWED)),
new Change("7.8", "Security hotspots are returned by default"),
new Change("7.7", format("Value 'authors' in parameter '%s' is deprecated, please use '%s' instead", FACETS, PARAM_AUTHOR)),
new Change("7.6", format("The use of module keys in parameter '%s' is deprecated", PARAM_COMPONENT_KEYS)),
new Change("6.5", "parameters 'projects', 'projectUuids', 'moduleUuids', 'directories', 'fileUuids' are marked as internal"),
new Change("6.3", "response field 'email' is renamed 'avatar'"),
new Change("5.5", "response fields 'reporter' and 'actionPlan' are removed (drop of action plan and manual issue features)"),
- new Change("5.5", "parameters 'reporters', 'actionPlans' and 'planned' are dropped and therefore ignored (drop of action plan and manual issue features)"),
+ new Change("5.5", "parameters 'reporters', 'actionPlans' and 'planned' are dropped and therefore ignored (drop of action plan and" +
+ " manual issue features)"),
new Change("5.5", "response field 'debt' is renamed 'effort'"))
.setResponseExample(getClass().getResource("search-example.json"));
action.addSortParams(IssueQuery.SORTS, null, true);
action.createParam(PARAM_ADDITIONAL_FIELDS)
.setSince("5.2")
- .setDescription("Comma-separated list of the optional fields to be returned in response. Action plans are dropped in 5.5, it is not returned in the response.")
+ .setDescription("Comma-separated list of the optional fields to be returned in response. Action plans are dropped in 5.5, it is not" +
+ " returned in the response.")
.setPossibleValues(SearchAdditionalField.possibleValues());
addComponentRelatedParams(action);
action.createParam(PARAM_ISSUES)
.setDescription("Comma-separated list of CWE identifiers. Use '" + UNKNOWN_STANDARD + "' to select issues not associated to any CWE.")
.setExampleValue("12,125," + UNKNOWN_STANDARD);
action.createParam(PARAM_SONARSOURCE_SECURITY)
- .setDescription("Comma-separated list of SonarSource security categories. Use '" + SQCategory.OTHERS.getKey() + "' to select issues not associated" +
+ .setDescription("Comma-separated list of SonarSource security categories. Use '" + SQCategory.OTHERS.getKey() + "' to select issues" +
+ " not associated" +
" with any category")
.setSince("7.8")
.setPossibleValues(Arrays.stream(SQCategory.values()).map(SQCategory::getKey).toList());
.setDescription("SCM accounts. To set several values, the parameter must be called once for each value.")
.setExampleValue("author=torvalds@linux-foundation.org&author=linux@fondation.org");
action.createParam(PARAM_ASSIGNEES)
- .setDescription("Comma-separated list of assignee logins. The value '__me__' can be used as a placeholder for user who performs the request")
+ .setDescription("Comma-separated list of assignee logins. The value '__me__' can be used as a placeholder for user who performs the" +
+ " request")
.setExampleValue("admin,usera,__me__");
action.createParam(PARAM_ASSIGNED)
.setDescription("To retrieve assigned or unassigned issues")
.setSince("9.4");
action.createParam(PARAM_TIMEZONE)
.setDescription(
- "To resolve dates passed to '" + PARAM_CREATED_AFTER + "' or '" + PARAM_CREATED_BEFORE + "' (does not apply to datetime) and to compute creation date histogram")
+ "To resolve dates passed to '" + PARAM_CREATED_AFTER + "' or '" + PARAM_CREATED_BEFORE + "' (does not apply to datetime) and to " +
+ "compute creation date histogram")
.setRequired(false)
.setExampleValue("'Europe/Paris', 'Z' or '+02:00'")
.setSince("8.6");
action.createParam(PARAM_COMPONENTS)
.setDeprecatedKey(PARAM_COMPONENT_KEYS, "10.2")
- .setDescription("Comma-separated list of component keys. Retrieve issues associated to a specific list of components (and all its descendants). " +
+ .setDescription("Comma-separated list of component keys. Retrieve issues associated to a specific list of components (and all its " +
+ "descendants). " +
"A component can be a portfolio, project, module, directory or file.")
.setExampleValue(KEY_PROJECT_EXAMPLE_001);
.setDescription("Pull request id. Not available in the community edition.")
.setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001)
.setSince("7.1");
+
+ action.createParam(PARAM_FIXED_IN_PULL_REQUEST)
+ .setDescription("Pull request id to filter issues that would be fixed in the specified project or branch by the pull request. " +
+ "Should not be used together with + '" + PARAM_PULL_REQUEST + "'. At least the '" + PARAM_COMPONENTS + "' must be be specified " +
+ "when this param is used. Not available in the community edition.")
+ .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001)
+ .setSince("10.4");
}
@Override
.filter(FACETS_REQUIRING_PROJECT::contains)
.collect(Collectors.toSet());
checkArgument(facetsRequiringProjectParameter.isEmpty() ||
- (!query.projectUuids().isEmpty()), "Facet(s) '%s' require to also filter by project",
+ (!query.projectUuids().isEmpty()), "Facet(s) '%s' require to also filter by project",
String.join(",", facetsRequiringProjectParameter));
// execute request
}
private static TotalHits getTotalHits(SearchResponse response) {
- return ofNullable(response.getHits().getTotalHits()).orElseThrow(() -> new IllegalStateException("Could not get total hits of search results"));
+ return ofNullable(response.getHits().getTotalHits()).orElseThrow(() -> new IllegalStateException("Could not get total hits of search " +
+ "results"));
}
private static SearchOptions createSearchOptionsFromRequest(SearchRequest request) {
options.setPage(request.getPage(), request.getPageSize());
List<String> facets = request.getFacets();
-
- if (facets == null || facets.isEmpty()) {
- return options;
+ if (facets != null && !facets.isEmpty()) {
+ options.addFacets(facets);
}
- options.addFacets(facets);
return options;
}
.setCwe(request.paramAsStrings(PARAM_CWE))
.setSonarsourceSecurity(request.paramAsStrings(PARAM_SONARSOURCE_SECURITY))
.setTimeZone(request.param(PARAM_TIMEZONE))
- .setCodeVariants(request.paramAsStrings(PARAM_CODE_VARIANTS));
+ .setCodeVariants(request.paramAsStrings(PARAM_CODE_VARIANTS))
+ .setFixedInPullRequest(request.param(PARAM_FIXED_IN_PULL_REQUEST));
}
private void checkIfNeedIssueSync(DbSession dbSession, SearchRequest searchRequest) {
public static final String KEY_APPLICATION_EXAMPLE_001 = "my_app";
public static final String KEY_FILE_EXAMPLE_001 = "my_project:/src/foo/Bar.php";
- public static final String KEY_FILE_EXAMPLE_002 = "another_project:/src/foo/Foo.php";
public static final String KEY_PROJECT_EXAMPLE_001 = "my_project";
public static final String KEY_PROJECT_EXAMPLE_002 = "another_project";
public static final String KEY_PROJECT_EXAMPLE_003 = "third_project";
- public static final String KEY_ORG_EXAMPLE_001 = "my-org";
- public static final String KEY_ORG_EXAMPLE_002 = "foo-company";
-
public static final String KEY_BRANCH_EXAMPLE_001 = "feature/my_branch";
public static final String KEY_PULL_REQUEST_EXAMPLE_001 = "5461";
public static final String PARAM_ADDITIONAL_FIELDS = "additionalFields";
public static final String PARAM_TIMEZONE = "timeZone";
public static final String PARAM_CODE_VARIANTS = "codeVariants";
+ public static final String PARAM_FIXED_IN_PULL_REQUEST = "fixedInPullRequest";
public static final String FACET_MODE_EFFORT = "effort";