feat: [code-1991]: adding merge conflict files to be seen in merge section (#2126)

This commit is contained in:
Calvin Lee 2024-06-21 15:46:17 +00:00 committed by Harness
parent 1b244fe624
commit 16fa4e7acf
10 changed files with 626 additions and 159 deletions

View File

@ -250,6 +250,18 @@ export interface StringsMap {
cloneText: string
close: string
closed: string
'cmdlineInfo.content': string
'cmdlineInfo.stepFive': string
'cmdlineInfo.stepFiveSub': string
'cmdlineInfo.stepFour': string
'cmdlineInfo.stepFourSub': string
'cmdlineInfo.stepOne': string
'cmdlineInfo.stepOneSub': string
'cmdlineInfo.stepThree': string
'cmdlineInfo.stepThreeSub': string
'cmdlineInfo.stepTwo': string
'cmdlineInfo.stepTwoSub': string
'cmdlineInfo.title': string
code: string
'codeOwner.approvalCompleted': string
'codeOwner.changesRequested': string
@ -259,6 +271,7 @@ export interface StringsMap {
codeSearch: string
codeSearchModal: string
comingSoon: string
commandLine: string
comment: string
commentDeleted: string
commit: string
@ -286,6 +299,7 @@ export interface StringsMap {
confirmRepoVisButton: string
confirmStrat: string
confirmation: string
conflictsFoundInThisBranch: string
content: string
contents: string
contributor: string
@ -802,6 +816,7 @@ export interface StringsMap {
'pr.titlePlaceHolder': string
'pr.toggleComments': string
'pr.unified': string
'pr.useCmdLineToResolveConflicts': string
'prChecks.error': string
'prChecks.failure': string
'prChecks.killed': string
@ -973,6 +988,7 @@ export interface StringsMap {
status: string
'step.select': string
'stepCategory.select': string
stepNum: string
submitReview: string
success: string
suggestion: string

View File

@ -252,6 +252,7 @@ webhook: Webhook
diff: Diff
draft: Draft
conversation: Conversation
commandLine: command line
pr:
toggleComments: Toggle comments
suggestedChange: Suggested change
@ -267,6 +268,7 @@ pr:
expandFullFile: Expand all
collapseFullFile: Collapse expanded lines
ableToMerge: Able to merge.
useCmdLineToResolveConflicts: Use the {cmd} to resolve conflicts
cantBeMerged: This branch has conflicts with the {{name}} branch.
cantMerge: Can't be merged. You can still create the pull request.
failedToCreate: Failed to create Pull Request.
@ -1043,6 +1045,7 @@ resolveComments: There are {{n}} unresolved comments
view: View
mergeCheckInProgress: Merge check in progress...
allConflictsNeedToBeResolved: All conflicts have to be resolved before merging
conflictsFoundInThisBranch: Conflicts found in this branch
details: Details
showCheckAll: Show all checks
showLessCheck: Show less checks
@ -1053,7 +1056,7 @@ customizeMergeCommitMessage: Customize merge commit message
mergeStrategy: Merge strategy
selectMergeStrat: Select merge strategy
writeDownCommit: Write down commit message here
prHasNoConflicts: This branch has no conflicts with {{name}} branch
prHasNoConflicts: This branch has no conflicts with {name} branch
checkStatus:
succeeded: Succeeded in {time}
failed: Failed in {time}
@ -1181,3 +1184,17 @@ changesRequestedBy: CHANGES REQUESTED BY
mergeBranchTitle: Merge branch {{branchName}} of {{repoPath}} (#{{prNum}})
http: HTTP
ssh: SSH
stepNum: STEP {{num}}
cmdlineInfo:
title: Resolve conflicts via command line
content: If the conflicts on this branch are too complex to resolve in the web editor, you can check it out via command line to resolve the conflicts
stepOne: Clone the repository or update your local repository with the latest changes
stepOneSub: git pull origin {target}
stepTwo: Switch to the head branch of the pull request
stepTwoSub: git checkout {source}
stepThree: Merge the base branch into the head branch
stepThreeSub: git merge {target}
stepFour: Fix the conflicts and commit the result
stepFourSub: See Resolving a merge conflict using the command line for step-by-step instruction on resolving merge conflicts
stepFive: Push the changes
stepFiveSub: git push -u origin {source}

View File

@ -38,11 +38,10 @@ import cx from 'classnames'
import ReactTimeago from 'react-timeago'
import type { OpenapiStatePullReqRequest, TypesPullReq, TypesRuleViolations } from 'services/code'
import { useStrings } from 'framework/strings'
import { CodeIcon, MergeStrategy, PullRequestFilterOption, PullRequestState } from 'utils/GitUtils'
import { CodeIcon, MergeStrategy, PullRequestFilterOption, PullRequestState, dryMerge } from 'utils/GitUtils'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import { useAppContext } from 'AppContext'
import {
dryMerge,
extractInfoFromRuleViolationArr,
getErrorMessage,
inlineMergeFormRefType,
@ -65,7 +64,8 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
onPRStateChanged,
allowedStrategy,
pullReqCommits,
PRStateLoading
PRStateLoading,
setConflictingFiles
}) => {
const { getString } = useStrings()
const { showError } = useToaster()
@ -126,7 +126,8 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
setRuleViolationArr,
setAllowedStrats,
pullRequestSection,
showError
showError,
setConflictingFiles
) // eslint-disable-next-line react-hooks/exhaustive-deps
}, [unchecked, pullReqMetadata?.source_sha])
const [prMerged, setPrMerged] = useState(false)
@ -144,7 +145,8 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
setRuleViolationArr,
setAllowedStrats,
pullRequestSection,
showError
showError,
setConflictingFiles
)
}
}, POLLING_INTERVAL) // Poll every 20 seconds

