diff --git a/src/cloneCommand.ts b/src/cloneCommand.ts new file mode 100644 index 000000000..551cdd22d --- /dev/null +++ b/src/cloneCommand.ts @@ -0,0 +1,88 @@ +import { + JupyterFrontEnd, + JupyterFrontEndPlugin +} from '@jupyterlab/application'; +import { ITranslator, nullTranslator } from '@jupyterlab/translation'; +import { CommandIDs, IGitExtension, Level } from './tokens'; +import { IFileBrowserFactory } from '@jupyterlab/filebrowser'; +import { Dialog, showDialog } from '@jupyterlab/apputils'; +import { GitCloneForm } from './widgets/GitCloneForm'; +import { logger } from './logger'; +import { + addFileBrowserContextMenu, + IGitCloneArgs, + Operation, + showGitOperationDialog +} from './commandsAndMenu'; +import { GitExtension } from './model'; +import { addCloneButton } from './widgets/gitClone'; + +export const gitCloneCommandPlugin: JupyterFrontEndPlugin = { + id: '@jupyterlab/git:clone', + requires: [ITranslator, IGitExtension, IFileBrowserFactory], + activate: ( + app: JupyterFrontEnd, + translator: ITranslator, + gitModel: IGitExtension, + fileBrowserFactory: IFileBrowserFactory + ) => { + translator = translator || nullTranslator; + const trans = translator.load('jupyterlab_git'); + const fileBrowser = fileBrowserFactory.defaultBrowser; + const fileBrowserModel = fileBrowser.model; + /** Add git clone command */ + app.commands.addCommand(CommandIDs.gitClone, { + label: trans.__('Clone a Repository'), + caption: trans.__('Clone a repository from a URL'), + isEnabled: () => gitModel.pathRepository === null, + execute: async () => { + const result = await showDialog({ + title: trans.__('Clone a repo'), + body: new GitCloneForm(trans), + focusNodeSelector: 'input', + buttons: [ + Dialog.cancelButton({ label: trans.__('Cancel') }), + Dialog.okButton({ label: trans.__('Clone') }) + ] + }); + + if (result.button.accept && result.value) { + logger.log({ + level: Level.RUNNING, + message: trans.__('Cloning…') + }); + try { + const details = await showGitOperationDialog( + gitModel as GitExtension, + Operation.Clone, + trans, + { path: fileBrowserModel.path, url: result.value } + ); + logger.log({ + message: trans.__('Successfully cloned'), + level: Level.SUCCESS, + details + }); + await fileBrowserModel.refresh(); + } catch (error) { + console.error( + 'Encountered an error when cloning the repository. Error: ', + error + ); + logger.log({ + message: trans.__('Failed to clone'), + level: Level.ERROR, + error: error as Error + }); + } + } + } + }); + // Add a clone button to the file browser extension toolbar + addCloneButton(gitModel, fileBrowser, app.commands); + + // Add the context menu items for the default file browser + addFileBrowserContextMenu(gitModel, fileBrowser, app.contextMenu); + }, + autoStart: true +}; diff --git a/src/commandsAndMenu.tsx b/src/commandsAndMenu.tsx index fe34bda64..5f5459ec1 100644 --- a/src/commandsAndMenu.tsx +++ b/src/commandsAndMenu.tsx @@ -47,9 +47,8 @@ import { } from './tokens'; import { GitCredentialsForm } from './widgets/CredentialsBox'; import { discardAllChanges } from './widgets/discardAllChanges'; -import { GitCloneForm } from './widgets/GitCloneForm'; -interface IGitCloneArgs { +export interface IGitCloneArgs { /** * Path in which to clone the Git repository */ @@ -63,7 +62,7 @@ interface IGitCloneArgs { /** * Git operations requiring authentication */ -enum Operation { +export enum Operation { Clone = 'Clone', Pull = 'Pull', Push = 'Push', @@ -291,55 +290,6 @@ export function addCommands( } }); - /** Add git clone command */ - commands.addCommand(CommandIDs.gitClone, { - label: trans.__('Clone a Repository'), - caption: trans.__('Clone a repository from a URL'), - isEnabled: () => gitModel.pathRepository === null, - execute: async () => { - const result = await showDialog({ - title: trans.__('Clone a repo'), - body: new GitCloneForm(trans), - focusNodeSelector: 'input', - buttons: [ - Dialog.cancelButton({ label: trans.__('Cancel') }), - Dialog.okButton({ label: trans.__('Clone') }) - ] - }); - - if (result.button.accept && result.value) { - logger.log({ - level: Level.RUNNING, - message: trans.__('Cloning…') - }); - try { - const details = await Private.showGitOperationDialog( - gitModel, - Operation.Clone, - trans, - { path: fileBrowserModel.path, url: result.value } - ); - logger.log({ - message: trans.__('Successfully cloned'), - level: Level.SUCCESS, - details - }); - await fileBrowserModel.refresh(); - } catch (error) { - console.error( - 'Encountered an error when cloning the repository. Error: ', - error - ); - logger.log({ - message: trans.__('Failed to clone'), - level: Level.ERROR, - error: error as Error - }); - } - } - } - }); - /** Add git open gitignore command */ commands.addCommand(CommandIDs.gitOpenGitignore, { label: trans.__('Open .gitignore'), @@ -364,7 +314,7 @@ export function addCommands( message: trans.__('Pushing…') }); try { - const details = await Private.showGitOperationDialog( + const details = await showGitOperationDialog( gitModel, args.force ? Operation.ForcePush : Operation.Push, trans @@ -410,7 +360,7 @@ export function addCommands( level: Level.RUNNING, message: trans.__('Pulling…') }); - const details = await Private.showGitOperationDialog( + const details = await showGitOperationDialog( gitModel, Operation.Pull, trans @@ -1399,84 +1349,80 @@ export function addFileBrowserContextMenu( } } -/* eslint-disable no-inner-declarations */ -namespace Private { - /** - * Handle Git operation that may require authentication. - * - * @private - * @param model - Git extension model - * @param operation - Git operation name - * @param trans - language translator - * @param args - Git operation arguments - * @param authentication - Git authentication information - * @param retry - Is this operation retried? - * @returns Promise for displaying a dialog - */ - export async function showGitOperationDialog( - model: GitExtension, - operation: Operation, - trans: TranslationBundle, - args?: T, - authentication?: Git.IAuth, - retry = false - ): Promise { - try { - let result: Git.IResultWithMessage; - // the Git action - switch (operation) { - case Operation.Clone: - // eslint-disable-next-line no-case-declarations - const { path, url } = args as any as IGitCloneArgs; - result = await model.clone(path, url, authentication); - break; - case Operation.Pull: - result = await model.pull(authentication); - break; - case Operation.Push: - result = await model.push(authentication); - break; - case Operation.ForcePush: - result = await model.push(authentication, true); - break; - default: - result = { code: -1, message: 'Unknown git command' }; - break; - } +/** + * Handle Git operation that may require authentication. + * + * @private + * @param model - Git extension model + * @param operation - Git operation name + * @param trans - language translator + * @param args - Git operation arguments + * @param authentication - Git authentication information + * @param retry - Is this operation retried? + * @returns Promise for displaying a dialog + */ +export async function showGitOperationDialog( + model: GitExtension, + operation: Operation, + trans: TranslationBundle, + args?: T, + authentication?: Git.IAuth, + retry = false +): Promise { + try { + let result: Git.IResultWithMessage; + // the Git action + switch (operation) { + case Operation.Clone: + // eslint-disable-next-line no-case-declarations + const { path, url } = args as any as IGitCloneArgs; + result = await model.clone(path, url, authentication); + break; + case Operation.Pull: + result = await model.pull(authentication); + break; + case Operation.Push: + result = await model.push(authentication); + break; + case Operation.ForcePush: + result = await model.push(authentication, true); + break; + default: + result = { code: -1, message: 'Unknown git command' }; + break; + } - return result.message; - } catch (error) { - if ( - AUTH_ERROR_MESSAGES.some( - errorMessage => (error as Error).message.indexOf(errorMessage) > -1 + return result.message; + } catch (error) { + if ( + AUTH_ERROR_MESSAGES.some( + errorMessage => (error as Error).message.indexOf(errorMessage) > -1 + ) + ) { + // If the error is an authentication error, ask the user credentials + const credentials = await showDialog({ + title: trans.__('Git credentials required'), + body: new GitCredentialsForm( + trans, + trans.__('Enter credentials for remote repository'), + retry ? trans.__('Incorrect username or password.') : '' ) - ) { - // If the error is an authentication error, ask the user credentials - const credentials = await showDialog({ - title: trans.__('Git credentials required'), - body: new GitCredentialsForm( - trans, - trans.__('Enter credentials for remote repository'), - retry ? trans.__('Incorrect username or password.') : '' - ) - }); + }); - if (credentials.button.accept) { - // Retry the operation if the user provides its credentials - return await showGitOperationDialog( - model, - operation, - trans, - args, - credentials.value, - true - ); - } + if (credentials.button.accept) { + // Retry the operation if the user provides its credentials + return await showGitOperationDialog( + model, + operation, + trans, + args, + credentials.value, + true + ); } - // Throw the error if it cannot be handled or - // if the user did not accept to provide its credentials - throw error; } + // Throw the error if it cannot be handled or + // if the user did not accept to provide its credentials + throw error; } } -/* eslint-enable no-inner-declarations */ diff --git a/src/components/GitPanel.tsx b/src/components/GitPanel.tsx index 084eb066a..19440e560 100644 --- a/src/components/GitPanel.tsx +++ b/src/components/GitPanel.tsx @@ -472,15 +472,17 @@ export class GitPanel extends React.Component { > {this.props.trans.__('Initialize a Repository')} - + {commands.hasCommand(CommandIDs.gitClone) && ( + + )} ); } diff --git a/src/index.ts b/src/index.ts index 589e92427..9515ff1a3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,7 @@ import { gitIcon } from './style/icons'; import { Git, IGitExtension } from './tokens'; import { addCloneButton } from './widgets/gitClone'; import { GitWidget } from './widgets/GitWidget'; +import { gitCloneCommandPlugin } from './cloneCommand'; export { DiffModel } from './components/diff/model'; export { NotebookDiff } from './components/diff/NotebookDiff'; @@ -54,7 +55,7 @@ const plugin: JupyterFrontEndPlugin = { /** * Export the plugin as default. */ -export default plugin; +export default [plugin, gitCloneCommandPlugin]; /** * Activate the running plugin. diff --git a/tests/plugin.spec.ts b/tests/plugin.spec.ts index a56c14c11..ff37a55dd 100644 --- a/tests/plugin.spec.ts +++ b/tests/plugin.spec.ts @@ -1,6 +1,6 @@ import 'jest'; import * as git from '../src/git'; -import plugin from '../src/index'; +import plugins from '../src/index'; import { version } from '../src/version'; import { ISettingRegistry, SettingRegistry } from '@jupyterlab/settingregistry'; import { JupyterLab } from '@jupyterlab/application'; @@ -17,6 +17,8 @@ jest.mock('@jupyterlab/application'); jest.mock('@jupyterlab/apputils'); jest.mock('@jupyterlab/settingregistry'); +const plugin = plugins[0]; + describe('plugin', () => { const mockGit = git as jest.Mocked; let app: jest.Mocked;