feat: [CODE-2386] added rules to update branch [ block_branch_update , block_force_push ] (#2710)

* fix: [CODE-2386] updated return for branch rules map logic
* fix: [CODE-2386] fix force update on require PR
* fix: [CODE-2382] disable force-push for require pr
* fix: [CODE-2382] disable force-push and require pr rule if branch update rule is selected
* fix: [CODE-2386] commented sub heading, will be added later
* feat: [CODE-2386] added rules to update branch
CODE-2402
Ritik Kapoor 2024-09-23 17:01:46 +00:00 committed by Harness
parent 1725841f67
commit 534f5a8293
8 changed files with 383 additions and 66 deletions

View File

@ -37,7 +37,13 @@ import { useHistory } from 'react-router-dom'
import { useGet, useMutate } from 'restful-react'
import { BranchTargetType, MergeStrategy, SettingTypeMode, SettingsTab, branchTargetOptions } from 'utils/GitUtils'
import { useStrings } from 'framework/strings'
import { REGEX_VALID_REPO_NAME, getErrorMessage, permissionProps, rulesFormInitialPayload } from 'utils/Utils'
import {
REGEX_VALID_REPO_NAME,
RulesFormPayload,
getErrorMessage,
permissionProps,
rulesFormInitialPayload
} from 'utils/Utils'
import type {
RepoRepositoryOutput,
OpenapiRule,
@ -147,7 +153,7 @@ const BranchProtectionForm = (props: {
}
const history = useHistory()
const initialValues = useMemo(() => {
const initialValues = useMemo((): RulesFormPayload => {
if (editMode && rule) {
const minReviewerCheck =
((rule.definition as ProtectionBranch)?.pullreq?.approvals?.require_minimum_count as number) > 0 ? true : false
@ -197,8 +203,16 @@ const BranchProtectionForm = (props: {
rebaseMerge: isRebasePresent,
autoDelete: (rule.definition as ProtectionBranch)?.pullreq?.merge?.delete_branch,
blockBranchCreation: (rule.definition as ProtectionBranch)?.lifecycle?.create_forbidden,
blockBranchUpdate:
(rule.definition as ProtectionBranch)?.lifecycle?.update_forbidden &&
(rule.definition as ProtectionBranch)?.pullreq?.merge?.block,
blockBranchDeletion: (rule.definition as ProtectionBranch)?.lifecycle?.delete_forbidden,
requirePr: (rule.definition as ProtectionBranch)?.lifecycle?.update_forbidden,
blockForcePush:
(rule.definition as ProtectionBranch)?.lifecycle?.update_forbidden ||
(rule.definition as ProtectionBranch)?.lifecycle?.update_force_forbidden,
requirePr:
(rule.definition as ProtectionBranch)?.lifecycle?.update_forbidden &&
!(rule.definition as ProtectionBranch)?.pullreq?.merge?.block,
targetSet: false,
bypassSet: false
}
@ -218,7 +232,7 @@ const BranchProtectionForm = (props: {
[space]
)
return (
<Formik
<Formik<RulesFormPayload>
formName="branchProtectionRulesNewEditForm"
initialValues={initialValues}
enableReinitialize
@ -265,7 +279,8 @@ const BranchProtectionForm = (props: {
},
merge: {
strategies_allowed: stratArray,
delete_branch: formData.autoDelete
delete_branch: formData.autoDelete,
block: formData.blockBranchUpdate
},
status_checks: {
require_identifiers: formData.statusChecks
@ -274,7 +289,8 @@ const BranchProtectionForm = (props: {
lifecycle: {
create_forbidden: formData.blockBranchCreation,
delete_forbidden: formData.blockBranchDeletion,
update_forbidden: formData.requirePr
update_forbidden: formData.requirePr || formData.blockBranchUpdate,
update_force_forbidden: formData.blockForcePush && !formData.requirePr && !formData.blockBranchUpdate
}
}
}
@ -304,7 +320,7 @@ const BranchProtectionForm = (props: {
const requireStatusChecks = formik.values.requireStatusChecks
const filteredUserOptions = userOptions.filter(
(item: SelectOption) => !bypassList.includes(item.value as string)
(item: SelectOption) => !bypassList?.includes(item.value as string)
)
return (
@ -394,7 +410,7 @@ const BranchProtectionForm = (props: {
if (formik.values.target !== '') {
formik.setFieldValue('targetSet', true)
targetList.push([BranchTargetType.INCLUDE, formik.values.target])
targetList.push([BranchTargetType.INCLUDE, formik.values.target ?? ''])
formik.setFieldValue('targetList', targetList)
formik.setFieldValue('target', '')
}
@ -409,7 +425,7 @@ const BranchProtectionForm = (props: {
if (formik.values.target !== '') {
formik.setFieldValue('targetSet', true)
targetList.push([BranchTargetType.EXCLUDE, formik.values.target])
targetList.push([BranchTargetType.EXCLUDE, formik.values.target ?? ''])
formik.setFieldValue('targetList', targetList)
formik.setFieldValue('target', '')
}
@ -473,7 +489,7 @@ const BranchProtectionForm = (props: {
<BypassList bypassList={bypassList} setFieldValue={formik.setFieldValue} />
</Container>
<ProtectionRulesForm
setFieldValue={formik.setFieldValue}
formik={formik}
requireStatusChecks={requireStatusChecks}
minReviewers={minReviewers}
statusOptions={statusOptions}

View File

@ -14,36 +14,40 @@
* limitations under the License.
*/
import React from 'react'
import cx from 'classnames'
import { Container, FlexExpander, FormInput, Layout, SelectOption, Text } from '@harnessio/uicore'
import { Icon } from '@harnessio/icons'
import { FontVariation } from '@harnessio/design-system'
import { Color, FontVariation } from '@harnessio/design-system'
import type { FormikProps } from 'formik'
import { Classes, Popover, PopoverInteractionKind, PopoverPosition } from '@blueprintjs/core'
import { useStrings } from 'framework/strings'
import type { RulesFormPayload } from 'utils/Utils'
import css from '../BranchProtectionForm.module.scss'
const ProtectionRulesForm = (props: {
requireStatusChecks: boolean
minReviewers: boolean
statusOptions: SelectOption[]
statusChecks: string[]
limitMergeStrats: boolean // eslint-disable-next-line @typescript-eslint/no-explicit-any
setFieldValue: (field: string, value: any, shouldValidate?: boolean | undefined) => void
setSearchStatusTerm: React.Dispatch<React.SetStateAction<string>>
formik: FormikProps<RulesFormPayload>
}) => {
const {
setFieldValue,
statusChecks,
setSearchStatusTerm,
minReviewers,
requireStatusChecks,
statusOptions,
limitMergeStrats
limitMergeStrats,
formik
} = props
const { getString } = useStrings()
const setFieldValue = formik.setFieldValue
const filteredStatusOptions = statusOptions.filter(
(item: SelectOption) => !statusChecks.includes(item.value as string)
)
const { values } = formik
return (
<Container margin={{ top: 'medium' }} className={css.generalContainer}>
<Text className={css.headingSize} padding={{ bottom: 'medium' }} font={{ variation: FontVariation.H4 }}>
@ -68,16 +72,76 @@ const ProtectionRulesForm = (props: {
<Text padding={{ left: 'xlarge' }} className={css.checkboxText}>
{getString('branchProtection.blockBranchDeletionText')}
</Text>
<hr className={css.dividerContainer} />
<FormInput.CheckBox
className={css.checkboxLabel}
label={getString('branchProtection.requirePr')}
name={'requirePr'}
label={getString('branchProtection.blockBranchUpdate')}
name={'blockBranchUpdate'}
onChange={() => {
setFieldValue('blockForcePush', !(values.blockBranchUpdate && values.blockForcePush))
setFieldValue('requirePr', false)
}}
/>
<Text padding={{ left: 'xlarge' }} className={css.checkboxText}>
{getString('branchProtection.requirePrText')}
{getString('branchProtection.blockBranchUpdateText')}
</Text>
<hr className={css.dividerContainer} />
<Popover
interactionKind={PopoverInteractionKind.HOVER}
position={PopoverPosition.TOP_LEFT}
popoverClassName={Classes.DARK}
disabled={!(values.blockBranchUpdate || values.requirePr)}
content={
<Container padding="medium">
<Text font={{ variation: FontVariation.FORM_HELP }} color={Color.WHITE}>
{values.requirePr ? getString('pushBlockedMessage') : getString('ruleBlockedMessage')}
</Text>
</Container>
}>
<>
<FormInput.CheckBox
disabled={values.blockBranchUpdate || values.requirePr}
className={css.checkboxLabel}
label={getString('branchProtection.blockForcePush')}
name={'blockForcePush'}
/>
<Text padding={{ left: 'xlarge' }} className={css.checkboxText}>
{getString('branchProtection.blockForcePushText')}
</Text>
</>
</Popover>
<hr className={css.dividerContainer} />
<Popover
interactionKind={PopoverInteractionKind.HOVER}
position={PopoverPosition.TOP_LEFT}
popoverClassName={Classes.DARK}
disabled={!values.blockBranchUpdate}
content={
<Container padding="medium">
<Text font={{ variation: FontVariation.FORM_HELP }} color={Color.WHITE}>
{getString('ruleBlockedMessage')}
</Text>
</Container>
}>
<>
<FormInput.CheckBox
disabled={values.blockBranchUpdate}
className={css.checkboxLabel}
label={getString('branchProtection.requirePr')}
name={'requirePr'}
onChange={() => {
setFieldValue('blockForcePush', !values.requirePr)
}}
/>
<Text padding={{ left: 'xlarge' }} className={css.checkboxText}>
{getString('branchProtection.requirePrText')}
</Text>
</>
</Popover>
<hr className={css.dividerContainer} />
<FormInput.CheckBox
className={css.checkboxLabel}

View File

@ -39,7 +39,16 @@ import { useHistory } from 'react-router-dom'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
import { useQueryParams } from 'hooks/useQueryParams'
import { usePageIndex } from 'hooks/usePageIndex'
import { getErrorMessage, LIST_FETCHING_LIMIT, permissionProps, type PageBrowserProps } from 'utils/Utils'
import {
getErrorMessage,
LIST_FETCHING_LIMIT,
permissionProps,
type PageBrowserProps,
Rule,
RuleFields,
BranchProtectionRulesMapType,
createRuleFieldsMap
} from 'utils/Utils'
import { SettingTypeMode } from 'utils/GitUtils'
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
import { NoResultCard } from 'components/NoResultCard/NoResultCard'
@ -50,6 +59,7 @@ import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButto
import type { OpenapiRule, ProtectionPattern } from 'services/code'
import { useAppContext } from 'AppContext'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
import Include from '../../icons/Include.svg?url'
import Exclude from '../../icons/Exclude.svg?url'
import BranchProtectionForm from './BranchProtectionForm/BranchProtectionForm'
@ -73,6 +83,7 @@ const BranchProtectionListing = (props: { activeTab: string }) => {
const {
data: rules,
refetch: refetchRules,
loading: loadingRules,
response
} = useGet<OpenapiRule[]>({
path: `/api/v1/repos/${repoMetadata?.path}/+/rules`,
@ -87,6 +98,89 @@ const BranchProtectionListing = (props: { activeTab: string }) => {
lazy: !repoMetadata || !!editRule
})
const branchProtectionRules = {
requireMinReviewersTitle: {
title: getString('branchProtection.requireMinReviewersTitle'),
requiredRule: {
[RuleFields.APPROVALS_REQUIRE_MINIMUM_COUNT]: true
}
},
reqReviewFromCodeOwnerTitle: {
title: getString('branchProtection.reqReviewFromCodeOwnerTitle'),
requiredRule: {
[RuleFields.APPROVALS_REQUIRE_CODE_OWNERS]: true
}
},
reqResOfChanges: {
title: getString('branchProtection.reqResOfChanges'),
requiredRule: {
[RuleFields.APPROVALS_REQUIRE_NO_CHANGE_REQUEST]: true
}
},
reqNewChangesTitle: {
title: getString('branchProtection.reqNewChangesTitle'),
requiredRule: {
[RuleFields.APPROVALS_REQUIRE_LATEST_COMMIT]: true
}
},
reqCommentResolutionTitle: {
title: getString('branchProtection.reqCommentResolutionTitle'),
requiredRule: {
[RuleFields.COMMENTS_REQUIRE_RESOLVE_ALL]: true
}
},
reqStatusChecksTitle: {
title: getString('branchProtection.reqStatusChecksTitle'),
requiredRule: {
[RuleFields.STATUS_CHECKS_ALL_MUST_SUCCEED]: true
}
},
limitMergeStrategies: {
title: getString('branchProtection.limitMergeStrategies'),
requiredRule: {
[RuleFields.MERGE_STRATEGIES_ALLOWED]: true
}
},
autoDeleteTitle: {
title: getString('branchProtection.autoDeleteTitle'),
requiredRule: {
[RuleFields.MERGE_DELETE_BRANCH]: true
}
},
blockBranchCreation: {
title: getString('branchProtection.blockBranchCreation'),
requiredRule: {
[RuleFields.LIFECYCLE_CREATE_FORBIDDEN]: true
}
},
blockBranchDeletion: {
title: getString('branchProtection.blockBranchDeletion'),
requiredRule: {
[RuleFields.LIFECYCLE_DELETE_FORBIDDEN]: true
}
},
blockBranchUpdate: {
title: getString('branchProtection.blockBranchUpdate'),
requiredRule: {
[RuleFields.MERGE_BLOCK]: true,
[RuleFields.LIFECYCLE_UPDATE_FORBIDDEN]: true
}
},
requirePr: {
title: getString('branchProtection.requirePr'),
requiredRule: {
[RuleFields.LIFECYCLE_UPDATE_FORBIDDEN]: true,
[RuleFields.MERGE_BLOCK]: false
}
},
blockForcePush: {
title: getString('branchProtection.blockForcePush'),
requiredRule: {
[RuleFields.LIFECYCLE_UPDATE_FORCE_FORBIDDEN]: true
}
}
}
const columns: Column<OpenapiRule>[] = useMemo(
() => [
{
@ -137,48 +231,35 @@ const BranchProtectionListing = (props: { activeTab: string }) => {
</Text>
)
type Rule = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
}
const fieldsToCheck = {
'pullreq.approvals.require_minimum_count': getString('branchProtection.requireMinReviewersTitle'),
'pullreq.approvals.require_code_owners': getString('branchProtection.reqReviewFromCodeOwnerTitle'),
'pullreq.approvals.require_no_change_request': getString('branchProtection.reqResOfChanges'),
'pullreq.approvals.require_latest_commit': getString('branchProtection.reqNewChangesTitle'),
'pullreq.comments.require_resolve_all': getString('branchProtection.reqCommentResolutionTitle'),
'pullreq.status_checks.all_must_succeed': getString('branchProtection.reqStatusChecksTitle'),
'pullreq.status_checks.require_identifiers': getString('branchProtection.reqStatusChecksTitle'),
'pullreq.merge.strategies_allowed': getString('branchProtection.limitMergeStrategies'),
'pullreq.merge.delete_branch': getString('branchProtection.autoDeleteTitle'),
'lifecycle.create_forbidden': getString('branchProtection.blockBranchCreation'),
'lifecycle.delete_forbidden': getString('branchProtection.blockBranchDeletion'),
'lifecycle.update_forbidden': getString('branchProtection.requirePr')
}
type NonEmptyRule = {
field: string // eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any
}
const checkFieldsNotEmpty = (rulesArr: Rule, fields: { [key: string]: string }): NonEmptyRule[] => {
const checkAppliedRules = (rulesData: Rule, rulesList: BranchProtectionRulesMapType): NonEmptyRule[] => {
const nonEmptyFields: NonEmptyRule[] = []
for (const field in fields) {
const keys = field.split('.')
let value = rulesArr
for (const key of keys) {
value = value[key]
if (value == null) break
}
if (value !== undefined && (Array.isArray(value) ? value.length > 0 : true)) {
nonEmptyFields.push({ field, value: fields[field] }) // Use value from fieldsToCheck
const rulesDefinitionData: Record<RuleFields, boolean> = createRuleFieldsMap(rulesData)
for (const [key, rule] of Object.entries(rulesList)) {
const { title, requiredRule } = rule
const isApplicable = Object.entries(requiredRule).every(([ruleField, requiredValue]) => {
const ruleFieldEnum = ruleField as RuleFields
const actualValue = rulesDefinitionData[ruleFieldEnum]
if (requiredValue) return actualValue
return !actualValue
})
if (isApplicable) {
nonEmptyFields.push({
field: key,
value: title
})
}
}
return nonEmptyFields
}
const nonEmptyRules = checkFieldsNotEmpty(row.original.definition as Rule, fieldsToCheck)
const nonEmptyRules = checkAppliedRules(row.original.definition as Rule, branchProtectionRules)
const { hooks, standalone } = useAppContext()
const space = useGetSpaceParam()
@ -346,7 +427,7 @@ const BranchProtectionListing = (props: { activeTab: string }) => {
{nonEmptyRules.map((rule: { value: string }) => {
return (
<Text
key={`${row.original.identifier}-${rule}`}
key={`${row.original.identifier}-${rule.value}`}
className={css.appliedRulesTextContainer}>
{rule.value}
</Text>
@ -386,6 +467,7 @@ const BranchProtectionListing = (props: { activeTab: string }) => {
)
return (
<Container>
<LoadingSpinner visible={loadingRules} />
{repoMetadata && !newRule && !editRule && (
<BranchProtectionHeader
activeTab={activeTab}

View File

@ -70,6 +70,10 @@ export interface StringsMap {
'branchProtection.blockBranchCreationText': string
'branchProtection.blockBranchDeletion': string
'branchProtection.blockBranchDeletionText': string
'branchProtection.blockBranchUpdate': string
'branchProtection.blockBranchUpdateText': string
'branchProtection.blockForcePush': string
'branchProtection.blockForcePushText': string
'branchProtection.bypassList': string
'branchProtection.commitDirectlyAlertBtn': string
'branchProtection.commitDirectlyAlertText': string
@ -169,6 +173,8 @@ export interface StringsMap {
'changesSection.noReviewsReq': string
'changesSection.pendingAppFromCodeOwners': string
'changesSection.pendingLatestApprovalCodeOwners': string
'changesSection.prMergeBlockedMessage': string
'changesSection.prMergeBlockedTitle': string
'changesSection.pullReqWithoutAnyReviews': string
'changesSection.reqChangeFromCodeOwners': string
'changesSection.someChangesWereAppByCodeOwner': string
@ -872,6 +878,7 @@ export interface StringsMap {
pullRequestNotFoundforFilter: string
pullRequestalreadyExists: string
pullRequests: string
pushBlockedMessage: string
quote: string
reTriggeredExecution: string
reactivate: string
@ -940,6 +947,7 @@ export interface StringsMap {
reviewerNotFound: string
reviewers: string
role: string
ruleBlockedMessage: string
run: string
running: string
samplePayloadUrl: string

View File

@ -243,6 +243,8 @@ webhookAllEventsSelected: 'All Events'
branchTagCreation: 'Branch or tag creation'
branchTagDeletion: 'Branch or tag deletion'
branchProtectionRules: 'Branch protection rules'
ruleBlockedMessage: 'All branch updates are blocked'
pushBlockedMessage: 'All branch pushes are blocked'
checkRuns: 'Check runs'
checkSuites: 'Check suites'
scanAlerts: 'Code scanning alerts'
@ -1002,9 +1004,13 @@ branchProtection:
autoDeleteTitle: Auto delete branch on merge
autoDeleteText: Automatically delete the source branch of a pull request after it is merged
blockBranchCreation: Block branch creation
blockBranchUpdate: Block branch update
blockBranchUpdateText: Only allow users with bypass permission to update matching branches
blockBranchCreationText: Only allow users with bypass permission to create matching branches
blockBranchDeletion: Block branch deletion
blockBranchDeletionText: Only allow users with bypass permission to delete matching branches
blockForcePush: Block force push
blockForcePushText: Only allow users with bypass permission to force push to matching branches
editRule: Edit Rule
saveRule: Save Rule
deleteRule: Delete Rule
@ -1098,6 +1104,8 @@ checkStatus:
pending: Pending...
error: Errored in {time}
changesSection:
prMergeBlockedTitle: Base branch does not allow updates
prMergeBlockedMessage: Read about Protected Branches
reqChangeFromCodeOwners: Changes requested by code owner
codeOwnerReqChanges: Code owner requested changes
pendingAppFromCodeOwners: Approvals pending from code owners

View File

@ -104,6 +104,7 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
const [minReqLatestApproval, setMinReqLatestApproval] = useState(0)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [resolvedCommentArr, setResolvedCommentArr] = useState<any>()
const [mergeBlockedRule, setMergeBlockedRule] = useState<boolean>(false)
const [PRStateLoading, setPRStateLoading] = useState(isClosed ? false : true)
const { pullRequestSection } = useGetRepositoryMetadata()
const mergeable = useMemo(() => pullReqMetadata.merge_check_status === MergeCheckStatus.MERGEABLE, [pullReqMetadata])
@ -199,9 +200,13 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
useEffect(() => {
if (ruleViolationArr) {
const requireResCommentRule = extractSpecificViolations(ruleViolationArr, 'pullreq.comments.require_resolve_all')
const mergeBlockedViaRule = extractSpecificViolations(ruleViolationArr, 'pullreq.merge.blocked')
if (requireResCommentRule) {
setResolvedCommentArr(requireResCommentRule[0])
}
setMergeBlockedRule(mergeBlockedViaRule.length > 0)
} else {
setMergeBlockedRule(false)
}
}, [ruleViolationArr, pullReqMetadata, repoMetadata, data, ruleViolation])
@ -232,7 +237,7 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
}, [unchecked, pullReqMetadata?.source_sha, activities])
const rebasePossible = useMemo(
() => pullReqMetadata.merge_target_sha !== pullReqMetadata.merge_base_sha,
() => pullReqMetadata.merge_target_sha !== pullReqMetadata.merge_base_sha && !pullReqMetadata.merged,
[pullReqMetadata]
)
@ -278,6 +283,7 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
minReqLatestApproval={minReqLatestApproval}
reqCodeOwnerLatestApproval={reqCodeOwnerLatestApproval}
refetchCodeOwners={refetchCodeOwners}
mergeBlockedRule={mergeBlockedRule}
/>
</Render>
),

View File

@ -59,6 +59,7 @@ interface ChangesSectionProps {
reviewers: TypesPullReqReviewer[] | null
minReqLatestApproval: number
reqCodeOwnerLatestApproval: boolean
mergeBlockedRule: boolean
loadingReviewers: boolean
refetchReviewers: () => void
refetchCodeOwners: () => void
@ -76,6 +77,7 @@ const ChangesSection = (props: ChangesSectionProps) => {
reqCodeOwnerLatestApproval,
minReqLatestApproval,
loadingReviewers,
mergeBlockedRule,
refetchReviewers,
refetchCodeOwners
} = props
@ -91,7 +93,7 @@ const ChangesSection = (props: ChangesSectionProps) => {
const reviewers = useMemo(() => {
refetchCodeOwners()
return currReviewers // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currReviewers, refetchReviewers])
}, [currReviewers, refetchReviewers, mergeBlockedRule])
const codeOwners = useMemo(() => {
return currCodeOwners // eslint-disable-next-line react-hooks/exhaustive-deps
@ -144,9 +146,15 @@ const ChangesSection = (props: ChangesSectionProps) => {
reqCodeOwnerApproval ||
minApproval > 0 ||
reqCodeOwnerLatestApproval ||
minReqLatestApproval > 0
minReqLatestApproval > 0 ||
mergeBlockedRule
) {
if (codeOwnerChangeReqEntries.length > 0 && (reqCodeOwnerApproval || reqCodeOwnerLatestApproval)) {
if (mergeBlockedRule) {
title = getString('changesSection.prMergeBlockedTitle')
// statusMessage = getString('changesSection.prMergeBlockedMessage')
statusColor = Color.RED_700
statusIcon = 'warning-icon'
} else if (codeOwnerChangeReqEntries.length > 0 && (reqCodeOwnerApproval || reqCodeOwnerLatestApproval)) {
title = getString('changesSection.reqChangeFromCodeOwners')
statusMessage = getString('changesSection.codeOwnerReqChanges')
statusColor = Color.RED_700
@ -258,7 +266,9 @@ const ChangesSection = (props: ChangesSectionProps) => {
reqCodeOwnerLatestApproval,
minReqLatestApproval,
refetchReviewers,
refetchCodeOwners
refetchCodeOwners,
mergeBlockedRule,
approvedEvaluations
])
function renderCodeOwnerStatus() {
@ -376,13 +386,14 @@ const ChangesSection = (props: ChangesSectionProps) => {
)
}
const viewBtn =
minApproval > minReqLatestApproval ||
(!isEmpty(approvedEvaluations) && minReqLatestApproval === 0) ||
(minApproval > 0 && minReqLatestApproval === undefined) ||
minReqLatestApproval > 0 ||
!isEmpty(changeReqEvaluations) ||
!isEmpty(codeOwners) ||
false
!mergeBlockedRule &&
(minApproval > minReqLatestApproval ||
(!isEmpty(approvedEvaluations) && minReqLatestApproval === 0) ||
(minApproval > 0 && minReqLatestApproval === undefined) ||
minReqLatestApproval > 0 ||
!isEmpty(changeReqEvaluations) ||
!isEmpty(codeOwners) ||
false)
return (
<Render when={!loading && !loadingReviewers && status}>
<Container className={cx(css.sectionContainer, css.borderContainer)}>
@ -399,7 +410,7 @@ const ChangesSection = (props: ChangesSectionProps) => {
)}
<Layout.Vertical padding={{ left: 'medium' }}>
<Text
padding={{ bottom: 'xsmall' }}
padding={contentText ? { bottom: 'xsmall' } : undefined}
className={css.sectionTitle}
color={
headerText === getString('changesSection.noReviewsReq')

View File

@ -333,7 +333,7 @@ export interface Violation {
violation: string
}
export const rulesFormInitialPayload = {
export const rulesFormInitialPayload: RulesFormPayload = {
name: '',
desc: '',
enable: true,
@ -357,11 +357,44 @@ export const rulesFormInitialPayload = {
autoDelete: false,
blockBranchCreation: false,
blockBranchDeletion: false,
blockBranchUpdate: false,
blockForcePush: false,
requirePr: false,
bypassSet: false,
targetSet: false
}
export type RulesFormPayload = {
name?: string
desc?: string
enable: boolean
target?: string
targetDefault?: boolean
targetList: string[][]
allRepoOwners?: boolean
bypassList?: string[]
requireMinReviewers: boolean
minReviewers?: string | number
requireCodeOwner?: boolean
requireNewChanges?: boolean
reqResOfChanges?: boolean
requireCommentResolution?: boolean
requireStatusChecks: boolean
statusChecks: string[]
limitMergeStrategies: boolean
mergeCommit?: boolean
squashMerge?: boolean
rebaseMerge?: boolean
autoDelete?: boolean
blockBranchCreation?: boolean
blockBranchDeletion?: boolean
blockBranchUpdate?: boolean
blockForcePush?: boolean
requirePr?: boolean
bypassSet: boolean
targetSet: boolean
}
/**
* Make any HTML element as a clickable button with keyboard accessibility
* support (hit Enter/Space will trigger click event)
@ -851,3 +884,92 @@ export const getScopeData = (space: string, scope: number, standalone: boolean)
return { scopeRef: space, scopeIcon: 'nav-project' as IconName, scopeId: scope }
}
}
export enum RuleFields {
APPROVALS_REQUIRE_MINIMUM_COUNT = 'pullreq.approvals.require_minimum_count',
APPROVALS_REQUIRE_CODE_OWNERS = 'pullreq.approvals.require_code_owners',
APPROVALS_REQUIRE_NO_CHANGE_REQUEST = 'pullreq.approvals.require_no_change_request',
APPROVALS_REQUIRE_LATEST_COMMIT = 'pullreq.approvals.require_latest_commit',
COMMENTS_REQUIRE_RESOLVE_ALL = 'pullreq.comments.require_resolve_all',
STATUS_CHECKS_ALL_MUST_SUCCEED = 'pullreq.status_checks.all_must_succeed',
STATUS_CHECKS_REQUIRE_IDENTIFIERS = 'pullreq.status_checks.require_identifiers',
MERGE_STRATEGIES_ALLOWED = 'pullreq.merge.strategies_allowed',
MERGE_DELETE_BRANCH = 'pullreq.merge.delete_branch',
LIFECYCLE_CREATE_FORBIDDEN = 'lifecycle.create_forbidden',
LIFECYCLE_DELETE_FORBIDDEN = 'lifecycle.delete_forbidden',
MERGE_BLOCK = 'pullreq.merge.block',
LIFECYCLE_UPDATE_FORBIDDEN = 'lifecycle.update_forbidden',
LIFECYCLE_UPDATE_FORCE_FORBIDDEN = 'lifecycle.update_force_forbidden'
}
export type RuleFieldsMap = Record<RuleFields, boolean>
export type Rule = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
}
export type BranchProtectionRule = {
title: string
requiredRule: {
[key in RuleFields]?: boolean
}
}
export type BranchProtectionRulesMapType = Record<string, BranchProtectionRule>
export function createRuleFieldsMap(ruleDefinition: Rule): RuleFieldsMap {
const ruleFieldsMap: RuleFieldsMap = {
[RuleFields.APPROVALS_REQUIRE_MINIMUM_COUNT]: false,
[RuleFields.APPROVALS_REQUIRE_CODE_OWNERS]: false,
[RuleFields.APPROVALS_REQUIRE_NO_CHANGE_REQUEST]: false,
[RuleFields.APPROVALS_REQUIRE_LATEST_COMMIT]: false,
[RuleFields.COMMENTS_REQUIRE_RESOLVE_ALL]: false,
[RuleFields.STATUS_CHECKS_ALL_MUST_SUCCEED]: false,
[RuleFields.STATUS_CHECKS_REQUIRE_IDENTIFIERS]: false,
[RuleFields.MERGE_STRATEGIES_ALLOWED]: false,
[RuleFields.MERGE_DELETE_BRANCH]: false,
[RuleFields.LIFECYCLE_CREATE_FORBIDDEN]: false,
[RuleFields.LIFECYCLE_DELETE_FORBIDDEN]: false,
[RuleFields.MERGE_BLOCK]: false,
[RuleFields.LIFECYCLE_UPDATE_FORBIDDEN]: false,
[RuleFields.LIFECYCLE_UPDATE_FORCE_FORBIDDEN]: false
}
if (ruleDefinition.pullreq) {
if (ruleDefinition.pullreq.approvals) {
ruleFieldsMap[RuleFields.APPROVALS_REQUIRE_CODE_OWNERS] = !!ruleDefinition.pullreq.approvals.require_code_owners
ruleFieldsMap[RuleFields.APPROVALS_REQUIRE_LATEST_COMMIT] =
!!ruleDefinition.pullreq.approvals.require_latest_commit
ruleFieldsMap[RuleFields.APPROVALS_REQUIRE_MINIMUM_COUNT] =
typeof ruleDefinition.pullreq.approvals.require_minimum_count === 'number'
ruleFieldsMap[RuleFields.APPROVALS_REQUIRE_NO_CHANGE_REQUEST] =
!!ruleDefinition.pullreq.approvals.require_no_change_request
}
if (ruleDefinition.pullreq.comments) {
ruleFieldsMap[RuleFields.COMMENTS_REQUIRE_RESOLVE_ALL] = !!ruleDefinition.pullreq.comments.require_resolve_all
}
if (ruleDefinition.pullreq.merge) {
ruleFieldsMap[RuleFields.MERGE_BLOCK] = !!ruleDefinition.pullreq.merge.block
ruleFieldsMap[RuleFields.MERGE_DELETE_BRANCH] = !!ruleDefinition.pullreq.merge.delete_branch
ruleFieldsMap[RuleFields.MERGE_STRATEGIES_ALLOWED] =
Array.isArray(ruleDefinition.pullreq.merge.strategies_allowed) &&
ruleDefinition.pullreq.merge.strategies_allowed.length > 0
}
if (ruleDefinition.pullreq.status_checks) {
ruleFieldsMap[RuleFields.STATUS_CHECKS_REQUIRE_IDENTIFIERS] =
Array.isArray(ruleDefinition.pullreq.status_checks.require_identifiers) &&
ruleDefinition.pullreq.status_checks.require_identifiers.length > 0
}
}
if (ruleDefinition.lifecycle) {
ruleFieldsMap[RuleFields.LIFECYCLE_CREATE_FORBIDDEN] = !!ruleDefinition.lifecycle.create_forbidden
ruleFieldsMap[RuleFields.LIFECYCLE_DELETE_FORBIDDEN] = !!ruleDefinition.lifecycle.delete_forbidden
ruleFieldsMap[RuleFields.LIFECYCLE_UPDATE_FORBIDDEN] = !!ruleDefinition.lifecycle.update_forbidden
ruleFieldsMap[RuleFields.LIFECYCLE_UPDATE_FORCE_FORBIDDEN] = !!ruleDefinition.lifecycle.update_force_forbidden
}
return ruleFieldsMap
}