View File

@ -0,0 +1,209 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import { Color, FontVariation } from '@harnessio/design-system'
import { Container, Layout, StringSubstitute, Text } from '@harnessio/uicore'
import type { TypesPullReq } from 'services/code'
import { useStrings } from 'framework/strings'
import { CopyButton } from 'components/CopyButton/CopyButton'
import { CodeIcon } from 'utils/GitUtils'
import css from './PullRequestOverviewPanel.module.scss'
interface CommandLineInfoProps {
pullReqMetadata: TypesPullReq
}
const CommandLineInfo = (props: CommandLineInfoProps) => {
const { pullReqMetadata } = props
const { getString } = useStrings()
const stepOneCopy = getString('cmdlineInfo.stepOneSub').replace('{target}', pullReqMetadata.target_branch as string)
const stepTwoCopy = getString('cmdlineInfo.stepTwoSub').replace('{source}', pullReqMetadata.source_branch as string)
const stepThreeCopy = getString('cmdlineInfo.stepThreeSub').replace(
'{target}',
pullReqMetadata.target_branch as string
)
const stepFiveCopy = getString('cmdlineInfo.stepFiveSub').replace('{source}', pullReqMetadata.source_branch as string)
return (
<Container
className={css.cmdInfoContainer}
margin={{ top: 'small', bottom: 'small' }}
padding={{ top: 'large', left: 'xxxlarge', right: 'xlarge', bottom: 'large' }}>
<Layout.Vertical>
<Container className={css.cmdTextTitleContainer}>
<Text padding={{ bottom: 'xsmall' }} font={{ variation: FontVariation.H5 }}>
{getString('cmdlineInfo.title')}
</Text>
</Container>
<Text
color={Color.GREY_450}
className={css.stepText}
font={{ variation: FontVariation.BODY2 }}
padding={{ top: 'xsmall' }}>
{getString('cmdlineInfo.content')}
</Text>
<Layout.Vertical>
<Layout.Horizontal flex={{ alignItems: 'center', justifyContent: 'flex-start' }} padding={{ top: 'small' }}>
<Text className={css.checkName} padding={{ right: 'small' }} font={{ variation: FontVariation.CARD_TITLE }}>
{getString('stepNum', { num: 1 }).toUpperCase()}
</Text>
<Text color={Color.GREY_450} className={css.stepText} font={{ variation: FontVariation.BODY2 }}>
{getString('cmdlineInfo.stepOne')}
</Text>
</Layout.Horizontal>
<Layout.Horizontal margin={{ left: 'small' }} padding={{ top: 'xsmall', left: 'xxxlarge' }}>
<Layout.Horizontal
flex={{ justifyContent: 'space-between' }}
className={css.blueCopyContainer}
padding={{ top: 'small', left: 'medium', right: 'medium', bottom: 'small' }}>
<Text className={css.stepFont}>
<StringSubstitute
str={getString('cmdlineInfo.stepOneSub')}
vars={{
target: pullReqMetadata.target_branch
}}
/>
</Text>
<CopyButton
className={css.copyIconContainer}
content={stepOneCopy}
icon={CodeIcon.Copy}
color={Color.PRIMARY_7}
iconProps={{ size: 14, color: Color.PRIMARY_7 }}
background={Color.PRIMARY_1}
/>
</Layout.Horizontal>
</Layout.Horizontal>
<Layout.Horizontal flex={{ alignItems: 'center', justifyContent: 'flex-start' }} padding={{ top: 'small' }}>
<Text className={css.checkName} padding={{ right: 'small' }} font={{ variation: FontVariation.CARD_TITLE }}>
{getString('stepNum', { num: 2 }).toUpperCase()}
</Text>
<Text color={Color.GREY_450} className={css.stepText} font={{ variation: FontVariation.BODY2 }}>
{getString('cmdlineInfo.stepTwo')}
</Text>
</Layout.Horizontal>
<Layout.Horizontal margin={{ left: 'small' }} padding={{ top: 'xsmall', left: 'xxxlarge' }}>
<Layout.Horizontal
flex={{ justifyContent: 'space-between' }}
className={css.blueCopyContainer}
padding={{ top: 'small', left: 'medium', right: 'medium', bottom: 'small' }}>
<Text className={css.stepFont}>
<StringSubstitute
str={getString('cmdlineInfo.stepTwoSub')}
vars={{
source: pullReqMetadata.source_branch
}}
/>
</Text>
<CopyButton
className={css.copyIconContainer}
content={stepTwoCopy}
icon={CodeIcon.Copy}
color={Color.PRIMARY_7}
iconProps={{ size: 14, color: Color.PRIMARY_7 }}
background={Color.PRIMARY_1}
/>
</Layout.Horizontal>
</Layout.Horizontal>
<Layout.Horizontal flex={{ alignItems: 'center', justifyContent: 'flex-start' }} padding={{ top: 'small' }}>
<Text className={css.checkName} padding={{ right: 'small' }} font={{ variation: FontVariation.CARD_TITLE }}>
{getString('stepNum', { num: 3 }).toUpperCase()}
</Text>
<Text color={Color.GREY_450} className={css.stepText} font={{ variation: FontVariation.BODY2 }}>
{getString('cmdlineInfo.stepThree')}
</Text>
</Layout.Horizontal>
<Layout.Horizontal margin={{ left: 'small' }} padding={{ top: 'xsmall', left: 'xxxlarge' }}>
<Layout.Horizontal
flex={{ justifyContent: 'space-between' }}
className={css.blueCopyContainer}
padding={{ top: 'small', left: 'medium', right: 'medium', bottom: 'small' }}>
<Text className={css.stepFont}>
<StringSubstitute
str={getString('cmdlineInfo.stepThreeSub')}
vars={{
target: pullReqMetadata.target_branch
}}
/>
</Text>
<CopyButton
className={css.copyIconContainer}
content={stepThreeCopy}
icon={CodeIcon.Copy}
color={Color.PRIMARY_7}
iconProps={{ size: 14, color: Color.PRIMARY_7 }}
background={Color.PRIMARY_1}
/>
</Layout.Horizontal>
</Layout.Horizontal>
<Layout.Horizontal flex={{ alignItems: 'center', justifyContent: 'flex-start' }} padding={{ top: 'small' }}>
<Text className={css.checkName} padding={{ right: 'small' }} font={{ variation: FontVariation.CARD_TITLE }}>
{getString('stepNum', { num: 4 }).toUpperCase()}
</Text>
<Text color={Color.GREY_450} className={css.stepText} font={{ variation: FontVariation.BODY2 }}>
{getString('cmdlineInfo.stepFour')}
</Text>
</Layout.Horizontal>
<Layout.Horizontal margin={{ left: 'small' }} padding={{ top: 'xsmall', left: 'xxxlarge' }}>
<Layout.Horizontal flex={{ justifyContent: 'space-between' }}>
<Text
padding={{ left: 'tiny' }}
color={Color.GREY_450}
className={css.stepText}
font={{ variation: FontVariation.BODY2 }}>
{getString('cmdlineInfo.stepFourSub')}
</Text>
</Layout.Horizontal>
</Layout.Horizontal>
<Layout.Horizontal flex={{ alignItems: 'center', justifyContent: 'flex-start' }} padding={{ top: 'small' }}>
<Text className={css.checkName} padding={{ right: 'small' }} font={{ variation: FontVariation.CARD_TITLE }}>
{getString('stepNum', { num: 5 }).toUpperCase()}
</Text>
<Text color={Color.GREY_450} className={css.stepText} font={{ variation: FontVariation.BODY2 }}>
{getString('cmdlineInfo.stepFive')}
</Text>
</Layout.Horizontal>
<Layout.Horizontal margin={{ left: 'small' }} padding={{ top: 'xsmall', left: 'xxxlarge' }}>
<Layout.Horizontal
flex={{ justifyContent: 'space-between' }}
className={css.blueCopyContainer}
padding={{ top: 'small', left: 'medium', right: 'medium', bottom: 'small' }}>
<Text className={css.stepFont}>
<StringSubstitute
str={getString('cmdlineInfo.stepFiveSub')}
vars={{
source: pullReqMetadata.source_branch
}}
/>
</Text>
<CopyButton
className={css.copyIconContainer}
content={stepFiveCopy}
icon={CodeIcon.Copy}
color={Color.PRIMARY_7}
iconProps={{ size: 14, color: Color.PRIMARY_7 }}
background={Color.PRIMARY_1}
/>
</Layout.Horizontal>
</Layout.Horizontal>
</Layout.Vertical>
</Layout.Vertical>
</Container>
)
}
export default CommandLineInfo

