feat: [CODE-2337] add rebase button in PR Conversation page (#2683)

* fix: [CODE-2362] updated message
* fix: [CODE-2362] lint
* fix: [CODE-2362] memoize sha state
* fix: [CODE-2362] add permission check
* fix: [CODE-2362] refetch activity on rebase
* fix: [CODE-2362] updated activity for rebase
* feat: [CODE-2337] add rebase button in PR Conversation page
CODE-2402
Ritik Kapoor 2024-09-18 21:32:00 +00:00 committed by Harness
parent b9be854b72
commit 2d96b62e07
13 changed files with 558 additions and 32 deletions

View File

@ -806,6 +806,7 @@ export interface StringsMap {
'pr.openForReview': string
'pr.outdated': string
'pr.prBranchDeleteInfo': string
'pr.prBranchForcePushInfo': string
'pr.prBranchPushInfo': string
'pr.prCanBeMerged': string
'pr.prClosed': string
@ -870,7 +871,11 @@ export interface StringsMap {
reactivate: string
readMe: string
reader: string
rebase: string
rebaseBranch: string
rebaseMerge: string
'rebaseSource.message': string
'rebaseSource.title': string
recursiveSearchLabel: string
recursiveSearchTooltip: string
refresh: string
@ -1089,7 +1094,9 @@ export interface StringsMap {
updateLabel: string
updateUser: string
updateWebhook: string
updateWithRebase: string
updated: string
updatedBranchMessageRebase: string
upload: string
uploadAFileError: string
user: string

View File

@ -319,6 +319,7 @@ pr:
prMergedInfo: '{user} merged changes from {source} into {target} as {mergeSha} {time}'
prRebasedInfo: '{user} rebased changes from branch {source} onto {target}, now at {mergeSha} {time}'
prBranchPushInfo: '{user} pushed a new commit {commit}'
prBranchForcePushInfo: '{user} force-pushed the {gitRef} branch from {oldCommit} to {newCommit}'
prBranchDeleteInfo: '{user} deleted the source branch with latest commit {commit}'
prStateChanged: '{user} changed pull request state from {old} to {new}.'
prStateChangedDraft: '{user} {changedToDraft|true:marked pull request as draft.,opened pull request for review.}'
@ -1122,6 +1123,9 @@ checkSection:
allReqChecksPassed: All required checks passed
someChecksFailed: Some checks have failed
someChecksRunning: Some checks are running
rebaseSource:
title: This branch is out-of-date with the base branch
message: Merge the latest changes from {target} into {source}
importFailed: Import Failed
uploadAFileError: There is no image or video uploaded. Please upload an image or video.
securitySettings:
@ -1256,3 +1260,7 @@ labels:
addaValue: Add a value
stringMax: '{entity} must be 50 characters or less'
noNewLine: '{entity} cannot contain new lines'
rebase: Rebase
rebaseBranch: Rebase branch
updateWithRebase: Update with rebase
updatedBranchMessageRebase: Updated branch with rebase

View File

@ -118,7 +118,7 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
const [ruleViolationArr, setRuleViolationArr] = useState<{ data: { rule_violations: TypesRuleViolations[] } }>()
const [notBypassable, setNotBypassable] = useState(false)
const [bypass, setBypass] = useState(false)
const { mutate: updatePRState, loading: loadingState } = useMutate({
const { mutate: updatePRState } = useMutate({
verb: 'POST',
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullReqMetadata.number}/state`
})
@ -318,9 +318,6 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
)}
</Text>
<FlexExpander />
<Render when={loading || loadingState}>
<Icon name={CodeIcon.InputSpinner} size={16} margin={{ right: 'xsmall' }} />
</Render>
<PullReqSuggestionsBatch />
<Match expr={isDraft}>
<Truthy>

View File

@ -81,6 +81,14 @@
}
}
.blueTextColor {
:global {
.bp3-button-text {
color: var(--primary-7) !important;
}
}
}
.requiredText {
font-size: 10px !important;
font-style: normal !important;

View File

@ -18,6 +18,7 @@
// This is an auto-generated file
export declare const blueCopyContainer: string
export declare const blueText: string
export declare const blueTextColor: string
export declare const boldText: string
export declare const borderContainer: string
export declare const borderRadius: string

View File

@ -46,6 +46,7 @@ import MergeSection from './sections/MergeSection'
import CommentsSection from './sections/CommentsSection'
import ChangesSection from './sections/ChangesSection'
import BranchActionsSection from './sections/BranchActionsSection'
import RebaseSourceSection from './sections/RebaseSourceSection'
import css from './PullRequestOverviewPanel.module.scss'
interface PullRequestOverviewPanelProps {
@ -139,7 +140,7 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
path: `/api/v1/repos/${repoMetadata.path}/+/branches`,
pathParams: { repo_ref: repoMetadata.path }
})
const { mutate: mergePR } = useMutate({
const { mutate: mergePR, loading: mergeLoading } = useMutate({
verb: 'POST',
path: `/api/v1/repos/${repoMetadata.path}/+/pullreq/${pullReqMetadata.number}/merge`
})
@ -231,6 +232,11 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
) // eslint-disable-next-line react-hooks/exhaustive-deps
}, [unchecked, pullReqMetadata?.source_sha, activities])
const rebasePossible = useMemo(
() => pullReqMetadata.merge_target_sha !== pullReqMetadata.merge_base_sha,
[pullReqMetadata]
)
return (
<Container margin={{ bottom: 'medium' }} className={css.mainContainer}>
<Layout.Vertical>
@ -298,7 +304,16 @@ const PullRequestOverviewPanel = (props: PullRequestOverviewPanelProps) => {
mergeable={mergeable}
conflictingFiles={conflictingFiles}
/>
)
),
[PanelSectionOutletPosition.REBASE_SOURCE_BRANCH]: rebasePossible &&
!mergeLoading &&
!conflictingFiles?.length && (
<RebaseSourceSection
pullReqMetadata={pullReqMetadata}
repoMetadata={repoMetadata}
refetchActivities={refetchActivities}
/>
)
}}
/>
) : (

View File

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

View File

@ -78,7 +78,7 @@ const MergeSection = (props: MergeSectionProps) => {
)
return (
<>
<Container className={cx(css.sectionContainer, css.borderRadius)}>
<Container className={cx(css.sectionContainer, css.borderContainer)}>
<Layout.Horizontal flex={{ justifyContent: 'space-between' }}>
<Layout.Horizontal flex={{ alignItems: 'center', justifyContent: 'start' }}>
{(unchecked && <img src={Images.PrUnchecked} width={25} height={25} />) || (

View File

@ -0,0 +1,141 @@
/*
* 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.
*/
import React from 'react'
import { Color, FontVariation } from '@harnessio/design-system'
import cx from 'classnames'
import {
Button,
ButtonSize,
ButtonVariation,
Container,
Layout,
StringSubstitute,
Text,
useToaster
} from '@harnessio/uicore'
import { useMutate } from 'restful-react'
import type { RebaseBranchRequestBody, RepoRepositoryOutput, TypesPullReq } from 'services/code'
import { useStrings } from 'framework/strings'
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 css from '../PullRequestOverviewPanel.module.scss'
interface RebaseSourceSectionProps {
pullReqMetadata: TypesPullReq
repoMetadata: RepoRepositoryOutput
refetchActivities: () => void
}
const RebaseSourceSection = (props: RebaseSourceSectionProps) => {
const { pullReqMetadata, repoMetadata, refetchActivities } = props
const { getString } = useStrings()
const { showSuccess, showError } = useToaster()
const { mutate: rebase } = useMutate<RebaseBranchRequestBody>({
verb: 'POST',
path: `/api/v1/repos/${repoMetadata.path}/+/rebase`
})
const {
hooks: { usePermissionTranslate },
standalone,
routes
} = useAppContext()
const space = useGetSpaceParam()
const permPushResult = usePermissionTranslate(
{
resource: {
resourceType: 'CODE_REPOSITORY',
resourceIdentifier: repoMetadata?.identifier as string
},
permissions: ['code_repo_push']
},
[space]
)
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
}
return (
<>
<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} />
<Layout.Vertical padding={{ left: 'medium' }}>
<Text padding={{ bottom: 'xsmall' }} className={css.sectionTitle} color={Color.GREY_600}>
{getString('rebaseSource.title')}
</Text>
<Text className={css.sectionSubheader} color={Color.GREY_450} font={{ variation: FontVariation.BODY }}>
<StringSubstitute
str={getString('rebaseSource.message')}
vars={{
target: (
<GitRefLink
text={pullReqMetadata.target_branch as string}
url={routes.toCODERepository({
repoPath: repoMetadata.path as string,
gitRef: pullReqMetadata.target_branch
})}
showCopy
/>
),
source: (
<GitRefLink
text={pullReqMetadata.source_branch as string}
url={routes.toCODERepository({
repoPath: repoMetadata.path as string,
gitRef: pullReqMetadata.source_branch
})}
showCopy
/>
)
}}
/>
</Text>
</Layout.Vertical>
</Layout.Horizontal>
<Button
className={cx(css.blueTextColor)}
variation={ButtonVariation.TERTIARY}
size={ButtonSize.MEDIUM}
text={getString('updateWithRebase')}
onClick={() =>
rebase(rebaseRequestPayload)
.then(() => {
showSuccess(getString('updatedBranchMessageRebase'))
setTimeout(() => {
refetchActivities()
}, 1000)
})
.catch(err => showError(getErrorMessage(err)))
}
{...permissionProps(permPushResult, standalone)}
/>
</Layout.Horizontal>
</Container>
</>
)
}
export default RebaseSourceSection

View File

@ -31,6 +31,7 @@ import { CommitActions } from 'components/CommitActions/CommitActions'
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
import { TimePopoverWithLocal } from 'utils/timePopoverLocal/TimePopoverWithLocal'
import { Label } from 'components/Label/Label'
import { GitRefLink } from 'components/GitRefLink/GitRefLink'
import css from './Conversation.module.scss'
interface SystemCommentProps extends Pick<GitInfoProps, 'pullReqMetadata'> {
@ -149,30 +150,83 @@ export const SystemComment: React.FC<SystemCommentProps> = ({ pullReqMetadata, c
<Layout.Horizontal spacing="small" style={{ alignItems: 'center' }} className={css.mergedBox}>
<Avatar name={payload?.author?.display_name} size="small" hoverCard={false} />
<Text flex tag="div">
<StringSubstitute
str={getString('pr.prBranchPushInfo')}
vars={{
user: (
<Text padding={{ right: 'small' }} inline>
<strong>{payload?.author?.display_name}</strong>
</Text>
),
commit: (
<Container className={css.commitContainer} padding={{ left: 'small' }}>
<CommitActions
enableCopy
sha={(payload?.payload as Unknown)?.new}
href={routes.toCODEPullRequest({
repoPath: repoMetadataPath as string,
pullRequestSection: PullRequestSection.FILES_CHANGED,
pullRequestId: String(pullReqMetadata.number),
commitSHA: (payload?.payload as Unknown)?.new as string
})}
/>
</Container>
)
}}
/>
{(payload?.payload as Unknown)?.forced ? (
<StringSubstitute
str={getString('pr.prBranchForcePushInfo')}
vars={{
user: (
<Text padding={{ right: 'small' }} inline>
<strong>{payload?.author?.display_name}</strong>
</Text>
),
gitRef: (
<Container padding={{ left: 'small', right: 'small' }}>
<GitRefLink
text={pullReqMetadata.source_branch as string}
url={routes.toCODERepository({
repoPath: repoMetadataPath as string,
gitRef: pullReqMetadata.source_branch
})}
showCopy
/>
</Container>
),
oldCommit: (
<Container className={css.commitContainer} padding={{ left: 'small', right: 'small' }}>
<CommitActions
enableCopy
sha={(payload?.payload as Unknown)?.old}
href={routes.toCODEPullRequest({
repoPath: repoMetadataPath as string,
pullRequestSection: PullRequestSection.FILES_CHANGED,
pullRequestId: String(pullReqMetadata.number),
commitSHA: (payload?.payload as Unknown)?.old as string
})}
/>
</Container>
),
newCommit: (
<Container className={css.commitContainer} padding={{ left: 'small' }}>
<CommitActions
enableCopy
sha={(payload?.payload as Unknown)?.new}
href={routes.toCODEPullRequest({
repoPath: repoMetadataPath as string,
pullRequestSection: PullRequestSection.FILES_CHANGED,
pullRequestId: String(pullReqMetadata.number),
commitSHA: (payload?.payload as Unknown)?.new as string
})}
/>
</Container>
)
}}
/>
) : (
<StringSubstitute
str={getString('pr.prBranchPushInfo')}
vars={{
user: (
<Text padding={{ right: 'small' }} inline>
<strong>{payload?.author?.display_name}</strong>
</Text>
),
commit: (
<Container className={css.commitContainer} padding={{ left: 'small' }}>
<CommitActions
enableCopy
sha={(payload?.payload as Unknown)?.new}
href={routes.toCODEPullRequest({
repoPath: repoMetadataPath as string,
pullRequestSection: PullRequestSection.FILES_CHANGED,
pullRequestId: String(pullReqMetadata.number),
commitSHA: (payload?.payload as Unknown)?.new as string
})}
/>
</Container>
)
}}
/>
)}
</Text>
<PipeSeparator height={9} />
<TimePopoverWithLocal

View File

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

View File

@ -1436,6 +1436,12 @@ export interface TypesPullReqStats {
unresolved_count?: number
}
export interface TypesRebaseResponse {
dry_run_rules?: boolean
new_head_branch_sha?: ShaSHA
rule_violations?: TypesRuleViolations[]
}
export interface TypesRenameDetails {
commit_sha_after?: string
commit_sha_before?: string
@ -6047,6 +6053,70 @@ export const useGetRaw = ({ repo_ref, path, ...props }: UseGetRawProps) =>
{ base: getConfig('code/api/v1'), pathParams: { repo_ref, path }, ...props }
)
export interface RebaseBranchPathParams {
repo_ref: string
}
export interface RebaseBranchRequestBody {
base_branch?: string
bypass_rules?: boolean
dry_run_rules?: boolean
head_branch?: string
head_commit_sha?: ShaSHA
}
export type RebaseBranchProps = Omit<
MutateProps<
TypesRebaseResponse,
UsererrorError | TypesMergeViolations,
void,
RebaseBranchRequestBody,
RebaseBranchPathParams
>,
'path' | 'verb'
> &
RebaseBranchPathParams
export const RebaseBranch = ({ repo_ref, ...props }: RebaseBranchProps) => (
<Mutate<
TypesRebaseResponse,
UsererrorError | TypesMergeViolations,
void,
RebaseBranchRequestBody,
RebaseBranchPathParams
>
verb="POST"
path={`/repos/${repo_ref}/rebase`}
base={getConfig('code/api/v1')}
{...props}
/>
)
export type UseRebaseBranchProps = Omit<
UseMutateProps<
TypesRebaseResponse,
UsererrorError | TypesMergeViolations,
void,
RebaseBranchRequestBody,
RebaseBranchPathParams
>,
'path' | 'verb'
> &
RebaseBranchPathParams
export const useRebaseBranch = ({ repo_ref, ...props }: UseRebaseBranchProps) =>
useMutate<
TypesRebaseResponse,
UsererrorError | TypesMergeViolations,
void,
RebaseBranchRequestBody,
RebaseBranchPathParams
>('POST', (paramsInPath: RebaseBranchPathParams) => `/repos/${paramsInPath.repo_ref}/rebase`, {
base: getConfig('code/api/v1'),
pathParams: { repo_ref },
...props
})
export interface RestoreRepositoryQueryParams {
/**
* The exact time the resource was delete at in epoch format.
@ -8154,6 +8224,59 @@ export const usePurgeSpace = ({ space_ref, ...props }: UsePurgeSpaceProps) =>
{ base: getConfig('code/api/v1'), pathParams: { space_ref }, ...props }
)
export interface ListReposQueryParams {
/**
* The substring which is used to filter the repositories by their path name.
*/
query?: string
/**
* The data by which the repositories are sorted.
*/
sort?: 'identifier' | 'created' | 'updated'
/**
* The order of the output.
*/
order?: 'asc' | 'desc'
/**
* The page to return.
*/
page?: number
/**
* The maximum number of results to return.
*/
limit?: number
}
export interface ListReposPathParams {
space_ref: string
}
export type ListReposProps = Omit<
GetProps<TypesRepository[], UsererrorError, ListReposQueryParams, ListReposPathParams>,
'path'
> &
ListReposPathParams
export const ListRepos = ({ space_ref, ...props }: ListReposProps) => (
<Get<TypesRepository[], UsererrorError, ListReposQueryParams, ListReposPathParams>
path={`/spaces/${space_ref}/repos`}
base={getConfig('code/api/v1')}
{...props}
/>
)
export type UseListReposProps = Omit<
UseGetProps<TypesRepository[], UsererrorError, ListReposQueryParams, ListReposPathParams>,
'path'
> &
ListReposPathParams
export const useListRepos = ({ space_ref, ...props }: UseListReposProps) =>
useGet<TypesRepository[], UsererrorError, ListReposQueryParams, ListReposPathParams>(
(paramsInPath: ListReposPathParams) => `/spaces/${paramsInPath.space_ref}/repos`,
{ base: getConfig('code/api/v1'), pathParams: { space_ref }, ...props }
)
export interface RestoreSpaceQueryParams {
/**
* The exact time the resource was delete at in epoch format.

View File

@ -6070,6 +6070,76 @@ paths:
description: Internal Server Error
tags:
- repository
/repos/{repo_ref}/rebase:
post:
operationId: rebaseBranch
parameters:
- in: path
name: repo_ref
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
properties:
base_branch:
type: string
bypass_rules:
type: boolean
dry_run_rules:
type: boolean
head_branch:
type: string
head_commit_sha:
$ref: '#/components/schemas/ShaSHA'
type: object
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/TypesRebaseResponse'
description: OK
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/UsererrorError'
description: Bad Request
'401':
content:
application/json:
schema:
$ref: '#/components/schemas/UsererrorError'
description: Unauthorized
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/UsererrorError'
description: Forbidden
'404':
content:
application/json:
schema:
$ref: '#/components/schemas/UsererrorError'
description: Not Found
'422':
content:
application/json:
schema:
$ref: '#/components/schemas/TypesMergeViolations'
description: Unprocessable Entity
'500':
content:
application/json:
schema:
$ref: '#/components/schemas/UsererrorError'
description: Internal Server Error
tags:
- repository
/repos/{repo_ref}/restore:
post:
operationId: restoreRepository
@ -9225,6 +9295,95 @@ paths:
description: Internal Server Error
tags:
- space
/spaces/{space_ref}/repos:
get:
operationId: listRepos
parameters:
- description: The substring which is used to filter the repositories by their
path name.
in: query
name: query
required: false
schema:
type: string
- description: The data by which the repositories are sorted.
in: query
name: sort
required: false
schema:
default: identifier
enum:
- identifier
- created
- updated
type: string
- description: The order of the output.
in: query
name: order
required: false
schema:
default: asc
enum:
- asc
- desc
type: string
- description: The page to return.
in: query
name: page
required: false
schema:
default: 1
minimum: 1
type: integer
- description: The maximum number of results to return.
in: query
name: limit
required: false
schema:
default: 30
maximum: 100
minimum: 1
type: integer
- in: path
name: space_ref
required: true
schema:
type: string
responses:
'200':
content:
application/json:
schema:
items:
$ref: '#/components/schemas/TypesRepository'
type: array
description: OK
'401':
content:
application/json:
schema:
$ref: '#/components/schemas/UsererrorError'
description: Unauthorized
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/UsererrorError'
description: Forbidden
'404':
content:
application/json:
schema:
$ref: '#/components/schemas/UsererrorError'
description: Not Found
'500':
content:
application/json:
schema:
$ref: '#/components/schemas/UsererrorError'
description: Internal Server Error
tags:
- space
/spaces/{space_ref}/restore:
post:
operationId: restoreSpace
@ -12657,6 +12816,17 @@ components:
unresolved_count:
type: integer
type: object
TypesRebaseResponse:
properties:
dry_run_rules:
type: boolean
new_head_branch_sha:
$ref: '#/components/schemas/ShaSHA'
rule_violations:
items:
$ref: '#/components/schemas/TypesRuleViolations'
type: array
type: object
TypesRenameDetails:
properties:
commit_sha_after: