Browse Source

Revert SONAR-14478, SONAR-14462, SONAR-14461

* Revert "SONAR-14478 - Main Branch Documentation"

This reverts commit 59eae7cf3f.

* Revert "SONAR-14462 Do not display the branch name until the main branch is analyzed for the first time"

This reverts commit 20f7319c06.

* Revert "SONAR-14461 main branch detection"

This reverts commit c04baa1e8e.

* Revert "SONAR-14461 Remove hardcoded usage of 'master'"

This reverts commit 32eefaf2d3.

* Revert "SONAR-14461 save the default main branch when needed"

This reverts commit 879a4be2af.
tags/8.8.0.42792
lukasz-jarocki-sonarsource 3 years ago
parent
commit
9cb17b6dbc
32 changed files with 1436 additions and 710 deletions
  1. 0
    1
      server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java
  2. 0
    79
      server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/UpdateMainBranchStep.java
  3. 0
    136
      server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/UpdateMainBranchStepTest.java
  4. 10
    0
      server/sonar-docs/src/pages/analysis/branch-pr-analysis-overview.md
  5. 7
    21
      server/sonar-docs/src/pages/branches/overview.md
  6. 139
    0
      server/sonar-web/src/main/js/api/news.ts
  7. 67
    22
      server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx
  8. 135
    0
      server/sonar-web/src/main/js/app/components/embed-docs-modal/ProductNewsMenuItem.tsx
  9. 48
    0
      server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/ProductNewsMenuItem-test.tsx
  10. 97
    0
      server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/ProductNewsMenuItem-test.tsx.snap
  11. 0
    5
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx
  12. 1
    18
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx
  13. 0
    3
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap
  14. 93
    0
      server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx
  15. 134
    0
      server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx
  16. 67
    0
      server/sonar-web/src/main/js/app/components/notifications/__tests__/NavLatestNotification-test.tsx
  17. 119
    0
      server/sonar-web/src/main/js/app/components/notifications/__tests__/NotificationsSidebar-test.tsx
  18. 82
    0
      server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap
  19. 269
    0
      server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NotificationsSidebar-test.tsx.snap
  20. 157
    0
      server/sonar-web/src/main/js/app/components/notifications/notifications.css
  21. 3
    7
      server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx
  22. 1
    1
      server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx
  23. 1
    3
      server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx
  24. 2
    9
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx
  25. 1
    2
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx
  26. 1
    180
      server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap
  27. 0
    11
      sonar-plugin-api/src/main/java/org/sonar/api/batch/scm/ScmProvider.java
  28. 2
    20
      sonar-scanner-engine/src/main/java/org/sonar/scanner/report/MetadataPublisher.java
  29. 0
    15
      sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmProvider.java
  30. 0
    45
      sonar-scanner-engine/src/test/java/org/sonar/scanner/report/MetadataPublisherTest.java
  31. 0
    130
      sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitScmProviderTest.java
  32. 0
    2
      sonar-scanner-protocol/src/main/protobuf/scanner_report.proto

+ 0
- 1
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/ReportComputationSteps.java View File

@@ -107,7 +107,6 @@ public class ReportComputationSteps extends AbstractComputationSteps {
PurgeDatastoresStep.class,
IndexAnalysisStep.class,
UpdateNeedIssueSyncStep.class,
UpdateMainBranchStep.class,

// notifications are sent at the end, so that webapp displays up-to-date information
SendIssueNotificationsStep.class,

+ 0
- 79
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/UpdateMainBranchStep.java View File

@@ -1,79 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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.ce.task.projectanalysis.step;

import java.util.Optional;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
import org.sonar.ce.task.projectanalysis.batch.BatchReportReader;
import org.sonar.ce.task.step.ComputationStep;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.project.ProjectDto;

public class UpdateMainBranchStep implements ComputationStep {

private static final Logger LOGGER = Loggers.get(UpdateMainBranchStep.class);

private final BatchReportReader batchReportReader;
private final DbClient dbClient;
private final AnalysisMetadataHolder analysisMetadataHolder;

public UpdateMainBranchStep(BatchReportReader batchReportReader, DbClient dbClient, AnalysisMetadataHolder analysisMetadataHolder) {
this.batchReportReader = batchReportReader;
this.dbClient = dbClient;
this.analysisMetadataHolder = analysisMetadataHolder;
}

@Override
public void execute(Context context) {

if (!analysisMetadataHolder.isFirstAnalysis()) {
return;
}

String gitDefaultMainBranch = batchReportReader.readMetadata().getGitDefaultMainBranch();
if (gitDefaultMainBranch.isEmpty()) {
LOGGER.debug("GIT default main branch detected is empty");
return;
}
LOGGER.debug(String.format("GIT default main branch detected is [%s]", gitDefaultMainBranch));
updateProjectDefaultMainBranch(gitDefaultMainBranch);
}

private void updateProjectDefaultMainBranch(String gitDefaultMainBranch) {
try (DbSession dbSession = dbClient.openSession(false)) {
String projectKey = analysisMetadataHolder.getProject().getKey();
Optional<ProjectDto> projectDto = dbClient.projectDao().selectProjectByKey(dbSession, projectKey);
if (!projectDto.isPresent()) {
throw new IllegalStateException(String.format("root component key [%s] is not a project", projectKey));
}
LOGGER.info(String.format("updating project [%s] default main branch to [%s]", projectKey, gitDefaultMainBranch));
dbClient.branchDao().updateMainBranchName(dbSession, projectDto.get().getUuid(), gitDefaultMainBranch);
dbSession.commit();
}
}

@Override
public String getDescription() {
return "Update the project main branch name, based on GIT information. Only for the first project's analysis.";
}
}

+ 0
- 136
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/UpdateMainBranchStepTest.java View File

@@ -1,136 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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.ce.task.projectanalysis.step;

import java.util.Collection;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.utils.System2;
import org.sonar.api.utils.log.LogTester;
import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder;
import org.sonar.ce.task.projectanalysis.batch.BatchReportReader;
import org.sonar.ce.task.step.ComputationStep;
import org.sonar.db.DbTester;
import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ComponentDto;
import org.sonar.scanner.protocol.output.ScannerReport;
import org.sonar.server.project.Project;

import static java.util.Collections.emptyList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.sonar.api.utils.log.LoggerLevel.TRACE;

public class UpdateMainBranchStepTest {

@Rule
public DbTester dbTester = DbTester.create(System2.INSTANCE);

@Rule
public LogTester logTester = new LogTester().setLevel(TRACE);

private final AnalysisMetadataHolder analysisMetadataHolder = mock(AnalysisMetadataHolder.class);
BatchReportReader batchReportReader = mock(BatchReportReader.class);

private final UpdateMainBranchStep underTest = new UpdateMainBranchStep(batchReportReader, dbTester.getDbClient(), analysisMetadataHolder);
private ComputationStep.Context context = mock(ComputationStep.Context.class);

@Test
public void update_main_branch_on_first_analysis() {
ScannerReport.Metadata metadata = ScannerReport.Metadata.newBuilder().setGitDefaultMainBranch("new_name").buildPartial();
when(batchReportReader.readMetadata()).thenReturn(metadata);
when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(true);
ComponentDto privateProject = dbTester.components().insertPrivateProject();
when(analysisMetadataHolder.getProject())
.thenReturn(new Project(privateProject.uuid(), privateProject.getKey(), privateProject.name(), privateProject.description(), emptyList()));

assertMainBranchName(privateProject, "master");

underTest.execute(context);

assertMainBranchName(privateProject, "new_name");
assertThat(logTester.logs()).contains("GIT default main branch detected is [new_name]");
assertThat(logTester.logs()).contains(String.format("updating project [%s] default main branch to [new_name]", privateProject.getKey()));
}

@Test
public void do_not_update_main_branch_on_second_analysis() {
ScannerReport.Metadata metadata = ScannerReport.Metadata.newBuilder().setGitDefaultMainBranch("new_name").buildPartial();
when(batchReportReader.readMetadata()).thenReturn(metadata);
when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false);
ComponentDto privateProject = dbTester.components().insertPrivateProject();
when(analysisMetadataHolder.getProject())
.thenReturn(new Project(privateProject.uuid(), privateProject.getKey(), privateProject.name(), privateProject.description(), emptyList()));

assertMainBranchName(privateProject, "master");

underTest.execute(context);

assertMainBranchName(privateProject, "master");
assertThat(logTester.logs()).isEmpty();
}

@Test
public void do_not_update_main_branch_if_no_git_info_found() {
String emptyGitMainBranchInfo = "";
ScannerReport.Metadata metadata = ScannerReport.Metadata.newBuilder().setGitDefaultMainBranch(emptyGitMainBranchInfo).buildPartial();
when(batchReportReader.readMetadata()).thenReturn(metadata);
when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(true);
ComponentDto privateProject = dbTester.components().insertPrivateProject();
when(analysisMetadataHolder.getProject())
.thenReturn(new Project(privateProject.uuid(), privateProject.getKey(), privateProject.name(), privateProject.description(), emptyList()));

assertMainBranchName(privateProject, "master");

underTest.execute(context);

assertMainBranchName(privateProject, "master");
assertThat(logTester.logs()).contains("GIT default main branch detected is empty");
}

@Test
public void fail_on_invalid_project_key() {
ScannerReport.Metadata metadata = ScannerReport.Metadata.newBuilder().setGitDefaultMainBranch("new_name").buildPartial();
when(batchReportReader.readMetadata()).thenReturn(metadata);
when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(true);
ComponentDto privateProject = dbTester.components().insertPrivateProject();
when(analysisMetadataHolder.getProject())
.thenReturn(new Project(privateProject.uuid(), "invalid project key", privateProject.name(), privateProject.description(), emptyList()));

assertThatThrownBy(() -> underTest.execute(context))
.isInstanceOf(IllegalStateException.class)
.hasMessage("root component key [invalid project key] is not a project");
assertThat(logTester.logs()).contains("GIT default main branch detected is [new_name]");
}

@Test
public void getDescription() {
assertThat(underTest.getDescription()).isNotEmpty();
}

private void assertMainBranchName(ComponentDto privateProject, String expectedBranchName) {
Collection<BranchDto> branches = dbTester.getDbClient().branchDao().selectByComponent(dbTester.getSession(), privateProject);
assertThat(branches).isNotEmpty();
assertThat(branches).hasSize(1);
assertThat(branches.iterator().next().getKey()).isEqualTo(expectedBranchName);
}
}