View File

@ -204,8 +204,9 @@
.gridContainer {
display: grid !important;
grid-template-columns: 1fr 1fr !important;
grid-template-columns: 1fr minmax(0, 1fr) !important;
position: relative !important;
min-width: 0;
}
.row {
@ -225,6 +226,7 @@
margin-left: var(--spacing-small) !important;
grid-column: 2 !important;
text-align: right !important;
min-width: 0;
}
/* Pseudo-elements for placeholders to maintain grid structure */
@ -251,3 +253,107 @@
padding-right: unset !important;
}
}
.conflictingFilesTable {
.row {
font-size: 12px !important;
box-shadow: unset !important;
border-bottom: 1px solid rgb(217, 218, 229, 0.6) !important;
padding: var(--spacing-small) !important;
margin-bottom: unset !important;
background: #f6f8fa !important;
&:last-child {
border-bottom: none !important;
}
}
:global {
[class*='TableV2--header'] {
border-bottom: 1px solid rgb(217, 218, 229, 0.6) !important;
padding-top: var(--spacing-small) !important;
padding-left: var(--spacing-medium) !important;
padding-right: var(--spacing-medium) !important;
padding-bottom: var(--spacing-xsmall) !important;
font-size: 12px !important;
}
.conflictingFileName {
font-family: 'Roboto Mono' !important;
}
[class*='TableV2--cell'] {
p {
font-size: 12px !important;
}
}
.bp3-icon-double-caret-vertical {
fill: var(--grey-200) !important;
}
}
}
.conflictingContainer {
border-top: 1px solid var(--grey-100) !important;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
padding: var(--spacing-5) 2rem !important;
padding-right: 4.05rem !important;
background: #f6f8fa !important;
max-height: 150px;
overflow: auto !important;
}
.cmdText {
color: var(--primary-7) !important;
&:hover {
text-decoration: underline;
cursor: pointer;
}
}
.cmdInfoContainer {
background: var(--primary-bg) !important;
border: 1px solid var(--grey-100) !important;
.cmdTextTitleContainer {
border-bottom: 1px solid var(--grey-100) !important;
> p {
font-size: 14px !important;
font-weight: 600 !important;
}
}
}
.copyIconContainer {
background-color: var(--primary-1) !important;
color: var(--primary-7) !important;
border-radius: 4px !important;
--button-height: unset !important;
padding: unset !important ;
--padding-right: 2px !important;
padding-left: 4px !important;
min-width: unset !important;
padding-bottom: 1px !important;
--background-color: var(--primary-7);
--text-color: var(--primary-7) !important;
}
.blueCopyContainer {
border: 0.5px solid var(--primary-3) !important;
background: var(--primary-1) !important;
width: 70% !important;
}
.stepFont {
font-size: 12px !important;
color: var(--black) !important;
font-family: 'Courier New', Courier, monospace !important;
}
.stepText {
font-size: 12px !important;
font-weight: 400 !important;
}
.conflictingFileName {
font-family: 'Roboto Mono' !important;
font-weight: 600 !important;
}

