Skip to content

Commit 7517a87

Browse files
3coinsPiyush Jainfcollonval
authored
Moved clone command to plugin for extensibility (#1051)
* Moved clone command to plugin for extensibility * Renamed plugin id Co-authored-by: Frédéric Collonval <fcollonval@users.noreply.github.com> Co-authored-by: Piyush Jain <pijain@amazon.com> Co-authored-by: Frédéric Collonval <fcollonval@users.noreply.github.com>
1 parent 7707fc2 commit 7517a87

File tree

5 files changed

+178
-139
lines changed

5 files changed

+178
-139
lines changed

src/cloneCommand.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {
2+
JupyterFrontEnd,
3+
JupyterFrontEndPlugin
4+
} from '@jupyterlab/application';
5+
import { ITranslator, nullTranslator } from '@jupyterlab/translation';
6+
import { CommandIDs, IGitExtension, Level } from './tokens';
7+
import { IFileBrowserFactory } from '@jupyterlab/filebrowser';
8+
import { Dialog, showDialog } from '@jupyterlab/apputils';
9+
import { GitCloneForm } from './widgets/GitCloneForm';
10+
import { logger } from './logger';
11+
import {
12+
addFileBrowserContextMenu,
13+
IGitCloneArgs,
14+
Operation,
15+
showGitOperationDialog
16+
} from './commandsAndMenu';
17+
import { GitExtension } from './model';
18+
import { addCloneButton } from './widgets/gitClone';
19+
20+
export const gitCloneCommandPlugin: JupyterFrontEndPlugin<void> = {
21+
id: '@jupyterlab/git:clone',
22+
requires: [ITranslator, IGitExtension, IFileBrowserFactory],
23+
activate: (
24+
app: JupyterFrontEnd,
25+
translator: ITranslator,
26+
gitModel: IGitExtension,
27+
fileBrowserFactory: IFileBrowserFactory
28+
) => {
29+
translator = translator || nullTranslator;
30+
const trans = translator.load('jupyterlab_git');
31+
const fileBrowser = fileBrowserFactory.defaultBrowser;
32+
const fileBrowserModel = fileBrowser.model;
33+
/** Add git clone command */
34+
app.commands.addCommand(CommandIDs.gitClone, {
35+
label: trans.__('Clone a Repository'),
36+
caption: trans.__('Clone a repository from a URL'),
37+
isEnabled: () => gitModel.pathRepository === null,
38+
execute: async () => {
39+
const result = await showDialog({
40+
title: trans.__('Clone a repo'),
41+
body: new GitCloneForm(trans),
42+
focusNodeSelector: 'input',
43+
buttons: [
44+
Dialog.cancelButton({ label: trans.__('Cancel') }),
45+
Dialog.okButton({ label: trans.__('Clone') })
46+
]
47+
});
48+
49+
if (result.button.accept && result.value) {
50+
logger.log({
51+
level: Level.RUNNING,
52+
message: trans.__('Cloning…')
53+
});
54+
try {
55+
const details = await showGitOperationDialog<IGitCloneArgs>(
56+
gitModel as GitExtension,
57+
Operation.Clone,
58+
trans,
59+
{ path: fileBrowserModel.path, url: result.value }
60+
);
61+
logger.log({
62+
message: trans.__('Successfully cloned'),
63+
level: Level.SUCCESS,
64+
details
65+
});
66+
await fileBrowserModel.refresh();
67+
} catch (error) {
68+
console.error(
69+
'Encountered an error when cloning the repository. Error: ',
70+
error
71+
);
72+
logger.log({
73+
message: trans.__('Failed to clone'),
74+
level: Level.ERROR,
75+
error: error as Error
76+
});
77+
}
78+
}
79+
}
80+
});
81+
// Add a clone button to the file browser extension toolbar
82+
addCloneButton(gitModel, fileBrowser, app.commands);
83+
84+
// Add the context menu items for the default file browser
85+
addFileBrowserContextMenu(gitModel, fileBrowser, app.contextMenu);
86+
},
87+
autoStart: true
88+
};

src/commandsAndMenu.tsx

Lines changed: 74 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,8 @@ import {
4747
} from './tokens';
4848
import { GitCredentialsForm } from './widgets/CredentialsBox';
4949
import { discardAllChanges } from './widgets/discardAllChanges';
50-
import { GitCloneForm } from './widgets/GitCloneForm';
5150

52-
interface IGitCloneArgs {
51+
export interface IGitCloneArgs {
5352
/**
5453
* Path in which to clone the Git repository
5554
*/
@@ -63,7 +62,7 @@ interface IGitCloneArgs {
6362
/**
6463
* Git operations requiring authentication
6564
*/
66-
enum Operation {
65+
export enum Operation {
6766
Clone = 'Clone',
6867
Pull = 'Pull',
6968
Push = 'Push',
@@ -291,55 +290,6 @@ export function addCommands(
291290
}
292291
});
293292

294-
/** Add git clone command */
295-
commands.addCommand(CommandIDs.gitClone, {
296-
label: trans.__('Clone a Repository'),
297-
caption: trans.__('Clone a repository from a URL'),
298-
isEnabled: () => gitModel.pathRepository === null,
299-
execute: async () => {
300-
const result = await showDialog({
301-
title: trans.__('Clone a repo'),
302-
body: new GitCloneForm(trans),
303-
focusNodeSelector: 'input',
304-
buttons: [
305-
Dialog.cancelButton({ label: trans.__('Cancel') }),
306-
Dialog.okButton({ label: trans.__('Clone') })
307-
]
308-
});
309-
310-
if (result.button.accept && result.value) {
311-
logger.log({
312-
level: Level.RUNNING,
313-
message: trans.__('Cloning…')
314-
});
315-
try {
316-
const details = await Private.showGitOperationDialog<IGitCloneArgs>(
317-
gitModel,
318-
Operation.Clone,
319-
trans,
320-
{ path: fileBrowserModel.path, url: result.value }
321-
);
322-
logger.log({
323-
message: trans.__('Successfully cloned'),
324-
level: Level.SUCCESS,
325-
details
326-
});
327-
await fileBrowserModel.refresh();
328-
} catch (error) {
329-
console.error(
330-
'Encountered an error when cloning the repository. Error: ',
331-
error
332-
);
333-
logger.log({
334-
message: trans.__('Failed to clone'),
335-
level: Level.ERROR,
336-
error: error as Error
337-
});
338-
}
339-
}
340-
}
341-
});
342-
343293
/** Add git open gitignore command */
344294
commands.addCommand(CommandIDs.gitOpenGitignore, {
345295
label: trans.__('Open .gitignore'),
@@ -364,7 +314,7 @@ export function addCommands(
364314
message: trans.__('Pushing…')
365315
});
366316
try {
367-
const details = await Private.showGitOperationDialog(
317+
const details = await showGitOperationDialog(
368318
gitModel,
369319
args.force ? Operation.ForcePush : Operation.Push,
370320
trans
@@ -410,7 +360,7 @@ export function addCommands(
410360
level: Level.RUNNING,
411361
message: trans.__('Pulling…')
412362
});
413-
const details = await Private.showGitOperationDialog(
363+
const details = await showGitOperationDialog(
414364
gitModel,
415365
Operation.Pull,
416366
trans
@@ -1399,84 +1349,80 @@ export function addFileBrowserContextMenu(
13991349
}
14001350
}
14011351

1402-
/* eslint-disable no-inner-declarations */
1403-
namespace Private {
1404-
/**
1405-
* Handle Git operation that may require authentication.
1406-
*
1407-
* @private
1408-
* @param model - Git extension model
1409-
* @param operation - Git operation name
1410-
* @param trans - language translator
1411-
* @param args - Git operation arguments
1412-
* @param authentication - Git authentication information
1413-
* @param retry - Is this operation retried?
1414-
* @returns Promise for displaying a dialog
1415-
*/
1416-
export async function showGitOperationDialog<T>(
1417-
model: GitExtension,
1418-
operation: Operation,
1419-
trans: TranslationBundle,
1420-
args?: T,
1421-
authentication?: Git.IAuth,
1422-
retry = false
1423-
): Promise<string> {
1424-
try {
1425-
let result: Git.IResultWithMessage;
1426-
// the Git action
1427-
switch (operation) {
1428-
case Operation.Clone:
1429-
// eslint-disable-next-line no-case-declarations
1430-
const { path, url } = args as any as IGitCloneArgs;
1431-
result = await model.clone(path, url, authentication);
1432-
break;
1433-
case Operation.Pull:
1434-
result = await model.pull(authentication);
1435-
break;
1436-
case Operation.Push:
1437-
result = await model.push(authentication);
1438-
break;
1439-
case Operation.ForcePush:
1440-
result = await model.push(authentication, true);
1441-
break;
1442-
default:
1443-
result = { code: -1, message: 'Unknown git command' };
1444-
break;
1445-
}
1352+
/**
1353+
* Handle Git operation that may require authentication.
1354+
*
1355+
* @private
1356+
* @param model - Git extension model
1357+
* @param operation - Git operation name
1358+
* @param trans - language translator
1359+
* @param args - Git operation arguments
1360+
* @param authentication - Git authentication information
1361+
* @param retry - Is this operation retried?
1362+
* @returns Promise for displaying a dialog
1363+
*/
1364+
export async function showGitOperationDialog<T>(
1365+
model: GitExtension,
1366+
operation: Operation,
1367+
trans: TranslationBundle,
1368+
args?: T,
1369+
authentication?: Git.IAuth,
1370+
retry = false
1371+
): Promise<string> {
1372+
try {
1373+
let result: Git.IResultWithMessage;
1374+
// the Git action
1375+
switch (operation) {
1376+
case Operation.Clone:
1377+
// eslint-disable-next-line no-case-declarations
1378+
const { path, url } = args as any as IGitCloneArgs;
1379+
result = await model.clone(path, url, authentication);
1380+
break;
1381+
case Operation.Pull:
1382+
result = await model.pull(authentication);
1383+
break;
1384+
case Operation.Push:
1385+
result = await model.push(authentication);
1386+
break;
1387+
case Operation.ForcePush:
1388+
result = await model.push(authentication, true);
1389+
break;
1390+
default:
1391+
result = { code: -1, message: 'Unknown git command' };
1392+
break;
1393+
}
14461394

1447-
return result.message;
1448-
} catch (error) {
1449-
if (
1450-
AUTH_ERROR_MESSAGES.some(
1451-
errorMessage => (error as Error).message.indexOf(errorMessage) > -1
1395+
return result.message;
1396+
} catch (error) {
1397+
if (
1398+
AUTH_ERROR_MESSAGES.some(
1399+
errorMessage => (error as Error).message.indexOf(errorMessage) > -1
1400+
)
1401+
) {
1402+
// If the error is an authentication error, ask the user credentials
1403+
const credentials = await showDialog({
1404+
title: trans.__('Git credentials required'),
1405+
body: new GitCredentialsForm(
1406+
trans,
1407+
trans.__('Enter credentials for remote repository'),
1408+
retry ? trans.__('Incorrect username or password.') : ''
14521409
)
1453-
) {
1454-
// If the error is an authentication error, ask the user credentials
1455-
const credentials = await showDialog({
1456-
title: trans.__('Git credentials required'),
1457-
body: new GitCredentialsForm(
1458-
trans,
1459-
trans.__('Enter credentials for remote repository'),
1460-
retry ? trans.__('Incorrect username or password.') : ''
1461-
)
1462-
});
1410+
});
14631411

1464-
if (credentials.button.accept) {
1465-
// Retry the operation if the user provides its credentials
1466-
return await showGitOperationDialog<T>(
1467-
model,
1468-
operation,
1469-
trans,
1470-
args,
1471-
credentials.value,
1472-
true
1473-
);
1474-
}
1412+
if (credentials.button.accept) {
1413+
// Retry the operation if the user provides its credentials
1414+
return await showGitOperationDialog<T>(
1415+
model,
1416+
operation,
1417+
trans,
1418+
args,
1419+
credentials.value,
1420+
true
1421+
);
14751422
}
1476-
// Throw the error if it cannot be handled or
1477-
// if the user did not accept to provide its credentials
1478-
throw error;
14791423
}
1424+
// Throw the error if it cannot be handled or
1425+
// if the user did not accept to provide its credentials
1426+
throw error;
14801427
}
14811428
}
1482-
/* eslint-enable no-inner-declarations */

src/components/GitPanel.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -472,15 +472,17 @@ export class GitPanel extends React.Component<IGitPanelProps, IGitPanelState> {
472472
>
473473
{this.props.trans.__('Initialize a Repository')}
474474
</button>
475-
<button
476-
className={repoButtonClass}
477-
onClick={async () => {
478-
await commands.execute(CommandIDs.gitClone);
479-
await commands.execute('filebrowser:toggle-main');
480-
}}
481-
>
482-
{this.props.trans.__('Clone a Repository')}
483-
</button>
475+
{commands.hasCommand(CommandIDs.gitClone) && (
476+
<button
477+
className={repoButtonClass}
478+
onClick={async () => {
479+
await commands.execute(CommandIDs.gitClone);
480+
await commands.execute('filebrowser:toggle-main');
481+
}}
482+
>
483+
{this.props.trans.__('Clone a Repository')}
484+
</button>
485+
)}
484486
</React.Fragment>
485487
);
486488
}

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { gitIcon } from './style/icons';
2525
import { Git, IGitExtension } from './tokens';
2626
import { addCloneButton } from './widgets/gitClone';
2727
import { GitWidget } from './widgets/GitWidget';
28+
import { gitCloneCommandPlugin } from './cloneCommand';
2829

2930
export { DiffModel } from './components/diff/model';
3031
export { NotebookDiff } from './components/diff/NotebookDiff';
@@ -54,7 +55,7 @@ const plugin: JupyterFrontEndPlugin<IGitExtension> = {
5455
/**
5556
* Export the plugin as default.
5657
*/
57-
export default plugin;
58+
export default [plugin, gitCloneCommandPlugin];
5859

5960
/**
6061
* Activate the running plugin.

tests/plugin.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import 'jest';
22
import * as git from '../src/git';
3-
import plugin from '../src/index';
3+
import plugins from '../src/index';
44
import { version } from '../src/version';
55
import { ISettingRegistry, SettingRegistry } from '@jupyterlab/settingregistry';
66
import { JupyterLab } from '@jupyterlab/application';
@@ -17,6 +17,8 @@ jest.mock('@jupyterlab/application');
1717
jest.mock('@jupyterlab/apputils');
1818
jest.mock('@jupyterlab/settingregistry');
1919

20+
const plugin = plugins[0];
21+
2022
describe('plugin', () => {
2123
const mockGit = git as jest.Mocked<typeof git>;
2224
let app: jest.Mocked<JupyterLab>;

0 commit comments

Comments
 (0)