mirror of
https://github.com/desktop/desktop
synced 2024-09-17 23:21:55 +00:00
Merge pull request #10053 from desktop/prompt-to-stash
Show specific dialog when we get stashed files git errors
This commit is contained in:
commit
eee3cf8268
|
@ -4416,8 +4416,16 @@ export class AppStore extends TypedBaseStore<IAppState> {
|
|||
}
|
||||
|
||||
const gitStore = this.gitStoreCache.get(repository)
|
||||
const result = await gitStore.performFailableOperation(() =>
|
||||
rebase(repository, baseBranch, targetBranch, progressCallback)
|
||||
const result = await gitStore.performFailableOperation(
|
||||
() => rebase(repository, baseBranch, targetBranch, progressCallback),
|
||||
{
|
||||
retryAction: {
|
||||
type: RetryActionType.Rebase,
|
||||
repository,
|
||||
baseBranch,
|
||||
targetBranch,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return result || RebaseResult.Error
|
||||
|
|
|
@ -1411,6 +1411,12 @@ export class GitStore extends BaseStore {
|
|||
currentBranch,
|
||||
theirBranch: branch,
|
||||
},
|
||||
retryAction: {
|
||||
type: RetryActionType.Merge,
|
||||
currentBranch,
|
||||
theirBranch: branch,
|
||||
repository: this.repository,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -260,3 +260,8 @@ export type Popup =
|
|||
type: PopupType.ChooseForkSettings
|
||||
repository: RepositoryWithForkedGitHubRepository
|
||||
}
|
||||
| {
|
||||
type: PopupType.LocalChangesOverwritten
|
||||
repository: Repository
|
||||
retryAction: RetryAction
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ export enum RetryActionType {
|
|||
Fetch,
|
||||
Clone,
|
||||
Checkout,
|
||||
Merge,
|
||||
Rebase,
|
||||
}
|
||||
|
||||
/** The retriable actions and their associated data. */
|
||||
|
@ -28,3 +30,15 @@ export type RetryAction =
|
|||
repository: Repository
|
||||
branch: Branch
|
||||
}
|
||||
| {
|
||||
type: RetryActionType.Merge
|
||||
repository: Repository
|
||||
currentBranch: string
|
||||
theirBranch: string
|
||||
}
|
||||
| {
|
||||
type: RetryActionType.Rebase
|
||||
repository: Repository
|
||||
baseBranch: Branch
|
||||
targetBranch: Branch
|
||||
}
|
||||
|
|
|
@ -119,6 +119,7 @@ import { CreateTag } from './create-tag'
|
|||
import { DeleteTag } from './delete-tag'
|
||||
import { ChooseForkSettings } from './choose-fork-settings'
|
||||
import { DiscardSelection } from './discard-changes/discard-selection-dialog'
|
||||
import { LocalChangesOverwrittenDialog } from './local-changes-overwritten/local-changes-overwritten-dialog'
|
||||
|
||||
const MinuteInMilliseconds = 1000 * 60
|
||||
const HourInMilliseconds = MinuteInMilliseconds * 60
|
||||
|
@ -182,6 +183,15 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
return this.state.currentPopup !== null || this.state.errors.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a memoized instance of onPopupDismissed() bound to the
|
||||
* passed popupType, so it can be used in render() without creating
|
||||
* multiple instances when the component gets re-rendered.
|
||||
*/
|
||||
private getOnPopupDismissedFn = (popupType: PopupType) => {
|
||||
return () => this.onPopupDismissed(popupType)
|
||||
}
|
||||
|
||||
public constructor(props: IAppProps) {
|
||||
super(props)
|
||||
|
||||
|
@ -1263,7 +1273,9 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
)
|
||||
}
|
||||
|
||||
private onPopupDismissed = () => this.props.dispatcher.closePopup()
|
||||
private onPopupDismissed = (popupType?: PopupType) => {
|
||||
return this.props.dispatcher.closePopup(popupType)
|
||||
}
|
||||
|
||||
private onSignInDialogDismissed = () => {
|
||||
this.props.dispatcher.resetSignInState()
|
||||
|
@ -1986,6 +1998,24 @@ export class App extends React.Component<IAppProps, IAppState> {
|
|||
/>
|
||||
)
|
||||
}
|
||||
case PopupType.LocalChangesOverwritten:
|
||||
const selectedState = this.state.selectedState
|
||||
|
||||
const existingStash =
|
||||
selectedState !== null &&
|
||||
selectedState.type === SelectionType.Repository
|
||||
? selectedState.state.changesState.stashEntry
|
||||
: null
|
||||
|
||||
return (
|
||||
<LocalChangesOverwrittenDialog
|
||||
repository={popup.repository}
|
||||
dispatcher={this.props.dispatcher}
|
||||
hasExistingStash={existingStash !== null}
|
||||
retryAction={popup.retryAction}
|
||||
onDismissed={this.getOnPopupDismissedFn(popup.type)}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return assertNever(popup, `Unknown popup type: ${popup}`)
|
||||
}
|
||||
|
|
|
@ -1903,6 +1903,20 @@ export class Dispatcher {
|
|||
await this.checkoutBranch(retryAction.repository, retryAction.branch)
|
||||
break
|
||||
|
||||
case RetryActionType.Merge:
|
||||
return this.mergeBranch(
|
||||
retryAction.repository,
|
||||
retryAction.theirBranch,
|
||||
null
|
||||
)
|
||||
|
||||
case RetryActionType.Rebase:
|
||||
return this.rebase(
|
||||
retryAction.repository,
|
||||
retryAction.baseBranch,
|
||||
retryAction.targetBranch
|
||||
)
|
||||
|
||||
default:
|
||||
return assertNever(retryAction, `Unknown retry action: ${retryAction}`)
|
||||
}
|
||||
|
|
|
@ -431,7 +431,7 @@ export async function rebaseConflictsHandler(
|
|||
* Handler for when we attempt to checkout a branch and there are some files that would
|
||||
* be overwritten.
|
||||
*/
|
||||
export async function localChangesOverwrittenHandler(
|
||||
export async function localChangesOverwrittenOnCheckoutHandler(
|
||||
error: Error,
|
||||
dispatcher: Dispatcher
|
||||
): Promise<Error | null> {
|
||||
|
@ -690,6 +690,56 @@ export async function schannelUnableToCheckRevocationForCertificate(
|
|||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for when an action the user attempts cannot be done because there are local
|
||||
* changes that would get overwritten.
|
||||
*/
|
||||
export async function localChangesOverwrittenHandler(
|
||||
error: Error,
|
||||
dispatcher: Dispatcher
|
||||
): Promise<Error | null> {
|
||||
const e = asErrorWithMetadata(error)
|
||||
if (e === null) {
|
||||
return error
|
||||
}
|
||||
|
||||
const gitError = asGitError(e.underlyingError)
|
||||
if (gitError === null) {
|
||||
return error
|
||||
}
|
||||
|
||||
const dugiteError = gitError.result.gitError
|
||||
if (dugiteError === null) {
|
||||
return error
|
||||
}
|
||||
|
||||
if (
|
||||
dugiteError !== DugiteError.LocalChangesOverwritten &&
|
||||
dugiteError !== DugiteError.MergeWithLocalChanges &&
|
||||
dugiteError !== DugiteError.RebaseWithLocalChanges
|
||||
) {
|
||||
return error
|
||||
}
|
||||
|
||||
const { repository } = e.metadata
|
||||
|
||||
if (!(repository instanceof Repository)) {
|
||||
return error
|
||||
}
|
||||
|
||||
if (e.metadata.retryAction === undefined) {
|
||||
return error
|
||||
}
|
||||
|
||||
dispatcher.showPopup({
|
||||
type: PopupType.LocalChangesOverwritten,
|
||||
repository,
|
||||
retryAction: e.metadata.retryAction,
|
||||
})
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract lines from Git's stderr output starting with the
|
||||
* prefix `remote: `. Useful to extract server-specific
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
pushNeedsPullHandler,
|
||||
upstreamAlreadyExistsHandler,
|
||||
rebaseConflictsHandler,
|
||||
localChangesOverwrittenOnCheckoutHandler,
|
||||
localChangesOverwrittenHandler,
|
||||
refusedWorkflowUpdate,
|
||||
samlReauthRequired,
|
||||
|
@ -292,6 +293,7 @@ dispatcher.registerErrorHandler(samlReauthRequired)
|
|||
dispatcher.registerErrorHandler(backgroundTaskHandler)
|
||||
dispatcher.registerErrorHandler(missingRepositoryHandler)
|
||||
dispatcher.registerErrorHandler(localChangesOverwrittenHandler)
|
||||
dispatcher.registerErrorHandler(localChangesOverwrittenOnCheckoutHandler)
|
||||
dispatcher.registerErrorHandler(rebaseConflictsHandler)
|
||||
dispatcher.registerErrorHandler(refusedWorkflowUpdate)
|
||||
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
import * as React from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DefaultDialogFooter,
|
||||
} from '../dialog'
|
||||
import { OkCancelButtonGroup } from '../dialog/ok-cancel-button-group'
|
||||
import { Repository } from '../../models/repository'
|
||||
import { RetryAction, RetryActionType } from '../../models/retry-actions'
|
||||
import { Dispatcher } from '../dispatcher'
|
||||
import { assertNever } from '../../lib/fatal-error'
|
||||
|
||||
interface ILocalChangesOverwrittenDialogProps {
|
||||
readonly repository: Repository
|
||||
readonly dispatcher: Dispatcher
|
||||
/**
|
||||
* Whether there's already a stash entry for the local branch.
|
||||
*/
|
||||
readonly hasExistingStash: boolean
|
||||
/**
|
||||
* The action that should get executed if the user selects "Stash and Continue".
|
||||
*/
|
||||
readonly retryAction: RetryAction
|
||||
/**
|
||||
* Callback to use when the dialog gets closed.
|
||||
*/
|
||||
readonly onDismissed: () => void
|
||||
}
|
||||
interface ILocalChangesOverwrittenDialogState {
|
||||
readonly stashingAndRetrying: boolean
|
||||
}
|
||||
|
||||
export class LocalChangesOverwrittenDialog extends React.Component<
|
||||
ILocalChangesOverwrittenDialogProps,
|
||||
ILocalChangesOverwrittenDialogState
|
||||
> {
|
||||
public constructor(props: ILocalChangesOverwrittenDialogProps) {
|
||||
super(props)
|
||||
this.state = { stashingAndRetrying: false }
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Dialog
|
||||
title="Error"
|
||||
loading={this.state.stashingAndRetrying}
|
||||
disabled={this.state.stashingAndRetrying}
|
||||
onDismissed={this.props.onDismissed}
|
||||
onSubmit={this.onSubmit}
|
||||
type="error"
|
||||
>
|
||||
<DialogContent>
|
||||
<p>
|
||||
Unable to {this.getRetryActionName()} when changes are present on
|
||||
your branch.
|
||||
</p>
|
||||
{this.renderStashText()}
|
||||
</DialogContent>
|
||||
{this.renderFooter()}
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
private renderStashText() {
|
||||
if (this.props.hasExistingStash && !this.state.stashingAndRetrying) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <p>You can stash your changes now and recover them afterwards.</p>
|
||||
}
|
||||
|
||||
private renderFooter() {
|
||||
if (this.props.hasExistingStash && !this.state.stashingAndRetrying) {
|
||||
return <DefaultDialogFooter />
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogFooter>
|
||||
<OkCancelButtonGroup
|
||||
okButtonText={
|
||||
__DARWIN__
|
||||
? 'Stash Changes and Continue'
|
||||
: 'Stash changes and continue'
|
||||
}
|
||||
okButtonTitle="This will create a stash with your current changes. You can recover them by restoring the stash afterwards."
|
||||
cancelButtonText="Close"
|
||||
/>
|
||||
</DialogFooter>
|
||||
)
|
||||
}
|
||||
|
||||
private onSubmit = async () => {
|
||||
if (this.props.hasExistingStash) {
|
||||
// When there's an existing stash we don't let the user stash the changes and we
|
||||
// only show a "Close" button on the modal.
|
||||
// In that case, the "Close" button submits the dialog and should only dismiss it.
|
||||
this.props.onDismissed()
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({ stashingAndRetrying: true })
|
||||
|
||||
await this.props.dispatcher.createStashForCurrentBranch(
|
||||
this.props.repository,
|
||||
true
|
||||
)
|
||||
await this.props.dispatcher.performRetry(this.props.retryAction)
|
||||
|
||||
this.props.onDismissed()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a user-friendly string to describe the current retryAction.
|
||||
*/
|
||||
private getRetryActionName() {
|
||||
switch (this.props.retryAction.type) {
|
||||
case RetryActionType.Checkout:
|
||||
return 'checkout'
|
||||
case RetryActionType.Pull:
|
||||
return 'pull'
|
||||
case RetryActionType.Merge:
|
||||
return 'merge'
|
||||
case RetryActionType.Rebase:
|
||||
return 'rebase'
|
||||
case RetryActionType.Clone:
|
||||
return 'clone'
|
||||
case RetryActionType.Fetch:
|
||||
return 'fetch'
|
||||
case RetryActionType.Push:
|
||||
return 'push'
|
||||
default:
|
||||
assertNever(
|
||||
this.props.retryAction,
|
||||
`Unknown retryAction: ${this.props.retryAction}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue