Merge pull request #10053 from desktop/prompt-to-stash

Show specific dialog when we get stashed files git errors
This commit is contained in:
Markus Olsson 2020-07-08 17:13:39 +02:00 committed by GitHub
commit eee3cf8268
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 272 additions and 4 deletions

View file

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

View file

@ -1411,6 +1411,12 @@ export class GitStore extends BaseStore {
currentBranch,
theirBranch: branch,
},
retryAction: {
type: RetryActionType.Merge,
currentBranch,
theirBranch: branch,
repository: this.repository,
},
})
}

View file

@ -260,3 +260,8 @@ export type Popup =
type: PopupType.ChooseForkSettings
repository: RepositoryWithForkedGitHubRepository
}
| {
type: PopupType.LocalChangesOverwritten
repository: Repository
retryAction: RetryAction
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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