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

* feat: [CODE-2411] add support for Fast-forward merge
This commit is contained in:
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( const isRebasePresent = (rule.definition as ProtectionBranch)?.pullreq?.merge?.strategies_allowed?.includes(
MergeStrategy.REBASE 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 // List of strings to be included in the final array
const includeList = (rule?.pattern as ProtectionPattern)?.include ?? [] const includeList = (rule?.pattern as ProtectionPattern)?.include ?? []
const excludeList = (rule?.pattern as ProtectionPattern)?.exclude ?? [] const excludeList = (rule?.pattern as ProtectionPattern)?.exclude ?? []
@ -201,6 +204,7 @@ const BranchProtectionForm = (props: {
mergeCommit: isMergePresent, mergeCommit: isMergePresent,
squashMerge: isSquashPresent, squashMerge: isSquashPresent,
rebaseMerge: isRebasePresent, rebaseMerge: isRebasePresent,
fastForwardMerge: isFFMergePresent,
autoDelete: (rule.definition as ProtectionBranch)?.pullreq?.merge?.delete_branch, autoDelete: (rule.definition as ProtectionBranch)?.pullreq?.merge?.delete_branch,
blockBranchCreation: (rule.definition as ProtectionBranch)?.lifecycle?.create_forbidden, blockBranchCreation: (rule.definition as ProtectionBranch)?.lifecycle?.create_forbidden,
blockBranchUpdate: blockBranchUpdate:
@ -244,7 +248,8 @@ const BranchProtectionForm = (props: {
const stratArray = [ const stratArray = [
formData.squashMerge && MergeStrategy.SQUASH, formData.squashMerge && MergeStrategy.SQUASH,
formData.rebaseMerge && MergeStrategy.REBASE, formData.rebaseMerge && MergeStrategy.REBASE,
formData.mergeCommit && MergeStrategy.MERGE formData.mergeCommit && MergeStrategy.MERGE,
formData.fastForwardMerge && MergeStrategy.FAST_FORWARD
].filter(Boolean) as EnumMergeMethod[] ].filter(Boolean) as EnumMergeMethod[]
const includeArray = const includeArray =
formData?.targetList?.filter(([type]) => type === 'include').map(([, value]) => value) ?? [] 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('mergeCommit')} name={'mergeCommit'} />
<FormInput.CheckBox className={css.minText} label={getString('squashMerge')} name={'squashMerge'} /> <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('rebaseMerge')} name={'rebaseMerge'} />
<FormInput.CheckBox
className={css.minText}
label={getString('fastForwardMerge')}
name={'fastForwardMerge'}
/>
</Container> </Container>
</Container> </Container>
)} )}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,7 +33,7 @@ import { GitRefLink } from 'components/GitRefLink/GitRefLink'
import { getErrorMessage, permissionProps } from 'utils/Utils' import { getErrorMessage, permissionProps } from 'utils/Utils'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam' import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import { useAppContext } from 'AppContext' 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' import css from '../PullRequestOverviewPanel.module.scss'
interface RebaseSourceSectionProps { interface RebaseSourceSectionProps {
@ -80,9 +80,9 @@ const RebaseSourceSection = (props: RebaseSourceSectionProps) => {
<Container className={cx(css.sectionContainer, css.borderRadius)}> <Container className={cx(css.sectionContainer, css.borderRadius)}>
<Layout.Horizontal flex={{ justifyContent: 'space-between' }}> <Layout.Horizontal flex={{ justifyContent: 'space-between' }}>
<Layout.Horizontal flex={{ alignItems: 'center' }}> <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' }}> <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')} {getString('rebaseSource.title')}
</Text> </Text>
<Text className={css.sectionSubheader} color={Color.GREY_450} font={{ variation: FontVariation.BODY }}> <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> </Container>
<Avatar name={pullReqMetadata.merger?.display_name} size="small" hoverCard={false} /> <Avatar name={pullReqMetadata.merger?.display_name} size="small" hoverCard={false} />
<Text flex tag="div"> <Text flex tag="div" style={{ whiteSpace: 'nowrap' }}>
<StringSubstitute <StringSubstitute
str={ str={
(payload?.payload as MergePayload)?.merge_method === MergeStrategy.REBASE (payload?.payload as MergePayload)?.merge_method === MergeStrategy.REBASE
@ -74,8 +74,16 @@ export const SystemComment: React.FC<SystemCommentProps> = ({ pullReqMetadata, c
} }
vars={{ vars={{
user: <strong className={css.rightTextPadding}>{pullReqMetadata.merger?.display_name}</strong>, user: <strong className={css.rightTextPadding}>{pullReqMetadata.merger?.display_name}</strong>,
source: <strong className={css.textPadding}>{pullReqMetadata.source_branch}</strong>, source: (
target: <strong className={css.textPadding}>{pullReqMetadata.target_branch}</strong>, <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, bypassed: (payload?.payload as MergePayload)?.rules_bypassed,
mergeSha: ( mergeSha: (
<Container className={css.commitContainer} padding={{ left: 'small', right: 'xsmall' }}> <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'), label: getString('pr.mergeOptions.rebaseAndMerge'),
value: MergeStrategy.REBASE 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', method: 'close',
title: getString('pr.mergeOptions.close'), title: getString('pr.mergeOptions.close'),

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.