PurgeDatastoresStep.class,
IndexAnalysisStep.class,
UpdateNeedIssueSyncStep.class,
- UpdateMainBranchStep.class,
// notifications are sent at the end, so that webapp displays up-to-date information
SendIssueNotificationsStep.class,
+++ /dev/null
-/*
- * 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.";
- }
-}
+++ /dev/null
-/*
- * 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);
- }
-}
[[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.
## 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.
* 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.
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'"
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.
--- /dev/null
+/*
+ * 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
+ }
+ }));
+}
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 {
);
}
+ 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>
{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>
);
--- /dev/null
+/*
+ * 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);
--- /dev/null
+/*
+ * 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();
+});
--- /dev/null
+// 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>
+`;
/>
);
- // 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', {
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();
<BranchLikeNavigation
appState={mockAppState()}
branchLikes={branchLikes}
- component={mockComponent({ analysisDate: '2021-01-01 01:01:01' })}
+ component={mockComponent()}
currentBranchLike={branchLikes[0]}
{...props}
/>
branchesEnabled={false}
component={
Object {
- "analysisDate": "2021-01-01 01:01:01",
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
}
component={
Object {
- "analysisDate": "2021-01-01 01:01:01",
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
branchesEnabled={true}
component={
Object {
- "analysisDate": "2021-01-01 01:01:01",
"breadcrumbs": Array [],
"key": "my-project",
"name": "MyProject",
--- /dev/null
+/*
+ * 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>
+ </>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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>
+ );
+}
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
--- /dev/null
+/*
+ * 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}
+ />
+ );
+}
--- /dev/null
+// 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>
+`;
--- /dev/null
+// 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>
+`;
--- /dev/null
+/*
+ * 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;
+}
import ProjectBaselineSelector from './ProjectBaselineSelector';
interface Props {
- branchLike: Branch;
branchLikes: BranchLike[];
branchesEnabled?: boolean;
canAdmin?: boolean;
}
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]) => {
};
render() {
- const { branchesEnabled, canAdmin, component, branchLike } = this.props;
+ const { branchesEnabled, canAdmin, component } = this.props;
const {
analysis,
branchList,
{generalSetting && overrideGeneralSetting !== undefined && (
<ProjectBaselineSelector
analysis={analysis}
- branch={branchLike}
branchList={branchList}
branchesEnabled={branchesEnabled}
component={component.key}
interface Props {
analysis: string;
- branch?: string;
+ branch: string;
component: string;
onSelectAnalysis: (analysis: T.ParsedAnalysis) => void;
}
export interface ProjectBaselineSelectorProps {
analysis?: string;
- branch: Branch;
branchList: Branch[];
branchesEnabled?: boolean;
component: string;
export default function ProjectBaselineSelector(props: ProjectBaselineSelectorProps) {
const {
analysis,
- branch,
branchList,
branchesEnabled,
component,
{selected === 'SPECIFIC_ANALYSIS' && (
<BranchAnalysisList
analysis={analysis || ''}
- branch={branch.name}
+ branch="master"
component={component}
onSelectAnalysis={props.onSelectAnalysis}
/>
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 () => {
function shallowRender(props: Partial<App['props']> = {}) {
return shallow<App>(
<App
- branchLike={mockBranch()}
branchLikes={[mockMainBranch()]}
branchesEnabled={true}
canAdmin={true}
*/
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', () => {
function shallowRender(props: Partial<ProjectBaselineSelectorProps> = {}) {
return shallow(
<ProjectBaselineSelector
- branch={mockBranch()}
branchList={[mockMainBranch()]}
branchesEnabled={true}
component=""
<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>
`;
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
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) {
.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) {
}
}
+ ScmProvider scmProvider = scmConfiguration.provider();
if (scmProvider == null) {
return;
}
+ Path projectBasedir = moduleHierarchy.root().getBaseDir();
try {
builder.setRelativePathFromScmRoot(toSonarQubePath(scmProvider.relativePathFromScmRoot(projectBasedir)));
} catch (UnsupportedOperationException e) {
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) {
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();
- }
}
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;
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++) {
map<string, int32> not_analyzed_files_by_language = 20;
- string gitDefaultMainBranch = 21;
-
message QProfile {
string key = 1;
string name = 2;