Skip to content

Commit 7707fc2

Browse files
navn-rfcollonval
andauthored
Add 'Discard All and Pull' button (#1020)
* Add discard and pull menu item * Apply suggestions from code review Co-authored-by: Frédéric Collonval <fcollonval@gmail.com> * 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 <fcollonval@gmail.com> * 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 <fcollonval@gmail.com> Co-authored-by: Frédéric Collonval <fcollonval@users.noreply.github.com>
1 parent b268a40 commit 7707fc2

File tree

5 files changed

+108
-35
lines changed

5 files changed

+108
-35
lines changed

schema/plugin.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,17 @@
9898
{
9999
"command": "git:push"
100100
},
101+
{
102+
"command": "git:push",
103+
"args": { "force": true }
104+
},
101105
{
102106
"command": "git:pull"
103107
},
108+
{
109+
"command": "git:pull",
110+
"args": { "force": true }
111+
},
104112
{
105113
"command": "git:add-remote"
106114
},

src/commandsAndMenu.tsx

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
Level
4747
} from './tokens';
4848
import { GitCredentialsForm } from './widgets/CredentialsBox';
49+
import { discardAllChanges } from './widgets/discardAllChanges';
4950
import { GitCloneForm } from './widgets/GitCloneForm';
5051

5152
interface IGitCloneArgs {
@@ -389,15 +390,26 @@ export function addCommands(
389390

390391
/** Add git pull command */
391392
commands.addCommand(CommandIDs.gitPull, {
392-
label: trans.__('Pull from Remote'),
393-
caption: trans.__('Pull latest code from remote repository'),
393+
label: args =>
394+
args.force
395+
? trans.__('Pull from Remote (Force)')
396+
: trans.__('Pull from Remote'),
397+
caption: args =>
398+
args.force
399+
? trans.__(
400+
'Discard all current changes and pull from remote repository'
401+
)
402+
: trans.__('Pull latest code from remote repository'),
394403
isEnabled: () => gitModel.pathRepository !== null,
395-
execute: async () => {
396-
logger.log({
397-
level: Level.RUNNING,
398-
message: trans.__('Pulling…')
399-
});
404+
execute: async args => {
400405
try {
406+
if (args.force) {
407+
await discardAllChanges(gitModel, trans, args.fallback as boolean);
408+
}
409+
logger.log({
410+
level: Level.RUNNING,
411+
message: trans.__('Pulling…')
412+
});
401413
const details = await Private.showGitOperationDialog(
402414
gitModel,
403415
Operation.Pull,
@@ -413,11 +425,37 @@ export function addCommands(
413425
'Encountered an error when pulling changes. Error: ',
414426
error
415427
);
416-
logger.log({
417-
message: trans.__('Failed to pull'),
418-
level: Level.ERROR,
419-
error: error as Error
420-
});
428+
429+
const errorMsg =
430+
typeof error === 'string' ? error : (error as Error).message;
431+
432+
// Discard changes then retry pull
433+
if (
434+
errorMsg
435+
.toLowerCase()
436+
.includes(
437+
'your local changes to the following files would be overwritten by merge'
438+
)
439+
) {
440+
await commands.execute(CommandIDs.gitPull, {
441+
force: true,
442+
fallback: true
443+
});
444+
} else {
445+
if ((error as any).cancelled) {
446+
// Empty message to hide alert
447+
logger.log({
448+
message: '',
449+
level: Level.INFO
450+
});
451+
} else {
452+
logger.log({
453+
message: trans.__('Failed to pull'),
454+
level: Level.ERROR,
455+
error
456+
});
457+
}
458+
}
421459
}
422460
}
423461
});
@@ -1155,10 +1193,13 @@ export function createGitMenu(
11551193
CommandIDs.gitAddRemote,
11561194
CommandIDs.gitTerminalCommand
11571195
].forEach(command => {
1196+
menu.addItem({ command });
11581197
if (command === CommandIDs.gitPush) {
11591198
menu.addItem({ command, args: { force: true } });
11601199
}
1161-
menu.addItem({ command });
1200+
if (command === CommandIDs.gitPull) {
1201+
menu.addItem({ command, args: { force: true } });
1202+
}
11621203
});
11631204

11641205
menu.addItem({ type: 'separator' });

src/components/FileList.tsx

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { ContextCommandIDs, CommandIDs, Git } from '../tokens';
2121
import { ActionButton } from './ActionButton';
2222
import { FileItem } from './FileItem';
2323
import { GitStage } from './GitStage';
24+
import { discardAllChanges } from '../widgets/discardAllChanges';
2425

2526
export interface IFileListState {
2627
selectedFile: Git.IStatusFile | null;
@@ -189,7 +190,7 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
189190
const result = await showDialog({
190191
title: this.props.trans.__('Discard all changes'),
191192
body: this.props.trans.__(
192-
'Are you sure you want to permanently discard changes to all files? This action cannot be undone.'
193+
'Are you sure you want to permanently discard changes to all unstaged files? This action cannot be undone.'
193194
),
194195
buttons: [
195196
Dialog.cancelButton({ label: this.props.trans.__('Cancel') }),
@@ -211,26 +212,7 @@ export class FileList extends React.Component<IFileListProps, IFileListState> {
211212
/** Discard changes in all unstaged and staged files */
212213
discardAllChanges = async (event: React.MouseEvent): Promise<void> => {
213214
event.stopPropagation();
214-
const result = await showDialog({
215-
title: this.props.trans.__('Discard all changes'),
216-
body: this.props.trans.__(
217-
'Are you sure you want to permanently discard changes to all files? This action cannot be undone.'
218-
),
219-
buttons: [
220-
Dialog.cancelButton({ label: this.props.trans.__('Cancel') }),
221-
Dialog.warnButton({ label: this.props.trans.__('Discard') })
222-
]
223-
});
224-
if (result.button.accept) {
225-
try {
226-
await this.props.model.resetToCommit();
227-
} catch (reason) {
228-
showErrorMessage(
229-
this.props.trans.__('Discard all changes failed.'),
230-
reason
231-
);
232-
}
233-
}
215+
await discardAllChanges(this.props.model, this.props.trans);
234216
};
235217

236218
/** Add a specific unstaged file */

src/tokens.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1074,7 +1074,6 @@ export enum CommandIDs {
10741074
gitMerge = 'git:merge',
10751075
gitOpenGitignore = 'git:open-gitignore',
10761076
gitPush = 'git:push',
1077-
gitForcePush = 'git:force-push',
10781077
gitPull = 'git:pull',
10791078
gitSubmitCommand = 'git:submit-commit',
10801079
gitShowDiff = 'git:show-diff'

src/widgets/discardAllChanges.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { showDialog, Dialog, showErrorMessage } from '@jupyterlab/apputils';
2+
import { TranslationBundle } from '@jupyterlab/translation';
3+
import { IGitExtension } from '../tokens';
4+
5+
/**
6+
* Discard changes in all unstaged and staged files
7+
*
8+
* @param isFallback If dialog is called when the classical pull operation fails
9+
*/
10+
export async function discardAllChanges(
11+
model: IGitExtension,
12+
trans: TranslationBundle,
13+
isFallback?: boolean
14+
): Promise<void> {
15+
const result = await showDialog({
16+
title: trans.__('Discard all changes'),
17+
body: isFallback
18+
? trans.__(
19+
'Your current changes forbid pulling the latest changes. Do you want to permanently discard those changes? This action cannot be undone.'
20+
)
21+
: trans.__(
22+
'Are you sure you want to permanently discard changes to all files? This action cannot be undone.'
23+
),
24+
buttons: [
25+
Dialog.cancelButton({ label: trans.__('Cancel') }),
26+
Dialog.warnButton({ label: trans.__('Discard') })
27+
]
28+
});
29+
30+
if (result.button.accept) {
31+
try {
32+
return model.resetToCommit('HEAD');
33+
} catch (reason) {
34+
showErrorMessage(trans.__('Discard all changes failed.'), reason);
35+
return Promise.reject(reason);
36+
}
37+
}
38+
39+
return Promise.reject({
40+
cancelled: true,
41+
message: 'The user refused to discard all changes'
42+
});
43+
}

0 commit comments

Comments
 (0)