mirror of https://github.com/harness/drone.git
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 branchCODE-2402
parent
1725841f67
commit
534f5a8293
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
),
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue