mirror of
https://github.com/harness/drone.git
synced 2025-05-03 20:39:48 +08:00

* fix: lint check * fix: page overflow * resolve comments * resolve comments * fix: space label addition * resolve comments : added enum * update: settings -> manage repository * update: resolved comments * update: bugbash comments * fix: ref exact scope for individual label values call * fix lint * add hook to handle current scope in HC and fix scope filter * update space delete * prettier check * update labelAPIs to use getConfig in base * support for harness-code labels * fix no result card * resolved comments for types * resolved comments * added sorting in labels and handled edge cases * fix: replacement of any value label * fix: spacing in value filter search input * add: update modal on click for spaces * added search for values in filter * fix: UI issues and some enhancements * handle empty labels list in space and remove tooltip for PR labels * added getConifg and any values for label filter * change label value color to enum * make value-id a pionter * update ordering * expose value id * handle long values * update search in label selector * handle edge cases * fix lint * added FF : CODE_PULLREQ_LABELS and standalone flag to labels * fix popover on scrolling and added strings * fix checks * fix checks * swagger update * labels support in Gitness
578 lines
18 KiB
TypeScript
578 lines
18 KiB
TypeScript
/*
|
|
* 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.
|
|
*/
|
|
|
|
// Code copied from https://github.com/vweevers/is-git-ref-name-valid and
|
|
// https://github.com/vweevers/is-git-branch-name-valid (MIT, © Vincent Weevers)
|
|
// 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,
|
|
OpenapiDirContent,
|
|
OpenapiGetContentOutput,
|
|
TypesCommit,
|
|
TypesPullReq,
|
|
RepoRepositoryOutput,
|
|
TypesRuleViolations
|
|
} from 'services/code'
|
|
import { getConfig } from 'services/config'
|
|
import { PullRequestSection, getErrorMessage } from './Utils'
|
|
|
|
export interface GitInfoProps {
|
|
repoMetadata: RepoRepositoryOutput
|
|
gitRef: string
|
|
resourcePath: string
|
|
resourceContent: OpenapiGetContentOutput
|
|
commitRef: string
|
|
commits: TypesCommit[]
|
|
pullReqMetadata: TypesPullReq
|
|
}
|
|
export interface RepoFormData {
|
|
name: string
|
|
description: string
|
|
license: string
|
|
defaultBranch: string
|
|
gitignore: string
|
|
addReadme: boolean
|
|
isPublic: RepoVisibility
|
|
}
|
|
export interface ImportFormData {
|
|
gitProvider: GitProviders
|
|
hostUrl: string
|
|
org: string
|
|
project: string
|
|
repo: string
|
|
username: string
|
|
password: string
|
|
name: string
|
|
description: string
|
|
importPipelineLabel: boolean
|
|
}
|
|
|
|
export interface ExportFormData {
|
|
accountId: string
|
|
token: string
|
|
organization: string
|
|
name: string
|
|
}
|
|
|
|
export interface ExportFormDataExtended extends ExportFormData {
|
|
repoCount: number
|
|
}
|
|
|
|
export interface ImportSpaceFormData {
|
|
gitProvider: GitProviders
|
|
username: string
|
|
password: string
|
|
name: string
|
|
description: string
|
|
organization: string
|
|
project: string
|
|
host: string
|
|
importPipelineLabel: boolean
|
|
}
|
|
|
|
export interface RepositorySummaryData {
|
|
default_branch_commit_count: number
|
|
branch_count: number
|
|
tag_count: number
|
|
pull_req_summary: {
|
|
open_count: number
|
|
closed_count: number
|
|
merged_count: number
|
|
}
|
|
}
|
|
|
|
export enum RepoVisibility {
|
|
PUBLIC = 'public',
|
|
PRIVATE = 'private'
|
|
}
|
|
|
|
export enum RepoCreationType {
|
|
IMPORT = 'import',
|
|
CREATE = 'create',
|
|
IMPORT_MULTIPLE = 'import_multiple'
|
|
}
|
|
|
|
export enum SpaceCreationType {
|
|
IMPORT = 'import',
|
|
CREATE = 'create'
|
|
}
|
|
|
|
export enum GitContentType {
|
|
FILE = 'file',
|
|
DIR = 'dir',
|
|
SYMLINK = 'symlink',
|
|
SUBMODULE = 'submodule'
|
|
}
|
|
export enum SettingsTab {
|
|
webhooks = 'webhook',
|
|
general = '/',
|
|
branchProtection = 'rules',
|
|
security = 'security',
|
|
labels = 'labels'
|
|
}
|
|
|
|
export enum SpaceSettingsTab {
|
|
general = '/',
|
|
labels = 'labels'
|
|
}
|
|
|
|
export enum VulnerabilityScanningType {
|
|
DETECT = 'detect',
|
|
BLOCK = 'block',
|
|
DISABLED = 'disabled'
|
|
}
|
|
|
|
export enum GitBranchType {
|
|
ACTIVE = 'active',
|
|
INACTIVE = 'inactive',
|
|
YOURS = 'yours',
|
|
ALL = 'all'
|
|
}
|
|
|
|
export enum GitRefType {
|
|
BRANCH = 'branch',
|
|
TAG = 'tag'
|
|
}
|
|
|
|
export enum PrincipalUserType {
|
|
USER = 'user',
|
|
SERVICE = 'service'
|
|
}
|
|
|
|
export enum SettingTypeMode {
|
|
EDIT = 'edit',
|
|
NEW = 'new'
|
|
}
|
|
|
|
export enum BranchTargetType {
|
|
INCLUDE = 'include',
|
|
EXCLUDE = 'exclude'
|
|
}
|
|
|
|
export interface BranchTargetOption {
|
|
type: BranchTargetType
|
|
title: string
|
|
}
|
|
|
|
export const branchTargetOptions: BranchTargetOption[] = [
|
|
{
|
|
type: BranchTargetType.INCLUDE,
|
|
title: 'Include'
|
|
},
|
|
{
|
|
type: BranchTargetType.EXCLUDE,
|
|
title: 'Exclude'
|
|
}
|
|
]
|
|
|
|
export enum GitCommitAction {
|
|
DELETE = 'DELETE',
|
|
CREATE = 'CREATE',
|
|
UPDATE = 'UPDATE',
|
|
MOVE = 'MOVE'
|
|
}
|
|
|
|
export enum PullRequestState {
|
|
OPEN = 'open',
|
|
MERGED = 'merged',
|
|
CLOSED = 'closed'
|
|
}
|
|
|
|
export enum GitProviders {
|
|
GITHUB = 'GitHub',
|
|
GITHUB_ENTERPRISE = 'GitHub Enterprise',
|
|
GITLAB = 'GitLab',
|
|
GITLAB_SELF_HOSTED = 'GitLab Self-Hosted',
|
|
BITBUCKET = 'Bitbucket',
|
|
BITBUCKET_SERVER = 'Bitbucket Server',
|
|
GITEA = 'Gitea',
|
|
GOGS = 'Gogs',
|
|
AZURE = 'Azure DevOps'
|
|
}
|
|
|
|
export enum ConvertPipelineLabel {
|
|
CONVERT = 'convert',
|
|
IGNORE = 'ignore'
|
|
}
|
|
|
|
export const PullRequestFilterOption = {
|
|
...PullRequestState,
|
|
// REJECTED: 'rejected',
|
|
DRAFT: 'draft',
|
|
YOURS: 'yours',
|
|
ALL: 'all'
|
|
}
|
|
|
|
export enum MergeStrategy {
|
|
MERGE = 'merge',
|
|
SQUASH = 'squash',
|
|
REBASE = 'rebase'
|
|
}
|
|
|
|
export const CodeIcon = {
|
|
Logo: 'code' as IconName,
|
|
PullRequest: 'git-pull' as IconName,
|
|
Merged: 'code-merged' as IconName,
|
|
Draft: 'code-draft' as IconName,
|
|
Rejected: 'code-rejected' as IconName,
|
|
PullRequestRejected: 'main-close' as IconName,
|
|
Add: 'plus' as IconName,
|
|
BranchSmall: 'code-branch-small' as IconName,
|
|
Branch: 'code-branch' as IconName,
|
|
Tag: 'main-tags' as IconName,
|
|
Clone: 'code-clone' as IconName,
|
|
Close: 'code-close' as IconName,
|
|
CommitLight: 'code-commit-light' as IconName,
|
|
CommitSmall: 'code-commit-small' as IconName,
|
|
Commit: 'code-commit' as IconName,
|
|
Copy: 'code-copy' as IconName,
|
|
Delete: 'code-delete' as IconName,
|
|
Edit: 'code-edit' as IconName,
|
|
FileLight: 'code-file-light' as IconName,
|
|
File: 'code-file' as IconName,
|
|
Folder: 'code-folder' as IconName,
|
|
History: 'code-history' as IconName,
|
|
Info: 'code-info' as IconName,
|
|
More: 'code-more' as IconName,
|
|
Repo: 'code-repo' as IconName,
|
|
Settings: 'code-settings' as IconName,
|
|
Webhook: 'code-webhook' as IconName,
|
|
InputSpinner: 'steps-spinne' as IconName,
|
|
InputSearch: 'search' as IconName,
|
|
Chat: 'code-chat' as IconName,
|
|
Checks: 'main-tick' as IconName,
|
|
ChecksSuccess: 'success-tick' as IconName,
|
|
CheckIcon: 'code-checks' as IconName
|
|
}
|
|
|
|
export const normalizeGitRef = (gitRef: string | undefined) => {
|
|
if (isRefATag(gitRef)) {
|
|
return gitRef
|
|
} else if (isRefABranch(gitRef)) {
|
|
return gitRef
|
|
} else if (gitRef === '') {
|
|
return ''
|
|
} else if (gitRef && isGitRev(gitRef)) {
|
|
return gitRef
|
|
} else {
|
|
return `refs/heads/${gitRef}`
|
|
}
|
|
}
|
|
|
|
export const REFS_TAGS_PREFIX = 'refs/tags/'
|
|
export const REFS_BRANCH_PREFIX = 'refs/heads/'
|
|
|
|
export const FILE_VIEWED_OBSOLETE_SHA = 'ffffffffffffffffffffffffffffffffffffffff'
|
|
|
|
export function formatTriggers(triggers: EnumWebhookTrigger[]) {
|
|
return triggers.map(trigger => {
|
|
return trigger
|
|
.split('_')
|
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(' ')
|
|
})
|
|
}
|
|
|
|
export const handleUpload = (
|
|
blob: File,
|
|
setMarkdownContent: (data: string) => void,
|
|
repoMetadata: RepoRepositoryOutput | undefined,
|
|
showError: (message: React.ReactNode, timeout?: number | undefined, key?: string | undefined) => void,
|
|
standalone: boolean,
|
|
routingId?: string
|
|
) => {
|
|
const reader = new FileReader()
|
|
// Set up a function to be called when the load event is triggered
|
|
reader.onload = async function () {
|
|
const markdown = await uploadImage(reader.result, showError, repoMetadata, standalone, routingId)
|
|
setMarkdownContent(markdown) // Set the markdown content
|
|
}
|
|
reader.readAsArrayBuffer(blob) // This will trigger the onload function when the reading is complete
|
|
}
|
|
|
|
export const uploadImage = async (
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
fileBlob: any,
|
|
showError: (message: React.ReactNode, timeout?: number | undefined, key?: string | undefined) => void,
|
|
repoMetadata: RepoRepositoryOutput | undefined,
|
|
standalone: boolean,
|
|
routingId?: string
|
|
) => {
|
|
try {
|
|
const response = await fetch(
|
|
`${window.location.origin}${getConfig(
|
|
`code/api/v1/repos/${repoMetadata?.path}/+/uploads${standalone || !routingId ? `` : `?routingId=${routingId}`}`
|
|
)}`,
|
|
{
|
|
method: 'POST',
|
|
headers: {
|
|
Accept: 'application/json',
|
|
'content-type': 'application/octet-stream'
|
|
},
|
|
body: fileBlob,
|
|
redirect: 'follow'
|
|
}
|
|
)
|
|
const result = await response.json()
|
|
if (!response.ok && result) {
|
|
showError(getErrorMessage(result))
|
|
return ''
|
|
}
|
|
const filePath = result.file_path
|
|
return `${window.location.origin}${getConfig(
|
|
`code/api/v1/repos/${repoMetadata?.path}/+/uploads/${filePath}${
|
|
standalone || !routingId ? `` : `?routingId=${routingId}`
|
|
}`
|
|
)}`
|
|
} catch (exception) {
|
|
showError(getErrorMessage(exception))
|
|
return ''
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-control-regex
|
|
const BAD_GIT_REF_REGREX = /(^|[/.])([/.]|$)|^@$|@{|[\x00-\x20\x7f~^:?*[\\]|\.lock(\/|$)/
|
|
const BAD_GIT_BRANCH_REGREX = /^(-|HEAD$)/
|
|
|
|
function isGitRefValid(name: string, onelevel: boolean): boolean {
|
|
return !BAD_GIT_REF_REGREX.test(name) && (!!onelevel || name.includes('/'))
|
|
}
|
|
|
|
export function isGitBranchNameValid(name: string): boolean {
|
|
return isGitRefValid(name, true) && !BAD_GIT_BRANCH_REGREX.test(name)
|
|
}
|
|
|
|
export const isDir = (content: Nullable<OpenapiGetContentOutput>): boolean => content?.type === GitContentType.DIR
|
|
export const isFile = (content: Nullable<OpenapiGetContentOutput>): boolean => content?.type === GitContentType.FILE
|
|
export const isSymlink = (content: Nullable<OpenapiGetContentOutput>): boolean =>
|
|
content?.type === GitContentType.SYMLINK
|
|
export const isSubmodule = (content: Nullable<OpenapiGetContentOutput>): boolean =>
|
|
content?.type === GitContentType.SUBMODULE
|
|
|
|
export const findReadmeInfo = (content: Nullable<OpenapiGetContentOutput>): OpenapiContentInfo | undefined =>
|
|
(content?.content as OpenapiDirContent)?.entries?.find(
|
|
entry => entry.type === GitContentType.FILE && /^readme(.md)?$/.test(entry?.name?.toLowerCase() || '')
|
|
)
|
|
|
|
export const findMarkdownInfo = (content: Nullable<OpenapiGetContentOutput>): OpenapiContentInfo | undefined =>
|
|
content?.type === GitContentType.FILE && /.md$/.test(content?.name?.toLowerCase() || '') ? content : undefined
|
|
|
|
export const isRefATag = (gitRef: string | undefined) => gitRef?.includes(REFS_TAGS_PREFIX) || false
|
|
export const isRefABranch = (gitRef: string | undefined) => gitRef?.includes(REFS_BRANCH_PREFIX) || false
|
|
|
|
/**
|
|
* Make a diff refs string to use in URL.
|
|
* @param targetGitRef target git ref (base ref).
|
|
* @param sourceGitRef source git ref (compare ref).
|
|
* @returns A concatenation string of `targetGitRef...sourceGitRef`.
|
|
*/
|
|
export const makeDiffRefs = (targetGitRef: string, sourceGitRef: string) => `${targetGitRef}...${sourceGitRef}`
|
|
|
|
/**
|
|
* Split a diff refs string into targetRef and sourceRef.
|
|
* @param diffRefs diff refs string.
|
|
* @returns An object of { targetGitRef, sourceGitRef }
|
|
*/
|
|
export const diffRefsToRefs = (diffRefs: string) => {
|
|
const parts = diffRefs.split('...')
|
|
|
|
return {
|
|
targetGitRef: parts[0] || '',
|
|
sourceGitRef: parts[1] || ''
|
|
}
|
|
}
|
|
|
|
export const decodeGitContent = (content = '') => {
|
|
try {
|
|
// Decode base64 content for text file
|
|
return decodeURIComponent(escape(window.atob(content)))
|
|
} catch (_exception) {
|
|
try {
|
|
// Return original base64 content for binary file
|
|
return content
|
|
} catch (exception) {
|
|
console.error(exception) // eslint-disable-line no-console
|
|
}
|
|
}
|
|
return ''
|
|
}
|
|
|
|
// Check if gitRef is a git commit hash (https://github.com/diegohaz/is-git-rev, MIT © Diego Haz)
|
|
export const isGitRev = (gitRef = ''): boolean => /^[0-9a-f]{7,40}$/i.test(gitRef)
|
|
|
|
export const getProviderTypeMapping = (provider: GitProviders): string => {
|
|
switch (provider) {
|
|
case GitProviders.BITBUCKET_SERVER:
|
|
return 'stash'
|
|
case GitProviders.GITHUB_ENTERPRISE:
|
|
return 'github'
|
|
case GitProviders.GITLAB_SELF_HOSTED:
|
|
return 'gitlab'
|
|
case GitProviders.AZURE:
|
|
return 'azure'
|
|
default:
|
|
return provider.toLowerCase()
|
|
}
|
|
}
|
|
|
|
export const getOrgLabel = (gitProvider: string) => {
|
|
switch (gitProvider) {
|
|
case GitProviders.BITBUCKET:
|
|
return 'importRepo.workspace'
|
|
case GitProviders.BITBUCKET_SERVER:
|
|
return 'importRepo.project'
|
|
case GitProviders.GITLAB:
|
|
case GitProviders.GITLAB_SELF_HOSTED:
|
|
return 'importRepo.group'
|
|
default:
|
|
return 'importRepo.org'
|
|
}
|
|
}
|
|
|
|
export const getOrgPlaceholder = (gitProvider: string) => {
|
|
switch (gitProvider) {
|
|
case GitProviders.BITBUCKET:
|
|
return 'importRepo.workspacePlaceholder'
|
|
case GitProviders.BITBUCKET_SERVER:
|
|
return 'importRepo.projectPlaceholder'
|
|
case GitProviders.GITLAB:
|
|
case GitProviders.GITLAB_SELF_HOSTED:
|
|
return 'importRepo.groupPlaceholder'
|
|
default:
|
|
return 'importRepo.orgPlaceholder'
|
|
}
|
|
}
|
|
|
|
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)
|
|
})
|
|
}
|
|
}
|