]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19850 Add action to set branch as main branch
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Mon, 10 Jul 2023 07:13:32 +0000 (09:13 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 19 Jul 2023 20:03:06 +0000 (20:03 +0000)
12 files changed:
server/sonar-web/src/main/js/api/branches.ts
server/sonar-web/src/main/js/api/mocks/BranchesServiceMock.ts
server/sonar-web/src/main/js/api/mocks/data/branches.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/projectBranches/ProjectBranchesApp.tsx
server/sonar-web/src/main/js/apps/projectBranches/__tests__/ProjectBranchesApp-it.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeRow.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTable.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTabs.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/BranchPurgeSetting.tsx
server/sonar-web/src/main/js/apps/projectBranches/components/SetAsMainBranchModal.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/queries/branch.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 7e192d7a5d5adde80e6edcc83f17212588546b50..0ccba960aa24a10ae6e25f28c33fb91e98dc8649 100644 (file)
@@ -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);
+}
index af60faa5205e13f4870e9c826bc3c22f7ecf10bb..af29f2d23360600508541afc4d03d9f5cd6bd7fc 100644 (file)
@@ -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<T>(response: T): Promise<T> {
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 (file)
index 0000000..06255aa
--- /dev/null
@@ -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' },
+    }),
+  ];
+}
index c52f677190ef0c8d9e20378a22e55c4401713b5e..a4218d59441beb46e203e30c84c71139032db48b 100644 (file)
@@ -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)));
index 08a5718589399449cf8d10679588a7557bbf6d44..abb2a03c313c82e02c1eba31d108ec07daef7351 100644 (file)
@@ -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();
index 4f6c91183f5012269727b7cd51419848f33ccc62..669b4fe761a0cb7ce53810cc44d9261036fc0b1d 100644 (file)
@@ -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) && (
+            <ActionsDropdownItem onClick={props.onSetAsMain}>
+              {translate('project_branch_pull_request.branch.set_main')}
+            </ActionsDropdownItem>
+          )}
+
           {isMainBranch(branchLike) ? (
             <ActionsDropdownItem className="js-rename" onClick={props.onRename}>
               {translate('project_branch_pull_request.branch.rename')}
index 159b898de030077f8d8d17726b0ac934196bf813..c4ce15ed618fa89dd427c5a4a434c6e544c0cbb8 100644 (file)
@@ -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)}
             />
           ))}
         </tbody>
index 9514fdd7373418b3a7ba547c90b7f90c5149d5ec..ac18a821f2cce35b3c9d94834ca1d2d12836c23d 100644 (file)
@@ -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>(Tabs.Branch);
   const [renaming, setRenaming] = useState<BranchLike>();
-
+  const [settingAsMain, setSettingAsMain] = useState<Branch>();
   const [deleting, setDeleting] = useState<BranchLike>();
 
   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}
         />
       </div>
@@ -122,6 +131,15 @@ export default function BranchLikeTabs(props: Props) {
       {renaming && isMainBranch(renaming) && (
         <RenameBranchModal branch={renaming} component={component} onClose={handleClose} />
       )}
+
+      {settingAsMain && !isMainBranch(settingAsMain) && (
+        <SetAsMainBranchModal
+          branch={settingAsMain}
+          component={component}
+          onClose={handleClose}
+          onSetAsMain={handleClose}
+        />
+      )}
     </>
   );
 }
index c69b18521fc2fb93d8fd285eb05d580c34864402..d0fd998284e5630c8417521b5a3d4659fec7802d 100644 (file)
@@ -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 (file)
index 0000000..de7c44a
--- /dev/null
@@ -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 (
+    <Modal
+      headerTitle={translateWithParameters(
+        'project_branch_pull_request.branch.set_x_as_main',
+        branch.name
+      )}
+      loading={isLoading}
+      onClose={onClose}
+      body={
+        <>
+          <p className="sw-mb-4">
+            {translateWithParameters(
+              'project_branch_pull_request.branch.main_branch.are_you_sure',
+              branch.name
+            )}
+          </p>
+          <p className="sw-mb-4">
+            <FormattedMessage
+              id="project_branch_pull_request.branch.main_branch.learn_more"
+              defaultMessage={translate(
+                'project_branch_pull_request.branch.main_branch.learn_more'
+              )}
+              values={{
+                documentation: (
+                  <DocLink to="/analyzing-source-code/branches/branch-analysis/#main-branch">
+                    {translate('documentation')}
+                  </DocLink>
+                ),
+              }}
+            />
+          </p>
+          <FlagMessage variant="warning">
+            {translate('project_branch_pull_request.branch.main_branch.requires_reindex')}
+          </FlagMessage>
+        </>
+      }
+      primaryButton={
+        <ButtonPrimary disabled={isLoading} onClick={handleClick}>
+          {translateWithParameters('project_branch_pull_request.branch.set_x_as_main', branch.name)}
+        </ButtonPrimary>
+      }
+      secondaryButtonLabel={translate('cancel')}
+    />
+  );
+}
index 1d99799b29027d65b8eb7e3f0320aa88cc39d137..add79f22f144639ced399012c17dbc6732dfe036 100644 (file)
@@ -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
index e7b5f022536b215b14e29a7e6cd2c40a4dd4ddda..c9245f0e66cb4d7a660d652b006b3f71abf8b8d9 100644 (file)
@@ -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.