mirror of
https://github.com/desktop/desktop
synced 2024-09-19 08:02:22 +00:00
Merge branch 'development' into cherry-picking-drag-and-drop
This commit is contained in:
commit
386759cd63
|
@ -353,13 +353,16 @@ export function isRebaseConflictState(
|
|||
}
|
||||
|
||||
/**
|
||||
* Conflicts can occur during a rebase or a merge.
|
||||
* Conflicts can occur during a rebase, merge, or cherry pick.
|
||||
*
|
||||
* Callers should inspect the `kind` field to determine the kind of conflict
|
||||
* that is occurring, as this will then provide additional information specific
|
||||
* to the conflict, to help with resolving the issue.
|
||||
*/
|
||||
export type ConflictState = MergeConflictState | RebaseConflictState
|
||||
export type ConflictState =
|
||||
| MergeConflictState
|
||||
| RebaseConflictState
|
||||
| CherryPickConflictState
|
||||
|
||||
export interface IRepositoryState {
|
||||
readonly commitSelection: ICommitSelection
|
||||
|
@ -746,4 +749,35 @@ export interface ICherryPickState {
|
|||
* initiate the rebase.
|
||||
*/
|
||||
readonly progress: ICherryPickProgress | null
|
||||
|
||||
/**
|
||||
* Whether the user has done work to resolve any conflicts as part of this
|
||||
* cherry pick.
|
||||
*/
|
||||
readonly userHasResolvedConflicts: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores information about a cherry pick conflict when it occurs
|
||||
*/
|
||||
export type CherryPickConflictState = {
|
||||
readonly kind: 'cherryPick'
|
||||
|
||||
/**
|
||||
* Manual resolutions chosen by the user for conflicted files to be applied
|
||||
* before continuing the cherry pick.
|
||||
*/
|
||||
readonly manualResolutions: Map<string, ManualConflictResolution>
|
||||
|
||||
/**
|
||||
* The branch chosen by the user to copy the cherry picked commits to
|
||||
*/
|
||||
readonly targetBranchName: string
|
||||
}
|
||||
|
||||
/** Guard function for checking conflicts are from a rebase */
|
||||
export function isCherryPickConflictState(
|
||||
conflictStatus: ConflictState
|
||||
): conflictStatus is CherryPickConflictState {
|
||||
return conflictStatus.kind === 'cherryPick'
|
||||
}
|
||||
|
|
|
@ -203,8 +203,7 @@ function parseCherryPickResult(result: IGitResult): CherryPickResult {
|
|||
export async function getCherryPickSnapshot(
|
||||
repository: Repository
|
||||
): Promise<ICherryPickSnapshot | null> {
|
||||
const cherryPickHead = readCherryPickHead(repository)
|
||||
if (cherryPickHead === null) {
|
||||
if (!isCherryPickHeadFound(repository)) {
|
||||
// If there no cherry pick head, there is no cherry pick in progress.
|
||||
return null
|
||||
}
|
||||
|
@ -321,8 +320,7 @@ export async function continueCherryPick(
|
|||
}
|
||||
|
||||
// make sure cherry pick is still in progress to continue
|
||||
const cherryPickCurrentCommit = await readCherryPickHead(repository)
|
||||
if (cherryPickCurrentCommit === null) {
|
||||
if (await !isCherryPickHeadFound(repository)) {
|
||||
return CherryPickResult.Aborted
|
||||
}
|
||||
|
||||
|
@ -368,29 +366,24 @@ export async function abortCherryPick(repository: Repository) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Attempt to read the `.git/CHERRY_PICK_HEAD` file inside a repository to confirm
|
||||
* the cherry pick is still active.
|
||||
* Check if the `.git/CHERRY_PICK_HEAD` file exists
|
||||
*/
|
||||
async function readCherryPickHead(
|
||||
export async function isCherryPickHeadFound(
|
||||
repository: Repository
|
||||
): Promise<string | null> {
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const cherryPickHead = Path.join(
|
||||
const cherryPickHeadPath = Path.join(
|
||||
repository.path,
|
||||
'.git',
|
||||
'CHERRY_PICK_HEAD'
|
||||
)
|
||||
const cherryPickCurrentCommitOutput = await FSE.readFile(
|
||||
cherryPickHead,
|
||||
'utf8'
|
||||
)
|
||||
return cherryPickCurrentCommitOutput.trim()
|
||||
return FSE.pathExists(cherryPickHeadPath)
|
||||
} catch (err) {
|
||||
log.warn(
|
||||
`[cherryPick] a problem was encountered reading .git/CHERRY_PICK_HEAD,
|
||||
so it is unsafe to continue cherry picking`,
|
||||
err
|
||||
)
|
||||
return null
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import { isMergeHeadSet } from './merge'
|
|||
import { getBinaryPaths } from './diff'
|
||||
import { getRebaseInternalState } from './rebase'
|
||||
import { RebaseInternalState } from '../../models/rebase'
|
||||
import { isCherryPickHeadFound } from './cherry-pick'
|
||||
|
||||
/**
|
||||
* V8 has a limit on the size of string it can create (~256MB), and unless we want to
|
||||
|
@ -61,6 +62,9 @@ export interface IStatusResult {
|
|||
/** details about the rebase operation, if found */
|
||||
readonly rebaseInternalState: RebaseInternalState | null
|
||||
|
||||
/** true if repository is in cherry pick state */
|
||||
readonly isCherryPickingHeadFound: boolean
|
||||
|
||||
/** the absolute path to the repository's working directory */
|
||||
readonly workingDirectory: WorkingDirectoryStatus
|
||||
}
|
||||
|
@ -229,6 +233,8 @@ export async function getStatus(
|
|||
|
||||
const workingDirectory = WorkingDirectoryStatus.fromFiles([...files.values()])
|
||||
|
||||
const isCherryPickingHeadFound = await isCherryPickHeadFound(repository)
|
||||
|
||||
return {
|
||||
currentBranch,
|
||||
currentTip,
|
||||
|
@ -238,6 +244,7 @@ export async function getStatus(
|
|||
mergeHeadFound,
|
||||
rebaseInternalState,
|
||||
workingDirectory,
|
||||
isCherryPickingHeadFound,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -100,12 +100,14 @@ import {
|
|||
RepositorySectionTab,
|
||||
SelectionType,
|
||||
MergeConflictState,
|
||||
isMergeConflictState,
|
||||
RebaseConflictState,
|
||||
IRebaseState,
|
||||
IRepositoryState,
|
||||
ChangesSelectionKind,
|
||||
ChangesWorkingDirectorySelection,
|
||||
isRebaseConflictState,
|
||||
isCherryPickConflictState,
|
||||
isMergeConflictState,
|
||||
} from '../app-state'
|
||||
import {
|
||||
findEditorOrDefault,
|
||||
|
@ -268,8 +270,15 @@ import {
|
|||
getShowSideBySideDiff,
|
||||
setShowSideBySideDiff,
|
||||
} from '../../ui/lib/diff-mode'
|
||||
import { CherryPickFlowStep } from '../../models/cherry-pick'
|
||||
import { cherryPick, CherryPickResult } from '../git/cherry-pick'
|
||||
import {
|
||||
CherryPickFlowStep,
|
||||
CherryPickStepKind,
|
||||
} from '../../models/cherry-pick'
|
||||
import {
|
||||
abortCherryPick,
|
||||
cherryPick,
|
||||
CherryPickResult,
|
||||
} from '../git/cherry-pick'
|
||||
|
||||
const LastSelectedRepositoryIDKey = 'last-selected-repository-id'
|
||||
|
||||
|
@ -1885,6 +1894,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
}))
|
||||
|
||||
this.updateRebaseFlowConflictsIfFound(repository)
|
||||
this.updateCherryPickFlowConflictsIfFound(repository)
|
||||
|
||||
if (this.selectedRepository === repository) {
|
||||
this._triggerConflictsFlow(repository)
|
||||
|
@ -1906,7 +1916,7 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
)
|
||||
const { conflictState } = changesState
|
||||
|
||||
if (conflictState === null || isMergeConflictState(conflictState)) {
|
||||
if (conflictState === null || !isRebaseConflictState(conflictState)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1933,6 +1943,32 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Push changes from latest conflicts into current cherry pick flow step, if needed
|
||||
* - i.e. - multiple instance of running in to conflicts
|
||||
*/
|
||||
private updateCherryPickFlowConflictsIfFound(repository: Repository) {
|
||||
const { changesState, cherryPickState } = this.repositoryStateCache.get(
|
||||
repository
|
||||
)
|
||||
const { conflictState } = changesState
|
||||
|
||||
if (conflictState === null || !isCherryPickConflictState(conflictState)) {
|
||||
return
|
||||
}
|
||||
|
||||
const { step } = cherryPickState
|
||||
if (step === null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (step.kind === CherryPickStepKind.ShowConflicts) {
|
||||
this.repositoryStateCache.updateCherryPickState(repository, () => ({
|
||||
step: { ...step, conflictState },
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
private async _triggerConflictsFlow(repository: Repository) {
|
||||
const state = this.repositoryStateCache.get(repository)
|
||||
const { conflictState } = state.changesState
|
||||
|
@ -1942,10 +1978,12 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
return
|
||||
}
|
||||
|
||||
if (conflictState.kind === 'merge') {
|
||||
if (isMergeConflictState(conflictState)) {
|
||||
await this.showMergeConflictsDialog(repository, conflictState)
|
||||
} else if (conflictState.kind === 'rebase') {
|
||||
} else if (isRebaseConflictState(conflictState)) {
|
||||
await this.showRebaseConflictsDialog(repository, conflictState)
|
||||
} else if (isCherryPickConflictState(conflictState)) {
|
||||
// TODO: launch cherry pick conflicts dialog
|
||||
} else {
|
||||
assertNever(conflictState, `Unsupported conflict kind`)
|
||||
}
|
||||
|
@ -5484,8 +5522,17 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
}
|
||||
})
|
||||
|
||||
// update rebase flow state after choosing manual resolution
|
||||
this.updateRebaseStateAfterManualResolution(repository)
|
||||
this.updateCherryPickStateAfterManualResolution(repository)
|
||||
|
||||
this.emitUpdate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the rebase flow conflict step state as the manual resolutions
|
||||
* have been changed.
|
||||
*/
|
||||
private updateRebaseStateAfterManualResolution(repository: Repository) {
|
||||
const currentState = this.repositoryStateCache.get(repository)
|
||||
|
||||
const { changesState, rebaseState } = currentState
|
||||
|
@ -5502,8 +5549,33 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
step: { ...step, conflictState },
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
this.emitUpdate()
|
||||
/**
|
||||
* Updates the cherry pick flow conflict step state as the manual resolutions
|
||||
* have been changed.
|
||||
*/
|
||||
private updateCherryPickStateAfterManualResolution(
|
||||
repository: Repository
|
||||
): void {
|
||||
const currentState = this.repositoryStateCache.get(repository)
|
||||
|
||||
const { changesState, cherryPickState } = currentState
|
||||
const { conflictState } = changesState
|
||||
const { step } = cherryPickState
|
||||
|
||||
if (
|
||||
conflictState === null ||
|
||||
step === null ||
|
||||
!isCherryPickConflictState(conflictState) ||
|
||||
step.kind !== CherryPickStepKind.ShowConflicts
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
this.repositoryStateCache.updateCherryPickState(repository, () => ({
|
||||
step: { ...step, conflictState },
|
||||
}))
|
||||
}
|
||||
|
||||
private async createStashAndDropPreviousEntry(
|
||||
|
@ -5766,15 +5838,42 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
return result || CherryPickResult.Error
|
||||
}
|
||||
|
||||
/** This shouldn't be called directly. See `Dispatcher`. */
|
||||
public async _abortCherryPick(
|
||||
repository: Repository,
|
||||
sourceBranch: Branch
|
||||
): Promise<void> {
|
||||
const gitStore = this.gitStoreCache.get(repository)
|
||||
|
||||
await gitStore.performFailableOperation(() => abortCherryPick(repository))
|
||||
|
||||
await this.withAuthenticatingUser(repository, async (r, account) => {
|
||||
await gitStore.performFailableOperation(() =>
|
||||
checkoutBranch(repository, account, sourceBranch)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/** This shouldn't be called directly. See `Dispatcher`. */
|
||||
public _endCherryPickFlow(repository: Repository): void {
|
||||
this.repositoryStateCache.updateCherryPickState(repository, () => ({
|
||||
step: null,
|
||||
progress: null,
|
||||
userHasResolvedConflicts: false,
|
||||
}))
|
||||
|
||||
this.emitUpdate()
|
||||
}
|
||||
|
||||
/** This shouldn't be called directly. See `Dispatcher`. */
|
||||
public _setCherryPickConflictsResolved(repository: Repository) {
|
||||
// an update is not emitted here because there is no need
|
||||
// to trigger a re-render at this point
|
||||
|
||||
this.repositoryStateCache.updateCherryPickState(repository, () => ({
|
||||
userHasResolvedConflicts: true,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -185,6 +185,7 @@ function getInitialRepositoryState(): IRepositoryState {
|
|||
cherryPickState: {
|
||||
step: null,
|
||||
progress: null,
|
||||
userHasResolvedConflicts: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -157,6 +157,18 @@ function getConflictState(
|
|||
}
|
||||
}
|
||||
|
||||
if (status.isCherryPickingHeadFound) {
|
||||
const { currentBranch: targetBranchName } = status
|
||||
if (targetBranchName == null) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
kind: 'cherryPick',
|
||||
manualResolutions,
|
||||
targetBranchName,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { CherryPickConflictState } from '../lib/app-state'
|
||||
import { Branch } from './branch'
|
||||
import { CommitOneLine } from './commit'
|
||||
import { ICherryPickProgress } from './progress'
|
||||
|
@ -11,7 +12,10 @@ export interface ICherryPickSnapshot {
|
|||
}
|
||||
|
||||
/** Union type representing the possible states of the cherry pick flow */
|
||||
export type CherryPickFlowStep = ChooseTargetBranchesStep | ShowProgressStep
|
||||
export type CherryPickFlowStep =
|
||||
| ChooseTargetBranchesStep
|
||||
| ShowProgressStep
|
||||
| ShowConflictsStep
|
||||
|
||||
export const enum CherryPickStepKind {
|
||||
/**
|
||||
|
@ -29,6 +33,15 @@ export const enum CherryPickStepKind {
|
|||
* This should be the default view when there are no conflicts to address.
|
||||
*/
|
||||
ShowProgress = 'ShowProgress',
|
||||
|
||||
/**
|
||||
* The cherry pick has encountered conflicts that need resolved. This will be
|
||||
* shown as a list of files and the conflict state.
|
||||
*
|
||||
* Once the conflicts are resolved, the user can continue the cherry pick and
|
||||
* the view will switch back to `ShowProgress`.
|
||||
*/
|
||||
ShowConflicts = 'ShowConflicts',
|
||||
}
|
||||
|
||||
/** Shape of data needed to choose the base branch for a cherry pick */
|
||||
|
@ -44,3 +57,9 @@ export type ChooseTargetBranchesStep = {
|
|||
export type ShowProgressStep = {
|
||||
readonly kind: CherryPickStepKind.ShowProgress
|
||||
}
|
||||
|
||||
/** Shape of data to show conflicts that need to be resolved by the user */
|
||||
export type ShowConflictsStep = {
|
||||
readonly kind: CherryPickStepKind.ShowConflicts
|
||||
conflictState: CherryPickConflictState
|
||||
}
|
||||
|
|
|
@ -274,4 +274,5 @@ export type Popup =
|
|||
type: PopupType.CherryPick
|
||||
repository: Repository
|
||||
commits: ReadonlyArray<CommitOneLine>
|
||||
sourceBranch: Branch
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
SelectionType,
|
||||
HistoryTabMode,
|
||||
ICherryPickState,
|
||||
isRebaseConflictState,
|
||||
} from '../lib/app-state'
|
||||
import { Dispatcher } from './dispatcher'
|
||||
import { AppStore, GitHubUserStore, IssuesStore } from '../lib/stores'
|
||||
|
@ -128,6 +129,7 @@ import {
|
|||
} from '../models/cherry-pick'
|
||||
import { getAccountForRepository } from '../lib/get-account-for-repository'
|
||||
import { CommitOneLine } from '../models/commit'
|
||||
import { WorkingDirectoryStatus } from '../models/status'
|
||||
|
||||
const MinuteInMilliseconds = 1000 * 60
|
||||
const HourInMilliseconds = MinuteInMilliseconds * 60
|
||||
|
@ -2000,25 +2002,39 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
)
|
||||
case PopupType.CherryPick: {
|
||||
const cherryPickState = this.getCherryPickState()
|
||||
if (cherryPickState === null || cherryPickState.step == null) {
|
||||
const workingDirectory = this.getWorkingDirectory()
|
||||
if (
|
||||
cherryPickState === null ||
|
||||
cherryPickState.step == null ||
|
||||
workingDirectory === null
|
||||
) {
|
||||
log.warn(
|
||||
`[App] Invalid state encountered:
|
||||
cherry pick flow should not be active when step is null
|
||||
or the selected app state is not a repository state.`
|
||||
cherry pick flow should not be active when step is null,
|
||||
the selected app state is not a repository state,
|
||||
or cannot obtain the working directory.`
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const { step, progress, userHasResolvedConflicts } = cherryPickState
|
||||
|
||||
return (
|
||||
<CherryPickFlow
|
||||
key="cherry-pick-flow"
|
||||
repository={popup.repository}
|
||||
dispatcher={this.props.dispatcher}
|
||||
onDismissed={onPopupDismissedFn}
|
||||
step={cherryPickState.step}
|
||||
step={step}
|
||||
emoji={this.state.emoji}
|
||||
progress={cherryPickState.progress}
|
||||
progress={progress}
|
||||
commits={popup.commits}
|
||||
openFileInExternalEditor={this.openFileInExternalEditor}
|
||||
workingDirectory={workingDirectory}
|
||||
userHasResolvedConflicts={userHasResolvedConflicts}
|
||||
resolvedExternalEditor={this.state.resolvedExternalEditor}
|
||||
openRepositoryInShell={this.openCurrentRepositoryInShell}
|
||||
sourceBranch={popup.sourceBranch}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -2054,9 +2070,9 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
)
|
||||
const { conflictState } = changesState
|
||||
|
||||
if (conflictState === null || conflictState.kind === 'merge') {
|
||||
if (conflictState === null || !isRebaseConflictState(conflictState)) {
|
||||
log.debug(
|
||||
`[App.onShowRebaseConflictsBanner] no conflict state found, ignoring...`
|
||||
`[App.onShowRebaseConflictsBanner] no rebase conflict state found, ignoring...`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
@ -2763,6 +2779,7 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
type: PopupType.CherryPick,
|
||||
repository,
|
||||
commits,
|
||||
sourceBranch: currentBranch,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -2778,6 +2795,17 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
const { cherryPickState } = selectedState.state
|
||||
return cherryPickState
|
||||
}
|
||||
|
||||
private getWorkingDirectory(): WorkingDirectoryStatus | null {
|
||||
const { selectedState } = this.state
|
||||
if (
|
||||
selectedState === null ||
|
||||
selectedState.type !== SelectionType.Repository
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return selectedState.state.changesState.workingDirectory
|
||||
}
|
||||
}
|
||||
|
||||
function NoRepositorySelected() {
|
||||
|
|
213
app/src/ui/cherry-pick/cherry-pick-conflicts-dialog.tsx
Normal file
213
app/src/ui/cherry-pick/cherry-pick-conflicts-dialog.tsx
Normal file
|
@ -0,0 +1,213 @@
|
|||
import * as React from 'react'
|
||||
|
||||
import {
|
||||
WorkingDirectoryStatus,
|
||||
WorkingDirectoryFileChange,
|
||||
} from '../../models/status'
|
||||
import { Repository } from '../../models/repository'
|
||||
|
||||
import {
|
||||
getUnmergedFiles,
|
||||
getConflictedFiles,
|
||||
isConflictedFile,
|
||||
getResolvedFiles,
|
||||
} from '../../lib/status'
|
||||
import {
|
||||
renderUnmergedFilesSummary,
|
||||
renderShellLink,
|
||||
renderAllResolved,
|
||||
} from '../lib/conflicts/render-functions'
|
||||
import { renderUnmergedFile } from '../lib/conflicts/unmerged-file'
|
||||
|
||||
import {
|
||||
DialogContent,
|
||||
Dialog,
|
||||
DialogFooter,
|
||||
OkCancelButtonGroup,
|
||||
} from '../dialog'
|
||||
import { Dispatcher } from '../dispatcher'
|
||||
import { ShowConflictsStep } from '../../models/cherry-pick'
|
||||
|
||||
interface ICherryPickConflictsDialogProps {
|
||||
readonly dispatcher: Dispatcher
|
||||
readonly repository: Repository
|
||||
|
||||
readonly step: ShowConflictsStep
|
||||
|
||||
readonly userHasResolvedConflicts: boolean
|
||||
readonly workingDirectory: WorkingDirectoryStatus
|
||||
|
||||
// For display in manual resolution context menu
|
||||
readonly sourceBranchName: string
|
||||
|
||||
readonly onDismissed: () => void
|
||||
readonly onContinueCherryPick: (step: ShowConflictsStep) => void
|
||||
readonly onAbortCherryPick: (step: ShowConflictsStep) => void
|
||||
readonly showCherryPickConflictsBanner: (step: ShowConflictsStep) => void
|
||||
|
||||
readonly openFileInExternalEditor: (path: string) => void
|
||||
readonly resolvedExternalEditor: string | null
|
||||
readonly openRepositoryInShell: (repository: Repository) => void
|
||||
}
|
||||
|
||||
interface ICherryPickConflictsDialogState {
|
||||
readonly isAborting: boolean
|
||||
}
|
||||
|
||||
export class CherryPickConflictsDialog extends React.Component<
|
||||
ICherryPickConflictsDialogProps,
|
||||
ICherryPickConflictsDialogState
|
||||
> {
|
||||
public constructor(props: ICherryPickConflictsDialogProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isAborting: false,
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
const {
|
||||
workingDirectory,
|
||||
step,
|
||||
userHasResolvedConflicts,
|
||||
dispatcher,
|
||||
repository,
|
||||
} = this.props
|
||||
|
||||
// skip this work once we know conflicts have been resolved
|
||||
if (userHasResolvedConflicts) {
|
||||
return
|
||||
}
|
||||
|
||||
const { conflictState } = step
|
||||
const { manualResolutions } = conflictState
|
||||
|
||||
const resolvedConflicts = getResolvedFiles(
|
||||
workingDirectory,
|
||||
manualResolutions
|
||||
)
|
||||
|
||||
if (resolvedConflicts.length > 0) {
|
||||
dispatcher.setCherryPickConflictsResolved(repository)
|
||||
}
|
||||
}
|
||||
|
||||
private onCancel = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault()
|
||||
this.setState({ isAborting: true })
|
||||
|
||||
this.props.onAbortCherryPick(this.props.step)
|
||||
|
||||
this.setState({ isAborting: false })
|
||||
}
|
||||
|
||||
private onDismissed = () => {
|
||||
this.props.onDismissed()
|
||||
this.props.showCherryPickConflictsBanner(this.props.step)
|
||||
}
|
||||
|
||||
private onSubmit = async () => {
|
||||
this.props.onContinueCherryPick(this.props.step)
|
||||
}
|
||||
|
||||
private openThisRepositoryInShell = () =>
|
||||
this.props.openRepositoryInShell(this.props.repository)
|
||||
|
||||
private renderUnmergedFiles(
|
||||
files: ReadonlyArray<WorkingDirectoryFileChange>
|
||||
) {
|
||||
const {
|
||||
resolvedExternalEditor,
|
||||
openFileInExternalEditor,
|
||||
repository,
|
||||
dispatcher,
|
||||
step,
|
||||
sourceBranchName: ourBranch,
|
||||
} = this.props
|
||||
|
||||
const {
|
||||
manualResolutions,
|
||||
targetBranchName: theirBranch,
|
||||
} = step.conflictState
|
||||
|
||||
return (
|
||||
<ul className="unmerged-file-statuses">
|
||||
{files.map(f =>
|
||||
isConflictedFile(f.status)
|
||||
? renderUnmergedFile({
|
||||
path: f.path,
|
||||
status: f.status,
|
||||
resolvedExternalEditor,
|
||||
openFileInExternalEditor,
|
||||
repository,
|
||||
dispatcher,
|
||||
manualResolution: manualResolutions.get(f.path),
|
||||
theirBranch,
|
||||
ourBranch,
|
||||
})
|
||||
: null
|
||||
)}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
private renderContent(
|
||||
unmergedFiles: ReadonlyArray<WorkingDirectoryFileChange>,
|
||||
conflictedFilesCount: number
|
||||
): JSX.Element {
|
||||
if (unmergedFiles.length === 0) {
|
||||
return renderAllResolved()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderUnmergedFilesSummary(conflictedFilesCount)}
|
||||
{this.renderUnmergedFiles(unmergedFiles)}
|
||||
{renderShellLink(this.openThisRepositoryInShell)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { workingDirectory, step } = this.props
|
||||
const { manualResolutions } = step.conflictState
|
||||
|
||||
const unmergedFiles = getUnmergedFiles(workingDirectory)
|
||||
const conflictedFilesCount = getConflictedFiles(
|
||||
workingDirectory,
|
||||
manualResolutions
|
||||
).length
|
||||
|
||||
const tooltipString =
|
||||
conflictedFilesCount > 0
|
||||
? 'Resolve all conflicts before continuing'
|
||||
: undefined
|
||||
|
||||
const ok = __DARWIN__ ? 'Continue Cherry Pick' : 'Continue cherry pick'
|
||||
const cancel = __DARWIN__ ? 'Abort Cherry Pick' : 'Abort cherry pick'
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
id="cherry-pick-conflicts-list"
|
||||
onDismissed={this.onDismissed}
|
||||
title="Resolve conflicts before cherry picking"
|
||||
onSubmit={this.onSubmit}
|
||||
>
|
||||
<DialogContent>
|
||||
{this.renderContent(unmergedFiles, conflictedFilesCount)}
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<OkCancelButtonGroup
|
||||
okButtonText={ok}
|
||||
okButtonDisabled={conflictedFilesCount > 0}
|
||||
okButtonTitle={tooltipString}
|
||||
cancelButtonText={cancel}
|
||||
cancelButtonDisabled={this.state.isAborting}
|
||||
onCancelButtonClick={this.onCancel}
|
||||
/>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import { Branch } from '../../models/branch'
|
|||
import {
|
||||
CherryPickFlowStep,
|
||||
CherryPickStepKind,
|
||||
ShowConflictsStep,
|
||||
} from '../../models/cherry-pick'
|
||||
import { ICherryPickProgress } from '../../models/progress'
|
||||
|
||||
|
@ -13,6 +14,8 @@ import { Dispatcher } from '../dispatcher'
|
|||
import { ChooseTargetBranchDialog } from './choose-target-branch'
|
||||
import { CherryPickProgressDialog } from './cherry-pick-progress-dialog'
|
||||
import { CommitOneLine } from '../../models/commit'
|
||||
import { CherryPickConflictsDialog } from './cherry-pick-conflicts-dialog'
|
||||
import { WorkingDirectoryStatus } from '../../models/status'
|
||||
|
||||
interface ICherryPickFlowProps {
|
||||
readonly repository: Repository
|
||||
|
@ -22,6 +25,21 @@ interface ICherryPickFlowProps {
|
|||
readonly progress: ICherryPickProgress | null
|
||||
readonly emoji: Map<string, string>
|
||||
|
||||
/** The branch the commits come from - needed so abort can switch back to it */
|
||||
readonly sourceBranch: Branch
|
||||
|
||||
/** Properties required for conflict flow step. */
|
||||
readonly workingDirectory: WorkingDirectoryStatus
|
||||
readonly userHasResolvedConflicts: boolean
|
||||
|
||||
/**
|
||||
* Callbacks for the conflict selection components to let the user jump out
|
||||
* to their preferred editor.
|
||||
*/
|
||||
readonly openFileInExternalEditor: (path: string) => void
|
||||
readonly resolvedExternalEditor: string | null
|
||||
readonly openRepositoryInShell: (repository: Repository) => void
|
||||
|
||||
readonly onDismissed: () => void
|
||||
}
|
||||
|
||||
|
@ -32,11 +50,22 @@ export class CherryPickFlow extends React.Component<ICherryPickFlowProps> {
|
|||
}
|
||||
|
||||
private onCherryPick = (targetBranch: Branch) => {
|
||||
this.props.dispatcher.startCherryPick(
|
||||
this.props.repository,
|
||||
targetBranch,
|
||||
this.props.commits
|
||||
)
|
||||
const { dispatcher, repository, commits } = this.props
|
||||
dispatcher.startCherryPick(repository, targetBranch, commits)
|
||||
}
|
||||
|
||||
private onContinueCherryPick = (step: ShowConflictsStep) => {
|
||||
// TODO: dispatch to continue the cherry pick
|
||||
}
|
||||
|
||||
private onAbortCherryPick = (step: ShowConflictsStep) => {
|
||||
const { dispatcher, repository, sourceBranch } = this.props
|
||||
dispatcher.abortCherryPick(repository, sourceBranch)
|
||||
dispatcher.closePopup()
|
||||
}
|
||||
|
||||
private showCherryPickConflictsBanner = (step: ShowConflictsStep) => {
|
||||
// TODO: dispatch to show cherry pick conflicts banner
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
@ -76,6 +105,35 @@ export class CherryPickFlow extends React.Component<ICherryPickFlowProps> {
|
|||
emoji={this.props.emoji}
|
||||
/>
|
||||
)
|
||||
case CherryPickStepKind.ShowConflicts:
|
||||
const {
|
||||
repository,
|
||||
resolvedExternalEditor,
|
||||
openFileInExternalEditor,
|
||||
openRepositoryInShell,
|
||||
dispatcher,
|
||||
workingDirectory,
|
||||
userHasResolvedConflicts,
|
||||
sourceBranch,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<CherryPickConflictsDialog
|
||||
dispatcher={dispatcher}
|
||||
repository={repository}
|
||||
step={step}
|
||||
userHasResolvedConflicts={userHasResolvedConflicts}
|
||||
workingDirectory={workingDirectory}
|
||||
onDismissed={this.onFlowEnded}
|
||||
onContinueCherryPick={this.onContinueCherryPick}
|
||||
onAbortCherryPick={this.onAbortCherryPick}
|
||||
showCherryPickConflictsBanner={this.showCherryPickConflictsBanner}
|
||||
openFileInExternalEditor={openFileInExternalEditor}
|
||||
resolvedExternalEditor={resolvedExternalEditor}
|
||||
openRepositoryInShell={openRepositoryInShell}
|
||||
sourceBranchName={sourceBranch.name}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return assertNever(step, 'Unknown cherry pick step found')
|
||||
}
|
||||
|
|
|
@ -13,8 +13,9 @@ import {
|
|||
FoldoutType,
|
||||
ICompareFormUpdate,
|
||||
RepositorySectionTab,
|
||||
isMergeConflictState,
|
||||
RebaseConflictState,
|
||||
isRebaseConflictState,
|
||||
isCherryPickConflictState,
|
||||
} from '../../lib/app-state'
|
||||
import { assertNever, fatalError } from '../../lib/fatal-error'
|
||||
import {
|
||||
|
@ -456,7 +457,7 @@ export class Dispatcher {
|
|||
const repositoryState = this.repositoryStateManager.get(repository)
|
||||
const { conflictState } = repositoryState.changesState
|
||||
|
||||
if (conflictState === null || conflictState.kind === 'merge') {
|
||||
if (conflictState === null || !isRebaseConflictState(conflictState)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1026,9 +1027,9 @@ export class Dispatcher {
|
|||
return
|
||||
}
|
||||
|
||||
if (isMergeConflictState(conflictState)) {
|
||||
if (!isRebaseConflictState(conflictState)) {
|
||||
log.warn(
|
||||
`[rebase] conflict state after rebase is merge conflicts - unable to continue`
|
||||
`[rebase] conflict state after rebase is not rebase conflicts - unable to continue`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
@ -1119,9 +1120,9 @@ export class Dispatcher {
|
|||
return
|
||||
}
|
||||
|
||||
if (isMergeConflictState(conflictState)) {
|
||||
if (!isRebaseConflictState(conflictState)) {
|
||||
log.warn(
|
||||
`[continueRebase] conflict state after rebase is merge conflicts - unable to continue`
|
||||
`[continueRebase] conflict state after rebase is not rebase conflicts - unable to continue`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
@ -2570,6 +2571,10 @@ export class Dispatcher {
|
|||
commits
|
||||
)
|
||||
|
||||
// This will update the conflict state of the app. This is needed to start
|
||||
// conflict flow if cherry pick results in conflict.
|
||||
await this.appStore._loadStatus(repository)
|
||||
|
||||
switch (result) {
|
||||
case CherryPickResult.CompletedWithoutError:
|
||||
await this.completeCherryPick(
|
||||
|
@ -2578,7 +2583,9 @@ export class Dispatcher {
|
|||
commits.length
|
||||
)
|
||||
break
|
||||
// TODO: handle conflicts and other handled errors
|
||||
case CherryPickResult.ConflictsEncountered:
|
||||
this.startConflictCherryPickFlow(repository)
|
||||
break
|
||||
default:
|
||||
this.appStore._endCherryPickFlow(repository)
|
||||
throw Error(
|
||||
|
@ -2588,6 +2595,26 @@ export class Dispatcher {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the current app conflict state and switches cherry pick flow to
|
||||
* show conflicts step
|
||||
*/
|
||||
private startConflictCherryPickFlow(repository: Repository): void {
|
||||
const stateAfter = this.repositoryStateManager.get(repository)
|
||||
const { conflictState } = stateAfter.changesState
|
||||
if (conflictState === null || !isCherryPickConflictState(conflictState)) {
|
||||
log.warn(
|
||||
'[cherryPick] - conflict state was null or not in a cherry pick conflict state - unable to continue'
|
||||
)
|
||||
return
|
||||
}
|
||||
this.setCherryPickFlowStep(repository, {
|
||||
kind: CherryPickStepKind.ShowConflicts,
|
||||
conflictState,
|
||||
})
|
||||
}
|
||||
|
||||
/** Tidy up the cherry pick flow after reaching the end */
|
||||
/** Wrap cherry pick up actions:
|
||||
* - closes flow popup
|
||||
* - displays success banner
|
||||
|
@ -2640,4 +2667,19 @@ export class Dispatcher {
|
|||
await sleep(500)
|
||||
this.cherryPick(repository, targetBranch, commits)
|
||||
}
|
||||
|
||||
/** Aborts an ongoing cherry pick and switches back to the source branch. */
|
||||
public async abortCherryPick(repository: Repository, sourceBranch: Branch) {
|
||||
await this.appStore._abortCherryPick(repository, sourceBranch)
|
||||
await this.appStore._loadStatus(repository)
|
||||
this.appStore._endCherryPickFlow(repository)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the cherry pick state to indicate the user has resolved conflicts in
|
||||
* the current repository.
|
||||
*/
|
||||
public setCherryPickConflictsResolved(repository: Repository) {
|
||||
return this.appStore._setCherryPickConflictsResolved(repository)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
@import '../../mixins';
|
||||
|
||||
dialog#merge-conflicts-list,
|
||||
dialog#cherry-pick-conflicts-list,
|
||||
dialog#rebase-conflicts-list {
|
||||
width: 500px;
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ export function createStatus<K extends keyof IStatusResult>(
|
|||
exists: true,
|
||||
mergeHeadFound: false,
|
||||
rebaseInternalState: null,
|
||||
isCherryPickingHeadFound: false,
|
||||
workingDirectory: WorkingDirectoryStatus.fromFiles([]),
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue