]> source.dussan.org Git - sonarqube.git/commitdiff
MMF-1134 Make pull request a 1st class citizen
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Thu, 1 Feb 2018 14:30:48 +0000 (15:30 +0100)
committerTeryk Bellahsene <teryk@users.noreply.github.com>
Tue, 13 Mar 2018 13:05:36 +0000 (14:05 +0100)
SONAR-10366 Add pull request object to api/project_branches/list and api/ce/activity

SONAR-10365 Analyze pull requests as 1st class citizen

SONAR-10366 SONAR-10367 Create WS api/projet_pull_requests/list and delete

SONAR-10383 Add Pull Request information when listing CE tasks

SONAR-10365 Add key type in PROJECT_BRANCHES (#3063)

SONAR-10371 Add pullRequest parameter in the Web API (#3076)

* ComponentFinder searches by branch or pull request
* Add pullRequest parameter to WS api/issues/* WS
* Add pullRequest parameter to api/settings/* WS
* Add pullRequest parameter to api/badges/* WS
* Add pullRequest parameter to api/components/* WS
* Add pullRequest parameter to api/sources/* WS

SONAR-10368 Copy issue states from pull request after it's merged

SONAR-10373 Send notifications for events on issues of a pull request

SONAR-10365 Add pull_request_binary column in project_branches (#3073)

SONAR-10433 Store pull request in projects table

SONAR-10371 Add pullRequest field in the Web API

SONAR-10365 Analyze pull requests as 1st class citizen

BRANCH-45 Expose issue resolution for PR decoration

BRANCH-49 Basic support of pull request analysis on pull request branch

BRANCH-47 Fail when user tries to analyze a pull request and the plugin is not available

SONAR-10366 update pull request decorated links to project and issues

SONAR-10365 Use pull request id as key instead of branch name

SONAR-10454 Update embedded Git 1.4 and SVN 1.7

SONAR-10365 rename sonar.pullrequest.id to sonar.pullrequest.key

SONAR-10383 api/navigation/component returns the pull request key

273 files changed:
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
server/sonar-db-core/src/main/resources/org/sonar/db/version/schema-h2.ddl
server/sonar-db-dao/src/main/java/org/sonar/db/ce/CeTaskCharacteristicDto.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchDto.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchMapper.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/BranchType.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDto.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentKeyUpdaterDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentMapper.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/KeyType.java [new file with mode: 0644]
server/sonar-db-dao/src/main/protobuf/db-project-branches.proto [new file with mode: 0644]
server/sonar-db-dao/src/main/resources/org/sonar/db/component/BranchMapper.xml
server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml
server/sonar-db-dao/src/test/java/org/sonar/db/component/BranchDaoTest.java
server/sonar-db-dao/src/test/java/org/sonar/db/component/BranchDtoTest.java
server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDtoTest.java
server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentKeyUpdaterDaoTest.java
server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentTesting.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/AddKeyTypeInProjectBranches.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/AddPullRequestBinaryInProjectBranches.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/DbVersion71.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/IncreaseBranchTypeSizeForPullRequest.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/MakeKeyTypeNotNullableInProjectBranches.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/ReplaceIndexInProjectBranches.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/SetKeyTypeToBranchInProjectBranches.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/AddKeyTypeInProjectBranchesTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/AddPullRequestBinaryInProjectBranchesTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/DbVersion71Test.java
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/IncreaseBranchTypeSizeForPullRequestTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/MakeKeyTypeNotNullableInProjectBranchesTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/ReplaceIndexInProjectBranchesTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/SetKeyTypeToBranchInProjectBranchesTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/AddKeyTypeInProjectBranchesTest/project_branches.sql [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/AddPullRequestBinaryInProjectBranchesTest/project_branches.sql [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/IncreaseBranchTypeSizeForPullRequestTest/project_branches.sql [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/MakeKeyTypeNotNullableInProjectBranchesTest/project_branches.sql [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/ReplaceIndexInProjectBranchesTest/project_branches.sql [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/SetKeyTypeToBranchInProjectBranchesTest/project_branches.sql [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/ce/settings/ProjectConfigurationFactory.java
server/sonar-server/src/main/java/org/sonar/server/badge/ws/MeasureAction.java
server/sonar-server/src/main/java/org/sonar/server/badge/ws/QualityGateAction.java
server/sonar-server/src/main/java/org/sonar/server/batch/ProjectAction.java
server/sonar-server/src/main/java/org/sonar/server/batch/ProjectDataLoader.java
server/sonar-server/src/main/java/org/sonar/server/batch/ProjectDataQuery.java
server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/DeleteAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/ListAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/PullRequestWsAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/PullRequestWsModule.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/PullRequestsWs.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/PullRequestsWsParameters.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/package-info.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/branch/ws/DeleteAction.java
server/sonar-server/src/main/java/org/sonar/server/branch/ws/ListAction.java
server/sonar-server/src/main/java/org/sonar/server/branch/ws/RenameAction.java
server/sonar-server/src/main/java/org/sonar/server/ce/ws/ActivityAction.java
server/sonar-server/src/main/java/org/sonar/server/ce/ws/TaskFormatter.java
server/sonar-server/src/main/java/org/sonar/server/component/ComponentFinder.java
server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java
server/sonar-server/src/main/java/org/sonar/server/component/ws/ComponentDtoToWsComponent.java
server/sonar-server/src/main/java/org/sonar/server/component/ws/MeasuresWsParameters.java
server/sonar-server/src/main/java/org/sonar/server/component/ws/ShowAction.java
server/sonar-server/src/main/java/org/sonar/server/component/ws/TreeAction.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/analysis/AnalysisMetadataHolder.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/analysis/AnalysisMetadataHolderImpl.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/analysis/Branch.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/analysis/MutableAnalysisMetadataHolder.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutor.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/component/BranchPersisterImpl.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/component/DefaultBranchImpl.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/component/MergeBranchComponentUuids.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/component/ShortBranchComponentsWithIssues.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/IssueTrackingDelegator.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/ShortBranchIssuesLoader.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/LoadQualityGateStep.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/QualityGateEventsStep.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/SendIssueNotificationsStep.java
server/sonar-server/src/main/java/org/sonar/server/duplication/ws/DuplicationsParser.java
server/sonar-server/src/main/java/org/sonar/server/duplication/ws/ShowAction.java
server/sonar-server/src/main/java/org/sonar/server/duplication/ws/ShowResponseBuilder.java
server/sonar-server/src/main/java/org/sonar/server/issue/IssueQueryFactory.java
server/sonar-server/src/main/java/org/sonar/server/issue/SearchRequest.java
server/sonar-server/src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java
server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueChangeNotification.java
server/sonar-server/src/main/java/org/sonar/server/issue/notification/IssueChangesEmailTemplate.java
server/sonar-server/src/main/java/org/sonar/server/issue/notification/MyNewIssuesEmailTemplate.java
server/sonar-server/src/main/java/org/sonar/server/issue/notification/NewIssuesNotification.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java
server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputerImpl.java
server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentAction.java
server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentDtoToWsComponent.java
server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java
server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentTreeRequest.java
server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchHistoryAction.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/ProjectAnalysesWsParameters.java
server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/SearchAction.java
server/sonar-server/src/main/java/org/sonar/server/projectanalysis/ws/SearchRequest.java
server/sonar-server/src/main/java/org/sonar/server/qualitygate/ShortLivingBranchQualityGate.java
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListener.java
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java
server/sonar-server/src/main/java/org/sonar/server/setting/ws/ListDefinitionsAction.java
server/sonar-server/src/main/java/org/sonar/server/setting/ws/ResetAction.java
server/sonar-server/src/main/java/org/sonar/server/setting/ws/SetAction.java
server/sonar-server/src/main/java/org/sonar/server/setting/ws/SettingsWsParameters.java
server/sonar-server/src/main/java/org/sonar/server/setting/ws/SettingsWsSupport.java
server/sonar-server/src/main/java/org/sonar/server/setting/ws/ValuesAction.java
server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesAction.java
server/sonar-server/src/main/java/org/sonar/server/source/ws/RawAction.java
server/sonar-server/src/main/java/org/sonar/server/test/ws/ListAction.java
server/sonar-server/src/main/java/org/sonar/server/ui/ws/ComponentAction.java
server/sonar-server/src/main/java/org/sonar/server/user/ws/CreateAction.java
server/sonar-server/src/main/java/org/sonar/server/user/ws/SetHomepageAction.java
server/sonar-server/src/main/java/org/sonar/server/webhook/Branch.java
server/sonar-server/src/main/java/org/sonar/server/webhook/WebhookPayloadFactoryImpl.java
server/sonar-server/src/main/java/org/sonar/server/ws/KeyExamples.java
server/sonar-server/src/main/resources/org/sonar/server/branch/pr/ws/list-example.json [new file with mode: 0644]
server/sonar-server/src/main/resources/org/sonar/server/user/ws/create-example.json [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/badge/ws/MeasureActionTest.java
server/sonar-server/src/test/java/org/sonar/server/badge/ws/QualityGateActionTest.java
server/sonar-server/src/test/java/org/sonar/server/branch/pr/ws/DeleteActionTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/branch/pr/ws/ListActionTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/branch/pr/ws/PullRequestWsModuleTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/branch/pr/ws/PullRequestsWsParametersTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/branch/pr/ws/PullRequestsWsTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/branch/ws/ListActionTest.java
server/sonar-server/src/test/java/org/sonar/server/ce/ws/ActivityActionTest.java
server/sonar-server/src/test/java/org/sonar/server/component/ComponentFinderTest.java
server/sonar-server/src/test/java/org/sonar/server/component/ws/AppActionTest.java
server/sonar-server/src/test/java/org/sonar/server/component/ws/ShowActionTest.java
server/sonar-server/src/test/java/org/sonar/server/component/ws/TreeActionTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/analysis/AnalysisMetadataHolderImplTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/analysis/AnalysisMetadataHolderRule.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/analysis/MutableAnalysisMetadataHolderRule.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutorTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/component/BranchPersisterImplTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/component/ShortBranchComponentsWithIssuesTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IssueTrackingDelegatorTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/LoadQualityGateStepTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/QualityGateEventsStepTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/ReportPersistComponentsStepTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/SendIssueNotificationsStepTest.java
server/sonar-server/src/test/java/org/sonar/server/duplication/ws/DuplicationsParserTest.java
server/sonar-server/src/test/java/org/sonar/server/duplication/ws/ShowActionTest.java
server/sonar-server/src/test/java/org/sonar/server/duplication/ws/ShowResponseBuilderTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/notification/IssueChangeNotificationTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/notification/IssueChangesEmailTemplateTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/notification/NewIssuesNotificationTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java
server/sonar-server/src/test/java/org/sonar/server/measure/live/LiveMeasureComputerImplTest.java
server/sonar-server/src/test/java/org/sonar/server/measure/live/LiveQualityGateComputerImplTest.java
server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentActionTest.java
server/sonar-server/src/test/java/org/sonar/server/measure/ws/ComponentTreeActionTest.java
server/sonar-server/src/test/java/org/sonar/server/measure/ws/SearchHistoryActionTest.java
server/sonar-server/src/test/java/org/sonar/server/projectanalysis/ws/SearchActionTest.java
server/sonar-server/src/test/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImplTest.java
server/sonar-server/src/test/java/org/sonar/server/setting/ws/ListDefinitionsActionTest.java
server/sonar-server/src/test/java/org/sonar/server/setting/ws/ResetActionTest.java
server/sonar-server/src/test/java/org/sonar/server/setting/ws/SetActionTest.java
server/sonar-server/src/test/java/org/sonar/server/setting/ws/ValuesActionTest.java
server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java
server/sonar-server/src/test/java/org/sonar/server/source/ws/SourcesWsTest.java
server/sonar-server/src/test/java/org/sonar/server/test/ws/ListActionTest.java
server/sonar-server/src/test/java/org/sonar/server/webhook/WebhookPayloadFactoryImplTest.java
sonar-core/src/main/java/org/sonar/core/config/ScannerProperties.java
sonar-core/src/test/java/org/sonar/core/config/CorePropertyDefinitionsTest.java
sonar-plugin-api/src/main/java/org/sonar/api/ce/posttask/Branch.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/cpd/CpdExecutor.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ComponentsPublisher.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/report/MetadataPublisher.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/report/ReportPublisher.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/report/TestExecutionAndCoveragePublisher.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectReactorValidator.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/ProjectScanContainer.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/branch/BranchConfiguration.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/branch/BranchConfigurationLoader.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/branch/BranchConfigurationProvider.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/branch/BranchType.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/branch/DefaultBranchConfiguration.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/branch/ProjectBranchesProvider.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/branch/ProjectPullRequests.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/branch/ProjectPullRequestsLoader.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/branch/ProjectPullRequestsProvider.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/branch/PullRequestInfo.java [new file with mode: 0644]
sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/filesystem/InputComponentStore.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scm/ScmChangedFilesProvider.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/scm/ScmPublisher.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorContext.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/DefaultSensorStorage.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/cpd/CpdExecutorTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/ScannerMediumTester.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ComponentsPublisherTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/report/ReportPublisherTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/report/TestExecutionAndCoveragePublisherTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/ProjectReactorValidatorTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/branch/BranchConfigurationProviderTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/branch/ProjectBranchesProviderTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/branch/ProjectPullRequestsProviderTest.java [new file with mode: 0644]
sonar-scanner-engine/src/test/java/org/sonar/scanner/scm/ScmChangedFilesProviderTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorContextTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/sensor/DefaultSensorStorageTest.java
sonar-scanner-protocol/src/main/protobuf/scanner_report.proto
sonar-ws/src/main/java/org/sonarqube/ws/client/DefaultWsClient.java
sonar-ws/src/main/java/org/sonarqube/ws/client/WsClient.java
sonar-ws/src/main/java/org/sonarqube/ws/client/component/ComponentsWsParameters.java
sonar-ws/src/main/java/org/sonarqube/ws/client/components/AppRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/components/ComponentsService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/components/ShowRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/components/TreeRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/duplications/DuplicationsService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/duplications/ShowRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java
sonar-ws/src/main/java/org/sonarqube/ws/client/issues/IssuesService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/issues/SearchRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/measures/ComponentRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/measures/ComponentTreeRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/measures/MeasuresService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/measures/SearchHistoryRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/navigation/ComponentRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/navigation/NavigationService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/permissions/TemplateGroupsRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/projectanalyses/ProjectAnalysesService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/projectanalyses/SearchRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/projectbranches/ProjectBranchesService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/projectpullrequests/DeleteRequest.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/projectpullrequests/ListRequest.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/projectpullrequests/ProjectPullRequestsService.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/projectpullrequests/package-info.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/qualitygates/CopyRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/qualitygates/CreateConditionRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/qualitygates/CreateRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/qualitygates/DeleteConditionRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/qualitygates/DeselectRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/qualitygates/DestroyRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/qualitygates/GetByProjectRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/qualitygates/ListRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/qualitygates/ProjectStatusRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/qualitygates/QualitygatesService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/qualitygates/RenameRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/qualitygates/SearchRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/qualitygates/SelectRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/qualitygates/SetAsDefaultRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/qualitygates/ShowRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/qualitygates/UpdateConditionRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/rules/SearchRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/settings/ListDefinitionsRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/settings/ResetRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/settings/SetRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/settings/SettingsService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/settings/ValuesRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/sources/LinesRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/sources/RawRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/sources/SourcesService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/tests/ListRequest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/tests/TestsService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/users/SetHomepageRequest.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/users/UsersService.java
sonar-ws/src/main/protobuf/ws-ce.proto
sonar-ws/src/main/protobuf/ws-commons.proto
sonar-ws/src/main/protobuf/ws-components.proto
sonar-ws/src/main/protobuf/ws-duplications.proto
sonar-ws/src/main/protobuf/ws-issues.proto
sonar-ws/src/main/protobuf/ws-measures.proto
sonar-ws/src/main/protobuf/ws-projectbranches.proto
sonar-ws/src/main/protobuf/ws-projectpullrequests.proto [new file with mode: 0644]
sonar-ws/src/main/protobuf/ws-tests.proto
tests/src/test/java/org/sonarqube/tests/issue/AutoAssignTest.java
tests/src/test/java/org/sonarqube/tests/issue/IssueCreationDatePluginChangedTest.java
tests/src/test/java/org/sonarqube/tests/user/LocalAuthenticationTest.java
travis.sh

index 64ed2a4a89b910977bc880a4fd4cc111b22f9401..f959d2a3fdc228ab3a675d81e06c31c10ecb6b5f 100644 (file)
@@ -120,7 +120,7 @@ public class ComputeEngineContainerImplTest {
         + 26 // level 1
         + 53 // content of DaoModule
         + 3 // content of EsModule
-        + 57 // content of CorePropertyDefinitions
+        + 59 // content of CorePropertyDefinitions
         + 1 // StopFlagContainer
     );
     assertThat(
index e036bd73dda8ed1350860b612e37ee7f2fc648f1..961ca45a1444c410e8bc5566064009e3acc52a1d 100644 (file)
@@ -733,13 +733,15 @@ CREATE TABLE "PROJECT_BRANCHES" (
   "UUID" VARCHAR(50) NOT NULL PRIMARY KEY,
   "PROJECT_UUID" VARCHAR(50) NOT NULL,
   "KEE" VARCHAR(255) NOT NULL,
-  "BRANCH_TYPE" VARCHAR(5),
+  "KEY_TYPE" VARCHAR(12) NOT NULL,
+  "BRANCH_TYPE" VARCHAR(12),
   "MERGE_BRANCH_UUID" VARCHAR(50),
+  "PULL_REQUEST_BINARY" BLOB,
   "CREATED_AT" BIGINT NOT NULL,
   "UPDATED_AT" BIGINT NOT NULL
 );
 CREATE UNIQUE INDEX "PK_PROJECT_BRANCHES" ON "PROJECT_BRANCHES" ("UUID");
-CREATE UNIQUE INDEX "PROJECT_BRANCHES_KEE" ON "PROJECT_BRANCHES" ("PROJECT_UUID", "KEE");
+CREATE UNIQUE INDEX "PROJECT_BRANCHES_KEE_KEY_TYPE" ON "PROJECT_BRANCHES" ("PROJECT_UUID", "KEE", "KEY_TYPE");
 
 CREATE TABLE "ANALYSIS_PROPERTIES" (
   "UUID" VARCHAR(40) NOT NULL PRIMARY KEY,
index 1e342a28a93543a71e3bbadcd3a8e1779e60a69d..1be8ca7416fbcc9a0f159d8906487290e82150b8 100644 (file)
@@ -23,6 +23,7 @@ public class CeTaskCharacteristicDto {
 
   public static final String BRANCH_KEY = "branch";
   public static final String BRANCH_TYPE_KEY = "branchType";
+  public static final String PULL_REQUEST = "pullRequest";
 
   private String uuid;
   private String taskUuid;
index fbbecbc5a7b16bb68402946e9f62bc2769927705..834e21bb0cdbacbf44837fa181831a210e1a8820 100644 (file)
@@ -37,24 +37,42 @@ public class BranchDao implements Dao {
   }
 
   public void insert(DbSession dbSession, BranchDto dto) {
+    setKeyType(dto);
     mapper(dbSession).insert(dto, system2.now());
   }
 
   public void upsert(DbSession dbSession, BranchDto dto) {
     BranchMapper mapper = mapper(dbSession);
     long now = system2.now();
+    setKeyType(dto);
     if (mapper.update(dto, now) == 0) {
       mapper.insert(dto, now);
     }
   }
 
+  private static void setKeyType(BranchDto dto) {
+    if (dto.getBranchType() == BranchType.PULL_REQUEST) {
+      dto.setKeyType(KeyType.PULL_REQUEST);
+    } else {
+      dto.setKeyType(KeyType.BRANCH);
+    }
+  }
+
   public int updateMainBranchName(DbSession dbSession, String projectUuid, String newBranchKey) {
     long now = system2.now();
     return mapper(dbSession).updateMainBranchName(projectUuid, newBranchKey, now);
   }
 
-  public Optional<BranchDto> selectByKey(DbSession dbSession, String projectUuid, String key) {
-    return Optional.ofNullable(mapper(dbSession).selectByKey(projectUuid, key));
+  public Optional<BranchDto> selectByBranchKey(DbSession dbSession, String projectUuid, String key) {
+    return selectByKey(dbSession, projectUuid, key, KeyType.BRANCH);
+  }
+
+  public Optional<BranchDto> selectByPullRequestKey(DbSession dbSession, String projectUuid, String key) {
+    return selectByKey(dbSession, projectUuid, key, KeyType.PULL_REQUEST);
+  }
+
+  private static Optional<BranchDto> selectByKey(DbSession dbSession, String projectUuid, String key, KeyType keyType) {
+    return Optional.ofNullable(mapper(dbSession).selectByKey(projectUuid, key, keyType));
   }
 
   public Collection<BranchDto> selectByComponent(DbSession dbSession, ComponentDto component) {
index 2586d87efc074c447f18a32f73ad0433086466cd..a1b11b8d2fb6a9fed786d7c8f169986494a9dc53 100644 (file)
  */
 package org.sonar.db.component;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.util.Objects;
+import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
+import org.sonar.db.protobuf.DbProjectBranches;
 
 import static com.google.common.base.Preconditions.checkArgument;
 
@@ -49,10 +54,18 @@ public class BranchDto {
   private String projectUuid;
 
   /**
-   * Name of branch, for example "feature/foo".
+   * Key that identifies a branch or a pull request.
+   * For keyType=BRANCH, this is the name of the branch, for example "feature/foo".
+   * For keyType=PULL_REQUEST, this is the ID of the pull request in some external system, for example 123 in GitHub.
    */
   private String kee;
 
+  /**
+   * Key type, as provided by {@link KeyType}.
+   * Not null.
+   */
+  private KeyType keyType;
+
   /**
    * Branch type, as provided by {@link BranchType}.
    * Not null.
@@ -69,6 +82,12 @@ public class BranchDto {
   @Nullable
   private String mergeBranchUuid;
 
+  /**
+   * Pull Request data, such as branch name, title, url, and provider specific attributes
+   */
+  @Nullable
+  private byte[] pullRequestBinary;
+
   public String getUuid() {
     return uuid;
   }
@@ -115,6 +134,11 @@ public class BranchDto {
     return this;
   }
 
+  BranchDto setKeyType(@Nullable KeyType keyType) {
+    this.keyType = keyType;
+    return this;
+  }
+
   public BranchType getBranchType() {
     return branchType;
   }
@@ -134,6 +158,37 @@ public class BranchDto {
     return this;
   }
 
+  public BranchDto setPullRequestData(DbProjectBranches.PullRequestData pullRequestData) {
+    this.pullRequestBinary = encodePullRequestData(pullRequestData);
+    return this;
+  }
+
+  @CheckForNull
+  public DbProjectBranches.PullRequestData getPullRequestData() {
+    if (pullRequestBinary == null) {
+      return null;
+    }
+    return decodePullRequestData(pullRequestBinary);
+  }
+
+  private static byte[] encodePullRequestData(DbProjectBranches.PullRequestData pullRequestData) {
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    try {
+      pullRequestData.writeTo(outputStream);
+      return outputStream.toByteArray();
+    } catch (IOException e) {
+      throw new IllegalStateException("Fail to serialize pull request data", e);
+    }
+  }
+
+  private static DbProjectBranches.PullRequestData decodePullRequestData(byte[] pullRequestBinary) {
+    try (ByteArrayInputStream inputStream = new ByteArrayInputStream(pullRequestBinary)) {
+      return DbProjectBranches.PullRequestData.parseFrom(inputStream);
+    } catch (IOException e) {
+      throw new IllegalStateException("Fail to deserialize pull request data", e);
+    }
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) {
@@ -161,6 +216,7 @@ public class BranchDto {
     sb.append("uuid='").append(uuid).append('\'');
     sb.append(", projectUuid='").append(projectUuid).append('\'');
     sb.append(", kee='").append(kee).append('\'');
+    sb.append(", keyType=").append(keyType);
     sb.append(", branchType=").append(branchType);
     sb.append(", mergeBranchUuid='").append(mergeBranchUuid).append('\'');
     sb.append('}');
index b613ab9f44b466cdf8be9c5fd4094c1e1ac06504..17872a13a0e8a029244f03869ebba943d5db0792 100644 (file)
@@ -31,7 +31,7 @@ public interface BranchMapper {
 
   int updateMainBranchName(@Param("projectUuid") String projectUuid, @Param("newBranchName") String newBranchName, @Param("now") long now);
 
-  BranchDto selectByKey(@Param("projectUuid") String projectUuid, @Param("key") String key);
+  BranchDto selectByKey(@Param("projectUuid") String projectUuid, @Param("key") String key, @Param("keyType") KeyType keyType);
 
   BranchDto selectByUuid(@Param("uuid") String uuid);
 
index 3358caf82cf311cf241455451c6dd26b6ce988b1..d5937b7d97d0fabf27501b71ba2bb3d9f7c0abed 100644 (file)
@@ -29,5 +29,10 @@ public enum BranchType {
   /**
    * Short-lived branch
    */
-  SHORT
+  SHORT,
+
+  /**
+   * Pull request
+   */
+  PULL_REQUEST
 }
index 24eb17266cef830587b57682760b7aabcbadb7cc..535d585ad73cdffc8d67c6b21c2c683bdac152f2 100644 (file)
@@ -50,6 +50,7 @@ import static org.sonar.db.DatabaseUtils.executeLargeInputs;
 import static org.sonar.db.DatabaseUtils.executeLargeUpdates;
 import static org.sonar.db.WildcardPosition.BEFORE_AND_AFTER;
 import static org.sonar.db.component.ComponentDto.generateBranchKey;
+import static org.sonar.db.component.ComponentDto.generatePullRequestKey;
 
 public class ComponentDao implements Dao {
 
@@ -201,6 +202,12 @@ public class ComponentDao implements Dao {
     return executeLargeInputs(allKeys, subKeys -> mapper(session).selectByKeysAndBranch(subKeys, branch));
   }
 
+  public List<ComponentDto> selectByKeysAndPullRequest(DbSession session, Collection<String> keys, String pullRequestId) {
+    List<String> dbKeys = keys.stream().map(k -> generatePullRequestKey(k, pullRequestId)).collect(toList());
+    List<String> allKeys = Stream.of(keys, dbKeys).flatMap(Collection::stream).collect(toList());
+    return executeLargeInputs(allKeys, subKeys -> mapper(session).selectByKeysAndBranch(subKeys, pullRequestId));
+  }
+
   public List<ComponentDto> selectComponentsHavingSameKeyOrderedById(DbSession session, String key) {
     return mapper(session).selectComponentsHavingSameKeyOrderedById(key);
   }
@@ -247,7 +254,11 @@ public class ComponentDao implements Dao {
   }
 
   public java.util.Optional<ComponentDto> selectByKeyAndBranch(DbSession session, String key, String branch) {
-    return java.util.Optional.ofNullable(mapper(session).selectByKeyAndBranch(key, generateBranchKey(key, branch), branch));
+    return java.util.Optional.ofNullable(mapper(session).selectByKeyAndBranchKey(key, generateBranchKey(key, branch), branch));
+  }
+
+  public java.util.Optional<ComponentDto> selectByKeyAndPullRequest(DbSession session, String key, String pullRequestId) {
+    return java.util.Optional.ofNullable(mapper(session).selectByKeyAndBranchKey(key, generatePullRequestKey(key, pullRequestId), pullRequestId));
   }
 
   public List<UuidWithProjectUuidDto> selectAllViewsAndSubViews(DbSession session) {
index 75d23fd49e550c8d622913920c7ce69929d0a5c6..ef402d4e48fdcd63da0f4e53154d5cea2803abc0 100644 (file)
@@ -24,15 +24,16 @@ import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import java.util.Date;
 import java.util.List;
+import java.util.regex.Pattern;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
-import org.apache.commons.lang.StringUtils;
 import org.apache.commons.lang.builder.ToStringBuilder;
 import org.sonar.api.resources.Scopes;
 import org.sonar.db.WildcardPosition;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static java.lang.String.format;
+import static org.apache.commons.lang.StringUtils.substringBeforeLast;
 import static org.sonar.db.DaoDatabaseUtils.buildLikeValue;
 import static org.sonar.db.component.ComponentValidator.checkComponentKey;
 import static org.sonar.db.component.ComponentValidator.checkComponentName;
@@ -44,8 +45,11 @@ public class ComponentDto {
    * Separator used to generate the key of the branch
    */
   public static final String BRANCH_KEY_SEPARATOR = ":BRANCH:";
+  public static final String PULL_REQUEST_SEPARATOR = ":PULL_REQUEST:";
 
+  private static final Splitter BRANCH_OR_PULL_REQUEST_SPLITTER = Splitter.on(Pattern.compile(BRANCH_KEY_SEPARATOR + "|" + PULL_REQUEST_SEPARATOR));
   private static final Splitter BRANCH_KEY_SPLITTER = Splitter.on(BRANCH_KEY_SEPARATOR);
+  private static final Splitter PULL_REQUEST_SPLITTER = Splitter.on(PULL_REQUEST_SEPARATOR);
 
   public static final String UUID_PATH_SEPARATOR = ".";
   public static final String UUID_PATH_OF_ROOT = UUID_PATH_SEPARATOR;
@@ -65,7 +69,7 @@ public class ComponentDto {
   private String organizationUuid;
 
   /**
-   * Non-empty and unique functional key
+   * Non-empty and unique functional key. Do not rename, used by MyBatis.
    */
   private String kee;
 
@@ -206,20 +210,6 @@ public class ComponentDto {
     return UUID_PATH_SPLITTER.splitToList(uuidPath);
   }
 
-  /**
-   * Used my MyBatis mapper
-   */
-  private String getKee(){
-    return kee;
-  }
-
-  /**
-   * Used my MyBatis mapper
-   */
-  private void setKee(String kee){
-    this.kee = kee;
-  }
-
   public String getDbKey() {
     return kee;
   }
@@ -233,7 +223,7 @@ public class ComponentDto {
    * The key to be displayed to user, doesn't contain information on branches
    */
   public String getKey() {
-    List<String> split = BRANCH_KEY_SPLITTER.splitToList(kee);
+    List<String> split = BRANCH_OR_PULL_REQUEST_SPLITTER.splitToList(kee);
     return split.size() == 2 ? split.get(0) : kee;
   }
 
@@ -246,6 +236,15 @@ public class ComponentDto {
     return split.size() == 2 ? split.get(1) : null;
   }
 
+  /**
+   * @return the pull request id. It will be null when the component is not on a pull request
+   */
+  @CheckForNull
+  public String getPullRequest() {
+    List<String> split = PULL_REQUEST_SPLITTER.splitToList(kee);
+    return split.size() == 2 ? split.get(1) : null;
+  }
+
   public String scope() {
     return scope;
   }
@@ -525,8 +524,11 @@ public class ComponentDto {
     return format("%s%s%s", componentKey, BRANCH_KEY_SEPARATOR, branch);
   }
 
-  public static String removeBranchFromKey(String componentKey) {
-    return StringUtils.substringBeforeLast(componentKey, ComponentDto.BRANCH_KEY_SEPARATOR);
+  public static String generatePullRequestKey(String componentKey, String pullRequest) {
+    return format("%s%s%s", componentKey, PULL_REQUEST_SEPARATOR, pullRequest);
   }
 
+  public static String removeBranchAndPullRequestFromKey(String componentKey) {
+    return substringBeforeLast(substringBeforeLast(componentKey, ComponentDto.BRANCH_KEY_SEPARATOR), ComponentDto.PULL_REQUEST_SEPARATOR);
+  }
 }
index 5290e3e2a4a36564e115b5bf0ff4b4bfa24e355d..c3a737369265b000620fa667ee63aab8508f223a 100644 (file)
@@ -141,10 +141,14 @@ public class ComponentKeyUpdaterDao implements Dao {
 
   private static String branchBaseKey(String key) {
     int index = key.lastIndexOf(ComponentDto.BRANCH_KEY_SEPARATOR);
-    if (index == -1) {
-      return key;
+    if (index > -1) {
+      return key.substring(0, index);
     }
-    return key.substring(0, index);
+    index = key.lastIndexOf(ComponentDto.PULL_REQUEST_SEPARATOR);
+    if (index > -1) {
+      return key.substring(0, index);
+    }
+    return key;
   }
 
   private static void runBatchUpdateForAllResources(Collection<ResourceDto> resources, String oldKey, String newKey, ComponentKeyUpdaterMapper mapper) {
index 5101dc9dd2eb114677e2c248bea68f3618c886b9..4560af88e6ca1abd616fd287975c78a15f2ff6e0 100644 (file)
@@ -33,7 +33,7 @@ public interface ComponentMapper {
   ComponentDto selectByKey(String key);
 
   @CheckForNull
-  ComponentDto selectByKeyAndBranch(@Param("key") String key, @Param("dbKey") String dbKey, @Param("branch") String branch);
+  ComponentDto selectByKeyAndBranchKey(@Param("key") String key, @Param("dbKey") String dbKey, @Param("branch") String branch);
 
   @CheckForNull
   ComponentDto selectById(long id);
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/component/KeyType.java b/server/sonar-db-dao/src/main/java/org/sonar/db/component/KeyType.java
new file mode 100644 (file)
index 0000000..f8b2767
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.component;
+
+public enum KeyType {
+  BRANCH,
+
+  PULL_REQUEST
+}
diff --git a/server/sonar-db-dao/src/main/protobuf/db-project-branches.proto b/server/sonar-db-dao/src/main/protobuf/db-project-branches.proto
new file mode 100644 (file)
index 0000000..ac6f504
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+    SonarQube, open source software quality management tool.
+    Copyright (C) 2008-2016 SonarSource
+    mailto:contact AT sonarsource DOT com
+
+    SonarQube 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.
+
+    SonarQube 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.
+*/
+
+// Structure of db column PROJECT_BRANCHES.PULL_REQUEST_DATA
+
+syntax = "proto3";
+
+package sonarqube.db.project_branches;
+
+// The java package can be changed without breaking compatibility.
+// it impacts only the generated Java code.
+option java_package = "org.sonar.db.protobuf";
+option optimize_for = SPEED;
+
+message PullRequestData {
+  string branch = 1;
+  string title = 2;
+  string url = 3;
+
+  map<string, string> attributes = 4;
+}
index f14a166566b7207b235a09f2f32ca087f2ff40a8..caf89177c7db996780391f4ac30aaf0a97a228ef 100644 (file)
@@ -6,8 +6,10 @@
     pb.uuid as uuid,
     pb.project_uuid as projectUuid,
     pb.kee as kee,
+    pb.key_type as keyType,
     pb.branch_type as branchType,
-    pb.merge_branch_uuid as mergeBranchUuid
+    pb.merge_branch_uuid as mergeBranchUuid,
+    pb.pull_request_binary as pullRequestBinary
   </sql>
 
   <insert id="insert" parameterType="map" useGeneratedKeys="false">
       uuid,
       project_uuid,
       kee,
+      key_type,
       branch_type,
       merge_branch_uuid,
+      pull_request_binary,
       created_at,
       updated_at
     ) values (
       #{dto.uuid, jdbcType=VARCHAR},
       #{dto.projectUuid, jdbcType=VARCHAR},
       #{dto.kee, jdbcType=VARCHAR},
+      #{dto.keyType, jdbcType=VARCHAR},
       #{dto.branchType, jdbcType=VARCHAR},
       #{dto.mergeBranchUuid, jdbcType=VARCHAR},
+      #{dto.pullRequestBinary, jdbcType=BINARY},
       #{now, jdbcType=BIGINT},
       #{now, jdbcType=BIGINT}
     )
@@ -43,6 +49,7 @@
     update project_branches
     set
     merge_branch_uuid = #{dto.mergeBranchUuid, jdbcType=VARCHAR},
+    pull_request_binary = #{dto.pullRequestBinary, jdbcType=BINARY},
     updated_at = #{now, jdbcType=BIGINT}
     where
     uuid = #{dto.uuid, jdbcType=VARCHAR}
@@ -53,7 +60,8 @@
     from project_branches pb
     where
     pb.project_uuid = #{projectUuid, jdbcType=VARCHAR} and
-    pb.kee = #{key, jdbcType=VARCHAR}
+    pb.kee = #{key, jdbcType=VARCHAR} and
+    pb.key_type = #{keyType, jdbcType=VARCHAR}
   </select>
 
   <select id="selectByProjectUuid" parameterType="string" resultType="org.sonar.db.component.BranchDto">
index e415632ecf7328c73175822d56204a290e20bb14..0ba7f68f4e88767f32b97319856027ad5bf68567 100644 (file)
@@ -36,7 +36,7 @@
       p.kee=#{key,jdbcType=VARCHAR}
   </select>
 
-  <select id="selectByKeyAndBranch" parameterType="String" resultType="Component">
+  <select id="selectByKeyAndBranchKey" parameterType="String" resultType="Component">
     SELECT
     <include refid="componentColumns"/>
     FROM projects p
       ON p.uuid = i.component_uuid
     JOIN project_branches b
       ON i.project_uuid = b.uuid
-      AND b.branch_type = 'SHORT'
+      AND (b.branch_type = 'SHORT' OR b.branch_type = 'PULL_REQUEST')
       AND b.merge_branch_uuid = #{mergeBranchUuid,jdbcType=VARCHAR}
       AND i.status != 'CLOSED'
   </select>
index 6a37a316ceb4ab0b22ed22722b4b8bbcc22155f5..795b3f207386eccd3c18876d0515ad7b4bc9af55 100644 (file)
@@ -26,6 +26,7 @@ import org.sonar.api.utils.System2;
 import org.sonar.api.utils.internal.TestSystem2;
 import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
+import org.sonar.db.protobuf.DbProjectBranches;
 
 import static java.util.Arrays.asList;
 import static java.util.Collections.singletonList;
@@ -37,7 +38,7 @@ public class BranchDaoTest {
 
   private static final long NOW = 1_000L;
   private static final String SELECT_FROM = "select project_uuid as \"projectUuid\", uuid as \"uuid\", branch_type as \"branchType\",  " +
-    "kee as \"kee\", merge_branch_uuid as \"mergeBranchUuid\", created_at as \"createdAt\", updated_at as \"updatedAt\" " +
+    "kee as \"kee\", merge_branch_uuid as \"mergeBranchUuid\", pull_request_binary as \"pullRequestBinary\", created_at as \"createdAt\", updated_at as \"updatedAt\" " +
     "from project_branches ";
   private System2 system2 = new TestSystem2().setNow(NOW);
 
@@ -64,6 +65,7 @@ public class BranchDaoTest {
       entry("branchType", "SHORT"),
       entry("kee", "feature/foo"),
       entry("mergeBranchUuid", null),
+      entry("pullRequestBinary", null),
       entry("createdAt", 1_000L),
       entry("updatedAt", 1_000L));
   }
@@ -85,7 +87,7 @@ public class BranchDaoTest {
     underTest.insert(dbSession, dto2);
 
     underTest.updateMainBranchName(dbSession, "U1", "master");
-    BranchDto loaded = underTest.selectByKey(dbSession, "U1", "master").get();
+    BranchDto loaded = underTest.selectByBranchKey(dbSession, "U1", "master").get();
     assertThat(loaded.getMergeBranchUuid()).isNull();
     assertThat(loaded.getProjectUuid()).isEqualTo("U1");
     assertThat(loaded.getBranchType()).isEqualTo(BranchType.LONG);
@@ -110,7 +112,76 @@ public class BranchDaoTest {
   }
 
   @Test
-  public void upsert() {
+  public void insert_pull_request_branch_with_only_non_null_fields() {
+    String projectUuid = "U1";
+    String uuid = "U2";
+    BranchType branchType = BranchType.PULL_REQUEST;
+    String kee = "123";
+
+    BranchDto dto = new BranchDto();
+    dto.setProjectUuid(projectUuid);
+    dto.setUuid(uuid);
+    dto.setBranchType(branchType);
+    dto.setKey(kee);
+
+    underTest.insert(dbSession, dto);
+
+    BranchDto loaded = underTest.selectByUuid(dbSession, dto.getUuid()).get();
+
+    assertThat(loaded.getProjectUuid()).isEqualTo(projectUuid);
+    assertThat(loaded.getUuid()).isEqualTo(uuid);
+    assertThat(loaded.getBranchType()).isEqualTo(branchType);
+    assertThat(loaded.getKey()).isEqualTo(kee);
+    assertThat(loaded.getMergeBranchUuid()).isNull();
+    assertThat(loaded.getPullRequestData()).isNull();
+  }
+
+  @Test
+  public void insert_pull_request_branch_with_all_fields() {
+    String projectUuid = "U1";
+    String uuid = "U2";
+    BranchType branchType = BranchType.PULL_REQUEST;
+    String kee = "123";
+
+    String branch = "feature/pr1";
+    String title = "Dummy Feature Title";
+    String url = "http://example.com/pullRequests/pr1";
+    String tokenAttributeName = "token";
+    String tokenAttributeValue = "dummy token";
+    DbProjectBranches.PullRequestData pullRequestData = DbProjectBranches.PullRequestData.newBuilder()
+      .setBranch(branch)
+      .setTitle(title)
+      .setUrl(url)
+      .putAttributes(tokenAttributeName, tokenAttributeValue)
+      .build();
+
+    BranchDto dto = new BranchDto();
+    dto.setProjectUuid(projectUuid);
+    dto.setUuid(uuid);
+    dto.setBranchType(branchType);
+    dto.setKey(kee);
+    dto.setPullRequestData(pullRequestData);
+
+    underTest.insert(dbSession, dto);
+
+    BranchDto loaded = underTest.selectByUuid(dbSession, dto.getUuid()).get();
+
+    assertThat(loaded.getProjectUuid()).isEqualTo(projectUuid);
+    assertThat(loaded.getUuid()).isEqualTo(uuid);
+    assertThat(loaded.getBranchType()).isEqualTo(branchType);
+    assertThat(loaded.getKey()).isEqualTo(kee);
+    assertThat(loaded.getMergeBranchUuid()).isNull();
+
+    DbProjectBranches.PullRequestData loadedPullRequestData = loaded.getPullRequestData();
+    assertThat(loadedPullRequestData).isNotNull();
+    assertThat(loadedPullRequestData.getBranch()).isEqualTo(branch);
+    assertThat(loadedPullRequestData.getTitle()).isEqualTo(title);
+    assertThat(loadedPullRequestData.getUrl()).isEqualTo(url);
+    assertThat(loadedPullRequestData.getAttributesMap().get(tokenAttributeName)).isEqualTo(tokenAttributeValue);
+  }
+
+  @Test
+  public void upsert_branch() {
     BranchDto dto = new BranchDto();
     dto.setProjectUuid("U1");
     dto.setUuid("U2");
@@ -126,14 +197,110 @@ public class BranchDaoTest {
     dto.setBranchType(BranchType.SHORT);
     underTest.upsert(dbSession, dto);
 
-    BranchDto loaded = underTest.selectByKey(dbSession, "U1", "foo").get();
+    BranchDto loaded = underTest.selectByBranchKey(dbSession, "U1", "foo").get();
     assertThat(loaded.getMergeBranchUuid()).isEqualTo("U3");
     assertThat(loaded.getProjectUuid()).isEqualTo("U1");
     assertThat(loaded.getBranchType()).isEqualTo(BranchType.LONG);
   }
 
   @Test
-  public void selectByKey() {
+  public void upsert_pull_request() {
+    BranchDto dto = new BranchDto();
+    dto.setProjectUuid("U1");
+    dto.setUuid("U2");
+    dto.setBranchType(BranchType.PULL_REQUEST);
+    dto.setKey("foo");
+    underTest.insert(dbSession, dto);
+
+    // the fields that can be updated
+    dto.setMergeBranchUuid("U3");
+
+    String branch = "feature/pr1";
+    String title = "Dummy Feature Title";
+    String url = "http://example.com/pullRequests/pr1";
+    String tokenAttributeName = "token";
+    String tokenAttributeValue = "dummy token";
+    DbProjectBranches.PullRequestData pullRequestData = DbProjectBranches.PullRequestData.newBuilder()
+      .setBranch(branch)
+      .setTitle(title)
+      .setUrl(url)
+      .putAttributes(tokenAttributeName, tokenAttributeValue)
+      .build();
+    dto.setPullRequestData(pullRequestData);
+
+    // the fields that can't be updated. New values are ignored.
+    dto.setProjectUuid("ignored");
+    dto.setBranchType(BranchType.SHORT);
+    underTest.upsert(dbSession, dto);
+
+    BranchDto loaded = underTest.selectByPullRequestKey(dbSession, "U1", "foo").get();
+    assertThat(loaded.getMergeBranchUuid()).isEqualTo("U3");
+    assertThat(loaded.getProjectUuid()).isEqualTo("U1");
+    assertThat(loaded.getBranchType()).isEqualTo(BranchType.PULL_REQUEST);
+
+    DbProjectBranches.PullRequestData loadedPullRequestData = loaded.getPullRequestData();
+    assertThat(loadedPullRequestData).isNotNull();
+    assertThat(loadedPullRequestData.getBranch()).isEqualTo(branch);
+    assertThat(loadedPullRequestData.getTitle()).isEqualTo(title);
+    assertThat(loadedPullRequestData.getUrl()).isEqualTo(url);
+    assertThat(loadedPullRequestData.getAttributesMap().get(tokenAttributeName)).isEqualTo(tokenAttributeValue);
+  }
+
+  @Test
+  public void update_pull_request_data() {
+    BranchDto dto = new BranchDto();
+    dto.setProjectUuid("U1");
+    dto.setUuid("U2");
+    dto.setBranchType(BranchType.PULL_REQUEST);
+    dto.setKey("foo");
+
+    // the fields that can be updated
+    String mergeBranchUuid = "U3";
+    dto.setMergeBranchUuid(mergeBranchUuid + "-dummy-suffix");
+
+    String branch = "feature/pr1";
+    String title = "Dummy Feature Title";
+    String url = "http://example.com/pullRequests/pr1";
+    String tokenAttributeName = "token";
+    String tokenAttributeValue = "dummy token";
+    DbProjectBranches.PullRequestData pullRequestData = DbProjectBranches.PullRequestData.newBuilder()
+      .setBranch(branch + "-dummy-suffix")
+      .setTitle(title + "-dummy-suffix")
+      .setUrl(url + "-dummy-suffix")
+      .putAttributes(tokenAttributeName, tokenAttributeValue + "-dummy-suffix")
+      .build();
+    dto.setPullRequestData(pullRequestData);
+
+    underTest.insert(dbSession, dto);
+
+    // modify pull request data
+
+    dto.setMergeBranchUuid(mergeBranchUuid);
+    pullRequestData = DbProjectBranches.PullRequestData.newBuilder()
+      .setBranch(branch)
+      .setTitle(title)
+      .setUrl(url)
+      .putAttributes(tokenAttributeName, tokenAttributeValue)
+      .build();
+    dto.setPullRequestData(pullRequestData);
+
+    underTest.upsert(dbSession, dto);
+
+    BranchDto loaded = underTest.selectByPullRequestKey(dbSession, "U1", "foo").get();
+    assertThat(loaded.getMergeBranchUuid()).isEqualTo(mergeBranchUuid);
+    assertThat(loaded.getProjectUuid()).isEqualTo("U1");
+    assertThat(loaded.getBranchType()).isEqualTo(BranchType.PULL_REQUEST);
+
+    DbProjectBranches.PullRequestData loadedPullRequestData = loaded.getPullRequestData();
+    assertThat(loadedPullRequestData).isNotNull();
+    assertThat(loadedPullRequestData.getBranch()).isEqualTo(branch);
+    assertThat(loadedPullRequestData.getTitle()).isEqualTo(title);
+    assertThat(loadedPullRequestData.getUrl()).isEqualTo(url);
+    assertThat(loadedPullRequestData.getAttributesMap().get(tokenAttributeName)).isEqualTo(tokenAttributeValue);
+  }
+
+  @Test
+  public void selectByBranchKey() {
     BranchDto mainBranch = new BranchDto();
     mainBranch.setProjectUuid("U1");
     mainBranch.setUuid("U1");
@@ -150,7 +317,7 @@ public class BranchDaoTest {
     underTest.insert(dbSession, featureBranch);
 
     // select the feature branch
-    BranchDto loaded = underTest.selectByKey(dbSession, "U1", "feature/foo").get();
+    BranchDto loaded = underTest.selectByBranchKey(dbSession, "U1", "feature/foo").get();
     assertThat(loaded.getUuid()).isEqualTo(featureBranch.getUuid());
     assertThat(loaded.getKey()).isEqualTo(featureBranch.getKey());
     assertThat(loaded.getProjectUuid()).isEqualTo(featureBranch.getProjectUuid());
@@ -158,7 +325,37 @@ public class BranchDaoTest {
     assertThat(loaded.getMergeBranchUuid()).isEqualTo(featureBranch.getMergeBranchUuid());
 
     // select a branch on another project with same branch name
-    assertThat(underTest.selectByKey(dbSession, "U3", "feature/foo")).isEmpty();
+    assertThat(underTest.selectByBranchKey(dbSession, "U3", "feature/foo")).isEmpty();
+  }
+
+  @Test
+  public void selectByPullRequestKey() {
+    BranchDto mainBranch = new BranchDto();
+    mainBranch.setProjectUuid("U1");
+    mainBranch.setUuid("U1");
+    mainBranch.setBranchType(BranchType.LONG);
+    mainBranch.setKey("master");
+    underTest.insert(dbSession, mainBranch);
+
+    String pullRequestId = "123";
+    BranchDto pullRequest = new BranchDto();
+    pullRequest.setProjectUuid("U1");
+    pullRequest.setUuid("U2");
+    pullRequest.setBranchType(BranchType.PULL_REQUEST);
+    pullRequest.setKey(pullRequestId);
+    pullRequest.setMergeBranchUuid("U3");
+    underTest.insert(dbSession, pullRequest);
+
+    // select the feature branch
+    BranchDto loaded = underTest.selectByPullRequestKey(dbSession, "U1", pullRequestId).get();
+    assertThat(loaded.getUuid()).isEqualTo(pullRequest.getUuid());
+    assertThat(loaded.getKey()).isEqualTo(pullRequest.getKey());
+    assertThat(loaded.getProjectUuid()).isEqualTo(pullRequest.getProjectUuid());
+    assertThat(loaded.getBranchType()).isEqualTo(pullRequest.getBranchType());
+    assertThat(loaded.getMergeBranchUuid()).isEqualTo(pullRequest.getMergeBranchUuid());
+
+    // select a branch on another project with same branch name
+    assertThat(underTest.selectByPullRequestKey(dbSession, "U3", pullRequestId)).isEmpty();
   }
 
   @Test
index b5904e6709fec61042d284befb9616d8ef086eac..7da1bce7caa35e812c5196b6156b8c6f3aff38f3 100644 (file)
  */
 package org.sonar.db.component;
 
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.db.protobuf.DbProjectBranches;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
 public class BranchDtoTest {
 
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
   private BranchDto underTest = new BranchDto();
 
   @Test
@@ -42,4 +48,30 @@ public class BranchDtoTest {
 
     assertThat(underTest.isMain()).isFalse();
   }
+
+  @Test
+  public void encode_and_decode_pull_request_data() {
+    String branch = "feature/pr1";
+    String title = "Dummy Feature Title";
+    String url = "http://example.com/pullRequests/pr1";
+
+    DbProjectBranches.PullRequestData pullRequestData = DbProjectBranches.PullRequestData.newBuilder()
+      .setBranch(branch)
+      .setTitle(title)
+      .setUrl(url)
+      .build();
+
+    underTest.setPullRequestData(pullRequestData);
+
+    DbProjectBranches.PullRequestData decoded = underTest.getPullRequestData();
+    assertThat(decoded).isNotNull();
+    assertThat(decoded.getBranch()).isEqualTo(branch);
+    assertThat(decoded.getTitle()).isEqualTo(title);
+    assertThat(decoded.getUrl()).isEqualTo(url);
+  }
+
+  @Test
+  public void getPullRequestData_returns_null_when_data_is_null() {
+    assertThat(underTest.getPullRequestData()).isNull();
+  }
 }
index 709308d9ce6ad827566a180dd5b5a56a6cbb5150..0b36b57897f6f4c023f3f17b19ce0959c0c5f35d 100644 (file)
@@ -128,4 +128,15 @@ public class ComponentDtoTest {
     assertThat(underTest.getKey()).isEqualTo("my_key");
     assertThat(underTest.getBranch()).isNull();
   }
+
+  @Test
+  public void getKey_and_getPullRequest() {
+    ComponentDto underTest = new ComponentDto().setDbKey("my_key:PULL_REQUEST:pr-123");
+    assertThat(underTest.getKey()).isEqualTo("my_key");
+    assertThat(underTest.getPullRequest()).isEqualTo("pr-123");
+
+    underTest = new ComponentDto().setDbKey("my_key");
+    assertThat(underTest.getKey()).isEqualTo("my_key");
+    assertThat(underTest.getPullRequest()).isNull();
+  }
 }
index bd1b66fd2f65932b83e6fb80377e2eee6b566634..3166d3aab8b22fa927a6254d498bc33e5977fcf3 100644 (file)
@@ -34,6 +34,7 @@ import org.sonar.db.organization.OrganizationDto;
 import static com.google.common.collect.Lists.newArrayList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.entry;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
 import static org.sonar.db.component.ComponentKeyUpdaterDao.computeNewKey;
 import static org.sonar.db.component.ComponentTesting.newDirectory;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
@@ -105,6 +106,33 @@ public class ComponentKeyUpdaterDaoTest {
       .forEach(map -> map.values().forEach(k -> assertThat(k.toString()).startsWith(newProjectKey)));
   }
 
+  @Test
+  public void updateKey_updates_pull_requests_too() {
+    ComponentDto project = db.components().insertMainBranch();
+    ComponentDto pullRequest = db.components().insertProjectBranch(project, b -> b.setBranchType(PULL_REQUEST));
+    db.components().insertComponent(newFileDto(pullRequest));
+    db.components().insertComponent(newFileDto(pullRequest));
+    int branchComponentCount = 3;
+
+    String oldProjectKey = project.getKey();
+    assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, oldProjectKey)).hasSize(1);
+
+    String oldBranchKey = pullRequest.getDbKey();
+    assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, oldBranchKey)).hasSize(branchComponentCount);
+
+    String newProjectKey = "newKey";
+    String newBranchKey = ComponentDto.generatePullRequestKey(newProjectKey, pullRequest.getPullRequest());
+    underTest.updateKey(dbSession, project.uuid(), newProjectKey);
+
+    assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, oldProjectKey)).isEmpty();
+    assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, oldBranchKey)).isEmpty();
+
+    assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, newProjectKey)).hasSize(1);
+    assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, newBranchKey)).hasSize(branchComponentCount);
+    db.select(dbSession, "select kee from projects")
+      .forEach(map -> map.values().forEach(k -> assertThat(k.toString()).startsWith(newProjectKey)));
+  }
+
   @Test
   public void bulk_updateKey_updates_branches_too() {
     ComponentDto project = db.components().insertMainBranch();
@@ -133,6 +161,34 @@ public class ComponentKeyUpdaterDaoTest {
       .forEach(map -> map.values().forEach(k -> assertThat(k.toString()).startsWith(newProjectKey)));
   }
 
+  @Test
+  public void bulk_updateKey_updates_pull_requests_too() {
+    ComponentDto project = db.components().insertMainBranch();
+    ComponentDto pullRequest = db.components().insertProjectBranch(project, b -> b.setBranchType(PULL_REQUEST));
+    ComponentDto module = db.components().insertComponent(prefixDbKeyWithKey(newModuleDto(pullRequest), project.getKey()));
+    db.components().insertComponent(prefixDbKeyWithKey(newFileDto(module), module.getKey()));
+    db.components().insertComponent(prefixDbKeyWithKey(newFileDto(module), module.getKey()));
+    int branchComponentCount = 4;
+
+    String oldProjectKey = project.getKey();
+    assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, oldProjectKey)).hasSize(1);
+
+    String oldPullRequestKey = pullRequest.getDbKey();
+    assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, oldPullRequestKey)).hasSize(branchComponentCount);
+
+    String newProjectKey = "newKey";
+    String newPullRequestKey = ComponentDto.generatePullRequestKey(newProjectKey, pullRequest.getPullRequest());
+    underTest.bulkUpdateKey(dbSession, project.uuid(), oldProjectKey, newProjectKey);
+
+    assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, oldProjectKey)).isEmpty();
+    assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, oldPullRequestKey)).isEmpty();
+
+    assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, newProjectKey)).hasSize(1);
+    assertThat(dbClient.componentDao().selectAllComponentsFromProjectKey(dbSession, newPullRequestKey)).hasSize(branchComponentCount);
+    db.select(dbSession, "select kee from projects")
+      .forEach(map -> map.values().forEach(k -> assertThat(k.toString()).startsWith(newProjectKey)));
+  }
+
   private ComponentDto prefixDbKeyWithKey(ComponentDto componentDto, String key) {
     return componentDto.setDbKey(key + ":" + componentDto.getDbKey());
   }
index 835ea64119729e036b9a2a4aa8e9961bf1c761cd..b0408ddb77344843726f8723b30567f23fd4fc01 100644 (file)
@@ -29,6 +29,7 @@ import org.sonar.db.organization.OrganizationDto;
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
 import static org.sonar.db.component.ComponentDto.UUID_PATH_SEPARATOR;
 
 public class ComponentTesting {
@@ -99,7 +100,15 @@ public class ComponentTesting {
 
   private static String generateKey(String key, ComponentDto parentModuleOrProject) {
     String branch = parentModuleOrProject.getBranch();
-    return branch == null ? key : ComponentDto.generateBranchKey(key, branch);
+    if (branch != null) {
+      return ComponentDto.generateBranchKey(key, branch);
+    }
+    String pullRequest = parentModuleOrProject.getPullRequest();
+    if (pullRequest != null) {
+      return ComponentDto.generatePullRequestKey(key, pullRequest);
+    }
+
+    return key;
   }
 
   public static ComponentDto newModuleDto(ComponentDto subProjectOrProject) {
@@ -231,6 +240,7 @@ public class ComponentTesting {
     checkArgument(project.qualifier().equals(Qualifiers.PROJECT));
     checkArgument(project.getMainBranchProjectUuid() == null);
     String branchName = branchDto.getKey();
+    String branchSeparator = branchDto.getBranchType() == PULL_REQUEST ? ":PULL_REQUEST:" : ":BRANCH:";
     String uuid = branchDto.getUuid();
     return new ComponentDto()
       .setUuid(uuid)
@@ -240,7 +250,7 @@ public class ComponentTesting {
       .setModuleUuidPath(UUID_PATH_SEPARATOR + uuid + UUID_PATH_SEPARATOR)
       .setRootUuid(uuid)
       // name of the branch is not mandatory on the main branch
-      .setDbKey(branchName != null ? project.getDbKey() + ":BRANCH:" + branchName : project.getKey())
+      .setDbKey(branchName != null ? project.getDbKey() + branchSeparator + branchName : project.getKey())
       .setMainBranchProjectUuid(project.uuid())
       .setName(project.name())
       .setLongName(project.longName())
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/AddKeyTypeInProjectBranches.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/AddKeyTypeInProjectBranches.java
new file mode 100644 (file)
index 0000000..8b4137c
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.platform.db.migration.version.v71;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class AddKeyTypeInProjectBranches extends DdlChange {
+
+  public static final String TABLE_NAME = "project_branches";
+
+  public AddKeyTypeInProjectBranches(Database db) {
+    super(db);
+  }
+
+  @Override
+  public void execute(Context context) throws SQLException {
+    context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+      .addColumn(VarcharColumnDef.newVarcharColumnDefBuilder()
+        .setColumnName("key_type")
+        .setIsNullable(true)
+        .setLimit(12)
+        .build())
+      .build());
+  }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/AddPullRequestBinaryInProjectBranches.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/AddPullRequestBinaryInProjectBranches.java
new file mode 100644 (file)
index 0000000..2c6872a
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.platform.db.migration.version.v71;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.BlobColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class AddPullRequestBinaryInProjectBranches extends DdlChange {
+
+  static final String TABLE_NAME = "project_branches";
+  static final String COLUMN_NAME = "pull_request_binary";
+
+  public AddPullRequestBinaryInProjectBranches(Database db) {
+    super(db);
+  }
+
+  @Override
+  public void execute(Context context) throws SQLException {
+    context.execute(new AddColumnsBuilder(getDialect(), TABLE_NAME)
+      .addColumn(BlobColumnDef.newBlobColumnDefBuilder()
+        .setColumnName(COLUMN_NAME)
+        .setIsNullable(true)
+        .build())
+      .build());
+  }
+}
index 9c406e6eb99c1b15d39df47c9a5c284e40e9f0ca..d8f5d82738c4908d928fe606d69f1fbf91c5e886 100644 (file)
@@ -43,6 +43,12 @@ public class DbVersion71 implements DbVersion {
       .add(2013, "Create WEBHOOKS Table", CreateWebhooksTable.class)
       .add(2014, "Migrate webhooks from SETTINGS table to WEBHOOKS table", MigrateWebhooksToWebhooksTable.class)
       .add(2015, "Add webhook key to WEBHOOK_DELIVERIES table", AddWebhookKeyToWebhookDeliveriesTable.class)
+      .add(2016, "Increase branch type size in PROJECT_BRANCHES", IncreaseBranchTypeSizeForPullRequest.class)
+      .add(2017, "Add key_type column in PROJECT_BRANCHES", AddKeyTypeInProjectBranches.class)
+      .add(2018, "Fill key_type column in PROJECT_BRANCHES", SetKeyTypeToBranchInProjectBranches.class)
+      .add(2019, "Make key_type not nullable in PROJECT_BRANCHES", MakeKeyTypeNotNullableInProjectBranches.class)
+      .add(2020, "Replace index in PROJECT_BRANCHES", ReplaceIndexInProjectBranches.class)
+      .add(2021, "Add pull_request_data in PROJECT_BRANCHES", AddPullRequestBinaryInProjectBranches.class)
     ;
   }
 }
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/IncreaseBranchTypeSizeForPullRequest.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/IncreaseBranchTypeSizeForPullRequest.java
new file mode 100644 (file)
index 0000000..a0c2df0
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.platform.db.migration.version.v71;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.sql.AlterColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder;
+
+public class IncreaseBranchTypeSizeForPullRequest extends DdlChange {
+  private static final String TABLE_NAME = "project_branches";
+
+  public IncreaseBranchTypeSizeForPullRequest(Database db) {
+    super(db);
+  }
+
+  @Override
+  public void execute(Context context) throws SQLException {
+    context.execute(new AlterColumnsBuilder(getDialect(), TABLE_NAME)
+      .updateColumn(newVarcharColumnDefBuilder()
+        .setColumnName("branch_type")
+        .setLimit(12)
+        .build())
+      .build());
+  }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/MakeKeyTypeNotNullableInProjectBranches.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/MakeKeyTypeNotNullableInProjectBranches.java
new file mode 100644 (file)
index 0000000..4c4951e
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.platform.db.migration.version.v71;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.sql.AlterColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder;
+
+public class MakeKeyTypeNotNullableInProjectBranches extends DdlChange {
+  static final String TABLE_NAME = "project_branches";
+
+  public MakeKeyTypeNotNullableInProjectBranches(Database db) {
+    super(db);
+  }
+
+  @Override
+  public void execute(Context context) throws SQLException {
+    context.execute(new AlterColumnsBuilder(getDialect(), TABLE_NAME)
+      .updateColumn(newVarcharColumnDefBuilder()
+        .setColumnName("key_type")
+        .setLimit(12)
+        .setIsNullable(false)
+        .build())
+      .build());
+  }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/ReplaceIndexInProjectBranches.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/ReplaceIndexInProjectBranches.java
new file mode 100644 (file)
index 0000000..3fb289a
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.platform.db.migration.version.v71;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
+import org.sonar.server.platform.db.migration.sql.DropIndexBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class ReplaceIndexInProjectBranches extends DdlChange {
+
+  static final String TABLE_NAME = "project_branches";
+  private static final String OLD_INDEX_NAME = "project_branches_kee";
+  static final String NEW_INDEX_NAME = "project_branches_kee_key_type";
+
+  static final VarcharColumnDef PROJECT_UUID_COLUMN = VarcharColumnDef.newVarcharColumnDefBuilder()
+    .setColumnName("project_uuid")
+    .setIsNullable(false)
+    .setLimit(50)
+    .build();
+
+  static final VarcharColumnDef KEE_COLUMN = VarcharColumnDef.newVarcharColumnDefBuilder()
+    .setColumnName("kee")
+    .setIsNullable(false)
+    .setLimit(255)
+    .build();
+
+  static final VarcharColumnDef KEY_TYPE_COLUMN = VarcharColumnDef.newVarcharColumnDefBuilder()
+    .setColumnName("key_type")
+    .setIsNullable(false)
+    .setLimit(12)
+    .build();
+
+  public ReplaceIndexInProjectBranches(Database db) {
+    super(db);
+  }
+
+  @Override
+  public void execute(Context context) throws SQLException {
+    context.execute(new DropIndexBuilder(getDialect())
+      .setTable(TABLE_NAME)
+      .setName(OLD_INDEX_NAME)
+      .build());
+
+    context.execute(new CreateIndexBuilder(getDialect())
+      .addColumn(PROJECT_UUID_COLUMN)
+      .addColumn(KEE_COLUMN)
+      .addColumn(KEY_TYPE_COLUMN)
+      .setUnique(true)
+      .setTable(TABLE_NAME)
+      .setName(NEW_INDEX_NAME)
+      .build()
+    );
+  }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/SetKeyTypeToBranchInProjectBranches.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v71/SetKeyTypeToBranchInProjectBranches.java
new file mode 100644 (file)
index 0000000..7f3739f
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.platform.db.migration.version.v71;
+
+import java.sql.SQLException;
+import org.sonar.api.utils.System2;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.MassUpdate;
+
+public class SetKeyTypeToBranchInProjectBranches extends DataChange {
+  static final String TABLE_NAME = "project_branches";
+  static final String DEFAULT_KEY_TYPE = "BRANCH";
+
+  private final System2 system2;
+
+  public SetKeyTypeToBranchInProjectBranches(Database db, System2 system2) {
+    super(db);
+    this.system2 = system2;
+  }
+
+  @Override
+  protected void execute(Context context) throws SQLException {
+    long now = system2.now();
+    MassUpdate massUpdate = context.prepareMassUpdate();
+    massUpdate.rowPluralName("branches");
+    massUpdate.select("select uuid from " + TABLE_NAME + " where key_type is null");
+    massUpdate.update("update " + TABLE_NAME + " set key_type=?, updated_at=? where uuid = ?");
+    massUpdate.execute((row, update) -> {
+      update.setString(1, DEFAULT_KEY_TYPE);
+      update.setLong(2, now);
+      update.setString(3, row.getString(1));
+      return true;
+    });
+  }
+}
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/AddKeyTypeInProjectBranchesTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/AddKeyTypeInProjectBranchesTest.java
new file mode 100644 (file)
index 0000000..b4b9ff0
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.platform.db.migration.version.v71;
+
+import java.sql.SQLException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.db.CoreDbTester;
+
+import static java.sql.Types.VARCHAR;
+
+public class AddKeyTypeInProjectBranchesTest {
+  public static final String TABLE_NAME = "project_branches";
+
+  @Rule
+  public final CoreDbTester dbTester = CoreDbTester.createForSchema(AddKeyTypeInProjectBranchesTest.class, TABLE_NAME + ".sql");
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private AddKeyTypeInProjectBranches underTest = new AddKeyTypeInProjectBranches(dbTester.database());
+
+  @Test
+  public void column_is_added_to_table() throws SQLException {
+    underTest.execute();
+
+    dbTester.assertColumnDefinition(TABLE_NAME, "key_type", VARCHAR, null, true);
+  }
+
+  @Test
+  public void migration_is_not_reentrant() throws SQLException {
+    underTest.execute();
+
+    expectedException.expect(IllegalStateException.class);
+
+    underTest.execute();
+  }
+}
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/AddPullRequestBinaryInProjectBranchesTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/AddPullRequestBinaryInProjectBranchesTest.java
new file mode 100644 (file)
index 0000000..970b5ea
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.platform.db.migration.version.v71;
+
+import java.sql.SQLException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.db.CoreDbTester;
+
+import static java.sql.Types.BLOB;
+import static org.sonar.server.platform.db.migration.version.v71.AddPullRequestBinaryInProjectBranches.COLUMN_NAME;
+import static org.sonar.server.platform.db.migration.version.v71.AddPullRequestBinaryInProjectBranches.TABLE_NAME;
+
+public class AddPullRequestBinaryInProjectBranchesTest {
+
+  @Rule
+  public final CoreDbTester dbTester = CoreDbTester.createForSchema(AddPullRequestBinaryInProjectBranchesTest.class, TABLE_NAME + ".sql");
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private AddPullRequestBinaryInProjectBranches underTest = new AddPullRequestBinaryInProjectBranches(dbTester.database());
+
+  @Test
+  public void column_is_added_to_table() throws SQLException {
+    underTest.execute();
+
+    dbTester.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, BLOB, null, true);
+  }
+
+  @Test
+  public void migration_is_not_reentrant() throws SQLException {
+    underTest.execute();
+
+    expectedException.expect(IllegalStateException.class);
+
+    underTest.execute();
+  }
+}
index 6b5041ef512bf9340524a70ad4392f106fd09297..f50e8910b81d20ace172178fa81115ec85b49936 100644 (file)
@@ -36,7 +36,7 @@ public class DbVersion71Test {
 
   @Test
   public void verify_migration_count() {
-    verifyMigrationCount(underTest, 16);
+    verifyMigrationCount(underTest, 22);
   }
 
 }
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/IncreaseBranchTypeSizeForPullRequestTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/IncreaseBranchTypeSizeForPullRequestTest.java
new file mode 100644 (file)
index 0000000..d9d121a
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.platform.db.migration.version.v71;
+
+import java.sql.SQLException;
+import java.sql.Types;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.db.CoreDbTester;
+import org.sonar.scanner.protocol.output.ScannerReport;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IncreaseBranchTypeSizeForPullRequestTest {
+  private static final String TABLE_NAME = "project_branches";
+
+  @Rule
+  public CoreDbTester db = CoreDbTester.createForSchema(IncreaseBranchTypeSizeForPullRequestTest.class, "project_branches.sql");
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private IncreaseBranchTypeSizeForPullRequest underTest = new IncreaseBranchTypeSizeForPullRequest(db.database());
+
+  @Test
+  public void cannot_insert_PULL_REQUEST_type_before_migration() {
+    expectedException.expect(IllegalStateException.class);
+
+    insertRow();
+  }
+
+  @Test
+  public void can_insert_PULL_REQUEST_after_execute() throws SQLException {
+    underTest.execute();
+    assertThat(db.countRowsOfTable(TABLE_NAME)).isEqualTo(0);
+    insertRow();
+    assertThat(db.countRowsOfTable(TABLE_NAME)).isEqualTo(1);
+  }
+
+  private void insertRow() {
+    db.executeInsert(
+      "PROJECT_BRANCHES",
+      "UUID", "dummy_uuid",
+      "PROJECT_UUID", "dummy_project_uuid",
+      "KEE", "dummy_key",
+      "CREATED_AT", 456789,
+      "UPDATED_AT", 456789,
+      "BRANCH_TYPE", "PULL_REQUEST");
+  }
+}
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/MakeKeyTypeNotNullableInProjectBranchesTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/MakeKeyTypeNotNullableInProjectBranchesTest.java
new file mode 100644 (file)
index 0000000..7b7d7f3
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.platform.db.migration.version.v71;
+
+import java.sql.SQLException;
+import java.sql.Types;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.rule.RuleScope;
+import org.sonar.db.CoreDbTester;
+
+import static org.sonar.server.platform.db.migration.version.v71.MakeKeyTypeNotNullableInProjectBranches.TABLE_NAME;
+
+public class MakeKeyTypeNotNullableInProjectBranchesTest {
+  @Rule
+  public CoreDbTester db = CoreDbTester.createForSchema(MakeKeyTypeNotNullableInProjectBranchesTest.class, "project_branches.sql");
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private MakeKeyTypeNotNullableInProjectBranches underTest = new MakeKeyTypeNotNullableInProjectBranches(db.database());
+
+  @Test
+  public void execute_makes_column_not_null() throws SQLException {
+    db.assertColumnDefinition(TABLE_NAME, "key_type", Types.VARCHAR, null, true);
+    insertRow();
+
+    underTest.execute();
+
+    db.assertColumnDefinition(TABLE_NAME, "key_type", Types.VARCHAR, null, false);
+  }
+
+  private void insertRow() {
+    db.executeInsert(
+      "PROJECT_BRANCHES",
+      "UUID", "dummy_uuid",
+      "PROJECT_UUID", "dummy_project_uuid",
+      "KEE", "dummy_key",
+      "KEY_TYPE", "BRANCH",
+      "CREATED_AT", 456789,
+      "UPDATED_AT", 456789,
+      "BRANCH_TYPE", "BRANCH");
+  }
+}
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/ReplaceIndexInProjectBranchesTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/ReplaceIndexInProjectBranchesTest.java
new file mode 100644 (file)
index 0000000..70a0510
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.platform.db.migration.version.v71;
+
+import java.sql.SQLException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.db.CoreDbTester;
+
+import static org.sonar.server.platform.db.migration.version.v71.ReplaceIndexInProjectBranches.NEW_INDEX_NAME;
+import static org.sonar.server.platform.db.migration.version.v71.ReplaceIndexInProjectBranches.KEE_COLUMN;
+import static org.sonar.server.platform.db.migration.version.v71.ReplaceIndexInProjectBranches.KEY_TYPE_COLUMN;
+import static org.sonar.server.platform.db.migration.version.v71.ReplaceIndexInProjectBranches.PROJECT_UUID_COLUMN;
+import static org.sonar.server.platform.db.migration.version.v71.ReplaceIndexInProjectBranches.TABLE_NAME;
+
+public class ReplaceIndexInProjectBranchesTest {
+  @Rule
+  public final CoreDbTester dbTester = CoreDbTester.createForSchema(ReplaceIndexInProjectBranchesTest.class, "project_branches.sql");
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private ReplaceIndexInProjectBranches underTest = new ReplaceIndexInProjectBranches(dbTester.database());
+
+  @Test
+  public void column_is_part_of_index() throws SQLException {
+    underTest.execute();
+
+    dbTester.assertUniqueIndex(TABLE_NAME, NEW_INDEX_NAME, PROJECT_UUID_COLUMN.getName(), KEE_COLUMN.getName(), KEY_TYPE_COLUMN.getName());
+  }
+
+  @Test
+  public void adding_pr_with_same_key_as_existing_branch_fails_before_migration() {
+    expectedException.expect(IllegalStateException.class);
+
+    String key = "feature/foo";
+    insertBranch(1, key);
+    insertPullRequest(2, key);
+  }
+
+  @Test
+  public void adding_pr_with_same_key_as_existing_branch_works_after_migration() throws SQLException {
+    underTest.execute();
+
+    String key = "feature/foo";
+    insertBranch(1, key);
+    insertPullRequest(2, key);
+  }
+
+  private void insertBranch(int id, String name) {
+    insertRow(id, "SHORT", name, "BRANCH");
+  }
+
+  private void insertPullRequest(int id, String pullRequestId) {
+    insertRow(id, "PULL_REQUEST", pullRequestId, "PULL_REQUEST");
+  }
+
+  private void insertRow(int id, String branchType, String key, String keyType) {
+    dbTester.executeInsert(
+      "PROJECT_BRANCHES",
+      "UUID", "dummy_uuid" + id,
+      "PROJECT_UUID", "dummy_project_uuid",
+      "KEE", key,
+      "KEY_TYPE", keyType,
+      "CREATED_AT", 456789 + id,
+      "UPDATED_AT", 456789 + id,
+      "BRANCH_TYPE", branchType);
+  }
+}
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/SetKeyTypeToBranchInProjectBranchesTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v71/SetKeyTypeToBranchInProjectBranchesTest.java
new file mode 100644 (file)
index 0000000..c843878
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.platform.db.migration.version.v71;
+
+import java.sql.SQLException;
+import javax.annotation.Nullable;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.internal.TestSystem2;
+import org.sonar.db.CoreDbTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.server.platform.db.migration.version.v71.SetKeyTypeToBranchInProjectBranches.DEFAULT_KEY_TYPE;
+import static org.sonar.server.platform.db.migration.version.v71.SetKeyTypeToBranchInProjectBranches.TABLE_NAME;
+
+public class SetKeyTypeToBranchInProjectBranchesTest {
+  private static final long PAST = 10_000_000_000L;
+  private static final long NOW = 50_000_000_000L;
+
+  private System2 system2 = new TestSystem2().setNow(NOW);
+
+  @Rule
+  public final CoreDbTester dbTester = CoreDbTester.createForSchema(SetKeyTypeToBranchInProjectBranchesTest.class, "project_branches.sql");
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private SetKeyTypeToBranchInProjectBranches underTest = new SetKeyTypeToBranchInProjectBranches(dbTester.database(), system2);
+
+  @Test
+  public void has_no_effect_if_table_project_branches_is_empty() throws SQLException {
+    underTest.execute();
+
+    assertThat(dbTester.countRowsOfTable(TABLE_NAME)).isEqualTo(0);
+  }
+
+  @Test
+  public void updates_rows_to_BRANCH() throws SQLException {
+    insertRow(1, "SHORT");
+    insertRow(2, "LONG");
+    insertRow(3, "SHORT");
+    insertRow(4, "LONG");
+
+    String countUpdatedAtSQL = "select count(uuid) from " + TABLE_NAME + " where updated_at = ";
+
+    assertThat(countRowsWithValue(null)).isEqualTo(4);
+    assertThat(countRowsWithValue(DEFAULT_KEY_TYPE)).isEqualTo(0);
+    assertThat(dbTester.countSql(countUpdatedAtSQL + PAST)).isEqualTo(4);
+
+    underTest.execute();
+
+    assertThat(countRowsWithValue(null)).isEqualTo(0);
+    assertThat(countRowsWithValue(DEFAULT_KEY_TYPE)).isEqualTo(4);
+    assertThat(dbTester.countSql(countUpdatedAtSQL + NOW)).isEqualTo(4);
+  }
+
+  @Test
+  public void execute_is_reentreant() throws SQLException {
+    insertRow(1, "SHORT");
+    insertRow(2, "LONG");
+    insertRow(3, "SHORT");
+    insertRow(4, "LONG");
+
+    underTest.execute();
+
+    underTest.execute();
+
+    assertThat(countRowsWithValue(null)).isEqualTo(0);
+    assertThat(countRowsWithValue(DEFAULT_KEY_TYPE)).isEqualTo(4);
+  }
+
+  private int countRowsWithValue(@Nullable String value) {
+    if (value == null) {
+      return dbTester.countSql("select count(1) from " + TABLE_NAME + " where key_type is null");
+    }
+    return dbTester.countSql("select count(1) from " + TABLE_NAME + " where key_type = '" + value + "'");
+  }
+
+  private void insertRow(int id, String branchType) {
+    dbTester.executeInsert(
+      "PROJECT_BRANCHES",
+      "UUID", "dummy_uuid" + id,
+      "PROJECT_UUID", "dummy_project_uuid" + id,
+      "KEE", "dummy_key" + id,
+      "CREATED_AT", PAST,
+      "UPDATED_AT", PAST,
+      "BRANCH_TYPE", branchType);
+  }
+}
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/AddKeyTypeInProjectBranchesTest/project_branches.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/AddKeyTypeInProjectBranchesTest/project_branches.sql
new file mode 100644 (file)
index 0000000..7f1ef8a
--- /dev/null
@@ -0,0 +1,11 @@
+CREATE TABLE "PROJECT_BRANCHES" (
+  "UUID" VARCHAR(50) NOT NULL PRIMARY KEY,
+  "PROJECT_UUID" VARCHAR(50) NOT NULL,
+  "KEE" VARCHAR(255) NOT NULL,
+  "BRANCH_TYPE" VARCHAR(12),
+  "MERGE_BRANCH_UUID" VARCHAR(50),
+  "CREATED_AT" BIGINT NOT NULL,
+  "UPDATED_AT" BIGINT NOT NULL
+);
+CREATE UNIQUE INDEX "PK_PROJECT_BRANCHES" ON "PROJECT_BRANCHES" ("UUID");
+CREATE UNIQUE INDEX "PROJECT_BRANCHES_KEE" ON "PROJECT_BRANCHES" ("PROJECT_UUID", "KEE");
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/AddPullRequestBinaryInProjectBranchesTest/project_branches.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/AddPullRequestBinaryInProjectBranchesTest/project_branches.sql
new file mode 100644 (file)
index 0000000..d368096
--- /dev/null
@@ -0,0 +1,12 @@
+CREATE TABLE "PROJECT_BRANCHES" (
+  "UUID" VARCHAR(50) NOT NULL PRIMARY KEY,
+  "PROJECT_UUID" VARCHAR(50) NOT NULL,
+  "KEE" VARCHAR(255) NOT NULL,
+  "KEY_TYPE" VARCHAR(12) NOT NULL,
+  "BRANCH_TYPE" VARCHAR(12),
+  "MERGE_BRANCH_UUID" VARCHAR(50),
+  "CREATED_AT" BIGINT NOT NULL,
+  "UPDATED_AT" BIGINT NOT NULL
+);
+CREATE UNIQUE INDEX "PK_PROJECT_BRANCHES" ON "PROJECT_BRANCHES" ("UUID");
+CREATE UNIQUE INDEX "PROJECT_BRANCHES_KEE" ON "PROJECT_BRANCHES" ("PROJECT_UUID", "KEE", "KEY_TYPE");
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/IncreaseBranchTypeSizeForPullRequestTest/project_branches.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/IncreaseBranchTypeSizeForPullRequestTest/project_branches.sql
new file mode 100644 (file)
index 0000000..e05ff3b
--- /dev/null
@@ -0,0 +1,11 @@
+CREATE TABLE "PROJECT_BRANCHES" (
+  "UUID" VARCHAR(50) NOT NULL PRIMARY KEY,
+  "PROJECT_UUID" VARCHAR(50) NOT NULL,
+  "KEE" VARCHAR(255) NOT NULL,
+  "BRANCH_TYPE" VARCHAR(5),
+  "MERGE_BRANCH_UUID" VARCHAR(50),
+  "CREATED_AT" BIGINT NOT NULL,
+  "UPDATED_AT" BIGINT NOT NULL
+);
+CREATE UNIQUE INDEX "PK_PROJECT_BRANCHES" ON "PROJECT_BRANCHES" ("UUID");
+CREATE UNIQUE INDEX "PROJECT_BRANCHES_KEE" ON "PROJECT_BRANCHES" ("PROJECT_UUID", "KEE");
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/MakeKeyTypeNotNullableInProjectBranchesTest/project_branches.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/MakeKeyTypeNotNullableInProjectBranchesTest/project_branches.sql
new file mode 100644 (file)
index 0000000..6441ce1
--- /dev/null
@@ -0,0 +1,12 @@
+CREATE TABLE "PROJECT_BRANCHES" (
+  "UUID" VARCHAR(50) NOT NULL PRIMARY KEY,
+  "PROJECT_UUID" VARCHAR(50) NOT NULL,
+  "KEE" VARCHAR(255) NOT NULL,
+  "KEY_TYPE" VARCHAR(12) NULL,
+  "BRANCH_TYPE" VARCHAR(12),
+  "MERGE_BRANCH_UUID" VARCHAR(50),
+  "CREATED_AT" BIGINT NOT NULL,
+  "UPDATED_AT" BIGINT NOT NULL
+);
+CREATE UNIQUE INDEX "PK_PROJECT_BRANCHES" ON "PROJECT_BRANCHES" ("UUID");
+CREATE UNIQUE INDEX "PROJECT_BRANCHES_KEE" ON "PROJECT_BRANCHES" ("PROJECT_UUID", "KEE");
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/ReplaceIndexInProjectBranchesTest/project_branches.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/ReplaceIndexInProjectBranchesTest/project_branches.sql
new file mode 100644 (file)
index 0000000..b7553cf
--- /dev/null
@@ -0,0 +1,12 @@
+CREATE TABLE "PROJECT_BRANCHES" (
+  "UUID" VARCHAR(50) NOT NULL PRIMARY KEY,
+  "PROJECT_UUID" VARCHAR(50) NOT NULL,
+  "KEE" VARCHAR(255) NOT NULL,
+  "KEY_TYPE" VARCHAR(12) NOT NULL,
+  "BRANCH_TYPE" VARCHAR(12),
+  "MERGE_BRANCH_UUID" VARCHAR(50),
+  "CREATED_AT" BIGINT NOT NULL,
+  "UPDATED_AT" BIGINT NOT NULL
+);
+CREATE UNIQUE INDEX "PK_PROJECT_BRANCHES" ON "PROJECT_BRANCHES" ("UUID");
+CREATE UNIQUE INDEX "PROJECT_BRANCHES_KEE" ON "PROJECT_BRANCHES" ("PROJECT_UUID", "KEE");
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/SetKeyTypeToBranchInProjectBranchesTest/project_branches.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v71/SetKeyTypeToBranchInProjectBranchesTest/project_branches.sql
new file mode 100644 (file)
index 0000000..6441ce1
--- /dev/null
@@ -0,0 +1,12 @@
+CREATE TABLE "PROJECT_BRANCHES" (
+  "UUID" VARCHAR(50) NOT NULL PRIMARY KEY,
+  "PROJECT_UUID" VARCHAR(50) NOT NULL,
+  "KEE" VARCHAR(255) NOT NULL,
+  "KEY_TYPE" VARCHAR(12) NULL,
+  "BRANCH_TYPE" VARCHAR(12),
+  "MERGE_BRANCH_UUID" VARCHAR(50),
+  "CREATED_AT" BIGINT NOT NULL,
+  "UPDATED_AT" BIGINT NOT NULL
+);
+CREATE UNIQUE INDEX "PK_PROJECT_BRANCHES" ON "PROJECT_BRANCHES" ("UUID");
+CREATE UNIQUE INDEX "PROJECT_BRANCHES_KEE" ON "PROJECT_BRANCHES" ("PROJECT_UUID", "KEE");
index c4f90f0d41d9c1f5aa53a71c0a3f9e307149f645..13b6c4ae0232cf2e10bab96f23330979a463b97f 100644 (file)
@@ -24,10 +24,12 @@ import org.sonar.api.config.Configuration;
 import org.sonar.api.config.Settings;
 import org.sonar.api.config.internal.ConfigurationBridge;
 import org.sonar.db.DbClient;
+import org.sonar.db.component.BranchType;
 import org.sonar.server.computation.task.projectanalysis.analysis.Branch;
 import org.sonar.server.settings.ChildSettings;
 
 import static org.sonar.db.component.ComponentDto.generateBranchKey;
+import static org.sonar.db.component.ComponentDto.generatePullRequestKey;
 
 @ComputeEngineSide
 public class ProjectConfigurationFactory {
@@ -43,7 +45,11 @@ public class ProjectConfigurationFactory {
   public Configuration newProjectConfiguration(String projectKey, Branch branch) {
     Settings projectSettings = new ChildSettings(globalSettings);
     addSettings(projectSettings, projectKey);
-    addSettings(projectSettings, generateBranchKey(projectKey, branch.getName()));
+    if (branch.getType() == BranchType.PULL_REQUEST) {
+      addSettings(projectSettings, generatePullRequestKey(projectKey, branch.getPullRequestId()));
+    } else {
+      addSettings(projectSettings, generateBranchKey(projectKey, branch.getName()));
+    }
     return new ConfigurationBridge(projectSettings);
   }
 
index 08cf30bae79dd38471b0054baa45a3c66141bf68..0fbac10bdb522de5d53a293dd818496a816d1225 100644 (file)
@@ -55,7 +55,6 @@ import static org.sonar.api.measures.CoreMetrics.SECURITY_RATING_KEY;
 import static org.sonar.api.measures.CoreMetrics.SQALE_RATING_KEY;
 import static org.sonar.api.measures.CoreMetrics.TECHNICAL_DEBT_KEY;
 import static org.sonar.api.measures.CoreMetrics.VULNERABILITIES_KEY;
-import static org.sonar.api.measures.CoreMetrics.NCLOC_KEY;
 import static org.sonar.api.measures.Metric.Level;
 import static org.sonar.api.measures.Metric.ValueType;
 import static org.sonar.api.measures.Metric.Level.ERROR;
@@ -73,12 +72,14 @@ import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rat
 import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.valueOf;
 import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
 import static org.sonarqube.ws.MediaTypes.SVG;
 
 public class MeasureAction implements ProjectBadgesWsAction {
 
   private static final String PARAM_PROJECT = "project";
   private static final String PARAM_BRANCH = "branch";
+  private static final String PARAM_PULL_REQUEST = "pullRequest";
   private static final String PARAM_METRIC = "metric";
 
   private static final Map<String, String> METRIC_NAME_BY_KEY = ImmutableMap.<String, String>builder()
@@ -140,6 +141,10 @@ public class MeasureAction implements ProjectBadgesWsAction {
       .createParam(PARAM_BRANCH)
       .setDescription("Branch key")
       .setExampleValue(KEY_BRANCH_EXAMPLE_001);
+    action
+      .createParam(PARAM_PULL_REQUEST)
+      .setDescription("Pull request id")
+      .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001);
     action.createParam(PARAM_METRIC)
       .setDescription("Metric key")
       .setRequired(true)
@@ -151,9 +156,10 @@ public class MeasureAction implements ProjectBadgesWsAction {
     response.stream().setMediaType(SVG);
     String projectKey = request.mandatoryParam(PARAM_PROJECT);
     String branch = request.param(PARAM_BRANCH);
+    String pullRequest = request.param(PARAM_PULL_REQUEST);
     String metricKey = request.mandatoryParam(PARAM_METRIC);
     try (DbSession dbSession = dbClient.openSession(false)) {
-      ComponentDto project = componentFinder.getByKeyAndOptionalBranch(dbSession, projectKey, branch);
+      ComponentDto project = componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, projectKey, branch, pullRequest);
       userSession.checkComponentPermission(USER, project);
       MetricDto metric = dbClient.metricDao().selectByKey(dbSession, metricKey);
       checkState(metric != null && metric.isEnabled(), "Metric '%s' hasn't been found", metricKey);
index 7b969514073a6fdce2a84b67df120044b59c3761..bdf45ad78c6c686784674e2d783e4103e28d8c87 100644 (file)
@@ -41,12 +41,14 @@ import static org.apache.commons.io.IOUtils.write;
 import static org.sonar.api.web.UserRole.USER;
 import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
 import static org.sonarqube.ws.MediaTypes.SVG;
 
 public class QualityGateAction implements ProjectBadgesWsAction {
 
   private static final String PARAM_PROJECT = "project";
   private static final String PARAM_BRANCH = "branch";
+  private static final String PARAM_PULL_REQUEST = "pullRequest";
 
   private final UserSession userSession;
   private final DbClient dbClient;
@@ -76,6 +78,10 @@ public class QualityGateAction implements ProjectBadgesWsAction {
       .createParam(PARAM_BRANCH)
       .setDescription("Branch key")
       .setExampleValue(KEY_BRANCH_EXAMPLE_001);
+    action
+      .createParam(PARAM_PULL_REQUEST)
+      .setDescription("Pull request id")
+      .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001);
   }
 
   @Override
@@ -83,8 +89,9 @@ public class QualityGateAction implements ProjectBadgesWsAction {
     response.stream().setMediaType(SVG);
     String projectKey = request.mandatoryParam(PARAM_PROJECT);
     String branch = request.param(PARAM_BRANCH);
+    String pullRequest = request.param(PARAM_PULL_REQUEST);
     try (DbSession dbSession = dbClient.openSession(false)) {
-      ComponentDto project = componentFinder.getByKeyAndOptionalBranch(dbSession, projectKey, branch);
+      ComponentDto project = componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, projectKey, branch, pullRequest);
       userSession.checkComponentPermission(USER, project);
       Level qualityGateStatus = getQualityGate(dbSession, project);
       write(svgGenerator.generateQualityGate(qualityGateStatus), response.stream().output(), UTF_8);
index 0332472367f61008fc5e0f796f6533342ee242b2..a67d99799e143631e5b725a968d6493da2a02072 100644 (file)
@@ -33,6 +33,7 @@ import org.sonarqube.ws.Batch.WsProjectResponse.FileData.Builder;
 import static org.sonar.core.util.Protobuf.setNullable;
 import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
 
 public class ProjectAction implements BatchWsAction {
@@ -41,6 +42,7 @@ public class ProjectAction implements BatchWsAction {
   private static final String PARAM_PROFILE = "profile";
   private static final String PARAM_ISSUES_MODE = "issues_mode";
   private static final String PARAM_BRANCH = "branch";
+  private static final String PARAM_PULL_REQUEST = "pullRequest";
 
   private final ProjectDataLoader projectDataLoader;
 
@@ -79,6 +81,12 @@ public class ProjectAction implements BatchWsAction {
       .setSince("6.6")
       .setDescription("Branch key")
       .setExampleValue(KEY_BRANCH_EXAMPLE_001);
+
+    action
+      .createParam(PARAM_PULL_REQUEST)
+      .setSince("7.1")
+      .setDescription("Pull request id")
+      .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001);
   }
 
   @Override
@@ -87,7 +95,8 @@ public class ProjectAction implements BatchWsAction {
       .setModuleKey(wsRequest.mandatoryParam(PARAM_KEY))
       .setProfileName(wsRequest.param(PARAM_PROFILE))
       .setIssuesMode(wsRequest.mandatoryParamAsBoolean(PARAM_ISSUES_MODE))
-      .setBranch(wsRequest.param(PARAM_BRANCH)));
+      .setBranch(wsRequest.param(PARAM_BRANCH))
+      .setPullRequest(wsRequest.param(PARAM_PULL_REQUEST)));
 
     WsProjectResponse projectResponse = buildResponse(data);
     writeProtobuf(projectResponse, wsRequest, wsResponse);
index ea244900d6df249abdaeebd34e7049d7b98e995e..cdd7ec30ba1659399e4d26d1203fb331de64366a 100644 (file)
@@ -71,13 +71,15 @@ public class ProjectDataLoader {
       ProjectRepositories data = new ProjectRepositories();
       String moduleKey = query.getModuleKey();
       String branch = query.getBranch();
+      String pullRequest = query.getPullRequest();
       ComponentDto mainModule = componentFinder.getByKey(session, moduleKey);
       checkRequest(isProjectOrModule(mainModule), "Key '%s' belongs to a component which is not a Project", moduleKey);
       boolean hasScanPerm = userSession.hasComponentPermission(SCAN_EXECUTION, mainModule) ||
         userSession.hasPermission(OrganizationPermission.SCAN, mainModule.getOrganizationUuid());
       boolean hasBrowsePerm = userSession.hasComponentPermission(USER, mainModule);
       checkPermission(query.isIssuesMode(), hasScanPerm, hasBrowsePerm);
-      ComponentDto branchOrMainModule = branch == null ? mainModule : componentFinder.getByKeyAndBranch(session, moduleKey, branch);
+      ComponentDto branchOrMainModule = (branch == null && pullRequest == null) ? mainModule
+        : componentFinder.getByKeyAndOptionalBranchOrPullRequest(session, moduleKey, branch, pullRequest);
 
       ComponentDto project = getProject(branchOrMainModule, session);
       if (!project.getKey().equals(branchOrMainModule.getKey())) {
index 472be1f73c5b89f82407784322bbfe93d290bee0..952a7b99b28feed4fb8e3cdea6b61f56be57d11d 100644 (file)
@@ -28,6 +28,7 @@ public class ProjectDataQuery {
   private String profileName;
   private boolean issuesMode;
   private String branch;
+  private String pullRequest;
 
   private ProjectDataQuery() {
     // No direct call
@@ -71,6 +72,16 @@ public class ProjectDataQuery {
     return this;
   }
 
+  @CheckForNull
+  public String getPullRequest() {
+    return pullRequest;
+  }
+
+  public ProjectDataQuery setPullRequest(@Nullable String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
+
   public static ProjectDataQuery create() {
     return new ProjectDataQuery();
   }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/DeleteAction.java b/server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/DeleteAction.java
new file mode 100644 (file)
index 0000000..644b752
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.branch.pr.ws;
+
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.server.ws.WebService.NewController;
+import org.sonar.api.web.UserRole;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.component.ComponentCleanerService;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.user.UserSession;
+
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
+import static org.sonar.server.branch.pr.ws.PullRequestsWs.addProjectParam;
+import static org.sonar.server.branch.pr.ws.PullRequestsWs.addPullRequestParam;
+import static org.sonar.server.branch.pr.ws.PullRequestsWsParameters.PARAM_PROJECT;
+import static org.sonar.server.branch.pr.ws.PullRequestsWsParameters.PARAM_PULL_REQUEST;
+import static org.sonar.server.branch.ws.ProjectBranchesParameters.ACTION_DELETE;
+
+public class DeleteAction implements PullRequestWsAction {
+  private final DbClient dbClient;
+  private final UserSession userSession;
+  private final ComponentCleanerService componentCleanerService;
+  private final ComponentFinder componentFinder;
+
+  public DeleteAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession, ComponentCleanerService componentCleanerService) {
+    this.dbClient = dbClient;
+    this.componentFinder = componentFinder;
+    this.userSession = userSession;
+    this.componentCleanerService = componentCleanerService;
+  }
+
+  @Override
+  public void define(NewController context) {
+    WebService.NewAction action = context.createAction(ACTION_DELETE)
+      .setSince("7.1")
+      .setDescription("Delete a pull request.<br/>" +
+        "Requires 'Administer' rights on the specified project.")
+      .setPost(true)
+      .setHandler(this);
+
+    addProjectParam(action);
+    addPullRequestParam(action);
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    userSession.checkLoggedIn();
+    String projectKey = request.mandatoryParam(PARAM_PROJECT);
+    String pullRequestId = request.mandatoryParam(PARAM_PULL_REQUEST);
+
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      ComponentDto project = componentFinder.getRootComponentByUuidOrKey(dbSession, null, projectKey);
+      checkPermission(project);
+
+      BranchDto pullRequest = dbClient.branchDao().selectByPullRequestKey(dbSession, project.uuid(), pullRequestId)
+        .filter(branch -> branch.getBranchType() == PULL_REQUEST)
+        .orElseThrow(() -> new NotFoundException(String.format("Pull request '%s' is not found for project '%s'", pullRequestId, projectKey)));
+
+      ComponentDto branchComponent = componentFinder.getByKeyAndPullRequest(dbSession, projectKey, pullRequest.getKey());
+      componentCleanerService.deleteBranch(dbSession, branchComponent);
+      response.noContent();
+    }
+  }
+
+  private void checkPermission(ComponentDto project) {
+    userSession.checkComponentPermission(UserRole.ADMIN, project);
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/ListAction.java b/server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/ListAction.java
new file mode 100644 (file)
index 0000000..c6d63a1
--- /dev/null
@@ -0,0 +1,142 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.branch.pr.ws;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+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.api.web.UserRole;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.protobuf.DbProjectBranches;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.issue.index.BranchStatistics;
+import org.sonar.server.issue.index.IssueIndex;
+import org.sonar.server.user.UserSession;
+import org.sonarqube.ws.ProjectPullRequests;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+import static org.sonar.api.resources.Qualifiers.PROJECT;
+import static org.sonar.api.utils.DateUtils.formatDateTime;
+import static org.sonar.core.util.Protobuf.setNullable;
+import static org.sonar.core.util.stream.MoreCollectors.toList;
+import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
+import static org.sonar.server.branch.pr.ws.PullRequestsWs.addProjectParam;
+import static org.sonar.server.branch.pr.ws.PullRequestsWsParameters.PARAM_PROJECT;
+import static org.sonar.server.ws.WsUtils.writeProtobuf;
+
+public class ListAction implements PullRequestWsAction {
+
+  private final DbClient dbClient;
+  private final UserSession userSession;
+  private final ComponentFinder componentFinder;
+  private final IssueIndex issueIndex;
+
+  public ListAction(DbClient dbClient, UserSession userSession, ComponentFinder componentFinder, IssueIndex issueIndex) {
+    this.dbClient = dbClient;
+    this.userSession = userSession;
+    this.componentFinder = componentFinder;
+    this.issueIndex = issueIndex;
+  }
+
+  @Override
+  public void define(WebService.NewController context) {
+    WebService.NewAction action = context.createAction("list")
+      .setSince("7.1")
+      .setDescription("List the pull requests of a project.<br/>" +
+        "Requires 'Administer' rights on the specified project.")
+      .setResponseExample(getClass().getResource("list-example.json"))
+      .setHandler(this);
+
+    addProjectParam(action);
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    String projectKey = request.mandatoryParam(PARAM_PROJECT);
+
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      ComponentDto project = componentFinder.getByKey(dbSession, projectKey);
+      userSession.checkComponentPermission(UserRole.USER, project);
+      checkArgument(project.isEnabled() && PROJECT.equals(project.qualifier()), "Invalid project key");
+
+      List<BranchDto> pullRequests = dbClient.branchDao().selectByComponent(dbSession, project).stream()
+        .filter(b -> b.getBranchType() == PULL_REQUEST)
+        .collect(toList());
+      List<String> pullRequestUuids = pullRequests.stream().map(BranchDto::getUuid).collect(toList());
+
+      Map<String, BranchDto> mergeBranchesByUuid = dbClient.branchDao()
+        .selectByUuids(dbSession, pullRequests.stream().map(BranchDto::getMergeBranchUuid).filter(Objects::nonNull).collect(toList()))
+        .stream().collect(uniqueIndex(BranchDto::getUuid));
+      Map<String, BranchStatistics> branchStatisticsByBranchUuid = issueIndex.searchBranchStatistics(project.uuid(), pullRequestUuids).stream()
+        .collect(uniqueIndex(BranchStatistics::getBranchUuid, Function.identity()));
+      Map<String, String> analysisDateByBranchUuid = dbClient.snapshotDao().selectLastAnalysesByRootComponentUuids(dbSession, pullRequestUuids).stream()
+        .collect(uniqueIndex(SnapshotDto::getComponentUuid, s -> formatDateTime(s.getCreatedAt())));
+
+      ProjectPullRequests.ListWsResponse.Builder protobufResponse = ProjectPullRequests.ListWsResponse.newBuilder();
+      pullRequests
+        .forEach(b -> addPullRequest(protobufResponse, b, mergeBranchesByUuid, branchStatisticsByBranchUuid.get(b.getUuid()),
+          analysisDateByBranchUuid.get(b.getUuid())));
+      writeProtobuf(protobufResponse.build(), request, response);
+    }
+  }
+
+  private static void addPullRequest(ProjectPullRequests.ListWsResponse.Builder response, BranchDto branch, Map<String, BranchDto> mergeBranchesByUuid,
+                                     BranchStatistics branchStatistics, @Nullable String analysisDate) {
+    Optional<BranchDto> mergeBranch = Optional.ofNullable(mergeBranchesByUuid.get(branch.getMergeBranchUuid()));
+
+    ProjectPullRequests.PullRequest.Builder builder = ProjectPullRequests.PullRequest.newBuilder();
+    builder.setKey(branch.getKey());
+
+    DbProjectBranches.PullRequestData pullRequestData = requireNonNull(branch.getPullRequestData(), "Pull request data should be available for branch type PULL_REQUEST");
+    builder.setBranch(pullRequestData.getBranch());
+    builder.setUrl(pullRequestData.getUrl());
+    builder.setTitle(pullRequestData.getTitle());
+
+    if (mergeBranch.isPresent()) {
+      String mergeBranchKey = mergeBranch.get().getKey();
+      builder.setBase(mergeBranchKey);
+    } else {
+      builder.setIsOrphan(true);
+    }
+    setNullable(analysisDate, builder::setAnalysisDate);
+    setBranchStatus(builder, branchStatistics);
+    response.addPullRequests(builder);
+  }
+
+  private static void setBranchStatus(ProjectPullRequests.PullRequest.Builder builder, @Nullable BranchStatistics branchStatistics) {
+    ProjectPullRequests.Status.Builder statusBuilder = ProjectPullRequests.Status.newBuilder();
+    statusBuilder.setBugs(branchStatistics == null ? 0L : branchStatistics.getBugs());
+    statusBuilder.setVulnerabilities(branchStatistics == null ? 0L : branchStatistics.getVulnerabilities());
+    statusBuilder.setCodeSmells(branchStatistics == null ? 0L : branchStatistics.getCodeSmells());
+    builder.setStatus(statusBuilder);
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/PullRequestWsAction.java b/server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/PullRequestWsAction.java
new file mode 100644 (file)
index 0000000..2543d5d
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.branch.pr.ws;
+
+import org.sonar.server.ws.WsAction;
+
+public interface PullRequestWsAction extends WsAction {
+  // marker interface
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/PullRequestWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/PullRequestWsModule.java
new file mode 100644 (file)
index 0000000..35cc522
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.branch.pr.ws;
+
+import org.sonar.core.platform.Module;
+
+public class PullRequestWsModule extends Module {
+  @Override
+  protected void configureModule() {
+    add(
+      ListAction.class,
+      DeleteAction.class,
+      PullRequestsWs.class);
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/PullRequestsWs.java b/server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/PullRequestsWs.java
new file mode 100644 (file)
index 0000000..0e63e78
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.branch.pr.ws;
+
+import org.sonar.api.server.ws.WebService;
+
+import static java.util.Arrays.stream;
+import static org.sonar.server.branch.pr.ws.PullRequestsWsParameters.PARAM_PROJECT;
+import static org.sonar.server.branch.pr.ws.PullRequestsWsParameters.PARAM_PULL_REQUEST;
+import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+
+public class PullRequestsWs implements WebService {
+  private final PullRequestWsAction[] actions;
+
+  public PullRequestsWs(PullRequestWsAction... actions) {
+    this.actions = actions;
+  }
+
+  @Override
+  public void define(Context context) {
+    NewController controller = context.createController("api/project_pull_requests")
+      .setSince("7.1")
+      .setDescription("Manage pull request (only available when the Branch plugin is installed)");
+    stream(actions).forEach(action -> action.define(controller));
+    controller.done();
+  }
+
+  static void addProjectParam(NewAction action) {
+    action
+      .createParam(PARAM_PROJECT)
+      .setDescription("Project key")
+      .setExampleValue(KEY_PROJECT_EXAMPLE_001)
+      .setRequired(true);
+  }
+
+  static void addPullRequestParam(NewAction action) {
+    action
+      .createParam(PARAM_PULL_REQUEST)
+      .setDescription("Pull request id")
+      .setExampleValue("1543")
+      .setRequired(true);
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/PullRequestsWsParameters.java b/server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/PullRequestsWsParameters.java
new file mode 100644 (file)
index 0000000..809aef3
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.branch.pr.ws;
+
+public class PullRequestsWsParameters {
+
+  public static final String PARAM_PROJECT = "project";
+  public static final String PARAM_COMPONENT = "component";
+  public static final String PARAM_PULL_REQUEST = "pullRequest";
+
+  private PullRequestsWsParameters() {
+    // static utility class
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/branch/pr/ws/package-info.java
new file mode 100644 (file)
index 0000000..e96a6da
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.branch.pr.ws;
+
+import javax.annotation.ParametersAreNonnullByDefault;
index c4e34de75038bb4cc9a9d9458c4109b315fa3df1..cde6921b059603392308c3752fc9c40c1b0b07b2 100644 (file)
@@ -19,7 +19,6 @@
  */
 package org.sonar.server.branch.ws;
 
-import com.google.common.io.Resources;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
@@ -35,10 +34,10 @@ import org.sonar.server.user.UserSession;
 
 import static org.sonar.server.branch.ws.BranchesWs.addBranchParam;
 import static org.sonar.server.branch.ws.BranchesWs.addProjectParam;
-import static org.sonar.server.ws.WsUtils.checkFoundWithOptional;
 import static org.sonar.server.branch.ws.ProjectBranchesParameters.ACTION_DELETE;
 import static org.sonar.server.branch.ws.ProjectBranchesParameters.PARAM_BRANCH;
 import static org.sonar.server.branch.ws.ProjectBranchesParameters.PARAM_PROJECT;
+import static org.sonar.server.ws.WsUtils.checkFoundWithOptional;
 
 public class DeleteAction implements BranchWsAction {
   private final DbClient dbClient;
@@ -59,7 +58,6 @@ public class DeleteAction implements BranchWsAction {
       .setSince("6.6")
       .setDescription("Delete a non-main branch of a project.<br/>" +
         "Requires 'Administer' rights on the specified project.")
-      .setResponseExample(Resources.getResource(getClass(), "list-example.json"))
       .setPost(true)
       .setHandler(this);
 
@@ -78,7 +76,7 @@ public class DeleteAction implements BranchWsAction {
       checkPermission(project);
 
       BranchDto branch = checkFoundWithOptional(
-        dbClient.branchDao().selectByKey(dbSession, project.uuid(), branchKey),
+        dbClient.branchDao().selectByBranchKey(dbSession, project.uuid(), branchKey),
         "Branch '%s' not found for project '%s'", branchKey, projectKey);
 
       if (branch.isMain()) {
index 4583eb92be9efb734637c7827abb9e05f5d080ba..472a2a4175f4ab9cddea10fdaff6fc6c0e478ea9 100644 (file)
@@ -21,17 +21,18 @@ package org.sonar.server.branch.ws;
 
 import com.google.common.io.Resources;
 import java.util.Collection;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.function.Function;
-import java.util.stream.Collectors;
 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.api.web.UserRole;
 import org.sonar.core.util.Protobuf;
+import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.BranchDto;
@@ -99,30 +100,34 @@ public class ListAction implements BranchWsAction {
       checkPermission(project);
       checkArgument(project.isEnabled() && PROJECT.equals(project.qualifier()), "Invalid project key");
 
-      Collection<BranchDto> branches = dbClient.branchDao().selectByComponent(dbSession, project);
+      Collection<BranchDto> branches = dbClient.branchDao().selectByComponent(dbSession, project).stream()
+        .filter(b -> b.getBranchType() == SHORT || b.getBranchType() == LONG)
+        .collect(MoreCollectors.toList());
+      List<String> branchUuids = branches.stream().map(BranchDto::getUuid).collect(toList());
+
       Map<String, BranchDto> mergeBranchesByUuid = dbClient.branchDao()
         .selectByUuids(dbSession, branches.stream().map(BranchDto::getMergeBranchUuid).filter(Objects::nonNull).collect(toList()))
         .stream().collect(uniqueIndex(BranchDto::getUuid));
       Map<String, LiveMeasureDto> qualityGateMeasuresByComponentUuids = dbClient.liveMeasureDao()
-        .selectByComponentUuidsAndMetricKeys(dbSession, branches.stream().map(BranchDto::getUuid).collect(toList()), singletonList(ALERT_STATUS_KEY))
-        .stream().collect(uniqueIndex(LiveMeasureDto::getComponentUuid));
+        .selectByComponentUuidsAndMetricKeys(dbSession, branchUuids, singletonList(ALERT_STATUS_KEY)).stream()
+        .collect(uniqueIndex(LiveMeasureDto::getComponentUuid));
       Map<String, BranchStatistics> branchStatisticsByBranchUuid = issueIndex.searchBranchStatistics(project.uuid(), branches.stream()
         .filter(b -> b.getBranchType().equals(SHORT))
-        .map(BranchDto::getUuid).collect(toList()))
-        .stream().collect(uniqueIndex(BranchStatistics::getBranchUuid, Function.identity()));
+        .map(BranchDto::getUuid).collect(toList())).stream()
+        .collect(uniqueIndex(BranchStatistics::getBranchUuid, Function.identity()));
       Map<String, String> analysisDateByBranchUuid = dbClient.snapshotDao()
-        .selectLastAnalysesByRootComponentUuids(dbSession, branches.stream().map(BranchDto::getUuid).collect(Collectors.toList()))
-        .stream().collect(uniqueIndex(SnapshotDto::getComponentUuid, s -> formatDateTime(s.getCreatedAt())));
+        .selectLastAnalysesByRootComponentUuids(dbSession, branchUuids).stream()
+        .collect(uniqueIndex(SnapshotDto::getComponentUuid, s -> formatDateTime(s.getCreatedAt())));
 
       ProjectBranches.ListWsResponse.Builder protobufResponse = ProjectBranches.ListWsResponse.newBuilder();
       branches.forEach(b -> addBranch(protobufResponse, b, mergeBranchesByUuid, qualityGateMeasuresByComponentUuids.get(b.getUuid()), branchStatisticsByBranchUuid.get(b.getUuid()),
-          analysisDateByBranchUuid.get(b.getUuid())));
+        analysisDateByBranchUuid.get(b.getUuid())));
       WsUtils.writeProtobuf(protobufResponse.build(), request, response);
     }
   }
 
   private static void addBranch(ProjectBranches.ListWsResponse.Builder response, BranchDto branch, Map<String, BranchDto> mergeBranchesByUuid,
-                                @Nullable LiveMeasureDto qualityGateMeasure, BranchStatistics branchStatistics, @Nullable String analysisDate) {
+    @Nullable LiveMeasureDto qualityGateMeasure, BranchStatistics branchStatistics, @Nullable String analysisDate) {
     ProjectBranches.Branch.Builder builder = toBranchBuilder(branch, Optional.ofNullable(mergeBranchesByUuid.get(branch.getMergeBranchUuid())));
     setBranchStatus(builder, branch, qualityGateMeasure, branchStatistics);
     if (analysisDate != null) {
@@ -137,7 +142,7 @@ public class ListAction implements BranchWsAction {
     setNullable(branchKey, builder::setName);
     builder.setIsMain(branch.isMain());
     builder.setType(Common.BranchType.valueOf(branch.getBranchType().name()));
-    if (branch.getBranchType().equals(SHORT)) {
+    if (branch.getBranchType() == SHORT) {
       if (mergeBranch.isPresent()) {
         String mergeBranchKey = mergeBranch.get().getKey();
         builder.setMergeBranch(mergeBranchKey);
@@ -149,8 +154,8 @@ public class ListAction implements BranchWsAction {
   }
 
   private static void setBranchStatus(ProjectBranches.Branch.Builder builder, BranchDto branch, @Nullable LiveMeasureDto qualityGateMeasure,
-                                      @Nullable BranchStatistics branchStatistics) {
-    ProjectBranches.Branch.Status.Builder statusBuilder = ProjectBranches.Branch.Status.newBuilder();
+    @Nullable BranchStatistics branchStatistics) {
+    ProjectBranches.Status.Builder statusBuilder = ProjectBranches.Status.newBuilder();
     if (branch.getBranchType() == LONG && qualityGateMeasure != null) {
       Protobuf.setNullable(qualityGateMeasure.getDataAsString(), statusBuilder::setQualityGateStatus);
     }
index fa433fbec57ec505b1a6d086739129730088609d..b2efad6dacf31d1dd61a05f8a36a32cf35ccab8a 100644 (file)
@@ -77,7 +77,7 @@ public class RenameAction implements BranchWsAction {
       ComponentDto project = componentFinder.getRootComponentByUuidOrKey(dbSession, null, projectKey);
       checkPermission(project);
 
-      Optional<BranchDto> existingBranch = dbClient.branchDao().selectByKey(dbSession, project.uuid(), newBranchName);
+      Optional<BranchDto> existingBranch = dbClient.branchDao().selectByBranchKey(dbSession, project.uuid(), newBranchName);
       checkArgument(!existingBranch.filter(b -> !b.isMain()).isPresent(),
         "Impossible to update branch name: a branch with name \"%s\" already exists in the project.", newBranchName);
 
index 0d8bf400b831e3df0582c58148f2a2988efc834b..dcbe6290f974809eaf9cd614bf6feaa422124fb0 100644 (file)
@@ -100,7 +100,8 @@ public class ActivityAction implements CeWsAction {
       .setChangelog(
         new Change("5.5", "it's no more possible to specify the page parameter."),
         new Change("6.1", "field \"logs\" is deprecated and its value is always false"),
-        new Change("6.6", "fields \"branch\" and \"branchType\" added"))
+        new Change("6.6", "fields \"branch\" and \"branchType\" added"),
+        new Change("7.1", "fields \"pullRequest\" and \"pullRequestTitle\" added"))
       .setSince("5.2");
 
     action.createParam(PARAM_COMPONENT_ID)
index fe4a1e3bc685413273406a27e70f467fdf6203c0..23f1ba3e091d98e58f90cd7a69e7663f870b4762 100644 (file)
@@ -89,7 +89,7 @@ public class TaskFormatter {
     builder.setSubmittedAt(formatDateTime(new Date(dto.getCreatedAt())));
     setNullable(dto.getStartedAt(), builder::setStartedAt, DateUtils::formatDateTime);
     setNullable(computeExecutionTimeMs(dto), builder::setExecutionTimeMs);
-    setBranch(builder, dto.getUuid(), componentDtoCache);
+    setBranchOrPullRequest(builder, dto.getUuid(), componentDtoCache);
     return builder.build();
   }
 
@@ -115,10 +115,8 @@ public class TaskFormatter {
     builder.setLogs(false);
     setNullable(dto.getComponentUuid(), uuid -> setComponent(builder, uuid, componentDtoCache).setComponentId(uuid));
     String analysisUuid = dto.getAnalysisUuid();
-    if (analysisUuid != null) {
-      builder.setAnalysisId(analysisUuid);
-    }
-    setBranch(builder, dto.getUuid(), componentDtoCache);
+    setNullable(analysisUuid, builder::setAnalysisId);
+    setBranchOrPullRequest(builder, dto.getUuid(), componentDtoCache);
     setNullable(analysisUuid, builder::setAnalysisId);
     setNullable(dto.getSubmitterLogin(), builder::setSubmitterLogin);
     builder.setSubmittedAt(formatDateTime(new Date(dto.getSubmittedAt())));
@@ -144,13 +142,22 @@ public class TaskFormatter {
     return builder;
   }
 
-  private static Ce.Task.Builder setBranch(Ce.Task.Builder builder, String taskUuid, DtoCache componentDtoCache) {
-    componentDtoCache.getBranchName(taskUuid).ifPresent(
+  private static Ce.Task.Builder setBranchOrPullRequest(Ce.Task.Builder builder, String taskUuid, DtoCache componentDtoCache) {
+    componentDtoCache.getBranchKey(taskUuid).ifPresent(
       b -> {
-        builder.setBranch(b);
-        builder.setBranchType(componentDtoCache.getBranchType(taskUuid)
-          .orElseThrow(() -> new IllegalStateException(format("Could not find branch type of task '%s'", taskUuid))));
+        Common.BranchType branchType = componentDtoCache.getBranchType(taskUuid)
+          .orElseThrow(() -> new IllegalStateException(format("Could not find branch type of task '%s'", taskUuid)));
+        switch (branchType) {
+          case LONG:
+          case SHORT:
+            builder.setBranchType(branchType);
+            builder.setBranch(b);
+            break;
+          default:
+            throw new IllegalStateException(String.format("Unknown branch type '%s'", branchType));
+        }
       });
+    componentDtoCache.getPullRequest(taskUuid).ifPresent(builder::setPullRequest);
     return builder;
   }
 
@@ -237,7 +244,7 @@ public class TaskFormatter {
       return organizationDto.getKey();
     }
 
-    Optional<String> getBranchName(String taskUuid) {
+    Optional<String> getBranchKey(String taskUuid) {
       return characteristicsByTaskUuid.get(taskUuid).stream()
         .filter(c -> c.getKey().equals(CeTaskCharacteristicDto.BRANCH_KEY))
         .map(CeTaskCharacteristicDto::getValue)
@@ -250,6 +257,13 @@ public class TaskFormatter {
         .map(c -> Common.BranchType.valueOf(c.getValue()))
         .findAny();
     }
+
+    Optional<String> getPullRequest(String taskUuid) {
+      return characteristicsByTaskUuid.get(taskUuid).stream()
+        .filter(c -> c.getKey().equals(CeTaskCharacteristicDto.PULL_REQUEST))
+        .map(CeTaskCharacteristicDto::getValue)
+        .findAny();
+    }
   }
 
   /**
index 5a2568bdd7ca780cfbf59b51ad56e9fa62bc3453..a1e6d94c6289a6742911bf5546646f8173ccb6fd 100644 (file)
@@ -154,8 +154,24 @@ public class ComponentFinder {
     throw new NotFoundException(format("Component '%s' on branch '%s' not found", key, branch));
   }
 
-  public ComponentDto getByKeyAndOptionalBranch(DbSession dbSession, String key, @Nullable String branch) {
-    return branch == null ? getByKey(dbSession, key) : getByKeyAndBranch(dbSession, key, branch);
+  public ComponentDto getByKeyAndPullRequest(DbSession dbSession, String key, String pullRequest) {
+    java.util.Optional<ComponentDto> componentDto = dbClient.componentDao().selectByKeyAndPullRequest(dbSession, key, pullRequest);
+    if (componentDto.isPresent() && componentDto.get().isEnabled()) {
+      return componentDto.get();
+    }
+    throw new NotFoundException(format("Component '%s' of pull request '%s' not found", key, pullRequest));
+  }
+
+  public ComponentDto getByKeyAndOptionalBranchOrPullRequest(DbSession dbSession, String key, @Nullable String branch, @Nullable String pullRequest) {
+    checkArgument(branch == null || pullRequest == null, "Either branch or pull request can be provided, not both");
+    if (branch != null) {
+      return getByKeyAndBranch(dbSession, key, branch);
+    }
+    if (pullRequest != null) {
+      return getByKeyAndPullRequest(dbSession, key, pullRequest);
+    }
+
+    return getByKey(dbSession, key);
   }
 
   public enum ParamNames {
@@ -165,7 +181,7 @@ public class ComponentFinder {
     UUID_AND_KEY("uuid", "key"),
     ID_AND_KEY("id", "key"),
     COMPONENT_ID_AND_KEY("componentId", "componentKey"),
-    BASE_COMPONENT_ID_AND_KEY("baseComponentId", "baseComponentKey"),
+    BASE_COMPONENT_ID_AND_KEY("baseComponentId", "component"),
     DEVELOPER_ID_AND_KEY("developerId", "developerKey"),
     COMPONENT_ID_AND_COMPONENT("componentId", "component"),
     PROJECT_ID_AND_PROJECT("projectId", "project"),
index 562ffd31f7a636b9a6928ba305d497209b37bfce..f9ca3b8e5567d7ba18ed5adc1b6de63d507eaea7 100644 (file)
@@ -57,9 +57,11 @@ import static org.sonar.api.measures.CoreMetrics.VIOLATIONS;
 import static org.sonar.api.measures.CoreMetrics.VIOLATIONS_KEY;
 import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01;
 import static org.sonar.server.component.ComponentFinder.ParamNames.COMPONENT_ID_AND_COMPONENT;
+import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_BRANCH;
+import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_PULL_REQUEST;
 import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
-import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_BRANCH;
+import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
 
 public class AppAction implements ComponentsWsAction {
 
@@ -110,6 +112,12 @@ public class AppAction implements ComponentsWsAction {
       .setSince("6.6")
       .setInternal(true)
       .setExampleValue(KEY_BRANCH_EXAMPLE_001);
+
+    action.createParam(PARAM_PULL_REQUEST)
+      .setDescription("Pull request id")
+      .setSince("7.1")
+      .setInternal(true)
+      .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001);
   }
 
   @Override
@@ -131,12 +139,14 @@ public class AppAction implements ComponentsWsAction {
 
   private ComponentDto loadComponent(DbSession dbSession, Request request) {
     String componentUuid = request.param(PARAM_COMPONENT_ID);
-    String branch = request.param("branch");
-    checkArgument(componentUuid == null || branch == null, "'%s' and '%s' parameters cannot be used at the same time", PARAM_COMPONENT_ID, PARAM_BRANCH);
-    if (branch == null) {
+    String branch = request.param(PARAM_BRANCH);
+    String pullRequest = request.param(PARAM_PULL_REQUEST);
+    checkArgument(componentUuid == null || (branch == null && pullRequest == null), "Parameter '%s' cannot be used at the same time as '%s' or '%s'", PARAM_COMPONENT_ID,
+      PARAM_BRANCH, PARAM_PULL_REQUEST);
+    if (branch == null && pullRequest == null) {
       return componentFinder.getByUuidOrKey(dbSession, componentUuid, request.param(PARAM_COMPONENT), COMPONENT_ID_AND_COMPONENT);
     }
-    return componentFinder.getByKeyAndOptionalBranch(dbSession, request.mandatoryParam(PARAM_COMPONENT), branch);
+    return componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, request.mandatoryParam(PARAM_COMPONENT), branch, pullRequest);
   }
 
   private void appendComponent(JsonWriter json, ComponentDto component, UserSession userSession, DbSession session) {
@@ -168,6 +178,10 @@ public class AppAction implements ComponentsWsAction {
     if (branch != null) {
       json.prop("branch", branch);
     }
+    String pullRequest = project.getPullRequest();
+    if (pullRequest != null) {
+      json.prop("pullRequest", pullRequest);
+    }
 
     json.prop("fav", isFavourite);
   }
index b03f5d26d1dd88fa9d0cad0bd3238ce1dcc159b1..5adde10f4f4dd64bde42b0f593c7607dce23c83f 100644 (file)
@@ -62,6 +62,7 @@ class ComponentDtoToWsComponent {
       .setName(dto.name())
       .setQualifier(dto.qualifier());
     setNullable(emptyToNull(dto.getBranch()), wsComponent::setBranch);
+    setNullable(emptyToNull(dto.getPullRequest()), wsComponent::setPullRequest);
     setNullable(emptyToNull(dto.path()), wsComponent::setPath);
     setNullable(emptyToNull(dto.description()), wsComponent::setDescription);
     setNullable(emptyToNull(dto.language()), wsComponent::setLanguage);
index d101548ab7418c148d885cc411f0284f6f447867..19de6f1494780a5bc919b8075aa70de90e0497f2 100644 (file)
@@ -35,6 +35,7 @@ public class MeasuresWsParameters {
   public static final String DEPRECATED_PARAM_BASE_COMPONENT_KEY = "baseComponentKey";
   public static final String PARAM_COMPONENT = "component";
   public static final String PARAM_BRANCH = "branch";
+  public static final String PARAM_PULL_REQUEST = "pullRequest";
   public static final String PARAM_STRATEGY = "strategy";
   public static final String PARAM_QUALIFIERS = "qualifiers";
   public static final String PARAM_METRICS = "metrics";
index 92631450f7f5d5f1290b83297cbaeccf2051e080..6da99f6b35333562e72ebf5b601837d120828da2 100644 (file)
@@ -22,6 +22,8 @@ package org.sonar.server.component.ws;
 import java.util.List;
 import java.util.Optional;
 import java.util.stream.IntStream;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
 import org.sonar.api.server.ws.Change;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
@@ -35,21 +37,21 @@ import org.sonar.server.component.ComponentFinder;
 import org.sonar.server.user.UserSession;
 import org.sonarqube.ws.Components.ShowWsResponse;
 
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
-
 import static com.google.common.base.Preconditions.checkArgument;
 import static java.lang.String.format;
 import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01;
 import static org.sonar.server.component.ComponentFinder.ParamNames.COMPONENT_ID_AND_COMPONENT;
 import static org.sonar.server.component.ws.ComponentDtoToWsComponent.componentDtoToWsComponent;
+import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_BRANCH;
+import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_PULL_REQUEST;
 import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
+import static org.sonar.server.ws.WsUtils.checkRequest;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
 import static org.sonarqube.ws.client.component.ComponentsWsParameters.ACTION_SHOW;
 import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_COMPONENT;
 import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_COMPONENT_ID;
-import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_BRANCH;
 
 public class ShowAction implements ComponentsWsAction {
   private final UserSession userSession;
@@ -97,6 +99,12 @@ public class ShowAction implements ComponentsWsAction {
       .setExampleValue(KEY_BRANCH_EXAMPLE_001)
       .setInternal(true)
       .setSince("6.6");
+
+    action.createParam(PARAM_PULL_REQUEST)
+      .setDescription("Pull request id")
+      .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001)
+      .setInternal(true)
+      .setSince("7.1");
   }
 
   @Override
@@ -110,6 +118,7 @@ public class ShowAction implements ComponentsWsAction {
   private ShowWsResponse doHandle(Request request) {
     try (DbSession dbSession = dbClient.openSession(false)) {
       ComponentDto component = loadComponent(dbSession, request);
+      userSession.checkComponentPermission(UserRole.USER, component);
       Optional<SnapshotDto> lastAnalysis = dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, component.projectUuid());
       List<ComponentDto> ancestors = dbClient.componentDao().selectAncestors(dbSession, component);
       OrganizationDto organizationDto = componentFinder.getOrganization(dbSession, component);
@@ -121,12 +130,14 @@ public class ShowAction implements ComponentsWsAction {
     String componentId = request.getId();
     String componentKey = request.getKey();
     String branch = request.getBranch();
-    checkArgument(componentId == null || branch == null, "'%s' and '%s' parameters cannot be used at the same time", PARAM_COMPONENT_ID, PARAM_BRANCH);
-    ComponentDto component = branch == null
-      ? componentFinder.getByUuidOrKey(dbSession, componentId, componentKey, COMPONENT_ID_AND_COMPONENT)
-      : componentFinder.getByKeyAndBranch(dbSession, componentKey, branch);
-    userSession.checkComponentPermission(UserRole.USER, component);
-    return component;
+    String pullRequest = request.getPullRequest();
+    checkArgument(componentId == null || (branch == null && pullRequest == null), "Parameter '%s' cannot be used at the same time as '%s' or '%s'", PARAM_COMPONENT_ID,
+      PARAM_BRANCH, PARAM_PULL_REQUEST);
+    if (branch == null && pullRequest == null) {
+      return componentFinder.getByUuidOrKey(dbSession, componentId, componentKey, COMPONENT_ID_AND_COMPONENT);
+    }
+    checkRequest(componentKey!=null, "The '%s' parameter is missing", PARAM_COMPONENT);
+    return componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, componentKey, branch, pullRequest);
   }
 
   private static ShowWsResponse buildResponse(ComponentDto component, OrganizationDto organizationDto, List<ComponentDto> orderedAncestors, Optional<SnapshotDto> lastAnalysis) {
@@ -144,13 +155,15 @@ public class ShowAction implements ComponentsWsAction {
     return new Request()
       .setId(request.param(PARAM_COMPONENT_ID))
       .setKey(request.param(PARAM_COMPONENT))
-      .setBranch(request.param(PARAM_BRANCH));
+      .setBranch(request.param(PARAM_BRANCH))
+      .setPullRequest(request.param(PARAM_PULL_REQUEST));
   }
 
   private static class Request {
     private String id;
     private String key;
     private String branch;
+    private String pullRequest;
 
     @CheckForNull
     public String getId() {
@@ -181,5 +194,15 @@ public class ShowAction implements ComponentsWsAction {
       this.branch = branch;
       return this;
     }
+
+    @CheckForNull
+    public String getPullRequest() {
+      return pullRequest;
+    }
+
+    public Request setPullRequest(@Nullable String pullRequest) {
+      this.pullRequest = pullRequest;
+      return this;
+    }
   }
 }
index 5187b0cc797aae4af4b99b417a058fbd9aaac83a..ef86ba3c9c77a8f48fb5bc2b9e1f40238fbc164f 100644 (file)
@@ -66,15 +66,18 @@ import static org.sonar.server.component.ComponentFinder.ParamNames.COMPONENT_ID
 import static org.sonar.server.component.ws.ComponentDtoToWsComponent.componentDtoToWsComponent;
 import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
-import static org.sonar.server.ws.WsParameterBuilder.createQualifiersParameter;
+import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
 import static org.sonar.server.ws.WsParameterBuilder.QualifierParameterContext.newQualifierParameterContext;
+import static org.sonar.server.ws.WsParameterBuilder.createQualifiersParameter;
+import static org.sonar.server.ws.WsUtils.checkRequest;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
 import static org.sonarqube.ws.client.component.ComponentsWsParameters.ACTION_TREE;
+import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_BRANCH;
 import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_COMPONENT;
 import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_COMPONENT_ID;
+import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_PULL_REQUEST;
 import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_QUALIFIERS;
 import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_STRATEGY;
-import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_BRANCH;
 
 public class TreeAction implements ComponentsWsAction {
 
@@ -138,6 +141,12 @@ public class TreeAction implements ComponentsWsAction {
       .setInternal(true)
       .setSince("6.6");
 
+    action.createParam(PARAM_PULL_REQUEST)
+      .setDescription("Pull request id")
+      .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001)
+      .setInternal(true)
+      .setSince("7.1");
+
     action.createSortParams(SORTS, NAME_SORT, true)
       .setDescription("Comma-separated list of sort fields")
       .setExampleValue(NAME_SORT + ", " + PATH_SORT);
@@ -190,12 +199,16 @@ public class TreeAction implements ComponentsWsAction {
 
   private ComponentDto loadComponent(DbSession dbSession, Request request) {
     String componentId = request.getBaseComponentId();
-    String componentKey = request.getBaseComponentKey();
+    String componentKey = request.getComponent();
     String branch = request.getBranch();
-    checkArgument(componentId == null || branch == null, "'%s' and '%s' parameters cannot be used at the same time", PARAM_COMPONENT_ID, PARAM_BRANCH);
-    return branch == null
-      ? componentFinder.getByUuidOrKey(dbSession, componentId, componentKey, COMPONENT_ID_AND_COMPONENT)
-      : componentFinder.getByKeyAndBranch(dbSession, componentKey, branch);
+    String pullRequest = request.getPullRequest();
+    checkArgument(componentId == null || (branch == null && pullRequest == null), "Parameter '%s' cannot be used at the same time as '%s' or '%s'", PARAM_COMPONENT_ID,
+      PARAM_BRANCH, PARAM_PULL_REQUEST);
+    if (branch == null && pullRequest == null) {
+      return componentFinder.getByUuidOrKey(dbSession, componentId, componentKey, COMPONENT_ID_AND_COMPONENT);
+    }
+    checkRequest(componentKey != null, "The '%s' parameter is missing", PARAM_COMPONENT);
+    return componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, componentKey, branch, pullRequest);
   }
 
   private Map<String, ComponentDto> searchReferenceComponentsByUuid(DbSession dbSession, List<ComponentDto> components) {
@@ -285,8 +298,9 @@ public class TreeAction implements ComponentsWsAction {
   private static Request toTreeWsRequest(org.sonar.api.server.ws.Request request) {
     return new Request()
       .setBaseComponentId(request.param(PARAM_COMPONENT_ID))
-      .setBaseComponentKey(request.param(PARAM_COMPONENT))
+      .setComponent(request.param(PARAM_COMPONENT))
       .setBranch(request.param(PARAM_BRANCH))
+      .setPullRequest(request.param(PARAM_PULL_REQUEST))
       .setStrategy(request.mandatoryParam(PARAM_STRATEGY))
       .setQuery(request.param(Param.TEXT_QUERY))
       .setQualifiers(request.paramAsStrings(PARAM_QUALIFIERS))
@@ -335,9 +349,9 @@ public class TreeAction implements ComponentsWsAction {
 
   private static class Request {
     private String baseComponentId;
-    private String baseComponentKey;
     private String component;
     private String branch;
+    private String pullRequest;
     private String strategy;
     private List<String> qualifiers;
     private String query;
@@ -364,24 +378,6 @@ public class TreeAction implements ComponentsWsAction {
       return this;
     }
 
-    /**
-     * @deprecated since 6.4, please use {@link #getComponent()} instead
-     */
-    @Deprecated
-    @CheckForNull
-    private String getBaseComponentKey() {
-      return baseComponentKey;
-    }
-
-    /**
-     * @deprecated since 6.4, please use {@link #setComponent(String)} instead
-     */
-    @Deprecated
-    private Request setBaseComponentKey(@Nullable String baseComponentKey) {
-      this.baseComponentKey = baseComponentKey;
-      return this;
-    }
-
     public Request setComponent(@Nullable String component) {
       this.component = component;
       return this;
@@ -402,6 +398,16 @@ public class TreeAction implements ComponentsWsAction {
       return this;
     }
 
+    @CheckForNull
+    public String getPullRequest() {
+      return pullRequest;
+    }
+
+    public Request setPullRequest(@Nullable String pullRequest) {
+      this.pullRequest = pullRequest;
+      return this;
+    }
+
     @CheckForNull
     private String getStrategy() {
       return strategy;
index 97159e9875d9ad41d0b8fc8db477e1545c894aa4..77738cc6ffb810762974abc93f6359d7d76d7119 100644 (file)
@@ -82,6 +82,13 @@ public interface AnalysisMetadataHolder {
    */
   boolean isLongLivingBranch();
 
+  /**
+   * Convenience method equivalent to do the check using {@link #getBranch()}
+   *
+   * @throws IllegalStateException if branch has not been set
+   */
+  boolean isPullRequest();
+
   /**
    * @throws IllegalStateException if cross project duplication flag has not been set
    */
@@ -92,6 +99,13 @@ public interface AnalysisMetadataHolder {
    */
   Branch getBranch();
 
+  /**
+   * In a pull request analysis, return the ID of the pull request
+   *
+   * @throws IllegalStateException if current analysis is not a pull request
+   */
+  String getPullRequestId();
+
   /**
    * The project as represented by the main branch. It is used to load settings
    * like Quality gates, webhooks and configuration.
@@ -119,5 +133,4 @@ public interface AnalysisMetadataHolder {
    * Plugins used during the analysis on scanner side
    */
   Map<String, ScannerPlugin> getScannerPluginsByKey();
-
 }
index c26f123efdf842e63b74cef5e6d17b755f2bd1ab..6fee4c5f70c08e4b52d384d8e1483254848d23ad 100644 (file)
@@ -39,6 +39,7 @@ public class AnalysisMetadataHolderImpl implements MutableAnalysisMetadataHolder
   private final InitializedProperty<Analysis> baseProjectSnapshot = new InitializedProperty<>();
   private final InitializedProperty<Boolean> crossProjectDuplicationEnabled = new InitializedProperty<>();
   private final InitializedProperty<Branch> branch = new InitializedProperty<>();
+  private final InitializedProperty<String> pullRequestId = new InitializedProperty<>();
   private final InitializedProperty<Project> project = new InitializedProperty<>();
   private final InitializedProperty<Integer> rootComponentRef = new InitializedProperty<>();
   private final InitializedProperty<Map<String, QualityProfile>> qProfilesPerLanguage = new InitializedProperty<>();
@@ -148,6 +149,19 @@ public class AnalysisMetadataHolderImpl implements MutableAnalysisMetadataHolder
     return branch.getProperty();
   }
 
+  @Override
+  public MutableAnalysisMetadataHolder setPullRequestId(String pullRequestId) {
+    checkState(!this.pullRequestId.isInitialized(), "Pull request id has already been set");
+    this.pullRequestId.setProperty(pullRequestId);
+    return this;
+  }
+
+  @Override
+  public String getPullRequestId() {
+    checkState(pullRequestId.isInitialized(), "Pull request id has not been set");
+    return pullRequestId.getProperty();
+  }
+
   @Override
   public MutableAnalysisMetadataHolder setProject(Project project) {
     checkState(!this.project.isInitialized(), "Project has already been set");
@@ -201,16 +215,25 @@ public class AnalysisMetadataHolderImpl implements MutableAnalysisMetadataHolder
     return pluginsByKey.getProperty();
   }
 
+  @Override
   public boolean isShortLivingBranch() {
     checkState(this.branch.isInitialized(), BRANCH_NOT_SET);
     Branch prop = branch.getProperty();
     return prop != null && prop.getType() == BranchType.SHORT;
   }
 
+  @Override
   public boolean isLongLivingBranch() {
     checkState(this.branch.isInitialized(), BRANCH_NOT_SET);
     Branch prop = branch.getProperty();
     return prop != null && prop.getType() == BranchType.LONG;
   }
 
+  @Override
+  public boolean isPullRequest() {
+    checkState(this.branch.isInitialized(), BRANCH_NOT_SET);
+    Branch prop = branch.getProperty();
+    return prop != null && prop.getType() == BranchType.PULL_REQUEST;
+  }
+
 }
index f3ebcc87e80c321f25356a872fd9ed5fd73bc8d6..bc7fff0f769a49d87cccb70d8f3f1027b50ed605 100644 (file)
@@ -53,4 +53,9 @@ public interface Branch extends ComponentKeyGenerator {
    * or not.
    */
   boolean supportsCrossProjectCpd();
+
+  /**
+   * @throws IllegalStateException if this branch configuration is not a pull request.
+   */
+  String getPullRequestId();
 }
index b4e4e541cfd8f8f202196cef66ed9d16ae560162..09acc83d648320be924d3249f8b4058d2f1a2015 100644 (file)
@@ -60,6 +60,11 @@ public interface MutableAnalysisMetadataHolder extends AnalysisMetadataHolder {
    */
   MutableAnalysisMetadataHolder setBranch(Branch branch);
 
+  /**
+   * @throws IllegalStateException if pull request id has already been set
+   */
+  MutableAnalysisMetadataHolder setPullRequestId(String pullRequestId);
+
   /**
    * @throws IllegalStateException if project has already been set
    */
index 8af751c52bc781fc594dd90617a425540ad27670..e7f179b283c6d101b24d182bd898c180fb9191c2 100644 (file)
@@ -54,6 +54,7 @@ import static java.util.Optional.of;
 import static java.util.Optional.ofNullable;
 import static org.sonar.api.ce.posttask.CeTask.Status.FAILED;
 import static org.sonar.api.ce.posttask.CeTask.Status.SUCCESS;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
 
 /**
  * Responsible for calling {@link PostProjectAnalysisTask} implementations (if any).
@@ -180,7 +181,8 @@ public class PostProjectAnalysisTasksExecutor implements ComputationStepExecutor
   private BranchImpl createBranch() {
     org.sonar.server.computation.task.projectanalysis.analysis.Branch analysisBranch = analysisMetadataHolder.getBranch();
     if (!analysisBranch.isLegacyFeature()) {
-      return new BranchImpl(analysisBranch.isMain(), analysisBranch.getName(), Branch.Type.valueOf(analysisBranch.getType().name()));
+      String branchKey = analysisBranch.getType() == PULL_REQUEST ? analysisBranch.getPullRequestId() : analysisBranch.getName();
+      return new BranchImpl(analysisBranch.isMain(), branchKey, Branch.Type.valueOf(analysisBranch.getType().name()));
     }
     return null;
   }
index 1ed44701a51aa6ab3fd33cdc7cf85537b972b770..a58a8ca987ad1e1962c3b44822509520bb4502bf 100644 (file)
@@ -25,7 +25,9 @@ import org.sonar.api.utils.System2;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.BranchType;
 import org.sonar.db.component.ComponentDto;
+import org.sonar.db.protobuf.DbProjectBranches;
 import org.sonar.server.computation.task.projectanalysis.analysis.AnalysisMetadataHolder;
 import org.sonar.server.computation.task.projectanalysis.analysis.Branch;
 
@@ -55,12 +57,11 @@ public class BranchPersisterImpl implements BranchPersister {
     if (branch.isMain()) {
       checkState(branchComponentDtoOpt.isPresent(), "Project has been deleted by end-user during analysis");
       branchComponentDto = branchComponentDtoOpt.get();
-
     } else {
       // inserts new row in table projects if it's the first time branch is analyzed
       branchComponentDto = branchComponentDtoOpt.or(() -> insertIntoProjectsTable(dbSession, branchUuid));
-
     }
+
     // insert or update in table project_branches
     dbClient.branchDao().upsert(dbSession, toBranchDto(branchComponentDto, branch));
   }
@@ -75,15 +76,29 @@ public class BranchPersisterImpl implements BranchPersister {
     return (first != null) ? first : second;
   }
 
-  private static BranchDto toBranchDto(ComponentDto componentDto, Branch branch) {
+  private BranchDto toBranchDto(ComponentDto componentDto, Branch branch) {
     BranchDto dto = new BranchDto();
     dto.setUuid(componentDto.uuid());
+
     // MainBranchProjectUuid will be null if it's a main branch
     dto.setProjectUuid(firstNonNull(componentDto.getMainBranchProjectUuid(), componentDto.projectUuid()));
-    dto.setKey(branch.getName());
     dto.setBranchType(branch.getType());
+
     // merge branch is only present if it's a short living branch
     dto.setMergeBranchUuid(branch.getMergeBranchUuid().orElse(null));
+
+    if (branch.getType() == BranchType.PULL_REQUEST) {
+      dto.setKey(analysisMetadataHolder.getPullRequestId());
+
+      DbProjectBranches.PullRequestData pullRequestData = DbProjectBranches.PullRequestData.newBuilder()
+        .setBranch(branch.getName())
+        .setTitle(branch.getName())
+        .build();
+      dto.setPullRequestData(pullRequestData);
+    } else {
+      dto.setKey(branch.getName());
+    }
+
     return dto;
   }
 
index d3e76444576ceb41301b0ad2315ba592b893b571..63b49e588ef3f73f8e7eb7ff8ae8d9d520214795 100644 (file)
@@ -85,6 +85,11 @@ public class DefaultBranchImpl implements Branch {
     return !isLegacyBranch;
   }
 
+  @Override
+  public String getPullRequestId() {
+    throw new IllegalStateException("Only a branch of type PULL_REQUEST can have a pull request id.");
+  }
+
   @Override
   public String generateKey(ScannerReport.Component module, @Nullable ScannerReport.Component fileOrDir) {
     String moduleWithBranch = module.getKey();
index 5d12477afea05738b61653d6fef1c0b2b6e7a65f..1ec0b9e1d3b307212aeb546551d935572e564ca7 100644 (file)
@@ -31,7 +31,7 @@ import org.sonar.db.component.ComponentDto;
 import org.sonar.server.computation.task.projectanalysis.analysis.AnalysisMetadataHolder;
 
 import static com.google.common.base.Preconditions.checkState;
-import static org.sonar.db.component.ComponentDto.removeBranchFromKey;
+import static org.sonar.db.component.ComponentDto.removeBranchAndPullRequestFromKey;
 
 /**
  * Cache a map between component keys and uuids in the merge branch
@@ -74,7 +74,7 @@ public class MergeBranchComponentUuids {
   @CheckForNull
   public String getUuid(String dbKey) {
     lazyInit();
-    String cleanComponentKey = removeBranchFromKey(dbKey);
+    String cleanComponentKey = removeBranchAndPullRequestFromKey(dbKey);
     return uuidsByKey.get(cleanComponentKey);
   }
 }
index eb799f1ad8ffcb2be0623487277b90881187cf02..71d97f257c5ff699abdc315dff1915c966cbd8b1 100644 (file)
@@ -29,7 +29,7 @@ import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.KeyWithUuidDto;
 
-import static org.sonar.db.component.ComponentDto.removeBranchFromKey;
+import static org.sonar.db.component.ComponentDto.removeBranchAndPullRequestFromKey;
 
 /**
  * Cache a map of component key -> uuid in short branches that have issues with status either RESOLVED or CONFIRMED.
@@ -51,7 +51,7 @@ public class ShortBranchComponentsWithIssues {
     try (DbSession dbSession = dbClient.openSession(false)) {
       List<KeyWithUuidDto> components = dbClient.componentDao().selectComponentKeysHavingIssuesToMerge(dbSession, uuid);
       for (KeyWithUuidDto dto : components) {
-        uuidsByKey.computeIfAbsent(removeBranchFromKey(dto.key()), s -> new HashSet<>()).add(dto.uuid());
+        uuidsByKey.computeIfAbsent(removeBranchAndPullRequestFromKey(dto.key()), s -> new HashSet<>()).add(dto.uuid());
       }
     }
   }
index e9403cf007a662c1324378866349ec0b01d217e5..c60c9abade439185225f69b42c3ce7b1591ba15f 100644 (file)
@@ -44,7 +44,7 @@ public class IssueTrackingDelegator {
   }
 
   public TrackingResult track(Component component) {
-    if (analysisMetadataHolder.isShortLivingBranch()) {
+    if (analysisMetadataHolder.isShortLivingBranch() || analysisMetadataHolder.isPullRequest()) {
       return standardResult(shortBranchTracker.track(component));
     } else if (isFirstAnalysisSecondaryLongLivingBranch()) {
       Tracking<DefaultIssue, DefaultIssue> tracking = mergeBranchTracker.track(component);
index a69113a554d226fb52489a583dc89c640c168ee3..4d86d87b61eb3d38d1a0af5a060350e28f4a0e8a 100644 (file)
@@ -50,7 +50,7 @@ public class ShortBranchIssuesLoader {
   }
 
   public Collection<ShortBranchIssue> loadCandidateIssuesForMergingInTargetBranch(Component component) {
-    String componentKey = ComponentDto.removeBranchFromKey(component.getKey());
+    String componentKey = ComponentDto.removeBranchAndPullRequestFromKey(component.getKey());
     Set<String> uuids = shortBranchComponentsWithIssues.getUuids(componentKey);
     if (uuids.isEmpty()) {
       return Collections.emptyList();
index 7838e9091c25e654f1284aef2fc2016963015a56..89b97b839c0c8f351d53833e5b1e5fcb7e54325c 100644 (file)
@@ -67,7 +67,7 @@ public class LoadQualityGateStep implements ComputationStep {
   }
 
   private Optional<QualityGate> getShortLivingBranchQualityGate() {
-    if (analysisMetadataHolder.isShortLivingBranch()) {
+    if (analysisMetadataHolder.isShortLivingBranch() || analysisMetadataHolder.isPullRequest()) {
       Optional<QualityGate> qualityGate = qualityGateService.findById(ShortLivingBranchQualityGate.ID);
       if (qualityGate.isPresent()) {
         return qualityGate;
index fe9b452b6030e05c214af1aa81768bd8d5633ae8..d9bbf514fc5f717730031741b60a4a9e2167239d 100644 (file)
@@ -69,8 +69,8 @@ public class QualityGateEventsStep implements ComputationStep {
 
   @Override
   public void execute() {
-    // no notification on short living branch as there is no real Quality Gate on those
-    if (analysisMetadataHolder.isShortLivingBranch()) {
+    // no notification on short living branch and pull request as there is no real Quality Gate on those
+    if (analysisMetadataHolder.isShortLivingBranch() || analysisMetadataHolder.isPullRequest()) {
       return;
     }
     new DepthTraversalTypeAwareCrawler(
index 5addea9d4ca628617ab14df557d3b97e06b3c724..744a49f7291407a10b102df26a0feb661e3c704d 100644 (file)
@@ -28,6 +28,7 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Predicate;
+import javax.annotation.CheckForNull;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.utils.Duration;
 import org.sonar.core.issue.DefaultIssue;
@@ -49,6 +50,7 @@ import org.sonar.server.issue.notification.NewIssuesNotificationFactory;
 import org.sonar.server.issue.notification.NewIssuesStatistics;
 import org.sonar.server.notification.NotificationService;
 
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
 import static org.sonar.server.computation.task.projectanalysis.component.ComponentVisitor.Order.POST_ORDER;
 
 /**
@@ -127,7 +129,7 @@ public class SendIssueNotificationsStep implements ComputationStep {
     IssueChangeNotification changeNotification = new IssueChangeNotification();
     changeNotification.setRuleName(rules.getByKey(issue.ruleKey()).getName());
     changeNotification.setIssue(issue);
-    changeNotification.setProject(project.getPublicKey(), project.getName(), getBranchName());
+    changeNotification.setProject(project.getPublicKey(), project.getName(), getBranchName(), getPullRequest());
     getComponentKey(issue).ifPresent(c -> changeNotification.setComponent(c.getPublicKey(), c.getName()));
     service.deliver(changeNotification);
   }
@@ -136,7 +138,7 @@ public class SendIssueNotificationsStep implements ComputationStep {
     NewIssuesStatistics.Stats globalStatistics = statistics.globalStatistics();
     NewIssuesNotification notification = newIssuesNotificationFactory
       .newNewIssuesNotication()
-      .setProject(project.getPublicKey(), project.getName(), getBranchName())
+      .setProject(project.getPublicKey(), project.getName(), getBranchName(), getPullRequest())
       .setProjectVersion(project.getReportAttributes().getVersion())
       .setAnalysisDate(new Date(analysisDate))
       .setStatistics(project.getName(), globalStatistics)
@@ -155,7 +157,7 @@ public class SendIssueNotificationsStep implements ComputationStep {
           .newMyNewIssuesNotification()
           .setAssignee(assignee);
         myNewIssuesNotification
-          .setProject(project.getPublicKey(), project.getName(), getBranchName())
+          .setProject(project.getPublicKey(), project.getName(), getBranchName(), getPullRequest())
           .setProjectVersion(project.getReportAttributes().getVersion())
           .setAnalysisDate(new Date(analysisDate))
           .setStatistics(project.getName(), assigneeStatistics)
@@ -185,9 +187,16 @@ public class SendIssueNotificationsStep implements ComputationStep {
     return "Send issue notifications";
   }
 
+  @CheckForNull
   private String getBranchName() {
     Branch branch = analysisMetadataHolder.getBranch();
-    return branch.isMain() ? null : branch.getName();
+    return branch.isMain() || branch.getType() == PULL_REQUEST ? null : branch.getName();
+  }
+
+  @CheckForNull
+  private String getPullRequest() {
+    Branch branch = analysisMetadataHolder.getBranch();
+    return branch.getType() == PULL_REQUEST ? analysisMetadataHolder.getPullRequestId() : null;
   }
 
 }
index a94371a998e40b3e36be7a008f598d623a536735..1b9fb9908fe24830cee0da2319424ee543551a6b 100644 (file)
@@ -52,7 +52,7 @@ public class DuplicationsParser {
     this.componentDao = componentDao;
   }
 
-  public List<Block> parse(DbSession session, ComponentDto component, @Nullable String branch, @Nullable String duplicationsData) {
+  public List<Block> parse(DbSession session, ComponentDto component, @Nullable String branch, @Nullable String pullRequest, @Nullable String duplicationsData) {
     Map<String, ComponentDto> componentsByKey = newHashMap();
     List<Block> blocks = newArrayList();
     if (duplicationsData != null) {
@@ -69,7 +69,7 @@ public class DuplicationsParser {
             String size = bCursor.getAttrValue("l");
             String componentKey = bCursor.getAttrValue("r");
             if (from != null && size != null && componentKey != null) {
-              duplications.add(createDuplication(componentsByKey, branch, from, size, componentKey, session));
+              duplications.add(createDuplication(componentsByKey, branch, pullRequest, from, size, componentKey, session));
             }
           }
           Collections.sort(duplications, new DuplicationComparator(component.uuid(), component.projectUuid()));
@@ -83,12 +83,19 @@ public class DuplicationsParser {
     return blocks;
   }
 
-  private Duplication createDuplication(Map<String, ComponentDto> componentsByKey, @Nullable String branch, String from, String size, String componentDbKey, DbSession session) {
+  private Duplication createDuplication(Map<String, ComponentDto> componentsByKey, @Nullable String branch, @Nullable String pullRequest, String from, String size,
+    String componentDbKey, DbSession session) {
     String componentKey = convertToKey(componentDbKey);
     ComponentDto component = componentsByKey.get(componentKey);
     if (component == null) {
-      Optional<ComponentDto> componentDtoOptional = branch == null ? componentDao.selectByKey(session, componentKey)
-        : Optional.fromNullable(componentDao.selectByKeyAndBranch(session, componentKey, branch).orElseGet(null));
+      Optional<ComponentDto> componentDtoOptional;
+      if (branch != null) {
+        componentDtoOptional = Optional.fromNullable(componentDao.selectByKeyAndBranch(session, componentKey, branch).orElseGet(null));
+      } else if (pullRequest != null) {
+        componentDtoOptional = Optional.fromNullable(componentDao.selectByKeyAndPullRequest(session, componentKey, pullRequest).orElseGet(null));
+      } else {
+        componentDtoOptional = componentDao.selectByKey(session, componentKey);
+      }
       component = componentDtoOptional.isPresent() ? componentDtoOptional.get() : null;
       componentsByKey.put(componentKey, component);
     }
index df342e5d032d4f3491634cc4c5cc7dcc819f68e5..1889105b1b63d44f4af20fcdbf5da103098c5227 100644 (file)
@@ -37,11 +37,15 @@ import org.sonar.server.user.UserSession;
 import static com.google.common.base.Preconditions.checkArgument;
 import static org.sonar.server.component.ComponentFinder.ParamNames.UUID_AND_KEY;
 import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
-import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_BRANCH;
 
 public class ShowAction implements DuplicationsWsAction {
 
+  private static final String PARAM_KEY = "key";
+  private static final String PARAM_UUID = "uuid";
+  private static final String PARAM_BRANCH = "branch";
+  private static final String PARAM_PULL_REQUEST = "pullRequest";
   private final DbClient dbClient;
   private final DuplicationsParser parser;
   private final ShowResponseBuilder responseBuilder;
@@ -68,21 +72,28 @@ public class ShowAction implements DuplicationsWsAction {
       new Change("6.5", "The fields 'uuid', 'projectUuid', 'subProjectUuid' are deprecated in the response."));
 
     action
-      .createParam("key")
+      .createParam(PARAM_KEY)
       .setDescription("File key")
       .setExampleValue("my_project:/src/foo/Bar.php");
 
     action
-      .createParam("uuid")
+      .createParam(PARAM_UUID)
       .setDeprecatedSince("6.5")
       .setDescription("File ID. If provided, 'key' must not be provided.")
       .setExampleValue("584a89f2-8037-4f7b-b82c-8b45d2d63fb2");
 
     action
-      .createParam("branch")
+      .createParam(PARAM_BRANCH)
       .setDescription("Branch key")
       .setInternal(true)
       .setExampleValue(KEY_BRANCH_EXAMPLE_001);
+
+    action
+      .createParam(PARAM_PULL_REQUEST)
+      .setDescription("Pull request id")
+      .setInternal(true)
+      .setSince("7.1")
+      .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001);
   }
 
   @Override
@@ -92,19 +103,22 @@ public class ShowAction implements DuplicationsWsAction {
       userSession.checkComponentPermission(UserRole.CODEVIEWER, component);
       String duplications = findDataFromComponent(dbSession, component);
       String branch = component.getBranch();
-      List<DuplicationsParser.Block> blocks = parser.parse(dbSession, component, branch, duplications);
-      writeProtobuf(responseBuilder.build(dbSession, blocks, branch), request, response);
+      String pullRequest = component.getPullRequest();
+      List<DuplicationsParser.Block> blocks = parser.parse(dbSession, component, branch, pullRequest, duplications);
+      writeProtobuf(responseBuilder.build(dbSession, blocks, branch, pullRequest), request, response);
     }
   }
 
   private ComponentDto loadComponent(DbSession dbSession, Request request) {
-    String componentUuid = request.param("uuid");
-    String branch = request.param("branch");
-    checkArgument(componentUuid == null || branch == null, "'%s' and '%s' parameters cannot be used at the same time", "uuid", PARAM_BRANCH);
-    if (branch == null) {
-      return componentFinder.getByUuidOrKey(dbSession, componentUuid, request.param("key"), UUID_AND_KEY);
+    String componentUuid = request.param(PARAM_UUID);
+    String branch = request.param(PARAM_BRANCH);
+    String pullRequest = request.param(PARAM_PULL_REQUEST);
+    checkArgument(componentUuid == null || (branch == null && pullRequest == null), "Parameter '%s' cannot be used at the same time as '%s' or '%s'", PARAM_UUID,
+      PARAM_BRANCH, PARAM_PULL_REQUEST);
+    if (branch == null && pullRequest == null) {
+      return componentFinder.getByUuidOrKey(dbSession, componentUuid, request.param(PARAM_KEY), UUID_AND_KEY);
     }
-    return componentFinder.getByKeyAndOptionalBranch(dbSession, request.mandatoryParam("key"), branch);
+    return componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, request.mandatoryParam(PARAM_KEY), branch, pullRequest);
   }
 
   @CheckForNull
index 1c56c0392471d23b09839f03e38ee49cdb02c326..f6050bc65272eee625be857e490083e6b7910667 100644 (file)
@@ -49,14 +49,14 @@ public class ShowResponseBuilder {
     this.componentDao = componentDao;
   }
 
-  ShowResponse build(DbSession session, List<DuplicationsParser.Block> blocks, @Nullable String branch) {
+  ShowResponse build(DbSession session, List<DuplicationsParser.Block> blocks, @Nullable String branch, @Nullable String pullRequest) {
     ShowResponse.Builder response = ShowResponse.newBuilder();
     Map<String, String> refByComponentKey = newHashMap();
     blocks.stream()
       .map(block -> toWsDuplication(block, refByComponentKey))
       .forEach(response::addDuplications);
 
-    writeFiles(session, response, refByComponentKey, branch);
+    writeFiles(session, response, refByComponentKey, branch, pullRequest);
 
     return response.build();
   }
@@ -86,7 +86,8 @@ public class ShowResponseBuilder {
     return block;
   }
 
-  private static Duplications.File toWsFile(ComponentDto file, @Nullable ComponentDto project, @Nullable ComponentDto subProject, @Nullable String branch) {
+  private static Duplications.File toWsFile(ComponentDto file, @Nullable ComponentDto project, @Nullable ComponentDto subProject, @Nullable String branch,
+    @Nullable String pullRequest) {
     Duplications.File.Builder wsFile = Duplications.File.newBuilder();
     wsFile.setKey(file.getKey());
     wsFile.setUuid(file.uuid());
@@ -102,15 +103,14 @@ public class ShowResponseBuilder {
         wsFile.setSubProjectUuid(subProject.uuid());
         wsFile.setSubProjectName(subProject.longName());
       }
-      if (branch != null) {
-        wsFile.setBranch(branch);
-      }
+      setNullable(branch, wsFile::setBranch);
+      setNullable(pullRequest, wsFile::setPullRequest);
       return wsFile;
     });
     return wsFile.build();
   }
 
-  private void writeFiles(DbSession session, ShowResponse.Builder response, Map<String, String> refByComponentKey, @Nullable String branch) {
+  private void writeFiles(DbSession session, ShowResponse.Builder response, Map<String, String> refByComponentKey, @Nullable String branch, @Nullable String pullRequest) {
     Map<String, ComponentDto> projectsByUuid = newHashMap();
     Map<String, ComponentDto> parentModulesByUuid = newHashMap();
     Map<String, Duplications.File> filesByRef = response.getMutableFiles();
@@ -124,7 +124,7 @@ public class ShowResponseBuilder {
 
         ComponentDto project = getProject(file.projectUuid(), projectsByUuid, session);
         ComponentDto parentModule = getParentProject(file.getRootUuid(), parentModulesByUuid, session);
-        filesByRef.put(ref, toWsFile(file, project, parentModule, branch));
+        filesByRef.put(ref, toWsFile(file, project, parentModule, branch, pullRequest));
       }
     }
   }
index 18dd1eef55a4df8738a42a1bf7301c4614f4e3c2..2bc84dddb829a8ad612b928c78060a88c96c3e9e 100644 (file)
@@ -198,6 +198,7 @@ public class IssueQueryFactory {
     Collection<String> componentRootUuids = request.getComponentRootUuids();
     Collection<String> componentRoots = request.getComponentRoots();
     String branch = request.getBranch();
+    String pullRequest = request.getPullRequest();
 
     boolean effectiveOnComponentOnly = false;
 
@@ -208,15 +209,15 @@ public class IssueQueryFactory {
     if (componentRootUuids != null) {
       allComponents.addAll(getComponentsFromUuids(session, componentRootUuids));
     } else if (componentRoots != null) {
-      allComponents.addAll(getComponentsFromKeys(session, componentRoots, branch));
+      allComponents.addAll(getComponentsFromKeys(session, componentRoots, branch, pullRequest));
     } else if (components != null) {
-      allComponents.addAll(getComponentsFromKeys(session, components, branch));
+      allComponents.addAll(getComponentsFromKeys(session, components, branch, pullRequest));
       effectiveOnComponentOnly = true;
     } else if (componentUuids != null) {
       allComponents.addAll(getComponentsFromUuids(session, componentUuids));
       effectiveOnComponentOnly = BooleanUtils.isTrue(onComponentOnly);
     } else if (componentKeys != null) {
-      allComponents.addAll(getComponentsFromKeys(session, componentKeys, branch));
+      allComponents.addAll(getComponentsFromKeys(session, componentKeys, branch, pullRequest));
       effectiveOnComponentOnly = BooleanUtils.isTrue(onComponentOnly);
     }
 
@@ -229,13 +230,11 @@ public class IssueQueryFactory {
       .count() <= 1;
   }
 
-  private void addComponentParameters(IssueQuery.Builder builder, DbSession session, boolean onComponentOnly,
-    List<ComponentDto> components, SearchRequest request) {
-
+  private void addComponentParameters(IssueQuery.Builder builder, DbSession session, boolean onComponentOnly, List<ComponentDto> components, SearchRequest request) {
     builder.onComponentOnly(onComponentOnly);
     if (onComponentOnly) {
       builder.componentUuids(components.stream().map(ComponentDto::uuid).collect(toList()));
-      setBranch(builder, components.get(0), request.getBranch());
+      setBranch(builder, components.get(0), request.getBranch(), request.getPullRequest());
       return;
     }
 
@@ -246,9 +245,9 @@ public class IssueQueryFactory {
     if (projectUuids != null) {
       builder.projectUuids(projectUuids);
     } else if (projectKeys != null) {
-      List<ComponentDto> projects = getComponentsFromKeys(session, projectKeys, request.getBranch());
+      List<ComponentDto> projects = getComponentsFromKeys(session, projectKeys, request.getBranch(), request.getPullRequest());
       builder.projectUuids(projects.stream().map(IssueQueryFactory::toProjectUuid).collect(toList()));
-      setBranch(builder, projects.get(0), request.getBranch());
+      setBranch(builder, projects.get(0), request.getBranch(), request.getPullRequest());
     }
     builder.moduleUuids(request.getModuleUuids());
     builder.directories(request.getDirectories());
@@ -269,7 +268,7 @@ public class IssueQueryFactory {
     Set<String> qualifiers = components.stream().map(ComponentDto::qualifier).collect(toHashSet());
     checkArgument(qualifiers.size() == 1, "All components must have the same qualifier, found %s", String.join(",", qualifiers));
 
-    setBranch(builder, components.get(0), request.getBranch());
+    setBranch(builder, components.get(0), request.getBranch(), request.getPullRequest());
     String qualifier = qualifiers.iterator().next();
     switch (qualifier) {
       case Qualifiers.VIEW:
@@ -345,10 +344,15 @@ public class IssueQueryFactory {
     builder.directories(directoryPaths);
   }
 
-  private List<ComponentDto> getComponentsFromKeys(DbSession dbSession, Collection<String> componentKeys, @Nullable String branch) {
-    List<ComponentDto> componentDtos = branch == null
-      ? dbClient.componentDao().selectByKeys(dbSession, componentKeys)
-      : dbClient.componentDao().selectByKeysAndBranch(dbSession, componentKeys, branch);
+  private List<ComponentDto> getComponentsFromKeys(DbSession dbSession, Collection<String> componentKeys, @Nullable String branch, @Nullable String pullRequest) {
+    List<ComponentDto> componentDtos;
+    if (branch != null) {
+      componentDtos = dbClient.componentDao().selectByKeysAndBranch(dbSession, componentKeys, branch);
+    } else if (pullRequest != null) {
+      componentDtos = dbClient.componentDao().selectByKeysAndPullRequest(dbSession, componentKeys, pullRequest);
+    } else {
+      componentDtos = dbClient.componentDao().selectByKeys(dbSession, componentKeys);
+    }
     if (!componentKeys.isEmpty() && componentDtos.isEmpty()) {
       return singletonList(UNKNOWN_COMPONENT);
     }
@@ -376,8 +380,11 @@ public class IssueQueryFactory {
     return mainBranchProjectUuid == null ? componentDto.projectUuid() : mainBranchProjectUuid;
   }
 
-  private static void setBranch(IssueQuery.Builder builder, ComponentDto component, @Nullable String branch) {
-    builder.branchUuid(branch == null ? null : component.projectUuid());
-    builder.mainBranch(branch == null || component.equals(UNKNOWN_COMPONENT) || !branch.equals(component.getBranch()));
+  private static void setBranch(IssueQuery.Builder builder, ComponentDto component, @Nullable String branch, @Nullable String pullRequest) {
+    builder.branchUuid(branch == null && pullRequest == null ? null : component.projectUuid());
+    builder.mainBranch(UNKNOWN_COMPONENT.equals(component)
+      || (branch == null && pullRequest == null)
+      || (branch != null && !branch.equals(component.getBranch()))
+      || (pullRequest != null && !pullRequest.equals(component.getPullRequest())));
   }
 }
index 83a424bcb002d26ff1cf85b1a367b7df8c3b2ce9..8c9b7d4af110e62ced05958accce2782aac8a266 100644 (file)
@@ -48,6 +48,7 @@ public class SearchRequest {
   private List<String> moduleUuids;
   private Boolean onComponentOnly;
   private String branch;
+  private String pullRequest;
   private String organization;
   private Integer page;
   private Integer pageSize;
@@ -453,4 +454,14 @@ public class SearchRequest {
     this.branch = branch;
     return this;
   }
+
+  @CheckForNull
+  public String getPullRequest() {
+    return pullRequest;
+  }
+
+  public SearchRequest setPullRequest(@Nullable String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
 }
index 1746f383dfb989a2f69e8b9fd477d34faa20832b..d8bfbe925e9ca9d8223755593e0720d451b94c27 100644 (file)
@@ -54,6 +54,7 @@ public abstract class AbstractNewIssuesEmailTemplate extends EmailTemplate {
   static final String FIELD_PROJECT_VERSION = "projectVersion";
   static final String FIELD_ASSIGNEE = "assignee";
   static final String FIELD_BRANCH = "branch";
+  static final String FIELD_PULL_REQUEST = "pullRequest";
 
   protected final EmailSettings settings;
   protected final I18n i18n;
@@ -78,12 +79,16 @@ public abstract class AbstractNewIssuesEmailTemplate extends EmailTemplate {
     }
     String projectName = checkNotNull(notification.getFieldValue(FIELD_PROJECT_NAME));
     String branchName = notification.getFieldValue(FIELD_BRANCH);
+    String pullRequest = notification.getFieldValue(FIELD_PULL_REQUEST);
 
     StringBuilder message = new StringBuilder();
     message.append("Project: ").append(projectName).append(NEW_LINE);
     if (branchName != null) {
       message.append("Branch: ").append(branchName).append(NEW_LINE);
     }
+    if (pullRequest!= null) {
+      message.append("Pull request: ").append(pullRequest).append(NEW_LINE);
+    }
     String version = notification.getFieldValue(FIELD_PROJECT_VERSION);
     if (version != null) {
       message.append("Version: ").append(version).append(NEW_LINE);
@@ -203,6 +208,10 @@ public abstract class AbstractNewIssuesEmailTemplate extends EmailTemplate {
       if (branchName != null) {
         url += "&branch=" + encode(branchName);
       }
+      String pullRequest = notification.getFieldValue(FIELD_PULL_REQUEST);
+      if (pullRequest != null) {
+        url += "&pullRequest=" + encode(pullRequest);
+      }
       url += "&createdAt=" + encode(DateUtils.formatDateTime(date));
       message
         .append("More details at: ")
index 517b0e10fb974002e067ab9e928357d8bdd5609a..d25e8adb45ebe5a7ddfe93c2eed071a11834465e 100644 (file)
@@ -29,6 +29,11 @@ import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.FieldDiffs;
 import org.sonar.db.component.ComponentDto;
 
+import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_BRANCH;
+import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_PROJECT_KEY;
+import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_PROJECT_NAME;
+import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_PULL_REQUEST;
+
 public class IssueChangeNotification extends Notification {
 
   public static final String TYPE = "issue-changes";
@@ -54,14 +59,17 @@ public class IssueChangeNotification extends Notification {
   }
 
   public IssueChangeNotification setProject(ComponentDto project) {
-    return setProject(project.getKey(), project.name(), project.getBranch());
+    return setProject(project.getKey(), project.name(), project.getBranch(), project.getPullRequest());
   }
 
-  public IssueChangeNotification setProject(String projectKey, String projectName, @Nullable String branch) {
-    setFieldValue("projectName", projectName);
-    setFieldValue("projectKey", projectKey);
+  public IssueChangeNotification setProject(String projectKey, String projectName, @Nullable String branch, @Nullable String pullRequest) {
+    setFieldValue(FIELD_PROJECT_NAME, projectName);
+    setFieldValue(FIELD_PROJECT_KEY, projectKey);
     if (branch != null) {
-      setFieldValue("branch", branch);
+      setFieldValue(FIELD_BRANCH, branch);
+    }
+    if (pullRequest != null) {
+      setFieldValue(FIELD_PULL_REQUEST, pullRequest);
     }
     return this;
   }
index b6b9d239ad256cd3a2de1461f387343439a56870..487c7fd4c2c0eaf1bf87158148d6bc80d06b44ce 100644 (file)
@@ -33,6 +33,8 @@ import org.sonar.plugins.emailnotifications.api.EmailMessage;
 import org.sonar.plugins.emailnotifications.api.EmailTemplate;
 
 import static java.net.URLEncoder.encode;
+import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_BRANCH;
+import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_PULL_REQUEST;
 
 /**
  * Creates email message for notification "issue-changes".
@@ -99,10 +101,14 @@ public class IssueChangesEmailTemplate extends EmailTemplate {
 
   private static void appendHeader(Notification notif, StringBuilder sb) {
     appendLine(sb, StringUtils.defaultString(notif.getFieldValue("componentName"), notif.getFieldValue("componentKey")));
-    String branchName = notif.getFieldValue("branch");
+    String branchName = notif.getFieldValue(FIELD_BRANCH);
     if (branchName != null) {
       appendField(sb, "Branch", null, branchName);
     }
+    String pullRequest = notif.getFieldValue(FIELD_PULL_REQUEST);
+    if (pullRequest != null) {
+      appendField(sb, "Pull request", null, pullRequest);
+    }
     appendField(sb, "Rule", null, notif.getFieldValue("ruleName"));
     appendField(sb, "Message", null, notif.getFieldValue("message"));
   }
@@ -114,10 +120,14 @@ public class IssueChangesEmailTemplate extends EmailTemplate {
         .append("/project/issues?id=").append(encode(notification.getFieldValue("projectKey"), "UTF-8"))
         .append("&issues=").append(issueKey)
         .append("&open=").append(issueKey);
-      String branchName = notification.getFieldValue("branch");
+      String branchName = notification.getFieldValue(FIELD_BRANCH);
       if (branchName != null) {
         sb.append("&branch=").append(branchName);
       }
+      String pullRequest = notification.getFieldValue(FIELD_PULL_REQUEST);
+      if (pullRequest != null) {
+        sb.append("&pullRequest=").append(pullRequest);
+      }
       sb.append(NEW_LINE);
     } catch (UnsupportedEncodingException e) {
       throw new IllegalStateException("Encoding not supported", e);
index cebeeb6f2ed7f20e50425f26a1554dfc0da36072..ed50922ed7e42fbb5aff8fcae945d742f4b222de 100644 (file)
@@ -65,10 +65,14 @@ public class MyNewIssuesEmailTemplate extends AbstractNewIssuesEmailTemplate {
         settings.getServerBaseURL(),
         encode(projectKey),
         encode(assignee));
-      String branchName = notification.getFieldValue("branch");
+      String branchName = notification.getFieldValue(FIELD_BRANCH);
       if (branchName != null) {
         url += "&branch=" + encode(branchName);
       }
+      String pullRequest = notification.getFieldValue(FIELD_PULL_REQUEST);
+      if (pullRequest != null) {
+        url += "&pullRequest=" + encode(pullRequest);
+      }
       url += "&createdAt=" + encode(DateUtils.formatDateTime(date));
       message
         .append("More details at: ")
index e7e819ec86b04765c273df15c29cbf52621b2d93..df8c6bb943b60808d4cc734f99d990470633ba69 100644 (file)
@@ -46,6 +46,7 @@ import org.sonar.server.user.index.UserIndex;
 
 import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_BRANCH;
 import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_PROJECT_VERSION;
+import static org.sonar.server.issue.notification.AbstractNewIssuesEmailTemplate.FIELD_PULL_REQUEST;
 import static org.sonar.server.issue.notification.NewIssuesEmailTemplate.FIELD_PROJECT_DATE;
 import static org.sonar.server.issue.notification.NewIssuesEmailTemplate.FIELD_PROJECT_KEY;
 import static org.sonar.server.issue.notification.NewIssuesEmailTemplate.FIELD_PROJECT_NAME;
@@ -79,12 +80,15 @@ public class NewIssuesNotification extends Notification {
     return this;
   }
 
-  public NewIssuesNotification setProject(String projectKey, String projectName, @Nullable String branchName) {
+  public NewIssuesNotification setProject(String projectKey, String projectName, @Nullable String branchName, @Nullable String pullRequest) {
     setFieldValue(FIELD_PROJECT_NAME, projectName);
     setFieldValue(FIELD_PROJECT_KEY, projectKey);
     if (branchName != null) {
       setFieldValue(FIELD_BRANCH, branchName);
     }
+    if (pullRequest != null) {
+      setFieldValue(FIELD_PULL_REQUEST, pullRequest);
+    }
     return this;
   }
 
index a76a989689e1e1d9b894844b3259904c989c2265..3bd129ba174f49d813af5436ccc92ee46ea85c03 100644 (file)
@@ -72,6 +72,7 @@ import static org.sonar.core.util.stream.MoreCollectors.toSet;
 import static org.sonar.server.es.SearchOptions.MAX_LIMIT;
 import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_SEARCH;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.DEPRECATED_FACET_MODE_DEBT;
@@ -106,6 +107,7 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PLANNED;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PROJECTS;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PROJECT_KEYS;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PROJECT_UUIDS;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PULL_REQUEST;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_REPORTERS;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RESOLUTIONS;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RESOLVED;
@@ -319,6 +321,12 @@ public class SearchAction implements IssuesWsAction {
       .setInternal(true)
       .setSince("6.6");
 
+    action.createParam(PARAM_PULL_REQUEST)
+      .setDescription("Pull request id")
+      .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001)
+      .setInternal(true)
+      .setSince("7.1");
+
     action.createParam(PARAM_ORGANIZATION)
       .setDescription("Organization key")
       .setRequired(false)
@@ -568,6 +576,7 @@ public class SearchAction implements IssuesWsAction {
       .setModuleUuids(request.paramAsStrings(PARAM_MODULE_UUIDS))
       .setOnComponentOnly(request.paramAsBoolean(PARAM_ON_COMPONENT_ONLY))
       .setBranch(request.param(PARAM_BRANCH))
+      .setPullRequest(request.param(PARAM_PULL_REQUEST))
       .setOrganization(request.param(PARAM_ORGANIZATION))
       .setPage(request.mandatoryParamAsInt(Param.PAGE))
       .setPageSize(request.mandatoryParamAsInt(Param.PAGE_SIZE))
index e41a988935c2a12247bddc8c08fb719f388586ed..a59cd7f5eb25caca2300603b15d57bad628d4c4b 100644 (file)
@@ -162,6 +162,7 @@ public class SearchResponseFormat {
     issueBuilder.setOrganization(data.getOrganizationKey(component.getOrganizationUuid()));
     issueBuilder.setComponent(component.getKey());
     setNullable(component.getBranch(), issueBuilder::setBranch);
+    setNullable(component.getPullRequest(), issueBuilder::setPullRequest);
     ComponentDto project = data.getComponentByUuid(dto.getProjectUuid());
     if (project != null) {
       issueBuilder.setProject(project.getKey());
@@ -309,6 +310,7 @@ public class SearchResponseFormat {
         .setLongName(nullToEmpty(dto.longName()))
         .setEnabled(dto.isEnabled());
       setNullable(dto.getBranch(), builder::setBranch);
+      setNullable(dto.getPullRequest(), builder::setPullRequest);
       String path = dto.path();
       // path is not applicable to the components that are not files.
       // Value must not be "" in this case.
index b7cf692c7e8c804936f5cb63da7a1847b53b4282..431821f00950b09ac4acc4684ad693898de616cf 100644 (file)
@@ -63,7 +63,7 @@ public class LiveQualityGateComputerImpl implements LiveQualityGateComputer {
 
   @Override
   public QualityGate loadQualityGate(DbSession dbSession, OrganizationDto organization, ComponentDto project, BranchDto branch) {
-    if (branch.getBranchType() == BranchType.SHORT) {
+    if (branch.getBranchType() == BranchType.SHORT || branch.getBranchType() == BranchType.PULL_REQUEST) {
       return ShortLivingBranchQualityGate.GATE;
     }
 
index 8ede31ea203d322ec71c447defc906e76a127b2a..f6257345fadea69e6763e6f56ff610ccab58f50f 100644 (file)
@@ -58,17 +58,7 @@ import static java.util.Collections.emptyMap;
 import static java.util.Collections.singletonList;
 import static java.util.Collections.singletonMap;
 import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01;
-import static org.sonar.server.component.ComponentFinder.ParamNames.COMPONENT_ID_AND_COMPONENT;
-import static org.sonar.server.measure.ws.ComponentDtoToWsComponent.componentDtoToWsComponent;
-import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createAdditionalFieldsParameter;
-import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createDeveloperParameters;
-import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createMetricKeysParameter;
-import static org.sonar.server.measure.ws.MetricDtoToWsMetric.metricDtoToWsMetric;
-import static org.sonar.server.measure.ws.SnapshotDtoToWsPeriods.snapshotToWsPeriods;
-import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
-import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
-import static org.sonar.server.ws.WsUtils.checkRequest;
-import static org.sonar.server.ws.WsUtils.writeProtobuf;
+import static org.sonar.server.component.ComponentFinder.ParamNames.COMPONENT_ID_AND_KEY;
 import static org.sonar.server.component.ws.MeasuresWsParameters.ACTION_COMPONENT;
 import static org.sonar.server.component.ws.MeasuresWsParameters.ADDITIONAL_METRICS;
 import static org.sonar.server.component.ws.MeasuresWsParameters.ADDITIONAL_PERIODS;
@@ -80,6 +70,18 @@ import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_COMPONENT
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_DEVELOPER_ID;
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_DEVELOPER_KEY;
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_METRIC_KEYS;
+import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_PULL_REQUEST;
+import static org.sonar.server.measure.ws.ComponentDtoToWsComponent.componentDtoToWsComponent;
+import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createAdditionalFieldsParameter;
+import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createDeveloperParameters;
+import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createMetricKeysParameter;
+import static org.sonar.server.measure.ws.MetricDtoToWsMetric.metricDtoToWsMetric;
+import static org.sonar.server.measure.ws.SnapshotDtoToWsPeriods.snapshotToWsPeriods;
+import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
+import static org.sonar.server.ws.WsUtils.checkRequest;
+import static org.sonar.server.ws.WsUtils.writeProtobuf;
 
 public class ComponentAction implements MeasuresWsAction {
   private static final Set<String> QUALIFIERS_ELIGIBLE_FOR_BEST_VALUE = ImmutableSortedSet.of(Qualifiers.FILE, Qualifiers.UNIT_TEST_FILE);
@@ -123,6 +125,12 @@ public class ComponentAction implements MeasuresWsAction {
       .setInternal(true)
       .setSince("6.6");
 
+    action.createParam(PARAM_PULL_REQUEST)
+      .setDescription("Pull request id")
+      .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001)
+      .setInternal(true)
+      .setSince("7.1");
+
     createMetricKeysParameter(action);
     createAdditionalFieldsParameter(action);
     createDeveloperParameters(action);
@@ -156,10 +164,14 @@ public class ComponentAction implements MeasuresWsAction {
     String componentKey = request.getComponent();
     String componentId = request.getComponentId();
     String branch = request.getBranch();
-    checkArgument(componentId == null || branch == null, "'%s' and '%s' parameters cannot be used at the same time", DEPRECATED_PARAM_COMPONENT_ID, PARAM_BRANCH);
-    return branch == null
-      ? componentFinder.getByUuidOrKey(dbSession, componentId, componentKey, COMPONENT_ID_AND_COMPONENT)
-      : componentFinder.getByKeyAndBranch(dbSession, componentKey, branch);
+    String pullRequest = request.getPullRequest();
+    checkArgument(componentId == null || (branch == null && pullRequest == null), "Parameter '%s' cannot be used at the same time as '%s' or '%s'",
+      DEPRECATED_PARAM_COMPONENT_ID, PARAM_BRANCH, PARAM_PULL_REQUEST);
+    if (branch == null && pullRequest == null) {
+      return componentFinder.getByUuidOrKey(dbSession, componentId, componentKey, COMPONENT_ID_AND_KEY);
+    }
+    checkRequest(componentKey != null, "The '%s' parameter is missing", PARAM_COMPONENT);
+    return componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, componentKey, branch, pullRequest);
   }
 
   private Optional<ComponentDto> getReferenceComponent(DbSession dbSession, ComponentDto component) {
@@ -251,6 +263,7 @@ public class ComponentAction implements MeasuresWsAction {
       .setComponentId(request.param(DEPRECATED_PARAM_COMPONENT_ID))
       .setComponent(request.param(PARAM_COMPONENT))
       .setBranch(request.param(PARAM_BRANCH))
+      .setPullRequest(request.param(PARAM_PULL_REQUEST))
       .setAdditionalFields(request.paramAsStrings(PARAM_ADDITIONAL_FIELDS))
       .setMetricKeys(request.mandatoryParamAsStrings(PARAM_METRIC_KEYS));
     checkRequest(!componentRequest.getMetricKeys().isEmpty(), "At least one metric key must be provided");
@@ -265,6 +278,7 @@ public class ComponentAction implements MeasuresWsAction {
     private String componentId;
     private String component;
     private String branch;
+    private String pullRequest;
     private List<String> metricKeys;
     private List<String> additionalFields;
     private String developerId;
@@ -308,6 +322,16 @@ public class ComponentAction implements MeasuresWsAction {
       return this;
     }
 
+    @CheckForNull
+    public String getPullRequest() {
+      return pullRequest;
+    }
+
+    public ComponentRequest setPullRequest(@Nullable String pullRequest) {
+      this.pullRequest = pullRequest;
+      return this;
+    }
+
     private List<String> getMetricKeys() {
       return metricKeys;
     }
index a0e4839f2c68099d99cdfc6dd3dd6571da8da58a..6129f9af5286ac143bf75ae9921e03f157ab3944 100644 (file)
@@ -59,6 +59,7 @@ class ComponentDtoToWsComponent {
       .setName(component.name())
       .setQualifier(component.qualifier());
     Protobuf.setNullable(component.getBranch(), wsComponent::setBranch);
+    Protobuf.setNullable(component.getPullRequest(), wsComponent::setPullRequest);
     Protobuf.setNullable(component.path(), wsComponent::setPath);
     Protobuf.setNullable(component.description(), wsComponent::setDescription);
     Protobuf.setNullable(component.language(), wsComponent::setLanguage);
index d593764c2a9df411ce79fd40908ac377f9c00409..5ba9c3f92281ebfa11a0d8db77a80151bbb8bbe5 100644 (file)
@@ -84,20 +84,6 @@ import static org.sonar.core.util.Uuids.UUID_EXAMPLE_02;
 import static org.sonar.db.component.ComponentTreeQuery.Strategy.CHILDREN;
 import static org.sonar.db.component.ComponentTreeQuery.Strategy.LEAVES;
 import static org.sonar.server.component.ComponentFinder.ParamNames.BASE_COMPONENT_ID_AND_KEY;
-import static org.sonar.server.component.ComponentFinder.ParamNames.DEVELOPER_ID_AND_KEY;
-import static org.sonar.server.measure.ws.ComponentDtoToWsComponent.componentDtoToWsComponent;
-import static org.sonar.server.measure.ws.MeasureDtoToWsMeasure.updateMeasureBuilder;
-import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createAdditionalFieldsParameter;
-import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createDeveloperParameters;
-import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createMetricKeysParameter;
-import static org.sonar.server.measure.ws.MetricDtoToWsMetric.metricDtoToWsMetric;
-import static org.sonar.server.measure.ws.SnapshotDtoToWsPeriods.snapshotToWsPeriods;
-import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
-import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
-import static org.sonar.server.ws.WsParameterBuilder.createQualifiersParameter;
-import static org.sonar.server.ws.WsParameterBuilder.QualifierParameterContext.newQualifierParameterContext;
-import static org.sonar.server.ws.WsUtils.checkRequest;
-import static org.sonar.server.ws.WsUtils.writeProtobuf;
 import static org.sonar.server.component.ws.MeasuresWsParameters.ACTION_COMPONENT_TREE;
 import static org.sonar.server.component.ws.MeasuresWsParameters.ADDITIONAL_METRICS;
 import static org.sonar.server.component.ws.MeasuresWsParameters.ADDITIONAL_PERIODS;
@@ -112,8 +98,23 @@ import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_METRIC_KE
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_METRIC_PERIOD_SORT;
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_METRIC_SORT;
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_METRIC_SORT_FILTER;
+import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_PULL_REQUEST;
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_QUALIFIERS;
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_STRATEGY;
+import static org.sonar.server.measure.ws.ComponentDtoToWsComponent.componentDtoToWsComponent;
+import static org.sonar.server.measure.ws.MeasureDtoToWsMeasure.updateMeasureBuilder;
+import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createAdditionalFieldsParameter;
+import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createDeveloperParameters;
+import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createMetricKeysParameter;
+import static org.sonar.server.measure.ws.MetricDtoToWsMetric.metricDtoToWsMetric;
+import static org.sonar.server.measure.ws.SnapshotDtoToWsPeriods.snapshotToWsPeriods;
+import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
+import static org.sonar.server.ws.WsParameterBuilder.QualifierParameterContext.newQualifierParameterContext;
+import static org.sonar.server.ws.WsParameterBuilder.createQualifiersParameter;
+import static org.sonar.server.ws.WsUtils.checkRequest;
+import static org.sonar.server.ws.WsUtils.writeProtobuf;
 
 /**
  * <p>Navigate through components based on different strategy with specified measures.
@@ -216,6 +217,12 @@ public class ComponentTreeAction implements MeasuresWsAction {
       .setInternal(true)
       .setSince("6.6");
 
+    action.createParam(PARAM_PULL_REQUEST)
+      .setDescription("Pull request id")
+      .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001)
+      .setInternal(true)
+      .setSince("7.1");
+
     action.createParam(PARAM_METRIC_SORT)
       .setDescription(
         format("Metric key to sort by. The '%s' parameter must contain the '%s' or '%s' value. It must be part of the '%s' parameter", Param.SORT, METRIC_SORT, METRIC_PERIOD_SORT,
@@ -343,6 +350,7 @@ public class ComponentTreeAction implements MeasuresWsAction {
       .setBaseComponentId(request.param(DEPRECATED_PARAM_BASE_COMPONENT_ID))
       .setComponent(request.param(PARAM_COMPONENT))
       .setBranch(request.param(PARAM_BRANCH))
+      .setPullRequest(request.param(PARAM_PULL_REQUEST))
       .setMetricKeys(metricKeys)
       .setStrategy(request.mandatoryParam(PARAM_STRATEGY))
       .setQualifiers(request.paramAsStrings(PARAM_QUALIFIERS))
@@ -428,13 +436,17 @@ public class ComponentTreeAction implements MeasuresWsAction {
   }
 
   private ComponentDto loadComponent(DbSession dbSession, ComponentTreeRequest request) {
-    String componentKey = request.getComponent();
     String componentId = request.getBaseComponentId();
+    String componentKey = request.getComponent();
     String branch = request.getBranch();
-    checkArgument(componentId == null || branch == null, "'%s' and '%s' parameters cannot be used at the same time", DEPRECATED_PARAM_BASE_COMPONENT_ID, PARAM_BRANCH);
-    return branch == null
-            ? componentFinder.getByUuidOrKey(dbSession, componentId, componentKey, BASE_COMPONENT_ID_AND_KEY)
-            : componentFinder.getByKeyAndBranch(dbSession, componentKey, branch);
+    String pullRequest = request.getPullRequest();
+    checkArgument(componentId == null || (branch == null && pullRequest == null), "Parameter '%s' cannot be used at the same time as '%s' or '%s'",
+      DEPRECATED_PARAM_BASE_COMPONENT_ID, PARAM_BRANCH, PARAM_PULL_REQUEST);
+    if (branch == null && pullRequest == null) {
+      return componentFinder.getByUuidOrKey(dbSession, componentId, componentKey, BASE_COMPONENT_ID_AND_KEY);
+    }
+    checkRequest(componentKey != null, "The '%s' parameter is missing", PARAM_COMPONENT);
+    return componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, componentKey, branch, pullRequest);
   }
 
   private Map<String, ComponentDto> searchReferenceComponentsById(DbSession dbSession, List<ComponentDto> components) {
index 6791e894cdc006b692e28b4a54bee83fb3ea1821..31b2525c06e3de429076d2ea35646f2ccb92ec5b 100644 (file)
@@ -28,6 +28,7 @@ class ComponentTreeRequest {
   private String baseComponentId;
   private String component;
   private String branch;
+  private String pullRequest;
   private String strategy;
   private List<String> qualifiers;
   private List<String> additionalFields;
@@ -81,6 +82,16 @@ class ComponentTreeRequest {
     return this;
   }
 
+  @CheckForNull
+  public String getPullRequest() {
+    return pullRequest;
+  }
+
+  public ComponentTreeRequest setPullRequest(@Nullable String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
+
   @CheckForNull
   public String getStrategy() {
     return strategy;
index df6766e218fb430480a979889bf6515b877f310c..4f3af4cbbc0dafe12263981f84830bdb546bef34 100644 (file)
@@ -25,6 +25,8 @@ import java.util.List;
 import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Stream;
+import javax.annotation.CheckForNull;
+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;
@@ -46,22 +48,21 @@ import org.sonar.server.user.UserSession;
 import org.sonar.server.ws.KeyExamples;
 import org.sonarqube.ws.Measures.SearchHistoryResponse;
 
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
-
 import static java.lang.String.format;
 import static org.sonar.api.utils.DateUtils.parseEndingDateOrDateTime;
 import static org.sonar.api.utils.DateUtils.parseStartingDateOrDateTime;
 import static org.sonar.core.util.Protobuf.setNullable;
 import static org.sonar.db.component.SnapshotDto.STATUS_PROCESSED;
-import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
-import static org.sonar.server.ws.WsUtils.writeProtobuf;
 import static org.sonar.server.component.ws.MeasuresWsParameters.ACTION_SEARCH_HISTORY;
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_BRANCH;
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_COMPONENT;
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_FROM;
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_METRICS;
+import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_PULL_REQUEST;
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_TO;
+import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
+import static org.sonar.server.ws.WsUtils.writeProtobuf;
 
 public class SearchHistoryAction implements MeasuresWsAction {
 
@@ -78,18 +79,6 @@ public class SearchHistoryAction implements MeasuresWsAction {
     this.userSession = userSession;
   }
 
-  private static SearchHistoryRequest toWsRequest(Request request) {
-    return SearchHistoryRequest.builder()
-      .setComponent(request.mandatoryParam(PARAM_COMPONENT))
-      .setBranch(request.param(PARAM_BRANCH))
-      .setMetrics(request.mandatoryParamAsStrings(PARAM_METRICS))
-      .setFrom(request.param(PARAM_FROM))
-      .setTo(request.param(PARAM_TO))
-      .setPage(request.mandatoryParamAsInt(Param.PAGE))
-      .setPageSize(request.mandatoryParamAsInt(Param.PAGE_SIZE))
-      .build();
-  }
-
   @Override
   public void define(WebService.NewController context) {
     WebService.NewAction action = context.createAction(ACTION_SEARCH_HISTORY)
@@ -112,6 +101,12 @@ public class SearchHistoryAction implements MeasuresWsAction {
       .setInternal(true)
       .setExampleValue(KEY_BRANCH_EXAMPLE_001);
 
+    action.createParam(PARAM_PULL_REQUEST)
+      .setDescription("Pull request id")
+      .setSince("7.1")
+      .setInternal(true)
+      .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001);
+
     action.createParam(PARAM_METRICS)
       .setDescription("Comma-separated list of metric keys")
       .setRequired(true)
@@ -141,6 +136,19 @@ public class SearchHistoryAction implements MeasuresWsAction {
     writeProtobuf(searchHistoryResponse, request, response);
   }
 
+  private static SearchHistoryRequest toWsRequest(Request request) {
+    return SearchHistoryRequest.builder()
+      .setComponent(request.mandatoryParam(PARAM_COMPONENT))
+      .setBranch(request.param(PARAM_BRANCH))
+      .setPullRequest(request.param(PARAM_PULL_REQUEST))
+      .setMetrics(request.mandatoryParamAsStrings(PARAM_METRICS))
+      .setFrom(request.param(PARAM_FROM))
+      .setTo(request.param(PARAM_TO))
+      .setPage(request.mandatoryParamAsInt(Param.PAGE))
+      .setPageSize(request.mandatoryParamAsInt(Param.PAGE_SIZE))
+      .build();
+  }
+
   private Function<SearchHistoryRequest, SearchHistoryResult> search() {
     return request -> {
       try (DbSession dbSession = dbClient.openSession(false)) {
@@ -199,15 +207,14 @@ public class SearchHistoryAction implements MeasuresWsAction {
   private ComponentDto loadComponent(DbSession dbSession, SearchHistoryRequest request) {
     String componentKey = request.getComponent();
     String branch = request.getBranch();
-    if (branch != null) {
-      return componentFinder.getByKeyAndBranch(dbSession, componentKey, branch);
-    }
-    return componentFinder.getByKey(dbSession, componentKey);
+    String pullRequest = request.getPullRequest();
+    return componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, componentKey, branch, pullRequest);
   }
 
   static class SearchHistoryRequest {
     private final String component;
     private final String branch;
+    private final String pullRequest;
     private final List<String> metrics;
     private final String from;
     private final String to;
@@ -217,6 +224,7 @@ public class SearchHistoryAction implements MeasuresWsAction {
     public SearchHistoryRequest(Builder builder) {
       this.component = builder.component;
       this.branch = builder.branch;
+      this.pullRequest = builder.pullRequest;
       this.metrics = builder.metrics;
       this.from = builder.from;
       this.to = builder.to;
@@ -233,6 +241,11 @@ public class SearchHistoryAction implements MeasuresWsAction {
       return branch;
     }
 
+    @CheckForNull
+    public String getPullRequest() {
+      return pullRequest;
+    }
+
     public List<String> getMetrics() {
       return metrics;
     }
@@ -263,6 +276,7 @@ public class SearchHistoryAction implements MeasuresWsAction {
   static class Builder {
     private String component;
     private String branch;
+    private String pullRequest;
     private List<String> metrics;
     private String from;
     private String to;
@@ -283,6 +297,11 @@ public class SearchHistoryAction implements MeasuresWsAction {
       return this;
     }
 
+    public Builder setPullRequest(@Nullable String pullRequest) {
+      this.pullRequest = pullRequest;
+      return this;
+    }
+
     public Builder setMetrics(List<String> metrics) {
       this.metrics = metrics;
       return this;
index 7de860aff009e7dd841facb8eff7089ab46c02e8..21d6a286cb1addfd6b358ed688b5c2dbf7b45dac 100644 (file)
@@ -38,6 +38,7 @@ import org.sonar.server.authentication.LogOAuthWarning;
 import org.sonar.server.badge.ws.ProjectBadgesWsModule;
 import org.sonar.server.batch.BatchWsModule;
 import org.sonar.server.branch.BranchFeatureProxyImpl;
+import org.sonar.server.branch.pr.ws.PullRequestWsModule;
 import org.sonar.server.branch.ws.BranchWsModule;
 import org.sonar.server.ce.ws.CeWsModule;
 import org.sonar.server.component.ComponentCleanerService;
@@ -401,6 +402,7 @@ public class PlatformLevel4 extends PlatformLevel {
 
       // components
       BranchWsModule.class,
+      PullRequestWsModule.class,
       ProjectsWsModule.class,
       ProjectsEsModule.class,
       ProjectTagsWsModule.class,
index 2eb9f12db753c94bb1c5dc26a7096b082901dd43..a2c56bd7fbdf37edc7f141c0fbe0ba9416db3507 100644 (file)
@@ -28,6 +28,7 @@ public class ProjectAnalysesWsParameters {
   public static final String PARAM_FROM = "from";
   public static final String PARAM_TO = "to";
   public static final String PARAM_BRANCH = "branch";
+  public static final String PARAM_PULL_REQUEST = "pullRequest";
 
   private ProjectAnalysesWsParameters() {
     // static access only
index c6ace6ced7f5fd8e845c925ed3c881ae079bb182..49105d01b439622d4d4b7f7cfaebcf3374c8fe7a 100644 (file)
@@ -47,15 +47,17 @@ import static org.sonar.api.utils.DateUtils.parseStartingDateOrDateTime;
 import static org.sonar.core.util.Protobuf.setNullable;
 import static org.sonar.db.component.SnapshotQuery.SORT_FIELD.BY_DATE;
 import static org.sonar.db.component.SnapshotQuery.SORT_ORDER.DESC;
-import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
-import static org.sonar.server.ws.WsUtils.writeProtobuf;
 import static org.sonar.server.projectanalysis.ws.EventCategory.OTHER;
 import static org.sonar.server.projectanalysis.ws.ProjectAnalysesWsParameters.PARAM_BRANCH;
 import static org.sonar.server.projectanalysis.ws.ProjectAnalysesWsParameters.PARAM_CATEGORY;
 import static org.sonar.server.projectanalysis.ws.ProjectAnalysesWsParameters.PARAM_FROM;
 import static org.sonar.server.projectanalysis.ws.ProjectAnalysesWsParameters.PARAM_PROJECT;
+import static org.sonar.server.projectanalysis.ws.ProjectAnalysesWsParameters.PARAM_PULL_REQUEST;
 import static org.sonar.server.projectanalysis.ws.ProjectAnalysesWsParameters.PARAM_TO;
 import static org.sonar.server.projectanalysis.ws.SearchRequest.DEFAULT_PAGE_SIZE;
+import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
+import static org.sonar.server.ws.WsUtils.writeProtobuf;
 
 public class SearchAction implements ProjectAnalysesWsAction {
   private static final Set<String> ALLOWED_QUALIFIERS = ImmutableSet.of(Qualifiers.PROJECT, Qualifiers.APP, Qualifiers.VIEW);
@@ -92,6 +94,12 @@ public class SearchAction implements ProjectAnalysesWsAction {
       .setInternal(true)
       .setExampleValue(KEY_BRANCH_EXAMPLE_001);
 
+    action.createParam(PARAM_PULL_REQUEST)
+      .setDescription("Pull request id")
+      .setSince("7.1")
+      .setInternal(true)
+      .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001);
+
     action.createParam(PARAM_CATEGORY)
       .setDescription("Event category. Filter analyses that have at least one event of the category specified.")
       .setPossibleValues(EnumSet.allOf(EventCategory.class))
@@ -123,6 +131,7 @@ public class SearchAction implements ProjectAnalysesWsAction {
     return SearchRequest.builder()
       .setProject(request.mandatoryParam(PARAM_PROJECT))
       .setBranch(request.param(PARAM_BRANCH))
+      .setPullRequest(request.param(PARAM_PULL_REQUEST))
       .setCategory(category == null ? null : EventCategory.valueOf(category))
       .setPage(request.mandatoryParamAsInt(Param.PAGE))
       .setPageSize(request.mandatoryParamAsInt(Param.PAGE_SIZE))
@@ -170,10 +179,8 @@ public class SearchAction implements ProjectAnalysesWsAction {
   private ComponentDto loadComponent(DbSession dbSession, SearchRequest request) {
     String project = request.getProject();
     String branch = request.getBranch();
-    if (branch != null) {
-      return componentFinder.getByKeyAndBranch(dbSession, project, branch);
-    }
-    return componentFinder.getByKey(dbSession, project);
+    String pullRequest = request.getPullRequest();
+    return componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, project, branch, pullRequest);
   }
 
 }
index 83c118c53561d5c5cdde5b428716324a9c7536df..be9a5eade9aeffd6e2a5d410de7ee5accdbf86ef 100644 (file)
@@ -31,6 +31,7 @@ class SearchRequest {
 
   private final String project;
   private final String branch;
+  private final String pullRequest;
   private final EventCategory category;
   private final int page;
   private final int pageSize;
@@ -39,7 +40,8 @@ class SearchRequest {
 
   private SearchRequest(Builder builder) {
     this.project = builder.project;
-    this.branch= builder.branch;
+    this.branch = builder.branch;
+    this.pullRequest = builder.pullRequest;
     this.category = builder.category;
     this.page = builder.page;
     this.pageSize = builder.pageSize;
@@ -56,6 +58,11 @@ class SearchRequest {
     return branch;
   }
 
+  @CheckForNull
+  public String getPullRequest() {
+    return pullRequest;
+  }
+
   @CheckForNull
   public EventCategory getCategory() {
     return category;
@@ -86,6 +93,7 @@ class SearchRequest {
   public static class Builder {
     private String project;
     private String branch;
+    private String pullRequest;
     private EventCategory category;
     private int page = 1;
     private int pageSize = DEFAULT_PAGE_SIZE;
@@ -106,6 +114,11 @@ class SearchRequest {
       return this;
     }
 
+    public Builder setPullRequest(@Nullable String pullRequest) {
+      this.pullRequest = pullRequest;
+      return this;
+    }
+
     public Builder setCategory(@Nullable EventCategory category) {
       this.category = category;
       return this;
index b086408aa9237c955cfd882c1a3d8b97067ed2c5..801dd6ffa7387dc1108a29b22402bd72835c30a8 100644 (file)
@@ -28,7 +28,7 @@ import org.sonar.api.measures.CoreMetrics;
 import static org.sonar.db.qualitygate.QualityGateConditionDto.OPERATOR_GREATER_THAN;
 
 /**
- * Offers constants describing the Hardcoded Quality Gate for short living branches.
+ * Offers constants describing the Hardcoded Quality Gate for short living branches and pull requests.
  */
 public final class ShortLivingBranchQualityGate {
   public static final long ID = -1_963_456_987L;
index 88fa20eb41a513762e902f76ed272f6361ecf9ce..7f7ce140c694df715e71736f206bc7bbc9eaefb4 100644 (file)
@@ -40,6 +40,12 @@ public interface QGChangeEventListener {
   }
 
   enum Status {
-    OPEN, CONFIRMED, REOPENED, RESOLVED, CLOSED
+    OPEN,
+    CONFIRMED,
+    REOPENED,
+    RESOLVED_FP,
+    RESOLVED_WF,
+    RESOLVED_FIXED
   }
+
 }
index b2fe05c836fceae3e52af4b7ccdbfb15e4fd6212..d46a318820fef93ba215eefc4e42663efdb59f31 100644 (file)
@@ -23,7 +23,9 @@ import com.google.common.collect.Multimap;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
+import org.sonar.api.issue.Issue;
 import org.sonar.api.rules.RuleType;
 import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
@@ -95,17 +97,47 @@ public class QGChangeEventListenersImpl implements QGChangeEventListeners {
     }
   }
 
-  private static class ChangedIssueImpl implements ChangedIssue {
+  static class ChangedIssueImpl implements ChangedIssue {
     private final String key;
     private final QGChangeEventListener.Status status;
     private final RuleType type;
 
     private ChangedIssueImpl(DefaultIssue issue) {
       this.key = issue.key();
-      this.status = QGChangeEventListener.Status.valueOf(issue.getStatus());
+      this.status = statusOf(issue);
       this.type = issue.type();
     }
 
+    static QGChangeEventListener.Status statusOf(DefaultIssue issue) {
+      switch (issue.status()) {
+        case Issue.STATUS_OPEN:
+          return QGChangeEventListener.Status.OPEN;
+        case Issue.STATUS_CONFIRMED:
+          return QGChangeEventListener.Status.CONFIRMED;
+        case Issue.STATUS_REOPENED:
+          return QGChangeEventListener.Status.REOPENED;
+        case Issue.STATUS_RESOLVED:
+          return statusOfResolved(issue);
+        default:
+          throw new IllegalStateException("Unexpected status: " + issue.status());
+      }
+    }
+
+    private static QGChangeEventListener.Status statusOfResolved(DefaultIssue issue) {
+      String resolution = issue.resolution();
+      Objects.requireNonNull(resolution, "A resolved issue should have a resolution");
+      switch (resolution) {
+        case Issue.RESOLUTION_FALSE_POSITIVE:
+          return QGChangeEventListener.Status.RESOLVED_FP;
+        case Issue.RESOLUTION_WONT_FIX:
+          return QGChangeEventListener.Status.RESOLVED_WF;
+        case Issue.RESOLUTION_FIXED:
+          return QGChangeEventListener.Status.RESOLVED_FIXED;
+        default:
+          throw new IllegalStateException("Unexpected resolution for a resolved issue: " + resolution);
+      }
+    }
+
     @Override
     public String getKey() {
       return key;
index 5118f8368d9b3aa89041b376688307639af31e39..c7ed814898fb1523dd7ad076d62c32fde582aa1c 100644 (file)
@@ -21,6 +21,8 @@ package org.sonar.server.setting.ws;
 
 import java.util.List;
 import java.util.Optional;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
 import org.sonar.api.config.PropertyDefinition;
 import org.sonar.api.config.PropertyDefinitions;
 import org.sonar.api.config.PropertyFieldDefinition;
@@ -42,6 +44,7 @@ import static org.sonar.core.util.Protobuf.setNullable;
 import static org.sonar.server.setting.ws.SettingsWs.SETTING_ON_BRANCHES;
 import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_BRANCH;
 import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_COMPONENT;
+import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_PULL_REQUEST;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
 
@@ -81,6 +84,7 @@ public class ListDefinitionsAction implements SettingsWsAction {
       .setDescription("Component key")
       .setExampleValue(KEY_PROJECT_EXAMPLE_001);
     settingsWsSupport.addBranchParam(action);
+    settingsWsSupport.addPullRequestParam(action);
   }
 
   @Override
@@ -107,7 +111,8 @@ public class ListDefinitionsAction implements SettingsWsAction {
   private static ListDefinitionsRequest toWsRequest(Request request) {
     return new ListDefinitionsRequest()
       .setComponent(request.param(PARAM_COMPONENT))
-      .setBranch(request.param(PARAM_BRANCH));
+      .setBranch(request.param(PARAM_BRANCH))
+      .setPullRequest(request.param(PARAM_PULL_REQUEST));
   }
 
   private static Optional<String> getQualifier(Optional<ComponentDto> component) {
@@ -120,7 +125,7 @@ public class ListDefinitionsAction implements SettingsWsAction {
       if (componentKey == null) {
         return Optional.empty();
       }
-      ComponentDto component = componentFinder.getByKeyAndOptionalBranch(dbSession, componentKey, request.getBranch());
+      ComponentDto component = componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, componentKey, request.getBranch(), request.getPullRequest());
       userSession.checkComponentPermission(USER, component);
       return Optional.of(component);
     }
@@ -164,23 +169,36 @@ public class ListDefinitionsAction implements SettingsWsAction {
 
     private String branch;
     private String component;
+    private String pullRequest;
 
-    public ListDefinitionsRequest setBranch(String branch) {
+    public ListDefinitionsRequest setComponent(@Nullable String component) {
+      this.component = component;
+      return this;
+    }
+
+    @CheckForNull
+    public String getComponent() {
+      return component;
+    }
+
+    public ListDefinitionsRequest setBranch(@Nullable String branch) {
       this.branch = branch;
       return this;
     }
 
+    @CheckForNull
     public String getBranch() {
       return branch;
     }
 
-    public ListDefinitionsRequest setComponent(String component) {
-      this.component = component;
+    public ListDefinitionsRequest setPullRequest(@Nullable String pullRequest) {
+      this.pullRequest = pullRequest;
       return this;
     }
 
-    public String getComponent() {
-      return component;
+    @CheckForNull
+    public String getPullRequest() {
+      return pullRequest;
     }
   }
 }
index 3c35e729f02b44aff7cb3f39a5465fb7493ed624..8a65b530f88bf709b9515c569cb739736e007a5b 100644 (file)
@@ -23,6 +23,8 @@ import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
 import org.sonar.api.config.PropertyDefinition;
 import org.sonar.api.config.PropertyDefinitions;
 import org.sonar.api.server.ws.Change;
@@ -42,8 +44,10 @@ import static java.util.Collections.emptyList;
 import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_BRANCH;
 import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_COMPONENT;
 import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_KEYS;
+import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_PULL_REQUEST;
 import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
 
 public class ResetAction implements SettingsWsAction {
 
@@ -92,6 +96,11 @@ public class ResetAction implements SettingsWsAction {
       .setExampleValue(KEY_BRANCH_EXAMPLE_001)
       .setInternal(true)
       .setSince("6.6");
+    action.createParam(PARAM_PULL_REQUEST)
+      .setDescription("Pull request id")
+      .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001)
+      .setInternal(true)
+      .setSince("7.1");
   }
 
   @Override
@@ -129,9 +138,10 @@ public class ResetAction implements SettingsWsAction {
 
   private static ResetRequest toWsRequest(Request request) {
     return new ResetRequest()
-      .setKeys(request.paramAsStrings(PARAM_KEYS))
+      .setKeys(request.mandatoryParamAsStrings(PARAM_KEYS))
       .setComponent(request.param(PARAM_COMPONENT))
-      .setBranch(request.param(PARAM_BRANCH));
+      .setBranch(request.param(PARAM_BRANCH))
+      .setPullRequest(request.param(PARAM_PULL_REQUEST));
   }
 
   private Optional<ComponentDto> getComponent(DbSession dbSession, ResetRequest request) {
@@ -139,7 +149,7 @@ public class ResetAction implements SettingsWsAction {
     if (componentKey == null) {
       return Optional.empty();
     }
-    return Optional.of(componentFinder.getByKeyAndOptionalBranch(dbSession, componentKey, request.getBranch()));
+    return Optional.of(componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, componentKey, request.getBranch(), request.getPullRequest()));
   }
 
   private void checkPermissions(Optional<ComponentDto> component) {
@@ -153,23 +163,36 @@ public class ResetAction implements SettingsWsAction {
   private static class ResetRequest {
 
     private String branch;
+    private String pullRequest;
     private String component;
     private List<String> keys;
 
-    public ResetRequest setBranch(String branch) {
+    public ResetRequest setBranch(@Nullable String branch) {
       this.branch = branch;
       return this;
     }
 
+    @CheckForNull
     public String getBranch() {
       return branch;
     }
 
-    public ResetRequest setComponent(String component) {
+    public ResetRequest setPullRequest(@Nullable String pullRequest) {
+      this.pullRequest = pullRequest;
+      return this;
+    }
+
+    @CheckForNull
+    public String getPullRequest() {
+      return pullRequest;
+    }
+
+    public ResetRequest setComponent(@Nullable String component) {
       this.component = component;
       return this;
     }
 
+    @CheckForNull
     public String getComponent() {
       return component;
     }
index ed4a9943cd622e9b7452a68671696dad2989f5b2..2113ff2070f55c70a838025b7c5cf2eec4566b6b 100644 (file)
@@ -34,6 +34,7 @@ import java.util.Set;
 import java.util.stream.Collector;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
+import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import org.apache.commons.lang.StringUtils;
 import org.sonar.api.PropertyType;
@@ -63,6 +64,7 @@ import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_BRANCH;
 import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_COMPONENT;
 import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_FIELD_VALUES;
 import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_KEY;
+import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_PULL_REQUEST;
 import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_VALUE;
 import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_VALUES;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
@@ -133,7 +135,9 @@ public class SetAction implements SettingsWsAction {
       .setDescription("Component key")
       .setDeprecatedKey("componentKey", "6.3")
       .setExampleValue(KEY_PROJECT_EXAMPLE_001);
+
     settingsWsSupport.addBranchParam(action);
+    settingsWsSupport.addPullRequestParam(action);
   }
 
   @Override
@@ -293,7 +297,8 @@ public class SetAction implements SettingsWsAction {
       .setValues(request.multiParam(PARAM_VALUES))
       .setFieldValues(request.multiParam(PARAM_FIELD_VALUES))
       .setComponent(request.param(PARAM_COMPONENT))
-      .setBranch(request.param(PARAM_BRANCH));
+      .setBranch(request.param(PARAM_BRANCH))
+      .setPullRequest(request.param(PARAM_PULL_REQUEST));
     checkArgument(isNotEmpty(set.getKey()), "Setting key is mandatory and must not be empty");
     checkArgument(set.getValues() != null, "Setting values must not be null");
     checkArgument(set.getFieldValues() != null, "Setting fields values must not be null");
@@ -316,7 +321,7 @@ public class SetAction implements SettingsWsAction {
     if (componentKey == null) {
       return Optional.empty();
     }
-    return Optional.of(componentFinder.getByKeyAndOptionalBranch(dbSession, componentKey, request.getBranch()));
+    return Optional.of(componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, componentKey, request.getBranch(), request.getPullRequest()));
   }
 
   private PropertyDto toProperty(SetRequest request, Optional<ComponentDto> component) {
@@ -351,26 +356,39 @@ public class SetAction implements SettingsWsAction {
   private static class SetRequest {
 
     private String branch;
+    private String pullRequest;
     private String component;
     private List<String> fieldValues;
     private String key;
     private String value;
     private List<String> values;
 
-    public SetRequest setBranch(String branch) {
+    public SetRequest setBranch(@Nullable String branch) {
       this.branch = branch;
       return this;
     }
 
+    @CheckForNull
     public String getBranch() {
       return branch;
     }
 
-    public SetRequest setComponent(String component) {
+    public SetRequest setPullRequest(@Nullable String pullRequest) {
+      this.pullRequest = pullRequest;
+      return this;
+    }
+
+    @CheckForNull
+    public String getPullRequest() {
+      return pullRequest;
+    }
+
+    public SetRequest setComponent(@Nullable String component) {
       this.component = component;
       return this;
     }
 
+    @CheckForNull
     public String getComponent() {
       return component;
     }
@@ -393,16 +411,17 @@ public class SetAction implements SettingsWsAction {
       return key;
     }
 
-    public SetRequest setValue(String value) {
+    public SetRequest setValue(@Nullable String value) {
       this.value = value;
       return this;
     }
 
+    @CheckForNull
     public String getValue() {
       return value;
     }
 
-    public SetRequest setValues(List<String> values) {
+    public SetRequest setValues(@Nullable List<String> values) {
       this.values = values;
       return this;
     }
index 61cdd2b3c0f4e569ad5009a53c1eb63625be87ad..d46cb8d0c1cab2cbd5c6bfe5552e1c303948b14b 100644 (file)
@@ -43,6 +43,7 @@ public class SettingsWsParameters {
 
   public static final String PARAM_COMPONENT = "component";
   public static final String PARAM_BRANCH = "branch";
+  public static final String PARAM_PULL_REQUEST = "pullRequest";
   public static final String PARAM_KEYS = "keys";
   public static final String PARAM_KEY = "key";
   public static final String PARAM_VALUE = "value";
index c53bf41b24b9140d35a3d116c029bd655e73f4b9..ab3d550195ae7695cec1538e6e6efef3b42e7e18 100644 (file)
@@ -39,7 +39,9 @@ import static org.sonar.api.PropertyType.LICENSE;
 import static org.sonar.api.web.UserRole.ADMIN;
 import static org.sonar.core.permission.GlobalPermissions.SCAN_EXECUTION;
 import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_BRANCH;
+import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_PULL_REQUEST;
 import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
 
 @ServerSide
 public class SettingsWsSupport {
@@ -108,4 +110,12 @@ public class SettingsWsSupport {
       .setInternal(true)
       .setSince("6.6");
   }
+
+  WebService.NewParam addPullRequestParam(WebService.NewAction action) {
+    return action.createParam(PARAM_PULL_REQUEST)
+      .setDescription("Pull request. Only available on following settings : %s", SettingsWs.SETTING_ON_BRANCHES.stream().collect(COMMA_JOINER))
+      .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001)
+      .setInternal(true)
+      .setSince("7.1");
+  }
 }
index 57fefaeb3a0c881a3e17723e334e65b9400d8d48..ec2d3a0ee236ecffd8fc9bacd44c784b14d89355 100644 (file)
@@ -30,6 +30,8 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
 import org.sonar.api.config.PropertyDefinition;
 import org.sonar.api.config.PropertyDefinitions;
 import org.sonar.api.server.ws.Change;
@@ -56,8 +58,8 @@ import static org.sonar.core.permission.GlobalPermissions.SCAN_EXECUTION;
 import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_BRANCH;
 import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_COMPONENT;
 import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_KEYS;
+import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_PULL_REQUEST;
 import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException;
-import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
 
@@ -108,11 +110,8 @@ public class ValuesAction implements SettingsWsAction {
     action.createParam(PARAM_COMPONENT)
       .setDescription("Component key")
       .setExampleValue(KEY_PROJECT_EXAMPLE_001);
-    action.createParam(PARAM_BRANCH)
-      .setDescription("Branch key")
-      .setExampleValue(KEY_BRANCH_EXAMPLE_001)
-      .setInternal(true)
-      .setSince("6.6");
+    settingsWsSupport.addBranchParam(action);
+    settingsWsSupport.addPullRequestParam(action);
   }
 
   @Override
@@ -136,7 +135,8 @@ public class ValuesAction implements SettingsWsAction {
   private static ValuesRequest toWsRequest(Request request) {
     ValuesRequest result = new ValuesRequest()
       .setComponent(request.param(PARAM_COMPONENT))
-      .setBranch(request.param(PARAM_BRANCH));
+      .setBranch(request.param(PARAM_BRANCH))
+      .setPullRequest(request.param(PARAM_PULL_REQUEST));
     if (request.hasParam(PARAM_KEYS)) {
       result.setKeys(request.paramAsStrings(PARAM_KEYS));
     }
@@ -154,11 +154,11 @@ public class ValuesAction implements SettingsWsAction {
     if (componentKey == null) {
       return Optional.empty();
     }
-    ComponentDto component = componentFinder.getByKeyAndOptionalBranch(dbSession, componentKey, valuesRequest.getBranch());
+    ComponentDto component = componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, componentKey, valuesRequest.getBranch(), valuesRequest.getPullRequest());
     if (!userSession.hasComponentPermission(USER, component) &&
-        !userSession.hasComponentPermission(SCAN_EXECUTION, component) &&
-        !userSession.hasPermission(OrganizationPermission.SCAN, component.getOrganizationUuid())) {
-        throw insufficientPrivilegesException();
+      !userSession.hasComponentPermission(SCAN_EXECUTION, component) &&
+      !userSession.hasPermission(OrganizationPermission.SCAN, component.getOrganizationUuid())) {
+      throw insufficientPrivilegesException();
     }
     return Optional.of(component);
   }
@@ -303,32 +303,46 @@ public class ValuesAction implements SettingsWsAction {
   private static class ValuesRequest {
 
     private String branch;
+    private String pullRequest;
     private String component;
     private List<String> keys;
 
-    public ValuesRequest setBranch(String branch) {
+    public ValuesRequest setBranch(@Nullable String branch) {
       this.branch = branch;
       return this;
     }
 
+    @CheckForNull
     public String getBranch() {
       return branch;
     }
 
-    public ValuesRequest setComponent(String component) {
+    public ValuesRequest setPullRequest(@Nullable String pullRequest) {
+      this.pullRequest = pullRequest;
+      return this;
+    }
+
+    @CheckForNull
+    public String getPullRequest() {
+      return pullRequest;
+    }
+
+    public ValuesRequest setComponent(@Nullable String component) {
       this.component = component;
       return this;
     }
 
+    @CheckForNull
     public String getComponent() {
       return component;
     }
 
-    public ValuesRequest setKeys(List<String> keys) {
+    public ValuesRequest setKeys(@Nullable List<String> keys) {
       this.keys = keys;
       return this;
     }
 
+    @CheckForNull
     public List<String> getKeys() {
       return keys;
     }
index 334a71e92124c2cc7052144d4484f2b274801665..263f4c7539e0c9a53f48fbaf4c650b84afd58691 100644 (file)
@@ -43,7 +43,9 @@ import static com.google.common.base.Preconditions.checkArgument;
 import static org.sonar.server.component.ComponentFinder.ParamNames.UUID_AND_KEY;
 import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
 import static org.sonar.server.ws.KeyExamples.KEY_FILE_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
 import static org.sonar.server.ws.WsUtils.checkFoundWithOptional;
+import static org.sonar.server.ws.WsUtils.checkRequest;
 
 public class LinesAction implements SourcesWsAction {
 
@@ -52,6 +54,7 @@ public class LinesAction implements SourcesWsAction {
   private static final String PARAM_FROM = "from";
   private static final String PARAM_TO = "to";
   private static final String PARAM_BRANCH = "branch";
+  private static final String PARAM_PULL_REQUEST = "pullRequest";
 
   private final ComponentFinder componentFinder;
   private final SourceService sourceService;
@@ -91,7 +94,7 @@ public class LinesAction implements SourcesWsAction {
           "has been renamed \"lineHits\", \"conditions\" and \"coveredConditions\""),
         new Change("6.2", "fields \"itLineHits\", \"itConditions\" and \"itCoveredConditions\" " +
           "are no more returned"),
-        new Change("6.6", "fields \"branch\" added"))
+        new Change("6.6", "field \"branch\" added"))
       .setHandler(this);
 
     action
@@ -110,6 +113,12 @@ public class LinesAction implements SourcesWsAction {
       .setInternal(true)
       .setExampleValue(KEY_BRANCH_EXAMPLE_001);
 
+    action
+      .createParam(PARAM_PULL_REQUEST)
+      .setDescription("Pull request id")
+      .setInternal(true)
+      .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001);
+
     action
       .createParam(PARAM_FROM)
       .setDescription("First line to return. Starts from 1")
@@ -146,11 +155,15 @@ public class LinesAction implements SourcesWsAction {
     String componentKey = wsRequest.param(PARAM_KEY);
     String componentId = wsRequest.param(PARAM_UUID);
     String branch = wsRequest.param(PARAM_BRANCH);
-    checkArgument(componentId == null || branch == null, "'%s' and '%s' parameters cannot be used at the same time", PARAM_UUID,
-      PARAM_BRANCH);
-    return branch == null
-      ? componentFinder.getByUuidOrKey(dbSession, componentId, componentKey, UUID_AND_KEY)
-      : componentFinder.getByKeyAndBranch(dbSession, componentKey, branch);
+    String pullRequest = wsRequest.param(PARAM_PULL_REQUEST);
+    checkArgument(componentId == null || (branch == null && pullRequest == null), "Parameter '%s' cannot be used at the same time as '%s' or '%s'",
+      PARAM_UUID, PARAM_BRANCH, PARAM_PULL_REQUEST);
+    if (branch == null && pullRequest == null) {
+      return componentFinder.getByUuidOrKey(dbSession, componentId, componentKey, UUID_AND_KEY);
+    }
+
+    checkRequest(componentKey!=null, "The '%s' parameter is missing", PARAM_KEY);
+    return componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, componentKey, branch, pullRequest);
   }
 
   private void writeSource(Iterable<DbFileSources.Line> lines, JsonWriter json) {
index 34a391c9a7d32e9164148cb22567a70ec5d93185..0591acd2a45a9e3c1a32a64bee1f96b66cc8e55b 100644 (file)
@@ -36,9 +36,13 @@ import org.sonar.server.source.SourceService;
 import org.sonar.server.user.UserSession;
 
 import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
 
 public class RawAction implements SourcesWsAction {
 
+  private static final String PARAM_KEY = "key";
+  private static final String PARAM_BRANCH = "branch";
+  private static final String PARAM_PULL_REQUEST = "pullRequest";
   private final DbClient dbClient;
   private final SourceService sourceService;
   private final UserSession userSession;
@@ -60,24 +64,31 @@ public class RawAction implements SourcesWsAction {
       .setHandler(this);
 
     action
-      .createParam("key")
+      .createParam(PARAM_KEY)
       .setRequired(true)
       .setDescription("File key")
       .setExampleValue("my_project:src/foo/Bar.php");
 
     action
-      .createParam("branch")
+      .createParam(PARAM_BRANCH)
       .setDescription("Branch key")
       .setInternal(true)
       .setExampleValue(KEY_BRANCH_EXAMPLE_001);
+
+    action
+      .createParam(PARAM_PULL_REQUEST)
+      .setDescription("Pull request id")
+      .setInternal(true)
+      .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001);
   }
 
   @Override
   public void handle(Request request, Response response) {
-    String fileKey = request.mandatoryParam("key");
-    String branch = request.param("branch");
+    String fileKey = request.mandatoryParam(PARAM_KEY);
+    String branch = request.param(PARAM_BRANCH);
+    String pullRequest = request.param(PARAM_PULL_REQUEST);
     try (DbSession dbSession = dbClient.openSession(false)) {
-      ComponentDto file = componentFinder.getByKeyAndOptionalBranch(dbSession, fileKey, branch);
+      ComponentDto file = componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, fileKey, branch, pullRequest);
       userSession.checkComponentPermission(UserRole.CODEVIEWER, file);
 
       Optional<Iterable<String>> lines = sourceService.getLinesAsRawText(dbSession, file.uuid(), 1, Integer.MAX_VALUE);
index 27f733238f9dff1c0ad83fc432f70b378f4b073c..ab1110af674b8343f51c36e08affcb7c7d5b88ce 100644 (file)
@@ -54,6 +54,7 @@ import static org.sonar.api.web.UserRole.CODEVIEWER;
 import static org.sonar.core.util.Protobuf.setNullable;
 import static org.sonar.server.es.SearchOptions.MAX_LIMIT;
 import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
 import static org.sonar.server.ws.WsUtils.checkFoundWithOptional;
 
 public class ListAction implements TestsWsAction {
@@ -64,6 +65,7 @@ public class ListAction implements TestsWsAction {
   public static final String SOURCE_FILE_KEY = "sourceFileKey";
   public static final String SOURCE_FILE_LINE_NUMBER = "sourceFileLineNumber";
   public static final String PARAM_BRANCH = "branch";
+  public static final String PARAM_PULL_REQUEST = "pullRequest";
 
   private final DbClient dbClient;
   private final TestIndex testIndex;
@@ -98,6 +100,7 @@ public class ListAction implements TestsWsAction {
       .setDeprecatedSince("5.6")
       .setHandler(this)
       .setChangelog(new Change("6.6", "\"fileBranch\" field is now returned"))
+      .setChangelog(new Change("7.1", "\"filePullRequest\" field is now returned"))
       .addPagingParams(100, MAX_LIMIT);
 
     action
@@ -136,6 +139,12 @@ public class ListAction implements TestsWsAction {
       .setSince("6.6")
       .setInternal(true)
       .setExampleValue(KEY_BRANCH_EXAMPLE_001);
+
+    action.createParam(PARAM_PULL_REQUEST)
+      .setDescription("Pull request id")
+      .setSince("7.1")
+      .setInternal(true)
+      .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001);
   }
 
   @Override
@@ -146,6 +155,7 @@ public class ListAction implements TestsWsAction {
     String sourceFileUuid = request.param(SOURCE_FILE_ID);
     String sourceFileKey = request.param(SOURCE_FILE_KEY);
     String branch = request.param(PARAM_BRANCH);
+    String pullRequest = request.param(PARAM_PULL_REQUEST);
     Integer sourceFileLineNumber = request.paramAsInt(SOURCE_FILE_LINE_NUMBER);
     SearchOptions searchOptions = new SearchOptions().setPage(
       request.mandatoryParamAsInt(PAGE),
@@ -154,7 +164,7 @@ public class ListAction implements TestsWsAction {
     SearchResult<TestDoc> tests;
     Map<String, ComponentDto> componentsByTestFileUuid;
     try (DbSession dbSession = dbClient.openSession(false)) {
-      tests = searchTests(dbSession, testUuid, testFileUuid, testFileKey, sourceFileUuid, sourceFileKey, branch, sourceFileLineNumber, searchOptions);
+      tests = searchTests(dbSession, testUuid, testFileUuid, testFileKey, sourceFileUuid, sourceFileKey, branch, pullRequest, sourceFileLineNumber, searchOptions);
       componentsByTestFileUuid = buildComponentsByTestFileUuid(dbSession, tests.getDocs());
     }
 
@@ -175,6 +185,7 @@ public class ListAction implements TestsWsAction {
         testBuilder.setFileKey(component.getKey());
         testBuilder.setFileName(component.longName());
         setNullable(component.getBranch(), testBuilder::setFileBranch);
+        setNullable(component.getPullRequest(), testBuilder::setFilePullRequest);
       }
       testBuilder.setStatus(Tests.TestStatus.valueOf(testDoc.status()));
       if (testDoc.durationInMs() != null) {
@@ -209,7 +220,8 @@ public class ListAction implements TestsWsAction {
   }
 
   private SearchResult<TestDoc> searchTests(DbSession dbSession, @Nullable String testUuid, @Nullable String testFileUuid, @Nullable String testFileKey,
-    @Nullable String sourceFileUuid, @Nullable String sourceFileKey, @Nullable String branch, @Nullable Integer sourceFileLineNumber, SearchOptions searchOptions) {
+    @Nullable String sourceFileUuid, @Nullable String sourceFileKey, @Nullable String branch, @Nullable String pullRequest,
+    @Nullable Integer sourceFileLineNumber, SearchOptions searchOptions) {
     if (testUuid != null) {
       TestDoc testDoc = checkFoundWithOptional(testIndex.getNullableByTestUuid(testUuid), "Test with id '%s' is not found", testUuid);
       checkComponentUuidPermission(dbSession, testDoc.fileUuid());
@@ -220,7 +232,7 @@ public class ListAction implements TestsWsAction {
       return testIndex.searchByTestFileUuid(testFileUuid, searchOptions);
     }
     if (testFileKey != null) {
-      ComponentDto testFile = componentFinder.getByKeyAndOptionalBranch(dbSession, testFileKey, branch);
+      ComponentDto testFile = componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, testFileKey, branch, pullRequest);
       userSession.checkComponentPermission(CODEVIEWER, testFile);
       return testIndex.searchByTestFileUuid(testFile.uuid(), searchOptions);
     }
@@ -230,7 +242,7 @@ public class ListAction implements TestsWsAction {
       return testIndex.searchBySourceFileUuidAndLineNumber(sourceFile.uuid(), sourceFileLineNumber, searchOptions);
     }
     if (sourceFileKey != null && sourceFileLineNumber != null) {
-      ComponentDto sourceFile = componentFinder.getByKeyAndOptionalBranch(dbSession, sourceFileKey, branch);
+      ComponentDto sourceFile = componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, sourceFileKey, branch, pullRequest);
       userSession.checkComponentPermission(CODEVIEWER, sourceFile);
       return testIndex.searchBySourceFileUuidAndLineNumber(sourceFile.uuid(), sourceFileLineNumber, searchOptions);
     }
index 580a62cd94abc5a40e9216315e10ae135177bd90..f0c22150fe76de452c855d2eea953fc6cd630cb4 100644 (file)
@@ -68,11 +68,13 @@ import static org.sonar.db.permission.OrganizationPermission.ADMINISTER_QUALITY_
 import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException;
 import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
 
 public class ComponentAction implements NavigationWsAction {
 
   static final String PARAM_COMPONENT = "component";
   static final String PARAM_BRANCH = "branch";
+  static final String PARAM_PULL_REQUEST = "pullRequest";
 
   private static final String PROPERTY_CONFIGURABLE = "configurable";
   private static final String PROPERTY_HAS_ROLE_POLICY = "hasRolePolicy";
@@ -124,6 +126,12 @@ public class ComponentAction implements NavigationWsAction {
       .setDescription("Branch key")
       .setInternal(true)
       .setExampleValue(KEY_BRANCH_EXAMPLE_001);
+
+    projectNavigation
+      .createParam(PARAM_PULL_REQUEST)
+      .setDescription("Pull request id")
+      .setInternal(true)
+      .setExampleValue(KEY_PULL_REQUEST_EXAMPLE_001);
   }
 
   @Override
@@ -131,7 +139,8 @@ public class ComponentAction implements NavigationWsAction {
     String componentKey = request.mandatoryParam(PARAM_COMPONENT);
     try (DbSession session = dbClient.openSession(false)) {
       String branch = request.param(PARAM_BRANCH);
-      ComponentDto component = componentFinder.getByKeyAndOptionalBranch(session, componentKey, branch);
+      String pullRequest = request.param(PARAM_PULL_REQUEST);
+      ComponentDto component = componentFinder.getByKeyAndOptionalBranchOrPullRequest(session, componentKey, branch, pullRequest);
       if (!userSession.hasComponentPermission(USER, component) &&
         !userSession.hasComponentPermission(ADMIN, component) &&
         !userSession.isSystemAdministrator()) {
index c4ab3e786ff147651849b7216ac05464db00fa88..96f91e244aebc14483cd418e3a732a46b261e5fa 100644 (file)
@@ -80,6 +80,7 @@ public class CreateAction implements UsersWsAction {
         new Change("6.3", "The password is only mandatory when creating local users, and should not be set on non local users"),
         new Change("6.3", "The 'infos' message is no more returned when a user is reactivated"))
       .setPost(true)
+      .setResponseExample(getClass().getResource("create-example.json"))
       .setHandler(this);
 
     action.createParam(PARAM_LOGIN)
@@ -104,13 +105,13 @@ public class CreateAction implements UsersWsAction {
       .setExampleValue("myname@email.com");
 
     action.createParam(PARAM_SCM_ACCOUNTS)
-      .setDescription("This parameter is deprecated, please use '%s' instead", PARAM_SCM_ACCOUNT)
+      .setDescription("Comma-separated list of SCM accounts. This parameter is deprecated, please use '%s' instead", PARAM_SCM_ACCOUNT)
       .setDeprecatedKey(PARAM_SCM_ACCOUNTS_DEPRECATED, "6.0")
       .setDeprecatedSince("6.1")
       .setExampleValue("myscmaccount1,myscmaccount2");
 
     action.createParam(PARAM_SCM_ACCOUNT)
-      .setDescription("SCM accounts. To set several values, the parameter must be called once for each value.")
+      .setDescription("List of SCM accounts. To set several values, the parameter must be called once for each value.")
       .setExampleValue("scmAccount=firstValue&scmAccount=secondValue&scmAccount=thirdValue");
 
     action.createParam(PARAM_LOCAL)
index d9fb7b1fd8eca56f0fd91b445d82480246445911..92bebf6eb56da98fc0dc24a92d78b11cf7b764a7 100644 (file)
@@ -122,7 +122,7 @@ public class SetHomepageAction implements UsersWsAction {
     switch (type) {
       case PROJECT:
         checkArgument(isNotBlank(componentParameter), PARAMETER_REQUIRED, type.name(), PARAM_COMPONENT);
-        return componentFinder.getByKeyAndOptionalBranch(dbSession, componentParameter, branchParameter).uuid();
+        return componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, componentParameter, branchParameter, null).uuid();
       case PORTFOLIO:
       case APPLICATION:
         checkArgument(isNotBlank(componentParameter), PARAMETER_REQUIRED, type.name(), PARAM_COMPONENT);
index c17c7de7f86ffb3856f37403bdc09cce60628f50..2d62e584785d77590025d6ef1c0d5f9eaa88977c 100644 (file)
@@ -49,7 +49,7 @@ public final class Branch {
   }
 
   public enum Type {
-    LONG, SHORT
+    LONG, SHORT, PULL_REQUEST
   }
 
   @Override
index 96c24d254fb9db4d54159b4f9633219eba6b95b8..39f6d102c5b47de11be2efb5f8b30dd25831117a 100644 (file)
@@ -123,10 +123,16 @@ public class WebhookPayloadFactoryImpl implements WebhookPayloadFactory {
       }
       return format("%s/dashboard?branch=%s&id=%s",
         server.getPublicRootUrl(), encode(branch.getName().orElse("")), encode(project.getKey()));
-    } else {
+    }
+    if (branch.getType() == Branch.Type.SHORT) {
       return format("%s/project/issues?branch=%s&id=%s&resolved=false",
         server.getPublicRootUrl(), encode(branch.getName().orElse("")), encode(project.getKey()));
     }
+    if (branch.getType() == Branch.Type.PULL_REQUEST) {
+      return format("%s/project/issues?pullRequest=%s&id=%s&resolved=false",
+        server.getPublicRootUrl(), encode(branch.getName().orElse("")), encode(project.getKey()));
+    }
+    return projectUrlOf(project);
   }
 
   private static void writeQualityGate(JsonWriter writer, EvaluatedQualityGate gate) {
index 2afa9edbc149a94e8b89c0c2faa0d3195a95bbcc..dea020eec4d0c7389c448f4b86bdcc21ebbcf120 100644 (file)
@@ -30,6 +30,7 @@ public class KeyExamples {
   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 NAME_WEBHOOK_EXAMPLE_001 = "My Webhook";
   public static final String URL_WEBHOOK_EXAMPLE_001 = "https://www.my-webhook-listener.com/sonar";
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/branch/pr/ws/list-example.json b/server/sonar-server/src/main/resources/org/sonar/server/branch/pr/ws/list-example.json
new file mode 100644 (file)
index 0000000..a925c1f
--- /dev/null
@@ -0,0 +1,17 @@
+{
+  "pullRequests": [
+    {
+      "key": "123",
+      "title": "Add feature X",
+      "branch": "feature/bar",
+      "base": "feature/foo",
+      "status": {
+        "bugs": 0,
+        "vulnerabilities": 0,
+        "codeSmells": 0
+      },
+      "analysisDate": "2017-04-01T02:15:42+0200",
+      "url": "https://github.com/SonarSource/sonar-core-plugins/pull/32"
+    }
+  ]
+}
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/user/ws/create-example.json b/server/sonar-server/src/main/resources/org/sonar/server/user/ws/create-example.json
new file mode 100644 (file)
index 0000000..b4bdfda
--- /dev/null
@@ -0,0 +1,10 @@
+{
+  "user": {
+    "login": "ada.lovelace",
+    "name": "Ada Lovelace",
+    "email": "ada.lovelace@noteg.com",
+    "scmAccounts": ["ada.lovelace"],
+    "active": true,
+    "local": true
+  }
+}
index 0f7a82d7c52af29b3bab201378ce642f34020342..034441c567d72b9e8ae0884595b55aedd9e6501e 100644 (file)
@@ -286,6 +286,7 @@ public class MeasureActionTest {
       .containsExactlyInAnyOrder(
         tuple("project", true),
         tuple("branch", false),
+        tuple("pullRequest", false),
         tuple("metric", true));
   }
 
index 56da90807b75d5153012fd622a492b7e925d9ac2..2071e6c2c6f4a71b36de82b51ee605fde97e7a7d 100644 (file)
@@ -189,7 +189,8 @@ public class QualityGateActionTest {
       .extracting(Param::key, Param::isRequired)
       .containsExactlyInAnyOrder(
         tuple("project", true),
-        tuple("branch", false));
+        tuple("branch", false),
+        tuple("pullRequest", false));
   }
 
   private MetricDto createQualityGateMetric() {
diff --git a/server/sonar-server/src/test/java/org/sonar/server/branch/pr/ws/DeleteActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/branch/pr/ws/DeleteActionTest.java
new file mode 100644 (file)
index 0000000..1301ef7
--- /dev/null
@@ -0,0 +1,147 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.branch.pr.ws;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.utils.System2;
+import org.sonar.api.web.UserRole;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.component.ComponentCleanerService;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.component.TestComponentFinder;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.WsActionTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
+
+public class DeleteActionTest {
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Rule
+  public DbTester db = DbTester.create(System2.INSTANCE);
+
+  private ComponentCleanerService componentCleanerService = mock(ComponentCleanerService.class);
+  private ComponentFinder componentFinder = TestComponentFinder.from(db);
+
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+
+  public WsActionTester ws = new WsActionTester(new DeleteAction(db.getDbClient(), componentFinder, userSession, componentCleanerService));
+
+  @Test
+  public void definition() {
+    WebService.Action definition = ws.getDef();
+
+    assertThat(definition.key()).isEqualTo("delete");
+    assertThat(definition.isPost()).isTrue();
+    assertThat(definition.isInternal()).isFalse();
+    assertThat(definition.params()).extracting(WebService.Param::key).containsExactlyInAnyOrder("project", "pullRequest");
+    assertThat(definition.since()).isEqualTo("7.1");
+  }
+
+  @Test
+  public void delete_pull_request() {
+    ComponentDto project = db.components().insertMainBranch();
+    ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("1984").setBranchType(PULL_REQUEST));
+
+    userSession.logIn().addProjectPermission(UserRole.ADMIN, project);
+
+    ws.newRequest()
+      .setParam("project", project.getKey())
+      .setParam("pullRequest", "1984")
+      .execute();
+    verifyDeletedKey(branch.getDbKey());
+  }
+
+  @Test
+  public void fail_if_missing_project_parameter() {
+    userSession.logIn();
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("The 'project' parameter is missing");
+
+    ws.newRequest().execute();
+  }
+
+  @Test
+  public void fail_if_missing_pull_request_parameter() {
+    userSession.logIn();
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("The 'pullRequest' parameter is missing");
+
+    ws.newRequest().setParam("project", "projectName").execute();
+  }
+
+  @Test
+  public void fail_if_not_logged_in() {
+    expectedException.expect(UnauthorizedException.class);
+    expectedException.expectMessage("Authentication is required");
+
+    ws.newRequest().execute();
+  }
+
+  @Test
+  public void fail_if_pull_request_does_not_exist() {
+    ComponentDto project = db.components().insertPrivateProject(p -> p.setDbKey("orwell"));
+    userSession.logIn().addProjectPermission(UserRole.ADMIN, project);
+
+    expectedException.expect(NotFoundException.class);
+    expectedException.expectMessage("Pull request '1984' is not found for project 'orwell'");
+
+    ws.newRequest()
+      .setParam("project", project.getDbKey())
+      .setParam("pullRequest", "1984")
+      .execute();
+  }
+
+  @Test
+  public void fail_if_project_does_not_exist() {
+    userSession.logIn();
+
+    expectedException.expect(NotFoundException.class);
+    expectedException.expectMessage("Project key 'foo' not found");
+
+    ws.newRequest()
+      .setParam("project", "foo")
+      .setParam("pullRequest", "123")
+      .execute();
+  }
+
+  private void verifyDeletedKey(String key) {
+    ArgumentCaptor<ComponentDto> argument = ArgumentCaptor.forClass(ComponentDto.class);
+    verify(componentCleanerService).deleteBranch(any(DbSession.class), argument.capture());
+    assertThat(argument.getValue().getDbKey()).isEqualTo(key);
+  }
+
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/branch/pr/ws/ListActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/branch/pr/ws/ListActionTest.java
new file mode 100644 (file)
index 0000000..e8715b9
--- /dev/null
@@ -0,0 +1,372 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.branch.pr.ws;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.resources.ResourceTypes;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.utils.DateUtils;
+import org.sonar.api.utils.System2;
+import org.sonar.api.web.UserRole;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ComponentTesting;
+import org.sonar.db.component.ResourceTypesRule;
+import org.sonar.db.component.SnapshotTesting;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.db.protobuf.DbProjectBranches;
+import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.issue.index.IssueIndex;
+import org.sonar.server.issue.index.IssueIndexDefinition;
+import org.sonar.server.issue.index.IssueIndexer;
+import org.sonar.server.issue.index.IssueIteratorFactory;
+import org.sonar.server.permission.index.AuthorizationTypeSupport;
+import org.sonar.server.permission.index.PermissionIndexerTester;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.WsActionTester;
+import org.sonarqube.ws.MediaTypes;
+import org.sonarqube.ws.ProjectPullRequests.ListWsResponse;
+import org.sonarqube.ws.ProjectPullRequests.PullRequest;
+
+import static java.lang.String.format;
+import static java.util.Collections.emptySet;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.sonar.api.issue.Issue.RESOLUTION_FALSE_POSITIVE;
+import static org.sonar.api.issue.Issue.RESOLUTION_FIXED;
+import static org.sonar.api.resources.Qualifiers.PROJECT;
+import static org.sonar.api.rules.RuleType.BUG;
+import static org.sonar.api.rules.RuleType.CODE_SMELL;
+import static org.sonar.api.rules.RuleType.VULNERABILITY;
+import static org.sonar.api.utils.DateUtils.dateToLong;
+import static org.sonar.api.utils.DateUtils.parseDateTime;
+import static org.sonar.db.component.BranchType.LONG;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
+import static org.sonar.test.JsonAssert.assertJson;
+import static org.sonarqube.ws.ProjectPullRequests.Status;
+
+public class ListActionTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+  @Rule
+  public DbTester db = DbTester.create(System2.INSTANCE);
+  @Rule
+  public EsTester es = new EsTester(new IssueIndexDefinition(new MapSettings().asConfig()));
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+
+  private ResourceTypes resourceTypes = new ResourceTypesRule().setRootQualifiers(PROJECT);
+  private IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient()));
+  private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSession, new AuthorizationTypeSupport(userSession));
+  private PermissionIndexerTester permissionIndexerTester = new PermissionIndexerTester(es, issueIndexer);
+
+  public WsActionTester ws = new WsActionTester(new ListAction(db.getDbClient(), userSession, new ComponentFinder(db.getDbClient(), resourceTypes), issueIndex));
+
+  @Test
+  public void definition() {
+    WebService.Action definition = ws.getDef();
+    assertThat(definition.key()).isEqualTo("list");
+    assertThat(definition.isPost()).isFalse();
+    assertThat(definition.isInternal()).isFalse();
+    assertThat(definition.params()).extracting(WebService.Param::key).containsExactlyInAnyOrder("project");
+    assertThat(definition.since()).isEqualTo("7.1");
+  }
+
+  @Test
+  public void json_example() {
+    ComponentDto project = db.components().insertPrivateProject(p -> p.setDbKey("sonarqube"));
+    ComponentDto longLivingBranch = db.components().insertProjectBranch(project, b -> b.setKey("feature/foo").setBranchType(LONG));
+    ComponentDto pullRequest = db.components().insertProjectBranch(project,
+      b -> b.setKey("123")
+        .setBranchType(PULL_REQUEST)
+        .setMergeBranchUuid(longLivingBranch.uuid())
+        .setPullRequestData(DbProjectBranches.PullRequestData.newBuilder()
+          .setBranch("feature/bar")
+          .setTitle("Add feature X")
+          .setUrl("https://github.com/SonarSource/sonar-core-plugins/pull/32")
+          .build()));
+    userSession.logIn().addProjectPermission(UserRole.USER, project);
+
+    db.getDbClient().snapshotDao().insert(db.getSession(),
+      SnapshotTesting.newAnalysis(pullRequest).setLast(true).setCreatedAt(DateUtils.parseDateTime("2017-04-01T01:15:42+0100").getTime()));
+    db.commit();
+
+    String json = ws.newRequest()
+      .setParam("project", project.getDbKey())
+      .execute()
+      .getInput();
+
+    assertJson(json).isSimilarTo(ws.getDef().responseExampleAsString());
+  }
+
+  @Test
+  public void pull_request() {
+    ComponentDto project = db.components().insertMainBranch();
+    db.components().insertProjectBranch(project,
+      b -> b.setKey("123")
+        .setBranchType(PULL_REQUEST)
+        .setMergeBranchUuid(project.uuid())
+        .setPullRequestData(DbProjectBranches.PullRequestData.newBuilder().setBranch("feature/bar").build()));
+    userSession.logIn().addProjectPermission(UserRole.USER, project);
+
+    ListWsResponse response = ws.newRequest()
+      .setParam("project", project.getDbKey())
+      .executeProtobuf(ListWsResponse.class);
+
+    assertThat(response.getPullRequestsList())
+      .extracting(PullRequest::getKey, PullRequest::getBranch, PullRequest::getIsOrphan)
+      .containsExactlyInAnyOrder(tuple("123", "feature/bar", false));
+  }
+
+  @Test
+  public void project_with_zero_branches() {
+    ComponentDto project = db.components().insertPrivateProject();
+    userSession.logIn().addProjectPermission(UserRole.USER, project);
+
+    String json = ws.newRequest()
+      .setParam("project", project.getDbKey())
+      .setMediaType(MediaTypes.JSON)
+      .execute()
+      .getInput();
+
+    assertJson(json).isSimilarTo("{\"pullRequests\": []}");
+  }
+
+  @Test
+  public void pull_requests() {
+    ComponentDto project = db.components().insertMainBranch();
+    userSession.logIn().addProjectPermission(UserRole.USER, project);
+    ComponentDto longLivingBranch = db.components().insertProjectBranch(project,
+      b -> b.setKey("long").setBranchType(BranchType.LONG));
+    ComponentDto pullRequestOnLong = db.components().insertProjectBranch(project,
+      b -> b.setKey("pull_request_on_long")
+        .setBranchType(PULL_REQUEST)
+        .setMergeBranchUuid(longLivingBranch.uuid())
+        .setPullRequestData(DbProjectBranches.PullRequestData.newBuilder().setBranch("feature/bar").build()));
+    ComponentDto pullRequestOnMaster = db.components().insertProjectBranch(project,
+      b -> b.setKey("pull_request_on_master")
+        .setBranchType(PULL_REQUEST)
+        .setMergeBranchUuid(project.uuid())
+        .setPullRequestData(DbProjectBranches.PullRequestData.newBuilder().setBranch("feature/bar").build()));
+
+    ListWsResponse response = ws.newRequest()
+      .setParam("project", project.getKey())
+      .executeProtobuf(ListWsResponse.class);
+
+    assertThat(response.getPullRequestsList())
+      .extracting(PullRequest::getKey, PullRequest::getBase)
+      .containsExactlyInAnyOrder(
+        tuple(pullRequestOnLong.getPullRequest(), longLivingBranch.getBranch()),
+        tuple(pullRequestOnMaster.getPullRequest(), "master"));
+  }
+
+  @Test
+  public void base_branch_is_using_default_main_name_when_main_branch_has_no_name() {
+    ComponentDto project = db.components().insertMainBranch();
+    userSession.logIn().addProjectPermission(UserRole.USER, project);
+    ComponentDto pullRequest = db.components().insertProjectBranch(project,
+      b -> b.setKey("pr-123")
+        .setBranchType(PULL_REQUEST)
+        .setMergeBranchUuid(project.uuid())
+        .setPullRequestData(DbProjectBranches.PullRequestData.newBuilder()
+          .setBranch("feature123").build()));
+
+    ListWsResponse response = ws.newRequest()
+      .setParam("project", pullRequest.getKey())
+      .executeProtobuf(ListWsResponse.class);
+
+    assertThat(response.getPullRequests(0))
+      .extracting(PullRequest::getKey, PullRequest::getBase)
+      .containsExactlyInAnyOrder(pullRequest.getPullRequest(), "master");
+  }
+
+  @Test
+  public void pull_request_on_removed_branch() {
+    ComponentDto project = db.components().insertMainBranch();
+    userSession.logIn().addProjectPermission(UserRole.USER, project);
+    ComponentDto pullRequest = db.components().insertProjectBranch(project,
+      b -> b.setKey("pr-123")
+        .setBranchType(PULL_REQUEST)
+        .setMergeBranchUuid("unknown")
+        .setPullRequestData(DbProjectBranches.PullRequestData.newBuilder().setBranch("feature/bar").build()));
+
+    ListWsResponse response = ws.newRequest()
+      .setParam("project", project.getKey())
+      .executeProtobuf(ListWsResponse.class);
+
+    assertThat(response.getPullRequestsList())
+      .extracting(PullRequest::getKey, PullRequest::hasBase, PullRequest::getIsOrphan)
+      .containsExactlyInAnyOrder(
+        tuple(pullRequest.getPullRequest(), false, true));
+  }
+
+  @Test
+  public void status_on_pull_requests() {
+    ComponentDto project = db.components().insertMainBranch();
+    userSession.logIn().addProjectPermission(UserRole.USER, project);
+    ComponentDto longLivingBranch = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.LONG));
+    ComponentDto pullRequest = db.components().insertProjectBranch(project,
+      b -> b.setKey("pr-123")
+        .setBranchType(PULL_REQUEST)
+        .setMergeBranchUuid(longLivingBranch.uuid())
+        .setPullRequestData(DbProjectBranches.PullRequestData.newBuilder().setBranch("feature/bar").build()));
+    RuleDefinitionDto rule = db.rules().insert();
+    db.issues().insert(rule, pullRequest, pullRequest, i -> i.setType(BUG).setResolution(null));
+    db.issues().insert(rule, pullRequest, pullRequest, i -> i.setType(BUG).setResolution(RESOLUTION_FIXED));
+    db.issues().insert(rule, pullRequest, pullRequest, i -> i.setType(VULNERABILITY).setResolution(null));
+    db.issues().insert(rule, pullRequest, pullRequest, i -> i.setType(VULNERABILITY).setResolution(null));
+    db.issues().insert(rule, pullRequest, pullRequest, i -> i.setType(CODE_SMELL).setResolution(null));
+    db.issues().insert(rule, pullRequest, pullRequest, i -> i.setType(CODE_SMELL).setResolution(null));
+    db.issues().insert(rule, pullRequest, pullRequest, i -> i.setType(CODE_SMELL).setResolution(null));
+    db.issues().insert(rule, pullRequest, pullRequest, i -> i.setType(CODE_SMELL).setResolution(RESOLUTION_FALSE_POSITIVE));
+    issueIndexer.indexOnStartup(emptySet());
+    permissionIndexerTester.allowOnlyAnyone(project);
+
+    ListWsResponse response = ws.newRequest()
+      .setParam("project", project.getKey())
+      .executeProtobuf(ListWsResponse.class);
+
+    assertThat(response.getPullRequestsList().stream().map(PullRequest::getStatus))
+      .extracting(Status::hasBugs, Status::getBugs, Status::hasVulnerabilities, Status::getVulnerabilities, Status::hasCodeSmells, Status::getCodeSmells)
+      .containsExactlyInAnyOrder(tuple(true, 1L, true, 2L, true, 3L));
+  }
+
+  @Test
+  public void status_on_pull_request_with_no_issue() {
+    ComponentDto project = db.components().insertMainBranch();
+    userSession.logIn().addProjectPermission(UserRole.USER, project);
+    ComponentDto longLivingBranch = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.LONG));
+    db.components().insertProjectBranch(project,
+      b -> b.setKey("pr-123")
+        .setBranchType(PULL_REQUEST)
+        .setMergeBranchUuid(longLivingBranch.uuid())
+        .setPullRequestData(DbProjectBranches.PullRequestData.newBuilder().setBranch("feature/bar").build()));
+    issueIndexer.indexOnStartup(emptySet());
+    permissionIndexerTester.allowOnlyAnyone(project);
+
+    ListWsResponse response = ws.newRequest()
+      .setParam("project", project.getKey())
+      .executeProtobuf(ListWsResponse.class);
+
+    assertThat(response.getPullRequestsList().stream().map(PullRequest::getStatus))
+      .extracting(Status::getBugs, Status::getVulnerabilities, Status::getCodeSmells)
+      .containsExactlyInAnyOrder(tuple(0L, 0L, 0L));
+  }
+
+  @Test
+  public void response_contains_date_of_last_analysis() {
+    Long lastAnalysisLongLivingBranch = dateToLong(parseDateTime("2017-04-01T00:00:00+0100"));
+    Long previousAnalysisPullRequest = dateToLong(parseDateTime("2017-04-02T00:00:00+0100"));
+    Long lastAnalysisPullRequest = dateToLong(parseDateTime("2017-04-03T00:00:00+0100"));
+
+    ComponentDto project = db.components().insertMainBranch();
+    userSession.logIn().addProjectPermission(UserRole.USER, project);
+
+    ComponentDto pullRequest1 = db.components().insertProjectBranch(project,
+      b -> b.setKey("pr1")
+        .setBranchType(PULL_REQUEST)
+        .setMergeBranchUuid(project.uuid())
+        .setPullRequestData(DbProjectBranches.PullRequestData.newBuilder().setBranch("feature/pr1").build()));
+
+    ComponentDto longLivingBranch2 = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.LONG));
+
+    ComponentDto pullRequest2 = db.components().insertProjectBranch(project,
+      b -> b.setKey("pr2")
+        .setBranchType(PULL_REQUEST)
+        .setMergeBranchUuid(longLivingBranch2.uuid())
+        .setPullRequestData(DbProjectBranches.PullRequestData.newBuilder().setBranch("feature/pr2").build()));
+
+    db.getDbClient().snapshotDao().insert(db.getSession(),
+      SnapshotTesting.newAnalysis(longLivingBranch2).setCreatedAt(lastAnalysisLongLivingBranch));
+    db.getDbClient().snapshotDao().insert(db.getSession(),
+      SnapshotTesting.newAnalysis(pullRequest2).setCreatedAt(previousAnalysisPullRequest).setLast(false));
+    db.getDbClient().snapshotDao().insert(db.getSession(),
+      SnapshotTesting.newAnalysis(pullRequest2).setCreatedAt(lastAnalysisPullRequest));
+    db.commit();
+    issueIndexer.indexOnStartup(emptySet());
+    permissionIndexerTester.allowOnlyAnyone(project);
+
+    ListWsResponse response = ws.newRequest()
+      .setParam("project", project.getKey())
+      .executeProtobuf(ListWsResponse.class);
+
+    assertThat(response.getPullRequestsList())
+      .extracting(PullRequest::hasAnalysisDate, b -> "".equals(b.getAnalysisDate()) ? null : dateToLong(parseDateTime(b.getAnalysisDate())))
+      .containsExactlyInAnyOrder(
+        tuple(false, null),
+        tuple(true, lastAnalysisPullRequest));
+  }
+
+  @Test
+  public void fail_when_using_branch_db_key() throws Exception {
+    OrganizationDto organization = db.organizations().insert();
+    ComponentDto project = db.components().insertMainBranch(organization);
+    userSession.logIn().addProjectPermission(UserRole.USER, project);
+    ComponentDto branch = db.components().insertProjectBranch(project);
+
+    expectedException.expect(NotFoundException.class);
+    expectedException.expectMessage(format("Component key '%s' not found", branch.getDbKey()));
+
+    ws.newRequest()
+      .setParam("project", branch.getDbKey())
+      .execute();
+  }
+
+  @Test
+  public void fail_if_missing_project_parameter() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("The 'project' parameter is missing");
+
+    ws.newRequest().execute();
+  }
+
+  @Test
+  public void fail_if_not_a_reference_on_project() {
+    ComponentDto project = db.components().insertPrivateProject();
+    ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
+    userSession.logIn().addProjectPermission(UserRole.USER, project);
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Invalid project key");
+
+    ws.newRequest()
+      .setParam("project", file.getDbKey())
+      .execute();
+  }
+
+  @Test
+  public void fail_if_project_does_not_exist() {
+    expectedException.expect(NotFoundException.class);
+    expectedException.expectMessage("Component key 'foo' not found");
+
+    ws.newRequest()
+      .setParam("project", "foo")
+      .execute();
+  }
+
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/branch/pr/ws/PullRequestWsModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/branch/pr/ws/PullRequestWsModuleTest.java
new file mode 100644 (file)
index 0000000..1db6e4b
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.branch.pr.ws;
+
+import org.junit.Test;
+import org.sonar.core.platform.ComponentContainer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER;
+
+public class PullRequestWsModuleTest {
+  @Test
+  public void verify_count_of_added_components() {
+    ComponentContainer container = new ComponentContainer();
+    new PullRequestWsModule().configure(container);
+    assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 3);
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/branch/pr/ws/PullRequestsWsParametersTest.java b/server/sonar-server/src/test/java/org/sonar/server/branch/pr/ws/PullRequestsWsParametersTest.java
new file mode 100644 (file)
index 0000000..03054ca
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.branch.pr.ws;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.test.TestUtils.hasOnlyPrivateConstructors;
+
+public class PullRequestsWsParametersTest {
+
+  @Test
+  public void private_method() {
+    assertThat(hasOnlyPrivateConstructors(PullRequestsWsParameters.class)).isTrue();
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/branch/pr/ws/PullRequestsWsTest.java b/server/sonar-server/src/test/java/org/sonar/server/branch/pr/ws/PullRequestsWsTest.java
new file mode 100644 (file)
index 0000000..59268f2
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.branch.pr.ws;
+
+import org.junit.Test;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class PullRequestsWsTest {
+
+  @Test
+  public void define_ws() {
+    PullRequestsWs underTest = new PullRequestsWs(new PullRequestWsAction() {
+      @Override
+      public void define(WebService.NewController context) {
+        context.createAction("foo").setHandler(this);
+      }
+
+      @Override
+      public void handle(Request request, Response response) {
+
+      }
+    });
+
+    WebService.Context context = new WebService.Context();
+    underTest.define(context);
+
+    assertThat(context.controller("api/project_pull_requests").action("foo")).isNotNull();
+  }
+
+}
index 937466ea2140b7b14bd516929077862388543572..c3a1608dbebfd523b5e3492e4a2fcef1039ae6d9 100644 (file)
@@ -70,7 +70,7 @@ import static org.sonar.api.utils.DateUtils.dateToLong;
 import static org.sonar.api.utils.DateUtils.parseDateTime;
 import static org.sonar.core.permission.GlobalPermissions.SCAN_EXECUTION;
 import static org.sonar.test.JsonAssert.assertJson;
-import static org.sonarqube.ws.ProjectBranches.Branch.Status;
+import static org.sonarqube.ws.ProjectBranches.Status;
 
 public class ListActionTest {
 
index e3b7bad2c69ce831eed6426763d781c5f4103b2a..a55739065f3930d8e0ff49b5e32309f253d8d155 100644 (file)
@@ -19,7 +19,6 @@
  */
 package org.sonar.server.ce.ws;
 
-import java.io.IOException;
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
@@ -38,6 +37,7 @@ import org.sonar.db.ce.CeActivityDto.Status;
 import org.sonar.db.ce.CeQueueDto;
 import org.sonar.db.ce.CeTaskCharacteristicDto;
 import org.sonar.db.ce.CeTaskTypes;
+import org.sonar.db.component.BranchType;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.SnapshotDto;
 import org.sonar.db.organization.OrganizationDto;
@@ -51,10 +51,10 @@ import org.sonar.server.ws.TestResponse;
 import org.sonar.server.ws.WsActionTester;
 import org.sonar.test.JsonAssert;
 import org.sonarqube.ws.Ce;
-import org.sonarqube.ws.Common;
-import org.sonarqube.ws.MediaTypes;
 import org.sonarqube.ws.Ce.ActivityResponse;
 import org.sonarqube.ws.Ce.Task;
+import org.sonarqube.ws.Common;
+import org.sonarqube.ws.MediaTypes;
 
 import static java.util.Arrays.asList;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -68,6 +68,7 @@ import static org.sonar.db.ce.CeQueueDto.Status.IN_PROGRESS;
 import static org.sonar.db.ce.CeQueueDto.Status.PENDING;
 import static org.sonar.db.ce.CeTaskCharacteristicDto.BRANCH_KEY;
 import static org.sonar.db.ce.CeTaskCharacteristicDto.BRANCH_TYPE_KEY;
+import static org.sonar.db.ce.CeTaskCharacteristicDto.PULL_REQUEST;
 import static org.sonar.db.component.BranchType.LONG;
 import static org.sonar.server.ce.ws.CeWsParameters.PARAM_COMPONENT_ID;
 import static org.sonar.server.ce.ws.CeWsParameters.PARAM_COMPONENT_QUERY;
@@ -381,6 +382,45 @@ public class ActivityActionTest {
         tuple("T2", branch, Common.BranchType.LONG, Ce.TaskStatus.PENDING));
   }
 
+  @Test
+  public void pull_request_in_past_activity() {
+    logInAsSystemAdministrator();
+    ComponentDto project = db.components().insertMainBranch();
+    userSession.addProjectPermission(UserRole.USER, project);
+    ComponentDto pullRequest = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.PULL_REQUEST));
+    SnapshotDto analysis = db.components().insertSnapshot(pullRequest);
+    CeActivityDto activity = insertActivity("T1", project, SUCCESS, analysis);
+    insertCharacteristic(activity, PULL_REQUEST, pullRequest.getPullRequest());
+
+    ActivityResponse response = ws.newRequest().executeProtobuf(ActivityResponse.class);
+
+    assertThat(response.getTasksList())
+      .extracting(Task::getId, Ce.Task::getPullRequest, Ce.Task::hasPullRequestTitle, Ce.Task::getStatus, Ce.Task::getComponentKey)
+      .containsExactlyInAnyOrder(
+        // TODO the pull request title must be loaded from db
+        tuple("T1", pullRequest.getPullRequest(), false, Ce.TaskStatus.SUCCESS, pullRequest.getKey()));
+  }
+
+  @Test
+  public void pull_request_in_queue_analysis() {
+    logInAsSystemAdministrator();
+    String branch = "pr-123";
+    CeQueueDto queue1 = insertQueue("T1", null, IN_PROGRESS);
+    insertCharacteristic(queue1, PULL_REQUEST, branch);
+    CeQueueDto queue2 = insertQueue("T2", null, PENDING);
+    insertCharacteristic(queue2, PULL_REQUEST, branch);
+
+    ActivityResponse response = ws.newRequest()
+      .setParam("status", "FAILED,IN_PROGRESS,PENDING")
+      .executeProtobuf(ActivityResponse.class);
+
+    assertThat(response.getTasksList())
+      .extracting(Task::getId, Ce.Task::getPullRequest, Ce.Task::hasPullRequestTitle, Ce.Task::getStatus)
+      .containsExactlyInAnyOrder(
+        tuple("T1", branch, false, Ce.TaskStatus.IN_PROGRESS),
+        tuple("T2", branch, false, Ce.TaskStatus.PENDING));
+  }
+
   @Test
   public void fail_if_both_filters_on_component_id_and_name() {
     expectedException.expect(BadRequestException.class);
index 6a7c2ec0d78f431578409af4df5e435b0a6cf8a7..fc4869d7280479f7d9f11196821635d5d56d7f5f 100644 (file)
@@ -30,6 +30,7 @@ import org.sonar.server.exceptions.NotFoundException;
 
 import static java.lang.String.format;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
 import static org.sonar.db.component.ComponentTesting.newDirectory;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
 import static org.sonar.db.component.ComponentTesting.newModuleDto;
@@ -192,6 +193,31 @@ public class ComponentFinderTest {
     assertThat(underTest.getByKeyAndBranch(dbSession, directory.getKey(), "my_branch").uuid()).isEqualTo(directory.uuid());
   }
 
+  @Test
+  public void get_by_key_and_pull_request() {
+    ComponentDto project = db.components().insertMainBranch();
+    ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("pr-123").setBranchType(PULL_REQUEST).setMergeBranchUuid(project.uuid()));
+    ComponentDto module = db.components().insertComponent(newModuleDto(branch));
+    ComponentDto directory = db.components().insertComponent(newDirectory(module, "scr"));
+    ComponentDto file = db.components().insertComponent(newFileDto(module));
+
+    assertThat(underTest.getByKeyAndOptionalBranchOrPullRequest(dbSession, project.getKey(), null, "pr-123").uuid()).isEqualTo(branch.uuid());
+    assertThat(underTest.getByKeyAndOptionalBranchOrPullRequest(dbSession, module.getKey(), null, "pr-123").uuid()).isEqualTo(module.uuid());
+    assertThat(underTest.getByKeyAndOptionalBranchOrPullRequest(dbSession, file.getKey(), null, "pr-123").uuid()).isEqualTo(file.uuid());
+    assertThat(underTest.getByKeyAndOptionalBranchOrPullRequest(dbSession, directory.getKey(), null, "pr-123").uuid()).isEqualTo(directory.uuid());
+  }
+
+  @Test
+  public void fail_when_pull_request_branch_provided() {
+    ComponentDto project = db.components().insertMainBranch();
+    ComponentDto pullRequest = db.components().insertProjectBranch(project, b -> b.setKey("pr-123").setBranchType(PULL_REQUEST));
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Either branch or pull request can be provided, not both");
+
+    assertThat(underTest.getByKeyAndOptionalBranchOrPullRequest(dbSession, project.getKey(), "pr-123", "pr-123").uuid()).isEqualTo(pullRequest.uuid());
+  }
+
   @Test
   public void get_by_key_and_branch_accept_main_branch() {
     ComponentDto project = db.components().insertMainBranch();
index 3abc3b46b23c6c0b70ab601677b337cd56deeaa6..2bbebd58d5db28fccb64e5c2161419a1e6adf2c4 100644 (file)
@@ -40,6 +40,7 @@ import static org.sonar.api.measures.CoreMetrics.TECHNICAL_DEBT_KEY;
 import static org.sonar.api.measures.CoreMetrics.TESTS_KEY;
 import static org.sonar.api.measures.CoreMetrics.VIOLATIONS_KEY;
 import static org.sonar.api.web.UserRole.USER;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
 import static org.sonar.db.component.ComponentTesting.newDirectory;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
 import static org.sonar.db.component.ComponentTesting.newModuleDto;
@@ -299,7 +300,7 @@ public class AppActionTest {
     ComponentDto file = db.components().insertComponent(newFileDto(branch));
 
     expectedException.expect(IllegalArgumentException.class);
-    expectedException.expectMessage("'componentId' and 'branch' parameters cannot be used at the same time");
+    expectedException.expectMessage("Parameter 'componentId' cannot be used at the same time as 'branch' or 'pullRequest'");
 
     ws.newRequest()
       .setParam("uuid", file.uuid())
@@ -307,6 +308,21 @@ public class AppActionTest {
       .execute();
   }
 
+  @Test
+  public void fail_if_both_componentId_and_pull_request_parameters_provided() {
+    ComponentDto project = db.components().insertMainBranch();
+    ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setBranchType(PULL_REQUEST));
+    ComponentDto file = db.components().insertComponent(newFileDto(branch));
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Parameter 'componentId' cannot be used at the same time as 'branch' or 'pullRequest'");
+
+    ws.newRequest()
+      .setParam("uuid", file.uuid())
+      .setParam("pullRequest", file.getPullRequest())
+      .execute();
+  }
+
   @Test
   public void fail_when_component_not_found() {
     ComponentDto project = db.components().insertPrivateProject();
@@ -352,7 +368,7 @@ public class AppActionTest {
     assertThat(action.isInternal()).isTrue();
     assertThat(action.isPost()).isFalse();
     assertThat(action.handler()).isNotNull();
-    assertThat(action.params()).hasSize(3);
+    assertThat(action.params()).hasSize(4);
   }
 
 }
index b7324a8974f375aecdd1d64248f122788e209a40..b03eae689f21539248ee451def507e71f4827c07 100644 (file)
@@ -19,7 +19,6 @@
  */
 package org.sonar.server.component.ws;
 
-import java.io.IOException;
 import java.util.Date;
 import javax.annotation.Nullable;
 import org.junit.Rule;
@@ -47,6 +46,7 @@ import static org.assertj.core.api.Assertions.tuple;
 import static org.sonar.api.utils.DateUtils.formatDateTime;
 import static org.sonar.api.utils.DateUtils.parseDateTime;
 import static org.sonar.api.web.UserRole.USER;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
 import static org.sonar.db.component.ComponentTesting.newDirectory;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
 import static org.sonar.db.component.ComponentTesting.newModuleDto;
@@ -56,6 +56,7 @@ import static org.sonar.test.JsonAssert.assertJson;
 import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_BRANCH;
 import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_COMPONENT;
 import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_COMPONENT_ID;
+import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_PULL_REQUEST;
 
 public class ShowActionTest {
   @Rule
@@ -81,7 +82,7 @@ public class ShowActionTest {
       tuple("6.5", "Leak period date is added to the response"),
       tuple("6.6", "'branch' is added to the response"),
       tuple("6.6", "'version' is added to the response"));
-    assertThat(action.params()).extracting(WebService.Param::key).containsExactlyInAnyOrder("component", "componentId", "branch");
+    assertThat(action.params()).extracting(WebService.Param::key).containsExactlyInAnyOrder("component", "componentId", "branch", "pullRequest");
 
     WebService.Param componentId = action.param(PARAM_COMPONENT_ID);
     assertThat(componentId.isRequired()).isFalse();
@@ -102,6 +103,11 @@ public class ShowActionTest {
     assertThat(branch.isInternal()).isTrue();
     assertThat(branch.isRequired()).isFalse();
     assertThat(branch.since()).isEqualTo("6.6");
+
+    WebService.Param pullRequest = action.param(PARAM_PULL_REQUEST);
+    assertThat(pullRequest.isInternal()).isTrue();
+    assertThat(pullRequest.isRequired()).isFalse();
+    assertThat(pullRequest.since()).isEqualTo("7.1");
   }
 
   @Test
@@ -306,6 +312,32 @@ public class ShowActionTest {
         tuple(branch.getKey(), branchKey, "1.1"));
   }
 
+  @Test
+  public void pull_request() {
+    ComponentDto project = db.components().insertMainBranch();
+    userSession.addProjectPermission(UserRole.USER, project);
+    String pullRequest = "pr-1234";
+    ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey(pullRequest).setBranchType(PULL_REQUEST));
+    ComponentDto module = db.components().insertComponent(newModuleDto(branch));
+    ComponentDto directory = db.components().insertComponent(newDirectory(module, "dir"));
+    ComponentDto file = db.components().insertComponent(newFileDto(directory));
+    db.components().insertSnapshot(branch, s -> s.setVersion("1.1"));
+
+    ShowWsResponse response = ws.newRequest()
+      .setParam(PARAM_COMPONENT, file.getKey())
+      .setParam(PARAM_PULL_REQUEST, pullRequest)
+      .executeProtobuf(ShowWsResponse.class);
+
+    assertThat(response.getComponent())
+      .extracting(Component::getKey, Component::getPullRequest, Component::getVersion)
+      .containsExactlyInAnyOrder(file.getKey(), pullRequest, "1.1");
+    assertThat(response.getAncestorsList()).extracting(Component::getKey, Component::getPullRequest, Component::getVersion)
+      .containsExactlyInAnyOrder(
+        tuple(directory.getKey(), pullRequest, "1.1"),
+        tuple(module.getKey(), pullRequest, "1.1"),
+        tuple(branch.getKey(), pullRequest, "1.1"));
+  }
+
   @Test
   public void throw_ForbiddenException_if_user_doesnt_have_browse_permission_on_project() {
     userSession.logIn();
@@ -343,7 +375,7 @@ public class ShowActionTest {
     ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("my_branch"));
 
     expectedException.expect(IllegalArgumentException.class);
-    expectedException.expectMessage("'componentId' and 'branch' parameters cannot be used at the same time");
+    expectedException.expectMessage("Parameter 'componentId' cannot be used at the same time as 'branch' or 'pullRequest'");
 
     ws.newRequest()
       .setParam(PARAM_COMPONENT_ID, branch.uuid())
index 713908c8f66893cf137c23260ef5d3b5ea9ca418..4452efaa621b741f6fea08e150aa5b4ae4031841 100644 (file)
@@ -61,6 +61,7 @@ import static org.assertj.core.api.Assertions.tuple;
 import static org.sonar.api.resources.Qualifiers.FILE;
 import static org.sonar.api.resources.Qualifiers.PROJECT;
 import static org.sonar.api.resources.Qualifiers.UNIT_TEST_FILE;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
 import static org.sonar.db.component.ComponentTesting.newChildComponent;
 import static org.sonar.db.component.ComponentTesting.newDirectory;
 import static org.sonar.db.component.ComponentTesting.newModuleDto;
@@ -71,6 +72,7 @@ import static org.sonar.db.component.ComponentTesting.newView;
 import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_BRANCH;
 import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_COMPONENT;
 import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_COMPONENT_ID;
+import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_PULL_REQUEST;
 import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_QUALIFIERS;
 import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_STRATEGY;
 
@@ -99,7 +101,7 @@ public class TreeActionTest {
     assertThat(action.responseExample()).isNotNull();
     assertThat(action.changelog()).extracting(Change::getVersion, Change::getDescription).containsExactlyInAnyOrder(
       tuple("6.4", "The field 'id' is deprecated in the response"));
-    assertThat(action.params()).extracting(Param::key).containsExactlyInAnyOrder("component", "componentId", "branch", "qualifiers", "strategy",
+    assertThat(action.params()).extracting(Param::key).containsExactlyInAnyOrder("component", "componentId", "branch", "pullRequest", "qualifiers", "strategy",
       "q", "s", "p", "asc", "ps");
 
     Param componentId = action.param(PARAM_COMPONENT_ID);
@@ -338,6 +340,29 @@ public class TreeActionTest {
         tuple(file.getKey(), branchKey));
   }
 
+  @Test
+  public void pull_request() {
+    ComponentDto project = db.components().insertPrivateProject();
+    userSession.addProjectPermission(UserRole.USER, project);
+    String pullRequestId = "pr-123";
+    ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey(pullRequestId).setBranchType(PULL_REQUEST));
+    ComponentDto module = db.components().insertComponent(newModuleDto(branch));
+    ComponentDto directory = db.components().insertComponent(newDirectory(module, "dir"));
+    ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(directory));
+
+    TreeWsResponse response = ws.newRequest()
+      .setParam(PARAM_COMPONENT, module.getKey())
+      .setParam(PARAM_PULL_REQUEST, pullRequestId)
+      .executeProtobuf(TreeWsResponse.class);
+
+    assertThat(response.getBaseComponent()).extracting(Components.Component::getKey, Components.Component::getPullRequest)
+      .containsExactlyInAnyOrder(module.getKey(), pullRequestId);
+    assertThat(response.getComponentsList()).extracting(Components.Component::getKey, Components.Component::getPullRequest)
+      .containsExactlyInAnyOrder(
+        tuple(directory.getKey(), pullRequestId),
+        tuple(file.getKey(), pullRequestId));
+  }
+
   @Test
   public void fail_when_using_branch_db_key() {
     ComponentDto project = db.components().insertMainBranch();
@@ -468,7 +493,7 @@ public class TreeActionTest {
     ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("my_branch"));
 
     expectedException.expect(IllegalArgumentException.class);
-    expectedException.expectMessage("'componentId' and 'branch' parameters cannot be used at the same time");
+    expectedException.expectMessage("Parameter 'componentId' cannot be used at the same time as 'branch' or 'pullRequest'");
 
     ws.newRequest()
       .setParam(PARAM_COMPONENT_ID, branch.uuid())
index cb603a7a98d8839832204e9482bd1b352fb9175d..4a112bbb4de586ca5629ec0b1177647ad5cd554d 100644 (file)
@@ -241,6 +241,34 @@ public class AnalysisMetadataHolderImplTest {
     underTest.setBranch(new DefaultBranchImpl("master"));
   }
 
+  @Test
+  public void setPullRequestId() {
+    AnalysisMetadataHolderImpl underTest = new AnalysisMetadataHolderImpl();
+
+    String pullRequestId = "pr-123";
+    underTest.setPullRequestId(pullRequestId);
+
+    assertThat(underTest.getPullRequestId()).isEqualTo(pullRequestId);
+  }
+
+  @Test
+  public void getPullRequestId_throws_ISE_when_holder_is_not_initialized() {
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Pull request id has not been set");
+
+    new AnalysisMetadataHolderImpl().getPullRequestId();
+  }
+
+  @Test
+  public void setPullRequestId_throws_ISE_when_called_twice() {
+    AnalysisMetadataHolderImpl underTest = new AnalysisMetadataHolderImpl();
+    underTest.setPullRequestId("pr-123");
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Pull request id has already been set");
+    underTest.setPullRequestId("pr-234");
+  }
+
   @Test
   public void set_and_get_project() {
     AnalysisMetadataHolderImpl underTest = new AnalysisMetadataHolderImpl();
@@ -315,4 +343,15 @@ public class AnalysisMetadataHolderImplTest {
 
     assertThat(underTest.isShortLivingBranch()).isTrue();
   }
+
+  @Test
+  public void getPullRequestBranch_returns_true() {
+    Branch branch = mock(Branch.class);
+    when(branch.getType()).thenReturn(BranchType.PULL_REQUEST);
+
+    AnalysisMetadataHolderImpl underTest = new AnalysisMetadataHolderImpl();
+    underTest.setBranch(branch);
+
+    assertThat(underTest.isPullRequest()).isTrue();
+  }
 }
index cf89ba5747c83b97ae2330ffea481699e53dd1fc..08f75f27e95e80b5640800a3d3f75a5cecd75d24 100644 (file)
@@ -49,6 +49,8 @@ public class AnalysisMetadataHolderRule extends ExternalResource implements Muta
 
   private final InitializedProperty<Branch> branch = new InitializedProperty<>();
 
+  private final InitializedProperty<String> pullRequestId = new InitializedProperty<>();
+
   private final InitializedProperty<Project> project = new InitializedProperty<>();
 
   private final InitializedProperty<Integer> rootComponentRef = new InitializedProperty<>();
@@ -167,6 +169,18 @@ public class AnalysisMetadataHolderRule extends ExternalResource implements Muta
     return branch.getProperty();
   }
 
+  @Override
+  public MutableAnalysisMetadataHolder setPullRequestId(String pullRequestId) {
+    this.pullRequestId.setProperty(pullRequestId);
+    return this;
+  }
+
+  @Override
+  public String getPullRequestId() {
+    checkState(pullRequestId.isInitialized(), "Pull request id has not been set");
+    return pullRequestId.getProperty();
+  }
+
   @Override
   public AnalysisMetadataHolderRule setProject(Project p) {
     this.project.setProperty(p);
@@ -226,4 +240,10 @@ public class AnalysisMetadataHolderRule extends ExternalResource implements Muta
     Branch property = this.branch.getProperty();
     return property != null && property.getType() == BranchType.LONG;
   }
+
+  @Override
+  public boolean isPullRequest() {
+    Branch property = this.branch.getProperty();
+    return property != null && property.getType() == BranchType.PULL_REQUEST;
+  }
 }
index 9935ae33d25bf3031b71e6b988c4672d05d7f034..24a8f1d15fd1ed4322c1e040994cdd4fdd5d8201 100644 (file)
@@ -120,6 +120,17 @@ public class MutableAnalysisMetadataHolderRule extends ExternalResource implemen
     return this;
   }
 
+  @Override
+  public String getPullRequestId() {
+    return delegate.getPullRequestId();
+  }
+
+  @Override
+  public MutableAnalysisMetadataHolder setPullRequestId(String pullRequestId) {
+    delegate.setPullRequestId(pullRequestId);
+    return this;
+  }
+
   @Override
   public MutableAnalysisMetadataHolderRule setProject(@Nullable Project project) {
     delegate.setProject(project);
@@ -173,4 +184,9 @@ public class MutableAnalysisMetadataHolderRule extends ExternalResource implemen
   public boolean isLongLivingBranch() {
     return delegate.isLongLivingBranch();
   }
+
+  @Override
+  public boolean isPullRequest() {
+    return delegate.isPullRequest();
+  }
 }
index 15e0705c3e423f2644649164e9cafcbd96a07217..4270765b2080c5ee86682f28d27c5a6dcac72413 100644 (file)
@@ -301,6 +301,11 @@ public class PostProjectAnalysisTasksExecutorTest {
         throw new UnsupportedOperationException();
       }
 
+      @Override
+      public String getPullRequestId() {
+        throw new UnsupportedOperationException();
+      }
+
       @Override
       public String generateKey(ScannerReport.Component module, @Nullable ScannerReport.Component fileOrDir) {
         throw new UnsupportedOperationException();
index ad333dd598df308ec170a98e1a0c122077916c1b..f37bf46dd46805c87dcf61f9d4d9bb51be987c9d 100644 (file)
@@ -81,7 +81,28 @@ public class BranchPersisterImplTest {
 
     assertThat(dbTester.countRowsOfTable("projects")).isEqualTo(2);
     assertThat(dbTester.countRowsOfTable("project_branches")).isEqualTo(1);
+  }
+
+  @Test
+  public void persist_pull_request_data() {
+    String pullRequestId = "pr-123";
+    analysisMetadataHolder.setBranch(createBranch(BranchType.PULL_REQUEST, false, pullRequestId));
+    analysisMetadataHolder.setPullRequestId(pullRequestId);
+    treeRootHolder.setRoot(BRANCH);
+
+    // add main branch in project table and in metadata
+    ComponentDto dto = ComponentTesting.newPrivateProjectDto(dbTester.organizations().insert(), MAIN.getUuid()).setDbKey(MAIN.getKey());
+    analysisMetadataHolder.setProject(Project.copyOf(dto));
+    dbTester.getDbClient().componentDao().insert(dbTester.getSession(), dto);
 
+    // this should add new columns in project and project_branches
+    underTest.persist(dbTester.getSession());
+
+    dbTester.getSession().commit();
+
+    assertThat(dbTester.countRowsOfTable("projects")).isEqualTo(2);
+    assertThat(dbTester.countRowsOfTable("project_branches")).isEqualTo(1);
+    assertThat(dbTester.countSql("select count(*) from project_branches where pull_request_binary is not null")).isEqualTo(1);
   }
 
   private static Branch createBranch(BranchType type, boolean isMain, String name) {
index 492b0ec5eb16071a7416a30b1653eda8370a45b5..f0fca60160f4dbedfc787b34d1fc940076a4f84a 100644 (file)
@@ -150,6 +150,25 @@ public class ShortBranchComponentsWithIssuesTest {
     assertThat(underTest.getUuids(fileWithResolvedIssue.getKey())).hasSize(1);
   }
 
+  @Test
+  public void should_find_components_with_issues_to_merge_on_derived_pull_request() {
+    ComponentDto project = db.components().insertMainBranch();
+    setRootUuid(project.uuid());
+
+    ComponentDto pullRequest = db.components().insertProjectBranch(project,
+      b -> b.setBranchType(BranchType.PULL_REQUEST),
+      b -> b.setMergeBranchUuid(project.uuid()));
+
+    RuleDefinitionDto rule = db.rules().insert();
+
+    ComponentDto fileWithResolvedIssue = db.components().insertComponent(ComponentTesting.newFileDto(pullRequest, null));
+    db.issues().insertIssue(IssueTesting.newIssue(rule, pullRequest, fileWithResolvedIssue).setStatus("RESOLVED"));
+
+    underTest = new ShortBranchComponentsWithIssues(treeRootHolder, db.getDbClient());
+
+    assertThat(underTest.getUuids(fileWithResolvedIssue.getKey())).hasSize(1);
+  }
+
   @Test
   public void should_not_find_components_with_issues_to_merge_on_derived_long() {
     ComponentDto project = db.components().insertMainBranch();
index b98b719412e789234b7e5dabe7db97dcd1a6fb41..69d84ffe759416237d27dad88e6f44bbec586701 100644 (file)
@@ -101,4 +101,18 @@ public class IssueTrackingDelegatorTest {
     verifyZeroInteractions(tracker);
     verifyZeroInteractions(mergeBranchTracker);
   }
+
+  @Test
+  public void delegate_pull_request_tracker() {
+    Branch branch = mock(Branch.class);
+    when(branch.getType()).thenReturn(BranchType.PULL_REQUEST);
+    when(analysisMetadataHolder.getBranch()).thenReturn(mock(Branch.class));
+    when(analysisMetadataHolder.isShortLivingBranch()).thenReturn(true);
+
+    underTest.track(component);
+
+    verify(shortBranchTracker).track(component);
+    verifyZeroInteractions(tracker);
+    verifyZeroInteractions(mergeBranchTracker);
+  }
 }
index 3d61a0369bb864ccc5d5b2a2b1932f2a29147b57..43cdc901f2a26738b159c360baefcad371bf7ca5 100644 (file)
@@ -67,6 +67,17 @@ public class LoadQualityGateStepTest {
     assertThat(mutableQualityGateHolder.getQualityGate().get()).isSameAs(qualityGate);
   }
 
+  @Test
+  public void add_hardcoded_QG_on_pull_request() {
+    when(analysisMetadataHolder.isPullRequest()).thenReturn(true);
+    QualityGate qualityGate = mock(QualityGate.class);
+    when(qualityGateService.findById(ShortLivingBranchQualityGate.ID)).thenReturn(Optional.of(qualityGate));
+
+    underTest.execute();
+
+    assertThat(mutableQualityGateHolder.getQualityGate().get()).isSameAs(qualityGate);
+  }
+
   @Test
   public void execute_sets_default_QualityGate_when_project_has_no_settings() {
     when(settingsRepository.getConfiguration()).thenReturn(new MapSettings().asConfig());
index 1bf8cb095213d4b1f9c49bc5dddcf320be2021e5..0501ef46791888ae835b95dc69627e5fde1dfebd 100644 (file)
@@ -325,4 +325,23 @@ public class QualityGateEventsStepTest {
 
     verifyZeroInteractions(treeRootHolder, metricRepository, measureRepository, eventRepository, notificationService);
   }
+
+  @Test
+  public void no_alert_on_pull_request_branches() {
+    Branch shortBranch = mock(Branch.class);
+    when(shortBranch.getType()).thenReturn(BranchType.PULL_REQUEST);
+    analysisMetadataHolder.setBranch(shortBranch);
+    TreeRootHolder treeRootHolder = mock(TreeRootHolder.class);
+    MetricRepository metricRepository = mock(MetricRepository.class);
+    MeasureRepository measureRepository = mock(MeasureRepository.class);
+    EventRepository eventRepository = mock(EventRepository.class);
+    NotificationService notificationService = mock(NotificationService.class);
+
+    QualityGateEventsStep underTest = new QualityGateEventsStep(treeRootHolder, metricRepository, measureRepository,
+      eventRepository, notificationService, analysisMetadataHolder);
+
+    underTest.execute();
+
+    verifyZeroInteractions(treeRootHolder, metricRepository, measureRepository, eventRepository, notificationService);
+  }
 }
index 5dddeaf60338f685e370a8aa03d35e6080dac8c1..75a68862c44654b14a285d63d7b5a9cbf7e1e376 100644 (file)
@@ -943,6 +943,11 @@ public class ReportPersistComponentsStepTest extends BaseStepTest {
       return false;
     }
 
+    @Override
+    public String getPullRequestId() {
+      throw new UnsupportedOperationException();
+    }
+
     @Override
     public String generateKey(ScannerReport.Component module, @Nullable ScannerReport.Component fileOrDir) {
       String moduleKey = module.getKey();
index 4778aa7a0f23818c167e607bc5b392103e88fe32..eacd042c8ca9d0ef8f557c4d498e6bf0daa76d6d 100644 (file)
@@ -67,6 +67,7 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
 import static org.sonar.db.component.ComponentTesting.newBranchDto;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
 import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto;
@@ -79,6 +80,7 @@ import static org.sonar.server.computation.task.projectanalysis.component.Report
 public class SendIssueNotificationsStepTest extends BaseStepTest {
 
   private static final String BRANCH_NAME = "feature";
+  private static final String PULL_REQUEST_ID = "pr-123";
 
   private static final long ANALYSE_DATE = 123L;
   private static final int FIVE_MINUTES_IN_MS = 1000 * 60 * 5;
@@ -143,7 +145,7 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
     underTest.execute();
 
     verify(notificationService).deliver(newIssuesNotificationMock);
-    verify(newIssuesNotificationMock).setProject(PROJECT.getPublicKey(), PROJECT.getName(), null);
+    verify(newIssuesNotificationMock).setProject(PROJECT.getPublicKey(), PROJECT.getName(), null, null);
     verify(newIssuesNotificationMock).setAnalysisDate(new Date(ANALYSE_DATE));
     verify(newIssuesNotificationMock).setStatistics(eq(PROJECT.getName()), any());
     verify(newIssuesNotificationMock).setDebt(ISSUE_DURATION);
@@ -206,7 +208,25 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
     underTest.execute();
 
     verify(notificationService).deliver(newIssuesNotificationMock);
-    verify(newIssuesNotificationMock).setProject(branch.getKey(), branch.longName(), BRANCH_NAME);
+    verify(newIssuesNotificationMock).setProject(branch.getKey(), branch.longName(), BRANCH_NAME, null);
+    verify(newIssuesNotificationMock).setAnalysisDate(new Date(ANALYSE_DATE));
+    verify(newIssuesNotificationMock).setStatistics(eq(branch.longName()), any(NewIssuesStatistics.Stats.class));
+    verify(newIssuesNotificationMock).setDebt(ISSUE_DURATION);
+  }
+
+  @Test
+  public void send_global_new_issues_notification_on_pull_request() {
+    ComponentDto branch = setUpProjectWithBranch();
+    issueCache.newAppender().append(
+      new DefaultIssue().setType(randomRuleType).setEffort(ISSUE_DURATION).setCreationDate(new Date(ANALYSE_DATE))).close();
+    when(notificationService.hasProjectSubscribersForTypes(branch.uuid(), SendIssueNotificationsStep.NOTIF_TYPES)).thenReturn(true);
+    analysisMetadataHolder.setBranch(newPullRequest());
+    analysisMetadataHolder.setPullRequestId(PULL_REQUEST_ID);
+
+    underTest.execute();
+
+    verify(notificationService).deliver(newIssuesNotificationMock);
+    verify(newIssuesNotificationMock).setProject(branch.getKey(), branch.longName(), null, PULL_REQUEST_ID);
     verify(newIssuesNotificationMock).setAnalysisDate(new Date(ANALYSE_DATE));
     verify(newIssuesNotificationMock).setStatistics(eq(branch.longName()), any(NewIssuesStatistics.Stats.class));
     verify(newIssuesNotificationMock).setDebt(ISSUE_DURATION);
@@ -238,7 +258,7 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
     verify(notificationService).deliver(newIssuesNotificationMock);
     verify(notificationService).deliver(myNewIssuesNotificationMock);
     verify(myNewIssuesNotificationMock).setAssignee(ISSUE_ASSIGNEE);
-    verify(myNewIssuesNotificationMock).setProject(PROJECT.getPublicKey(), PROJECT.getName(), null);
+    verify(myNewIssuesNotificationMock).setProject(PROJECT.getPublicKey(), PROJECT.getName(), null, null);
     verify(myNewIssuesNotificationMock).setAnalysisDate(new Date(ANALYSE_DATE));
     verify(myNewIssuesNotificationMock).setStatistics(eq(PROJECT.getName()), any(NewIssuesStatistics.Stats.class));
     verify(myNewIssuesNotificationMock).setDebt(ISSUE_DURATION);
@@ -424,7 +444,7 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
 
   private NewIssuesNotification createNewIssuesNotificationMock() {
     NewIssuesNotification notification = mock(NewIssuesNotification.class);
-    when(notification.setProject(any(), any(), any())).thenReturn(notification);
+    when(notification.setProject(any(), any(), any(), any())).thenReturn(notification);
     when(notification.setProjectVersion(any())).thenReturn(notification);
     when(notification.setAnalysisDate(any())).thenReturn(notification);
     when(notification.setStatistics(any(), any())).thenReturn(notification);
@@ -435,7 +455,7 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
   private MyNewIssuesNotification createMyNewIssuesNotificationMock() {
     MyNewIssuesNotification notification = mock(MyNewIssuesNotification.class);
     when(notification.setAssignee(any())).thenReturn(notification);
-    when(notification.setProject(any(), any(), any())).thenReturn(notification);
+    when(notification.setProject(any(), any(), any(), any())).thenReturn(notification);
     when(notification.setProjectVersion(any())).thenReturn(notification);
     when(notification.setAnalysisDate(any())).thenReturn(notification);
     when(notification.setStatistics(any(), any())).thenReturn(notification);
@@ -450,6 +470,15 @@ public class SendIssueNotificationsStepTest extends BaseStepTest {
     return branch;
   }
 
+  private static Branch newPullRequest() {
+    Branch branch = mock(Branch.class);
+    when(branch.isMain()).thenReturn(false);
+    when(branch.getType()).thenReturn(PULL_REQUEST);
+    when(branch.getName()).thenReturn(BRANCH_NAME);
+    when(branch.getPullRequestId()).thenReturn(PULL_REQUEST_ID);
+    return branch;
+  }
+
   private ComponentDto setUpProjectWithBranch() {
     ComponentDto project = newPrivateProjectDto(newOrganizationDto());
     ComponentDto branch = newProjectBranch(project, newBranchDto(project).setKey(BRANCH_NAME));
index 3764f6ca825d9760292c1f7af9922f93b9463842..d45ce9934fc0ee42f9db0dc1e93c5af31857d18d 100644 (file)
  */
 package org.sonar.server.duplication.ws;
 
-import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
 import java.util.List;
 import javax.annotation.Nullable;
 import org.junit.Rule;
 import org.junit.Test;
 import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchType;
 import org.sonar.db.component.ComponentDto;
 
 import static java.lang.String.format;
@@ -44,14 +44,14 @@ public class DuplicationsParserTest {
     ComponentDto project = db.components().insertPrivateProject();
     ComponentDto file = db.components().insertComponent(newFileDto(project));
 
-    assertThat(parser.parse(db.getSession(), file, null, null)).isEmpty();
+    assertThat(parser.parse(db.getSession(), file, null, null, null)).isEmpty();
   }
 
   @Test
   public void duplication_on_same_file() {
     ComponentDto project = db.components().insertPrivateProject();
     ComponentDto file = db.components().insertComponent(newFileDto(project));
-    List<DuplicationsParser.Block> blocks = parser.parse(db.getSession(), file, null,
+    List<DuplicationsParser.Block> blocks = parser.parse(db.getSession(), file, null, null,
       format("<duplications>\n" +
         "  <g>\n" +
         "    <b s=\"31\" l=\"5\" r=\"%s\"/>\n" +
@@ -80,7 +80,7 @@ public class DuplicationsParserTest {
     ComponentDto project = db.components().insertPrivateProject();
     ComponentDto file1 = db.components().insertComponent(newFileDto(project));
     ComponentDto file2 = db.components().insertComponent(newFileDto(project));
-    List<DuplicationsParser.Block> blocks = parser.parse(db.getSession(), file1, null,
+    List<DuplicationsParser.Block> blocks = parser.parse(db.getSession(), file1, null, null,
       format("<duplications>\n" +
         "  <g>\n" +
         "    <b s=\"20\" l=\"5\" r=\"%s\"/>\n" +
@@ -111,7 +111,7 @@ public class DuplicationsParserTest {
     ComponentDto file2 = db.components().insertComponent(newFileDto(project1));
     ComponentDto project2 = db.components().insertPrivateProject();
     ComponentDto fileOnProject2 = db.components().insertComponent(newFileDto(project2));
-    List<DuplicationsParser.Block> blocks = parser.parse(db.getSession(), file1, null,
+    List<DuplicationsParser.Block> blocks = parser.parse(db.getSession(), file1, null, null,
       format("<duplications>\n" +
         "  <g>\n" +
         "    <b s=\"148\" l=\"24\" r=\"%s\"/>\n" +
@@ -154,7 +154,7 @@ public class DuplicationsParserTest {
     ComponentDto file2 = db.components().insertComponent(newFileDto(project2)
       .setDbKey("com.sonarsource.orchestrator:sonar-orchestrator:src/main/java/com/sonar/orchestrator/util/CommandExecutor.java")
       .setLongName("CommandExecutor"));
-    List<DuplicationsParser.Block> blocks = parser.parse(db.getSession(), file1, null,
+    List<DuplicationsParser.Block> blocks = parser.parse(db.getSession(), file1, null, null,
       format("<duplications>\n" +
         "  <g>\n" +
         "    <b s=\"94\" l=\"101\" r=\"%s\"/>\n" +
@@ -180,7 +180,7 @@ public class DuplicationsParserTest {
   public void duplication_on_not_existing_file() {
     ComponentDto project = db.components().insertPrivateProject();
     ComponentDto file = db.components().insertComponent(newFileDto(project));
-    List<DuplicationsParser.Block> blocks = parser.parse(db.getSession(), file, null,
+    List<DuplicationsParser.Block> blocks = parser.parse(db.getSession(), file, null, null,
       format("<duplications>\n" +
         "  <g>\n" +
         "    <b s=\"20\" l=\"5\" r=\"%s\"/>\n" +
@@ -243,7 +243,39 @@ public class DuplicationsParserTest {
     ComponentDto branch = db.components().insertProjectBranch(project);
     ComponentDto file1 = db.components().insertComponent(newFileDto(branch));
     ComponentDto file2 = db.components().insertComponent(newFileDto(branch));
-    List<DuplicationsParser.Block> blocks = parser.parse(db.getSession(), file1, branch.getBranch(),
+    List<DuplicationsParser.Block> blocks = parser.parse(db.getSession(), file1, branch.getBranch(), null,
+      format("<duplications>\n" +
+        "  <g>\n" +
+        "    <b s=\"20\" l=\"5\" r=\"%s\"/>\n" +
+        "    <b s=\"31\" l=\"5\" r=\"%s\"/>\n" +
+        "  </g>\n" +
+        "</duplications>", file2.getDbKey(), file1.getDbKey()));
+    assertThat(blocks).hasSize(1);
+
+    List<DuplicationsParser.Duplication> duplications = blocks.get(0).getDuplications();
+    assertThat(duplications).hasSize(2);
+
+    // Current file comes first
+    DuplicationsParser.Duplication duplication1 = duplications.get(0);
+    assertThat(duplication1.file()).isEqualTo(file1);
+    assertThat(duplication1.file().getKey()).isEqualTo(file1.getKey());
+    assertThat(duplication1.from()).isEqualTo(31);
+    assertThat(duplication1.size()).isEqualTo(5);
+
+    DuplicationsParser.Duplication duplication2 = duplications.get(1);
+    assertThat(duplication2.file()).isEqualTo(file2);
+    assertThat(duplication2.file().getKey()).isEqualTo(file2.getKey());
+    assertThat(duplication2.from()).isEqualTo(20);
+    assertThat(duplication2.size()).isEqualTo(5);
+  }
+
+  @Test
+  public void duplication_on_pull_request() {
+    ComponentDto project = db.components().insertMainBranch();
+    ComponentDto pullRequest = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.PULL_REQUEST));
+    ComponentDto file1 = db.components().insertComponent(newFileDto(pullRequest));
+    ComponentDto file2 = db.components().insertComponent(newFileDto(pullRequest));
+    List<DuplicationsParser.Block> blocks = parser.parse(db.getSession(), file1, null, pullRequest.getPullRequest(),
       format("<duplications>\n" +
         "  <g>\n" +
         "    <b s=\"20\" l=\"5\" r=\"%s\"/>\n" +
@@ -270,12 +302,8 @@ public class DuplicationsParserTest {
   }
 
   private static DuplicationsParser.Duplication duplication(List<DuplicationsParser.Duplication> duplications, @Nullable final String componentKey) {
-    return Iterables.find(duplications, new Predicate<DuplicationsParser.Duplication>() {
-      @Override
-      public boolean apply(@Nullable DuplicationsParser.Duplication input) {
-        return input != null && (componentKey == null ? input.file() == null : input.file() != null && componentKey.equals(input.file().getDbKey()));
-      }
-    });
+    return Iterables.find(duplications, input -> input != null && (componentKey == null ? input.file() == null
+      : input.file() != null && componentKey.equals(input.file().getDbKey())));
   }
 
 }
index df9cb7591f28a0b66de5168d12d5c52ada1dc2a6..488fccfe260d1f8449014484dab7ea4a05811426 100644 (file)
@@ -28,6 +28,7 @@ import org.sonar.api.measures.CoreMetrics;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.api.web.UserRole;
 import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchType;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.metric.MetricDto;
 import org.sonar.db.organization.OrganizationDto;
@@ -76,7 +77,7 @@ public class ShowActionTest {
     assertThat(show.since()).isEqualTo("4.4");
     assertThat(show.isInternal()).isFalse();
     assertThat(show.responseExampleAsString()).isNotEmpty();
-    assertThat(show.params()).hasSize(3);
+    assertThat(show.params()).hasSize(4);
   }
 
   @Test
@@ -159,6 +160,58 @@ public class ShowActionTest {
         file.getKey(), file.longName(), file.uuid(), branch.getKey(), branch.uuid(), project.longName(), file.getBranch()));
   }
 
+  @Test
+  public void duplications_by_file_key_and_pull_request() {
+    ComponentDto project = db.components().insertMainBranch();
+    userSessionRule.addProjectPermission(UserRole.CODEVIEWER, project);
+    ComponentDto pullRequest = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.PULL_REQUEST));
+    ComponentDto file = db.components().insertComponent(newFileDto(pullRequest));
+    db.measures().insertLiveMeasure(file, dataMetric, m -> m.setData(format("<duplications>\n" +
+      "  <g>\n" +
+      "    <b s=\"31\" l=\"5\" r=\"%s\"/>\n" +
+      "    <b s=\"20\" l=\"5\" r=\"%s\"/>\n" +
+      "  </g>\n" +
+      "</duplications>\n", file.getDbKey(), file.getDbKey())));
+
+    String result = ws.newRequest()
+      .setParam("key", file.getKey())
+      .setParam("pullRequest", pullRequest.getPullRequest())
+      .execute()
+      .getInput();
+
+    assertJson(result).isSimilarTo(
+      format("{\n" +
+        "  \"duplications\": [\n" +
+        "    {\n" +
+        "      \"blocks\": [\n" +
+        "        {\n" +
+        "          \"from\": 20,\n" +
+        "          \"size\": 5,\n" +
+        "          \"_ref\": \"1\"\n" +
+        "        },\n" +
+        "        {\n" +
+        "          \"from\": 31,\n" +
+        "          \"size\": 5,\n" +
+        "          \"_ref\": \"1\"\n" +
+        "        }\n" +
+        "      ]\n" +
+        "    }\n" +
+        "  ],\n" +
+        "  \"files\": {\n" +
+        "    \"1\": {\n" +
+        "      \"key\": \"%s\",\n" +
+        "      \"name\": \"%s\",\n" +
+        "      \"uuid\": \"%s\",\n" +
+        "      \"project\": \"%s\",\n" +
+        "      \"projectUuid\": \"%s\",\n" +
+        "      \"projectName\": \"%s\"\n" +
+        "      \"pullRequest\": \"%s\"\n" +
+        "    }\n" +
+        "  }\n" +
+        "}",
+        file.getKey(), file.longName(), file.uuid(), pullRequest.getKey(), pullRequest.uuid(), project.longName(), file.getPullRequest()));
+  }
+
   @Test
   public void fail_if_file_does_not_exist() {
     expectedException.expect(NotFoundException.class);
index 67c85ae9b6af6dd6d465b780f15531c7df0f99e1..4042c185b339b69d03ac4947ab8c107e0208be8e 100644 (file)
@@ -32,6 +32,7 @@ import org.sonar.db.component.ComponentDto;
 import org.sonar.test.JsonAssert;
 
 import static com.google.common.collect.Lists.newArrayList;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
 import static org.sonar.db.component.ComponentTesting.newModuleDto;
 
@@ -53,7 +54,7 @@ public class ShowResponseBuilderTest {
       new DuplicationsParser.Duplication(file1, 57, 12),
       new DuplicationsParser.Duplication(file2, 73, 12))));
 
-    test(blocks, null,
+    test(blocks, null, null,
       "{\n" +
         "  \"duplications\": [\n" +
         "    {\n" +
@@ -98,7 +99,7 @@ public class ShowResponseBuilderTest {
       new DuplicationsParser.Duplication(file1, 57, 12),
       new DuplicationsParser.Duplication(file2, 73, 12))));
 
-    test(blocks, null,
+    test(blocks, null, null,
       "{\n" +
         "  \"duplications\": [\n" +
         "    {\n" +
@@ -139,7 +140,7 @@ public class ShowResponseBuilderTest {
       // Duplication on a removed file
       new DuplicationsParser.Duplication(null, 73, 12))));
 
-    test(blocks, null,
+    test(blocks, null, null,
       "{\n" +
         "  \"duplications\": [\n" +
         "    {\n" +
@@ -175,7 +176,7 @@ public class ShowResponseBuilderTest {
       new DuplicationsParser.Duplication(file1, 57, 12),
       new DuplicationsParser.Duplication(file2, 73, 12))));
 
-    test(blocks, branch.getBranch(),
+    test(blocks, branch.getBranch(), null,
       "{\n" +
         "  \"duplications\": [\n" +
         "    {\n" +
@@ -208,15 +209,59 @@ public class ShowResponseBuilderTest {
         "}");
   }
 
+  @Test
+  public void write_duplications_on_pull_request() {
+    ComponentDto project = db.components().insertMainBranch();
+    ComponentDto pullRequest = db.components().insertProjectBranch(project, b -> b.setBranchType(PULL_REQUEST));
+    ComponentDto file1 = db.components().insertComponent(newFileDto(pullRequest));
+    ComponentDto file2 = db.components().insertComponent(newFileDto(pullRequest));
+    List<DuplicationsParser.Block> blocks = newArrayList();
+    blocks.add(new DuplicationsParser.Block(newArrayList(
+      new DuplicationsParser.Duplication(file1, 57, 12),
+      new DuplicationsParser.Duplication(file2, 73, 12))));
+
+    test(blocks, null, pullRequest.getPullRequest(),
+      "{\n" +
+        "  \"duplications\": [\n" +
+        "    {\n" +
+        "      \"blocks\": [\n" +
+        "        {\n" +
+        "          \"from\": 57, \"size\": 12, \"_ref\": \"1\"\n" +
+        "        },\n" +
+        "        {\n" +
+        "          \"from\": 73, \"size\": 12, \"_ref\": \"2\"\n" +
+        "        }\n" +
+        "      ]\n" +
+        "    }," +
+        "  ],\n" +
+        "  \"files\": {\n" +
+        "    \"1\": {\n" +
+        "      \"key\": \"" + file1.getKey() + "\",\n" +
+        "      \"name\": \"" + file1.longName() + "\",\n" +
+        "      \"project\": \"" + pullRequest.getKey() + "\",\n" +
+        "      \"projectName\": \"" + pullRequest.longName() + "\",\n" +
+        "      \"pullRequest\": \"" + pullRequest.getPullRequest() + "\",\n" +
+        "    },\n" +
+        "    \"2\": {\n" +
+        "      \"key\": \"" + file2.getKey() + "\",\n" +
+        "      \"name\": \"" + file2.longName() + "\",\n" +
+        "      \"project\": \"" + pullRequest.getKey() + "\",\n" +
+        "      \"projectName\": \"" + pullRequest.longName() + "\",\n" +
+        "      \"pullRequest\": \"" + pullRequest.getPullRequest() + "\",\n" +
+        "    }\n" +
+        "  }" +
+        "}");
+  }
+
   @Test
   public void write_nothing_when_no_data() {
-    test(Collections.emptyList(), null, "{\"duplications\": [], \"files\": {}}");
+    test(Collections.emptyList(), null, null,"{\"duplications\": [], \"files\": {}}");
   }
 
-  private void test(List<DuplicationsParser.Block> blocks, @Nullable String branch, String expected) {
+  private void test(List<DuplicationsParser.Block> blocks, @Nullable String branch, @Nullable String pullRequest, String expected) {
     StringWriter output = new StringWriter();
     JsonWriter jsonWriter = JsonWriter.of(output);
-    ProtobufJsonFormat.write(underTest.build(db.getSession(), blocks, branch), jsonWriter);
+    ProtobufJsonFormat.write(underTest.build(db.getSession(), blocks, branch, pullRequest), jsonWriter);
     JsonAssert.assertJson(output.toString()).isSimilarTo(expected);
   }
 
index 26e244ece619dc063415377a16f49409fd9839c1..1ec5b97df725e2503caee0c55ec45abd3890095f 100644 (file)
@@ -84,7 +84,7 @@ public class IssueChangeNotificationTest {
 
   @Test
   public void set_project_without_branch() {
-    IssueChangeNotification result = notification.setProject("MyService", "My Service", null);
+    IssueChangeNotification result = notification.setProject("MyService", "My Service", null, null);
     assertThat(result.getFieldValue("projectKey")).isEqualTo("MyService");
     assertThat(result.getFieldValue("projectName")).isEqualTo("My Service");
     assertThat(result.getFieldValue("branch")).isNull();
@@ -92,12 +92,20 @@ public class IssueChangeNotificationTest {
 
   @Test
   public void set_project_with_branch() {
-    IssueChangeNotification result = notification.setProject("MyService", "My Service", "feature1");
+    IssueChangeNotification result = notification.setProject("MyService", "My Service", "feature1", null);
     assertThat(result.getFieldValue("projectKey")).isEqualTo("MyService");
     assertThat(result.getFieldValue("projectName")).isEqualTo("My Service");
     assertThat(result.getFieldValue("branch")).isEqualTo("feature1");
   }
 
+  @Test
+  public void set_project_with_pull_request() {
+    IssueChangeNotification result = notification.setProject("MyService", "My Service", null, "pr-123");
+    assertThat(result.getFieldValue("projectKey")).isEqualTo("MyService");
+    assertThat(result.getFieldValue("projectName")).isEqualTo("My Service");
+    assertThat(result.getFieldValue("pullRequest")).isEqualTo("pr-123");
+  }
+
   @Test
   public void set_component() {
     IssueChangeNotification result = notification.setComponent(new ComponentDto().setDbKey("MyService").setLongName("My Service"));
index b48a6aaad671f1749df5aebddb61e784ab8d840b..c608abfe57e71dfe50cda5c527f1c3ba3e2ac0ef 100644 (file)
@@ -170,7 +170,7 @@ public class IssueChangesEmailTemplateTest {
 
     Notification notification = new IssueChangeNotification()
       .setChangeAuthorLogin("simon")
-      .setProject("Struts", "org.apache:struts", null);
+      .setProject("Struts", "org.apache:struts", null, null);
 
     EmailMessage message = underTest.format(notification);
     assertThat(message.getFrom()).isEqualTo("Simon");
@@ -182,7 +182,7 @@ public class IssueChangesEmailTemplateTest {
 
     Notification notification = new IssueChangeNotification()
       .setChangeAuthorLogin("simon")
-      .setProject("Struts", "org.apache:struts", null);
+      .setProject("Struts", "org.apache:struts", null, null);
 
     EmailMessage message = underTest.format(notification);
     assertThat(message.getFrom()).isEqualTo("simon");
index bfaff09a8b6f4f519850ecf70f0f220f9212f7ca..819bdc2f9bf4c82d6744f5111883f44632da45e3 100644 (file)
@@ -83,7 +83,7 @@ public class NewIssuesNotificationTest {
 
   @Test
   public void set_project_without_branch() {
-    underTest.setProject("project-key", "project-long-name", null);
+    underTest.setProject("project-key", "project-long-name", null, null);
 
     assertThat(underTest.getFieldValue(NewIssuesEmailTemplate.FIELD_PROJECT_NAME)).isEqualTo("project-long-name");
     assertThat(underTest.getFieldValue(NewIssuesEmailTemplate.FIELD_PROJECT_KEY)).isEqualTo("project-key");
@@ -92,13 +92,22 @@ public class NewIssuesNotificationTest {
 
   @Test
   public void set_project_with_branch() {
-    underTest.setProject("project-key", "project-long-name", "feature");
+    underTest.setProject("project-key", "project-long-name", "feature", null);
 
     assertThat(underTest.getFieldValue(NewIssuesEmailTemplate.FIELD_PROJECT_NAME)).isEqualTo("project-long-name");
     assertThat(underTest.getFieldValue(NewIssuesEmailTemplate.FIELD_PROJECT_KEY)).isEqualTo("project-key");
     assertThat(underTest.getFieldValue(NewIssuesEmailTemplate.FIELD_BRANCH)).isEqualTo("feature");
   }
 
+  @Test
+  public void set_project_with_pull_request() {
+    underTest.setProject("project-key", "project-long-name", null, "pr-123");
+
+    assertThat(underTest.getFieldValue(NewIssuesEmailTemplate.FIELD_PROJECT_NAME)).isEqualTo("project-long-name");
+    assertThat(underTest.getFieldValue(NewIssuesEmailTemplate.FIELD_PROJECT_KEY)).isEqualTo("project-key");
+    assertThat(underTest.getFieldValue(NewIssuesEmailTemplate.FIELD_PULL_REQUEST)).isEqualTo("pr-123");
+  }
+
   @Test
   public void set_project_version() {
     String version = randomAlphanumeric(5);
index 3b7f3fdce8daac63e48c60a18014be11daacce74..538917daf43c9e54077591f8acd087eb008123fd 100644 (file)
@@ -68,6 +68,8 @@ import static org.sonar.api.utils.DateUtils.addDays;
 import static org.sonar.api.utils.DateUtils.parseDateTime;
 import static org.sonar.core.util.Uuids.UUID_EXAMPLE_01;
 import static org.sonar.core.util.Uuids.UUID_EXAMPLE_02;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
+import static org.sonar.db.component.BranchType.SHORT;
 import static org.sonar.db.component.ComponentTesting.newDirectory;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
 import static org.sonar.db.component.ComponentTesting.newModuleDto;
@@ -78,6 +80,7 @@ import static org.sonar.db.issue.IssueTesting.newIssue;
 import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_BRANCH;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_KEYS;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PROJECT_KEYS;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PULL_REQUEST;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SINCE_LEAK_PERIOD;
 
 public class SearchActionComponentsTest {
@@ -841,7 +844,7 @@ public class SearchActionComponentsTest {
     userSession.addProjectPermission(UserRole.USER, project);
     ComponentDto projectFile = db.components().insertComponent(newFileDto(project));
     IssueDto projectIssue = db.issues().insertIssue(newIssue(rule, project, projectFile));
-    ComponentDto branch = db.components().insertProjectBranch(project);
+    ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setBranchType(SHORT));
     ComponentDto branchFile = db.components().insertComponent(newFileDto(branch));
     IssueDto branchIssue = db.issues().insertIssue(newIssue(rule, branch, branchFile));
     allowAnyoneOnProjects(project);
@@ -862,6 +865,34 @@ public class SearchActionComponentsTest {
         tuple(branch.getKey(), branch.getBranch()));
   }
 
+  @Test
+  public void search_by_pull_request() {
+    RuleDefinitionDto rule = db.rules().insert();
+    ComponentDto project = db.components().insertPrivateProject();
+    userSession.addProjectPermission(UserRole.USER, project);
+    ComponentDto projectFile = db.components().insertComponent(newFileDto(project));
+    IssueDto projectIssue = db.issues().insertIssue(newIssue(rule, project, projectFile));
+    ComponentDto pullRequest = db.components().insertProjectBranch(project, b -> b.setBranchType(PULL_REQUEST));
+    ComponentDto pullRequestFile = db.components().insertComponent(newFileDto(pullRequest));
+    IssueDto pullRequestIssue = db.issues().insertIssue(newIssue(rule, pullRequest, pullRequestFile));
+    allowAnyoneOnProjects(project);
+    indexIssuesAndViews();
+
+    SearchWsResponse result = ws.newRequest()
+      .setParam(PARAM_COMPONENT_KEYS, pullRequest.getKey())
+      .setParam(PARAM_PULL_REQUEST, pullRequest.getPullRequest())
+      .executeProtobuf(SearchWsResponse.class);
+
+    assertThat(result.getIssuesList())
+      .extracting(Issue::getKey, Issue::getComponent, Issue::getPullRequest)
+      .containsExactlyInAnyOrder(tuple(pullRequestIssue.getKey(), pullRequestFile.getKey(), pullRequestFile.getPullRequest()));
+    assertThat(result.getComponentsList())
+      .extracting(Issues.Component::getKey, Issues.Component::getPullRequest)
+      .containsExactlyInAnyOrder(
+        tuple(pullRequestFile.getKey(), pullRequestFile.getPullRequest()),
+        tuple(pullRequest.getKey(), pullRequest.getPullRequest()));
+  }
+
   @Test
   public void search_using_main_branch_name() {
     RuleDefinitionDto rule = db.rules().insert();
index df3f8b2056a88aea71c5ae7b32a7693616a5453d..b8283b57c8bf98d098f60e90475bbf55288ecd6b 100644 (file)
@@ -135,7 +135,7 @@ public class SearchActionTest {
 
     assertThat(def.params()).extracting("key").containsExactlyInAnyOrder(
       "additionalFields", "asc", "assigned", "assignees", "authors", "componentKeys", "componentRootUuids", "componentRoots", "componentUuids", "components", "branch",
-      "organization",
+      "pullRequest", "organization",
       "createdAfter", "createdAt", "createdBefore", "createdInLast", "directories", "facetMode", "facets", "fileUuids", "issues", "languages", "moduleUuids", "onComponentOnly",
       "p", "projectUuids", "projects", "ps", "resolutions", "resolved", "rules", "s", "severities", "sinceLeakPeriod",
       "statuses", "tags", "types");
index 37bc24313552995377dc0254b868721514c11801..4aff277f099934a452c5dd4d10df51d5a29c53bb 100644 (file)
@@ -332,7 +332,7 @@ public class LiveMeasureComputerImplTest {
     markProjectAsAnalyzed(project);
     db.measures().insertLiveMeasure(project, alertStatusMetric, m -> m.setData(Metric.Level.WARN.name()));
     db.measures().insertLiveMeasure(project, intMetric, m -> m.setVariation(42.0).setValue(null));
-    BranchDto branch = db.getDbClient().branchDao().selectByKey(db.getSession(), project.projectUuid(), "master")
+    BranchDto branch = db.getDbClient().branchDao().selectByBranchKey(db.getSession(), project.projectUuid(), "master")
       .orElseThrow(() -> new IllegalStateException("Can't find master branch"));
 
     List<QGChangeEvent> result = run(file1, newQualifierBasedIntLeakFormula());
index 945165d987afbb1be6bad8d05d7ce174f9907f9a..602d34189068ac9440d47484b85a6b308879e405 100644 (file)
@@ -79,6 +79,18 @@ public class LiveQualityGateComputerImplTest {
     assertThat(result).isSameAs(ShortLivingBranchQualityGate.GATE);
   }
 
+  @Test
+  public void loadQualityGate_returns_hardcoded_gate_for_pull_requests() {
+    OrganizationDto organization = db.organizations().insert();
+    ComponentDto project = db.components().insertPublicProject(organization);
+    BranchDto pullRequest = newBranchDto(project).setBranchType(BranchType.PULL_REQUEST);
+    db.components().insertProjectBranch(project, pullRequest);
+
+    QualityGate result = underTest.loadQualityGate(db.getSession(), organization, project, pullRequest);
+
+    assertThat(result).isSameAs(ShortLivingBranchQualityGate.GATE);
+  }
+
   @Test
   public void loadQualityGate_on_long_branch_returns_organization_default_gate() {
     OrganizationDto organization = db.organizations().insert();
index 3b1542d0b7d462c731ae76cd2e917057a7fea66d..b900442a8ec663b7450b5074f348a239a44321de 100644 (file)
@@ -50,8 +50,10 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.tuple;
 import static org.sonar.api.utils.DateUtils.parseDateTime;
 import static org.sonar.api.web.UserRole.USER;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
 import static org.sonar.db.component.ComponentTesting.newProjectCopy;
+import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_PULL_REQUEST;
 import static org.sonar.server.computation.task.projectanalysis.metric.Metric.MetricType.INT;
 import static org.sonar.test.JsonAssert.assertJson;
 import static org.sonar.server.component.ws.MeasuresWsParameters.DEPRECATED_PARAM_COMPONENT_ID;
@@ -78,7 +80,7 @@ public class ComponentActionTest {
 
     assertThat(def.since()).isEqualTo("5.4");
     assertThat(def.params()).extracting(Param::key)
-      .containsExactlyInAnyOrder("componentId", "component", "branch", "metricKeys", "additionalFields", "developerId", "developerKey");
+      .containsExactlyInAnyOrder("componentId", "component", "branch", "pullRequest", "metricKeys", "additionalFields", "developerId", "developerKey");
     assertThat(def.param("developerId").deprecatedSince()).isEqualTo("6.4");
     assertThat(def.param("developerKey").deprecatedSince()).isEqualTo("6.4");
     assertThat(def.param("componentId").deprecatedSince()).isEqualTo("6.6");
@@ -142,6 +144,29 @@ public class ComponentActionTest {
       .containsExactlyInAnyOrder(tuple(complexity.getKey(), measure.getValue()));
   }
 
+  @Test
+  public void pull_request() {
+    ComponentDto project = db.components().insertPrivateProject();
+    userSession.addProjectPermission(UserRole.USER, project);
+    ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("pr-123").setBranchType(PULL_REQUEST));
+    SnapshotDto analysis = db.components().insertSnapshot(branch);
+    ComponentDto file = db.components().insertComponent(newFileDto(branch));
+    MetricDto complexity = db.measures().insertMetric(m1 -> m1.setKey("complexity").setValueType(INT.name()));
+    LiveMeasureDto measure = db.measures().insertLiveMeasure(file, complexity, m -> m.setValue(12.0d).setVariation(2.0d));
+
+    ComponentWsResponse response = ws.newRequest()
+      .setParam(PARAM_COMPONENT, file.getKey())
+      .setParam(PARAM_PULL_REQUEST, "pr-123")
+      .setParam(PARAM_METRIC_KEYS, complexity.getKey())
+      .executeProtobuf(ComponentWsResponse.class);
+
+    assertThat(response.getComponent()).extracting(Component::getKey, Component::getPullRequest)
+      .containsExactlyInAnyOrder(file.getKey(), "pr-123");
+    assertThat(response.getComponent().getMeasuresList())
+      .extracting(Measures.Measure::getMetric, m -> parseDouble(m.getValue()))
+      .containsExactlyInAnyOrder(tuple(complexity.getKey(), measure.getValue()));
+  }
+
   @Test
   public void reference_uuid_in_the_response() {
     userSession.logIn().setRoot();
@@ -351,7 +376,7 @@ public class ComponentActionTest {
     db.components().insertProjectBranch(project, b -> b.setKey("my_branch"));
 
     expectedException.expect(IllegalArgumentException.class);
-    expectedException.expectMessage("'componentId' and 'branch' parameters cannot be used at the same time");
+    expectedException.expectMessage("Parameter 'componentId' cannot be used at the same time as 'branch' or 'pullRequest'");
 
     ws.newRequest()
       .setParam(DEPRECATED_PARAM_COMPONENT_ID, file.uuid())
index 36e71bc488d521540c4b28bb584aba6a9c4e0764..cc42a73db20d5a7470e01ca8f91e87422eaa0328 100644 (file)
@@ -68,6 +68,7 @@ import static org.sonar.api.resources.Qualifiers.UNIT_TEST_FILE;
 import static org.sonar.api.server.ws.WebService.Param.SORT;
 import static org.sonar.api.utils.DateUtils.parseDateTime;
 import static org.sonar.api.web.UserRole.USER;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
 import static org.sonar.db.component.ComponentTesting.newDirectory;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
 import static org.sonar.db.component.ComponentTesting.newProjectCopy;
@@ -82,6 +83,7 @@ import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_METRIC_KE
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_METRIC_PERIOD_SORT;
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_METRIC_SORT;
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_METRIC_SORT_FILTER;
+import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_PULL_REQUEST;
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_QUALIFIERS;
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_STRATEGY;
 import static org.sonar.server.measure.ws.ComponentTreeAction.LEAVES_STRATEGY;
@@ -489,6 +491,29 @@ public class ComponentTreeActionTest {
       .containsExactlyInAnyOrder(tuple(complexity.getKey(), measure.getValue()));
   }
 
+  @Test
+  public void pull_request() {
+    OrganizationDto organization = db.organizations().insert();
+    ComponentDto project = db.components().insertPrivateProject(organization);
+    ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("pr-123").setBranchType(PULL_REQUEST));
+    SnapshotDto analysis = db.components().insertSnapshot(branch);
+    ComponentDto file = db.components().insertComponent(newFileDto(branch));
+    MetricDto complexity = db.measures().insertMetric(m -> m.setValueType(INT.name()));
+    LiveMeasureDto measure = db.measures().insertLiveMeasure(file, complexity, m -> m.setValue(12.0d));
+
+    ComponentTreeWsResponse response = ws.newRequest()
+      .setParam(PARAM_COMPONENT, file.getKey())
+      .setParam(PARAM_PULL_REQUEST, "pr-123")
+      .setParam(PARAM_METRIC_KEYS, complexity.getKey())
+      .executeProtobuf(Measures.ComponentTreeWsResponse.class);
+
+    assertThat(response.getBaseComponent()).extracting(Measures.Component::getKey, Measures.Component::getPullRequest)
+      .containsExactlyInAnyOrder(file.getKey(), "pr-123");
+    assertThat(response.getBaseComponent().getMeasuresList())
+      .extracting(Measures.Measure::getMetric, m -> parseDouble(m.getValue()))
+      .containsExactlyInAnyOrder(tuple(complexity.getKey(), measure.getValue()));
+  }
+
   @Test
   public void return_deprecated_id_in_the_response() {
     ComponentDto project = db.components().insertPrivateProject();
index f56b7acecc170bbc79a814b5293a756db90c4abd..0b366d51a5033b5c3743d9cbcf3aa4eb91624f12 100644 (file)
@@ -62,12 +62,14 @@ import static org.assertj.core.api.Assertions.tuple;
 import static org.sonar.api.utils.DateUtils.formatDateTime;
 import static org.sonar.api.utils.DateUtils.parseDateTime;
 import static org.sonar.core.util.Protobuf.setNullable;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
 import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto;
 import static org.sonar.db.component.SnapshotDto.STATUS_UNPROCESSED;
 import static org.sonar.db.component.SnapshotTesting.newAnalysis;
 import static org.sonar.db.measure.MeasureTesting.newMeasureDto;
 import static org.sonar.db.metric.MetricTesting.newMetricDto;
+import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_PULL_REQUEST;
 import static org.sonar.test.JsonAssert.assertJson;
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_BRANCH;
 import static org.sonar.server.component.ws.MeasuresWsParameters.PARAM_COMPONENT;
@@ -287,6 +289,29 @@ public class SearchHistoryActionTest {
       .containsExactlyInAnyOrder(measure.getValue());
   }
 
+  @Test
+  public void pull_request() {
+    ComponentDto project = db.components().insertPrivateProject();
+    userSession.addProjectPermission(UserRole.USER, project);
+    ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("pr-123").setBranchType(PULL_REQUEST));
+    ComponentDto file = db.components().insertComponent(newFileDto(branch));
+    SnapshotDto analysis = db.components().insertSnapshot(branch);
+    MeasureDto measure = db.measures().insertMeasure(file, analysis, nclocMetric, m -> m.setValue(2d));
+
+    SearchHistoryResponse result = ws.newRequest()
+      .setParam(PARAM_COMPONENT, file.getKey())
+      .setParam(PARAM_PULL_REQUEST, "pr-123")
+      .setParam(PARAM_METRICS, "ncloc")
+      .executeProtobuf(SearchHistoryResponse.class);
+
+    assertThat(result.getMeasuresList()).extracting(HistoryMeasure::getMetric).hasSize(1);
+    HistoryMeasure historyMeasure = result.getMeasures(0);
+    assertThat(historyMeasure.getMetric()).isEqualTo(nclocMetric.getKey());
+    assertThat(historyMeasure.getHistoryList())
+      .extracting(m -> parseDouble(m.getValue()))
+      .containsExactlyInAnyOrder(measure.getValue());
+  }
+
   @Test
   public void fail_when_using_branch_db_key() throws Exception {
     OrganizationDto organization = db.organizations().insert();
@@ -372,7 +397,7 @@ public class SearchHistoryActionTest {
     assertThat(definition.isPost()).isFalse();
     assertThat(definition.isInternal()).isFalse();
     assertThat(definition.since()).isEqualTo("6.3");
-    assertThat(definition.params()).hasSize(7);
+    assertThat(definition.params()).hasSize(8);
 
     Param branch = definition.param("branch");
     assertThat(branch.since()).isEqualTo("6.6");
index adf30f7b3b688822405614a397d7f2a23bf15038..e5245cb74921bfe05e3ad98463adc7433ba39f76 100644 (file)
@@ -54,9 +54,11 @@ import static org.sonar.api.utils.DateUtils.formatDate;
 import static org.sonar.api.utils.DateUtils.formatDateTime;
 import static org.sonar.api.utils.DateUtils.parseDateTime;
 import static org.sonar.core.util.Protobuf.setNullable;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
 import static org.sonar.db.component.SnapshotTesting.newAnalysis;
 import static org.sonar.db.event.EventTesting.newEvent;
+import static org.sonar.server.projectanalysis.ws.ProjectAnalysesWsParameters.PARAM_PULL_REQUEST;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
 import static org.sonar.test.JsonAssert.assertJson;
 import static org.sonarqube.ws.client.WsRequest.Method.POST;
@@ -356,6 +358,24 @@ public class SearchActionTest {
 
   }
 
+  @Test
+  public void pull_request() {
+    ComponentDto project = db.components().insertPrivateProject();
+    userSession.addProjectPermission(UserRole.USER, project);
+    ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("pr-123").setBranchType(PULL_REQUEST));
+    SnapshotDto analysis = db.components().insertSnapshot(newAnalysis(branch));
+    EventDto event = db.events().insertEvent(newEvent(analysis).setCategory(EventCategory.QUALITY_GATE.getLabel()));
+
+    List<Analysis> result = call(SearchRequest.builder()
+      .setProject(project.getKey())
+      .setPullRequest("pr-123")
+      .build())
+        .getAnalysesList();
+
+    assertThat(result).extracting(Analysis::getKey).containsExactlyInAnyOrder(analysis.getUuid());
+    assertThat(result.get(0).getEventsList()).extracting(Event::getKey).containsExactlyInAnyOrder(event.getUuid());
+  }
+
   @Test
   public void empty_response() {
     ComponentDto project = db.components().insertPrivateProject();
@@ -422,7 +442,7 @@ public class SearchActionTest {
     assertThat(definition.responseExampleAsString()).isNotEmpty();
     assertThat(definition.param("project").isRequired()).isTrue();
     assertThat(definition.param("category")).isNotNull();
-    assertThat(definition.params()).hasSize(7);
+    assertThat(definition.params()).hasSize(8);
 
     Param from = definition.param("from");
     assertThat(from.since()).isEqualTo("6.5");
@@ -451,6 +471,7 @@ public class SearchActionTest {
       .setMethod(POST.name());
     setNullable(wsRequest.getProject(), project -> request.setParam(PARAM_PROJECT, project));
     setNullable(wsRequest.getBranch(), branch -> request.setParam(PARAM_BRANCH, branch));
+    setNullable(wsRequest.getPullRequest(), branch -> request.setParam(PARAM_PULL_REQUEST, branch));
     setNullable(wsRequest.getCategory(), category -> request.setParam(PARAM_CATEGORY, category.name()));
     setNullable(wsRequest.getPage(), page -> request.setParam(Param.PAGE, String.valueOf(page)));
     setNullable(wsRequest.getPageSize(), pageSize -> request.setParam(Param.PAGE_SIZE, String.valueOf(pageSize)));
index 1effcf08f68dfd9fc30bc3a5693cde23eeb7e749..43a40176670287007efbf7771890d67cb90cec84 100644 (file)
@@ -43,12 +43,15 @@ import org.sonar.api.utils.log.LoggerLevel;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.server.qualitygate.changeevent.QGChangeEventListener.ChangedIssue;
+import org.sonar.server.qualitygate.changeevent.QGChangeEventListenersImpl.ChangedIssueImpl;
 
+import static java.util.Arrays.asList;
 import static java.util.Collections.emptyList;
 import static java.util.Collections.emptySet;
 import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.tuple;
+import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.same;
 import static org.mockito.Mockito.doThrow;
@@ -224,20 +227,51 @@ public class QGChangeEventListenersImplTest {
     verifyNoMoreInteractions(listener1, listener2, listener3);
   }
 
+  @Test
+  public void test_status_mapping() {
+    assertThat(ChangedIssueImpl.statusOf(new DefaultIssue().setStatus(Issue.STATUS_OPEN))).isEqualTo(QGChangeEventListener.Status.OPEN);
+    assertThat(ChangedIssueImpl.statusOf(new DefaultIssue().setStatus(Issue.STATUS_REOPENED))).isEqualTo(QGChangeEventListener.Status.REOPENED);
+    assertThat(ChangedIssueImpl.statusOf(new DefaultIssue().setStatus(Issue.STATUS_CONFIRMED))).isEqualTo(QGChangeEventListener.Status.CONFIRMED);
+    assertThat(ChangedIssueImpl.statusOf(new DefaultIssue().setStatus(Issue.STATUS_RESOLVED).setResolution(Issue.RESOLUTION_FALSE_POSITIVE)))
+      .isEqualTo(QGChangeEventListener.Status.RESOLVED_FP);
+    assertThat(ChangedIssueImpl.statusOf(new DefaultIssue().setStatus(Issue.STATUS_RESOLVED).setResolution(Issue.RESOLUTION_WONT_FIX)))
+      .isEqualTo(QGChangeEventListener.Status.RESOLVED_WF);
+    assertThat(ChangedIssueImpl.statusOf(new DefaultIssue().setStatus(Issue.STATUS_RESOLVED).setResolution(Issue.RESOLUTION_FIXED)))
+      .isEqualTo(QGChangeEventListener.Status.RESOLVED_FIXED);
+    try {
+      ChangedIssueImpl.statusOf(new DefaultIssue().setStatus(Issue.STATUS_CLOSED));
+      fail("Expected exception");
+    } catch (Exception e) {
+      assertThat(e).hasMessage("Unexpected status: CLOSED");
+    }
+    try {
+      ChangedIssueImpl.statusOf(new DefaultIssue().setStatus(Issue.STATUS_RESOLVED));
+      fail("Expected exception");
+    } catch (Exception e) {
+      assertThat(e).hasMessage("A resolved issue should have a resolution");
+    }
+    try {
+      ChangedIssueImpl.statusOf(new DefaultIssue().setStatus(Issue.STATUS_RESOLVED).setResolution(Issue.RESOLUTION_REMOVED));
+      fail("Expected exception");
+    } catch (Exception e) {
+      assertThat(e).hasMessage("Unexpected resolution for a resolved issue: REMOVED");
+    }
+  }
+
   private void verifyListenerCalled(QGChangeEventListener listener, QGChangeEvent changeEvent, DefaultIssue... issues) {
     ArgumentCaptor<Set<ChangedIssue>> changedIssuesCaptor = newSetCaptor();
     verify(listener).onIssueChanges(same(changeEvent), changedIssuesCaptor.capture());
     Set<ChangedIssue> changedIssues = changedIssuesCaptor.getValue();
     Tuple[] expected = Arrays.stream(issues)
-      .map(issue -> tuple(issue.key(), issue.status(), issue.type()))
+      .map(issue -> tuple(issue.key(), ChangedIssueImpl.statusOf(issue), issue.type()))
       .toArray(Tuple[]::new);
     assertThat(changedIssues)
       .hasSize(issues.length)
-      .extracting(ChangedIssue::getKey, t -> t.getStatus().name(), ChangedIssue::getType)
+      .extracting(ChangedIssue::getKey, t -> t.getStatus(), ChangedIssue::getType)
       .containsOnly(expected);
   }
 
-  private static final String[] STATUSES = Issue.STATUSES.stream().toArray(String[]::new);
+  private static final String[] POSSIBLE_STATUSES = asList(Issue.STATUS_CONFIRMED, Issue.STATUS_REOPENED, Issue.STATUS_RESOLVED).stream().toArray(String[]::new);
   private static int issueIdCounter = 0;
 
   private static DefaultIssue newDefaultIssue(String projectUuid) {
@@ -245,10 +279,23 @@ public class QGChangeEventListenersImplTest {
     defaultIssue.setKey("issue_" + issueIdCounter++);
     defaultIssue.setProjectUuid(projectUuid);
     defaultIssue.setType(RuleType.values()[new Random().nextInt(RuleType.values().length)]);
-    defaultIssue.setStatus(STATUSES[new Random().nextInt(STATUSES.length)]);
+    defaultIssue.setStatus(POSSIBLE_STATUSES[new Random().nextInt(POSSIBLE_STATUSES.length)]);
+    String[] possibleResolutions = possibleResolutions(defaultIssue.getStatus());
+    if (possibleResolutions.length > 0) {
+      defaultIssue.setResolution(possibleResolutions[new Random().nextInt(possibleResolutions.length)]);
+    }
     return defaultIssue;
   }
 
+  private static String[] possibleResolutions(String status) {
+    switch (status) {
+      case Issue.STATUS_RESOLVED:
+        return new String[] {Issue.RESOLUTION_FALSE_POSITIVE, Issue.RESOLUTION_WONT_FIX};
+      default:
+        return new String[0];
+    }
+  }
+
   private static ComponentDto newComponentDto(String uuid) {
     ComponentDto componentDto = new ComponentDto();
     componentDto.setUuid(uuid);
index b297d9612ff2394795bc2ad392d424fecd9a72fc..49ab397b29cab6498121a52cc524e17dd90096fc 100644 (file)
@@ -456,7 +456,7 @@ public class ListDefinitionsActionTest {
     assertThat(action.isInternal()).isFalse();
     assertThat(action.isPost()).isFalse();
     assertThat(action.responseExampleAsString()).isNotEmpty();
-    assertThat(action.params()).extracting(Param::key).containsExactlyInAnyOrder("component", "branch");
+    assertThat(action.params()).extracting(Param::key).containsExactlyInAnyOrder("component", "branch", "pullRequest");
   }
 
   @Test
index 6f7d06cf48eaa156b80d8eef960c75e27227c7e6..320e37ea8497f3798c7f562a75a22aecdd9d3d93 100644 (file)
@@ -238,7 +238,7 @@ public class ResetActionTest {
     assertThat(action.isInternal()).isFalse();
     assertThat(action.isPost()).isTrue();
     assertThat(action.responseExampleAsString()).isNull();
-    assertThat(action.params()).extracting(Param::key).containsExactlyInAnyOrder("keys", "component", "branch");
+    assertThat(action.params()).extracting(Param::key).containsExactlyInAnyOrder("keys", "component", "branch", "pullRequest");
   }
 
   @Test
index 82b5ca6026c22faab21c16d40d611f1132318e6e..8dee11d1b92953773a5020d9a757034d08d7b93b 100644 (file)
@@ -1015,7 +1015,7 @@ public class SetActionTest {
     assertThat(definition.isInternal()).isFalse();
     assertThat(definition.since()).isEqualTo("6.1");
     assertThat(definition.params()).extracting(Param::key)
-      .containsOnly("key", "value", "values", "fieldValues", "component", "branch");
+      .containsOnly("key", "value", "values", "fieldValues", "component", "branch", "pullRequest");
 
     Param branch = definition.param("branch");
     assertThat(branch.isInternal()).isTrue();
index 5ee088868516ff051b1254df4d555608b0e3a980..b2d1e54a5988b9ca75b2efd7bca8c6509c9f2a0d 100644 (file)
@@ -871,7 +871,7 @@ public class ValuesActionTest {
     assertThat(action.isInternal()).isFalse();
     assertThat(action.isPost()).isFalse();
     assertThat(action.responseExampleAsString()).isNotEmpty();
-    assertThat(action.params()).extracting(WebService.Param::key).containsExactlyInAnyOrder("keys", "component", "branch");
+    assertThat(action.params()).extracting(WebService.Param::key).containsExactlyInAnyOrder("keys", "component", "branch", "pullRequest");
   }
 
   private ValuesWsResponse executeRequestForComponentProperties(ComponentDto componentDto, String... keys) {
index 30e5de83212eb2f53533e5594e2e5f681ea223b1..25cfea083deb8c5937d5dc1b7b7e9a9de1fa4086 100644 (file)
@@ -47,6 +47,7 @@ import static java.lang.String.format;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
 
 public class LinesActionTest {
@@ -64,14 +65,14 @@ public class LinesActionTest {
   @Rule
   public UserSessionRule userSession = UserSessionRule.standalone();
 
-  SourceService sourceService;
-  HtmlSourceDecorator htmlSourceDecorator;
-  ComponentDao componentDao;
+  private SourceService sourceService;
+  private HtmlSourceDecorator htmlSourceDecorator;
+  private ComponentDao componentDao;
 
-  ComponentDto project;
-  ComponentDto file;
+  private ComponentDto project;
+  private ComponentDto file;
 
-  WsTester wsTester;
+  private WsTester wsTester;
 
   @Before
   public void setUp() {
@@ -141,6 +142,26 @@ public class LinesActionTest {
     request.execute().assertJson(getClass(), "show_source.json");
   }
 
+  @Test
+  public void pull_request() throws Exception {
+    ComponentDto project = db.components().insertMainBranch();
+    userSession.addProjectPermission(UserRole.USER, project);
+    ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setBranchType(PULL_REQUEST));
+    ComponentDto file = db.components().insertComponent(newFileDto(branch));
+    db.getDbClient().fileSourceDao().insert(db.getSession(), new FileSourceDto()
+      .setProjectUuid(branch.uuid())
+      .setFileUuid(file.uuid())
+      .setSourceData(FileSourceTesting.newFakeData(3).build()));
+    db.commit();
+    userSession.logIn("login").addProjectPermission(UserRole.CODEVIEWER, project, file);
+
+    WsTester.TestRequest request = wsTester.newGetRequest("api/sources", "lines")
+      .setParam("key", file.getKey())
+      .setParam("pullRequest", file.getPullRequest());
+
+    request.execute().assertJson(getClass(), "show_source.json");
+  }
+
   @Test
   public void fail_when_no_uuid_or_key_param() throws Exception {
     expectedException.expect(IllegalArgumentException.class);
@@ -285,7 +306,7 @@ public class LinesActionTest {
     db.components().insertProjectBranch(project, b -> b.setKey("my_branch"));
 
     expectedException.expect(IllegalArgumentException.class);
-    expectedException.expectMessage("'uuid' and 'branch' parameters cannot be used at the same time");
+    expectedException.expectMessage("Parameter 'uuid' cannot be used at the same time as 'branch' or 'pullRequest'");
 
     wsTester.newGetRequest("api/sources", "lines")
       .setParam("uuid", file.uuid())
index 9518ba177e28648eff33d26c86509ab29fc4893a..bfe7f515dc387adc01f1e197b1be1931cfbc6a6e 100644 (file)
@@ -64,7 +64,7 @@ public class SourcesWsTest {
     assertThat(raw.since()).isEqualTo("5.0");
     assertThat(raw.isInternal()).isFalse();
     assertThat(raw.responseExampleAsString()).isNotEmpty();
-    assertThat(raw.params()).hasSize(2);
+    assertThat(raw.params()).hasSize(3);
 
     WebService.Action lines = controller.action("lines");
     assertThat(lines).isNotNull();
@@ -72,7 +72,7 @@ public class SourcesWsTest {
     assertThat(lines.since()).isEqualTo("5.0");
     assertThat(lines.isInternal()).isTrue();
     assertThat(lines.responseExampleAsString()).isNotEmpty();
-    assertThat(lines.params()).hasSize(5);
+    assertThat(lines.params()).hasSize(6);
 
     WebService.Action hash = controller.action("hash");
     assertThat(hash).isNotNull();
index 59491622a70288203ecf85b385afff2b6febe2e5..d99241c41a9062f458755f380931b0c7d0998903 100644 (file)
@@ -52,9 +52,11 @@ import static org.assertj.core.api.Assertions.tuple;
 import static org.sonar.api.resources.Qualifiers.UNIT_TEST_FILE;
 import static org.sonar.api.web.UserRole.CODEVIEWER;
 import static org.sonar.api.web.UserRole.USER;
+import static org.sonar.db.component.BranchType.PULL_REQUEST;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
 import static org.sonar.db.protobuf.DbFileSources.Test.TestStatus.OK;
 import static org.sonar.server.test.db.TestTesting.newTest;
+import static org.sonar.server.test.ws.ListAction.PARAM_PULL_REQUEST;
 import static org.sonar.server.test.ws.ListAction.SOURCE_FILE_ID;
 import static org.sonar.server.test.ws.ListAction.SOURCE_FILE_KEY;
 import static org.sonar.server.test.ws.ListAction.SOURCE_FILE_LINE_NUMBER;
@@ -99,7 +101,7 @@ public class ListActionTest {
     assertThat(action.isPost()).isFalse();
     assertThat(action.handler()).isNotNull();
     assertThat(action.responseExampleAsString()).isNotEmpty();
-    assertThat(action.params()).hasSize(9);
+    assertThat(action.params()).hasSize(10);
     assertThat(action.description()).isEqualTo("Get the list of tests either in a test file or that test a given line of source code.<br /> " +
       "Requires 'Browse' permission on the file's project.<br /> " +
       "One (and only one) of the following combination of parameters must be provided: " +
@@ -208,6 +210,29 @@ public class ListActionTest {
         tuple(test2.getUuid(), testFile.getKey(), testFile.getBranch()));
   }
 
+  @Test
+  public void list_tests_by_test_file_key_and_pull_request() {
+    ComponentDto project = db.components().insertMainBranch();
+    userSessionRule.addProjectPermission(CODEVIEWER, project);
+    ComponentDto pullRequest = db.components().insertProjectBranch(project, b -> b.setBranchType(PULL_REQUEST));
+    ComponentDto mainFile = db.components().insertComponent(newFileDto(pullRequest));
+    ComponentDto testFile = db.components().insertComponent(newFileDto(pullRequest).setQualifier(UNIT_TEST_FILE));
+
+    DbFileSources.Test test1 = newTest(mainFile, 10).build();
+    DbFileSources.Test test2 = newTest(mainFile, 11).build();
+    insertTests(testFile, test1, test2);
+
+    ListResponse request = call(ws.newRequest()
+      .setParam(TEST_FILE_KEY, testFile.getKey())
+      .setParam(PARAM_PULL_REQUEST, testFile.getPullRequest()));
+
+    assertThat(request.getTestsList())
+      .extracting(Tests.Test::getId, Tests.Test::getFileKey, Tests.Test::getFilePullRequest)
+      .containsOnly(
+        tuple(test1.getUuid(), testFile.getKey(), testFile.getPullRequest()),
+        tuple(test2.getUuid(), testFile.getKey(), testFile.getPullRequest()));
+  }
+
   @Test
   public void list_tests_by_source_file_uuid_and_line_number() {
     userSessionRule.addProjectPermission(CODEVIEWER, project);
index 7415178a93d067e845c34a66afccfa42b76eace4..be8df1f933e495306da660addac83e6a551b9f6b 100644 (file)
@@ -241,14 +241,31 @@ public class WebhookPayloadFactoryImplTest {
     assertJson(payload.getJson())
       .isSimilarTo("{" +
         "\"branch\": {" +
-        "  \"name\": \"feature/foo\"" +
-        "  \"type\": \"SHORT\"" +
+        "  \"name\": \"feature/foo\"," +
+        "  \"type\": \"SHORT\"," +
         "  \"isMain\": false," +
         "  \"url\": \"http://foo/project/issues?branch=feature%2Ffoo&id=P1&resolved=false\"" +
         "}" +
         "}");
   }
 
+  @Test
+  public void create_payload_on_pull_request() {
+    CeTask task = new CeTask("#1", CeTask.Status.SUCCESS);
+    ProjectAnalysis analysis = newAnalysis(task, null, new Branch(false, "pr/foo", Branch.Type.PULL_REQUEST), 1_500_000_000_000L, emptyMap());
+
+    WebhookPayload payload = underTest.create(analysis);
+    assertJson(payload.getJson())
+      .isSimilarTo("{" +
+        "\"branch\": {" +
+        "  \"name\": \"pr/foo\"," +
+        "  \"type\": \"PULL_REQUEST\"," +
+        "  \"isMain\": false," +
+        "  \"url\": \"http://foo/project/issues?pullRequest=pr%2Ffoo&id=P1&resolved=false\"" +
+        "}" +
+        "}");
+  }
+
   @Test
   public void create_without_ce_task() {
     ProjectAnalysis analysis = newAnalysis(null, null, null, null, emptyMap());
index 1cc3bddeef59409347f86eb74dc355540fa18a9d..a9c3088f6fb3058501d6ff44a6862a180fa2f434 100644 (file)
@@ -31,9 +31,14 @@ public class ScannerProperties {
 
   public static final String BRANCHES_DOC_LINK = "https://redirect.sonarsource.com/doc/branches.html";
 
+  public static final String ORGANIZATION = "sonar.organization";
+
   public static final String BRANCH_NAME = "sonar.branch.name";
   public static final String BRANCH_TARGET = "sonar.branch.target";
-  public static final String ORGANIZATION = "sonar.organization";
+
+  public static final String PULL_REQUEST_KEY = "sonar.pullrequest.key";
+  public static final String PULL_REQUEST_BRANCH = "sonar.pullrequest.branch";
+  public static final String PULL_REQUEST_BASE = "sonar.pullrequest.base";
 
   public static final String LINKS_SOURCES_DEV = "sonar.links.scm_dev";
 
@@ -70,8 +75,20 @@ public class ScannerProperties {
       PropertyDefinition.builder(BRANCH_TARGET)
         .name("Optional name of target branch to merge into")
         .description(
-          "Defines what is the target branch of the branch being analyzed. The main branch cannot have a target. "
-            + "If no target is defined for other branches, the main branch is used as a target.")
+          "Defines the target branch of the branch being analyzed. The main branch cannot have a target. "
+            + "If no target is defined, the main branch is used as the target.")
+        .hidden()
+        .build(),
+      PropertyDefinition.builder(PULL_REQUEST_BRANCH)
+        .name("Optional name of pull request")
+        .description("Provide a name for the pull request being analyzed. It might match an existing pull request of the project, otherwise a new pull request will be created.")
+        .hidden()
+        .build(),
+      PropertyDefinition.builder(PULL_REQUEST_BASE)
+        .name("Optional name of target branch to merge into")
+        .description(
+          "Defines the target branch of the pull request being analyzed. "
+            + "If no target is defined, the main branch is used as the target.")
         .hidden()
         .build());
   }
index a3cfb1e2bc5a19bb724d41a352dccff03ec6fbbc..b3054a31a22143ac21403b3c84aa3a93ed43c5c4 100644 (file)
@@ -30,7 +30,7 @@ public class CorePropertyDefinitionsTest {
   @Test
   public void all() {
     List<PropertyDefinition> defs = CorePropertyDefinitions.all();
-    assertThat(defs).hasSize(57);
+    assertThat(defs).hasSize(59);
   }
 
   @Test
index c64ccaf898d7a83f7366248dd135830d6767c6d8..50c5f39e2fe4ecd53ab01e612bd5faedfdc2269b 100644 (file)
@@ -27,7 +27,7 @@ import java.util.Optional;
 public interface Branch {
 
   enum Type {
-    LONG, SHORT
+    LONG, SHORT, PULL_REQUEST
   }
 
   boolean isMain();
index 5d47999757e4fea0db37fe0d9e99d4c1cffea20f..b143361ec41d5e436f520a1590026712753e15c3 100644 (file)
@@ -83,8 +83,8 @@ public class CpdExecutor {
   }
 
   public void execute() {
-    if (branchConfiguration.isShortLivingBranch()) {
-      LOG.info("Skipping CPD calculation for short living branch");
+    if (branchConfiguration.isShortOrPullRequest()) {
+      LOG.info("Skipping CPD calculation for short living branch and pull request");
       return;
     }
     execute(TIMEOUT);
index c1edd6f92f73a2e312d1271cc83c6e6aaa906e9b..c36b8a196e67565ad418d2ed4b4400ebb3f22a44 100644 (file)
@@ -152,7 +152,8 @@ public class ComponentsPublisher implements ReportPublisherStep {
   }
 
   private boolean shouldSkipComponent(DefaultInputComponent component, Collection<InputComponent> children) {
-    if (component instanceof InputModule && children.isEmpty() && branchConfiguration.isShortLivingBranch()) {
+    if (component instanceof InputModule && children.isEmpty()
+      && (branchConfiguration.isShortOrPullRequest())) {
       // no children on a module in short branch analysis -> skip it (except root)
       return !moduleHierarchy.isRoot((InputModule) component);
     } else if (component instanceof InputDir && children.isEmpty()) {
@@ -165,7 +166,7 @@ public class ComponentsPublisher implements ReportPublisherStep {
     } else if (component instanceof DefaultInputFile) {
       // skip files not marked for publishing
       DefaultInputFile inputFile = (DefaultInputFile) component;
-      return !inputFile.isPublished() || (branchConfiguration.isShortLivingBranch() && inputFile.status() == Status.SAME);
+      return !inputFile.isPublished() || (branchConfiguration.isShortOrPullRequest() && inputFile.status() == Status.SAME);
     }
     return false;
   }
index 202d8edae8fa5e8b10b4b72abd8e964ef2fcbb69..a17c691f8bb1d4df85284aac8121b73f6b61bc39 100644 (file)
@@ -91,30 +91,13 @@ public class MetadataPublisher implements ReportPublisherStep {
     settings.get(ORGANIZATION).ifPresent(builder::setOrganizationKey);
 
     if (branchConfiguration.branchName() != null) {
-      builder.setBranchName(branchConfiguration.branchName());
-      builder.setBranchType(toProtobufBranchType(branchConfiguration.branchType()));
-      String branchTarget = branchConfiguration.branchTarget();
-      if (branchTarget != null) {
-        builder.setMergeBranchName(branchTarget);
-      }
+      addBranchInformation(builder);
     }
+
     Optional.ofNullable(rootProject.getBranch()).ifPresent(builder::setDeprecatedBranch);
 
     if (scmConfiguration != null) {
-      ScmProvider scmProvider = scmConfiguration.provider();
-      if (scmProvider != null) {
-        Path projectBasedir = moduleHierarchy.root().getBaseDir();
-        try {
-          builder.setRelativePathFromScmRoot(toSonarQubePath(scmProvider.relativePathFromScmRoot(projectBasedir)));
-        } catch (UnsupportedOperationException e) {
-          LOG.debug(e.getMessage());
-        }
-        try {
-          builder.setScmRevisionId(scmProvider.revisionId(projectBasedir));
-        } catch (UnsupportedOperationException e) {
-          LOG.debug(e.getMessage());
-        }
-      }
+      addScmInformation(builder);
     }
 
     for (QProfile qp : qProfiles.findAll()) {
@@ -132,7 +115,40 @@ public class MetadataPublisher implements ReportPublisherStep {
     writer.writeMetadata(builder.build());
   }
 
+  private void addScmInformation(ScannerReport.Metadata.Builder builder) {
+    ScmProvider scmProvider = scmConfiguration.provider();
+    if (scmProvider != null) {
+      Path projectBasedir = moduleHierarchy.root().getBaseDir();
+      try {
+        builder.setRelativePathFromScmRoot(toSonarQubePath(scmProvider.relativePathFromScmRoot(projectBasedir)));
+      } catch (UnsupportedOperationException e) {
+        LOG.debug(e.getMessage());
+      }
+      try {
+        builder.setScmRevisionId(scmProvider.revisionId(projectBasedir));
+      } catch (UnsupportedOperationException e) {
+        LOG.debug(e.getMessage());
+      }
+    }
+  }
+
+  private void addBranchInformation(ScannerReport.Metadata.Builder builder) {
+    builder.setBranchName(branchConfiguration.branchName());
+    BranchType branchType = toProtobufBranchType(branchConfiguration.branchType());
+    builder.setBranchType(branchType);
+    String branchTarget = branchConfiguration.branchTarget();
+    if (branchTarget != null) {
+      builder.setMergeBranchName(branchTarget);
+    }
+    if (branchType == BranchType.PULL_REQUEST) {
+      builder.setPullRequestKey(branchConfiguration.pullRequestKey());
+    }
+  }
+
   private static BranchType toProtobufBranchType(org.sonar.scanner.scan.branch.BranchType branchType) {
+    if (branchType == org.sonar.scanner.scan.branch.BranchType.PULL_REQUEST) {
+      return BranchType.PULL_REQUEST;
+    }
     if (branchType == org.sonar.scanner.scan.branch.BranchType.LONG) {
       return BranchType.LONG;
     }
index 58066ab035189ae208874fd5c393a072a8b6b8fd..97b01015e2c53dbebb724bef3093555993a1cd91 100644 (file)
@@ -57,6 +57,7 @@ import org.sonarqube.ws.client.WsResponse;
 import static org.sonar.core.config.ScannerProperties.BRANCH_NAME;
 import static org.sonar.core.config.ScannerProperties.ORGANIZATION;
 import static org.sonar.core.util.FileUtils.deleteQuietly;
+import static org.sonar.scanner.scan.branch.BranchType.PULL_REQUEST;
 
 @ScannerSide
 public class ReportPublisher implements Startable {
@@ -180,8 +181,12 @@ public class ReportPublisher implements Startable {
 
     String branchName = branchConfiguration.branchName();
     if (branchName != null) {
-      post.setParam(CHARACTERISTIC, "branch=" + branchName);
-      post.setParam(CHARACTERISTIC, "branchType=" + branchConfiguration.branchType().name());
+      if (branchConfiguration.branchType() != PULL_REQUEST) {
+        post.setParam(CHARACTERISTIC, "branch=" + branchName);
+        post.setParam(CHARACTERISTIC, "branchType=" + branchConfiguration.branchType().name());
+      } else {
+        post.setParam(CHARACTERISTIC, "pullRequest=" + branchConfiguration.pullRequestKey());
+      }
     }
 
     WsResponse response;
index f63b163ae37d3a479f9d6675f1bc7ce86a4b967c..45b9e7f139e612c6e05400b999a3fb20a4e636f1 100644 (file)
@@ -55,7 +55,7 @@ public class TestExecutionAndCoveragePublisher implements ReportPublisherStep {
 
   @Override
   public void publish(ScannerReportWriter writer) {
-    if (branchConfiguration.isShortLivingBranch()) {
+    if (branchConfiguration.isShortOrPullRequest()) {
       return;
     }
     final ScannerReport.Test.Builder testBuilder = ScannerReport.Test.newBuilder();
index 15a5b42076c5c0687c5f59e52abf152b180f1bec..942332568714aa49674021d4365fc213f05904f0 100644 (file)
@@ -23,17 +23,26 @@ import com.google.common.base.Joiner;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.stream.Stream;
 import javax.annotation.Nullable;
-import org.apache.commons.lang.StringUtils;
 import org.sonar.api.batch.AnalysisMode;
 import org.sonar.api.batch.bootstrap.ProjectDefinition;
 import org.sonar.api.batch.bootstrap.ProjectReactor;
 import org.sonar.api.utils.MessageException;
 import org.sonar.core.component.ComponentKeys;
-import org.sonar.core.config.ScannerProperties;
 import org.sonar.scanner.bootstrap.GlobalConfiguration;
 import org.sonar.scanner.scan.branch.BranchParamsValidator;
 
+import static java.lang.String.format;
+import static java.util.Objects.nonNull;
+import static org.apache.commons.lang.StringUtils.isNotEmpty;
+import static org.sonar.core.config.ScannerProperties.BRANCHES_DOC_LINK;
+import static org.sonar.core.config.ScannerProperties.BRANCH_NAME;
+import static org.sonar.core.config.ScannerProperties.BRANCH_TARGET;
+import static org.sonar.core.config.ScannerProperties.PULL_REQUEST_BASE;
+import static org.sonar.core.config.ScannerProperties.PULL_REQUEST_BRANCH;
+import static org.sonar.core.config.ScannerProperties.PULL_REQUEST_KEY;
+
 /**
  * This class aims at validating project reactor
  * @since 3.6
@@ -69,11 +78,11 @@ public class ProjectReactorValidator {
 
     String deprecatedBranchName = reactor.getRoot().getBranch();
 
-    if (branchParamsValidator != null) {
-      // branch plugin is present
+    if (isBranchFeatureAvailable()) {
       branchParamsValidator.validate(validationMessages, deprecatedBranchName);
     } else {
       validateBranchParamsWhenPluginAbsent(validationMessages);
+      validatePullRequestParamsWhenPluginAbsent(validationMessages);
     }
 
     validateBranch(validationMessages, deprecatedBranchName);
@@ -84,38 +93,49 @@ public class ProjectReactorValidator {
   }
 
   private void validateBranchParamsWhenPluginAbsent(List<String> validationMessages) {
-    for (String param : Arrays.asList(ScannerProperties.BRANCH_NAME, ScannerProperties.BRANCH_TARGET)) {
-      if (StringUtils.isNotEmpty(settings.get(param).orElse(null))) {
-        validationMessages.add(String.format("To use the property \"%s\", the branch plugin is required but not installed. "
-          + "See the documentation of branch support: %s.", param, ScannerProperties.BRANCHES_DOC_LINK));
+    for (String param : Arrays.asList(BRANCH_NAME, BRANCH_TARGET)) {
+      if (isNotEmpty(settings.get(param).orElse(null))) {
+        validationMessages.add(format("To use the property \"%s\", the branch plugin is required but not installed. "
+          + "See the documentation of branch support: %s.", param, BRANCHES_DOC_LINK));
       }
     }
   }
 
+  private void validatePullRequestParamsWhenPluginAbsent(List<String> validationMessages) {
+    Stream.of(PULL_REQUEST_KEY, PULL_REQUEST_BRANCH, PULL_REQUEST_BASE)
+      .filter(param -> nonNull(settings.get(param).orElse(null)))
+      .forEach(param -> validationMessages.add(format("To use the property \"%s\", the branch plugin is required but not installed. "
+        + "See the documentation of branch support: %s.", param, BRANCHES_DOC_LINK)));
+  }
+
   private static void validateModuleIssuesMode(ProjectDefinition moduleDef, List<String> validationMessages) {
     if (!ComponentKeys.isValidModuleKeyIssuesMode(moduleDef.getKey())) {
-      validationMessages.add(String.format("\"%s\" is not a valid project or module key. "
+      validationMessages.add(format("\"%s\" is not a valid project or module key. "
         + "Allowed characters in issues mode are alphanumeric, '-', '_', '.', '/' and ':', with at least one non-digit.", moduleDef.getKey()));
     }
   }
 
   private static void validateModule(ProjectDefinition moduleDef, List<String> validationMessages) {
     if (!ComponentKeys.isValidModuleKey(moduleDef.getKey())) {
-      validationMessages.add(String.format("\"%s\" is not a valid project or module key. "
+      validationMessages.add(format("\"%s\" is not a valid project or module key. "
         + "Allowed characters are alphanumeric, '-', '_', '.' and ':', with at least one non-digit.", moduleDef.getKey()));
     }
     String originalVersion = moduleDef.getOriginalVersion();
     if (originalVersion != null && originalVersion.length() > 100) {
-      validationMessages.add(String.format("\"%s\" is not a valid version name for module \"%s\". " +
+      validationMessages.add(format("\"%s\" is not a valid version name for module \"%s\". " +
         "The maximum length for version numbers is 100 characters.", originalVersion, moduleDef.getKey()));
     }
   }
 
   private static void validateBranch(List<String> validationMessages, @Nullable String branch) {
-    if (StringUtils.isNotEmpty(branch) && !ComponentKeys.isValidBranch(branch)) {
-      validationMessages.add(String.format("\"%s\" is not a valid branch name. "
+    if (isNotEmpty(branch) && !ComponentKeys.isValidBranch(branch)) {
+      validationMessages.add(format("\"%s\" is not a valid branch name. "
         + "Allowed characters are alphanumeric, '-', '_', '.' and '/'.", branch));
     }
   }
 
+  private boolean isBranchFeatureAvailable() {
+    return branchParamsValidator != null;
+  }
+
 }
index b57cc8f10b2575deac9bf5baaae32cbebe810aa7..3dd06bbab782a28b334be1e1447879dcf61982d8 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.scanner.scan;
 
 import com.google.common.annotations.VisibleForTesting;
+import javax.annotation.Nullable;
 import org.apache.commons.lang.StringUtils;
 import org.sonar.api.CoreProperties;
 import org.sonar.api.batch.InstantiationStrategy;
@@ -91,6 +92,7 @@ import org.sonar.scanner.scan.branch.BranchConfiguration;
 import org.sonar.scanner.scan.branch.BranchConfigurationProvider;
 import org.sonar.scanner.scan.branch.BranchType;
 import org.sonar.scanner.scan.branch.ProjectBranchesProvider;
+import org.sonar.scanner.scan.branch.ProjectPullRequestsProvider;
 import org.sonar.scanner.scan.filesystem.BatchIdGenerator;
 import org.sonar.scanner.scan.filesystem.InputComponentStoreProvider;
 import org.sonar.scanner.scan.filesystem.StatusDetection;
@@ -146,6 +148,7 @@ public class ProjectScanContainer extends ComponentContainer {
       new RulesProvider(),
       new BranchConfigurationProvider(),
       new ProjectBranchesProvider(),
+      new ProjectPullRequestsProvider(),
       DefaultAnalysisMode.class,
       new ProjectRepositoriesProvider(),
 
@@ -254,7 +257,13 @@ public class ProjectScanContainer extends ComponentContainer {
     String branchName = props.property(ScannerProperties.BRANCH_NAME);
     if (branchName != null) {
       BranchConfiguration branchConfig = getComponentByType(BranchConfiguration.class);
-      LOG.info("Branch name: {}, type: {}", branchName, toDisplayName(branchConfig.branchType()));
+      LOG.info("Branch name: {}, type: {}", branchName, branchTypeToDisplayName(branchConfig.branchType()));
+    }
+
+    String pullRequestBranch = props.property(ScannerProperties.PULL_REQUEST_BRANCH);
+    if (pullRequestBranch != null) {
+      String pullRequestBase = props.property(ScannerProperties.PULL_REQUEST_BASE);
+      LOG.info("Pull request into {}: {}", pullRequestBaseToDisplayName(pullRequestBase), pullRequestBranch);
     }
 
     LOG.debug("Start recursive analysis of project modules");
@@ -265,7 +274,11 @@ public class ProjectScanContainer extends ComponentContainer {
     }
   }
 
-  private static String toDisplayName(BranchType branchType) {
+  private static String pullRequestBaseToDisplayName(@Nullable String pullRequestBase) {
+    return pullRequestBase != null ? pullRequestBase : "default branch";
+  }
+
+  private static String branchTypeToDisplayName(BranchType branchType) {
     switch (branchType) {
       case LONG:
         return "long living";
index 4ddcff98abdabb43c0a6eec42d7d2f2930b138bf..695c3c6d62d8573e91c4dcfe9c13521365e3a196 100644 (file)
@@ -36,8 +36,8 @@ public interface BranchConfiguration {
    */
   BranchType branchType();
 
-  default boolean isShortLivingBranch() {
-    return branchType() == BranchType.SHORT;
+  default boolean isShortOrPullRequest() {
+    return branchType() == BranchType.PULL_REQUEST || branchType() == BranchType.SHORT;
   }
 
   /**
@@ -54,7 +54,19 @@ public interface BranchConfiguration {
 
   /**
    * The name of the base branch to determine project repository and changed files.
+   *
+   * Note: this is important for the scanner during the analysis of long living branches.
+   * For short living branches, branchBase is always the same as branchTarget.
+   * For long living branches, branchBase is the target in case of first analysis,
+   * otherwise it's the branch itself.
    */
   @CheckForNull
   String branchBase();
+
+  /**
+   * The key of the pull request.
+   *
+   * @throws IllegalStateException if this branch configuration is not a pull request.
+   */
+  String pullRequestKey();
 }
index 24b57650ee91a438ff16362a8ad1e60b65b5d3d2..5b845195d05231416284deb2417c009b9936b755 100644 (file)
@@ -27,5 +27,5 @@ import org.sonar.api.batch.ScannerSide;
 @ScannerSide
 @InstantiationStrategy(InstantiationStrategy.PER_BATCH)
 public interface BranchConfigurationLoader {
-  BranchConfiguration load(Map<String, String> localSettings, Supplier<Map<String, String>> remoteSettingsSupplier, ProjectBranches branches);
+  BranchConfiguration load(Map<String, String> localSettings, Supplier<Map<String, String>> remoteSettingsSupplier, ProjectBranches branches, ProjectPullRequests pullRequests);
 }
index 08caee68ca2f8cb6f9cd59baabc671ff9a9cd4c6..276ba411c28dbe6d306489c44af45f2427df508b 100644 (file)
@@ -39,14 +39,14 @@ public class BranchConfigurationProvider extends ProviderAdapter {
   private BranchConfiguration branchConfiguration = null;
 
   public BranchConfiguration provide(@Nullable BranchConfigurationLoader loader, GlobalConfiguration globalConfiguration, ProjectKey projectKey,
-    SettingsLoader settingsLoader, ProjectBranches branches) {
+    SettingsLoader settingsLoader, ProjectBranches branches, ProjectPullRequests pullRequests) {
     if (branchConfiguration == null) {
       if (loader == null) {
         branchConfiguration = new DefaultBranchConfiguration();
       } else {
         Profiler profiler = Profiler.create(LOG).startInfo(LOG_MSG);
         Supplier<Map<String, String>> settingsSupplier = createSettingsSupplier(globalConfiguration, projectKey, settingsLoader);
-        branchConfiguration = loader.load(globalConfiguration.getProperties(), settingsSupplier, branches);
+        branchConfiguration = loader.load(globalConfiguration.getProperties(), settingsSupplier, branches, pullRequests);
         profiler.stopInfo();
       }
     }
index b90f633ded05c12e360919e7cdffbfd9368d836c..e8143ce4480903705c2fd94cdfa854226938cd30 100644 (file)
@@ -20,5 +20,5 @@
 package org.sonar.scanner.scan.branch;
 
 public enum BranchType {
-  SHORT, LONG
+  SHORT, LONG, PULL_REQUEST
 }
index 045c3e3ccd453033a5faca14d9d2444f3d9e9d91..2e048e6c68907133fb6f849481f6e2a50b48c2d4 100644 (file)
@@ -46,4 +46,9 @@ public class DefaultBranchConfiguration implements BranchConfiguration {
   public String branchBase() {
     return null;
   }
+
+  @Override
+  public String pullRequestKey() {
+    throw new IllegalStateException("Only a branch of type PULL_REQUEST can have a pull request id.");
+  }
 }
index 2a59148dbd0a6eec333f1b999b9da10b680e44a7..043360fb64fd9cc1b395672b927996b9f53b29c0 100644 (file)
@@ -26,8 +26,6 @@ import org.sonar.api.batch.bootstrap.ProjectKey;
 import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
 import org.sonar.api.utils.log.Profiler;
-import org.sonar.core.config.ScannerProperties;
-import org.sonar.scanner.bootstrap.GlobalConfiguration;
 
 public class ProjectBranchesProvider extends ProviderAdapter {
 
@@ -36,16 +34,19 @@ public class ProjectBranchesProvider extends ProviderAdapter {
 
   private ProjectBranches branches = null;
 
-  public ProjectBranches provide(@Nullable ProjectBranchesLoader loader, ProjectKey projectKey, GlobalConfiguration settings) {
-    if (branches == null) {
-      if (loader == null || !settings.get(ScannerProperties.BRANCH_NAME).isPresent()) {
-        branches = new ProjectBranches(Collections.emptyList());
-      } else {
-        Profiler profiler = Profiler.create(LOG).startInfo(LOG_MSG);
-        branches = loader.load(projectKey.get());
-        profiler.stopInfo();
-      }
+  public ProjectBranches provide(@Nullable ProjectBranchesLoader loader, ProjectKey projectKey) {
+    if (branches != null) {
+      return branches;
     }
+
+    if (loader == null) {
+      branches = new ProjectBranches(Collections.emptyList());
+      return branches;
+    }
+
+    Profiler profiler = Profiler.create(LOG).startInfo(LOG_MSG);
+    branches = loader.load(projectKey.get());
+    profiler.stopInfo();
     return branches;
   }
 }
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/branch/ProjectPullRequests.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/branch/ProjectPullRequests.java
new file mode 100644 (file)
index 0000000..a9f4c2c
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.scanner.scan.branch;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Container class for information about the pull requests of a project.
+ */
+@Immutable
+public class ProjectPullRequests {
+
+  private final Map<String, PullRequestInfo> pullRequestsById;
+
+  public ProjectPullRequests(List<PullRequestInfo> pullRequestsById) {
+    this.pullRequestsById = pullRequestsById.stream().collect(Collectors.toMap(PullRequestInfo::getBranch, Function.identity()));
+  }
+
+  @CheckForNull
+  public PullRequestInfo get(String branch) {
+    return pullRequestsById.get(branch);
+  }
+
+  public boolean isEmpty() {
+    return pullRequestsById.isEmpty();
+  }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/branch/ProjectPullRequestsLoader.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/branch/ProjectPullRequestsLoader.java
new file mode 100644 (file)
index 0000000..525afd5
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.scanner.scan.branch;
+
+import org.sonar.api.batch.InstantiationStrategy;
+import org.sonar.api.batch.ScannerSide;
+
+@ScannerSide
+@InstantiationStrategy(InstantiationStrategy.PER_BATCH)
+public interface ProjectPullRequestsLoader {
+
+  /**
+   * Load the pull requests of a project.
+   */
+  ProjectPullRequests load(String projectKey);
+
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/branch/ProjectPullRequestsProvider.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/branch/ProjectPullRequestsProvider.java
new file mode 100644 (file)
index 0000000..15023c8
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.scanner.scan.branch;
+
+import java.util.Collections;
+import org.picocontainer.injectors.ProviderAdapter;
+import org.sonar.api.batch.bootstrap.ProjectKey;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+
+public class ProjectPullRequestsProvider extends ProviderAdapter {
+
+  private static final Logger LOG = Loggers.get(ProjectPullRequestsProvider.class);
+  private static final String LOG_MSG = "Load project pull requests";
+
+  private ProjectPullRequests pullRequests = null;
+
+  public ProjectPullRequests provide(@org.picocontainer.annotations.Nullable ProjectPullRequestsLoader loader, ProjectKey projectKey) {
+    if (pullRequests != null) {
+      return pullRequests;
+    }
+
+    if (loader == null) {
+      pullRequests = new ProjectPullRequests(Collections.emptyList());
+      return pullRequests;
+    }
+
+    Profiler profiler = Profiler.create(LOG).startInfo(LOG_MSG);
+    pullRequests = loader.load(projectKey.get());
+    profiler.stopInfo();
+    return pullRequests;
+  }
+}
diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/branch/PullRequestInfo.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/scan/branch/PullRequestInfo.java
new file mode 100644 (file)
index 0000000..6ccc365
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.scanner.scan.branch;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Container class for information about a pull request.
+ */
+@Immutable
+public class PullRequestInfo {
+  private final String id;
+  private final String branch;
+  private final String base;
+
+  public PullRequestInfo(String id, String branch, @Nullable String base) {
+    this.id = id;
+    this.branch = branch;
+    this.base = base;
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  public String getBranch() {
+    return branch;
+  }
+
+  @CheckForNull
+  public String getBase() {
+    return base;
+  }
+}
index 3ea00232847cab646ceda3a9a201eff0fd2b345a..e6d96e517339f8c667f2fb9260246087234c645e 100644 (file)
@@ -80,7 +80,8 @@ public class InputComponentStore {
     return inputFileCache.values().stream()
       .map(f -> (DefaultInputFile) f)
       .filter(DefaultInputFile::isPublished)
-      .filter(f -> (!branchConfiguration.isShortLivingBranch()) || f.status() != Status.SAME)::iterator;
+      .filter(f -> !branchConfiguration.isShortOrPullRequest() || f.status() != Status.SAME)
+      ::iterator;
   }
 
   public Iterable<InputFile> allFiles() {
index 38e2416bfad11a7be9802dfc087f9fed8cf802f7..a04b4f5021a9a4205bf781459eb62ec3bdc85e04 100644 (file)
@@ -62,7 +62,7 @@ public class ScmChangedFilesProvider extends ProviderAdapter {
 
   @CheckForNull
   private static Collection<Path> loadChangedFilesIfNeeded(ScmConfiguration scmConfiguration, BranchConfiguration branchConfiguration, Path rootBaseDir) {
-    if (branchConfiguration.isShortLivingBranch() && branchConfiguration.branchTarget() != null) {
+    if (branchConfiguration.isShortOrPullRequest() && branchConfiguration.branchTarget() != null) {
       ScmProvider scmProvider = scmConfiguration.provider();
       if (scmProvider != null) {
         Profiler profiler = Profiler.create(LOG).startInfo(LOG_MSG);
index cbdafc68bcd9a988b4154c6fe2b62808219c3627..bb587269fd9bd6b71bb2efe70c8bf537717e4705 100644 (file)
@@ -103,7 +103,7 @@ public final class ScmPublisher {
       }
       if (configuration.forceReloadAll() || f.status() != Status.SAME) {
         addIfNotEmpty(filesToBlame, f);
-      } else if (!branchConfiguration.isShortLivingBranch()) {
+      } else if (!branchConfiguration.isShortOrPullRequest()) {
         // File status is SAME so that mean fileData exists
         FileData fileData = projectRepositories.fileData(inputModule.definition().getKeyWithBranch(), inputFile.getModuleRelativePath());
         if (StringUtils.isEmpty(fileData.revision())) {
index 6947f918d60c346e0d2a6cbd3ee24928240ea63d..6972e241500712b4028de5f587a0338208b97511 100644 (file)
@@ -148,7 +148,7 @@ public class DefaultSensorContext implements SensorContext {
 
   @Override
   public NewCoverage newCoverage() {
-    if (branchConfiguration.isShortLivingBranch()) {
+    if (branchConfiguration.isShortOrPullRequest()) {
       return NO_OP_NEW_COVERAGE;
     }
     return new DefaultCoverage(sensorStorage);
@@ -156,7 +156,7 @@ public class DefaultSensorContext implements SensorContext {
 
   @Override
   public NewCpdTokens newCpdTokens() {
-    if (analysisMode.isIssues() || branchConfiguration.isShortLivingBranch()) {
+    if (analysisMode.isIssues() || branchConfiguration.isShortOrPullRequest()) {
       return NO_OP_NEW_CPD_TOKENS;
     }
     return new DefaultCpdTokens(config, sensorStorage);
index 43d9198a06cb8c9eedb1ff0f578f4453bee93e2c..6b5a7137d94a396c295c47a74de9245f07ad88b1 100644 (file)
@@ -355,7 +355,7 @@ public class DefaultSensorStorage implements SensorStorage {
   }
 
   private boolean shouldSkipStorage(DefaultInputFile defaultInputFile) {
-    return branchConfiguration.isShortLivingBranch() && defaultInputFile.status() == InputFile.Status.SAME;
+    return branchConfiguration.isShortOrPullRequest() && defaultInputFile.status() == InputFile.Status.SAME;
   }
 
   /**
index 8605ab5418db40c9a2410eb3184b27c40fa76901..f685e4f22cf545b96e8ee35ba477e61bdc8494c9 100644 (file)
@@ -100,7 +100,18 @@ public class CpdExecutorTest {
 
   @Test
   public void skipIfShortBranch() {
-    when(branchConfig.isShortLivingBranch()).thenReturn(true);
+    when(branchConfig.isShortOrPullRequest()).thenReturn(true);
+    index = mock(SonarCpdBlockIndex.class);
+    executor = new CpdExecutor(settings, index, publisher, componentStore, branchConfig);
+
+    executor.execute();
+
+    verifyZeroInteractions(index);
+  }
+
+  @Test
+  public void skip_if_pull_request() {
+    when(branchConfig.isShortOrPullRequest()).thenReturn(true);
     index = mock(SonarCpdBlockIndex.class);
     executor = new CpdExecutor(settings, index, publisher, componentStore, branchConfig);
 
index 71d86e98de639f09b4aedfe2d6a33974792be811..eb28ba1a3d078bf59ca2bdfb636d23c9a34395b1 100644 (file)
@@ -72,6 +72,7 @@ import org.sonar.scanner.scan.branch.BranchConfiguration;
 import org.sonar.scanner.scan.branch.BranchConfigurationLoader;
 import org.sonar.scanner.scan.branch.BranchType;
 import org.sonar.scanner.scan.branch.ProjectBranches;
+import org.sonar.scanner.scan.branch.ProjectPullRequests;
 import org.sonarqube.ws.Qualityprofiles.SearchWsResponse.QualityProfile;
 import org.sonarqube.ws.Rules.ListResponse.Rule;
 
@@ -420,6 +421,11 @@ public class ScannerMediumTester extends ExternalResource {
     public String branchBase() {
       return branchBase;
     }
+
+    @Override
+    public String pullRequestKey() {
+      throw new UnsupportedOperationException();
+    }
   }
 
   public ScannerMediumTester setBranchType(BranchType branchType) {
@@ -439,7 +445,7 @@ public class ScannerMediumTester extends ExternalResource {
 
   private class FakeBranchConfigurationLoader implements BranchConfigurationLoader {
     @Override
-    public BranchConfiguration load(Map<String, String> localSettings, Supplier<Map<String, String>> settingsSupplier, ProjectBranches branches) {
+    public BranchConfiguration load(Map<String, String> localSettings, Supplier<Map<String, String>> settingsSupplier, ProjectBranches branches, ProjectPullRequests pullRequests) {
       return branchConfiguration;
     }
   }
index eda36629c96a8d51cf4ab63c8287fe80218f84cc..1e84cff44347a9cc1e211b24182c35b542045a30 100644 (file)
@@ -310,7 +310,77 @@ public class ComponentsPublisherTest {
 
   @Test
   public void skip_unchanged_components_in_short_branches() throws IOException {
-    when(branchConfiguration.isShortLivingBranch()).thenReturn(true);
+    when(branchConfiguration.isShortOrPullRequest()).thenReturn(true);
+    ProjectAnalysisInfo projectAnalysisInfo = mock(ProjectAnalysisInfo.class);
+    when(projectAnalysisInfo.analysisDate()).thenReturn(DateUtils.parseDate("2012-12-12"));
+
+    Path moduleBaseDir = temp.newFolder().toPath();
+    ProjectDefinition rootDef = ProjectDefinition.create()
+      .setKey("foo")
+      .setProperty(CoreProperties.PROJECT_VERSION_PROPERTY, "1.0")
+      .setName("Root project")
+      .setDescription("Root description")
+      .setBaseDir(moduleBaseDir.toFile())
+      .setWorkDir(temp.newFolder());
+    DefaultInputModule root = new DefaultInputModule(rootDef, 1);
+
+    moduleHierarchy = mock(InputModuleHierarchy.class);
+    when(moduleHierarchy.root()).thenReturn(root);
+    when(moduleHierarchy.children(root)).thenReturn(Collections.emptyList());
+
+    // dir with changed files
+    DefaultInputDir dir = new DefaultInputDir("module1", "src", 2)
+      .setModuleBaseDir(moduleBaseDir);
+    tree.index(dir, root);
+
+    // dir without changed files or issues
+    DefaultInputDir dir2 = new DefaultInputDir("module1", "src2", 3)
+      .setModuleBaseDir(moduleBaseDir);
+    tree.index(dir2, root);
+
+    // dir without changed files but has issues
+    DefaultInputDir dir3 = new DefaultInputDir("module1", "src3", 4)
+      .setModuleBaseDir(moduleBaseDir);
+    tree.index(dir3, root);
+    writeIssue(4);
+
+    DefaultInputFile file = new TestInputFileBuilder("module1", "src/Foo.java", 5)
+      .setLines(2)
+      .setPublish(true)
+      .setStatus(InputFile.Status.ADDED)
+      .build();
+    tree.index(file, dir);
+
+    DefaultInputFile file2 = new TestInputFileBuilder("module1", "src2/Foo2.java", 6)
+      .setPublish(true)
+      .setStatus(InputFile.Status.SAME)
+      .setLines(2)
+      .build();
+    tree.index(file2, dir2);
+
+    DefaultInputFile file3 = new TestInputFileBuilder("module1", "src3/Foo3.java", 7)
+      .setPublish(true)
+      .setStatus(InputFile.Status.SAME)
+      .setLines(2)
+      .build();
+    tree.index(file3, dir3);
+
+    ComponentsPublisher publisher = new ComponentsPublisher(moduleHierarchy, tree, branchConfiguration);
+    publisher.publish(writer);
+
+    assertThat(writer.hasComponentData(FileStructure.Domain.COMPONENT, 1)).isTrue();
+    assertThat(writer.hasComponentData(FileStructure.Domain.COMPONENT, 2)).isTrue();
+    assertThat(writer.hasComponentData(FileStructure.Domain.COMPONENT, 4)).isTrue();
+    assertThat(writer.hasComponentData(FileStructure.Domain.COMPONENT, 5)).isTrue();
+
+    assertThat(writer.hasComponentData(FileStructure.Domain.COMPONENT, 3)).isFalse();
+    assertThat(writer.hasComponentData(FileStructure.Domain.COMPONENT, 6)).isFalse();
+    assertThat(writer.hasComponentData(FileStructure.Domain.COMPONENT, 7)).isFalse();
+  }
+
+  @Test
+  public void skip_unchanged_components_in_pull_requests() throws IOException {
+    when(branchConfiguration.isShortOrPullRequest()).thenReturn(true);
     ProjectAnalysisInfo projectAnalysisInfo = mock(ProjectAnalysisInfo.class);
     when(projectAnalysisInfo.analysisDate()).thenReturn(DateUtils.parseDate("2012-12-12"));
 
index 128561c45bf61da91368917d952375e391310555..b25f773f536ca21e9eecba58351c5f6ec5a86a46 100644 (file)
@@ -59,6 +59,7 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
+import static org.sonar.scanner.scan.branch.BranchType.PULL_REQUEST;
 import static org.sonar.scanner.scan.branch.BranchType.SHORT;
 
 public class ReportPublisherTest {
@@ -267,4 +268,42 @@ public class ReportPublisherTest {
       .containsExactlyInAnyOrder("branch=" + branchName, "branchType=" + SHORT.name());
   }
 
+  @Test
+  public void send_pull_request_characteristic() throws Exception {
+    ReportPublisher underTest = new ReportPublisher(settings.asConfig(), wsClient, server, contextPublisher, moduleHierarchy, mode, mock(TempFolder.class),
+      new ReportPublisherStep[0], branchConfiguration);
+
+    String orgName = "MyOrg";
+    settings.setProperty(ScannerProperties.ORGANIZATION, orgName);
+
+    String branchName = "feature";
+    String pullRequestId = "pr-123";
+    when(branchConfiguration.branchName()).thenReturn(branchName);
+    when(branchConfiguration.branchType()).thenReturn(PULL_REQUEST);
+    when(branchConfiguration.pullRequestKey()).thenReturn(pullRequestId);
+
+    WsResponse response = mock(WsResponse.class);
+
+    PipedOutputStream out = new PipedOutputStream();
+    PipedInputStream in = new PipedInputStream(out);
+    Ce.SubmitResponse.newBuilder().build().writeTo(out);
+    out.close();
+
+    when(response.failIfNotSuccessful()).thenReturn(response);
+    when(response.contentStream()).thenReturn(in);
+
+    when(wsClient.call(any(WsRequest.class))).thenReturn(response);
+    underTest.upload(temp.newFile());
+
+    ArgumentCaptor<WsRequest> capture = ArgumentCaptor.forClass(WsRequest.class);
+    verify(wsClient).call(capture.capture());
+
+    WsRequest wsRequest = capture.getValue();
+    assertThat(wsRequest.getParameters().getKeys()).hasSize(3);
+    assertThat(wsRequest.getParameters().getValues("organization")).containsExactly(orgName);
+    assertThat(wsRequest.getParameters().getValues("projectKey")).containsExactly("struts");
+    assertThat(wsRequest.getParameters().getValues("characteristic"))
+      .containsExactlyInAnyOrder("pullRequest=" + pullRequestId);
+  }
+
 }
index 7b35a9af5419a5bd4e32fa2890d40464abe30c78..a1175ee2332e02540da193bd86636340de944b10 100644 (file)
@@ -40,7 +40,21 @@ public class TestExecutionAndCoveragePublisherTest {
   @Test
   public void do_nothing_for_short_living_branches() throws IOException {
     BranchConfiguration branchConfiguration = mock(BranchConfiguration.class);
-    when(branchConfiguration.isShortLivingBranch()).thenReturn(true);
+    when(branchConfiguration.isShortOrPullRequest()).thenReturn(true);
+    InputComponentStore componentStore = mock(InputComponentStore.class);
+    TestExecutionAndCoveragePublisher publisher = new TestExecutionAndCoveragePublisher(componentStore, null, branchConfiguration);
+    File outputDir = temp.newFolder();
+    ScannerReportWriter writer = new ScannerReportWriter(outputDir);
+
+    publisher.publish(writer);
+
+    verifyZeroInteractions(componentStore);
+  }
+
+  @Test
+  public void do_nothing_for_pull_requests() throws IOException {
+    BranchConfiguration branchConfiguration = mock(BranchConfiguration.class);
+    when(branchConfiguration.isShortOrPullRequest()).thenReturn(true);
     InputComponentStore componentStore = mock(InputComponentStore.class);
     TestExecutionAndCoveragePublisher publisher = new TestExecutionAndCoveragePublisher(componentStore, null, branchConfiguration);
     File outputDir = temp.newFolder();
index e71a71f161b437bd95e8421014b068da304c7094..0c0799c9ab311cc6569644ea5569ebefaf45da11 100644 (file)
@@ -190,6 +190,45 @@ public class ProjectReactorValidatorTest {
     validator.validate(reactor);
   }
 
+  @Test
+  public void fail_when_pull_request_id_specified_but_branch_plugin_not_present() {
+    ProjectDefinition def = ProjectDefinition.create().setProperty(CoreProperties.PROJECT_KEY_PROPERTY, "foo");
+    ProjectReactor reactor = new ProjectReactor(def);
+
+    when(settings.get(eq(ScannerProperties.PULL_REQUEST_KEY))).thenReturn(Optional.of("#1984"));
+
+    thrown.expect(MessageException.class);
+    thrown.expectMessage("the branch plugin is required but not installed");
+
+    validator.validate(reactor);
+  }
+
+  @Test
+  public void fail_when_pull_request_branch_is_specified_but_branch_plugin_not_present() {
+    ProjectDefinition def = ProjectDefinition.create().setProperty(CoreProperties.PROJECT_KEY_PROPERTY, "foo");
+    ProjectReactor reactor = new ProjectReactor(def);
+
+    when(settings.get(eq(ScannerProperties.PULL_REQUEST_BRANCH))).thenReturn(Optional.of("feature1"));
+
+    thrown.expect(MessageException.class);
+    thrown.expectMessage("the branch plugin is required but not installed");
+
+    validator.validate(reactor);
+  }
+
+  @Test
+  public void fail_when_pull_request_base_specified_but_branch_plugin_not_present() {
+    ProjectDefinition def = ProjectDefinition.create().setProperty(CoreProperties.PROJECT_KEY_PROPERTY, "foo");
+    ProjectReactor reactor = new ProjectReactor(def);
+
+    when(settings.get(eq(ScannerProperties.PULL_REQUEST_BASE))).thenReturn(Optional.of("feature1"));
+
+    thrown.expect(MessageException.class);
+    thrown.expectMessage("the branch plugin is required but not installed");
+
+    validator.validate(reactor);
+  }
+
   @Test
   public void not_fail_with_valid_version() {
     validator.validate(createProjectReactor("foo", def -> def.setVersion("1.0")));
index 067114dd43b24862404a60a7a43ea3c29ba45fd1..a8fbc75cc8bae444a6091c9b6477591d05ba1b75 100644 (file)
@@ -41,6 +41,7 @@ public class BranchConfigurationProviderTest {
   private BranchConfigurationLoader loader;
   private BranchConfiguration config;
   private ProjectBranches branches;
+  private ProjectPullRequests pullRequests;
   private ProjectKey projectKey;
   private Map<String, String> globalPropertiesMap;
   private Map<String, String> remoteProjectSettings;
@@ -52,6 +53,7 @@ public class BranchConfigurationProviderTest {
     loader = mock(BranchConfigurationLoader.class);
     config = mock(BranchConfiguration.class);
     branches = mock(ProjectBranches.class);
+    pullRequests = mock(ProjectPullRequests.class);
     settingsLoader = mock(SettingsLoader.class);
     projectKey = mock(ProjectKey.class);
     globalPropertiesMap = new HashMap<>();
@@ -61,22 +63,24 @@ public class BranchConfigurationProviderTest {
 
   @Test
   public void should_cache_config() {
-    BranchConfiguration configuration = provider.provide(null, globalConfiguration, projectKey, settingsLoader, branches);
-    assertThat(provider.provide(null, globalConfiguration, projectKey, settingsLoader, branches)).isSameAs(configuration);
+    BranchConfiguration configuration = provider.provide(null, globalConfiguration, projectKey, settingsLoader, branches, pullRequests);
+    assertThat(provider.provide(null, globalConfiguration, projectKey, settingsLoader, branches, pullRequests)).isSameAs(configuration);
   }
 
   @Test
   public void should_use_loader() {
-    when(loader.load(eq(globalPropertiesMap), any(Supplier.class), eq(branches))).thenReturn(config);
-    BranchConfiguration branchConfig = provider.provide(loader, globalConfiguration, projectKey, settingsLoader, branches);
+    when(loader.load(eq(globalPropertiesMap), any(Supplier.class), eq(branches), eq(pullRequests))).thenReturn(config);
 
-    assertThat(branchConfig).isSameAs(config);
+    BranchConfiguration result = provider.provide(loader, globalConfiguration, projectKey, settingsLoader, branches, pullRequests);
+
+    assertThat(result).isSameAs(config);
   }
 
   @Test
   public void should_return_default_if_no_loader() {
-    BranchConfiguration configuration = provider.provide(null, globalConfiguration, projectKey, settingsLoader, branches);
-    assertThat(configuration.branchTarget()).isNull();
-    assertThat(configuration.branchType()).isEqualTo(BranchType.LONG);
+    BranchConfiguration result = provider.provide(null, globalConfiguration, projectKey, settingsLoader, branches, pullRequests);
+
+    assertThat(result.branchTarget()).isNull();
+    assertThat(result.branchType()).isEqualTo(BranchType.LONG);
   }
 }
index 6b75d766e3e45127b8e573cda6e14a62b72477ec..fd22c4d96e8784422afc784acf9d2f15f054d676 100644 (file)
  */
 package org.sonar.scanner.scan.branch;
 
-import java.util.Optional;
 import org.junit.Before;
 import org.junit.Test;
-import org.sonar.api.config.Configuration;
-import org.sonar.scanner.bootstrap.GlobalConfiguration;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.anyString;
@@ -34,33 +31,30 @@ public class ProjectBranchesProviderTest {
   private ProjectBranchesProvider provider = new ProjectBranchesProvider();
   private ProjectBranchesLoader mockLoader;
   private ProjectBranches mockBranches;
-  private GlobalConfiguration mockSettings;
 
   @Before
   public void setUp() {
     mockLoader = mock(ProjectBranchesLoader.class);
     mockBranches = mock(ProjectBranches.class);
-    mockSettings = mock(GlobalConfiguration.class);
   }
 
   @Test
   public void should_cache_branches() {
-    ProjectBranches branches = provider.provide(null, () -> "project", mockSettings);
-    assertThat(provider.provide(null, () -> "project", mockSettings)).isSameAs(branches);
+    ProjectBranches branches = provider.provide(null, () -> "project");
+    assertThat(provider.provide(null, () -> "project")).isSameAs(branches);
   }
 
   @Test
   public void should_use_loader() {
     when(mockLoader.load("key")).thenReturn(mockBranches);
-    when(mockSettings.get(anyString())).thenReturn(Optional.of("somebranch"));
-    ProjectBranches branches = provider.provide(mockLoader, () -> "key", mockSettings);
+    ProjectBranches branches = provider.provide(mockLoader, () -> "key");
 
     assertThat(branches).isSameAs(mockBranches);
   }
 
   @Test
   public void should_return_default_if_no_loader() {
-    ProjectBranches branches = provider.provide(null, () -> "project", mockSettings);
+    ProjectBranches branches = provider.provide(null, () -> "project");
     assertThat(branches.isEmpty()).isTrue();
   }
 }
diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/branch/ProjectPullRequestsProviderTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/scan/branch/ProjectPullRequestsProviderTest.java
new file mode 100644 (file)
index 0000000..b3c1ab8
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.scanner.scan.branch;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static java.util.Collections.emptyList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ProjectPullRequestsProviderTest {
+  private ProjectPullRequestsProvider provider = new ProjectPullRequestsProvider();
+  private ProjectPullRequestsLoader mockLoader;
+  private ProjectPullRequests pullRequests;
+
+  @Before
+  public void setUp() {
+    mockLoader = mock(ProjectPullRequestsLoader.class);
+    pullRequests = new ProjectPullRequests(emptyList());
+  }
+
+  @Test
+  public void cache_pull_requests() {
+    ProjectPullRequests pullRequests = provider.provide(null, () -> "project");
+
+    assertThat(provider.provide(null, () -> "project")).isSameAs(pullRequests);
+  }
+
+  @Test
+  public void should_use_loader() {
+    when(mockLoader.load("key")).thenReturn(pullRequests);
+
+    ProjectPullRequests result = provider.provide(mockLoader, () -> "key");
+
+    assertThat(result).isSameAs(pullRequests);
+  }
+
+  @Test
+  public void should_return_default_if_no_loader() {
+    ProjectPullRequests result = provider.provide(null, () -> "project");
+
+    assertThat(result.isEmpty()).isTrue();
+  }
+}
index 83db811b6b95db5a17b4230f38aec133e7a3b708..bc212aa9489549390a573d8d8d71ad6bb700f449 100644 (file)
@@ -66,7 +66,7 @@ public class ScmChangedFilesProviderTest {
 
   @Test
   public void testNoScmProvider() {
-    when(branchConfiguration.isShortLivingBranch()).thenReturn(true);
+    when(branchConfiguration.isShortOrPullRequest()).thenReturn(true);
     when(branchConfiguration.branchTarget()).thenReturn("target");
 
     ScmChangedFiles scmChangedFiles = provider.provide(scmConfiguration, branchConfiguration, inputModuleHierarchy);
@@ -78,7 +78,7 @@ public class ScmChangedFilesProviderTest {
   @Test
   public void testFailIfRelativePath() {
     when(branchConfiguration.branchTarget()).thenReturn("target");
-    when(branchConfiguration.isShortLivingBranch()).thenReturn(true);
+    when(branchConfiguration.isShortOrPullRequest()).thenReturn(true);
     when(scmConfiguration.provider()).thenReturn(scmProvider);
     when(scmProvider.branchChangedFiles("target", rootBaseDir)).thenReturn(Collections.singleton(Paths.get("changedFile")));
 
@@ -90,7 +90,7 @@ public class ScmChangedFilesProviderTest {
   @Test
   public void testProviderDoesntSupport() {
     when(branchConfiguration.branchTarget()).thenReturn("target");
-    when(branchConfiguration.isShortLivingBranch()).thenReturn(true);
+    when(branchConfiguration.isShortOrPullRequest()).thenReturn(true);
     when(scmConfiguration.provider()).thenReturn(scmProvider);
     when(scmProvider.branchChangedFiles("target", rootBaseDir)).thenReturn(null);
     ScmChangedFiles scmChangedFiles = provider.provide(scmConfiguration, branchConfiguration, inputModuleHierarchy);
@@ -101,7 +101,7 @@ public class ScmChangedFilesProviderTest {
 
   @Test
   public void testNoOpInNonShortLivedBranch() {
-    when(branchConfiguration.isShortLivingBranch()).thenReturn(false);
+    when(branchConfiguration.isShortOrPullRequest()).thenReturn(false);
     ScmChangedFiles scmChangedFiles = provider.provide(scmConfiguration, branchConfiguration, inputModuleHierarchy);
 
     assertThat(scmChangedFiles.get()).isNull();
@@ -118,7 +118,7 @@ public class ScmChangedFilesProviderTest {
     };
 
     when(scmConfiguration.provider()).thenReturn(legacy);
-    when(branchConfiguration.isShortLivingBranch()).thenReturn(true);
+    when(branchConfiguration.isShortOrPullRequest()).thenReturn(true);
     when(branchConfiguration.branchTarget()).thenReturn("target");
 
     ScmChangedFiles scmChangedFiles = provider.provide(scmConfiguration, branchConfiguration, inputModuleHierarchy);
@@ -130,7 +130,7 @@ public class ScmChangedFilesProviderTest {
   @Test
   public void testReturnChangedFiles() {
     when(branchConfiguration.branchTarget()).thenReturn("target");
-    when(branchConfiguration.isShortLivingBranch()).thenReturn(true);
+    when(branchConfiguration.isShortOrPullRequest()).thenReturn(true);
     when(scmConfiguration.provider()).thenReturn(scmProvider);
     when(scmProvider.branchChangedFiles("target", rootBaseDir)).thenReturn(Collections.singleton(Paths.get("changedFile").toAbsolutePath()));
     ScmChangedFiles scmChangedFiles = provider.provide(scmConfiguration, branchConfiguration, inputModuleHierarchy);
@@ -143,7 +143,7 @@ public class ScmChangedFilesProviderTest {
   public void testCacheObject() {
     provider.provide(scmConfiguration, branchConfiguration, inputModuleHierarchy);
     provider.provide(scmConfiguration, branchConfiguration, inputModuleHierarchy);
-    verify(branchConfiguration).isShortLivingBranch();
+    verify(branchConfiguration).isShortOrPullRequest();
   }
 
 }
index d09769ee3a8834b1d6d98ada66763a3b26c150c5..68771fe50598aa88cb8ae09db8a2ede3cb22e9f1 100644 (file)
@@ -91,7 +91,14 @@ public class DefaultSensorContextTest {
 
   @Test
   public void shouldSkipDupsAndCoverageOnShortBranches() {
-    when(branchConfig.isShortLivingBranch()).thenReturn(true);
+    when(branchConfig.isShortOrPullRequest()).thenReturn(true);
+    assertThat(adaptor.newCpdTokens()).isEqualTo(DefaultSensorContext.NO_OP_NEW_CPD_TOKENS);
+    assertThat(adaptor.newCoverage()).isEqualTo(DefaultSensorContext.NO_OP_NEW_COVERAGE);
+  }
+
+  @Test
+  public void shouldSkipDupsAndCoverageOnPullRequests() {
+    when(branchConfig.isShortOrPullRequest()).thenReturn(true);
     assertThat(adaptor.newCpdTokens()).isEqualTo(DefaultSensorContext.NO_OP_NEW_CPD_TOKENS);
     assertThat(adaptor.newCoverage()).isEqualTo(DefaultSensorContext.NO_OP_NEW_COVERAGE);
   }
index 52da53edbf5ac595cd561a3215b0c2c418994d4c..843e4730b450616f9b558547529e7ba97f9f97bb 100644 (file)
@@ -127,7 +127,7 @@ public class DefaultSensorStorageTest {
   @Test
   public void should_skip_issue_on_short_branch_when_file_status_is_SAME() {
     InputFile file = new TestInputFileBuilder("foo", "src/Foo.php").setStatus(InputFile.Status.SAME).build();
-    when(branchConfiguration.isShortLivingBranch()).thenReturn(true);
+    when(branchConfiguration.isShortOrPullRequest()).thenReturn(true);
 
     DefaultIssue issue = new DefaultIssue().at(new DefaultIssueLocation().on(file));
     underTest.store(issue);
@@ -151,7 +151,7 @@ public class DefaultSensorStorageTest {
     DefaultInputFile file = new TestInputFileBuilder("foo", "src/Foo.php")
       .setContents("// comment")
       .setStatus(InputFile.Status.SAME).build();
-    when(branchConfiguration.isShortLivingBranch()).thenReturn(true);
+    when(branchConfiguration.isShortOrPullRequest()).thenReturn(true);
 
     DefaultHighlighting highlighting = new DefaultHighlighting(underTest).onFile(file).highlight(0, 1, TypeOfText.KEYWORD);
     underTest.store(highlighting);
@@ -178,7 +178,20 @@ public class DefaultSensorStorageTest {
   @Test
   public void should_skip_file_measure_on_short_branch_when_file_status_is_SAME() {
     InputFile file = new TestInputFileBuilder("foo", "src/Foo.php").setStatus(InputFile.Status.SAME).build();
-    when(branchConfiguration.isShortLivingBranch()).thenReturn(true);
+    when(branchConfiguration.isShortOrPullRequest()).thenReturn(true);
+
+    underTest.store(new DefaultMeasure()
+      .on(file)
+      .forMetric(CoreMetrics.NCLOC)
+      .withValue(10));
+
+    verifyZeroInteractions(measureCache);
+  }
+
+  @Test
+  public void should_skip_file_measure_on_pull_request_when_file_status_is_SAME() {
+    InputFile file = new TestInputFileBuilder("foo", "src/Foo.php").setStatus(InputFile.Status.SAME).build();
+    when(branchConfiguration.isShortOrPullRequest()).thenReturn(true);
 
     underTest.store(new DefaultMeasure()
       .on(file)
index 7d3123d24c12a167b19dd94931195cf5c0276e98..c99b9c9882e796809709c3ae19e8ccb206ea5798 100644 (file)
@@ -46,6 +46,8 @@ message Metadata {
   string relative_path_from_scm_root = 12;
   string scm_revision_id = 13;
 
+  string pull_request_key = 14;
+
   message QProfile {
     string key = 1;
     string name = 2;
@@ -62,6 +64,7 @@ message Metadata {
     UNSET = 0;
     LONG = 1;
     SHORT = 2;
+    PULL_REQUEST = 3;
   }
 }
 
index 3c5e64ec06abd2e8598b0c79d66f6c806ff7ce09..bdbbbf90c62408a6c320365dba233b65ec06e2da 100644 (file)
@@ -45,8 +45,9 @@ import org.sonarqube.ws.client.profiles.ProfilesService;
 import org.sonarqube.ws.client.projectanalyses.ProjectAnalysesService;
 import org.sonarqube.ws.client.projectbranches.ProjectBranchesService;
 import org.sonarqube.ws.client.projectlinks.ProjectLinksService;
-import org.sonarqube.ws.client.projecttags.ProjectTagsService;
+import org.sonarqube.ws.client.projectpullrequests.ProjectPullRequestsService;
 import org.sonarqube.ws.client.projects.ProjectsService;
+import org.sonarqube.ws.client.projecttags.ProjectTagsService;
 import org.sonarqube.ws.client.properties.PropertiesService;
 import org.sonarqube.ws.client.qualitygates.QualitygatesService;
 import org.sonarqube.ws.client.qualityprofiles.QualityprofilesService;
@@ -102,6 +103,7 @@ class DefaultWsClient implements WsClient {
   private final ProjectAnalysesService projectAnalysesService;
   private final ProjectBranchesService projectBranchesService;
   private final ProjectLinksService projectLinksService;
+  private final ProjectPullRequestsService projectPullRequestsService;
   private final ProjectTagsService projectTagsService;
   private final ProjectsService projectsService;
   private final PropertiesService propertiesService;
@@ -152,6 +154,7 @@ class DefaultWsClient implements WsClient {
     this.projectAnalysesService = new ProjectAnalysesService(wsConnector);
     this.projectBranchesService = new ProjectBranchesService(wsConnector);
     this.projectLinksService = new ProjectLinksService(wsConnector);
+    this.projectPullRequestsService = new ProjectPullRequestsService(wsConnector);
     this.projectTagsService = new ProjectTagsService(wsConnector);
     this.projectsService = new ProjectsService(wsConnector);
     this.propertiesService = new PropertiesService(wsConnector);
@@ -302,6 +305,11 @@ class DefaultWsClient implements WsClient {
     return projectLinksService;
   }
 
+  @Override
+  public ProjectPullRequestsService projectPullRequests() {
+    return projectPullRequestsService;
+  }
+
   @Override
   public ProjectTagsService projectTags() {
     return projectTagsService;
index 570b96cef4731d585c74209564f32c3b57851745..79ca15098c2133a5bcc8b6811345bdea31f2cd9f 100644 (file)
@@ -45,6 +45,7 @@ import org.sonarqube.ws.client.profiles.ProfilesService;
 import org.sonarqube.ws.client.projectanalyses.ProjectAnalysesService;
 import org.sonarqube.ws.client.projectbranches.ProjectBranchesService;
 import org.sonarqube.ws.client.projectlinks.ProjectLinksService;
+import org.sonarqube.ws.client.projectpullrequests.ProjectPullRequestsService;
 import org.sonarqube.ws.client.projecttags.ProjectTagsService;
 import org.sonarqube.ws.client.projects.ProjectsService;
 import org.sonarqube.ws.client.properties.PropertiesService;
@@ -139,6 +140,8 @@ public interface WsClient {
 
   ProjectLinksService projectLinks();
 
+  ProjectPullRequestsService projectPullRequests();
+
   ProjectTagsService projectTags();
 
   ProjectsService projects();
index 5e726b0eafbced9c7a599131906255938f577e69..f6fdf792115c5335bcfa3f9c063b616f285e7da8 100644 (file)
@@ -39,6 +39,7 @@ public class ComponentsWsParameters {
   public static final String PARAM_COMPONENT_ID = "componentId";
   public static final String PARAM_COMPONENT = "component";
   public static final String PARAM_BRANCH = "branch";
+  public static final String PARAM_PULL_REQUEST = "pullRequest";
 
   private ComponentsWsParameters() {
     // static utility class
index 772651aa09052da9afa85e174ddd86ee3c43c7d0..14441f113b9605bd03b953f4b58531f365e991a3 100644 (file)
@@ -34,6 +34,7 @@ public class AppRequest {
   private String branch;
   private String component;
   private String componentId;
+  private String pullRequest;
 
   /**
    * This is part of the internal API.
@@ -73,4 +74,17 @@ public class AppRequest {
   public String getComponentId() {
     return componentId;
   }
+
+  /**
+   * This is part of the internal API.
+   * Example value: "5461"
+   */
+  public AppRequest setPullRequest(String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
+
+  public String getPullRequest() {
+    return pullRequest;
+  }
 }
index 483474301b13ce204a1de0858f245a24939faa27..bc8d556b033b71e0abdf49269b1bb7f5d2fb17ee 100644 (file)
@@ -55,6 +55,7 @@ public class ComponentsService extends BaseService {
         .setParam("branch", request.getBranch())
         .setParam("component", request.getComponent())
         .setParam("componentId", request.getComponentId())
+        .setParam("pullRequest", request.getPullRequest())
         .setMediaType(MediaTypes.JSON)
       ).content();
   }
@@ -111,7 +112,8 @@ public class ComponentsService extends BaseService {
       new GetRequest(path("show"))
         .setParam("branch", request.getBranch())
         .setParam("component", request.getComponent())
-        .setParam("componentId", request.getComponentId()),
+        .setParam("componentId", request.getComponentId())
+        .setParam("pullRequest", request.getPullRequest()),
       ShowWsResponse.parser());
   }
 
@@ -147,6 +149,7 @@ public class ComponentsService extends BaseService {
         .setParam("componentId", request.getComponentId())
         .setParam("p", request.getP())
         .setParam("ps", request.getPs())
+        .setParam("pullRequest", request.getPullRequest())
         .setParam("q", request.getQ())
         .setParam("qualifiers", request.getQualifiers() == null ? null : request.getQualifiers().stream().collect(Collectors.joining(",")))
         .setParam("s", request.getS() == null ? null : request.getS().stream().collect(Collectors.joining(",")))
index b63065455988afc6da20204fd8de75f846a6020d..4a04cd56d1bda13c2429ff3896c9f22de3771985 100644 (file)
@@ -34,6 +34,7 @@ public class ShowRequest {
   private String branch;
   private String component;
   private String componentId;
+  private String pullRequest;
 
   /**
    * This is part of the internal API.
@@ -73,4 +74,17 @@ public class ShowRequest {
   public String getComponentId() {
     return componentId;
   }
+
+  /**
+   * This is part of the internal API.
+   * Example value: "5461"
+   */
+  public ShowRequest setPullRequest(String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
+
+  public String getPullRequest() {
+    return pullRequest;
+  }
 }
index 4aae16c2d9cb14514873e1e1316ebc7f2daeb5c1..82ae76cb9a638a572f832f9d494c22ae7c7016fd 100644 (file)
@@ -37,6 +37,7 @@ public class TreeRequest {
   private String componentId;
   private String p;
   private String ps;
+  private String pullRequest;
   private String q;
   private List<String> qualifiers;
   private List<String> s;
@@ -123,6 +124,19 @@ public class TreeRequest {
     return ps;
   }
 
+  /**
+   * This is part of the internal API.
+   * Example value: "5461"
+   */
+  public TreeRequest setPullRequest(String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
+
+  public String getPullRequest() {
+    return pullRequest;
+  }
+
   /**
    * Example value: "FILE_NAM"
    */
index de57a61729dac2fef7404a2453fceb8425d6f318..dc68920fc3d5b02dff88ee101dec03eb5a24c9b6 100644 (file)
@@ -50,6 +50,7 @@ public class DuplicationsService extends BaseService {
       new GetRequest(path("show"))
         .setParam("branch", request.getBranch())
         .setParam("key", request.getKey())
+        .setParam("pullRequest", request.getPullRequest())
         .setParam("uuid", request.getUuid()),
       ShowResponse.parser());
   }
index 0ee02965ce5b8095b567c16f8f7b4d5d6b578da4..333618abb66b35de1789f2dd2e3c0b3196a321ef 100644 (file)
@@ -33,6 +33,7 @@ public class ShowRequest {
 
   private String branch;
   private String key;
+  private String pullRequest;
   private String uuid;
 
   /**
@@ -60,6 +61,19 @@ public class ShowRequest {
     return key;
   }
 
+  /**
+   * This is part of the internal API.
+   * Example value: "5461"
+   */
+  public ShowRequest setPullRequest(String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
+
+  public String getPullRequest() {
+    return pullRequest;
+  }
+
   /**
    * Example value: "584a89f2-8037-4f7b-b82c-8b45d2d63fb2"
    * @deprecated since 6.5
index 3dbaa54c9fd71104e84e1f72995d34be05a63c65..0032e024f4d5d7a0dd44d418c0847cecf1376a6e 100644 (file)
@@ -70,6 +70,7 @@ public class IssuesWsParameters {
   public static final String PARAM_FILE_UUIDS = "fileUuids";
   public static final String PARAM_ON_COMPONENT_ONLY = "onComponentOnly";
   public static final String PARAM_BRANCH = "branch";
+  public static final String PARAM_PULL_REQUEST = "pullRequest";
   public static final String PARAM_ORGANIZATION = "organization";
   public static final String PARAM_RULES = "rules";
   public static final String PARAM_ACTIONS = "actions";
index 28a7ebe3775d0cb665c80566956ffe64be91f788..75918c48fad51f96cceea6607479db77aa1a084f 100644 (file)
@@ -232,6 +232,7 @@ public class IssuesService extends BaseService {
         .setParam("projectUuids", request.getProjectUuids() == null ? null : request.getProjectUuids().stream().collect(Collectors.joining(",")))
         .setParam("projects", request.getProjects() == null ? null : request.getProjects().stream().collect(Collectors.joining(",")))
         .setParam("ps", request.getPs())
+        .setParam("pullRequest", request.getPullRequest())
         .setParam("resolutions", request.getResolutions() == null ? null : request.getResolutions().stream().collect(Collectors.joining(",")))
         .setParam("resolved", request.getResolved())
         .setParam("rules", request.getRules() == null ? null : request.getRules().stream().collect(Collectors.joining(",")))
index 3e5def5f536a7d87d6f8e5b0b95c75b2341c02e5..74096bdde5888cd176a489d9e19508baa1b1a008 100644 (file)
@@ -59,6 +59,7 @@ public class SearchRequest {
   private List<String> projectUuids;
   private List<String> projects;
   private String ps;
+  private String pullRequest;
   private List<String> resolutions;
   private String resolved;
   private List<String> rules;
@@ -469,6 +470,19 @@ public class SearchRequest {
     return ps;
   }
 
+  /**
+   * This is part of the internal API.
+   * Example value: "5461"
+   */
+  public SearchRequest setPullRequest(String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
+
+  public String getPullRequest() {
+    return pullRequest;
+  }
+
   /**
    * Example value: "FIXED,REMOVED"
    * Possible values:
index 5029fb0e5727ef75905911756caf7a98e13f64f3..fa8041a095d25d9e559269a688cb2bc19bfb9efe 100644 (file)
@@ -38,6 +38,7 @@ public class ComponentRequest {
   private String developerId;
   private String developerKey;
   private List<String> metricKeys;
+  private String pullRequest;
 
   /**
    * Example value: "periods,metrics"
@@ -133,4 +134,17 @@ public class ComponentRequest {
   public List<String> getMetricKeys() {
     return metricKeys;
   }
+
+  /**
+   * This is part of the internal API.
+   * Example value: "5461"
+   */
+  public ComponentRequest setPullRequest(String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
+
+  public String getPullRequest() {
+    return pullRequest;
+  }
 }
index 8799bf1bc5c2bb03648f4af0d91d69d506e05892..d9416aa81c92da5fe1ee24b21405ab40ebf81397 100644 (file)
@@ -44,6 +44,7 @@ public class ComponentTreeRequest {
   private String metricSortFilter;
   private String p;
   private String ps;
+  private String pullRequest;
   private String q;
   private List<String> qualifiers;
   private List<String> s;
@@ -229,6 +230,19 @@ public class ComponentTreeRequest {
     return ps;
   }
 
+  /**
+   * This is part of the internal API.
+   * Example value: "5461"
+   */
+  public ComponentTreeRequest setPullRequest(String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
+
+  public String getPullRequest() {
+    return pullRequest;
+  }
+
   /**
    * Example value: "FILE_NAM"
    */
index 06e82e95deff603b4e745b38541c49894167c0f3..f9662d04ace092ac1c0e02f8e49c5d7dcf83bad3 100644 (file)
@@ -57,7 +57,8 @@ public class MeasuresService extends BaseService {
         .setParam("componentId", request.getComponentId())
         .setParam("developerId", request.getDeveloperId())
         .setParam("developerKey", request.getDeveloperKey())
-        .setParam("metricKeys", request.getMetricKeys() == null ? null : request.getMetricKeys().stream().collect(Collectors.joining(","))),
+        .setParam("metricKeys", request.getMetricKeys() == null ? null : request.getMetricKeys().stream().collect(Collectors.joining(",")))
+        .setParam("pullRequest", request.getPullRequest()),
       ComponentWsResponse.parser());
   }
 
@@ -84,6 +85,7 @@ public class MeasuresService extends BaseService {
         .setParam("metricSortFilter", request.getMetricSortFilter())
         .setParam("p", request.getP())
         .setParam("ps", request.getPs())
+        .setParam("pullRequest", request.getPullRequest())
         .setParam("q", request.getQ())
         .setParam("qualifiers", request.getQualifiers() == null ? null : request.getQualifiers().stream().collect(Collectors.joining(",")))
         .setParam("s", request.getS() == null ? null : request.getS().stream().collect(Collectors.joining(",")))
@@ -122,6 +124,7 @@ public class MeasuresService extends BaseService {
         .setParam("metrics", request.getMetrics() == null ? null : request.getMetrics().stream().collect(Collectors.joining(",")))
         .setParam("p", request.getP())
         .setParam("ps", request.getPs())
+        .setParam("pullRequest", request.getPullRequest())
         .setParam("to", request.getTo()),
       SearchHistoryResponse.parser());
   }
index 37f2b02da89184a1a0f57d4115d5ad68ad2bb514..04395aff33ab475a4e5add0732f3f2dfeb18b89a 100644 (file)
@@ -37,6 +37,7 @@ public class SearchHistoryRequest {
   private List<String> metrics;
   private String p;
   private String ps;
+  private String pullRequest;
   private String to;
 
   /**
@@ -114,6 +115,19 @@ public class SearchHistoryRequest {
     return ps;
   }
 
+  /**
+   * This is part of the internal API.
+   * Example value: "5461"
+   */
+  public SearchHistoryRequest setPullRequest(String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
+
+  public String getPullRequest() {
+    return pullRequest;
+  }
+
   /**
    * Example value: "2017-10-19 or 2017-10-19T13:00:00+0200"
    */
index 5dc8aa753606fb1380eacd7533dfd60c2448ab56..a442f0092c79d06b68fa41c2662c8389d9460bc2 100644 (file)
@@ -33,6 +33,7 @@ public class ComponentRequest {
 
   private String branch;
   private String component;
+  private String pullRequest;
 
   /**
    * This is part of the internal API.
@@ -58,4 +59,17 @@ public class ComponentRequest {
   public String getComponent() {
     return component;
   }
+
+  /**
+   * This is part of the internal API.
+   * Example value: "5461"
+   */
+  public ComponentRequest setPullRequest(String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
+
+  public String getPullRequest() {
+    return pullRequest;
+  }
 }
index 7dcbc5090f44d0a6409e7949300dfe7a2abba77f..0b8edf6a5b4f38c2b756d838172aa3cbcdd61585 100644 (file)
@@ -49,6 +49,7 @@ public class NavigationService extends BaseService {
       new GetRequest(path("component"))
         .setParam("branch", request.getBranch())
         .setParam("component", request.getComponent())
+        .setParam("pullRequest", request.getPullRequest())
         .setMediaType(MediaTypes.JSON)
       ).content();
   }
index 5388364e035d9b662fc7a36136505e5a129906cf..04d9b272bcecd0a1b23df6dd89ff180bdc311a8c 100644 (file)
@@ -65,7 +65,6 @@ public class TemplateGroupsRequest {
   }
 
   /**
-   * This is a mandatory parameter.
    * Possible values:
    * <ul>
    *   <li>"admin"</li>
index 3166e6f821550433457ce88299ccb4b9213bf76c..10ab3a2efbae96ad96388fb3391744c159d2a511 100644 (file)
@@ -102,6 +102,7 @@ public class ProjectAnalysesService extends BaseService {
         .setParam("p", request.getP())
         .setParam("project", request.getProject())
         .setParam("ps", request.getPs())
+        .setParam("pullRequest", request.getPullRequest())
         .setParam("to", request.getTo()),
       SearchResponse.parser());
   }
index 83e00959d0e3fdf9f855637480ebdad852bb9acc..1f81fa87296c038a702abd10473ee1513dd5cec8 100644 (file)
@@ -37,6 +37,7 @@ public class SearchRequest {
   private String p;
   private String project;
   private String ps;
+  private String pullRequest;
   private String to;
 
   /**
@@ -120,6 +121,19 @@ public class SearchRequest {
     return ps;
   }
 
+  /**
+   * This is part of the internal API.
+   * Example value: "5461"
+   */
+  public SearchRequest setPullRequest(String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
+
+  public String getPullRequest() {
+    return pullRequest;
+  }
+
   /**
    * Example value: "2017-10-19 or 2017-10-19T13:00:00+0200"
    */
index 1a65b559e8bce283d5ce1ebeca36bf245db9bd71..9eca20837c96c892378744c16be8c451ed1d5a96 100644 (file)
@@ -45,8 +45,8 @@ public class ProjectBranchesService extends BaseService {
    * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/project_branches/delete">Further information about this action online (including a response example)</a>
    * @since 6.6
    */
-  public String delete(DeleteRequest request) {
-    return call(
+  public void delete(DeleteRequest request) {
+    call(
       new PostRequest(path("delete"))
         .setParam("branch", request.getBranch())
         .setParam("project", request.getProject())
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/projectpullrequests/DeleteRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/projectpullrequests/DeleteRequest.java
new file mode 100644 (file)
index 0000000..aaaf886
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.sonarqube.ws.client.projectpullrequests;
+
+import java.util.List;
+import javax.annotation.Generated;
+
+/**
+ * This is part of the internal API.
+ * This is a POST request.
+ * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/project_pull_requests/delete">Further information about this action online (including a response example)</a>
+ * @since 7.1
+ */
+@Generated("sonar-ws-generator")
+public class DeleteRequest {
+
+  private String project;
+  private String pullRequest;
+
+  /**
+   * This is a mandatory parameter.
+   * Example value: "my_project"
+   */
+  public DeleteRequest setProject(String project) {
+    this.project = project;
+    return this;
+  }
+
+  public String getProject() {
+    return project;
+  }
+
+  /**
+   * This is a mandatory parameter.
+   * Example value: "1543"
+   */
+  public DeleteRequest setPullRequest(String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
+
+  public String getPullRequest() {
+    return pullRequest;
+  }
+}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/projectpullrequests/ListRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/projectpullrequests/ListRequest.java
new file mode 100644 (file)
index 0000000..fe7d93e
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.sonarqube.ws.client.projectpullrequests;
+
+import java.util.List;
+import javax.annotation.Generated;
+
+/**
+ * This is part of the internal API.
+ * This is a POST request.
+ * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/project_pull_requests/list">Further information about this action online (including a response example)</a>
+ * @since 7.1
+ */
+@Generated("sonar-ws-generator")
+public class ListRequest {
+
+  private String project;
+
+  /**
+   * This is a mandatory parameter.
+   * Example value: "my_project"
+   */
+  public ListRequest setProject(String project) {
+    this.project = project;
+    return this;
+  }
+
+  public String getProject() {
+    return project;
+  }
+}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/projectpullrequests/ProjectPullRequestsService.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/projectpullrequests/ProjectPullRequestsService.java
new file mode 100644 (file)
index 0000000..b7ba3da
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.sonarqube.ws.client.projectpullrequests;
+
+import java.util.stream.Collectors;
+import javax.annotation.Generated;
+import org.sonarqube.ws.MediaTypes;
+import org.sonarqube.ws.client.BaseService;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsConnector;
+import org.sonarqube.ws.ProjectPullRequests.ListWsResponse;
+
+/**
+ * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/project_pull_requests">Further information about this web service online</a>
+ */
+@Generated("sonar-ws-generator")
+public class ProjectPullRequestsService extends BaseService {
+
+  public ProjectPullRequestsService(WsConnector wsConnector) {
+    super(wsConnector, "api/project_pull_requests");
+  }
+
+  /**
+   *
+   * This is part of the internal API.
+   * This is a POST request.
+   * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/project_pull_requests/delete">Further information about this action online (including a response example)</a>
+   * @since 7.1
+   */
+  public void delete(DeleteRequest request) {
+    call(
+      new PostRequest(path("delete"))
+        .setParam("project", request.getProject())
+        .setParam("pullRequest", request.getPullRequest())
+        .setMediaType(MediaTypes.JSON)
+      ).content();
+  }
+
+  /**
+   *
+   * This is part of the internal API.
+   * This is a GET request.
+   * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/project_pull_requests/list">Further information about this action online (including a response example)</a>
+   * @since 7.1
+   */
+  public ListWsResponse list(ListRequest request) {
+    return call(
+      new GetRequest(path("list"))
+        .setParam("project", request.getProject()),
+      ListWsResponse.parser());
+  }
+}
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/projectpullrequests/package-info.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/projectpullrequests/package-info.java
new file mode 100644 (file)
index 0000000..e742bab
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+@ParametersAreNonnullByDefault
+@Generated("sonar-ws-generator")
+package org.sonarqube.ws.client.projectpullrequests;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+import javax.annotation.Generated;
index dffd09b323aa94e1e36df8edc38d31b7b7670e43..c0615b16482659bfd6ed7969815d04f6180f6af5 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonarqube.ws.client.qualitygates;
 
+import java.util.List;
 import javax.annotation.Generated;
 
 /**
index ebc887524f45023eb432cd54ff50d044679acb1d..0ef25a4c881ae5d6f3be5c7eeb1ccd0ae4bb89c0 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonarqube.ws.client.qualitygates;
 
+import java.util.List;
 import javax.annotation.Generated;
 
 /**
index 31ba90f154f2a62d8f9d6649051a82eb4eec2941..0fb572e240bb0a725563f616eef8420b6b6776d8 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonarqube.ws.client.qualitygates;
 
+import java.util.List;
 import javax.annotation.Generated;
 
 /**
index 6f9c7d6a308007363d36795c19a51d2947c4b6e7..d85bf386d0f8312f166a828bc05bc7ffb2d2adc0 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonarqube.ws.client.qualitygates;
 
+import java.util.List;
 import javax.annotation.Generated;
 
 /**
index f3ea2174093d70aa773277d182be065220d22402..2b7603aef0acfa586e7413a8bd680f5ead9733c0 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonarqube.ws.client.qualitygates;
 
+import java.util.List;
 import javax.annotation.Generated;
 
 /**
index 3e2169d78d1f393534972ce2eb76889ca217cb88..89c817d18631d29088b1b431c34f7f9e20d84efd 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonarqube.ws.client.qualitygates;
 
+import java.util.List;
 import javax.annotation.Generated;
 
 /**
index f07bc686d3199abf5f56d89e06e1373e9eea0493..809241b5fe47016b24ee2f1c61bec6312c012bad 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonarqube.ws.client.qualitygates;
 
+import java.util.List;
 import javax.annotation.Generated;
 
 /**
index 2f998f79002c2c53d20bf7e9ff199c1d5dc4441b..01b75ec4e4791aa769a7543400e3501d08eda1e3 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonarqube.ws.client.qualitygates;
 
+import java.util.List;
 import javax.annotation.Generated;
 
 /**
index 64272965eed5b1ce0eb1b76aa0127e1fe65abc66..557d4f53bf6a05d4aacdf06be39ad11577813118 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonarqube.ws.client.qualitygates;
 
+import java.util.List;
 import javax.annotation.Generated;
 
 /**
index 662524360a7aa0de19663ada9d1607a0e92cfcd7..1c65c9e9181f30be5f2e5217c3b9acc634ca061d 100644 (file)
  */
 package org.sonarqube.ws.client.qualitygates;
 
+import java.util.stream.Collectors;
 import javax.annotation.Generated;
 import org.sonarqube.ws.MediaTypes;
-import org.sonarqube.ws.Qualitygates.CreateConditionResponse;
+import org.sonarqube.ws.client.BaseService;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsConnector;
 import org.sonarqube.ws.Qualitygates.CreateResponse;
+import org.sonarqube.ws.Qualitygates.CreateConditionResponse;
 import org.sonarqube.ws.Qualitygates.GetByProjectResponse;
 import org.sonarqube.ws.Qualitygates.ListWsResponse;
 import org.sonarqube.ws.Qualitygates.ProjectStatusResponse;
 import org.sonarqube.ws.Qualitygates.SearchResponse;
 import org.sonarqube.ws.Qualitygates.ShowWsResponse;
 import org.sonarqube.ws.Qualitygates.UpdateConditionResponse;
-import org.sonarqube.ws.client.BaseService;
-import org.sonarqube.ws.client.GetRequest;
-import org.sonarqube.ws.client.PostRequest;
-import org.sonarqube.ws.client.WsConnector;
 
 /**
  * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/qualitygates">Further information about this web service online</a>
index f08636bda92f9cf228cdeb819130427ccc7ed0fb..d98af61c139b8bb30a2ead5d70968cbd860b095e 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonarqube.ws.client.qualitygates;
 
+import java.util.List;
 import javax.annotation.Generated;
 
 /**
index ec39eb47fffbd798e919b2d09197844b40da0df6..a7bcf5b94bd17bea767e5591faf87b5c247f90fa 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonarqube.ws.client.qualitygates;
 
+import java.util.List;
 import javax.annotation.Generated;
 
 /**
index 146cda0c09ef7eade928c58b46481a6aeff0dfef..e5bd51005c5ac5a541ddce8c9af4fce1dfa0c4f6 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonarqube.ws.client.qualitygates;
 
+import java.util.List;
 import javax.annotation.Generated;
 
 /**
index 7bdc088ec182eea8e1a080c7f39905feae7452fb..2692abf157c58d98ff524f79874d0643061b5a1a 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonarqube.ws.client.qualitygates;
 
+import java.util.List;
 import javax.annotation.Generated;
 
 /**
index 5bdcfa8214fa44e02b89dc9c6a99b1e20eaca88f..b36b8a4e2f5ee6dfb570eb1049cf9d84e148f679 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonarqube.ws.client.qualitygates;
 
+import java.util.List;
 import javax.annotation.Generated;
 
 /**
index 779345887822827ad8a6f4e2671cd8e18074caff..8435549d125088867e625ee89ee293cd3555042a 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonarqube.ws.client.qualitygates;
 
+import java.util.List;
 import javax.annotation.Generated;
 
 /**
index 0a15f7f8500a300e4993f4507a98db653a9c7e87..bb97414501511d5f90c305923779789724fa3e78 100644 (file)
@@ -162,6 +162,7 @@ public class SearchRequest {
    *   <li>"remFn"</li>
    *   <li>"remFnOverloaded"</li>
    *   <li>"repo"</li>
+   *   <li>"scope"</li>
    *   <li>"severity"</li>
    *   <li>"status"</li>
    *   <li>"sysTags"</li>
index 97859d258cd386c8d06dd52db390b6979772253a..716f8c45347c1e8cb79530a907f6c489141e3f40 100644 (file)
@@ -33,6 +33,7 @@ public class ListDefinitionsRequest {
 
   private String branch;
   private String component;
+  private String pullRequest;
 
   /**
    * This is part of the internal API.
@@ -58,4 +59,17 @@ public class ListDefinitionsRequest {
   public String getComponent() {
     return component;
   }
+
+  /**
+   * This is part of the internal API.
+   * Example value: "5461"
+   */
+  public ListDefinitionsRequest setPullRequest(String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
+
+  public String getPullRequest() {
+    return pullRequest;
+  }
 }
index 8d47493a4b972ff7592cbed9ddb787ee0086cb35..6f1e6d771da6dc5e08ca6a5f5383cb3f9863a255 100644 (file)
@@ -34,6 +34,7 @@ public class ResetRequest {
   private String branch;
   private String component;
   private List<String> keys;
+  private String pullRequest;
 
   /**
    * This is part of the internal API.
@@ -72,4 +73,17 @@ public class ResetRequest {
   public List<String> getKeys() {
     return keys;
   }
+
+  /**
+   * This is part of the internal API.
+   * Example value: "5461"
+   */
+  public ResetRequest setPullRequest(String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
+
+  public String getPullRequest() {
+    return pullRequest;
+  }
 }
index ef5da8de67618bee33f0d96393d7287869d3f1c9..819d0bf0d8bd7537d0fbef88011b3f65e4b7b95b 100644 (file)
@@ -35,6 +35,7 @@ public class SetRequest {
   private String component;
   private List<String> fieldValues;
   private String key;
+  private String pullRequest;
   private String value;
   private List<String> values;
 
@@ -88,6 +89,19 @@ public class SetRequest {
     return key;
   }
 
+  /**
+   * This is part of the internal API.
+   * Example value: "5461"
+   */
+  public SetRequest setPullRequest(String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
+
+  public String getPullRequest() {
+    return pullRequest;
+  }
+
   /**
    * Example value: "git@github.com:SonarSource/sonarqube.git"
    */
index f920dbb7a1aa9a424672631fc140d5159d4a55cc..d3b64a5579e547af85b9e54f7dff8408c066fb91 100644 (file)
@@ -93,7 +93,8 @@ public class SettingsService extends BaseService {
     return call(
       new GetRequest(path("list_definitions"))
         .setParam("branch", request.getBranch())
-        .setParam("component", request.getComponent()),
+        .setParam("component", request.getComponent())
+        .setParam("pullRequest", request.getPullRequest()),
       ListDefinitionsWsResponse.parser());
   }
 
@@ -110,6 +111,7 @@ public class SettingsService extends BaseService {
         .setParam("branch", request.getBranch())
         .setParam("component", request.getComponent())
         .setParam("keys", request.getKeys() == null ? null : request.getKeys().stream().collect(Collectors.joining(",")))
+        .setParam("pullRequest", request.getPullRequest())
         .setMediaType(MediaTypes.JSON)
       ).content();
   }
@@ -128,6 +130,7 @@ public class SettingsService extends BaseService {
         .setParam("component", request.getComponent())
         .setParam("fieldValues", request.getFieldValues() == null ? null : request.getFieldValues())
         .setParam("key", request.getKey())
+        .setParam("pullRequest", request.getPullRequest())
         .setParam("value", request.getValue())
         .setParam("values", request.getValues() == null ? null : request.getValues())
         .setMediaType(MediaTypes.JSON)
@@ -146,7 +149,8 @@ public class SettingsService extends BaseService {
       new GetRequest(path("values"))
         .setParam("branch", request.getBranch())
         .setParam("component", request.getComponent())
-        .setParam("keys", request.getKeys() == null ? null : request.getKeys().stream().collect(Collectors.joining(","))),
+        .setParam("keys", request.getKeys() == null ? null : request.getKeys().stream().collect(Collectors.joining(",")))
+        .setParam("pullRequest", request.getPullRequest()),
       ValuesWsResponse.parser());
   }
 }
index 3d7da5ea1cd4bc52855b507b2509b280c5b400be..be37e21c8d0071e99c4f2d26a22ecf33c36ec5ab 100644 (file)
@@ -34,6 +34,7 @@ public class ValuesRequest {
   private String branch;
   private String component;
   private List<String> keys;
+  private String pullRequest;
 
   /**
    * This is part of the internal API.
@@ -71,4 +72,17 @@ public class ValuesRequest {
   public List<String> getKeys() {
     return keys;
   }
+
+  /**
+   * This is part of the internal API.
+   * Example value: "5461"
+   */
+  public ValuesRequest setPullRequest(String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
+
+  public String getPullRequest() {
+    return pullRequest;
+  }
 }
index c100ee466914402510b6741fc9b6f293affdd633..00848510f6379637b414ed3ce91ad78b04dc3add 100644 (file)
@@ -34,6 +34,7 @@ public class LinesRequest {
   private String branch;
   private String from;
   private String key;
+  private String pullRequest;
   private String to;
   private String uuid;
 
@@ -74,6 +75,19 @@ public class LinesRequest {
     return key;
   }
 
+  /**
+   * This is part of the internal API.
+   * Example value: "5461"
+   */
+  public LinesRequest setPullRequest(String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
+
+  public String getPullRequest() {
+    return pullRequest;
+  }
+
   /**
    * Example value: "20"
    */
index 7fccf661dbf42be169856705146fb622a676949a..5a7652b671755abea218e3e373bd66365de637e3 100644 (file)
@@ -33,6 +33,7 @@ public class RawRequest {
 
   private String branch;
   private String key;
+  private String pullRequest;
 
   /**
    * This is part of the internal API.
@@ -59,4 +60,17 @@ public class RawRequest {
   public String getKey() {
     return key;
   }
+
+  /**
+   * This is part of the internal API.
+   * Example value: "5461"
+   */
+  public RawRequest setPullRequest(String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
+
+  public String getPullRequest() {
+    return pullRequest;
+  }
 }
index 37795a4e13c7af464f5ffc02f5ed1b5455a3e31f..f8fe1a17b1e9cc28d4313498c6964641aeac0f27 100644 (file)
@@ -82,6 +82,7 @@ public class SourcesService extends BaseService {
         .setParam("branch", request.getBranch())
         .setParam("from", request.getFrom())
         .setParam("key", request.getKey())
+        .setParam("pullRequest", request.getPullRequest())
         .setParam("to", request.getTo())
         .setParam("uuid", request.getUuid())
         .setMediaType(MediaTypes.JSON)
@@ -100,6 +101,7 @@ public class SourcesService extends BaseService {
       new GetRequest(path("raw"))
         .setParam("branch", request.getBranch())
         .setParam("key", request.getKey())
+        .setParam("pullRequest", request.getPullRequest())
         .setMediaType(MediaTypes.JSON)
       ).content();
   }
index 6e445f06c85fcefdcef4b01ff4cd24e2de96bd0d..a7a38ca8cd6e2a2954a2c29c631a981bc4707b56 100644 (file)
@@ -34,6 +34,7 @@ public class ListRequest {
   private String branch;
   private String p;
   private String ps;
+  private String pullRequest;
   private String sourceFileId;
   private String sourceFileKey;
   private String sourceFileLineNumber;
@@ -78,6 +79,19 @@ public class ListRequest {
     return ps;
   }
 
+  /**
+   * This is part of the internal API.
+   * Example value: "5461"
+   */
+  public ListRequest setPullRequest(String pullRequest) {
+    this.pullRequest = pullRequest;
+    return this;
+  }
+
+  public String getPullRequest() {
+    return pullRequest;
+  }
+
   /**
    * Example value: "AU-TpxcA-iU5OvuD2FL0"
    */
index 2552c9a1a99375e19263864cc0e43e53b517af65..3683b5d79c7439df5edcf9e858eedd48f159350c 100644 (file)
@@ -72,6 +72,7 @@ public class TestsService extends BaseService {
         .setParam("branch", request.getBranch())
         .setParam("p", request.getP())
         .setParam("ps", request.getPs())
+        .setParam("pullRequest", request.getPullRequest())
         .setParam("sourceFileId", request.getSourceFileId())
         .setParam("sourceFileKey", request.getSourceFileKey())
         .setParam("sourceFileLineNumber", request.getSourceFileLineNumber())
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/users/SetHomepageRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/users/SetHomepageRequest.java
new file mode 100644 (file)
index 0000000..8ad79cc
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.sonarqube.ws.client.users;
+
+import java.util.List;
+import javax.annotation.Generated;
+
+/**
+ * This is part of the internal API.
+ * This is a POST request.
+ * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/users/set_homepage">Further information about this action online (including a response example)</a>
+ * @since 7.0
+ */
+@Generated("sonar-ws-generator")
+public class SetHomepageRequest {
+
+  private String parameter;
+  private String type;
+
+  /**
+   * Example value: "my_project"
+   */
+  public SetHomepageRequest setParameter(String parameter) {
+    this.parameter = parameter;
+    return this;
+  }
+
+  public String getParameter() {
+    return parameter;
+  }
+
+  /**
+   * This is a mandatory parameter.
+   * Possible values:
+   * <ul>
+   *   <li>"PROJECT"</li>
+   *   <li>"ORGANIZATION"</li>
+   *   <li>"MY_PROJECTS"</li>
+   *   <li>"MY_ISSUES"</li>
+   * </ul>
+   */
+  public SetHomepageRequest setType(String type) {
+    this.type = type;
+    return this;
+  }
+
+  public String getType() {
+    return type;
+  }
+}
index c490b22eeb0108c6fa5b27a4360eb2f444c52b31..3dcec5eaa393295fe2282cec82309a3408dd8a54 100644 (file)
@@ -156,6 +156,22 @@ public class UsersService extends BaseService {
       SearchWsResponse.parser());
   }
 
+  /**
+   *
+   * This is part of the internal API.
+   * This is a POST request.
+   * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/users/set_homepage">Further information about this action online (including a response example)</a>
+   * @since 7.0
+   */
+  public void setHomepage(SetHomepageRequest request) {
+    call(
+      new PostRequest(path("set_homepage"))
+        .setParam("parameter", request.getParameter())
+        .setParam("type", request.getType())
+        .setMediaType(MediaTypes.JSON)
+      ).content();
+  }
+
   /**
    *
    * This is part of the internal API.
index dc18f7cb87e0ac15d98914648ae1e33df504d5d0..5787a326e5e235eb843fcf7b58c6feef3269aabf 100644 (file)
@@ -92,6 +92,8 @@ message Task {
   optional string branch = 21;
   optional sonarqube.ws.commons.BranchType branchType = 22;
   optional string errorType = 23;
+  optional string pullRequest = 24;
+  optional string pullRequestTitle = 25;
 }
 
 enum TaskStatus {
index b5b0167e8d0d7481b3b905ebcfe8dabf7aed48f0..947e24876570e55630651063d98e69bbb754cbf0 100644 (file)
@@ -127,4 +127,5 @@ enum BranchType {
 
   LONG = 1;
   SHORT = 2;
+  PULL_REQUEST = 3;
 }
index 73059133649bbf69a9e9148cf9a0b413bb668c0d..a995eb42cbc9c5c2d3d14f8cad8c0bcd65f7f69f 100644 (file)
@@ -122,6 +122,7 @@ message Component {
   optional string project = 17;
   optional string branch = 18;
   optional string version = 19;
+  optional string pullRequest = 20;
 
   message Tags {
     repeated string tags = 1;
index 6eeb5f29a3225ae41aaf8226c7d98e77932e4581..98cbe46885ca411d0347eee129e67e574ce5cc3d 100644 (file)
@@ -52,4 +52,5 @@ message File {
   string subProjectUuid = 8;
   string subProjectName = 9;
   string branch = 10;
+  string pullRequest = 11;
 }
index c869c9c1ff3d3f8ccb5aa240c7059a3b20f87c42..125f6e4c216201657a53f5cf3d0cbcc0696e332a 100644 (file)
@@ -156,6 +156,7 @@ message Issue {
 
   optional string organization = 29;
   optional string branch = 30;
+  optional string pullRequest = 32;
 }
 
 message Transitions {
@@ -234,6 +235,7 @@ message Component {
   optional int64 unusedProjectId = 9;
   optional int64 unusedSubProjectId = 10;
   optional string branch = 12;
+  optional string pullRequest = 13;
 }
 
 // Response of GET api/issues/changelog
index 36cd4ab6174ba8065742aae7d98f741f460b5e62..c861426c3ea2698cad57db4761832c303b567f76 100644 (file)
@@ -76,6 +76,7 @@ message Component {
   optional string language = 10;
   repeated Measure measures = 11;
   optional string branch = 12;
+  optional string pullRequest = 13;
 }
 
 message Period {
index 55d8e4032cec7ee14d0d41e1950ea7f4c8c20022..e75439ab06e5e2c64021d8596b73d9040412a064 100644 (file)
@@ -45,16 +45,15 @@ message Branch {
   optional Status status = 5;
   optional bool isOrphan = 6;
   optional string analysisDate = 7;
+}
 
-  message Status {
-    // Quality gate status is only present for long living branch
-    optional string qualityGateStatus = 1;
-    // Merge bugs, vulnerabilities and codeSmell are only present for short living branch
-    optional int64 bugs = 2;
-    optional int64 vulnerabilities = 3;
-    optional int64 codeSmells = 4;
-  }
-
+message Status {
+  // Quality gate status is only present for long living branch
+  optional string qualityGateStatus = 1;
+  // Merge bugs, vulnerabilities and codeSmell are only present for short living branch
+  optional int64 bugs = 2;
+  optional int64 vulnerabilities = 3;
+  optional int64 codeSmells = 4;
 }
 
 
diff --git a/sonar-ws/src/main/protobuf/ws-projectpullrequests.proto b/sonar-ws/src/main/protobuf/ws-projectpullrequests.proto
new file mode 100644 (file)
index 0000000..e6bc030
--- /dev/null
@@ -0,0 +1,49 @@
+// SonarQube, open source software quality management tool.
+// Copyright (C) 2008-2016 SonarSource
+// mailto:contact AT sonarsource DOT com
+//
+// SonarQube 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.
+//
+// SonarQube 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.
+
+syntax = "proto2";
+
+package sonarqube.ws.projectpullrequest;
+
+option java_package = "org.sonarqube.ws";
+option java_outer_classname = "ProjectPullRequests";
+option optimize_for = SPEED;
+
+import "ws-commons.proto";
+
+// WS api/project_pull_requests/list
+message ListWsResponse {
+  repeated PullRequest pullRequests = 1;
+}
+
+message PullRequest {
+  optional string key = 1;
+  optional string title = 2;
+  optional string branch = 3;
+  optional string base = 4;
+  optional Status status = 5;
+  optional bool isOrphan = 6;
+  optional string analysisDate = 7;
+  optional string url = 8;
+}
+
+message Status {
+  optional int64 bugs = 2;
+  optional int64 vulnerabilities = 3;
+  optional int64 codeSmells = 4;
+}
index 74865970ca60a08116ac50632ad56639834a57f0..6d389e735bdee0dd5fd226ca0a5a114ef331278b 100644 (file)
@@ -42,6 +42,7 @@ message CoveredFilesResponse {
     optional string longName = 3;
     optional int32 coveredLines = 4;
     optional string branch = 5;
+    optional string pullRequest = 6;
   }
 }
 
@@ -58,6 +59,7 @@ message Test {
   optional string message = 9;
   optional string stacktrace = 10;
   optional string fileBranch = 11;
+  optional string filePullRequest = 12;
 }
 
 enum TestStatus {
index 258ef9b92f319f4ba69fa5e32d5058bf5658fa3b..7fbf43db21ef77e0cf5f7d2a169ae48ad2994521 100644 (file)
@@ -185,7 +185,7 @@ public class AutoAssignTest extends AbstractIssueTest {
         .setName(name)
         .setEmail(email)
         .setPassword("xxxxxxx")
-        .setScmAccounts(asList(scmAccounts)));
+        .setScmAccount(asList(scmAccounts)));
   }
 
   private static void deleteAllUsers() {
index cff4e0f1aa59dd3ef7fd61342301bda7418857c7..76ae03a0a8b57979dad744ffdf205725d58d2f11 100644 (file)
@@ -95,7 +95,7 @@ public class IssueCreationDatePluginChangedTest {
 
     // Create a user and register her to receive notification on NewIssues
     tester.users().generate(t -> t.setLogin(USER_LOGIN).setPassword(USER_PASSWORD).setEmail(USER_EMAIL)
-      .setScmAccounts(ImmutableList.of("jhenry")));
+      .setScmAccount(ImmutableList.of("jhenry")));
     // Add notifications to the test user
     WsClient wsClient = newUserWsClient(ORCHESTRATOR, USER_LOGIN, USER_PASSWORD);
     wsClient.wsConnector().call(new PostRequest("api/notifications/add")
index 3f56905e7b23211da337f8c2b09998f9fac7c6c6..51e258bdfb84676a376ccc6366be8eea6dcdb6d7 100644 (file)
@@ -241,7 +241,7 @@ public class LocalAuthenticationTest {
       .setLogin("test")
       .setName("Test")
       .setEmail("test@email.com")
-      .setScmAccounts(asList("test1", "test2"))
+      .setScmAccount(asList("test1", "test2"))
       .setPassword("password"));
 
     assertThat(checkAuthenticationWithAuthenticateWebService("test", "password")).isTrue();
index eaf9879c83cdd2a9c25c356dc9993b1251c323c0..5386944b1486dab9cfbfb538c1a8f2c34d51156a 100755 (executable)
--- a/travis.sh
+++ b/travis.sh
@@ -34,7 +34,7 @@ function installNode {
 #
 function configureTravis {
   mkdir -p ~/.local
-  curl -sSL https://github.com/SonarSource/travis-utils/tarball/v41 | tar zx --strip-components 1 -C ~/.local
+  curl -sSL https://github.com/SonarSource/travis-utils/tarball/v47 | tar zx --strip-components 1 -C ~/.local
   source ~/.local/bin/install
 }
 configureTravis