diff --git a/README.md b/README.md
index 7e178eab0..8cb323747 100644
--- a/README.md
+++ b/README.md
@@ -33,8 +33,11 @@ jupyter lab build
Once installed, extension behavior can be modified via the following settings which can be set in JupyterLab's advanced settings editor:
+- **blockWhileCommandExecutes**: suspend JupyterLab user interaction until Git commands (e.g., `commit`, `pull`, `reset`, `revert`) finish executing. Setting this to `true` helps mitigate potential race conditions leading to data loss, conflicts, and a broken Git history. Unless running a slow network, UI suspension should not interfere with standard workflows. Setting this to `false` allows for actions to trigger multiple concurrent Git actions.
+- **cancelPullMergeConflict**: cancel pulling changes from a remote repository if there exists a merge conflict. If set to `true`, when fetching and integrating changes from a remote repository, a conflicting merge is canceled and the working tree left untouched.
- **disableBranchWithChanges**: disable all branch operations, such as creating a new branch or switching to a different branch, when there are changed/staged files. When set to `true`, this setting guards against overwriting and/or losing uncommitted changes.
-- **doubleClickDiff**: double click a file in the Git UI to open a diff of the file instead of opening the file for editing.
+- **displayStatus**: display Git extension status updates in the JupyterLab status bar. If `true`, the extension displays status updates in the JupyterLab status bar, such as when pulling and pushing changes, switching branches, and polling for changes. Depending on the level of extension activity, some users may find the status updates distracting. In which case, setting this to `false` should reduce visual noise.
+- **doubleClickDiff**: double click a file in the Git extension panel to open a diff of the file instead of opening the file for editing.
- **historyCount**: number of commits shown in the history log, beginning with the most recent. Displaying a larger number of commits can lead to performance degradation, so use caution when modifying this setting.
- **refreshInterval**: number of milliseconds between polling the file system for changes. In order to ensure that the UI correctly displays the current repository status, the extension must poll the file system for changes. Longer polling times increase the likelihood that the UI does not reflect the current status; however, longer polling times also incur less performance overhead.
- **simpleStaging**: enable a simplified concept of staging. When this setting is `true`, all files with changes are automatically staged. When we develop in JupyterLab, we often only care about what files have changed (in the broadest sense) and don't need to distinguish between "tracked" and "untracked" files. Accordingly, this setting allows us to simplify the visual presentation of changes, which is especially useful for those less acquainted with Git.
diff --git a/package.json b/package.json
index 04361eb61..05d8ee256 100644
--- a/package.json
+++ b/package.json
@@ -62,10 +62,12 @@
"@jupyterlab/settingregistry": "^2.0.0",
"@jupyterlab/terminal": "^2.0.0",
"@jupyterlab/ui-components": "^2.0.0",
+ "@lumino/collections": "^1.2.3",
"@lumino/polling": "^1.0.4",
"@lumino/widgets": "^1.11.1",
"@material-ui/core": "^4.8.2",
"@material-ui/icons": "^4.5.1",
+ "@material-ui/lab": "^4.0.0-alpha.54",
"diff-match-patch": "^1.0.4",
"nbdime": "^6.0.0",
"react": "~16.9.0",
diff --git a/schema/plugin.json b/schema/plugin.json
index f30944f28..32142b64b 100644
--- a/schema/plugin.json
+++ b/schema/plugin.json
@@ -5,6 +5,12 @@
"description": "jupyterlab-git settings.",
"type": "object",
"properties": {
+ "blockWhileCommandExecutes": {
+ "type": "boolean",
+ "title": "Suspend user interaction until commands finish",
+ "description": "Suspend JupyterLab user interaction until Git commands (e.g., commit, pull, reset, revert) finish executing. Setting this to true helps mitigate potential race conditions leading to data loss, conflicts, and a broken Git history. Unless running a slow network, UI suspension should not interfere with standard workflows. Setting this to false allows for actions to trigger multiple concurrent Git actions.",
+ "default": true
+ },
"cancelPullMergeConflict": {
"type": "boolean",
"title": "Cancel pull merge conflict",
@@ -17,10 +23,16 @@
"description": "Disable all branch operations (new, switch) when there are changed/staged files",
"default": false
},
+ "displayStatus": {
+ "type": "boolean",
+ "title": "Display Git status updates",
+ "description": "Display Git extension status updates in the JupyterLab status bar. If true, the extension displays status updates in the JupyterLab status bar, such as when pulling and pushing changes, switching branches, and polling for changes. Depending on the level of extension activity, some users may find the status updates distracting. In which case, setting this to false should reduce visual noise.",
+ "default": true
+ },
"doubleClickDiff": {
"type": "boolean",
"title": "Show diff on double click",
- "description": "If true, doubling clicking a file in the list of changed files will open a diff",
+ "description": "If true, doubling clicking a file in the list of changed files will open a diff.",
"default": false
},
"historyCount": {
diff --git a/src/gitMenuCommands.ts b/src/commandsAndMenu.ts
similarity index 84%
rename from src/gitMenuCommands.ts
rename to src/commandsAndMenu.ts
index ed203a479..15cd8c52b 100644
--- a/src/gitMenuCommands.ts
+++ b/src/commandsAndMenu.ts
@@ -9,10 +9,23 @@ import {
import { FileBrowser } from '@jupyterlab/filebrowser';
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { ITerminal } from '@jupyterlab/terminal';
+import { CommandRegistry } from '@lumino/commands';
+import { Menu } from '@lumino/widgets';
import { IGitExtension } from './tokens';
+import { GitCredentialsForm } from './widgets/CredentialsBox';
import { doGitClone } from './widgets/gitClone';
import { GitPullPushDialog, Operation } from './widgets/gitPushPull';
-import { GitCredentialsForm } from './widgets/CredentialsBox';
+
+const RESOURCES = [
+ {
+ text: 'Set Up Remotes',
+ url: 'https://www.atlassian.com/git/tutorials/setting-up-a-repository'
+ },
+ {
+ text: 'Git Documentation',
+ url: 'https://git-scm.com/doc'
+ }
+];
/**
* The command IDs used by the git plugin.
@@ -210,6 +223,52 @@ export function addCommands(
});
}
+/**
+ * Adds commands and menu items.
+ *
+ * @private
+ * @param app - Jupyter front end
+ * @param gitExtension - Git extension instance
+ * @param fileBrowser - file browser instance
+ * @param settings - extension settings
+ * @returns menu
+ */
+export function createGitMenu(commands: CommandRegistry): Menu {
+ const menu = new Menu({ commands });
+ menu.title.label = 'Git';
+ [
+ CommandIDs.gitInit,
+ CommandIDs.gitClone,
+ CommandIDs.gitPush,
+ CommandIDs.gitPull,
+ CommandIDs.gitAddRemote,
+ CommandIDs.gitTerminalCommand
+ ].forEach(command => {
+ menu.addItem({ command });
+ });
+
+ menu.addItem({ type: 'separator' });
+
+ menu.addItem({ command: CommandIDs.gitToggleSimpleStaging });
+
+ menu.addItem({ command: CommandIDs.gitToggleDoubleClickDiff });
+
+ menu.addItem({ type: 'separator' });
+
+ const tutorial = new Menu({ commands });
+ tutorial.title.label = ' Help ';
+ RESOURCES.map(args => {
+ tutorial.addItem({
+ args,
+ command: CommandIDs.gitOpenUrl
+ });
+ });
+
+ menu.addItem({ type: 'submenu', submenu: tutorial });
+
+ return menu;
+}
+
/* eslint-disable no-inner-declarations */
namespace Private {
/**
diff --git a/src/components/Alert.tsx b/src/components/Alert.tsx
new file mode 100644
index 000000000..b41a07e1d
--- /dev/null
+++ b/src/components/Alert.tsx
@@ -0,0 +1,127 @@
+import * as React from 'react';
+import Portal from '@material-ui/core/Portal';
+import Snackbar from '@material-ui/core/Snackbar';
+import Slide from '@material-ui/core/Slide';
+import { default as MuiAlert } from '@material-ui/lab/Alert';
+import { Severity } from '../tokens';
+
+/**
+ * Returns a React component for "sliding-in" an alert.
+ *
+ * @private
+ * @param props - component properties
+ * @returns React element
+ */
+function SlideTransition(props: any): React.ReactElement {
+ return ;
+}
+
+/**
+ * Interface describing component properties.
+ */
+export interface IAlertProps {
+ /**
+ * Boolean indicating whether to display an alert.
+ */
+ open: boolean;
+
+ /**
+ * Alert message.
+ */
+ message: string;
+
+ /**
+ * Alert severity.
+ */
+ severity?: Severity;
+
+ /**
+ * Alert duration (in milliseconds).
+ */
+ duration?: number;
+
+ /**
+ * Callback invoked upon clicking on an alert.
+ */
+ onClick?: (event?: any) => void;
+
+ /**
+ * Callback invoked upon closing an alert.
+ */
+ onClose: (event?: any) => void;
+}
+
+/**
+ * React component for rendering an alert.
+ */
+export class Alert extends React.Component {
+ /**
+ * Returns a React component for rendering an alert.
+ *
+ * @param props - component properties
+ * @returns React component
+ */
+ constructor(props: IAlertProps) {
+ super(props);
+ }
+
+ /**
+ * Renders the component.
+ *
+ * @returns React element
+ */
+ render(): React.ReactElement {
+ let duration: number | null = null;
+
+ const severity = this.props.severity || 'info';
+ if (severity === 'success') {
+ duration = this.props.duration || 5000; // milliseconds
+ }
+ return (
+
+
+
+ {this.props.message || '(missing message)'}
+
+
+
+ );
+ }
+
+ /**
+ * Callback invoked upon clicking on an alert.
+ *
+ * @param event - event object
+ */
+ private _onClick = (event: any): void => {
+ if (this.props.onClick) {
+ this.props.onClick(event);
+ return;
+ }
+ this._onClose(event, 'click');
+ };
+
+ /**
+ * Callback invoked upon closing an alert.
+ *
+ * @param event - event object
+ * @param reason - reason why the callback was invoked
+ */
+ private _onClose = (event: any, reason: string): void => {
+ if (reason === 'clickaway') {
+ return;
+ }
+ this.props.onClose(event);
+ };
+}
diff --git a/src/components/BranchMenu.tsx b/src/components/BranchMenu.tsx
index 0940b3b0f..7262d66fe 100644
--- a/src/components/BranchMenu.tsx
+++ b/src/components/BranchMenu.tsx
@@ -16,12 +16,61 @@ import {
newBranchButtonClass,
wrapperClass
} from '../style/BranchMenu';
-import { Git, IGitExtension } from '../tokens';
+import { Git, IGitExtension, ILogMessage } from '../tokens';
+import { sleep } from '../utils';
+import { Alert } from './Alert';
import { NewBranchDialog } from './NewBranchDialog';
+import { SuspendModal } from './SuspendModal';
const CHANGES_ERR_MSG =
'The current branch contains files with uncommitted changes. Please commit or discard these changes before switching to or creating another branch.';
+/**
+ * Callback invoked upon encountering an error when switching branches.
+ *
+ * @private
+ * @param err - error
+ */
+function onBranchError(err: any): void {
+ if (err.message.includes('following files would be overwritten')) {
+ showDialog({
+ title: 'Unable to switch branch',
+ body: (
+
+
+ Your changes to the following files would be overwritten by
+ switching:
+