+ 10
- 0
server/sonar-docs/src/pages/analysis/branch-pr-analysis-overview.md View File

@@ -10,6 +10,16 @@ SonarScanners running in GitLab CI/CD, Azure Pipelines, Cirrus CI, and Jenkins w
[[warning]]
| Automatic configuration is disabled if any branch or pull request properties have been set manually.

## Keeping your "master" branch history when upgrading from Community Edition to a commercial edition

In Community Edition, your analyzed branch is named "master" by default.

When upgrading to a current commercial edition version, automatic branch and pull request configuration creates branches based on their names in your code repository. If the name of your Main Branch (master) in SonarQube doesn't match the branch's name in your code repository, the history of your Main Branch won't be taken on by the branch you analyze.

**Before running analysis**, you can keep your branch history by renaming the Main Branch in SonarQube with the name of the branch in your code repository at **Project Settings > Branches and Pull Requests**.

For example, if your Main Branch is named "master" in SonarQube but "develop" in your code repository, rename your Main Branch "develop" in SonarQube.

## GitLab CI/CD
For GitLab CI/CD configuration, see the [GitLab ALM integration](/analysis/gitlab-integration/) page.


+ 7
- 21
server/sonar-docs/src/pages/branches/overview.md View File

@@ -7,19 +7,13 @@ _Branch analysis is available starting in [Developer Edition](https://redirect.s

## Overview

With branch analysis, you can ensure that you're maintaining consistent code quality all the way down to the branch level of your projects.
With Branch Analysis, you can ensure that you're maintaining consistent code quality all the way down to the branch level of your projects.

### Master/Main branch
### Master / Main Branch

This is the default branch and typically corresponds to what's being developed for your next release. This is usually known within a development team as "master" or "head" and is analyzed when no specific branch parameters are provided. It is labeled **MAIN BRANCH** in SonarQube.
This is the default branch and typically corresponds to what's being developed for your next release. This is usually known within a development team as "master" or "head" and is analyzed when no specific branch parameters are provided. It is labeled "Main Branch" and defaults to the name "master" but can be renamed from the project settings at **Administration > Branches and Pull Requests**. When you are using Community Edition, this is the only branch you see.

If you're using Git as your SCM, when you analyze your project for the first time, the main branch will be renamed to match the main branch of your repository. If SonarQube can't find information about the main branch during the first analysis, it's named "master" by default.

Starting in [Developer Edition](https://redirect.sonarsource.com/editions/developer.html), you can rename the main branch manually from the project settings at **Administration > Branches and Pull Requests**.

In Community Edition, this is the only branch you see. You can rename it using the [Web API](/extend/web-api/).

### Settings and quality profiles on branches
### Settings and Quality Profiles on Branches

Branch settings and Quality Profiles are the same as those set for the master branch, and by design, it's not possible to configure other values. The New Code Period is the only exception to this as it can be set on a branch-by-branch basis.

@@ -34,7 +28,7 @@ The branch Quality Gate lets you know if your branch is ready to be merged. Each
* Applies on conditions on New Code and overall code.
* Assigns a status (Passed or Failed).

## Setting up branch analysis
## Setting up Branch analysis

A branch is created when the `sonar.branch.name` parameter is passed during analysis.

@@ -54,9 +48,9 @@ if [[ "$CI_BRANCH_NAME" == master ]] || [[ "$CI_BRANCH_NAME" == release/* ]]; th
fi
```

### Issue Creation and synchronization
### Issue Creation and Synchronization

During the first analysis, issues (type, severity, status, assignee, change log, comments) are synchronized with the main branch. In each synchronized issue, a comment is added to the change log of the issue on the branch: "The issue has been copied from branch 'master' to branch yyy".
During the first analysis, issues (type, severity, status, assignee, change log, comments) are synchronized with the Main Branch. In each synchronized issue, a comment is added to the change log of the issue on the branch: "The issue has been copied from branch 'master' to branch yyy".

At each subsequent analysis of the branch, any new issue that comes from a pull request automatically inherits the attributes (type, severity, ...) the issue had in the pull request. A comment is added to the change log of the issue on the branch: "The issue has been merged from 'xxx' into 'yyy'"

@@ -84,11 +78,3 @@ For example, adding the pattern `release/.*` would keep any branches named relea
You can set a branch to **Keep when inactive** at the project level from from the **Branches** tab at **Project Settings > Branches and Pull Requests**. Here, you can also turn off protection for a branch so it will be deleted when it's inactive for the number of days that has been specified in the global settings at **Administration > General Settings > Housekeeping > Number of days before deleting inactive branches**.

**Note:** The main branch is always protected from automatic deletion, even if it's inactive. This can't be changed.

## Keeping your "master" branch history when upgrading from Community Edition to a commercial edition

When upgrading to a current commercial edition version, automatic branch and pull request configuration creates branches based on their names in your code repository. If the name of your main branch (master) in SonarQube doesn't match the branch's name in your code repository, the history of your main branch won't be taken on by the branch you analyze.

**Before running analysis**, you can keep your branch history by renaming the main branch in SonarQube with the name of the branch in your code repository at **Project Settings > Branches and Pull Requests**.

For example, if your main branch is named "master" in SonarQube but "develop" in your code repository, rename your main branch "develop" in SonarQube.

+ 139
- 0
server/sonar-web/src/main/js/api/news.ts View File

@@ -0,0 +1,139 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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.
*/
import { getCorsJSON } from 'sonar-ui-common/helpers/request';

interface PrismicRef {
id: string;
ref: string;
}

export interface PrismicNews {
data: { title: string };
last_publication_date: string;
uid: string;
}

interface PrismicResponse {
page: number;
results: PrismicResult[];
results_per_page: number;
total_results_size: number;
}

interface PrismicResult {
data: {
notification: string;
publication_date: string;
body: PrismicResultFeature[];
};
}

interface PrismicResultFeature {
items: Array<{
category: {
data: {
color: string;
name: string;
};
};
}>;
primary: {
description: string;
read_more_link: {
url?: string;
};
};
}

export interface PrismicFeatureNews {
notification: string;
publicationDate: string;
features: Array<{
categories: Array<{
color: string;
name: string;
}>;
description: string;
readMore?: string;
}>;
}

const PRISMIC_API_URL = 'https://sonarsource.cdn.prismic.io/api/v2';

export function fetchPrismicRefs() {
return getCorsJSON(PRISMIC_API_URL).then((response: { refs: PrismicRef[] }) => {
const master = response && response.refs.find(ref => ref.id === 'master');
if (!master) {
return Promise.reject('No master ref found');
}
return master;
});
}

export function fetchPrismicNews(data: {
accessToken: string;
ps?: number;
ref: string;
tag?: string;
}) {
const q = ['[[at(document.type, "blog_sonarsource_post")]]'];
if (data.tag) {
q.push(`[[at(document.tags,["${data.tag}"])]]`);
}
return getCorsJSON(PRISMIC_API_URL + '/documents/search', {
access_token: data.accessToken,
orderings: '[document.first_publication_date desc]',
pageSize: data.ps || 1,
q,
ref: data.ref
}).then(({ results }: { results: PrismicNews[] }) => results);
}

export function fetchPrismicFeatureNews(data: {
accessToken: string;
p?: number;
ps?: number;
ref: string;
}): Promise<{ news: PrismicFeatureNews[]; paging: T.Paging }> {
return getCorsJSON(PRISMIC_API_URL + '/documents/search', {
access_token: data.accessToken,
fetchLinks: 'sc_category.color,sc_category.name',
orderings: '[my.sc_product_news.publication_date desc]',
page: data.p || 1,
pageSize: data.ps || 1,
q: ['[[at(document.type, "sc_product_news")]]'],
ref: data.ref
}).then(({ page, results, results_per_page, total_results_size }: PrismicResponse) => ({
news: results.map(result => ({
notification: result.data.notification,
publicationDate: result.data.publication_date,
features: result.data.body.map(feature => ({
categories: feature.items.map(item => item.category.data).filter(Boolean),
description: feature.primary.description,
readMore: feature.primary.read_more_link.url
}))
})),
paging: {
pageIndex: page,
pageSize: results_per_page,
total: total_results_size
}
}));
}

+ 67
- 22
server/sonar-web/src/main/js/app/components/embed-docs-modal/EmbedDocsPopup.tsx View File

@@ -22,6 +22,8 @@ import { Link } from 'react-router';
import { DropdownOverlay } from 'sonar-ui-common/components/controls/Dropdown';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getBaseUrl } from 'sonar-ui-common/helpers/urls';
import { isSonarCloud } from '../../../helpers/system';
import ProductNewsMenuItem from './ProductNewsMenuItem';
import { SuggestionsContext } from './SuggestionsContext';

interface Props {
@@ -67,6 +69,70 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> {
);
}

renderSonarCloudLinks() {
return (
<>
<li className="divider" />
<li>
<a
href="https://community.sonarsource.com/c/help/sc"
rel="noopener noreferrer"
target="_blank">
{translate('embed_docs.get_help')}
</a>
</li>
<li className="divider" />
{this.renderTitle(translate('embed_docs.stay_connected'))}
<li>
{this.renderIconLink(
'https://twitter.com/sonarcloud',
'embed-doc/twitter-icon.svg',
'Twitter'
)}
</li>
<li>
{this.renderIconLink(
'https://blog.sonarsource.com/product/SonarCloud',
'sonarcloud-square-logo.svg',
translate('embed_docs.blog')
)}
</li>
<li>
<ProductNewsMenuItem tag="SonarCloud" />
</li>
</>
);
}

renderSonarQubeLinks() {
return (
<>
<li className="divider" />
<li>
<a href="https://community.sonarsource.com/" rel="noopener noreferrer" target="_blank">
{translate('embed_docs.get_help')}
</a>
</li>
<li className="divider" />
{this.renderTitle(translate('embed_docs.stay_connected'))}
<li>
{this.renderIconLink(
'https://www.sonarqube.org/whats-new/?referrer=sonarqube',
'embed-doc/sq-icon.svg',
translate('embed_docs.news')
)}
</li>
<li>
{this.renderIconLink(
'https://twitter.com/SonarQube',
'embed-doc/twitter-icon.svg',
'Twitter'
)}
</li>
</>
);
}

render() {
return (
<DropdownOverlay>
@@ -82,28 +148,7 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> {
{translate('api_documentation.page')}
</Link>
</li>
<li className="divider" />
<li>
<a href="https://community.sonarsource.com/" rel="noopener noreferrer" target="_blank">
{translate('embed_docs.get_help')}
</a>
</li>
<li className="divider" />
{this.renderTitle(translate('embed_docs.stay_connected'))}
<li>
{this.renderIconLink(
'https://www.sonarqube.org/whats-new/?referrer=sonarqube',
'embed-doc/sq-icon.svg',
translate('embed_docs.news')
)}
</li>
<li>
{this.renderIconLink(
'https://twitter.com/SonarQube',
'embed-doc/twitter-icon.svg',
'Twitter'
)}
</li>
{isSonarCloud() ? this.renderSonarCloudLinks() : this.renderSonarQubeLinks()}
</ul>
</DropdownOverlay>
);

+ 135
- 0
server/sonar-web/src/main/js/app/components/embed-docs-modal/ProductNewsMenuItem.tsx View File

@@ -0,0 +1,135 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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.
*/
import * as React from 'react';
import { connect } from 'react-redux';
import ChevronRightIcon from 'sonar-ui-common/components/icons/ChevronRightIcon';
import DateFormatter from 'sonar-ui-common/components/intl/DateFormatter';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { fetchPrismicNews, fetchPrismicRefs, PrismicNews } from '../../../api/news';
import PlaceholderBar from '../../../components/ui/PlaceholderBar';
import { getGlobalSettingValue, Store } from '../../../store/rootReducer';

interface OwnProps {
tag?: string;
}

interface StateProps {
accessToken?: string;
}

type Props = OwnProps & StateProps;

interface State {
loading: boolean;
news?: PrismicNews;
}

export class ProductNewsMenuItem extends React.PureComponent<Props, State> {
mounted = false;
state: State = { loading: false };

componentDidMount() {
this.mounted = true;
this.fetchProductNews();
}

componentWillUnmount() {
this.mounted = false;
}

fetchProductNews = () => {
const { accessToken, tag } = this.props;
if (accessToken) {
this.setState({ loading: true });
fetchPrismicRefs()
.then(({ ref }) => fetchPrismicNews({ accessToken, ref, tag }))
.then(
news => {
if (this.mounted) {
this.setState({ news: news[0], loading: false });
}
},
() => {
if (this.mounted) {
this.setState({ loading: false });
}
}
);
}
};

renderPlaceholder() {
return (
<a className="rich-item new-loading">
<div className="flex-1">
<div className="display-inline-flex-center">
<h4>{translate('embed_docs.latest_blog')}</h4>
<span className="note spacer-left">
<PlaceholderBar color="#aaa" width={60} />
</span>
</div>
<p className="little-spacer-bottom">
<PlaceholderBar color="#aaa" width={84} /> <PlaceholderBar color="#aaa" width={48} />{' '}
<PlaceholderBar color="#aaa" width={24} /> <PlaceholderBar color="#aaa" width={72} />{' '}
<PlaceholderBar color="#aaa" width={24} /> <PlaceholderBar color="#aaa" width={48} />
</p>
</div>
<ChevronRightIcon className="flex-0" />
</a>
);
}

render() {
const link = 'https://blog.sonarsource.com/';
const { loading, news } = this.state;

if (loading) {
return this.renderPlaceholder();
}

if (!news) {
return null;
}

return (
<a className="rich-item" href={link + news.uid} rel="noopener noreferrer" target="_blank">
<div className="flex-1">
<div className="display-inline-flex-center">
<h4>{translate('embed_docs.latest_blog')}</h4>
<DateFormatter date={news.last_publication_date}>
{formattedDate => <span className="note spacer-left">{formattedDate}</span>}
</DateFormatter>
</div>
<p className="little-spacer-bottom">{news.data.title}</p>
</div>
<ChevronRightIcon className="flex-0" />
</a>
);
}
}

const mapStateToProps = (state: Store): StateProps => {
const accessToken = getGlobalSettingValue(state, 'sonar.prismic.accessToken');
return {
accessToken: accessToken && accessToken.value
};
};

export default connect(mapStateToProps)(ProductNewsMenuItem);

+ 48
- 0
server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/ProductNewsMenuItem-test.tsx View File

@@ -0,0 +1,48 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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.
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { fetchPrismicNews, fetchPrismicRefs } from '../../../../api/news';
import { ProductNewsMenuItem } from '../ProductNewsMenuItem';

jest.mock('../../../../api/news', () => ({
fetchPrismicRefs: jest.fn().mockResolvedValue({ id: 'master', ref: 'master-ref' }),
fetchPrismicNews: jest.fn().mockResolvedValue([
{
data: { title: 'My Product News' },
last_publication_date: '2018-04-06T12:07:19+0000',
uid: 'my-product-news'
}
])
}));

it('should load the product news', async () => {
const wrapper = shallow(<ProductNewsMenuItem accessToken="token" tag="SonarCloud" />);
expect(wrapper).toMatchSnapshot();
await waitAndUpdate(wrapper);
expect(fetchPrismicRefs).toHaveBeenCalled();
expect(fetchPrismicNews).toHaveBeenCalledWith({
accessToken: 'token',
ref: 'master-ref',
tag: 'SonarCloud'
});
expect(wrapper).toMatchSnapshot();
});

+ 97
- 0
server/sonar-web/src/main/js/app/components/embed-docs-modal/__tests__/__snapshots__/ProductNewsMenuItem-test.tsx.snap View File

@@ -0,0 +1,97 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should load the product news 1`] = `
<a
className="rich-item new-loading"
>
<div
className="flex-1"
>
<div
className="display-inline-flex-center"
>
<h4>
embed_docs.latest_blog
</h4>
<span
className="note spacer-left"
>
<PlaceholderBar
color="#aaa"
width={60}
/>
</span>
</div>
<p
className="little-spacer-bottom"
>
<PlaceholderBar
color="#aaa"
width={84}
/>
<PlaceholderBar
color="#aaa"
width={48}
/>
<PlaceholderBar
color="#aaa"
width={24}
/>
<PlaceholderBar
color="#aaa"
width={72}
/>
<PlaceholderBar
color="#aaa"
width={24}
/>
<PlaceholderBar
color="#aaa"
width={48}
/>
</p>
</div>
<ChevronRightIcon
className="flex-0"
/>
</a>
`;

exports[`should load the product news 2`] = `
<a
className="rich-item"
href="https://blog.sonarsource.com/my-product-news"
rel="noopener noreferrer"
target="_blank"
>
<div
className="flex-1"
>
<div
className="display-inline-flex-center"
>
<h4>
embed_docs.latest_blog
</h4>
<DateFormatter
date="2018-04-06T12:07:19+0000"
>
<Component />
</DateFormatter>
</div>
<p
className="little-spacer-bottom"
>
My Product News
</p>
</div>
<ChevronRightIcon
className="flex-0"
/>
</a>
`;

+ 0
- 5
server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx View File

@@ -57,11 +57,6 @@ export function BranchLikeNavigation(props: BranchLikeNavigationProps) {
/>
);

// Main branch hasn't been analyzed yet && (CE || (DE+ && only one branch))
if (!component.analysisDate && (!branchesEnabled || !hasManyBranches)) {
return null;
}

return (
<span
className={classNames('big-spacer-left flex-0 branch-like-navigation-toggler-container', {

+ 1
- 18
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/BranchLikeNavigation-test.tsx View File

@@ -30,23 +30,6 @@ it('should render correctly', () => {
expect(wrapper).toMatchSnapshot();
});

it('should not render', () => {
// CE && main branch not analyzed yet
const wrapper = shallowRender({
appState: mockAppState({ branchesEnabled: false }),
component: mockComponent({ analysisDate: undefined })
});
expect(wrapper.type()).toBeNull();

// DE+ && main branch not analyzed yet && no other branches
const wrapper1 = shallowRender({
appState: mockAppState({ branchesEnabled: true }),
component: mockComponent({ analysisDate: undefined }),
branchLikes: []
});
expect(wrapper1.type()).toBeNull();
});

it('should render the menu trigger if branches are enabled', () => {
const wrapper = shallowRender({ appState: mockAppState({ branchesEnabled: true }) });
expect(wrapper).toMatchSnapshot();
@@ -84,7 +67,7 @@ function shallowRender(props?: Partial<BranchLikeNavigationProps>) {
<BranchLikeNavigation
appState={mockAppState()}
branchLikes={branchLikes}
component={mockComponent({ analysisDate: '2021-01-01 01:01:01' })}
component={mockComponent()}
currentBranchLike={branchLikes[0]}
{...props}
/>

+ 0
- 3
server/sonar-web/src/main/js/app/components/nav/component/branch-like/__tests__/__snapshots__/BranchLikeNavigation-test.tsx.snap View File

@@ -8,7 +8,6 @@ exports[`should render correctly 1`] = `
branchesEnabled={false}
component={
Object {
"analysisDate": "2021-01-01 01:01:01",
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
@@ -118,7 +117,6 @@ exports[`should render the menu trigger if branches are enabled 1`] = `
}
component={
Object {
"analysisDate": "2021-01-01 01:01:01",
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
@@ -160,7 +158,6 @@ exports[`should render the menu trigger if branches are enabled 1`] = `
branchesEnabled={true}
component={
Object {
"analysisDate": "2021-01-01 01:01:01",
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",

+ 93
- 0
server/sonar-web/src/main/js/app/components/notifications/NavLatestNotification.tsx View File

@@ -0,0 +1,93 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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.
*/
import * as differenceInSeconds from 'date-fns/difference_in_seconds';
import * as React from 'react';
import ClearIcon from 'sonar-ui-common/components/icons/ClearIcon';
import NotificationIcon from 'sonar-ui-common/components/icons/NotificationIcon';
import { parseDate } from 'sonar-ui-common/helpers/dates';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { PrismicFeatureNews } from '../../../api/news';
import './notifications.css';

interface Props {
lastNews: PrismicFeatureNews;
notificationsLastReadDate?: Date;
notificationsOptOut?: boolean;
onClick: () => void;
setCurrentUserSetting: (setting: T.CurrentUserSetting) => void;
}

export default class NavLatestNotification extends React.PureComponent<Props> {
mounted = false;

checkHasUnread = () => {
const { notificationsLastReadDate, lastNews } = this.props;
return (
!notificationsLastReadDate ||
differenceInSeconds(parseDate(lastNews.publicationDate), notificationsLastReadDate) > 0
);
};

handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
this.props.onClick();
};

handleDismiss = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.stopPropagation();

this.props.setCurrentUserSetting({
key: 'notifications.readDate',
value: Date.now().toString()
});
};

render() {
const { notificationsOptOut, lastNews } = this.props;
const hasUnread = this.checkHasUnread();
const showNotifications = Boolean(!notificationsOptOut && lastNews && hasUnread);
return (
<>
{showNotifications && (
<>
<li className="navbar-latest-notification" onClick={this.props.onClick}>
<div className="navbar-latest-notification-wrapper">
<span className="badge badge-info">{translate('new')}</span>
<span className="label">{lastNews.notification}</span>
</div>
</li>
<li className="navbar-latest-notification-dismiss">
<a className="navbar-icon" href="#" onClick={this.handleDismiss}>
<ClearIcon size={12} thin={true} />
</a>
</li>
</>
)}
<li>
<a className="navbar-icon" href="#" onClick={this.handleClick}>
<NotificationIcon hasUnread={hasUnread && !notificationsOptOut} />
</a>
</li>
</>
);
}
}

+ 134
- 0
server/sonar-web/src/main/js/app/components/notifications/NotificationsSidebar.tsx View File

@@ -0,0 +1,134 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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.
*/
import * as classNames from 'classnames';
import * as differenceInSeconds from 'date-fns/difference_in_seconds';
import * as React from 'react';
import { ClearButton } from 'sonar-ui-common/components/controls/buttons';
import Modal from 'sonar-ui-common/components/controls/Modal';
import DateFormatter from 'sonar-ui-common/components/intl/DateFormatter';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { PrismicFeatureNews } from '../../../api/news';

export interface Props {
fetchMoreFeatureNews: () => void;
loading: boolean;
loadingMore: boolean;
news: PrismicFeatureNews[];
onClose: () => void;
notificationsLastReadDate?: Date;
paging?: T.Paging;
}

export default function NotificationsSidebar(props: Props) {
const { loading, loadingMore, news, notificationsLastReadDate, paging } = props;
const header = translate('embed_docs.whats_new');
return (
<Modal contentLabel={header} onRequestClose={props.onClose}>
<div className="notifications-sidebar">
<div className="notifications-sidebar-top">
<h3>{header}</h3>
<ClearButton
className="button-tiny"
iconProps={{ size: 12, thin: true }}
onClick={props.onClose}
/>
</div>
<div className="notifications-sidebar-content">
{loading ? (
<div className="text-center">
<DeferredSpinner className="big-spacer-top" timeout={200} />
</div>
) : (
news.map((slice, index) => (
<Notification
key={slice.publicationDate}
notification={slice}
unread={isUnread(index, slice.publicationDate, notificationsLastReadDate)}
/>
))
)}
</div>
{!loading && paging && paging.total > news.length && (
<div className="notifications-sidebar-footer">
<div className="spacer-top note text-center">
<a className="spacer-left" href="#" onClick={props.fetchMoreFeatureNews}>
{translate('show_more')}
</a>
{loadingMore && (
<DeferredSpinner className="text-bottom spacer-left position-absolute" />
)}
</div>
</div>
)}
</div>
</Modal>
);
}

export function isUnread(index: number, notificationDate: string, lastReadDate?: Date) {
return !lastReadDate ? index < 1 : differenceInSeconds(notificationDate, lastReadDate) > 0;
}

interface NotificationProps {
notification: PrismicFeatureNews;
unread: boolean;
}

export function Notification({ notification, unread }: NotificationProps) {
return (
<div className={classNames('notifications-sidebar-slice', { unread })}>
<h4>
<DateFormatter date={notification.publicationDate} long={false} />
</h4>
{notification.features.map((feature, index) => (
<Feature feature={feature} key={index} />
))}
</div>
);
}

interface FeatureProps {
feature: PrismicFeatureNews['features'][0];
}

export function Feature({ feature }: FeatureProps) {
return (
<div className="feature">
<ul className="categories spacer-bottom">
{feature.categories.map(category => (
<li key={category.name} style={{ backgroundColor: category.color }}>
{category.name}
</li>
))}
</ul>
<span>{feature.description}</span>
{feature.readMore && (
<a
className="learn-more"
href={feature.readMore}
rel="noopener noreferrer nofollow"
target="_blank">
{translate('learn_more')}
</a>
)}
</div>
);
}

+ 67
- 0
server/sonar-web/src/main/js/app/components/notifications/__tests__/NavLatestNotification-test.tsx View File

@@ -0,0 +1,67 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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.
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { parseDate } from 'sonar-ui-common/helpers/dates';
import { PrismicFeatureNews } from '../../../../api/news';
import NavLatestNotification from '../NavLatestNotification';

it('should render correctly if there are new features, and the user has not opted out', () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('.navbar-latest-notification')).toHaveLength(1);
});

it('should render correctly if there are new features, but the user has opted out', () => {
const wrapper = shallowRender({ notificationsOptOut: true });
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('.navbar-latest-notification')).toHaveLength(0);
});

it('should render correctly if there are no new unread features', () => {
const wrapper = shallowRender({
notificationsLastReadDate: parseDate('2018-12-31T12:07:19+0000')
});
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('.navbar-latest-notification')).toHaveLength(0);
});

function shallowRender(props: Partial<NavLatestNotification['props']> = {}) {
const lastNews: PrismicFeatureNews = {
notification: '10 Java rules, Github checks, Security Hotspots, BitBucket branch decoration',
publicationDate: '2018-04-06',
features: [
{
categories: [{ color: '#ff0000', name: 'Java' }],
description: '10 new Java rules'
}
]
};
return shallow(
<NavLatestNotification
lastNews={lastNews}
notificationsLastReadDate={parseDate('2018-01-01T12:07:19+0000')}
notificationsOptOut={false}
onClick={jest.fn()}
setCurrentUserSetting={jest.fn()}
{...props}
/>
);
}

+ 119
- 0
server/sonar-web/src/main/js/app/components/notifications/__tests__/NotificationsSidebar-test.tsx View File

@@ -0,0 +1,119 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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.
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { parseDate } from 'sonar-ui-common/helpers/dates';
import NotificationsSidebar, {
Feature,
isUnread,
Notification,
Props
} from '../NotificationsSidebar';

const news: Props['news'] = [
{
notification: '10 Java rules, Github checks, Security Hotspots, BitBucket branch decoration',
publicationDate: '2018-04-06',
features: [
{
categories: [
{ color: '#ff0000', name: 'Java' },
{ color: '#00ff00', name: 'Rules' }
],
description: '10 new Java rules'
},
{
categories: [{ color: '#0000ff', name: 'BitBucket' }],
description: 'BitBucket branch decoration',
readMore: 'http://example.com'
}
]
},
{
notification: 'Some other notification',
publicationDate: '2018-04-05',
features: [
{
categories: [{ color: '#0000ff', name: 'BitBucket' }],
description: 'BitBucket branch decoration',
readMore: 'http://example.com'
}
]
}
];

describe('#NotificationSidebar', () => {
it('should render correctly if there are new features', () => {
const wrapper = shallowRender({ loading: true });
expect(wrapper).toMatchSnapshot();
wrapper.setProps({ loading: false });
expect(wrapper).toMatchSnapshot();
expect(wrapper.find('Notification')).toHaveLength(2);
});

it('should render correctly if there are no new unread features', () => {
const wrapper = shallowRender({
notificationsLastReadDate: parseDate('2018-12-31')
});
expect(wrapper.find('Notification')).toHaveLength(2);
expect(wrapper.find('Notification[unread=true]')).toHaveLength(0);
});
});

describe('#isUnread', () => {
it('should be unread', () => {
expect(isUnread(0, '2018-12-14', undefined)).toBe(true);
expect(isUnread(1, '2018-12-14', parseDate('2018-12-12'))).toBe(true);
});

it('should be read', () => {
expect(isUnread(0, '2018-12-16', parseDate('2018-12-16'))).toBe(false);
expect(isUnread(1, '2018-12-15', undefined)).toBe(false);
});
});

describe('#Notification', () => {
it('should render correctly', () => {
expect(shallow(<Notification notification={news[1]} unread={false} />)).toMatchSnapshot();
expect(shallow(<Notification notification={news[1]} unread={true} />)).toMatchSnapshot();
});
});

describe('#Feature', () => {
it('should render correctly', () => {
expect(shallow(<Feature feature={news[1].features[0]} />)).toMatchSnapshot();
expect(shallow(<Feature feature={news[0].features[0]} />)).toMatchSnapshot();
});
});

function shallowRender(props: Partial<Props> = {}) {
return shallow(
<NotificationsSidebar
fetchMoreFeatureNews={jest.fn()}
loading={false}
loadingMore={false}
news={news}
notificationsLastReadDate={parseDate('2018-01-01')}
onClose={jest.fn()}
paging={{ pageIndex: 1, pageSize: 10, total: 20 }}
{...props}
/>
);
}

+ 82
- 0
server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NavLatestNotification-test.tsx.snap View File

@@ -0,0 +1,82 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly if there are new features, and the user has not opted out 1`] = `
<Fragment>
<li
className="navbar-latest-notification"
onClick={[MockFunction]}
>
<div
className="navbar-latest-notification-wrapper"
>
<span
className="badge badge-info"
>
new
</span>
<span
className="label"
>
10 Java rules, Github checks, Security Hotspots, BitBucket branch decoration
</span>
</div>
</li>
<li
className="navbar-latest-notification-dismiss"
>
<a
className="navbar-icon"
href="#"
onClick={[Function]}
>
<ClearIcon
size={12}
thin={true}
/>
</a>
</li>
<li>
<a
className="navbar-icon"
href="#"
onClick={[Function]}
>
<NotificationIcon
hasUnread={true}
/>
</a>
</li>
</Fragment>
`;

exports[`should render correctly if there are new features, but the user has opted out 1`] = `
<Fragment>
<li>
<a
className="navbar-icon"
href="#"
onClick={[Function]}
>
<NotificationIcon
hasUnread={false}
/>
</a>
</li>
</Fragment>
`;

exports[`should render correctly if there are no new unread features 1`] = `
<Fragment>
<li>
<a
className="navbar-icon"
href="#"
onClick={[Function]}
>
<NotificationIcon
hasUnread={false}
/>
</a>
</li>
</Fragment>
`;

+ 269
- 0
server/sonar-web/src/main/js/app/components/notifications/__tests__/__snapshots__/NotificationsSidebar-test.tsx.snap View File

@@ -0,0 +1,269 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`#Feature should render correctly 1`] = `
<div
className="feature"
>
<ul
className="categories spacer-bottom"
>
<li
key="BitBucket"
style={
Object {
"backgroundColor": "#0000ff",
}
}
>
BitBucket
</li>
</ul>
<span>
BitBucket branch decoration
</span>
<a
className="learn-more"
href="http://example.com"
rel="noopener noreferrer nofollow"
target="_blank"
>
learn_more
</a>
</div>
`;

exports[`#Feature should render correctly 2`] = `
<div
className="feature"
>
<ul
className="categories spacer-bottom"
>
<li
key="Java"
style={
Object {
"backgroundColor": "#ff0000",
}
}
>
Java
</li>
<li
key="Rules"
style={
Object {
"backgroundColor": "#00ff00",
}
}
>
Rules
</li>
</ul>
<span>
10 new Java rules
</span>
</div>
`;

exports[`#Notification should render correctly 1`] = `
<div
className="notifications-sidebar-slice"
>
<h4>
<DateFormatter
date="2018-04-05"
long={false}
/>
</h4>
<Feature
feature={
Object {
"categories": Array [
Object {
"color": "#0000ff",
"name": "BitBucket",
},
],
"description": "BitBucket branch decoration",
"readMore": "http://example.com",
}
}
key="0"
/>
</div>
`;

exports[`#Notification should render correctly 2`] = `
<div
className="notifications-sidebar-slice unread"
>
<h4>
<DateFormatter
date="2018-04-05"
long={false}
/>
</h4>
<Feature
feature={
Object {
"categories": Array [
Object {
"color": "#0000ff",
"name": "BitBucket",
},
],
"description": "BitBucket branch decoration",
"readMore": "http://example.com",
}
}
key="0"
/>
</div>
`;

exports[`#NotificationSidebar should render correctly if there are new features 1`] = `
<Modal
contentLabel="embed_docs.whats_new"
onRequestClose={[MockFunction]}
>
<div
className="notifications-sidebar"
>
<div
className="notifications-sidebar-top"
>
<h3>
embed_docs.whats_new
</h3>
<ClearButton
className="button-tiny"
iconProps={
Object {
"size": 12,
"thin": true,
}
}
onClick={[MockFunction]}
/>
</div>
<div
className="notifications-sidebar-content"
>
<div
className="text-center"
>
<DeferredSpinner
className="big-spacer-top"
timeout={200}
/>
</div>
</div>
</div>
</Modal>
`;

exports[`#NotificationSidebar should render correctly if there are new features 2`] = `
<Modal
contentLabel="embed_docs.whats_new"
onRequestClose={[MockFunction]}
>
<div
className="notifications-sidebar"
>
<div
className="notifications-sidebar-top"
>
<h3>
embed_docs.whats_new
</h3>
<ClearButton
className="button-tiny"
iconProps={
Object {
"size": 12,
"thin": true,
}
}
onClick={[MockFunction]}
/>
</div>
<div
className="notifications-sidebar-content"
>
<Notification
key="2018-04-06"
notification={
Object {
"features": Array [
Object {
"categories": Array [
Object {
"color": "#ff0000",
"name": "Java",
},
Object {
"color": "#00ff00",
"name": "Rules",
},
],
"description": "10 new Java rules",
},
Object {
"categories": Array [
Object {
"color": "#0000ff",
"name": "BitBucket",
},
],
"description": "BitBucket branch decoration",
"readMore": "http://example.com",
},
],
"notification": "10 Java rules, Github checks, Security Hotspots, BitBucket branch decoration",
"publicationDate": "2018-04-06",
}
}
unread={true}
/>
<Notification
key="2018-04-05"
notification={
Object {
"features": Array [
Object {
"categories": Array [
Object {
"color": "#0000ff",
"name": "BitBucket",
},
],
"description": "BitBucket branch decoration",
"readMore": "http://example.com",
},
],
"notification": "Some other notification",
"publicationDate": "2018-04-05",
}
}
unread={true}
/>
</div>
<div
className="notifications-sidebar-footer"
>
<div
className="spacer-top note text-center"
>
<a
className="spacer-left"
href="#"
onClick={[MockFunction]}
>
show_more
</a>
</div>
</div>
</div>
</Modal>
`;

+ 157
- 0
server/sonar-web/src/main/js/app/components/notifications/notifications.css View File

@@ -0,0 +1,157 @@
/*
* SonarQube
* Copyright (C) 2009-2021 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.
*/
.navbar-latest-notification {
flex: 0 1 240px;
text-align: right;
overflow: hidden;
}

.navbar-latest-notification-wrapper {
position: relative;
display: inline-block;
padding: var(--gridSize);
padding-left: 50px;
height: 28px;
max-width: 100%;
box-sizing: border-box;
overflow: hidden;
vertical-align: middle;
font-size: var(--smallFontSize);
text-overflow: ellipsis;
white-space: nowrap;
color: var(--sonarcloudBlack500);
background-color: #000;
border-radius: 3px 0 0 3px;
cursor: pointer;
}

.navbar-latest-notification-wrapper:hover {
color: var(--sonarcloudBlack300);
}

.navbar-latest-notification-wrapper .badge-info {
position: absolute;
margin-right: var(--gridSize);
left: 6px;
top: 6px;
}

.navbar-latest-notification-wrapper .label {
display: block;
max-width: 330px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.navbar-latest-notification-dismiss .navbar-icon {
height: 28px;
background-color: #000;
border-radius: 0 3px 3px 0;
padding: var(--gridSize) 7px !important;
margin-left: 1px;
margin-right: var(--gridSize);
color: var(--sonarcloudBlack500) !important;
}

.navbar-latest-notification-dismiss .navbar-icon:hover {
color: var(--sonarcloudBlack300) !important;
}

.notifications-sidebar {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 400px;
display: flex;
flex-direction: column;
background: var(--sonarcloudBlack200);
}

.notifications-sidebar-top {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
padding: calc(2 * var(--gridSize));
border-bottom: 1px solid var(--sonarcloudBlack250);
background-color: var(--sonarcloudBlack100);
}

.notifications-sidebar-top h3 {
font-weight: normal;
font-size: var(--bigFontSize);
}

.notifications-sidebar-content {
flex: 1 1;
overflow-y: auto;
}

.notifications-sidebar-footer {
padding-top: var(--gridSize);
border-top: 1px solid var(--sonarcloudBlack250);
flex: 0 0 40px;
}

.notifications-sidebar-slice h4 {
padding: calc(2 * var(--gridSize));
padding-bottom: calc(var(--gridSize) / 2);
background-color: var(--sonarcloudBlack200);
font-weight: normal;
font-size: var(--smallFontSize);
text-align: right;
color: var(--sonarcloudBlack500);
}

.notifications-sidebar-slice .feature:last-of-type {
border-bottom: 1px solid var(--sonarcloudBlack250);
}

.notifications-sidebar-slice .feature {
padding: calc(2 * var(--gridSize));
background-color: var(--sonarcloudBlack100);
border-top: 1px solid var(--sonarcloudBlack250);
overflow: hidden;
}

.notifications-sidebar-slice.unread .feature {
background-color: #e6f6ff;
border-color: #cee4f2;
}

.notifications-sidebar-slice .learn-more {
clear: both;
float: right;
margin-top: var(--gridSize);
}

.notifications-sidebar-slice .categories li {
display: inline-block;
padding: 4px;
margin-right: var(--gridSize);
font-size: 9px;
line-height: 8px;
text-transform: uppercase;
font-weight: bold;
color: #fff;
border-radius: 3px;
}

+ 3
- 7
server/sonar-web/src/main/js/apps/projectBaseline/components/App.tsx View File

@@ -34,7 +34,6 @@ import BranchList from './BranchList';
import ProjectBaselineSelector from './ProjectBaselineSelector';

interface Props {
branchLike: Branch;
branchLikes: BranchLike[];
branchesEnabled?: boolean;
canAdmin?: boolean;
@@ -121,15 +120,13 @@ export default class App extends React.PureComponent<Props, State> {
}

fetchLeakPeriodSetting() {
const { branchLike, branchesEnabled, component } = this.props;

this.setState({ loading: true });

Promise.all([
getNewCodePeriod(),
getNewCodePeriod({
branch: branchesEnabled ? undefined : branchLike.name,
project: component.key
branch: !this.props.branchesEnabled ? 'master' : undefined,
project: this.props.component.key
})
]).then(
([generalSetting, setting]) => {
@@ -229,7 +226,7 @@ export default class App extends React.PureComponent<Props, State> {
};

render() {
const { branchesEnabled, canAdmin, component, branchLike } = this.props;
const { branchesEnabled, canAdmin, component } = this.props;
const {
analysis,
branchList,
@@ -259,7 +256,6 @@ export default class App extends React.PureComponent<Props, State> {
{generalSetting && overrideGeneralSetting !== undefined && (
<ProjectBaselineSelector
analysis={analysis}
branch={branchLike}
branchList={branchList}
branchesEnabled={branchesEnabled}
component={component.key}

+ 1
- 1
server/sonar-web/src/main/js/apps/projectBaseline/components/BranchAnalysisList.tsx View File

@@ -27,7 +27,7 @@ import BranchAnalysisListRenderer from './BranchAnalysisListRenderer';

interface Props {
analysis: string;
branch?: string;
branch: string;
component: string;
onSelectAnalysis: (analysis: T.ParsedAnalysis) => void;
}

+ 1
- 3
server/sonar-web/src/main/js/apps/projectBaseline/components/ProjectBaselineSelector.tsx View File

@@ -34,7 +34,6 @@ import BranchAnalysisList from './BranchAnalysisList';

export interface ProjectBaselineSelectorProps {
analysis?: string;
branch: Branch;
branchList: Branch[];
branchesEnabled?: boolean;
component: string;
@@ -83,7 +82,6 @@ function branchToOption(b: Branch) {
export default function ProjectBaselineSelector(props: ProjectBaselineSelectorProps) {
const {
analysis,
branch,
branchList,
branchesEnabled,
component,
@@ -165,7 +163,7 @@ export default function ProjectBaselineSelector(props: ProjectBaselineSelectorPr
{selected === 'SPECIFIC_ANALYSIS' && (
<BranchAnalysisList
analysis={analysis || ''}
branch={branch.name}
branch="master"
component={component}
onSelectAnalysis={props.onSelectAnalysis}
/>

+ 2
- 9
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/App-test.tsx View File

@@ -35,14 +35,8 @@ jest.mock('../../../../api/newCodePeriod', () => ({
setNewCodePeriod: jest.fn().mockResolvedValue({})
}));

it('should render correctly', async () => {
let wrapper = shallowRender();
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();

wrapper = shallowRender({ branchesEnabled: false });
await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot('without branch support');
it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot();
});

it('should initialize correctly', async () => {
@@ -106,7 +100,6 @@ it('should handle errors gracefully', async () => {
function shallowRender(props: Partial<App['props']> = {}) {
return shallow<App>(
<App
branchLike={mockBranch()}
branchLikes={[mockMainBranch()]}
branchesEnabled={true}
canAdmin={true}

+ 1
- 2
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/ProjectBaselineSelector-test.tsx View File

@@ -19,7 +19,7 @@
*/
import { shallow } from 'enzyme';
import * as React from 'react';
import { mockBranch, mockMainBranch } from '../../../../helpers/mocks/branch-like';
import { mockMainBranch } from '../../../../helpers/mocks/branch-like';
import ProjectBaselineSelector, { ProjectBaselineSelectorProps } from '../ProjectBaselineSelector';

it('should render correctly', () => {
@@ -105,7 +105,6 @@ it('should disable the save button when date is invalid', () => {
function shallowRender(props: Partial<ProjectBaselineSelectorProps> = {}) {
return shallow(
<ProjectBaselineSelector
branch={mockBranch()}
branchList={[mockMainBranch()]}
branchesEnabled={true}
component=""

+ 1
- 180
server/sonar-web/src/main/js/apps/projectBaseline/components/__tests__/__snapshots__/App-test.tsx.snap View File

@@ -11,186 +11,7 @@ exports[`should render correctly 1`] = `
<AppHeader
canAdmin={true}
/>
<div
className="panel-white project-baseline"
>
<h2>
project_baseline.default_setting
</h2>
<ProjectBaselineSelector
analysis=""
branch={
Object {
"analysisDate": "2018-01-01",
"excludedFromPurge": true,
"isMain": false,
"name": "branch-6.7",
}
}
branchList={
Array [
Object {
"analysisDate": "2018-01-01",
"excludedFromPurge": true,
"isMain": true,
"name": "master",
},
]
}
branchesEnabled={true}
component="my-project"
currentSetting="PREVIOUS_VERSION"
days="30"
generalSetting={
Object {
"type": "PREVIOUS_VERSION",
}
}
onCancel={[Function]}
onSelectAnalysis={[Function]}
onSelectDays={[Function]}
onSelectReferenceBranch={[Function]}
onSelectSetting={[Function]}
onSubmit={[Function]}
onToggleSpecificSetting={[Function]}
overrideGeneralSetting={true}
referenceBranch="master"
saving={false}
selected="PREVIOUS_VERSION"
/>
<div
className="spacer-top invisible"
>
<span
className="text-success"
>
<AlertSuccessIcon
className="spacer-right"
/>
settings.state.saved
</span>
</div>
<div
className="huge-spacer-top branch-baseline-selector"
>
<hr />
<h2>
project_baseline.configure_branches
</h2>
<BranchList
branchList={
Array [
Object {
"analysisDate": "2018-01-01",
"excludedFromPurge": true,
"isMain": true,
"name": "master",
},
]
}
component={
Object {
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
"qualifier": "TRK",
"qualityGate": Object {
"isDefault": true,
"key": "30",
"name": "Sonar way",
},
"qualityProfiles": Array [
Object {
"deleted": false,
"key": "my-qp",
"language": "ts",
"name": "Sonar way",
},
],
"tags": Array [],
}
}
inheritedSetting={
Object {
"type": "PREVIOUS_VERSION",
"value": undefined,
}
}
/>
</div>
</div>
</div>
</Fragment>
`;

exports[`should render correctly: without branch support 1`] = `
<Fragment>
<Suggestions
suggestions="project_baseline"
/>
<div
className="page page-limited"
>
<AppHeader
canAdmin={true}
/>
<div
className="panel-white project-baseline"
>
<ProjectBaselineSelector
analysis=""
branch={
Object {
"analysisDate": "2018-01-01",
"excludedFromPurge": true,
"isMain": false,
"name": "branch-6.7",
}
}
branchList={
Array [
Object {
"analysisDate": "2018-01-01",
"excludedFromPurge": true,
"isMain": true,
"name": "master",
},
]
}
branchesEnabled={false}
component="my-project"
currentSetting="PREVIOUS_VERSION"
days="30"
generalSetting={
Object {
"type": "PREVIOUS_VERSION",
}
}
onCancel={[Function]}
onSelectAnalysis={[Function]}
onSelectDays={[Function]}
onSelectReferenceBranch={[Function]}
onSelectSetting={[Function]}
onSubmit={[Function]}
onToggleSpecificSetting={[Function]}
overrideGeneralSetting={true}
referenceBranch="master"
saving={false}
selected="PREVIOUS_VERSION"
/>
<div
className="spacer-top invisible"
>
<span
className="text-success"
>
<AlertSuccessIcon
className="spacer-right"
/>
settings.state.saved
</span>
</div>
</div>
<DeferredSpinner />
</div>
</Fragment>
`;

+ 0
- 11
sonar-plugin-api/src/main/java/org/sonar/api/batch/scm/ScmProvider.java View File

@@ -67,17 +67,6 @@ public abstract class ScmProvider {
return null;
}

/**
* Return the main branch name.
*
* @return null if the SCM provider was not able to find the main branch.
* @since 8.8
*/
@CheckForNull
public String getMainBranch(Path rootBaseDir) {
return null;
}

/**
* Return a map between paths given as argument and the corresponding line numbers which are new compared to the provided target branch.
* If nothing is returned for a file, the scanner will consider that the provider was unable to determine changes for that file and it will

+ 2
- 20
sonar-scanner-engine/src/main/java/org/sonar/scanner/report/MetadataPublisher.java View File

@@ -61,9 +61,6 @@ public class MetadataPublisher implements ReportPublisherStep {
private final InputComponentStore componentStore;
private final ScmConfiguration scmConfiguration;

private ScmProvider scmProvider;
private Path projectBasedir;

public MetadataPublisher(ProjectInfo projectInfo, InputModuleHierarchy moduleHierarchy, QualityProfiles qProfiles,
CpdSettings cpdSettings, ScannerPluginRepository pluginRepository, BranchConfiguration branchConfiguration,
ScmRevision scmRevision, ForkDateSupplier forkDateSupplier, InputComponentStore componentStore, ScmConfiguration scmConfiguration) {
@@ -112,28 +109,11 @@ public class MetadataPublisher implements ReportPublisherStep {
.setUpdatedAt(pluginEntry.getValue().getUpdatedAt()).build());
}

scmProvider = scmConfiguration.provider();
projectBasedir = moduleHierarchy.root().getBaseDir();

addModulesRelativePaths(builder);
addMainBranch(builder);

writer.writeMetadata(builder.build());
}

private void addMainBranch(ScannerReport.Metadata.Builder builder) {
if (scmProvider == null) {
return;
}
String mainBranch = scmProvider.getMainBranch(projectBasedir);
if (mainBranch != null && !mainBranch.isEmpty()) {
LOG.debug("The main branch for '{}' is '{}'", projectBasedir.toString(), mainBranch);
builder.setGitDefaultMainBranch(mainBranch);
} else {
LOG.debug("The main branch for '{}' has not been found", projectBasedir.toString());
}
}

private void addForkPoint(ScannerReport.Metadata.Builder builder) {
Instant date = forkDateSupplier.get();
if (date != null) {
@@ -154,10 +134,12 @@ public class MetadataPublisher implements ReportPublisherStep {
}
}

ScmProvider scmProvider = scmConfiguration.provider();
if (scmProvider == null) {
return;
}

Path projectBasedir = moduleHierarchy.root().getBaseDir();
try {
builder.setRelativePathFromScmRoot(toSonarQubePath(scmProvider.relativePathFromScmRoot(projectBasedir)));
} catch (UnsupportedOperationException e) {

+ 0
- 15
sonar-scanner-engine/src/main/java/org/sonar/scm/git/GitScmProvider.java View File

@@ -97,21 +97,6 @@ public class GitScmProvider extends ScmProvider {
return this.jgitBlameCommand;
}

@CheckForNull
@Override
public String getMainBranch(Path rootBaseDir) {
try (Repository repo = buildRepo(rootBaseDir)) {
Set<String> branches = repo.getConfig().getSubsections("branch");
if(!branches.isEmpty()) {
// .git/config file will have the default branch at the time of cloning as its first branch
return branches.iterator().next();
}
} catch (IOException e) {
LOG.debug("Couldn't build a repo in order to retrieve the default branch name", e);
}
return null;
}

@CheckForNull
@Override
public Set<Path> branchChangedFiles(String targetBranchName, Path rootBaseDir) {

+ 0
- 45
sonar-scanner-engine/src/test/java/org/sonar/scanner/report/MetadataPublisherTest.java View File

@@ -286,49 +286,4 @@ public class MetadataPublisherTest {
ScannerReport.Metadata metadata = reader.readMetadata();
assertThat(metadata.getRelativePathFromScmRoot()).isEmpty();
}

@Test
public void addMainBranch_givenDefaultMainBranchSet_writeItToMetadata() throws IOException {
ScmProvider scmProvider = mock(ScmProvider.class);
when(scmProvider.getMainBranch(any())).thenReturn("Main");
when(scmProvider.relativePathFromScmRoot(any())).thenReturn(mock(Path.class));
when(scmConfiguration.provider()).thenReturn(scmProvider);

File outputDir = temp.newFolder();
underTest.publish(new ScannerReportWriter(outputDir));

ScannerReportReader reader = new ScannerReportReader(outputDir);
ScannerReport.Metadata metadata = reader.readMetadata();
assertThat(metadata.getGitDefaultMainBranch()).isEqualTo("Main");
}

@Test
public void addMainBranch_givenEmptyDefaultMainBranchSet_emptyDefaultMainBranchInMetadata() throws IOException {
ScmProvider scmProvider = mock(ScmProvider.class);
when(scmProvider.getMainBranch(any())).thenReturn("");
when(scmProvider.relativePathFromScmRoot(any())).thenReturn(mock(Path.class));
when(scmConfiguration.provider()).thenReturn(scmProvider);

File outputDir = temp.newFolder();
underTest.publish(new ScannerReportWriter(outputDir));

ScannerReportReader reader = new ScannerReportReader(outputDir);
ScannerReport.Metadata metadata = reader.readMetadata();
assertThat(metadata.getGitDefaultMainBranch()).isEmpty();
}

@Test
public void addMainBranch_givenNullMainBranchSet_emptyDefaultMainBranchInMetadata() throws IOException {
ScmProvider scmProvider = mock(ScmProvider.class);
when(scmProvider.getMainBranch(any())).thenReturn(null);
when(scmProvider.relativePathFromScmRoot(any())).thenReturn(mock(Path.class));
when(scmConfiguration.provider()).thenReturn(scmProvider);

File outputDir = temp.newFolder();
underTest.publish(new ScannerReportWriter(outputDir));

ScannerReportReader reader = new ScannerReportReader(outputDir);
ScannerReport.Metadata metadata = reader.readMetadata();
assertThat(metadata.getGitDefaultMainBranch()).isEmpty();
}
}

+ 0
- 130
sonar-scanner-engine/src/test/java/org/sonar/scm/git/GitScmProviderTest.java View File

@@ -39,19 +39,15 @@ import java.util.Random;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicInteger;

import org.eclipse.jgit.api.CreateBranchCommand;
import org.eclipse.jgit.api.DiffCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
import org.junit.Before;
import org.junit.Rule;
@@ -736,132 +732,6 @@ public class GitScmProviderTest {
assertThat(provider.revisionId(projectDir)).isNull();
}

@Test
public void getMainBranch_givenRepoWithOneBranchCalledMain_returnMainBranchCalledMain() throws Exception {
//given
worktree = temp.newFolder().toPath();
Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile());
repo.create();
git = new Git(repo);

addBranchInConfig("Main");

Path projectDir = worktree.resolve("project");
Files.createDirectory(projectDir);

GitScmProvider provider = newGitScmProvider();

//when
String mainBranch = provider.getMainBranch(projectDir);

//then
assertThat(mainBranch).isEqualTo("Main");
}

@Test
public void getMainBranch_givenRepoWithTwoBranches_returnFirstBranch() throws Exception {
//given
worktree = temp.newFolder().toPath();
Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile());
repo.create();
git = new Git(repo);

addBranchInConfig("First");
addBranchInConfig("Second");

Path projectDir = worktree.resolve("project");
Files.createDirectory(projectDir);

GitScmProvider provider = newGitScmProvider();

//when
String mainBranch = provider.getMainBranch(projectDir);

//then
assertThat(mainBranch).isEqualTo("First");
}

@Test
public void getMainBranch_givenNoBranches_dontThrowException() throws Exception {
//given
worktree = temp.newFolder().toPath();
Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile());
repo.create();
git = new Git(repo);

Path projectDir = worktree.resolve("project");
Files.createDirectory(projectDir);

GitScmProvider provider = newGitScmProvider();

//when
String mainBranch = provider.getMainBranch(projectDir);

//then no exception
assertThat(mainBranch).isNullOrEmpty();
}

@Test
public void getMainBranch_givenRepositoryNotFoundExceptionWhenBuildingRepo_returnNull() throws Exception {
//given

worktree = temp.newFolder().toPath();
Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile());
repo.create();
git = new Git(repo);
repo.getObjectDatabase().close(); //This is here to force RepositoryBuilder to throw subclass of IOException

Path projectDir = worktree.resolve("project");
Files.createDirectory(projectDir);

GitScmProvider provider = newGitScmProvider();

//when
String mainBranch = provider.getMainBranch(projectDir);

//then no exception
assertThat(mainBranch).isNullOrEmpty();
}

@Test
public void getMainBranch_givenIOExceptionWhenBuildingRepo_returnNull() throws Exception {
//given

worktree = temp.newFolder().toPath();
Repository repo = FileRepositoryBuilder.create(worktree.resolve(".git").toFile());
repo.create();
git = new Git(repo);

Path projectDir = worktree.resolve("project");
Files.createDirectory(projectDir);

GitScmProvider provider = new GitScmProvider(mockCommand(), analysisWarnings, gitIgnoreCommand, system2) {
@Override
Repository buildRepo(Path basedir) throws IOException {
throw new IOException();
}
};

//when
String mainBranch = provider.getMainBranch(projectDir);

//then no exception
assertThat(mainBranch).isNullOrEmpty();
}

/**
* Normally after cloning the repository we would have at least one
* branch it git config. This method adds these branches without
* cloning any repository (because unit tests ought to be fast)
*/
private void addBranchInConfig(String ... branches) throws IOException {
for(String branch : branches) {
git.getRepository().getConfig().setStringList("branch", branch, "remote", Arrays.asList("origin"));
git.getRepository().getConfig().setStringList("branch", branch, "merge", Arrays.asList("refs/head/" + branch));
}
git.getRepository().getConfig().save();
}

private String randomizedContent(String prefix, int numLines) {
StringBuilder sb = new StringBuilder();
for (int line = 0; line < numLines; line++) {

+ 0
- 2
sonar-scanner-protocol/src/main/protobuf/scanner_report.proto View File

@@ -60,8 +60,6 @@ message Metadata {

map<string, int32> not_analyzed_files_by_language = 20;

string gitDefaultMainBranch = 21;

message QProfile {
string key = 1;
string name = 2;

Loading…
Cancel
Save