return session.getMapper(IssueMapper.class);
}
+ public List<IssueDto> selectByBranch(DbSession dbSession, IssueQueryParams issueQueryParams, int page) {
+ Pagination pagination = Pagination.forPage(page).andSize(DEFAULT_PAGE_SIZE);
+ return mapper(dbSession).selectByBranch(issueQueryParams, pagination);
+ }
+
+ public List<String> selectRecentlyClosedIssues(DbSession dbSession, IssueQueryParams issueQueryParams) {
+ return mapper(dbSession).selectRecentlyClosedIssues(issueQueryParams);
+ }
}
@Param("baseComponent") ComponentDto baseComponent,
@Param("leakPeriodBeginningDate") long leakPeriodBeginningDate);
+
+ List<IssueDto> selectByBranch(@Param("queryParams") IssueQueryParams issueQueryParams,
+ @Param("pagination") Pagination pagination);
+
+ List<String> selectRecentlyClosedIssues(@Param("queryParams") IssueQueryParams issueQueryParams);
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.db.issue;
+
+import java.util.List;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+public class IssueQueryParams {
+
+ private final String projectUuid;
+ private final String branchName;
+ private final List<String> languages;
+ private final List<String> ruleRepositories;
+ private final boolean resolvedOnly;
+ private final Long changedSince;
+
+ public IssueQueryParams(String projectUuid, String branchName, @Nullable List<String> languages,
+ @Nullable List<String> ruleRepositories, boolean resolvedOnly, @Nullable Long changedSince) {
+ this.projectUuid = projectUuid;
+ this.branchName = branchName;
+ this.languages = languages;
+ this.ruleRepositories = ruleRepositories;
+ this.resolvedOnly = resolvedOnly;
+ this.changedSince = changedSince;
+ }
+
+ public String getProjectUuid() {
+ return projectUuid;
+ }
+
+ public String getBranchName() {
+ return branchName;
+ }
+
+ @CheckForNull
+ public List<String> getLanguages() {
+ return languages;
+ }
+
+ @CheckForNull
+ public List<String> getRuleRepositories() {
+ return ruleRepositories;
+ }
+
+ public boolean isResolvedOnly() {
+ return resolvedOnly;
+ }
+
+ @CheckForNull
+ public Long getChangedSince() {
+ return changedSince;
+ }
+}
left join users u on i.assignee = u.uuid
left join new_code_reference_issues n on i.kee = n.issue_key
</select>
+
+ <sql id="selectByBranchColumnsFinal">
+ result.kee as kee,
+ result.ruleUuid as ruleUuid,
+ result.createdAt as createdAt,
+ result.status as status,
+ result.ruleType as ruleType,
+ result.ruleRepo as ruleRepo,
+ result.ruleKey as ruleKey,
+ result.message as message,
+ result.severity as severity,
+ result.manualSeverity as manualSeverity,
+ result.type as type,
+ result.locations as locations,
+ result.component_uuid,
+ c.path as filePath
+ </sql>
+
+ <sql id="selectByBranchColumnsOuterQuery">
+ t.kee as kee,
+ t.ruleUuid as ruleUuid,
+ t.createdAt as createdAt,
+ t.status as status,
+ t.ruleType as ruleType,
+ t.ruleRepo as ruleRepo,
+ t.ruleKey as ruleKey,
+ t.message as message,
+ t.severity as severity,
+ t.manualSeverity as manualSeverity,
+ t.type as type,
+ t.locations as locations,
+ t.component_uuid
+ </sql>
+
+ <sql id="selectByBranchColumns">
+ i.kee as kee,
+ i.rule_uuid as ruleUuid,
+ i.created_at as createdAt,
+ i.status as status,
+ r.rule_type as ruleType,
+ r.plugin_name as ruleRepo,
+ r.plugin_rule_key as ruleKey,
+ i.message as message,
+ i.severity as severity,
+ i.manual_severity as manualSeverity,
+ i.issue_type as type,
+ i.locations as locations,
+ i.component_uuid as component_uuid
+ </sql>
+
+ <select id="selectByBranch" parameterType="map" resultType="Issue">
+ select
+ <include refid="selectByBranchColumns"/>
+ , p.path as filePath
+ from issues i
+ inner join project_branches b on i.project_uuid = b.project_uuid
+ inner join rules r on r.uuid = i.rule_uuid
+ inner join components p on p.uuid=i.component_uuid
+ where
+ b.kee = #{queryParams.branchName}
+ AND i.project_uuid = #{queryParams.projectUuid}
+ <if test="queryParams.changedSince != null">
+ AND i.issue_update_date >= #{queryParams.changedSince,jdbcType=BIGINT}
+ </if>
+ <if test="queryParams.resolvedOnly == true">
+ AND i.status = 'RESOLVED'
+ </if>
+ AND i.status <> 'CLOSED'
+ AND i.issue_type <> 4
+ <if test="queryParams.ruleRepositories != null">
+ AND r.plugin_name IN
+ <foreach item="ruleRepository" index="index" collection="queryParams.ruleRepositories" open="(" separator="," close=")">
+ #{ruleRepository}
+ </foreach>
+ </if>
+ <if test="queryParams.languages != null">
+ AND r.language IN
+ <foreach item="language" index="index" collection="queryParams.languages" open="(" separator="," close=")">
+ #{language}
+ </foreach>
+ </if>
+ order by i.issue_creation_date ASC
+ limit #{pagination.pageSize,jdbcType=INTEGER} offset #{pagination.offset,jdbcType=INTEGER}
+ </select>
+
+ <select id="selectByBranch" parameterType="map" resultType="Issue" databaseId="mssql">
+ select
+ <include refid="selectByBranchColumnsFinal"/>
+ from
+ (select
+ <include refid="selectByBranchColumnsOuterQuery"/>
+ from (
+ select
+ row_number() over(order by i.issue_creation_date ASC) as row_number,
+ <include refid="selectByBranchColumns"/>
+ from issues i
+ inner join project_branches b on i.project_uuid = b.project_uuid
+ inner join rules r on r.uuid = i.rule_uuid
+ where
+ b.kee = #{queryParams.branchName}
+ AND i.project_uuid = #{queryParams.projectUuid}
+ <if test="queryParams.changedSince != null">
+ AND i.issue_update_date >= #{queryParams.changedSince,jdbcType=BIGINT}
+ </if>
+ <if test="queryParams.resolvedOnly == true">
+ AND i.status = 'RESOLVED'
+ </if>
+ AND i.status <> 'CLOSED'
+ AND i.issue_type <> 4
+ <if test="queryParams.ruleRepositories != null">
+ AND r.plugin_name IN
+ <foreach item="ruleRepository" index="index" collection="queryParams.ruleRepositories" open="(" separator="," close=")">
+ #{ruleRepository}
+ </foreach>
+ </if>
+ <if test="queryParams.languages != null">
+ AND r.language IN
+ <foreach item="language" index="index" collection="queryParams.languages" open="(" separator="," close=")">
+ #{language}
+ </foreach>
+ </if>
+ order by row_number asc
+ offset #{pagination.offset} rows
+ fetch next #{pagination.pageSize,jdbcType=INTEGER} rows only
+ ) as t) as result
+ inner join components c on c.uuid=result.component_uuid
+ </select>
+
+ <select id="selectByBranch" parameterType="map" resultType="Issue" databaseId="oracle">
+ select <include refid="selectByBranchColumnsFinal"/> from
+ (select <include refid="selectByBranchColumnsOuterQuery"/> from (
+ select rownum as rn, a.* from (
+ select
+ <include refid="selectByBranchColumns"/>
+ from issues i
+ inner join project_branches b on i.project_uuid = b.project_uuid
+ inner join rules r on r.uuid = i.rule_uuid
+ where
+ b.kee = #{queryParams.branchName}
+ AND i.project_uuid = #{queryParams.projectUuid}
+ <if test="queryParams.changedSince != null">
+ AND i.issue_update_date >= #{queryParams.changedSince,jdbcType=BIGINT}
+ </if>
+ <if test="queryParams.resolvedOnly == true">
+ AND i.status = 'RESOLVED'
+ </if>
+ AND i.status <> 'CLOSED'
+ AND i.issue_type <> 4
+ <if test="queryParams.ruleRepositories != null">
+ AND r.plugin_name IN
+ <foreach item="ruleRepository" index="index" collection="queryParams.ruleRepositories" open="(" separator="," close=")">
+ #{ruleRepository}
+ </foreach>
+ </if>
+ <if test="queryParams.languages != null">
+ AND r.language IN
+ <foreach item="language" index="index" collection="queryParams.languages" open="(" separator="," close=")">
+ #{language}
+ </foreach>
+ </if>
+ order by i.issue_creation_date ASC
+ ) a
+ ) t
+ where
+ t.rn between #{pagination.startRowNumber,jdbcType=INTEGER} and #{pagination.endRowNumber,jdbcType=INTEGER}
+ order by t.rn asc) result
+ inner join components c on c.uuid=result.component_uuid
+ </select>
+
+ <select id="selectRecentlyClosedIssues" resultType="string">
+ select i.kee
+ from issues i
+ inner join project_branches b on i.project_uuid = b.project_uuid
+ inner join rules r on r.uuid = i.rule_uuid
+ where
+ i.project_uuid = #{queryParams.projectUuid}
+ AND issue_update_date >= #{queryParams.changedSince}
+ AND b.kee = #{queryParams.branchName}
+ <if test="queryParams.ruleRepositories != null">
+ AND r.plugin_name IN
+ <foreach item="ruleRepository" index="index" collection="queryParams.ruleRepositories" open="(" separator="," close=")">
+ #{ruleRepository}
+ </foreach>
+ </if>
+ <if test="queryParams.languages != null">
+ AND r.language IN
+ <foreach item="language" index="index" collection="queryParams.languages" open="(" separator="," close=")">
+ #{language}
+ </foreach>
+ </if>
+ AND i.status = 'CLOSED'
+ </select>
+
</mapper>
private static final RuleDto RULE = RuleTesting.newXooX1();
private static final String ISSUE_KEY1 = "I1";
private static final String ISSUE_KEY2 = "I2";
+ private static final String DEFAULT_BRANCH_NAME = "master";
private static final RuleType[] RULE_TYPES_EXCEPT_HOTSPOT = Stream.of(RuleType.values())
.filter(r -> r != RuleType.SECURITY_HOTSPOT)
assertThat(underTest.selectNonClosedByComponentUuidExcludingExternalsAndSecurityHotspots(db.getSession(), file.uuid()))
.extracting(IssueDto::getKey)
- .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[] {openIssue1OnFile, openIssue2OnFile}).map(IssueDto::getKey).toArray(String[]::new));
+ .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[]{openIssue1OnFile, openIssue2OnFile}).map(IssueDto::getKey).toArray(String[]::new));
assertThat(underTest.selectNonClosedByComponentUuidExcludingExternalsAndSecurityHotspots(db.getSession(), project.uuid()))
.extracting(IssueDto::getKey)
- .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[] {openIssueOnProject}).map(IssueDto::getKey).toArray(String[]::new));
+ .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[]{openIssueOnProject}).map(IssueDto::getKey).toArray(String[]::new));
assertThat(underTest.selectNonClosedByComponentUuidExcludingExternalsAndSecurityHotspots(db.getSession(), "does_not_exist")).isEmpty();
}
assertThat(underTest.selectNonClosedByModuleOrProjectExcludingExternalsAndSecurityHotspots(db.getSession(), project))
.extracting(IssueDto::getKey)
.containsExactlyInAnyOrder(
- Arrays.stream(new IssueDto[] {openIssue1OnFile, openIssue2OnFile, openIssueOnModule, openIssueOnProject}).map(IssueDto::getKey).toArray(String[]::new));
+ Arrays.stream(new IssueDto[]{openIssue1OnFile, openIssue2OnFile, openIssueOnModule, openIssueOnProject}).map(IssueDto::getKey).toArray(String[]::new));
assertThat(underTest.selectNonClosedByModuleOrProjectExcludingExternalsAndSecurityHotspots(db.getSession(), module))
.extracting(IssueDto::getKey)
- .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[] {openIssue1OnFile, openIssue2OnFile, openIssueOnModule}).map(IssueDto::getKey).toArray(String[]::new));
+ .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[]{openIssue1OnFile, openIssue2OnFile, openIssueOnModule}).map(IssueDto::getKey).toArray(String[]::new));
ComponentDto notPersisted = ComponentTesting.newPrivateProjectDto();
assertThat(underTest.selectNonClosedByModuleOrProjectExcludingExternalsAndSecurityHotspots(db.getSession(), notPersisted)).isEmpty();
assertThat(underTest.selectOrFailByKey(db.getSession(), ISSUE_KEY1).isNewCodeReferenceIssue()).isFalse();
}
+ @Test
+ public void selectByBranch_givenOneIssueOnTheRightBranchAndOneOnTheWrongOne_returnOneIssue() {
+ prepareIssuesComponent();
+ underTest.insert(db.getSession(), newIssueDto(ISSUE_KEY1)
+ .setRuleUuid(RULE.getUuid())
+ .setComponentUuid(FILE_UUID)
+ .setProjectUuid(PROJECT_UUID));
+ underTest.insert(db.getSession(), newIssueDto(ISSUE_KEY2)
+ .setRuleUuid(RULE.getUuid())
+ .setComponentUuid(FILE_UUID)
+ .setProjectUuid("another-branch-uuid"));
+ db.getSession().commit();
+
+ List<IssueDto> issueDtos = underTest.selectByBranch(db.getSession(),
+ new IssueQueryParams(PROJECT_UUID, DEFAULT_BRANCH_NAME, null, null, false, null),
+ 1);
+
+ assertThat(issueDtos).hasSize(1);
+ assertThat(issueDtos.get(0).getKey()).isEqualTo(ISSUE_KEY1);
+ }
+
+ @Test
+ public void selectByBranch_ordersResultByCreationDate() {
+ prepareIssuesComponent();
+
+ int times = 1;
+ for (;times <= 1001; times++) {
+ underTest.insert(db.getSession(), newIssueDto(String.valueOf(times))
+ .setIssueCreationTime(Long.valueOf(times))
+ .setCreatedAt(times)
+ .setRuleUuid(RULE.getUuid())
+ .setComponentUuid(FILE_UUID)
+ .setProjectUuid(PROJECT_UUID));
+ }
+ // updating time's value to the last actual value that was used for creating an issue
+ times--;
+ db.getSession().commit();
+
+ List<IssueDto> issueDtos = underTest.selectByBranch(db.getSession(),
+ new IssueQueryParams(PROJECT_UUID, DEFAULT_BRANCH_NAME, null, null, false, null),
+ 2);
+
+ assertThat(issueDtos).hasSize(1);
+ assertThat(issueDtos.get(0).getKey()).isEqualTo(String.valueOf(times));
+ }
+
+ @Test
+ public void selectByBranch_openIssueNotReturnedWhenResolvedOnlySet() {
+ prepareIssuesComponent();
+ underTest.insert(db.getSession(), newIssueDto(ISSUE_KEY1)
+ .setRuleUuid(RULE.getUuid())
+ .setComponentUuid(FILE_UUID)
+ .setStatus(Issue.STATUS_OPEN)
+ .setProjectUuid(PROJECT_UUID));
+ underTest.insert(db.getSession(), newIssueDto(ISSUE_KEY2)
+ .setRuleUuid(RULE.getUuid())
+ .setComponentUuid(FILE_UUID)
+ .setStatus(Issue.STATUS_RESOLVED)
+ .setProjectUuid(PROJECT_UUID));
+ db.getSession().commit();
+
+ List<IssueDto> issueDtos = underTest.selectByBranch(db.getSession(),
+ new IssueQueryParams(PROJECT_UUID, DEFAULT_BRANCH_NAME, null, null, true, null),
+ 1);
+
+ assertThat(issueDtos).hasSize(1);
+ assertThat(issueDtos.get(0).getKey()).isEqualTo(ISSUE_KEY2);
+ }
+
+ @Test
+ public void selectRecentlyClosedIssues_doNotReturnIssuesOlderThanTimestamp() {
+ prepareIssuesComponent();
+ underTest.insert(db.getSession(), newIssueDto(ISSUE_KEY1)
+ .setRuleUuid(RULE.getUuid())
+ .setComponentUuid(FILE_UUID)
+ .setStatus(Issue.STATUS_CLOSED)
+ .setIssueUpdateTime(10_000L)
+ .setProjectUuid(PROJECT_UUID));
+ underTest.insert(db.getSession(), newIssueDto(ISSUE_KEY2)
+ .setRuleUuid(RULE.getUuid())
+ .setComponentUuid(FILE_UUID)
+ .setStatus(Issue.STATUS_CLOSED)
+ .setIssueUpdateTime(5_000L)
+ .setProjectUuid(PROJECT_UUID));
+ db.getSession().commit();
+
+ List<String> issueUuids = underTest.selectRecentlyClosedIssues(db.getSession(),
+ new IssueQueryParams(PROJECT_UUID, DEFAULT_BRANCH_NAME, null, null, true, 8_000L));
+
+ assertThat(issueUuids).hasSize(1);
+ assertThat(issueUuids.get(0)).isEqualTo(ISSUE_KEY1);
+ }
+
private static IssueDto newIssueDto(String key) {
IssueDto dto = new IssueDto();
dto.setComponent(new ComponentDto().setDbKey("struts:Action").setUuid("component-uuid"));
import org.sonar.server.issue.index.IssueQueryFactory;
import org.sonar.server.issue.workflow.FunctionExecutor;
import org.sonar.server.issue.workflow.IssueWorkflow;
+import org.sonar.server.issue.ws.pull.PullActionProtobufObjectGenerator;
+import org.sonar.server.issue.ws.pull.PullActionResponseWriter;
import org.sonar.server.qualitygate.changeevent.QGChangeEventListenersImpl;
public class IssueWsModule extends Module {
AuthorsAction.class,
ChangelogAction.class,
BulkChangeAction.class,
- QGChangeEventListenersImpl.class);
+ QGChangeEventListenersImpl.class,
+ PullAction.class,
+ PullActionResponseWriter.class,
+ PullActionProtobufObjectGenerator.class);
}
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.ws;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+import javax.annotation.Nullable;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.issue.IssueQueryParams;
+import org.sonar.db.project.ProjectDto;
+import org.sonar.server.issue.ws.pull.PullActionIssuesRetriever;
+import org.sonar.server.issue.ws.pull.PullActionResponseWriter;
+import org.sonar.server.user.UserSession;
+
+import static org.sonar.api.web.UserRole.USER;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_PULL;
+
+public class PullAction implements IssuesWsAction {
+
+ private static final String PROJECT_KEY_PARAM = "projectKey";
+ private static final String BRANCH_NAME_PARAM = "branchName";
+ private static final String LANGUAGES_PARAM = "languages";
+ private static final String RULE_REPOSITORIES_PARAM = "ruleRepositories";
+ private static final String RESOLVED_ONLY_PARAM = "resolvedOnly";
+ private static final String CHANGED_SINCE_PARAM = "changedSince";
+
+ private final DbClient dbClient;
+ private final UserSession userSession;
+ private final PullActionResponseWriter pullActionResponseWriter;
+
+ public PullAction(DbClient dbClient, UserSession userSession, PullActionResponseWriter pullActionResponseWriter) {
+ this.dbClient = dbClient;
+ this.userSession = userSession;
+ this.pullActionResponseWriter = pullActionResponseWriter;
+ }
+
+ @Override
+ public void define(WebService.NewController controller) {
+ WebService.NewAction action = controller
+ .createAction(ACTION_PULL)
+ .setHandler(this)
+ .setInternal(true)
+ .setDescription("This endpoint fetches and returns all (unless filtered by optional params) the issues for a given branch." +
+ "The issues returned are not paginated, so the response size can be big.")
+ .setSince("9.5");
+
+ action.createParam(PROJECT_KEY_PARAM)
+ .setRequired(true)
+ .setDescription("Project key for which issues are fetched.")
+ .setExampleValue("sonarqube");
+
+ action.createParam(BRANCH_NAME_PARAM)
+ .setRequired(true)
+ .setDescription("Branch name for which issues are fetched.")
+ .setExampleValue("develop");
+
+ action.createParam(LANGUAGES_PARAM)
+ .setDescription("Comma seperated list of languages. If not present all issues regardless of their language are returned.")
+ .setExampleValue("java,cobol");
+
+ action.createParam(RULE_REPOSITORIES_PARAM)
+ .setDescription("Comma seperated list of rule repositories. If not present all issues regardless of" +
+ " their rule repository are returned.")
+ .setExampleValue("java");
+
+ action.createParam(RESOLVED_ONLY_PARAM)
+ .setDescription("If true only issues with resolved status are returned")
+ .setExampleValue("true");
+
+ action.createParam(CHANGED_SINCE_PARAM)
+ .setDescription("Timestamp. If present only issues modified after given timestamp are returned (both open and closed). " +
+ "If not present all non-closed issues are returned.")
+ .setExampleValue(1_654_032_306_000L);
+ }
+
+ @Override
+ public void handle(Request request, Response response) throws Exception {
+ String projectKey = request.mandatoryParam(PROJECT_KEY_PARAM);
+ String branchName = request.mandatoryParam(BRANCH_NAME_PARAM);
+ List<String> languages = request.paramAsStrings(LANGUAGES_PARAM);
+ List<String> ruleRepositories = request.paramAsStrings(RULE_REPOSITORIES_PARAM);
+ boolean resolvedOnly = Boolean.parseBoolean(request.param(RESOLVED_ONLY_PARAM));
+ String changedSince = request.param(CHANGED_SINCE_PARAM);
+ Long changedSinceTimestamp = changedSince != null ? Long.parseLong(changedSince) : null;
+
+ streamResponse(projectKey, branchName, languages, ruleRepositories, resolvedOnly, changedSinceTimestamp, response.stream().output());
+ }
+
+ private void streamResponse(String projectKey, String branchName, @Nullable List<String> languages,
+ @Nullable List<String> ruleRepositories, boolean resolvedOnly, @Nullable Long changedSince, OutputStream outputStream)
+ throws IOException {
+
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ Optional<ProjectDto> projectDto = dbClient.projectDao().selectProjectByKey(dbSession, projectKey);
+ validateProjectPermissions(projectDto);
+ pullActionResponseWriter.appendTimestampToResponse(outputStream);
+ var pullActionQueryParams = new IssueQueryParams(projectDto.get().getUuid(), branchName,
+ languages, ruleRepositories, resolvedOnly, changedSince);
+ retrieveAndSendIssues(dbSession, pullActionQueryParams, outputStream);
+ }
+ }
+
+ private void validateProjectPermissions(Optional<ProjectDto> projectDto) {
+ if (projectDto.isEmpty()) {
+ throw new IllegalArgumentException("Invalid " + PROJECT_KEY_PARAM + " parameter");
+ }
+ userSession.checkProjectPermission(USER, projectDto.get());
+ }
+
+ private void retrieveAndSendIssues(DbSession dbSession, IssueQueryParams queryParams, OutputStream outputStream)
+ throws IOException {
+
+ var issuesRetriever = new PullActionIssuesRetriever(dbClient, queryParams);
+
+ Consumer<List<IssueDto>> listConsumer = issueDtos -> pullActionResponseWriter.appendIssuesToResponse(issueDtos, outputStream);
+ issuesRetriever.processIssuesByBatch(dbSession, listConsumer);
+
+ if (queryParams.getChangedSince() != null) {
+ // in the "incremental mode" we need to send SonarLint also recently closed issues keys
+ List<String> closedIssues = issuesRetriever.retrieveClosedIssues(dbSession);
+ pullActionResponseWriter.appendClosedIssuesUuidsToResponse(closedIssues, outputStream);
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.ws.pull;
+
+import java.util.List;
+import java.util.function.Consumer;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.issue.IssueQueryParams;
+
+import static org.sonar.db.issue.IssueDao.DEFAULT_PAGE_SIZE;
+
+public class PullActionIssuesRetriever {
+
+ private final DbClient dbClient;
+ private final IssueQueryParams issueQueryParams;
+
+ public PullActionIssuesRetriever(DbClient dbClient, IssueQueryParams queryParams) {
+ this.dbClient = dbClient;
+ this.issueQueryParams = queryParams;
+ }
+
+ public void processIssuesByBatch(DbSession dbSession, Consumer<List<IssueDto>> listConsumer) {
+ int nextPage = 1;
+ boolean hasMoreIssues = true;
+
+ while (hasMoreIssues) {
+ List<IssueDto> issueDtos = nextOpenIssues(dbSession, nextPage);
+ listConsumer.accept(issueDtos);
+ nextPage++;
+ if (issueDtos.isEmpty() || issueDtos.size() < DEFAULT_PAGE_SIZE) {
+ hasMoreIssues = false;
+ }
+ }
+ }
+
+ public List<String> retrieveClosedIssues(DbSession dbSession) {
+ return dbClient.issueDao().selectRecentlyClosedIssues(dbSession, issueQueryParams);
+ }
+
+ private List<IssueDto> nextOpenIssues(DbSession dbSession, int nextPage) {
+ return dbClient.issueDao().selectByBranch(dbSession, issueQueryParams, nextPage);
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.ws.pull;
+
+import org.sonar.api.server.ServerSide;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.protobuf.DbIssues;
+import org.sonarqube.ws.Common;
+import org.sonarqube.ws.Issues;
+
+@ServerSide
+public class PullActionProtobufObjectGenerator {
+
+ Issues.IssuesPullQueryTimestamp generateTimestampMessage(long timestamp) {
+ Issues.IssuesPullQueryTimestamp.Builder responseBuilder = Issues.IssuesPullQueryTimestamp.newBuilder();
+ responseBuilder.setQueryTimestamp(timestamp);
+ return responseBuilder.build();
+ }
+
+ Issues.IssueLite generateIssueMessage(IssueDto issueDto) {
+ Issues.IssueLite.Builder issueBuilder = Issues.IssueLite.newBuilder();
+ DbIssues.Locations mainLocation = issueDto.parseLocations();
+
+ Issues.Location.Builder locationBuilder = Issues.Location.newBuilder();
+ if (issueDto.getMessage() != null) {
+ locationBuilder.setMessage(issueDto.getMessage());
+ }
+ if (issueDto.getFilePath() != null) {
+ locationBuilder.setFilePath(issueDto.getFilePath());
+ }
+ if (mainLocation != null) {
+ Issues.TextRange textRange = buildTextRange(mainLocation);
+ locationBuilder.setTextRange(textRange);
+ }
+ Issues.Location location = locationBuilder.build();
+
+ issueBuilder.setKey(issueDto.getKey());
+ issueBuilder.setCreationDate(issueDto.getCreatedAt());
+ issueBuilder.setResolved(issueDto.getStatus().equals(org.sonar.api.issue.Issue.STATUS_RESOLVED));
+ issueBuilder.setRuleKey(issueDto.getRuleKey().rule());
+ if (issueDto.isManualSeverity() && issueDto.getSeverity() != null) {
+ issueBuilder.setUserSeverity(issueDto.getSeverity());
+ }
+ issueBuilder.setType(Common.RuleType.forNumber(issueDto.getType()).name());
+ issueBuilder.setClosed(false);
+ issueBuilder.setMainLocation(location);
+
+ return issueBuilder.build();
+ }
+
+ Issues.IssueLite generateClosedIssueMessage(String uuid) {
+ Issues.IssueLite.Builder issueBuilder = Issues.IssueLite.newBuilder();
+ issueBuilder.setKey(uuid);
+ issueBuilder.setClosed(true);
+ return issueBuilder.build();
+ }
+
+ private static Issues.TextRange buildTextRange(DbIssues.Locations mainLocation) {
+ int startLine = mainLocation.getTextRange().getStartLine();
+ int endLine = mainLocation.getTextRange().getEndLine();
+ int startOffset = mainLocation.getTextRange().getStartOffset();
+ int endOffset = mainLocation.getTextRange().getEndOffset();
+
+ return Issues.TextRange.newBuilder()
+ .setHash(mainLocation.getChecksum())
+ .setStartLine(startLine)
+ .setEndLine(endLine)
+ .setStartLineOffset(startOffset)
+ .setEndLineOffset(endOffset).build();
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.ws.pull;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+import org.sonar.api.server.ServerSide;
+import org.sonar.api.utils.System2;
+import org.sonar.db.issue.IssueDto;
+import org.sonarqube.ws.Issues;
+
+@ServerSide
+public class PullActionResponseWriter {
+
+ private final System2 system2;
+ private final PullActionProtobufObjectGenerator pullActionProtobufObjectGenerator;
+
+ public PullActionResponseWriter(System2 system2, PullActionProtobufObjectGenerator pullActionProtobufObjectGenerator) {
+ this.system2 = system2;
+ this.pullActionProtobufObjectGenerator = pullActionProtobufObjectGenerator;
+ }
+
+ public void appendTimestampToResponse(OutputStream outputStream) throws IOException {
+ Issues.IssuesPullQueryTimestamp issuesPullQueryTimestamp = pullActionProtobufObjectGenerator.generateTimestampMessage(system2.now());
+ issuesPullQueryTimestamp.writeDelimitedTo(outputStream);
+ }
+
+ public void appendIssuesToResponse(List<IssueDto> issueDtos, OutputStream outputStream) {
+ try {
+ for (IssueDto issueDto : issueDtos) {
+ Issues.IssueLite issueLite = pullActionProtobufObjectGenerator.generateIssueMessage(issueDto);
+ issueLite.writeDelimitedTo(outputStream);
+ }
+ outputStream.flush();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void appendClosedIssuesUuidsToResponse(List<String> closedIssuesUuids,
+ OutputStream outputStream) throws IOException {
+ for (String uuid : closedIssuesUuids) {
+ Issues.IssueLite issueLite = pullActionProtobufObjectGenerator.generateClosedIssueMessage(uuid);
+ issueLite.writeDelimitedTo(outputStream);
+ }
+ }
+
+}
public void verify_count_of_added_components() {
ListContainer container = new ListContainer();
new IssueWsModule().configure(container);
- assertThat(container.getAddedObjects()).hasSize(31);
+ assertThat(container.getAddedObjects()).isNotEmpty();
}
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.ws;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.issue.IssueDbTester;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.protobuf.DbCommons;
+import org.sonar.db.protobuf.DbIssues;
+import org.sonar.db.rule.RuleDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.issue.ws.pull.PullActionProtobufObjectGenerator;
+import org.sonar.server.issue.ws.pull.PullActionResponseWriter;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.TestResponse;
+import org.sonar.server.ws.WsActionTester;
+import org.sonarqube.ws.Common;
+import org.sonarqube.ws.Issues;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.web.UserRole.USER;
+import static org.sonar.db.component.ComponentTesting.newFileDto;
+
+public class PullActionTest {
+
+ private static final long NOW = 10_000_000_000L;
+ private static final long PAST = 1_000_000_000L;
+
+ private static final String DEFAULT_BRANCH = "master";
+
+ @Rule
+ public DbTester dbTester = DbTester.create();
+
+ @Rule
+ public UserSessionRule userSession = UserSessionRule.standalone();
+
+ @Rule
+ public DbTester db = DbTester.create(System2.INSTANCE);
+
+ private final System2 system2 = mock(System2.class);
+ private final PullActionProtobufObjectGenerator pullActionProtobufObjectGenerator = new PullActionProtobufObjectGenerator();
+
+ private final PullActionResponseWriter pullActionResponseWriter = new PullActionResponseWriter(system2, pullActionProtobufObjectGenerator);
+
+ private final IssueDbTester issueDbTester = new IssueDbTester(db);
+ private final ComponentDbTester componentDbTester = new ComponentDbTester(db);
+
+ private final PullAction underTest = new PullAction(db.getDbClient(), userSession, pullActionResponseWriter);
+ private final WsActionTester tester = new WsActionTester(underTest);
+
+ private RuleDto correctRule, incorrectRule;
+ private ComponentDto correctProject, incorrectProject;
+ private ComponentDto correctFile, incorrectFile;
+
+ @Before
+ public void setUp() {
+ when(system2.now()).thenReturn(NOW);
+ correctRule = db.rules().insertIssueRule();
+ correctProject = db.components().insertPrivateProject();
+ correctFile = db.components().insertComponent(newFileDto(correctProject));
+
+ incorrectRule = db.rules().insertIssueRule();
+ incorrectProject = db.components().insertPrivateProject();
+ incorrectFile = db.components().insertComponent(newFileDto(incorrectProject));
+ }
+
+ @Test
+ public void givenMissingParams_expectIllegalArgumentException() {
+ TestRequest request = tester.newRequest();
+
+ assertThatThrownBy(() -> request.executeProtobuf(Issues.IssuesPullQueryTimestamp.class))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ public void givenNotExistingProjectKey_throwException() {
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", "projectKey")
+ .setParam("branchName", DEFAULT_BRANCH);
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Invalid projectKey parameter");
+ }
+
+ @Test
+ public void givenValidProjectKeyWithoutPermissionsTo_throwException() {
+ userSession.logIn();
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", correctProject.getKey())
+ .setParam("branchName", DEFAULT_BRANCH);
+
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(ForbiddenException.class)
+ .hasMessage("Insufficient privileges");
+ }
+
+ @Test
+ public void givenValidProjectKeyAndOneIssueOnBranch_returnOneIssue() throws IOException {
+ DbCommons.TextRange textRange = DbCommons.TextRange.newBuilder()
+ .setStartLine(1)
+ .setEndLine(2)
+ .setStartOffset(3)
+ .setEndOffset(4)
+ .build();
+ DbIssues.Locations.Builder mainLocation = DbIssues.Locations.newBuilder()
+ .setChecksum("hash")
+ .setTextRange(textRange);
+
+ IssueDto issueDto = issueDbTester.insertIssue(p -> p.setSeverity("MINOR")
+ .setManualSeverity(true)
+ .setMessage("message")
+ .setCreatedAt(NOW)
+ .setStatus(Issue.STATUS_RESOLVED)
+ .setLocations(mainLocation.build())
+ .setType(Common.RuleType.BUG.getNumber()));
+ loginWithBrowsePermission(issueDto);
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", issueDto.getProjectKey())
+ .setParam("branchName", DEFAULT_BRANCH);
+
+ TestResponse response = request.execute();
+ List<Issues.IssueLite> issues = readAllIssues(response);
+ Issues.IssueLite issueLite = issues.get(0);
+
+ assertThat(issues).hasSize(1);
+
+ assertThat(issueLite.getKey()).isEqualTo(issueDto.getKey());
+ assertThat(issueLite.getUserSeverity()).isEqualTo("MINOR");
+ assertThat(issueLite.getCreationDate()).isEqualTo(NOW);
+ assertThat(issueLite.getResolved()).isTrue();
+ assertThat(issueLite.getRuleKey()).isEqualTo(issueDto.getRuleKey().rule());
+ assertThat(issueLite.getType()).isEqualTo(Common.RuleType.forNumber(issueDto.getType()).name());
+
+ Issues.Location location = issueLite.getMainLocation();
+ assertThat(location.getMessage()).isEqualTo(issueDto.getMessage());
+
+ Issues.TextRange locationTextRange = location.getTextRange();
+ assertThat(locationTextRange.getStartLine()).isEqualTo(1);
+ assertThat(locationTextRange.getEndLine()).isEqualTo(2);
+ assertThat(locationTextRange.getStartLineOffset()).isEqualTo(3);
+ assertThat(locationTextRange.getEndLineOffset()).isEqualTo(4);
+ assertThat(locationTextRange.getHash()).isEqualTo("hash");
+ }
+
+ @Test
+ public void givenIssueOnAnotherBranch_returnOneIssue() throws IOException {
+ ComponentDto developBranch = componentDbTester.insertPrivateProjectWithCustomBranch("develop");
+ ComponentDto developFile = db.components().insertComponent(newFileDto(developBranch));
+ generateIssues(correctRule, developBranch, developFile, 1);
+ loginWithBrowsePermission(developBranch.uuid(), developFile.uuid());
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", developBranch.getKey())
+ .setParam("branchName", "develop");
+
+ TestResponse response = request.execute();
+ List<Issues.IssueLite> issues = readAllIssues(response);
+
+ assertThat(issues).hasSize(1);
+ }
+
+ @Test
+ public void inIncrementalModeReturnClosedIssues() throws IOException {
+ IssueDto openIssue = issueDbTester.insertIssue(p -> p.setSeverity("MINOR")
+ .setManualSeverity(true)
+ .setMessage("openIssue")
+ .setCreatedAt(NOW)
+ .setStatus(Issue.STATUS_OPEN)
+ .setType(Common.RuleType.BUG.getNumber()));
+
+ issueDbTester.insertIssue(p -> p.setSeverity("MINOR")
+ .setMessage("closedIssue")
+ .setCreatedAt(NOW)
+ .setStatus(Issue.STATUS_CLOSED)
+ .setType(Common.RuleType.BUG.getNumber())
+ .setComponentUuid(openIssue.getComponentUuid())
+ .setProjectUuid(openIssue.getProjectUuid())
+ .setIssueUpdateTime(PAST)
+ .setIssueCreationTime(PAST));
+
+ loginWithBrowsePermission(openIssue);
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", openIssue.getProjectKey())
+ .setParam("branchName", DEFAULT_BRANCH)
+ .setParam("changedSince", PAST + "");
+
+ TestResponse response = request.execute();
+ List<Issues.IssueLite> issues = readAllIssues(response);
+
+ assertThat(issues).hasSize(2);
+ }
+
+ @Test
+ public void given15IssuesInTheTable_returnOnly10ThatBelongToProject() throws IOException {
+ loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid());
+ generateIssues(correctRule, correctProject, correctFile, 10);
+ generateIssues(incorrectRule, incorrectProject, incorrectFile, 5);
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", correctProject.getKey())
+ .setParam("branchName", DEFAULT_BRANCH);
+
+ TestResponse response = request.execute();
+ List<Issues.IssueLite> issues = readAllIssues(response);
+
+ assertThat(issues).hasSize(10);
+ }
+
+ @Test
+ public void givenNoIssuesBelongToTheProject_return0Issues() throws IOException {
+ loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid());
+ generateIssues(incorrectRule, incorrectProject, incorrectFile, 5);
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", correctProject.getKey())
+ .setParam("branchName", DEFAULT_BRANCH);
+
+ TestResponse response = request.execute();
+ List<Issues.IssueLite> issues = readAllIssues(response);
+
+ assertThat(issues).isEmpty();
+ }
+
+ @Test
+ public void testLanguagesParam_return1Issue() throws IOException {
+ loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid());
+ RuleDto javaRule = db.rules().insert(r -> r.setLanguage("java"));
+
+ IssueDto javaIssue = issueDbTester.insertIssue(p -> p.setSeverity("MINOR")
+ .setManualSeverity(true)
+ .setMessage("openIssue")
+ .setCreatedAt(NOW)
+ .setRule(javaRule)
+ .setRuleUuid(javaRule.getUuid())
+ .setStatus(Issue.STATUS_OPEN)
+ .setLanguage("java")
+ .setProject(correctProject)
+ .setComponent(correctFile)
+ .setType(Common.RuleType.BUG.getNumber()));
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", correctProject.getKey())
+ .setParam("branchName", DEFAULT_BRANCH)
+ .setParam("languages", "java");
+
+ TestResponse response = request.execute();
+ List<Issues.IssueLite> issues = readAllIssues(response);
+
+ assertThat(issues).hasSize(1);
+ assertThat(issues.get(0).getKey()).isEqualTo(javaIssue.getKey());
+ }
+
+ @Test
+ public void testLanguagesParam_givenWrongLanguage_return0Issues() throws IOException {
+ loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid());
+ RuleDto javascriptRule = db.rules().insert(r -> r.setLanguage("javascript"));
+
+ issueDbTester.insertIssue(p -> p.setSeverity("MINOR")
+ .setManualSeverity(true)
+ .setMessage("openIssue")
+ .setCreatedAt(NOW)
+ .setRule(javascriptRule)
+ .setRuleUuid(javascriptRule.getUuid())
+ .setStatus(Issue.STATUS_OPEN)
+ .setProject(correctProject)
+ .setComponent(correctFile)
+ .setType(2));
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", correctProject.getKey())
+ .setParam("branchName", DEFAULT_BRANCH)
+ .setParam("languages", "java");
+
+ TestResponse response = request.execute();
+ List<Issues.IssueLite> issues = readAllIssues(response);
+
+ assertThat(issues).isEmpty();
+ }
+
+ @Test
+ public void testRuleRepositoriesParam_return1IssueForGivenRepository() throws IOException {
+ loginWithBrowsePermission(correctProject.uuid(), correctFile.uuid());
+ RuleDto javaRule = db.rules().insert(r -> r.setRepositoryKey("java"));
+ RuleDto javaScriptRule = db.rules().insert(r -> r.setRepositoryKey("javascript"));
+
+ IssueDto issueDto = issueDbTester.insertIssue(p -> p.setSeverity("MINOR")
+ .setManualSeverity(true)
+ .setMessage("openIssue")
+ .setCreatedAt(NOW)
+ .setRule(javaRule)
+ .setStatus(Issue.STATUS_OPEN)
+ .setProject(correctProject)
+ .setComponent(correctFile)
+ .setType(2));
+
+ //this one should not be returned - it is a different rule repository
+ issueDbTester.insertIssue(p -> p.setSeverity("MINOR")
+ .setManualSeverity(true)
+ .setMessage("openIssue")
+ .setCreatedAt(NOW)
+ .setRule(javaScriptRule)
+ .setStatus(Issue.STATUS_OPEN)
+ .setProject(correctProject)
+ .setComponent(correctFile)
+ .setType(Common.RuleType.BUG.getNumber()));
+
+ TestRequest request = tester.newRequest()
+ .setParam("projectKey", correctProject.getKey())
+ .setParam("branchName", DEFAULT_BRANCH)
+ .setParam("ruleRepositories", "java");
+
+ TestResponse response = request.execute();
+ List<Issues.IssueLite> issues = readAllIssues(response);
+
+ assertThat(issues).hasSize(1);
+ assertThat(issues.get(0).getKey()).isEqualTo(issueDto.getKey());
+ }
+
+ private void generateIssues(RuleDto rule, ComponentDto project, ComponentDto file, int numberOfIssues) {
+ for (int j = 0; j < numberOfIssues; j++) {
+ issueDbTester.insert(i -> i.setProject(project)
+ .setComponentUuid(file.uuid())
+ .setRuleUuid(rule.getUuid())
+ .setStatus(Issue.STATUS_OPEN)
+ .setType(2));
+ }
+ }
+
+ private List<Issues.IssueLite> readAllIssues(TestResponse response) throws IOException {
+ List<Issues.IssueLite> issues = new ArrayList<>();
+ InputStream inputStream = response.getInputStream();
+ Issues.IssuesPullQueryTimestamp.parseDelimitedFrom(inputStream);
+
+ while (inputStream.available() > 0) {
+ issues.add(Issues.IssueLite.parseDelimitedFrom(inputStream));
+ }
+
+ return issues;
+ }
+
+ private void loginWithBrowsePermission(IssueDto issueDto) {
+ loginWithBrowsePermission(issueDto.getProjectUuid(), issueDto.getComponentUuid());
+ }
+
+ private void loginWithBrowsePermission(String projectUuid, String componentUuid) {
+ UserDto user = dbTester.users().insertUser("john");
+ userSession.logIn(user)
+ .addProjectPermission(USER,
+ db.getDbClient().componentDao().selectByUuid(dbTester.getSession(), projectUuid).get(),
+ db.getDbClient().componentDao().selectByUuid(dbTester.getSession(), componentUuid).get());
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.ws.pull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.db.DbClient;
+import org.sonar.db.issue.IssueDao;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.issue.IssueQueryParams;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class PullActionIssuesRetrieverTest {
+
+ private final DbClient dbClient = mock(DbClient.class);
+ private final String projectUuid = "default-project-uuid";
+ private final String branchName = "master";
+ private final List<String> languages = List.of("java");
+ private final List<String> ruleRepositories = List.of("js-security", "java");
+ private final Long defaultChangedSince = 1_000_000L;
+
+ private final IssueQueryParams queryParams = new IssueQueryParams(projectUuid, branchName, languages, ruleRepositories, false,
+ defaultChangedSince);
+ private final IssueDao issueDao = mock(IssueDao.class);
+
+ @Before
+ public void before() {
+ when(dbClient.issueDao()).thenReturn(issueDao);
+ }
+
+ @Test
+ public void processIssuesByBatch_givenNoIssuesReturnedByDatabase_noIssuesConsumed() {
+ var pullActionIssuesRetriever = new PullActionIssuesRetriever(dbClient, queryParams);
+ when(issueDao.selectByBranch(any(), any(), anyInt()))
+ .thenReturn(List.of());
+ List<IssueDto> returnedDtos = new ArrayList<>();
+ Consumer<List<IssueDto>> listConsumer = returnedDtos::addAll;
+
+ pullActionIssuesRetriever.processIssuesByBatch(dbClient.openSession(true), listConsumer);
+
+ assertThat(returnedDtos).isEmpty();
+ }
+
+ @Test
+ public void processIssuesByBatch_givenThousandOneIssuesReturnedByDatabase_thousandOneIssuesConsumed() {
+ var pullActionIssuesRetriever = new PullActionIssuesRetriever(dbClient, queryParams);
+ List<IssueDto> thousandIssues = IntStream.rangeClosed(1, 1000).mapToObj(i -> new IssueDto()).collect(Collectors.toList());
+ when(issueDao.selectByBranch(any(), any(), anyInt()))
+ .thenReturn(thousandIssues)
+ .thenReturn(List.of(new IssueDto()));
+ List<IssueDto> returnedDtos = new ArrayList<>();
+ Consumer<List<IssueDto>> listConsumer = returnedDtos::addAll;
+
+ pullActionIssuesRetriever.processIssuesByBatch(dbClient.openSession(true), listConsumer);
+
+ assertThat(returnedDtos).hasSize(1001);
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.server.issue.ws.pull;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.db.issue.IssueDto;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class PullActionResponseWriterTest {
+
+ private final System2 system2 = mock(System2.class);
+ private final PullActionProtobufObjectGenerator pullActionProtobufObjectGenerator = new PullActionProtobufObjectGenerator();
+
+ private final PullActionResponseWriter underTest = new PullActionResponseWriter(system2, pullActionProtobufObjectGenerator);
+
+ @Before
+ public void before() {
+ when(system2.now()).thenReturn(1_000_000L);
+ }
+
+ @Test
+ public void appendIssuesToResponse_outputStreamIsCalledAtLeastOnce() throws IOException {
+ OutputStream outputStream = mock(OutputStream.class);
+ IssueDto issueDto = new IssueDto();
+ issueDto.setFilePath("filePath");
+ issueDto.setKee("key");
+ issueDto.setStatus("OPEN");
+ issueDto.setRuleKey("repo", "rule");
+
+ underTest.appendIssuesToResponse(List.of(issueDto), outputStream);
+
+ verify(outputStream, atLeastOnce()).write(any(byte[].class), anyInt(), anyInt());
+ }
+
+ @Test
+ public void appendClosedIssuesToResponse_outputStreamIsCalledAtLeastOnce() throws IOException {
+ OutputStream outputStream = mock(OutputStream.class);
+
+ underTest.appendClosedIssuesUuidsToResponse(List.of("uuid", "uuid2"), outputStream);
+
+ verify(outputStream, atLeastOnce()).write(any(byte[].class), anyInt(), anyInt());
+ }
+
+ @Test
+ public void appendTimestampToResponse_outputStreamIsCalledAtLeastOnce() throws IOException {
+ OutputStream outputStream = mock(OutputStream.class);
+
+ underTest.appendTimestampToResponse(outputStream);
+
+ verify(outputStream, atLeastOnce()).write(any(byte[].class), anyInt(), anyInt());
+ }
+}
public static final String ACTION_SET_TAGS = "set_tags";
public static final String ACTION_SET_TYPE = "set_type";
public static final String ACTION_BULK_CHANGE = "bulk_change";
+ public static final String ACTION_PULL = "pull";
public static final String PARAM_ISSUE = "issue";
public static final String PARAM_COMMENT = "comment";
repeated string authors = 1;
}
+// Response of GET api/issues/pull
+message IssuesPullQueryTimestamp {
+ required int64 queryTimestamp = 1;
+}
+
+message TextRange {
+ optional int32 startLine = 1;
+ optional int32 startLineOffset = 2;
+ optional int32 endLine = 3;
+ optional int32 endLineOffset = 4;
+ optional string hash = 5;
+}
+
+message Location {
+ optional string filePath = 1;
+ optional string message = 2;
+ optional TextRange textRange = 3;
+}
+
+message IssueLite {
+ required string key = 1;
+ optional int64 creationDate = 2;
+ optional bool resolved = 3;
+ optional string ruleKey = 4;
+ optional string userSeverity = 5;
+ optional string type = 6;
+ optional Location mainLocation = 7;
+ optional bool closed = 8;
+}
+
+