From 7707fc299824027c94c270656621b5ca46ae464f Mon Sep 17 00:00:00 2001 From: Navinn Ravindaran Date: Thu, 11 Nov 2021 08:27:18 -0500 Subject: [PATCH] Add 'Discard All and Pull' button (#1020) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add discard and pull menu item * Apply suggestions from code review Co-authored-by: Frédéric Collonval * Add missing entry menu in settings description * Add discard fallback to pull operation * Remove unused command ids * Apply suggestions from code review Co-authored-by: Frédéric Collonval * Apply suggestions from code review * Rename `discard` to `force` to align with `push` * Hide toast if action cancelled by user Co-authored-by: Frédéric Collonval Co-authored-by: Frédéric Collonval --- schema/plugin.json | 8 ++++ src/commandsAndMenu.tsx | 67 +++++++++++++++++++++++++------- src/components/FileList.tsx | 24 ++---------- src/tokens.ts | 1 - src/widgets/discardAllChanges.ts | 43 ++++++++++++++++++++ 5 files changed, 108 insertions(+), 35 deletions(-) create mode 100644 src/widgets/discardAllChanges.ts diff --git a/schema/plugin.json b/schema/plugin.json index ed2c96d51..e2d46d637 100644 --- a/schema/plugin.json +++ b/schema/plugin.json @@ -98,9 +98,17 @@ { "command": "git:push" }, + { + "command": "git:push", + "args": { "force": true } + }, { "command": "git:pull" }, + { + "command": "git:pull", + "args": { "force": true } + }, { "command": "git:add-remote" }, diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index bb2c42d57..fe34bda64 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -46,6 +46,7 @@ import { Level } from './tokens'; import { GitCredentialsForm } from './widgets/CredentialsBox'; +import { discardAllChanges } from './widgets/discardAllChanges'; import { GitCloneForm } from './widgets/GitCloneForm'; interface IGitCloneArgs { @@ -389,15 +390,26 @@ export function addCommands( /** Add git pull command */ commands.addCommand(CommandIDs.gitPull, { - label: trans.__('Pull from Remote'), - caption: trans.__('Pull latest code from remote repository'), + label: args => + args.force + ? trans.__('Pull from Remote (Force)') + : trans.__('Pull from Remote'), + caption: args => + args.force + ? trans.__( + 'Discard all current changes and pull from remote repository' + ) + : trans.__('Pull latest code from remote repository'), isEnabled: () => gitModel.pathRepository !== null, - execute: async () => { - logger.log({ - level: Level.RUNNING, - message: trans.__('Pulling…') - }); + execute: async args => { try { + if (args.force) { + await discardAllChanges(gitModel, trans, args.fallback as boolean); + } + logger.log({ + level: Level.RUNNING, + message: trans.__('Pulling…') + }); const details = await Private.showGitOperationDialog( gitModel, Operation.Pull, @@ -413,11 +425,37 @@ export function addCommands( 'Encountered an error when pulling changes. Error: ', error ); - logger.log({ - message: trans.__('Failed to pull'), - level: Level.ERROR, - error: error as Error - }); + + const errorMsg = + typeof error === 'string' ? error : (error as Error).message; + + // Discard changes then retry pull + if ( + errorMsg + .toLowerCase() + .includes( + 'your local changes to the following files would be overwritten by merge' + ) + ) { + await commands.execute(CommandIDs.gitPull, { + force: true, + fallback: true + }); + } else { + if ((error as any).cancelled) { + // Empty message to hide alert + logger.log({ + message: '', + level: Level.INFO + }); + } else { + logger.log({ + message: trans.__('Failed to pull'), + level: Level.ERROR, + error + }); + } + } } } }); @@ -1155,10 +1193,13 @@ export function createGitMenu( CommandIDs.gitAddRemote, CommandIDs.gitTerminalCommand ].forEach(command => { + menu.addItem({ command }); if (command === CommandIDs.gitPush) { menu.addItem({ command, args: { force: true } }); } - menu.addItem({ command }); + if (command === CommandIDs.gitPull) { + menu.addItem({ command, args: { force: true } }); + } }); menu.addItem({ type: 'separator' }); diff --git a/src/components/FileList.tsx b/src/components/FileList.tsx index 38027e163..2888d7c9a 100644 --- a/src/components/FileList.tsx +++ b/src/components/FileList.tsx @@ -21,6 +21,7 @@ import { ContextCommandIDs, CommandIDs, Git } from '../tokens'; import { ActionButton } from './ActionButton'; import { FileItem } from './FileItem'; import { GitStage } from './GitStage'; +import { discardAllChanges } from '../widgets/discardAllChanges'; export interface IFileListState { selectedFile: Git.IStatusFile | null; @@ -189,7 +190,7 @@ export class FileList extends React.Component { const result = await showDialog({ title: this.props.trans.__('Discard all changes'), body: this.props.trans.__( - 'Are you sure you want to permanently discard changes to all files? This action cannot be undone.' + 'Are you sure you want to permanently discard changes to all unstaged files? This action cannot be undone.' ), buttons: [ Dialog.cancelButton({ label: this.props.trans.__('Cancel') }), @@ -211,26 +212,7 @@ export class FileList extends React.Component { /** Discard changes in all unstaged and staged files */ discardAllChanges = async (event: React.MouseEvent): Promise => { event.stopPropagation(); - const result = await showDialog({ - title: this.props.trans.__('Discard all changes'), - body: this.props.trans.__( - 'Are you sure you want to permanently discard changes to all files? This action cannot be undone.' - ), - buttons: [ - Dialog.cancelButton({ label: this.props.trans.__('Cancel') }), - Dialog.warnButton({ label: this.props.trans.__('Discard') }) - ] - }); - if (result.button.accept) { - try { - await this.props.model.resetToCommit(); - } catch (reason) { - showErrorMessage( - this.props.trans.__('Discard all changes failed.'), - reason - ); - } - } + await discardAllChanges(this.props.model, this.props.trans); }; /** Add a specific unstaged file */ diff --git a/src/tokens.ts b/src/tokens.ts index 8d489554b..ee72573e7 100644 --- a/src/tokens.ts +++ b/src/tokens.ts @@ -1074,7 +1074,6 @@ export enum CommandIDs { gitMerge = 'git:merge', gitOpenGitignore = 'git:open-gitignore', gitPush = 'git:push', - gitForcePush = 'git:force-push', gitPull = 'git:pull', gitSubmitCommand = 'git:submit-commit', gitShowDiff = 'git:show-diff' diff --git a/src/widgets/discardAllChanges.ts b/src/widgets/discardAllChanges.ts new file mode 100644 index 000000000..e12a6082f --- /dev/null +++ b/src/widgets/discardAllChanges.ts @@ -0,0 +1,43 @@ +import { showDialog, Dialog, showErrorMessage } from '@jupyterlab/apputils'; +import { TranslationBundle } from '@jupyterlab/translation'; +import { IGitExtension } from '../tokens'; + +/** + * Discard changes in all unstaged and staged files + * + * @param isFallback If dialog is called when the classical pull operation fails + */ +export async function discardAllChanges( + model: IGitExtension, + trans: TranslationBundle, + isFallback?: boolean +): Promise { + const result = await showDialog({ + title: trans.__('Discard all changes'), + body: isFallback + ? trans.__( + 'Your current changes forbid pulling the latest changes. Do you want to permanently discard those changes? This action cannot be undone.' + ) + : trans.__( + 'Are you sure you want to permanently discard changes to all files? This action cannot be undone.' + ), + buttons: [ + Dialog.cancelButton({ label: trans.__('Cancel') }), + Dialog.warnButton({ label: trans.__('Discard') }) + ] + }); + + if (result.button.accept) { + try { + return model.resetToCommit('HEAD'); + } catch (reason) { + showErrorMessage(trans.__('Discard all changes failed.'), reason); + return Promise.reject(reason); + } + } + + return Promise.reject({ + cancelled: true, + message: 'The user refused to discard all changes' + }); +}