Merge branch 'development' into cherry-picking-drag-and-drop

This commit is contained in:
tidy-dev 2021-03-01 11:49:20 -05:00
commit 386759cd63
14 changed files with 554 additions and 45 deletions

View file

@ -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'
}

View file

@ -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
}
}

View file

@ -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,
}
}

View file

@ -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,
}))
}
}
/**

View file

@ -185,6 +185,7 @@ function getInitialRepositoryState(): IRepositoryState {
cherryPickState: {
step: null,
progress: null,
userHasResolvedConflicts: false,
},
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -274,4 +274,5 @@ export type Popup =
type: PopupType.CherryPick
repository: Repository
commits: ReadonlyArray<CommitOneLine>
sourceBranch: Branch
}

View file

@ -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() {

View 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>
)
}
}

View file

@ -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')
}

View file

@ -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)
}
}

View file

@ -1,6 +1,7 @@
@import '../../mixins';
dialog#merge-conflicts-list,
dialog#cherry-pick-conflicts-list,
dialog#rebase-conflicts-list {
width: 500px;

View file

@ -32,6 +32,7 @@ export function createStatus<K extends keyof IStatusResult>(
exists: true,
mergeHeadFound: false,
rebaseInternalState: null,
isCherryPickingHeadFound: false,
workingDirectory: WorkingDirectoryStatus.fromFiles([]),
}