View File

@ -16,6 +16,7 @@
/* eslint-disable */
// This is an auto-generated file
export declare const blueCopyContainer: string
export declare const blueText: string
export declare const borderContainer: string
export declare const borderRadius: string
@ -23,7 +24,14 @@ export declare const buttonPadding: string
export declare const changeContainerPadding: string
export declare const checkContainerPadding: string
export declare const checkName: string
export declare const cmdInfoContainer: string
export declare const cmdText: string
export declare const cmdTextTitleContainer: string
export declare const codeOwnerContainer: string
export declare const conflictingContainer: string
export declare const conflictingFileName: string
export declare const conflictingFilesTable: string
export declare const copyIconContainer: string
export declare const details: string
export declare const executionIcon: string
export declare const greyContainer: string
@ -48,6 +56,8 @@ export declare const sectionTitle: string
export declare const showMore: string
export declare const statusCircleContainer: string
export declare const statusIcon: string
export declare const stepFont: string
export declare const stepText: string
export declare const successIcon: string
export declare const textSize: string
export declare const timeoutIcon: string

View File

@ -28,8 +28,8 @@ import type {
TypesRuleViolations
} from 'services/code'
import { PanelSectionOutletPosition } from 'pages/PullRequest/PullRequestUtils'
import { MergeCheckStatus, PRMergeOption, dryMerge } from 'utils/Utils'
import { PullRequestState } from 'utils/GitUtils'
import { MergeCheckStatus, PRMergeOption } from 'utils/Utils'
import { PullRequestState, dryMerge } from 'utils/GitUtils'
import { useStrings } from 'framework/strings'
import type { PRChecksDecisionResult } from 'hooks/usePRChecksDecision'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
@ -79,6 +79,7 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
() => pullReqMetadata.merge_check_status === MergeCheckStatus.UNCHECKED && !isClosed,
[pullReqMetadata, isClosed]
)
const [conflictingFiles, setConflictingFiles] = useState<string[]>()
const [ruleViolation, setRuleViolation] = useState(false)
const [ruleViolationArr, setRuleViolationArr] = useState<{ data: { rule_violations: TypesRuleViolations[] } }>()
const [requiresCommentApproval, setRequiresCommentApproval] = useState(false)
@ -172,6 +173,7 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
setAllowedStrats,
pullRequestSection,
showError,
setConflictingFiles,
setRequiresCommentApproval,
setAtLeastOneReviewerRule,
setReqCodeOwnerApproval,
@ -186,6 +188,8 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
<Container margin={{ bottom: 'medium' }} className={css.mainContainer}>
<Layout.Vertical>
<PullRequestActionsBox
conflictingFiles={conflictingFiles}
setConflictingFiles={setConflictingFiles}
repoMetadata={repoMetadata}
pullReqMetadata={pullReqMetadata}
onPRStateChanged={onPRStateChanged}
@ -231,9 +235,12 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
<ChecksSection pullReqMetadata={pullReqMetadata} repoMetadata={repoMetadata} />
),
[PanelSectionOutletPosition.MERGEABILITY]: !pullReqMetadata.merged && (
<Container className={cx(css.sectionContainer, css.borderRadius)}>
<MergeSection pullReqMetadata={pullReqMetadata} unchecked={unchecked} mergeable={mergeable} />
</Container>
<MergeSection
pullReqMetadata={pullReqMetadata}
unchecked={unchecked}
mergeable={mergeable}
conflictingFiles={conflictingFiles}
/>
)
}}
/>

