Skip to content

Commit 593f189

Browse files
Add "Reset to Remote" feature (#1087)
* Add new command `git:reset-to-remote` Add command `git:reset-to-remote` and also add it to the main menu. * Implement logic for `git:reset-to-remote` `GitExtension.resetToCommit(str)` is re-used because it also does `git reset --hard`. * Format using `prettier` * Update src/commandsAndMenu.tsx Co-authored-by: Frédéric Collonval <[email protected]> * Construct a new widget for `git:reset-to-remote` Construct a widget form which can be used as a `Dialog` body for command `git:reset-to-remote`. Also aim to address /pull/1087#discussion_r824696243 * Use `GitResetToRemoteForm` Use `GitResetToRemoteForm` for `git:reset-to-remote`. Also solve /pull/1087#discussion_r824696243 * Add a test for `git:reset-to-remote` Add a test to make sure `FileBrowserModel.manager.closeAll()` gets invoked. * Format using `prettier` * Make `GitResetToCommitForm` generic Refactor `GitResetToCommitForm` and make it a generic `CheckboxForm`. This should address /pull/1087#pullrequestreview-910168641. * Adjust `commands.spec.tsx` to adopt naming changes It is now `ICheckboxFormValue` instead of `IGitResetToRemoteFormValue`. * Adopt docstring suggestions Address /pull/1087#pullrequestreview-911933051 Co-authored-by: Frédéric Collonval <[email protected]>
1 parent cca3808 commit 593f189

File tree

5 files changed

+201
-1
lines changed

5 files changed

+201
-1
lines changed

schema/plugin.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@
109109
"command": "git:pull",
110110
"args": { "force": true }
111111
},
112+
{
113+
"command": "git:reset-to-remote"
114+
},
112115
{
113116
"command": "git:add-remote"
114117
},

src/commandsAndMenu.tsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
} from './tokens';
4848
import { GitCredentialsForm } from './widgets/CredentialsBox';
4949
import { discardAllChanges } from './widgets/discardAllChanges';
50+
import { CheckboxForm } from './widgets/GitResetToRemoteForm';
5051

5152
export interface IGitCloneArgs {
5253
/**
@@ -410,6 +411,64 @@ export function addCommands(
410411
}
411412
});
412413

414+
/** Add git reset --hard <remote-tracking-branch> command */
415+
commands.addCommand(CommandIDs.gitResetToRemote, {
416+
label: trans.__('Reset to Remote'),
417+
caption: trans.__('Reset Current Branch to Remote State'),
418+
isEnabled: () => gitModel.pathRepository !== null,
419+
execute: async () => {
420+
const result = await showDialog({
421+
title: trans.__('Reset to Remote'),
422+
body: new CheckboxForm(
423+
trans.__(
424+
'To bring the current branch to the state of its corresponding remote tracking branch, \
425+
a hard reset will be performed, which may result in some files being permanently deleted \
426+
and some changes being permanently discarded. Are you sure you want to proceed? \
427+
This action cannot be undone.'
428+
),
429+
trans.__('Close all opened files to avoid conflicts')
430+
),
431+
buttons: [
432+
Dialog.cancelButton({ label: trans.__('Cancel') }),
433+
Dialog.warnButton({ label: trans.__('Proceed') })
434+
]
435+
});
436+
if (result.button.accept) {
437+
try {
438+
if (result.value.checked) {
439+
logger.log({
440+
message: trans.__('Closing all opened files...'),
441+
level: Level.RUNNING
442+
});
443+
await fileBrowserModel.manager.closeAll();
444+
}
445+
logger.log({
446+
message: trans.__('Resetting...'),
447+
level: Level.RUNNING
448+
});
449+
await gitModel.resetToCommit(gitModel.status.remote);
450+
logger.log({
451+
message: trans.__('Successfully reset'),
452+
level: Level.SUCCESS,
453+
details: trans.__(
454+
'Successfully reset the current branch to its remote state'
455+
)
456+
});
457+
} catch (error) {
458+
console.error(
459+
'Encountered an error when resetting the current branch to its remote state. Error: ',
460+
error
461+
);
462+
logger.log({
463+
message: trans.__('Reset failed'),
464+
level: Level.ERROR,
465+
error
466+
});
467+
}
468+
}
469+
}
470+
});
471+
413472
/**
414473
* Git display diff command - internal command
415474
*
@@ -1141,6 +1200,7 @@ export function createGitMenu(
11411200
CommandIDs.gitMerge,
11421201
CommandIDs.gitPush,
11431202
CommandIDs.gitPull,
1203+
CommandIDs.gitResetToRemote,
11441204
CommandIDs.gitAddRemote,
11451205
CommandIDs.gitTerminalCommand
11461206
].forEach(command => {

src/tokens.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,16 @@ export namespace Git {
10241024
super('Not in a Git Repository');
10251025
}
10261026
}
1027+
1028+
/**
1029+
* Interface for dialog with one checkbox.
1030+
*/
1031+
export interface ICheckboxFormValue {
1032+
/**
1033+
* Checkbox value
1034+
*/
1035+
checked: boolean;
1036+
}
10271037
}
10281038

