From 1d1954e62a4381e6d368873736e1cf8bb11d639f Mon Sep 17 00:00:00 2001 From: Wouter Admiraal Date: Mon, 10 Jul 2023 09:13:32 +0200 Subject: [PATCH] SONAR-19850 Add action to set branch as main branch --- server/sonar-web/src/main/js/api/branches.ts | 4 + .../main/js/api/mocks/BranchesServiceMock.ts | 63 +++++-------- .../src/main/js/api/mocks/data/branches.ts | 62 +++++++++++++ .../projectBranches/ProjectBranchesApp.tsx | 3 +- .../__tests__/ProjectBranchesApp-it.tsx | 52 ++++++++++- .../components/BranchLikeRow.tsx | 7 ++ .../components/BranchLikeTable.tsx | 2 + .../components/BranchLikeTabs.tsx | 20 ++++- .../components/BranchPurgeSetting.tsx | 5 ++ .../components/SetAsMainBranchModal.tsx | 88 +++++++++++++++++++ .../sonar-web/src/main/js/queries/branch.tsx | 16 ++++ .../resources/org/sonar/l10n/core.properties | 5 ++ 12 files changed, 279 insertions(+), 48 deletions(-) create mode 100644 server/sonar-web/src/main/js/api/mocks/data/branches.ts create mode 100644 server/sonar-web/src/main/js/apps/projectBranches/components/SetAsMainBranchModal.tsx diff --git a/server/sonar-web/src/main/js/api/branches.ts b/server/sonar-web/src/main/js/api/branches.ts index 7e192d7a5d5..0ccba960aa2 100644 --- a/server/sonar-web/src/main/js/api/branches.ts +++ b/server/sonar-web/src/main/js/api/branches.ts @@ -54,3 +54,7 @@ export function excludeBranchFromPurge(projectKey: string, branchName: string, e value: excluded, }).catch(throwGlobalError); } + +export function setMainBranch(project: string, branch: string) { + return post('/api/project_branches/set_main', { project, branch }).catch(throwGlobalError); +} diff --git a/server/sonar-web/src/main/js/api/mocks/BranchesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/BranchesServiceMock.ts index af60faa5205..af29f2d2336 100644 --- a/server/sonar-web/src/main/js/api/mocks/BranchesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/BranchesServiceMock.ts @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { cloneDeep } from 'lodash'; -import { mockBranch, mockPullRequest } from '../../helpers/mocks/branch-like'; import { Branch, PullRequest } from '../../types/branch-like'; import { deleteBranch, @@ -27,56 +26,27 @@ import { getBranches, getPullRequests, renameBranch, + setMainBranch, } from '../branches'; +import { mockBranchList, mockPullRequestList } from './data/branches'; jest.mock('../branches'); -const defaultBranches: Branch[] = [ - mockBranch({ isMain: true, name: 'main', status: { qualityGateStatus: 'OK' } }), - mockBranch({ - excludedFromPurge: false, - name: 'delete-branch', - analysisDate: '2018-01-30', - status: { qualityGateStatus: 'ERROR' }, - }), - mockBranch({ name: 'normal-branch', status: { qualityGateStatus: 'ERROR' } }), -]; - -const defaultPullRequests: PullRequest[] = [ - mockPullRequest({ - title: 'TEST-191 update master', - key: '01', - status: { qualityGateStatus: 'OK' }, - }), - mockPullRequest({ - title: 'TEST-192 update normal-branch', - key: '02', - analysisDate: '2018-01-30', - base: 'normal-branch', - target: 'normal-branch', - status: { qualityGateStatus: 'ERROR' }, - }), - mockPullRequest({ - title: 'TEST-193 dumb commit', - key: '03', - target: 'normal-branch', - status: { qualityGateStatus: 'ERROR' }, - }), -]; - export default class BranchesServiceMock { branches: Branch[]; pullRequests: PullRequest[]; constructor() { - this.branches = cloneDeep(defaultBranches); - this.pullRequests = cloneDeep(defaultPullRequests); + this.branches = mockBranchList(); + this.pullRequests = mockPullRequestList(); + jest.mocked(getBranches).mockImplementation(this.getBranchesHandler); jest.mocked(getPullRequests).mockImplementation(this.getPullRequestsHandler); jest.mocked(deleteBranch).mockImplementation(this.deleteBranchHandler); jest.mocked(deletePullRequest).mockImplementation(this.deletePullRequestHandler); jest.mocked(renameBranch).mockImplementation(this.renameBranchHandler); jest.mocked(excludeBranchFromPurge).mockImplementation(this.excludeBranchFromPurgeHandler); + jest.mocked(setMainBranch).mockImplementation(this.setMainBranchHandler); } getBranchesHandler = () => { @@ -89,24 +59,33 @@ export default class BranchesServiceMock { deleteBranchHandler: typeof deleteBranch = ({ branch }) => { this.branches = this.branches.filter((b) => b.name !== branch); - return this.reply({}); + return this.reply(null); }; deletePullRequestHandler: typeof deletePullRequest = ({ pullRequest }) => { this.pullRequests = this.pullRequests.filter((b) => b.key !== pullRequest); - return this.reply({}); + return this.reply(null); }; renameBranchHandler: typeof renameBranch = (_, name) => { this.branches = this.branches.map((b) => (b.isMain ? { ...b, name } : b)); - return this.reply({}); + return this.reply(null); }; excludeBranchFromPurgeHandler: typeof excludeBranchFromPurge = (_, name, value) => { this.branches = this.branches.map((b) => b.name === name ? { ...b, excludedFromPurge: value } : b ); - return this.reply({}); + return this.reply(null); + }; + + setMainBranchHandler: typeof setMainBranch = (_, branch) => { + this.branches = this.branches.map((b) => ({ + ...b, + excludedFromPurge: b.excludedFromPurge || b.isMain || b.name === branch, + isMain: b.name === branch, + })); + return this.reply(null); }; emptyBranches = () => { @@ -127,8 +106,8 @@ export default class BranchesServiceMock { }; reset = () => { - this.branches = cloneDeep(defaultBranches); - this.pullRequests = cloneDeep(defaultPullRequests); + this.branches = mockBranchList(); + this.pullRequests = mockPullRequestList(); }; reply(response: T): Promise { diff --git a/server/sonar-web/src/main/js/api/mocks/data/branches.ts b/server/sonar-web/src/main/js/api/mocks/data/branches.ts new file mode 100644 index 00000000000..06255aa1874 --- /dev/null +++ b/server/sonar-web/src/main/js/api/mocks/data/branches.ts @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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 { mockBranch, mockPullRequest } from '../../../helpers/mocks/branch-like'; + +export function mockBranchList() { + return [ + mockBranch({ + isMain: true, + name: 'main', + status: { qualityGateStatus: 'OK' }, + }), + mockBranch({ + excludedFromPurge: false, + name: 'delete-branch', + analysisDate: '2018-01-30', + status: { qualityGateStatus: 'ERROR' }, + }), + mockBranch({ name: 'normal-branch', status: { qualityGateStatus: 'ERROR' } }), + ]; +} + +export function mockPullRequestList() { + return [ + mockPullRequest({ + title: 'TEST-191 update master', + key: '01', + status: { qualityGateStatus: 'OK' }, + }), + mockPullRequest({ + title: 'TEST-192 update normal-branch', + key: '02', + analysisDate: '2018-01-30', + base: 'normal-branch', + target: 'normal-branch', + status: { qualityGateStatus: 'ERROR' }, + }), + mockPullRequest({ + title: 'TEST-193 dumb commit', + key: '03', + target: 'normal-branch', + status: { qualityGateStatus: 'ERROR' }, + }), + ]; +} diff --git a/server/sonar-web/src/main/js/apps/projectBranches/ProjectBranchesApp.tsx b/server/sonar-web/src/main/js/apps/projectBranches/ProjectBranchesApp.tsx index c52f677190e..a4218d59441 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/ProjectBranchesApp.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/ProjectBranchesApp.tsx @@ -21,6 +21,7 @@ import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import withComponentContext from '../../app/components/componentContext/withComponentContext'; import { translate } from '../../helpers/l10n'; +import { withBranchLikes } from '../../queries/branch'; import { Component } from '../../types/types'; import BranchLikeTabs from './components/BranchLikeTabs'; import LifetimeInformation from './components/LifetimeInformation'; @@ -45,4 +46,4 @@ function ProjectBranchesApp(props: ProjectBranchesAppProps) { ); } -export default withComponentContext(React.memo(ProjectBranchesApp)); +export default withComponentContext(withBranchLikes(React.memo(ProjectBranchesApp))); diff --git a/server/sonar-web/src/main/js/apps/projectBranches/__tests__/ProjectBranchesApp-it.tsx b/server/sonar-web/src/main/js/apps/projectBranches/__tests__/ProjectBranchesApp-it.tsx index 08a57185893..abb2a03c313 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/__tests__/ProjectBranchesApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/__tests__/ProjectBranchesApp-it.tsx @@ -17,7 +17,6 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ - import { act, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; @@ -74,6 +73,12 @@ const ui = new (class UI { branchRow = this.branchTabContent.byRole('row'); pullRequestRow = this.pullRequestTabContent.byRole('row'); + + getBranchRow = (name: string | RegExp) => + within(this.branchTabContent.get()).getByRole('row', { name }); + + setMainBranchBtn = byRole('button', { name: 'project_branch_pull_request.branch.set_main' }); + dialog = byRole('dialog'); })(); beforeEach(() => { @@ -96,7 +101,9 @@ it('should show all branches', async () => { expect(ui.pullRequestTabContent.query()).not.toBeInTheDocument(); expect(ui.linkForAdmin.query()).not.toBeInTheDocument(); expect(await ui.branchRow.findAll()).toHaveLength(4); - expect(ui.branchRow.getAt(1)).toHaveTextContent('mainbranches.main_branchOK1 month ago'); + expect(ui.branchRow.getAt(1)).toHaveTextContent( + 'mainbranches.main_branchOK1 month agoproject_branch_pull_request.branch.auto_deletion.main_branch_tooltip' + ); expect(within(ui.branchRow.getAt(1)).getByRole('switch')).toBeDisabled(); expect(within(ui.branchRow.getAt(1)).getByRole('switch')).toBeChecked(); expect(ui.branchRow.getAt(2)).toHaveTextContent('delete-branchERROR2 days ago'); @@ -125,17 +132,54 @@ it('should be able to rename main branch, but not others', async () => { expect( within(ui.renameBranchDialog.get()).getByRole('button', { name: 'rename' }) ).toBeDisabled(); - await user.type(within(ui.renameBranchDialog.get()).getByRole('textbox'), 'master'); + await user.type(within(ui.renameBranchDialog.get()).getByRole('textbox'), 'develop'); expect(within(ui.renameBranchDialog.get()).getByRole('button', { name: 'rename' })).toBeEnabled(); await act(() => user.click(within(ui.renameBranchDialog.get()).getByRole('button', { name: 'rename' })) ); - expect(ui.branchRow.getAt(1)).toHaveTextContent('masterbranches.main_branchOK1 month ago'); + expect(ui.branchRow.getAt(1)).toHaveTextContent( + 'developbranches.main_branchOK1 month agoproject_branch_pull_request.branch.auto_deletion.main_branch_tooltip' + ); await user.click(await ui.updateSecondBranchBtn.find()); expect(ui.renameBranchBtn.query()).not.toBeInTheDocument(); }); +it('should be able to set a branch as the main branch', async () => { + const user = userEvent.setup(); + renderProjectBranchesApp(); + + // Cannot set main branch as main branch. + await user.click(await ui.updateMasterBtn.find()); + expect(ui.setMainBranchBtn.query()).not.toBeInTheDocument(); + expect(ui.getBranchRow(/^main/)).toBeInTheDocument(); + expect(within(ui.getBranchRow(/^main/)).getByText('branches.main_branch')).toBeInTheDocument(); + expect(within(ui.getBranchRow(/^main/)).getByRole('switch')).toBeChecked(); + expect(within(ui.getBranchRow(/^main/)).getByRole('switch')).toBeDisabled(); + + // Change main branch. + await user.click(await ui.updateSecondBranchBtn.find()); + await user.click(ui.setMainBranchBtn.get()); + await act(async () => { + await user.click( + within(ui.dialog.get()).getByRole('button', { + name: 'project_branch_pull_request.branch.set_x_as_main.delete-branch', + }) + ); + }); + + // "delete-branch" is now the main branch. + expect(ui.getBranchRow(/delete-branch/)).toBeInTheDocument(); + expect( + within(ui.getBranchRow(/delete-branch/)).getByText('branches.main_branch') + ).toBeInTheDocument(); + expect(within(ui.getBranchRow(/delete-branch/)).getByRole('switch')).toBeChecked(); + expect(within(ui.getBranchRow(/delete-branch/)).getByRole('switch')).toBeDisabled(); + + // "main" is now excluded from purge + expect(within(ui.getBranchRow(/^main/)).getByRole('switch')).toBeChecked(); +}); + it('should be able to delete branch, but not main', async () => { const user = userEvent.setup(); renderProjectBranchesApp(); diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRow.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRow.tsx index 4f6c91183f5..669b4fe761a 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRow.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRow.tsx @@ -39,6 +39,7 @@ export interface BranchLikeRowProps { displayPurgeSetting?: boolean; onDelete: () => void; onRename: () => void; + onSetAsMain: () => void; } function BranchLikeRow(props: BranchLikeRowProps) { @@ -72,6 +73,12 @@ function BranchLikeRow(props: BranchLikeRowProps) { getBranchLikeDisplayName(branchLike) )} > + {isBranch(branchLike) && !isMainBranch(branchLike) && ( + + {translate('project_branch_pull_request.branch.set_main')} + + )} + {isMainBranch(branchLike) ? ( {translate('project_branch_pull_request.branch.rename')} diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTable.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTable.tsx index 159b898de03..c4ce15ed618 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTable.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTable.tsx @@ -31,6 +31,7 @@ export interface BranchLikeTableProps { displayPurgeSetting?: boolean; onDelete: (branchLike: BranchLike) => void; onRename: (branchLike: BranchLike) => void; + onSetAsMain: (branchLike: BranchLike) => void; title: string; } @@ -80,6 +81,7 @@ function BranchLikeTable(props: BranchLikeTableProps) { key={getBranchLikeKey(branchLike)} onDelete={() => props.onDelete(branchLike)} onRename={() => props.onRename(branchLike)} + onSetAsMain={() => props.onSetAsMain(branchLike)} /> ))} diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTabs.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTabs.tsx index 9514fdd7373..ac18a821f2c 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTabs.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTabs.tsx @@ -36,6 +36,7 @@ import { Component } from '../../../types/types'; import BranchLikeTable from './BranchLikeTable'; import DeleteBranchModal from './DeleteBranchModal'; import RenameBranchModal from './RenameBranchModal'; +import SetAsMainBranchModal from './SetAsMainBranchModal'; interface Props { component: Component; @@ -75,12 +76,19 @@ export default function BranchLikeTabs(props: Props) { const { component } = props; const [currentTab, setCurrentTab] = useState(Tabs.Branch); const [renaming, setRenaming] = useState(); - + const [settingAsMain, setSettingAsMain] = useState(); const [deleting, setDeleting] = useState(); const handleClose = () => { setRenaming(undefined); setDeleting(undefined); + setSettingAsMain(undefined); + }; + + const handleSetAsMainBranch = (branchLike: BranchLike) => { + if (isBranch(branchLike)) { + setSettingAsMain(branchLike); + } }; const { data: { branchLikes } = { branchLikes: [] } } = useBranchesQuery(component); @@ -111,6 +119,7 @@ export default function BranchLikeTabs(props: Props) { displayPurgeSetting={isBranchMode} onDelete={setDeleting} onRename={setRenaming} + onSetAsMain={handleSetAsMainBranch} title={title} /> @@ -122,6 +131,15 @@ export default function BranchLikeTabs(props: Props) { {renaming && isMainBranch(renaming) && ( )} + + {settingAsMain && !isMainBranch(settingAsMain) && ( + + )} ); } diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchPurgeSetting.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchPurgeSetting.tsx index c69b18521fc..d0fd998284e 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchPurgeSetting.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchPurgeSetting.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; +import { useEffect } from 'react'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import Toggle from '../../../components/controls/Toggle'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; @@ -36,6 +37,10 @@ export default function BranchPurgeSetting(props: Props) { const { branch, component } = props; const { mutate: excludeFromPurge, isLoading } = useExcludeFromPurgeMutation(); + useEffect(() => { + excludeFromPurge({ component, key: branch.name, exclude: branch.excludedFromPurge }); + }, [branch.excludedFromPurge]); + const handleOnChange = (exclude: boolean) => { excludeFromPurge({ component, key: branch.name, exclude }); }; diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/SetAsMainBranchModal.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/SetAsMainBranchModal.tsx new file mode 100644 index 00000000000..de7c44a323f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/SetAsMainBranchModal.tsx @@ -0,0 +1,88 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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 { ButtonPrimary, FlagMessage, Modal } from 'design-system'; +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import DocLink from '../../../components/common/DocLink'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { useSetMainBranchMutation } from '../../../queries/branch'; +import { Branch } from '../../../types/branch-like'; +import { Component } from '../../../types/types'; + +interface SetAsMainBranchModalProps { + branch: Branch; + component: Component; + onClose: () => void; + onSetAsMain: () => void; +} + +export default function SetAsMainBranchModal(props: SetAsMainBranchModalProps) { + const { branch, component, onClose, onSetAsMain } = props; + const { mutate: setMainBranch, isLoading } = useSetMainBranchMutation(); + + const handleClick = () => { + setMainBranch({ component, branchName: branch.name }, { onSuccess: onSetAsMain }); + }; + + return ( + +

+ {translateWithParameters( + 'project_branch_pull_request.branch.main_branch.are_you_sure', + branch.name + )} +

+

+ + {translate('documentation')} + + ), + }} + /> +

+ + {translate('project_branch_pull_request.branch.main_branch.requires_reindex')} + + + } + primaryButton={ + + {translateWithParameters('project_branch_pull_request.branch.set_x_as_main', branch.name)} + + } + secondaryButtonLabel={translate('cancel')} + /> + ); +} diff --git a/server/sonar-web/src/main/js/queries/branch.tsx b/server/sonar-web/src/main/js/queries/branch.tsx index 1d99799b290..add79f22f14 100644 --- a/server/sonar-web/src/main/js/queries/branch.tsx +++ b/server/sonar-web/src/main/js/queries/branch.tsx @@ -29,6 +29,7 @@ import { getBranches, getPullRequests, renameBranch, + setMainBranch, } from '../api/branches'; import { dismissAnalysisWarning, getAnalysisStatus } from '../api/ce'; import { getQualityGateProjectStatus } from '../api/quality-gates'; @@ -274,6 +275,21 @@ export function useRenameMainBranchMutation() { }); } +export function useSetMainBranchMutation() { + type SetAsMainBranchArg = { branchName: string; component: Component }; + const queryClient = useQueryClient(); + const invalidateKey = useMutateBranchQueryKey(); + + return useMutation({ + mutationFn: async ({ component, branchName }: SetAsMainBranchArg) => { + await setMainBranch(component.key, branchName); + }, + onSuccess() { + queryClient.invalidateQueries({ queryKey: invalidateKey }); + }, + }); +} + /** * Helper functions that sould be avoid. Instead convert the component into functional * and/or use proper react-query diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index e7b5f022536..c9245f0e66c 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -616,9 +616,14 @@ project_branch_pull_request.page=Branches & Pull Requests project_branch_pull_request.lifetime_information=Branches and Pull Requests are permanently deleted after {days} days without analysis. project_branch_pull_request.lifetime_information.admin=You can adjust this value globally in {settings}. project_branch_pull_request.branch.rename=Rename branch +project_branch_pull_request.branch.set_main=Set as main branch +project_branch_pull_request.branch.set_x_as_main=Set "{0}" as the main branch project_branch_pull_request.branch.delete=Delete branch project_branch_pull_request.branch.actions_label=Update {0} project_branch_pull_request.branch.delete.are_you_sure=Are you sure you want to delete branch "{0}"? +project_branch_pull_request.branch.main_branch.are_you_sure=Are you sure you want to set branch "{0}" as the main branch of this project? +project_branch_pull_request.branch.main_branch.requires_reindex=Changing the main branch of your project will trigger a project re-indexation and may impact the level of information that is available until re-indexing is complete. +project_branch_pull_request.branch.main_branch.learn_more=Please refer to the {documentation} to understand the impacts of changing the main branch. project_branch_pull_request.branch.auto_deletion.keep_when_inactive=Keep when inactive project_branch_pull_request.branch.auto_deletion.keep_when_inactive.tooltip=When turned on, the branch will not be automatically deleted when inactive. project_branch_pull_request.branch.auto_deletion.main_branch_tooltip=The main branch is always excluded from automatic deletion. -- 2.39.5