View File

@ -13,63 +13,156 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import React, { useMemo } from 'react'
import { Color, FontVariation } from '@harnessio/design-system'
import { Container, Layout, Text } from '@harnessio/uicore'
import cx from 'classnames'
import {
Button,
ButtonSize,
ButtonVariation,
Container,
Layout,
StringSubstitute,
TableV2,
Text,
useToggle
} from '@harnessio/uicore'
import { Render } from 'react-jsx-match'
import type { CellProps, Column } from 'react-table'
import { Images } from 'images'
import type { TypesPullReq } from 'services/code'
import { useStrings } from 'framework/strings'
import Success from '../../../../../icons/code-success.svg?url'
import Fail from '../../../../../icons/code-fail.svg?url'
import CommandLineInfo from '../CommandLineInfo'
import css from '../PullRequestOverviewPanel.module.scss'
interface MergeSectionProps {
mergeable: boolean
unchecked: boolean
pullReqMetadata: TypesPullReq
conflictingFiles: string[] | undefined
}
interface ConflictingFilesInterface {
name: string
}
const MergeSection = (props: MergeSectionProps) => {
const { mergeable, unchecked, pullReqMetadata } = props
const { mergeable, unchecked, pullReqMetadata, conflictingFiles } = props
const { getString } = useStrings()
const [isExpanded, toggleExpanded] = useToggle(false)
const [showCommandLineInfo, toggleShowCommandLineInfo] = useToggle(false)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const columns: Column<any>[] = useMemo(
() => [
{
id: 'conflictingFiles',
width: '45%',
sort: true,
Header: `Conflicting Files (${conflictingFiles?.length})`,
accessor: 'conflictingFiles',
Cell: ({ row }: CellProps<ConflictingFilesInterface>) => {
return (
<Text
lineClamp={1}
className={css.conflictingFileName}
padding={{ left: 'small', right: 'small' }}
color={Color.BLACK}>
{row.original}
</Text>
)
}
}
], // eslint-disable-next-line react-hooks/exhaustive-deps
[conflictingFiles]
)
return (
<Container>
<Layout.Horizontal flex={{ alignItems: 'center', justifyContent: 'start' }}>
{(unchecked && <img src={Images.PrUnchecked} width={25} height={25} />) || (
<>
{mergeable ? (
<img alt={getString('success')} width={26} height={26} src={Success} />
) : (
<img alt={getString('failed')} width={26} height={26} src={Fail} />
<Container className={cx(css.sectionContainer, css.borderRadius)}>
<Layout.Horizontal flex={{ justifyContent: 'space-between' }}>
<Layout.Horizontal flex={{ alignItems: 'center', justifyContent: 'start' }}>
{(unchecked && <img src={Images.PrUnchecked} width={25} height={25} />) || (
<>
{mergeable ? (
<img alt={getString('success')} width={26} height={26} src={Success} />
) : (
<img alt={getString('failed')} width={26} height={26} src={Fail} />
)}
</>
)}
</>
)}
{(unchecked && (
<Layout.Vertical padding={{ left: 'medium' }}>
<Text padding={{ bottom: 'xsmall' }} className={css.sectionTitle}>
{getString('mergeCheckInProgress')}
</Text>
<Text className={css.sectionSubheader}> {getString('pr.checkingToMerge')}</Text>
</Layout.Vertical>
)) || (
<Layout.Vertical>
{!mergeable && (
<Text className={css.sectionTitle} color={Color.RED_700} padding={{ left: 'medium', bottom: 'xsmall' }}>
{getString('allConflictsNeedToBeResolved')}
</Text>
{(unchecked && (
<Layout.Vertical padding={{ left: 'medium' }}>
<Text padding={{ bottom: 'xsmall' }} className={css.sectionTitle}>
{getString('mergeCheckInProgress')}
</Text>
<Text className={css.sectionSubheader}> {getString('pr.checkingToMerge')}</Text>
</Layout.Vertical>
)) || (
<Layout.Vertical>
{!mergeable && (
<Text
className={css.sectionTitle}
color={Color.RED_700}
padding={{ left: 'medium', bottom: 'xsmall' }}>
{getString('conflictsFoundInThisBranch')}
</Text>
)}
<Text
flex
className={mergeable ? css.sectionTitle : css.sectionSubheader}
color={mergeable ? Color.GREEN_800 : Color.GREY_450}
font={{ variation: FontVariation.BODY }}
padding={{ left: 'medium' }}>
<StringSubstitute
str={mergeable ? getString('prHasNoConflicts') : getString('pr.useCmdLineToResolveConflicts')}
vars={{
name: pullReqMetadata.target_branch,
cmd: (
<Text
onClick={toggleShowCommandLineInfo}
padding={{ left: 'xsmall', right: 'xsmall' }}
className={css.cmdText}>
{getString('commandLine')}
</Text>
)
}}
/>
</Text>
</Layout.Vertical>
)}
<Text
className={mergeable ? css.sectionTitle : css.sectionSubheader}
color={mergeable ? Color.GREEN_800 : Color.GREY_450}
font={{ variation: FontVariation.BODY }}
padding={{ left: 'medium' }}>
{getString(mergeable ? 'prHasNoConflicts' : 'pr.cantBeMerged', { name: pullReqMetadata?.target_branch })}
</Text>
</Layout.Vertical>
)}
</Layout.Horizontal>
</Layout.Horizontal>
{!mergeable && (
<Button
padding={{ right: 'unset' }}
className={cx(css.blueText, css.buttonPadding)}
variation={ButtonVariation.LINK}
size={ButtonSize.SMALL}
text={getString(isExpanded ? 'showLessMatches' : 'showMoreText')}
onClick={toggleExpanded}
rightIcon={isExpanded ? 'main-chevron-up' : 'main-chevron-down'}
iconProps={{ size: 10, margin: { left: 'xsmall' } }}
/>
)}
</Layout.Horizontal>
<Render when={showCommandLineInfo}>
<CommandLineInfo pullReqMetadata={pullReqMetadata} />
</Render>
</Container>
<Render when={isExpanded}>
<Container className={css.conflictingContainer}>
<Container padding={{ left: 'xxxlarge' }} className={css.greyContainer}>
<TableV2
className={css.conflictingFilesTable}
sortable
columns={columns}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data={conflictingFiles as any}
getRowClassName={() => css.row}
/>
</Container>
</Container>
</Render>
</Container>
)
}

View File

@ -19,6 +19,7 @@
// Last updated for git 2.29.0.
import type { IconName } from '@harnessio/icons'
import type { MutateRequestOptions } from 'restful-react/dist/Mutate'
import type {
EnumWebhookTrigger,
OpenapiContentInfo,
@ -26,10 +27,11 @@ import type {
OpenapiGetContentOutput,
TypesCommit,
TypesPullReq,
TypesRepository
TypesRepository,
TypesRuleViolations
} from 'services/code'
import { getConfig } from 'services/config'
import { getErrorMessage } from './Utils'
import { PullRequestSection, getErrorMessage } from './Utils'
export interface GitInfoProps {
repoMetadata: TypesRepository
@ -456,3 +458,114 @@ export const getProviders = () =>
Object.values(GitProviders).map(value => {
return { value, label: value }
})
export const codeOwnersNotFoundMessage = 'CODEOWNERS file not found'
export const codeOwnersNotFoundMessage2 = `path "CODEOWNERS" not found`
export const codeOwnersNotFoundMessage3 = `failed to find node 'CODEOWNERS' in 'main': failed to get tree node: failed to ls file: path "CODEOWNERS" not found`
export const dryMerge = (
isMounted: React.MutableRefObject<boolean>,
isClosed: boolean,
pullReqMetadata: TypesPullReq,
internalFlags: React.MutableRefObject<{
dryRun: boolean
}>,
mergePR: (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any,
mutateRequestOptions?:
| MutateRequestOptions<
{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
},
unknown
>
| undefined // eslint-disable-next-line @typescript-eslint/no-explicit-any
) => Promise<any>,
setRuleViolation: (value: React.SetStateAction<boolean>) => void,
setRuleViolationArr: (
value: React.SetStateAction<
| {
data: {
rule_violations: TypesRuleViolations[]
}
}
| undefined
>
) => void,
setAllowedStrats: (value: React.SetStateAction<string[]>) => void,
pullRequestSection: string | undefined,
showError: (message: React.ReactNode, timeout?: number | undefined, key?: string | undefined) => void,
setConflictingFiles: React.Dispatch<React.SetStateAction<string[] | undefined>>,
setRequiresCommentApproval?: (value: React.SetStateAction<boolean>) => void,
setAtLeastOneReviewerRule?: (value: React.SetStateAction<boolean>) => void,
setReqCodeOwnerApproval?: (value: React.SetStateAction<boolean>) => void,
setMinApproval?: (value: React.SetStateAction<number>) => void,
setReqCodeOwnerLatestApproval?: (value: React.SetStateAction<boolean>) => void,
setMinReqLatestApproval?: (value: React.SetStateAction<number>) => void,
setPRStateLoading?: (value: React.SetStateAction<boolean>) => void
) => {
if (isMounted.current && !isClosed && pullReqMetadata.state !== PullRequestState.MERGED) {
// Use an internal flag to prevent flickering during the loading state of buttons
internalFlags.current.dryRun = true
mergePR({ bypass_rules: true, dry_run: true, source_sha: pullReqMetadata?.source_sha })
.then(res => {
if (isMounted.current) {
if (res?.rule_violations?.length > 0) {
setRuleViolation(true)
setRuleViolationArr({ data: { rule_violations: res?.rule_violations } })
setAllowedStrats(res.allowed_methods)
setRequiresCommentApproval?.(res.requires_comment_resolution)
setAtLeastOneReviewerRule?.(res.requires_no_change_requests)
setReqCodeOwnerApproval?.(res.requires_code_owners_approval)
setMinApproval?.(res.minimum_required_approvals_count)
setReqCodeOwnerLatestApproval?.(res.requires_code_owners_approval_latest)
setMinReqLatestApproval?.(res.minimum_required_approvals_count_latest)
setConflictingFiles?.(res.conflict_files)
} else {
setRuleViolation(false)
setAllowedStrats(res.allowed_methods)
setRequiresCommentApproval?.(res.requires_comment_resolution)
setAtLeastOneReviewerRule?.(res.requires_no_change_requests)
setReqCodeOwnerApproval?.(res.requires_code_owners_approval)
setMinApproval?.(res.minimum_required_approvals_count)
setReqCodeOwnerLatestApproval?.(res.requires_code_owners_approval_latest)
setMinReqLatestApproval?.(res.minimum_required_approvals_count_latest)
setConflictingFiles?.(res.conflict_files)
}
}
})
.catch(err => {
if (isMounted.current) {
if (err.status === 422) {
setRuleViolation(true)
setRuleViolationArr(err)
setAllowedStrats(err.allowed_methods)
setRequiresCommentApproval?.(err.requires_comment_resolution)
setAtLeastOneReviewerRule?.(err.requires_no_change_requests)
setReqCodeOwnerApproval?.(err.requires_code_owners_approval)
setMinApproval?.(err.minimum_required_approvals_count)
setReqCodeOwnerLatestApproval?.(err.requires_code_owners_approval_latest)
setMinReqLatestApproval?.(err.minimum_required_approvals_count_latest)
setConflictingFiles?.(err.conflict_files)
} else if (
getErrorMessage(err) === codeOwnersNotFoundMessage ||
getErrorMessage(err) === codeOwnersNotFoundMessage2 ||
getErrorMessage(err) === codeOwnersNotFoundMessage3 ||
err.status === 423 // resource locked (merge / dry-run already ongoing)
) {
return
} else if (pullRequestSection !== PullRequestSection.CONVERSATION) {
return
} else {
showError(getErrorMessage(err))
}
}
})
.finally(() => {
internalFlags.current.dryRun = false
setPRStateLoading?.(false)
})
}
}

View File

@ -23,16 +23,14 @@ import type { editor } from 'monaco-editor'
import type { EditorView } from '@codemirror/view'
import type { FormikProps } from 'formik'
import type { SelectOption } from '@harnessio/uicore'
import type { MutateRequestOptions } from 'restful-react/dist/Mutate'
import type {
EnumMergeMethod,
TypesRuleViolations,
TypesViolation,
TypesCodeOwnerEvaluationEntry,
TypesPullReq,
TypesListCommitResponse
} from 'services/code'
import { PullRequestState, type GitInfoProps } from './GitUtils'
import type { GitInfoProps } from './GitUtils'
export enum ACCESS_MODES {
VIEW,
@ -160,6 +158,8 @@ export interface PullRequestActionsBoxProps extends Pick<GitInfoProps, 'repoMeta
allowedStrategy: string[]
pullReqCommits: TypesListCommitResponse | undefined
PRStateLoading: boolean
conflictingFiles: string[] | undefined
setConflictingFiles: React.Dispatch<React.SetStateAction<string[] | undefined>>
}
export interface PRMergeOption extends SelectOption {
@ -731,109 +731,3 @@ export function removeSpecificTextOptimized(
viewRef?.current?.dispatch({ changes })
}
}
export const codeOwnersNotFoundMessage = 'CODEOWNERS file not found'
export const codeOwnersNotFoundMessage2 = `path "CODEOWNERS" not found`
export const codeOwnersNotFoundMessage3 = `failed to find node 'CODEOWNERS' in 'main': failed to get tree node: failed to ls file: path "CODEOWNERS" not found`
export const dryMerge = (
isMounted: React.MutableRefObject<boolean>,
isClosed: boolean,
pullReqMetadata: TypesPullReq,
internalFlags: React.MutableRefObject<{
dryRun: boolean
}>,
mergePR: (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any,
mutateRequestOptions?:
| MutateRequestOptions<
{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
},
unknown
>
| undefined // eslint-disable-next-line @typescript-eslint/no-explicit-any
) => Promise<any>,
setRuleViolation: (value: React.SetStateAction<boolean>) => void,
setRuleViolationArr: (
value: React.SetStateAction<
| {
data: {
rule_violations: TypesRuleViolations[]
}
}
| undefined
>
) => void,
setAllowedStrats: (value: React.SetStateAction<string[]>) => void,
pullRequestSection: string | undefined,
showError: (message: React.ReactNode, timeout?: number | undefined, key?: string | undefined) => void,
setRequiresCommentApproval?: (value: React.SetStateAction<boolean>) => void,
setAtLeastOneReviewerRule?: (value: React.SetStateAction<boolean>) => void,
setReqCodeOwnerApproval?: (value: React.SetStateAction<boolean>) => void,
setMinApproval?: (value: React.SetStateAction<number>) => void,
setReqCodeOwnerLatestApproval?: (value: React.SetStateAction<boolean>) => void,
setMinReqLatestApproval?: (value: React.SetStateAction<number>) => void,
setPRStateLoading?: (value: React.SetStateAction<boolean>) => void
) => {
if (isMounted.current && !isClosed && pullReqMetadata.state !== PullRequestState.MERGED) {
// Use an internal flag to prevent flickering during the loading state of buttons
internalFlags.current.dryRun = true
mergePR({ bypass_rules: true, dry_run: true, source_sha: pullReqMetadata?.source_sha })
.then(res => {
if (isMounted.current) {
if (res?.rule_violations?.length > 0) {
setRuleViolation(true)
setRuleViolationArr({ data: { rule_violations: res?.rule_violations } })
setAllowedStrats(res.allowed_methods)
setRequiresCommentApproval?.(res.requires_comment_resolution)
setAtLeastOneReviewerRule?.(res.requires_no_change_requests)
setReqCodeOwnerApproval?.(res.requires_code_owners_approval)
setMinApproval?.(res.minimum_required_approvals_count)
setReqCodeOwnerLatestApproval?.(res.requires_code_owners_approval_latest)
setMinReqLatestApproval?.(res.minimum_required_approvals_count_latest)
} else {
setRuleViolation(false)
setAllowedStrats(res.allowed_methods)
setRequiresCommentApproval?.(res.requires_comment_resolution)
setAtLeastOneReviewerRule?.(res.requires_no_change_requests)
setReqCodeOwnerApproval?.(res.requires_code_owners_approval)
setMinApproval?.(res.minimum_required_approvals_count)
setReqCodeOwnerLatestApproval?.(res.requires_code_owners_approval_latest)
setMinReqLatestApproval?.(res.minimum_required_approvals_count_latest)
}
}
})
.catch(err => {
if (isMounted.current) {
if (err.status === 422) {
setRuleViolation(true)
setRuleViolationArr(err)
setAllowedStrats(err.allowed_methods)
setRequiresCommentApproval?.(err.requires_comment_resolution)
setAtLeastOneReviewerRule?.(err.requires_no_change_requests)
setReqCodeOwnerApproval?.(err.requires_code_owners_approval)
setMinApproval?.(err.minimum_required_approvals_count)
setReqCodeOwnerLatestApproval?.(err.requires_code_owners_approval_latest)
setMinReqLatestApproval?.(err.minimum_required_approvals_count_latest)
} else if (
getErrorMessage(err) === codeOwnersNotFoundMessage ||
getErrorMessage(err) === codeOwnersNotFoundMessage2 ||
getErrorMessage(err) === codeOwnersNotFoundMessage3 ||
err.status === 423 // resource locked (merge / dry-run already ongoing)
) {
return
} else if (pullRequestSection !== PullRequestSection.CONVERSATION) {
return
} else {
showError(getErrorMessage(err))
}
}
})
.finally(() => {
internalFlags.current.dryRun = false
setPRStateLoading?.(false)
})
}
}