10291039
/**
@@ -1097,6 +1107,7 @@ export enum CommandIDs {
10971107
gitOpenGitignore = 'git:open-gitignore',
10981108
gitPush = 'git:push',
10991109
gitPull = 'git:pull',
1110+
gitResetToRemote = 'git:reset-to-remote',
11001111
gitSubmitCommand = 'git:submit-commit',
11011112
gitShowDiff = 'git:show-diff'
11021113
}

src/widgets/GitResetToRemoteForm.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Dialog } from '@jupyterlab/apputils';
2+
import { Widget } from '@lumino/widgets';
3+
import { Git } from '../tokens';
4+
5+
/**
6+
* A widget form containing a text block and a checkbox,
7+
* can be used as a Dialog body.
8+
*/
9+
export class CheckboxForm
10+
extends Widget
11+
implements Dialog.IBodyWidget<Git.ICheckboxFormValue>
12+
{
13+
constructor(textBody: string, checkboxLabel: string) {
14+
super();
15+
this.node.appendChild(this.createBody(textBody, checkboxLabel));
16+
}
17+
18+
private createBody(textBody: string, checkboxLabel: string): HTMLElement {
19+
const mainNode = document.createElement('div');
20+
21+
const text = document.createElement('div');
22+
text.textContent = textBody;
23+
24+
const checkboxContainer = document.createElement('label');
25+
26+
this._checkbox = document.createElement('input');
27+
this._checkbox.type = 'checkbox';
28+
this._checkbox.checked = true;
29+
30+
const label = document.createElement('span');
31+
label.textContent = checkboxLabel;
32+
33+
checkboxContainer.appendChild(this._checkbox);
34+
checkboxContainer.appendChild(label);
35+
36+
mainNode.appendChild(text);
37+
mainNode.appendChild(checkboxContainer);
38+
39+
return mainNode;
40+
}
41+
42+
getValue(): Git.ICheckboxFormValue {
43+
return {
44+
checked: this._checkbox.checked
45+
};
46+
}
47+
48+
private _checkbox: HTMLInputElement;
49+
}

tests/commands.spec.tsx

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { JupyterFrontEnd } from '@jupyterlab/application';
22
import { showDialog } from '@jupyterlab/apputils';
3+
import { FileBrowserModel } from '@jupyterlab/filebrowser';
34
import { nullTranslator } from '@jupyterlab/translation';
45
import { CommandRegistry } from '@lumino/commands';
56
import 'jest';
@@ -16,13 +17,22 @@ import {
1617

1718
jest.mock('../src/git');
1819
jest.mock('@jupyterlab/apputils');
20+
jest.mock('@jupyterlab/filebrowser');
1921

2022
describe('git-commands', () => {
2123
const mockGit = git as jest.Mocked<typeof git>;
2224
let commands: CommandRegistry;
2325
let model: GitExtension;
2426
let mockResponses: IMockedResponses;
2527

28+
const mockedFileBrowserModel = {
29+
manager: {
30+
closeAll: jest
31+
.fn<Promise<void>, any[]>()
32+
.mockImplementation(() => Promise.resolve())
33+
}
34+
} as any as FileBrowserModel;
35+
2636
beforeEach(async () => {
2737
jest.restoreAllMocks();
2838

@@ -39,7 +49,13 @@ describe('git-commands', () => {
3949
};
4050

4151
model = new GitExtension(app as any);
42-
addCommands(app as JupyterFrontEnd, model, null, null, nullTranslator);
52+
addCommands(
53+
app as JupyterFrontEnd,
54+
model,
55+
mockedFileBrowserModel,
56+
null,
57+
nullTranslator
58+
);
4359
});
4460

4561
describe('git:add-remote', () => {
@@ -159,4 +175,65 @@ describe('git-commands', () => {
159175
});
160176
});
161177
});
178+
179+
describe('git:reset-to-remote', () => {
180+
[true, false].forEach(checked => {
181+
it(
182+
checked
183+
? 'should close all opened files when the checkbox is checked'
184+
: 'should not close all opened files when the checkbox is not checked',
185+
async () => {
186+
const mockDialog = showDialog as jest.MockedFunction<
187+
typeof showDialog
188+
>;
189+
mockDialog.mockResolvedValue({
190+
button: {
191+
accept: true,
192+
actions: [],
193+
caption: '',
194+
className: '',
195+
displayType: 'default',
196+
iconClass: '',
197+
iconLabel: '',
198+
label: ''
199+
},
200+
value: {
201+
checked
202+
} as Git.ICheckboxFormValue
203+
});
204+
205+
const spyCloseAll = jest.spyOn(
206+
mockedFileBrowserModel.manager,
207+
'closeAll'
208+
);
209+
spyCloseAll.mockResolvedValueOnce(undefined);
210+
211+
mockGit.requestAPI.mockImplementation(
212+
mockedRequestAPI({
213+
...mockResponses,
214+
reset_to_commit: {
215+
body: () => {
216+
return { code: 0 };
217+
}
218+
}
219+
})
220+
);
221+
222+
const path = DEFAULT_REPOSITORY_PATH;
223+
model.pathRepository = path;
224+
await model.ready;
225+
226+
await commands.execute(CommandIDs.gitResetToRemote);
227+
228+
if (checked) {
229+
expect(spyCloseAll).toHaveBeenCalled();
230+
} else {
231+
expect(spyCloseAll).not.toHaveBeenCalled();
232+
}
233+
234+
spyCloseAll.mockRestore();
235+
}
236+
);
237+
});
238+
});
162239
});

0 commit comments

Comments
 (0)