feat: [CODE-2411] add support for Fast-forward merge (#2803)

* feat: [CODE-2411] add support for Fast-forward merge
pull/3576/head
Ritik Kapoor 2024-10-10 22:36:19 +00:00 committed by Harness
parent bbb7bce02a
commit f6ac58f036
18 changed files with 167 additions and 34 deletions

View File

@ -166,6 +166,9 @@ const BranchProtectionForm = (props: {
const isRebasePresent = (rule.definition as ProtectionBranch)?.pullreq?.merge?.strategies_allowed?.includes(
MergeStrategy.REBASE
)
const isFFMergePresent = (rule.definition as ProtectionBranch)?.pullreq?.merge?.strategies_allowed?.includes(
MergeStrategy.FAST_FORWARD
)
// List of strings to be included in the final array
const includeList = (rule?.pattern as ProtectionPattern)?.include ?? []
const excludeList = (rule?.pattern as ProtectionPattern)?.exclude ?? []
@ -201,6 +204,7 @@ const BranchProtectionForm = (props: {
mergeCommit: isMergePresent,
squashMerge: isSquashPresent,
rebaseMerge: isRebasePresent,
fastForwardMerge: isFFMergePresent,
autoDelete: (rule.definition as ProtectionBranch)?.pullreq?.merge?.delete_branch,
blockBranchCreation: (rule.definition as ProtectionBranch)?.lifecycle?.create_forbidden,
blockBranchUpdate:
@ -244,7 +248,8 @@ const BranchProtectionForm = (props: {
const stratArray = [
formData.squashMerge && MergeStrategy.SQUASH,
formData.rebaseMerge && MergeStrategy.REBASE,
formData.mergeCommit && MergeStrategy.MERGE
formData.mergeCommit && MergeStrategy.MERGE,
formData.fastForwardMerge && MergeStrategy.FAST_FORWARD
].filter(Boolean) as EnumMergeMethod[]
const includeArray =
formData?.targetList?.filter(([type]) => type === 'include').map(([, value]) => value) ?? []

View File

@ -269,6 +269,11 @@ const ProtectionRulesForm = (props: {
<FormInput.CheckBox className={css.minText} label={getString('mergeCommit')} name={'mergeCommit'} />
<FormInput.CheckBox className={css.minText} label={getString('squashMerge')} name={'squashMerge'} />
<FormInput.CheckBox className={css.minText} label={getString('rebaseMerge')} name={'rebaseMerge'} />
<FormInput.CheckBox
className={css.minText}
label={getString('fastForwardMerge')}
name={'fastForwardMerge'}
/>
</Container>
</Container>
)}

View File

@ -424,6 +424,7 @@ export interface StringsMap {
failedToFetchFileContent: string
failedToImportSpace: string
failedToSavePipeline: string
fastForwardMerge: string
featureRoadmap: string
fileDeleted: string
fileTooLarge: string
@ -801,6 +802,8 @@ export interface StringsMap {
'pr.mergeOptions.createAMergeCommit': string
'pr.mergeOptions.createMergeCommit': string
'pr.mergeOptions.createMergeCommitDesc': string
'pr.mergeOptions.fastForwardMerge': string
'pr.mergeOptions.fastForwardMergeDesc': string
'pr.mergeOptions.rebaseAndMerge': string
'pr.mergeOptions.rebaseAndMergeDesc': string
'pr.mergeOptions.squashAndMerge': string
@ -825,6 +828,7 @@ export interface StringsMap {
'pr.prStateChanged': string
'pr.prStateChangedDraft': string
'pr.readyForReview': string
'pr.rebaseMergePossible': string
'pr.removeSuggestion': string
'pr.requestSubmitted': string
'pr.requestedChanges': string
@ -995,6 +999,7 @@ export interface StringsMap {
'securitySettings.vulnerabilityScanning': string
'securitySettings.vulnerabilityScanningDesc': string
seeNMoreMatches: string
selectAuthor: string
selectBranchPlaceHolder: string
selectLanguagePlaceholder: string
selectMergeStrat: string

View File

@ -302,6 +302,7 @@ pr:
reviewChanges: Review changes
mergePR: Merge pull request
branchHasNoConflicts: Pull request can be merged
rebaseMergePossible: Pull request can be merged after rebase
checkingToMerge: Checking for ability to merge automatically...
prCanBeMerged: Mergeing can be performed automatically.
enterDesc: Enter description here
@ -341,6 +342,8 @@ pr:
createMergeCommitDesc: All commits from this branch will be added to the base branch via a merge commit.
rebaseAndMerge: Rebase and merge
rebaseAndMergeDesc: All commits from this branch will be rebased and added to the base branch.
fastForwardMerge: Fast-forward merge
fastForwardMergeDesc: All commits from this branch will be added to the base branch without a merge commit. Rebase may be required.
close: Close pull request
closeDesc: Close this pull request. You can still re-open the request after closing.
createAMergeCommit: Create a merge commit
@ -539,6 +542,7 @@ zoomIn: Zoom In
zoomOut: Zoom Out
checks: Checks
blameCommitLine: '{author} committed {timestamp}'
selectAuthor: Select Author
tooltipRepoEdit: You are not authorized to {PERMS}
missingPerms: 'You are missing the following permission:'
createRepoPerms: 'Create / Edit Repository'
@ -964,6 +968,7 @@ setting: Setting
mergeCommit: Merge commit
squashMerge: Squash and merge
rebaseMerge: Rebase and merge
fastForwardMerge: Fast-forward merge
Enable: Enable
imageUpload:
title: Upload attachment

View File

@ -27,6 +27,7 @@ import { MergeStrategy } from 'utils/GitUtils'
import mergeVideo from '../../../../videos/merge.mp4'
import squashVideo from '../../../../videos/squash.mp4'
import rebaseVideo from '../../../../videos/rebase.mp4'
import fastForward from '../../../../videos/fastForward.mp4'
import css from './PullRequestActionsBox.module.scss'
interface InlineMergeBoxProps {
@ -96,6 +97,8 @@ const InlineMergeBox = (props: InlineMergeBoxProps) => {
<video height={36} width={148} src={rebaseVideo} autoPlay={true} loop={false} muted={true} />
) : mergeOption.method === MergeStrategy.SQUASH ? (
<video height={36} width={148} src={squashVideo} autoPlay={true} loop={false} muted={true} />
) : mergeOption.method === MergeStrategy.FAST_FORWARD ? (
<video height={36} width={148} src={fastForward} autoPlay={true} loop={false} muted={true} />
) : (
<video height={36} width={148} src={mergeVideo} autoPlay={true} loop={false} muted={true} />
)}
@ -142,11 +145,12 @@ const InlineMergeBox = (props: InlineMergeBoxProps) => {
{(mergeOption.method === MergeStrategy.SQUASH || mergeOption.method === MergeStrategy.MERGE) && (
<FormInput.Text name="commitTitle"></FormInput.Text>
)}
{mergeOption.method !== MergeStrategy.REBASE && (
<FormInput.TextArea
placeholder={getString('addOptionalCommitMessage')}
name="commitMessage"></FormInput.TextArea>
)}
{mergeOption.method !== MergeStrategy.REBASE &&
mergeOption.method !== MergeStrategy.FAST_FORWARD && (
<FormInput.TextArea
placeholder={getString('addOptionalCommitMessage')}
name="commitMessage"></FormInput.TextArea>
)}
</FormikForm>
)
}}

View File

@ -89,6 +89,8 @@
&.merged {
font-weight: unset !important;
color: var(--purple-700) !important;
white-space: nowrap !important;
font-size: 14px !important;
}
&.draft {
@ -108,7 +110,7 @@
.boldText {
color: var(--purple-700) !important;
font-weight: 600 !important;
font-size: 16px !important;
font-size: 14px !important;
line-height: 24px !important;
}
.widthContainer {

View File

@ -30,17 +30,18 @@ import {
useToaster
} from '@harnessio/uicore'
import { Icon } from '@harnessio/icons'
import { Color } from '@harnessio/design-system'
import { Color, FontVariation } from '@harnessio/design-system'
import { MutateMethod, useMutate } from 'restful-react'
import { Case, Else, Match, Render, Truthy } from 'react-jsx-match'
import { Menu, PopoverPosition, Icon as BIcon } from '@blueprintjs/core'
import cx from 'classnames'
import ReactTimeago from 'react-timeago'
import { defaultTo } from 'lodash-es'
import type {
CreateBranchPathParams,
DeletePullReqSourceBranchQueryParams,
OpenapiCreateBranchRequest,
OpenapiStatePullReqRequest,
RebaseBranchRequestBody,
TypesListCommitResponse,
TypesPullReq,
TypesRuleViolations
@ -58,9 +59,9 @@ import {
permissionProps
} from 'utils/Utils'
import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButton'
import { UserPreference, useUserPreference } from 'hooks/useUserPreference'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
import { PullReqSuggestionsBatch } from 'components/PullReqSuggestionsBatch/PullReqSuggestionsBatch'
import { TimePopoverWithLocal } from 'utils/timePopoverLocal/TimePopoverWithLocal'
import { BranchActionsButton } from '../PullRequestOverviewPanel/sections/BranchActionsSection'
import InlineMergeBox from './InlineMergeBox'
import css from './PullRequestActionsBox.module.scss'
@ -83,6 +84,9 @@ export interface PullRequestActionsBoxProps extends Pick<GitInfoProps, 'repoMeta
setShowDeleteBranchButton: React.Dispatch<React.SetStateAction<boolean>>
setShowRestoreBranchButton: React.Dispatch<React.SetStateAction<boolean>>
isSourceBranchDeleted: boolean
mergeOption: PRMergeOption
setMergeOption: (val: PRMergeOption) => void
rebasePossible: boolean
}
export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
@ -102,10 +106,13 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
showDeleteBranchButton,
setShowRestoreBranchButton,
setShowDeleteBranchButton,
isSourceBranchDeleted
isSourceBranchDeleted,
mergeOption,
setMergeOption,
rebasePossible
}) => {
const { getString } = useStrings()
const { showError } = useToaster()
const { showSuccess, showError } = useToaster()
const inlineMergeRef = useRef<inlineMergeFormRefType>(null)
const { hooks, standalone } = useAppContext()
const space = useGetSpaceParam()
@ -122,6 +129,19 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
verb: 'POST',
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullReqMetadata.number}/state`
})
const { mutate: rebase } = useMutate<RebaseBranchRequestBody>({
verb: 'POST',
path: `/api/v1/repos/${repoMetadata.path}/+/rebase`
})
const rebaseRequestPayload = {
base_branch: pullReqMetadata.target_branch,
bypass_rules: true,
dry_run_rules: false,
head_branch: pullReqMetadata.source_branch,
head_commit_sha: pullReqMetadata.source_sha
}
const mergeable = useMemo(() => pullReqMetadata.merge_check_status === MergeCheckStatus.MERGEABLE, [pullReqMetadata])
const isClosed = pullReqMetadata.state === PullRequestState.CLOSED
const isOpen = pullReqMetadata.state === PullRequestState.OPEN
@ -193,11 +213,12 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
} // eslint-disable-next-line react-hooks/exhaustive-deps
}, [onPRStateChanged, isMerged, isClosed, pullReqMetadata?.source_sha])
const mergeOptions = useMemo(() => getMergeOptions(getString, mergeable).slice(0, 3), [mergeable])
const mergeOptions = useMemo(() => getMergeOptions(getString, mergeable).slice(0, 4), [mergeable])
const [allowedStrats, setAllowedStrats] = useState<string[]>([
mergeOptions[0].method,
mergeOptions[1].method,
mergeOptions[2].method
mergeOptions[2].method,
mergeOptions[3].method
])
const draftOptions: PRDraftOption[] = [
{
@ -212,11 +233,6 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
}
]
const [showInlineMergeContainer, setShowInlineMergeContainer] = useState(false)
const [mergeOption, setMergeOption] = useUserPreference<PRMergeOption>(
UserPreference.PULL_REQUEST_MERGE_STRATEGY,
mergeOptions[0],
option => option.method !== 'close'
)
useEffect(() => {
if (allowedStrats) {
@ -283,6 +299,7 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
className={cx(css.main, {
[css.primary]: !PRStateLoading,
[css.error]: mergeable === false && !unchecked && !isClosed && !isDraft,
[css.error]: mergeOption.method === MergeStrategy.FAST_FORWARD && rebasePossible,
[css.unchecked]: unchecked,
[css.closed]: isClosed,
[css.draft]: isDraft,
@ -302,6 +319,7 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
[css.draft]: isDraft,
[css.closed]: isClosed,
[css.unmergeable]: mergeable === false && isOpen,
[css.unmergeable]: mergeOption.method === MergeStrategy.FAST_FORWARD && rebasePossible && isOpen,
[css.ruleViolate]: ruleViolation && !isClosed
})}>
{getString(
@ -315,6 +333,8 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
? 'branchProtection.prFailedText'
: ruleViolation
? 'branchProtection.prFailedText'
: mergeOption.method === MergeStrategy.FAST_FORWARD && rebasePossible
? 'branchProtection.prFailedText'
: 'pr.branchHasNoConflicts'
)}
</Text>
@ -473,7 +493,9 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
<Button
type="submit"
onClick={handleSubmit}
disabled={isMerged}
disabled={
isMerged || (mergeOption.method === MergeStrategy.FAST_FORWARD && rebasePossible)
}
variation={ButtonVariation.PRIMARY}
text={getString('confirmStrat', { strat: mergeOption.title })}
/>
@ -506,7 +528,25 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
})
.catch(exception => showError(getErrorMessage(exception)))
}
}
},
...(rebasePossible
? [
{
hasIcon: true,
iconName: 'code-pull',
text: getString('rebase'),
onClick: () =>
rebase(rebaseRequestPayload)
.then(() => {
showSuccess(getString('updatedBranchMessageRebase'))
setTimeout(() => {
refetchActivities()
}, 1000)
})
.catch(err => showError(getErrorMessage(err)))
}
]
: [])
]}
tooltipProps={{
interactionKind: 'click',
@ -593,7 +633,15 @@ const MergeInfo: React.FC<{
</strong>
</Container>
),
time: <ReactTimeago className={css.dateText} date={pullRequestMetadata.merged as number} />
time: (
<TimePopoverWithLocal
className={css.dateText}
time={defaultTo(pullRequestMetadata.merged as number, 0)}
inline={false}
font={{ variation: FontVariation.SMALL }}
color={Color.GREY_400}
/>
)
}}
/>
</Text>

View File

@ -30,15 +30,17 @@ import type {
TypesBranch
} from 'services/code'
import {
PRMergeOption,
PanelSectionOutletPosition,
extractSpecificViolations,
getMergeOptions
} from 'pages/PullRequest/PullRequestUtils'
import { MergeCheckStatus, extractInfoFromRuleViolationArr } from 'utils/Utils'
import { PullRequestState, dryMerge } from 'utils/GitUtils'
import { MergeStrategy, PullRequestState, dryMerge } from 'utils/GitUtils'
import { useStrings } from 'framework/strings'
import type { PRChecksDecisionResult } from 'hooks/usePRChecksDecision'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
import { UserPreference, useUserPreference } from 'hooks/useUserPreference'
import { PullRequestActionsBox } from '../PullRequestActionsBox/PullRequestActionsBox'
import PullRequestPanelSections from './PullRequestPanelSections'
import ChecksSection from './sections/ChecksSection'
@ -241,6 +243,12 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
[pullReqMetadata]
)
const [mergeOption, setMergeOption] = useUserPreference<PRMergeOption>(
UserPreference.PULL_REQUEST_MERGE_STRATEGY,
mergeOptions[0],
option => option.method !== 'close'
)
return (
<Container margin={{ bottom: 'medium' }} className={css.mainContainer}>
<Layout.Vertical>
@ -264,6 +272,9 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
setShowDeleteBranchButton={setShowDeleteBranchButton}
setShowRestoreBranchButton={setShowRestoreBranchButton}
isSourceBranchDeleted={isSourceBranchDeleted}
mergeOption={mergeOption}
setMergeOption={setMergeOption}
rebasePossible={rebasePossible}
/>
{!isClosed ? (
<PullRequestPanelSections
@ -312,7 +323,8 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
),
[PanelSectionOutletPosition.REBASE_SOURCE_BRANCH]: rebasePossible &&
!mergeLoading &&
!conflictingFiles?.length && (
!conflictingFiles?.length &&
mergeOption.method === MergeStrategy.FAST_FORWARD && (
<RebaseSourceSection
pullReqMetadata={pullReqMetadata}
repoMetadata={repoMetadata}

View File

@ -87,6 +87,7 @@ export const BranchActionsButton = ({
return (
<Button
style={{ whiteSpace: 'nowrap' }}
text={showDeleteBranchButton ? getString('deleteBranch') : getString('restoreBranch')}
variation={ButtonVariation.SECONDARY}
onClick={() => {

View File

@ -33,7 +33,7 @@ import { GitRefLink } from 'components/GitRefLink/GitRefLink'
import { getErrorMessage, permissionProps } from 'utils/Utils'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import { useAppContext } from 'AppContext'
import Fail from '../../../../../icons/code-fail-grey.svg?url'
import FailRed from '../../../../../icons/code-fail.svg?url'
import css from '../PullRequestOverviewPanel.module.scss'
interface RebaseSourceSectionProps {
@ -80,9 +80,9 @@ const RebaseSourceSection = (props: RebaseSourceSectionProps) => {
<Container className={cx(css.sectionContainer, css.borderRadius)}>
<Layout.Horizontal flex={{ justifyContent: 'space-between' }}>
<Layout.Horizontal flex={{ alignItems: 'center' }}>
<img alt={getString('failed')} width={26} height={26} color={Color.GREY_500} src={Fail} />
<img alt={getString('failed')} width={26} height={26} src={FailRed} />
<Layout.Vertical padding={{ left: 'medium' }}>
<Text padding={{ bottom: 'xsmall' }} className={css.sectionTitle} color={Color.GREY_600}>
<Text padding={{ bottom: 'xsmall' }} className={css.sectionTitle} color={Color.RED_500}>
{getString('rebaseSource.title')}
</Text>
<Text className={css.sectionSubheader} color={Color.GREY_450} font={{ variation: FontVariation.BODY }}>

View File

@ -65,7 +65,7 @@ export const SystemComment: React.FC<SystemCommentProps> = ({ pullReqMetadata, c
</Container>
<Avatar name={pullReqMetadata.merger?.display_name} size="small" hoverCard={false} />
<Text flex tag="div">
<Text flex tag="div" style={{ whiteSpace: 'nowrap' }}>
<StringSubstitute
str={
(payload?.payload as MergePayload)?.merge_method === MergeStrategy.REBASE
@ -74,8 +74,16 @@ export const SystemComment: React.FC<SystemCommentProps> = ({ pullReqMetadata, c
}
vars={{
user: <strong className={css.rightTextPadding}>{pullReqMetadata.merger?.display_name}</strong>,
source: <strong className={css.textPadding}>{pullReqMetadata.source_branch}</strong>,
target: <strong className={css.textPadding}>{pullReqMetadata.target_branch}</strong>,
source: (
<Text lineClamp={1}>
<strong className={css.textPadding}>{pullReqMetadata.source_branch}</strong>
</Text>
),
target: (
<Text lineClamp={1}>
<strong className={css.textPadding}>{pullReqMetadata.target_branch}</strong>
</Text>
),
bypassed: (payload?.payload as MergePayload)?.rules_bypassed,
mergeSha: (
<Container className={css.commitContainer} padding={{ left: 'small', right: 'xsmall' }}>

View File

@ -113,6 +113,14 @@ export const getMergeOptions = (getString: UseStringsReturn['getString'], mergea
label: getString('pr.mergeOptions.rebaseAndMerge'),
value: MergeStrategy.REBASE
},
{
method: MergeStrategy.FAST_FORWARD,
title: getString('pr.mergeOptions.fastForwardMerge'),
desc: getString('pr.mergeOptions.fastForwardMergeDesc'),
disabled: mergeable === false,
label: getString('pr.mergeOptions.fastForwardMerge'),
value: MergeStrategy.FAST_FORWARD
},
{
method: 'close',
title: getString('pr.mergeOptions.close'),

View File

@ -209,7 +209,7 @@ export function PullRequestsContentHeader({
popoverClassName={css.branchDropdown}
icon="nav-user-profile"
iconProps={{ size: 16 }}
placeholder="Select Authors"
placeholder={getString('selectAuthor')}
addClearBtn={true}
resetOnClose
resetOnSelect

View File

@ -116,7 +116,7 @@ export type EnumMembershipRole = 'contributor' | 'executor' | 'reader' | 'space_
export type EnumMergeCheckStatus = string
export type EnumMergeMethod = 'merge' | 'rebase' | 'squash'
export type EnumMergeMethod = 'fast-forward' | 'merge' | 'rebase' | 'squash'
export type EnumParentResourceType = 'space' | 'repo'
@ -1156,9 +1156,12 @@ export type TypesGitspaceInstance = {
access_key?: string | null
access_key_ref?: string | null
access_type?: EnumGitspaceAccessType
active_time_ended?: number | null
active_time_started?: number | null
created?: number
identifier?: string
last_used?: number
last_heartbeat?: number | null
last_used?: number | null
machine_user?: string | null
resource_usage?: string | null
space_path?: string
@ -1475,6 +1478,9 @@ export interface TypesPullReqStats {
}
export interface TypesRebaseResponse {
already_ancestor?: boolean
conflict_files?: string[]
dry_run?: boolean
dry_run_rules?: boolean
new_head_branch_sha?: ShaSHA
rule_violations?: TypesRuleViolations[]
@ -6240,6 +6246,7 @@ export interface RebaseBranchPathParams {
export interface RebaseBranchRequestBody {
base_branch?: string
bypass_rules?: boolean
dry_run?: boolean
dry_run_rules?: boolean
head_branch?: string
head_commit_sha?: ShaSHA

View File

@ -6225,6 +6225,8 @@ paths:
type: string
bypass_rules:
type: boolean
dry_run:
type: boolean
dry_run_rules:
type: boolean
head_branch:
@ -10540,6 +10542,7 @@ components:
type: string
EnumMergeMethod:
enum:
- fast-forward
- merge
- rebase
- squash
@ -12408,11 +12411,21 @@ components:
type: string
access_type:
$ref: '#/components/schemas/EnumGitspaceAccessType'
active_time_ended:
nullable: true
type: integer
active_time_started:
nullable: true
type: integer
created:
type: integer
identifier:
type: string
last_heartbeat:
nullable: true
type: integer
last_used:
nullable: true
type: integer
machine_user:
nullable: true
@ -13013,6 +13026,14 @@ components:
type: object
TypesRebaseResponse:
properties:
already_ancestor:
type: boolean
conflict_files:
items:
type: string
type: array
dry_run:
type: boolean
dry_run_rules:
type: boolean
new_head_branch_sha:

View File

@ -233,7 +233,8 @@ export const PullRequestFilterOption = {
export enum MergeStrategy {
MERGE = 'merge',
SQUASH = 'squash',
REBASE = 'rebase'
REBASE = 'rebase',
FAST_FORWARD = 'fast-forward'
}
export const CodeIcon = {

View File

@ -385,6 +385,7 @@ export type RulesFormPayload = {
mergeCommit?: boolean
squashMerge?: boolean
rebaseMerge?: boolean
fastForwardMerge?: boolean
autoDelete?: boolean
blockBranchCreation?: boolean
blockBranchDeletion?: boolean

Binary file not shown.