feat: [CODE-2338]: Add dry run rules param to create branch API (#2680)

* addressed review comments
* Merge branch 'main' of https://git0.harness.io/l7B_kbSEQD2wjrM7PShm5w/PROD/Harness_Commons/gitness into CODE-2346
* feat: [CODE-2346]: Add an option to restore branch for merged/closed PR
* feat: [CODE-2338]: Add dry run rules param to create branch API
CODE-2402
Karan Saraswat 2024-09-11 11:50:04 +00:00 committed by Harness
parent 80105035f4
commit 2f4b0036d1
11 changed files with 204 additions and 122 deletions

View File

@ -127,6 +127,7 @@ export interface StringsMap {
'branchProtection.targetPlaceholder': string
'branchProtection.title': string
branchProtectionRules: string
branchRestored: string
branchSource: string
branchSourceDesc: string
branchTagCreation: string
@ -908,6 +909,7 @@ export interface StringsMap {
resolveComments: string
resolved: string
resolvedComments: string
restoreBranch: string
results: string
reviewProjectSettings: string
reviewerNotFound: string

View File

@ -176,6 +176,7 @@ branchCreated: Branch {branch} created.
tagCreated: Tag {{tag}} created.
confirmation: Confirmation
deleteBranch: Delete Branch
restoreBranch: Restore Branch
deleteTag: Delete Tag
deleteTagConfirm: Are you sure you want to delete tag <strong>{{name}}</strong>? You can't undo this action.
deleteBranchConfirm: Are you sure you want to delete branch <strong>{{name}}</strong>? You can't undo this action.
@ -187,6 +188,7 @@ compare: Compare
commitString: 'Commit {commit}'
repoDeleted: Repository {{repo}} deleted.
branchDeleted: Branch {branch} deleted.
branchRestored: Branch {branch} restored.
tagDeleted: Tag {tag} deleted.
failedToDeleteBranch: Failed to delete Branch. Please try again.
createFile: Create __path__

View File

@ -37,6 +37,9 @@ import { Menu, PopoverPosition, Icon as BIcon } from '@blueprintjs/core'
import cx from 'classnames'
import ReactTimeago from 'react-timeago'
import type {
CreateBranchPathParams,
DeleteBranchQueryParams,
OpenapiCreateBranchRequest,
OpenapiStatePullReqRequest,
TypesListCommitResponse,
TypesPullReq,
@ -58,6 +61,7 @@ import { OptionsMenuButton } from 'components/OptionsMenuButton/OptionsMenuButto
import { UserPreference, useUserPreference } from 'hooks/useUserPreference'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
import { PullReqSuggestionsBatch } from 'components/PullReqSuggestionsBatch/PullReqSuggestionsBatch'
import { BranchActionsButton } from '../PullRequestOverviewPanel/sections/BranchActionsSection'
import InlineMergeBox from './InlineMergeBox'
import css from './PullRequestActionsBox.module.scss'
@ -71,18 +75,13 @@ export interface PullRequestActionsBoxProps extends Pick<GitInfoProps, 'repoMeta
setConflictingFiles: React.Dispatch<React.SetStateAction<string[] | undefined>>
refetchPullReq: () => void
refetchActivities: () => void
deleteBranch: MutateMethod<
any,
any,
{
bypass_rules: boolean
dry_run_rules: boolean
commit_sha: string
},
unknown
>
createBranch: MutateMethod<any, any, OpenapiCreateBranchRequest, CreateBranchPathParams>
refetchBranch: () => Promise<void>
deleteBranch: MutateMethod<any, any, DeleteBranchQueryParams, unknown>
showRestoreBranchButton: boolean
showDeleteBranchButton: boolean
setShowDeleteBranchButton: React.Dispatch<React.SetStateAction<boolean>>
setShowRestoreBranchButton: React.Dispatch<React.SetStateAction<boolean>>
isSourceBranchDeleted: boolean
}
@ -96,13 +95,17 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
setConflictingFiles,
refetchPullReq,
refetchActivities,
createBranch,
refetchBranch,
deleteBranch,
showRestoreBranchButton,
showDeleteBranchButton,
setShowRestoreBranchButton,
setShowDeleteBranchButton,
isSourceBranchDeleted
}) => {
const { getString } = useStrings()
const { showSuccess, showError } = useToaster()
const { showError } = useToaster()
const inlineMergeRef = useRef<inlineMergeFormRefType>(null)
const { hooks, standalone } = useAppContext()
const space = useGetSpaceParam()
@ -263,11 +266,13 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
return (
<MergeInfo
pullRequestMetadata={pullReqMetadata}
showRestoreBranchButton={showRestoreBranchButton}
showDeleteBranchButton={showDeleteBranchButton}
setShowDeleteBranchButton={setShowDeleteBranchButton}
setShowRestoreBranchButton={setShowRestoreBranchButton}
refetchBranch={refetchBranch}
createBranch={createBranch}
deleteBranch={deleteBranch}
showSuccess={showSuccess}
showError={showError}
/>
)
}
@ -542,28 +547,15 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
const MergeInfo: React.FC<{
pullRequestMetadata: TypesPullReq
showRestoreBranchButton: boolean
showDeleteBranchButton: boolean
setShowDeleteBranchButton: React.Dispatch<React.SetStateAction<boolean>>
deleteBranch: MutateMethod<
any,
any,
{
bypass_rules: boolean
dry_run_rules: boolean
commit_sha: string
},
unknown
>
showSuccess: (message: React.ReactNode, timeout?: number, key?: string) => void
showError: (message: React.ReactNode, timeout?: number, key?: string) => void
}> = ({
pullRequestMetadata,
showDeleteBranchButton,
setShowDeleteBranchButton,
deleteBranch,
showSuccess,
showError
}) => {
setShowRestoreBranchButton: React.Dispatch<React.SetStateAction<boolean>>
refetchBranch: () => Promise<void>
createBranch: MutateMethod<any, any, OpenapiCreateBranchRequest, CreateBranchPathParams>
deleteBranch: MutateMethod<any, any, DeleteBranchQueryParams, unknown>
}> = props => {
const { pullRequestMetadata, showRestoreBranchButton, showDeleteBranchButton } = props
const { getString } = useStrings()
return (
@ -606,36 +598,11 @@ const MergeInfo: React.FC<{
/>
</Text>
<FlexExpander />
{showDeleteBranchButton && (
<Button
margin={{ right: 'small' }}
text={getString('deleteBranch')}
variation={ButtonVariation.SECONDARY}
onClick={() => {
deleteBranch(
{},
{
queryParams: {
bypass_rules: true,
dry_run_rules: false,
commit_sha: pullRequestMetadata?.source_sha || ''
}
}
)
.then(() => {
setShowDeleteBranchButton(false)
showSuccess(
<StringSubstitute
str={getString('branchDeleted')}
vars={{
branch: pullRequestMetadata?.source_branch
}}
/>,
5000
)
})
.catch(err => showError(getErrorMessage(err)))
}}
{(showDeleteBranchButton || showRestoreBranchButton) && (
<BranchActionsButton
{...props}
sourceSha={pullRequestMetadata.source_sha || ''}
sourceBranch={pullRequestMetadata.source_branch || ''}
/>
)}
</Layout.Horizontal>

View File

@ -40,7 +40,7 @@
background: var(--grey-0) !important;
}
.deleteBranchSectionContainer {
.branchActionsSectionContainer {
padding: var(--spacing-5) 2rem !important;
background: var(--grey-0) !important;
}

View File

@ -21,6 +21,7 @@ export declare const blueText: string
export declare const boldText: string
export declare const borderContainer: string
export declare const borderRadius: string
export declare const branchActionsSectionContainer: string
export declare const branchContainer: string
export declare const buttonPadding: string
export declare const changeContainerPadding: string
@ -34,7 +35,6 @@ export declare const conflictingContainer: string
export declare const conflictingFileName: string
export declare const conflictingFilesTable: string
export declare const copyIconContainer: string
export declare const deleteBranchSectionContainer: string
export declare const details: string
export declare const executionIcon: string
export declare const greyContainer: string

View File

@ -26,7 +26,8 @@ import type {
TypesPullReqReviewer,
RepoRepositoryOutput,
TypesRuleViolations,
TypesBranch
TypesBranch,
DeleteBranchQueryParams
} from 'services/code'
import {
PanelSectionOutletPosition,
@ -44,7 +45,7 @@ import ChecksSection from './sections/ChecksSection'
import MergeSection from './sections/MergeSection'
import CommentsSection from './sections/CommentsSection'
import ChangesSection from './sections/ChangesSection'
import DeleteBranchSection from './sections/DeleteBranchSection'
import BranchActionsSection from './sections/BranchActionsSection'
import css from './PullRequestOverviewPanel.module.scss'
interface PullRequestOverviewPanelProps {
@ -113,6 +114,7 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
mergeOptions[3].method
])
const [showDeleteBranchButton, setShowDeleteBranchButton] = useState(false)
const [showRestoreBranchButton, setShowRestoreBranchButton] = useState(false)
const [isSourceBranchDeleted, setIsSourceBranchDeleted] = useState(false)
const {
@ -130,7 +132,12 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
const { mutate: deleteBranch } = useMutate({
verb: 'DELETE',
path: `/api/v1/repos/${repoMetadata.path}/+/branches/${pullReqMetadata.source_branch}`,
queryParams: { bypass_rules: true, dry_run_rules: true }
queryParams: { bypass_rules: true, dry_run_rules: true } as DeleteBranchQueryParams
})
const { mutate: createBranch } = useMutate({
verb: 'POST',
path: `/api/v1/repos/${repoMetadata.path}/+/branches`,
pathParams: { repo_ref: repoMetadata.path }
})
const { mutate: mergePR } = useMutate({
verb: 'POST',
@ -152,6 +159,23 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
useEffect(() => {
if (error && error.status === 404) {
setIsSourceBranchDeleted(true)
createBranch({
name: pullReqMetadata.source_branch,
target: pullReqMetadata.source_sha,
bypass_rules: true,
dry_run_rules: true
}).then(res => {
if (res?.rule_violations) {
const { checkIfBypassNotAllowed } = extractInfoFromRuleViolationArr(res.rule_violations)
if (!checkIfBypassNotAllowed) {
setShowRestoreBranchButton(true)
} else {
setShowRestoreBranchButton(false)
}
} else {
setShowRestoreBranchButton(true)
}
})
}
}, [error])
@ -165,6 +189,8 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
} else {
setShowDeleteBranchButton(false)
}
} else {
setShowDeleteBranchButton(true)
}
})
}
@ -220,9 +246,13 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
PRStateLoading={PRStateLoading || loadingReviewers}
refetchPullReq={refetchPullReq}
refetchActivities={refetchActivities}
createBranch={createBranch}
refetchBranch={refetchBranch}
deleteBranch={deleteBranch}
showRestoreBranchButton={showRestoreBranchButton}
showDeleteBranchButton={showDeleteBranchButton}
setShowDeleteBranchButton={setShowDeleteBranchButton}
setShowRestoreBranchButton={setShowRestoreBranchButton}
isSourceBranchDeleted={isSourceBranchDeleted}
/>
{!isClosed ? (
@ -274,11 +304,15 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
) : (
<PullRequestPanelSections
outlets={{
[PanelSectionOutletPosition.DELETE_BRANCH]: showDeleteBranchButton && (
<DeleteBranchSection
[PanelSectionOutletPosition.BRANCH_ACTIONS]: (showDeleteBranchButton || showRestoreBranchButton) && (
<BranchActionsSection
sourceSha={pullReqMetadata.source_sha || ''}
sourceBranch={sourceBranch}
sourceBranch={sourceBranch?.name || pullReqMetadata.source_branch || ''}
createBranch={createBranch}
refetchBranch={refetchBranch}
deleteBranch={deleteBranch}
showDeleteBranchButton={showDeleteBranchButton}
setShowRestoreBranchButton={setShowRestoreBranchButton}
setShowDeleteBranchButton={setShowDeleteBranchButton}
setIsSourceBranchDeleted={setIsSourceBranchDeleted}
/>

View File

@ -29,7 +29,7 @@ const PullRequestPanelSections = (props: PullRequestPanelSectionsProps) => {
{outlets[PanelSectionOutletPosition.COMMENTS]}
{outlets[PanelSectionOutletPosition.CHECKS]}
{outlets[PanelSectionOutletPosition.MERGEABILITY]}
{outlets[PanelSectionOutletPosition.DELETE_BRANCH]}
{outlets[PanelSectionOutletPosition.BRANCH_ACTIONS]}
</Layout.Vertical>
)
}

View File

@ -21,38 +21,26 @@ import { Icon } from '@harnessio/icons'
import { useStrings } from 'framework/strings'
import { CodeIcon } from 'utils/GitUtils'
import { getErrorMessage } from 'utils/Utils'
import type { TypesBranch } from 'services/code'
import type { CreateBranchPathParams, DeleteBranchQueryParams, OpenapiCreateBranchRequest } from 'services/code'
import css from '../PullRequestOverviewPanel.module.scss'
interface DeleteBranchSectionProps {
interface BranchActionsSectionProps {
sourceSha: string
sourceBranch: TypesBranch | null
deleteBranch: MutateMethod<
any,
any,
{
bypass_rules: boolean
dry_run_rules: boolean
commit_sha: string
},
unknown
>
sourceBranch: string
createBranch: MutateMethod<any, any, OpenapiCreateBranchRequest, CreateBranchPathParams>
refetchBranch: () => Promise<void>
deleteBranch: MutateMethod<any, any, DeleteBranchQueryParams, unknown>
showDeleteBranchButton: boolean
setShowDeleteBranchButton: React.Dispatch<React.SetStateAction<boolean>>
setIsSourceBranchDeleted: React.Dispatch<React.SetStateAction<boolean>>
setShowRestoreBranchButton: React.Dispatch<React.SetStateAction<boolean>>
setIsSourceBranchDeleted?: React.Dispatch<React.SetStateAction<boolean>>
}
const DeleteBranchSection = ({
sourceSha,
sourceBranch,
deleteBranch,
setShowDeleteBranchButton,
setIsSourceBranchDeleted
}: DeleteBranchSectionProps) => {
const BranchActionsSection = (props: BranchActionsSectionProps) => {
const { getString } = useStrings()
const { showSuccess, showError } = useToaster()
return (
<Container className={cx(css.deleteBranchSectionContainer, css.borderRadius)} padding={{ right: 'xlarge' }}>
<Container className={cx(css.branchActionsSectionContainer, css.borderRadius)} padding={{ right: 'xlarge' }}>
<Layout.Horizontal flex={{ justifyContent: 'space-between' }}>
<Text flex={{ alignItems: 'center' }}>
<StringSubstitute
@ -63,7 +51,7 @@ const DeleteBranchSection = ({
<strong className={cx(css.boldText, css.branchContainer)}>
<Icon name={CodeIcon.Branch} size={16} />
<Text className={cx(css.boldText, css.widthContainer)} lineClamp={1}>
{sourceBranch?.name}
{props.sourceBranch}
</Text>
</strong>
</Container>
@ -71,30 +59,67 @@ const DeleteBranchSection = ({
}}
/>
</Text>
<Button
text={getString('deleteBranch')}
variation={ButtonVariation.SECONDARY}
onClick={() => {
deleteBranch({}, { queryParams: { bypass_rules: true, dry_run_rules: false, commit_sha: sourceSha } })
<BranchActionsButton {...props} />
</Layout.Horizontal>
</Container>
)
}
export const BranchActionsButton = ({
sourceSha,
sourceBranch,
createBranch,
refetchBranch,
deleteBranch,
showDeleteBranchButton,
setShowRestoreBranchButton,
setShowDeleteBranchButton,
setIsSourceBranchDeleted
}: BranchActionsSectionProps) => {
const { getString } = useStrings()
const { showSuccess, showError } = useToaster()
return (
<Button
text={showDeleteBranchButton ? getString('deleteBranch') : getString('restoreBranch')}
variation={ButtonVariation.SECONDARY}
onClick={() => {
showDeleteBranchButton
? deleteBranch({}, { queryParams: { bypass_rules: true, dry_run_rules: false, commit_sha: sourceSha } })
.then(() => {
setIsSourceBranchDeleted(true)
refetchBranch()
setIsSourceBranchDeleted?.(true)
setShowDeleteBranchButton(false)
showSuccess(
<StringSubstitute
str={getString('branchDeleted')}
vars={{
branch: sourceBranch?.name
branch: sourceBranch
}}
/>,
5000
)
})
.catch(err => showError(getErrorMessage(err)))
}}
/>
</Layout.Horizontal>
</Container>
: createBranch({ name: sourceBranch, target: sourceSha, bypass_rules: true })
.then(() => {
refetchBranch()
setIsSourceBranchDeleted?.(false)
setShowRestoreBranchButton(false)
showSuccess(
<StringSubstitute
str={getString('branchRestored')}
vars={{
branch: sourceBranch
}}
/>,
5000
)
})
.catch(err => showError(getErrorMessage(err)))
}}
/>
)
}
export default DeleteBranchSection
export default BranchActionsSection

View File

@ -84,7 +84,7 @@ export enum PanelSectionOutletPosition {
COMMENTS = 'comments',
CHECKS = 'checks',
MERGEABILITY = 'mergeability',
DELETE_BRANCH = 'deleteBranch'
BRANCH_ACTIONS = 'branchActions'
}
export const getMergeOptions = (getString: UseStringsReturn['getString'], mergeable: boolean): PRMergeOption[] => [

View File

@ -347,6 +347,7 @@ export type OpenapiContentType = 'file' | 'dir' | 'symlink' | 'submodule'
export interface OpenapiCreateBranchRequest {
bypass_rules?: boolean
dry_run_rules?: boolean
name?: string
target?: string
}
@ -587,6 +588,9 @@ export interface OpenapiRule {
state?: EnumRuleState
type?: OpenapiRuleType
updated?: number
user_groups?: {
[key: string]: TypesUserGroupInfo
} | null
users?: {
[key: string]: TypesPrincipalInfo
} | null
@ -718,6 +722,7 @@ export interface ProtectionDefApprovals {
export interface ProtectionDefBypass {
repo_owners?: boolean
user_group_ids?: number[]
user_ids?: number[]
}
@ -1018,6 +1023,14 @@ export interface TypesConnector {
updated?: number
}
export interface TypesCreateBranchOutput {
commit?: TypesCommit
dry_run_rules?: boolean
name?: string
rule_violations?: TypesRuleViolations[]
sha?: string
}
export interface TypesDeleteBranchOutput {
dry_run_rules?: boolean
rule_violations?: TypesRuleViolations[]
@ -1142,8 +1155,6 @@ export interface TypesInfraProviderResource {
cpu?: string | null
created?: number
disk?: string | null
gateway_host?: string | null
gateway_port?: string | null
identifier?: string
infra_provider_type?: EnumInfraProviderType
memory?: string | null
@ -1646,6 +1657,12 @@ export interface TypesUser {
updated?: number
}
export interface TypesUserGroupInfo {
description?: string
identifier?: string
name?: string
}
export interface TypesUserGroupOwnerEvaluation {
evaluations?: TypesOwnerEvaluation[] | null
id?: string
@ -2757,7 +2774,7 @@ export interface CreateBranchPathParams {
export type CreateBranchProps = Omit<
MutateProps<
TypesBranch,
TypesCreateBranchOutput,
UsererrorError | TypesRulesViolations,
void,
OpenapiCreateBranchRequest,
@ -2768,7 +2785,13 @@ export type CreateBranchProps = Omit<
CreateBranchPathParams
export const CreateBranch = ({ repo_ref, ...props }: CreateBranchProps) => (
<Mutate<TypesBranch, UsererrorError | TypesRulesViolations, void, OpenapiCreateBranchRequest, CreateBranchPathParams>
<Mutate<
TypesCreateBranchOutput,
UsererrorError | TypesRulesViolations,
void,
OpenapiCreateBranchRequest,
CreateBranchPathParams
>
verb="POST"
path={`/repos/${repo_ref}/branches`}
base={getConfig('code/api/v1')}
@ -2778,7 +2801,7 @@ export const CreateBranch = ({ repo_ref, ...props }: CreateBranchProps) => (
export type UseCreateBranchProps = Omit<
UseMutateProps<
TypesBranch,
TypesCreateBranchOutput,
UsererrorError | TypesRulesViolations,
void,
OpenapiCreateBranchRequest,
@ -2790,7 +2813,7 @@ export type UseCreateBranchProps = Omit<
export const useCreateBranch = ({ repo_ref, ...props }: UseCreateBranchProps) =>
useMutate<
TypesBranch,
TypesCreateBranchOutput,
UsererrorError | TypesRulesViolations,
void,
OpenapiCreateBranchRequest,

View File

@ -1550,7 +1550,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesBranch'
$ref: '#/components/schemas/TypesCreateBranchOutput'
description: Created
'400':
content:
@ -10674,6 +10674,8 @@ components:
properties:
bypass_rules:
type: boolean
dry_run_rules:
type: boolean
name:
type: string
target:
@ -11099,6 +11101,11 @@ components:
$ref: '#/components/schemas/OpenapiRuleType'
updated:
type: integer
user_groups:
additionalProperties:
$ref: '#/components/schemas/TypesUserGroupInfo'
nullable: true
type: object
users:
additionalProperties:
$ref: '#/components/schemas/TypesPrincipalInfo'
@ -11351,6 +11358,10 @@ components:
properties:
repo_owners:
type: boolean
user_group_ids:
items:
type: integer
type: array
user_ids:
items:
type: integer
@ -11858,6 +11869,21 @@ components:
updated:
type: integer
type: object
TypesCreateBranchOutput:
properties:
commit:
$ref: '#/components/schemas/TypesCommit'
dry_run_rules:
type: boolean
name:
type: string
rule_violations:
items:
$ref: '#/components/schemas/TypesRuleViolations'
type: array
sha:
type: string
type: object
TypesDeleteBranchOutput:
properties:
dry_run_rules:
@ -12101,12 +12127,6 @@ components:
disk:
nullable: true
type: string
gateway_host:
nullable: true
type: string
gateway_port:
nullable: true
type: string
identifier:
type: string
infra_provider_type:
@ -13034,6 +13054,15 @@ components:
updated:
type: integer
type: object
TypesUserGroupInfo:
properties:
description:
type: string
identifier:
type: string
name:
type: string
type: object
TypesUserGroupOwnerEvaluation:
properties:
evaluations: